Skip to content

Commit

Permalink
New: support of loop disks added, tests and documentation updated.
Browse files Browse the repository at this point in the history
  • Loading branch information
petersulyok committed Jan 6, 2024
1 parent 7441df9 commit 39bc35b
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 146 deletions.
123 changes: 78 additions & 45 deletions src/diskinfo/disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@


class Disk:
"""The class can be initialized with specifying one of the five unique identifiers of the disk:
"""The Disk class contains all disk related information. The class can be initialized with specifying one of the
five unique identifiers of the disk:
* a disk name (e.g. `sda` or `nvme0n1`) located in `/dev/` directory.
* a disk serial number (e.g. `"92837A469FF876"`)
Expand All @@ -30,7 +31,7 @@ class Disk:
they use the disk name for comparision.
.. note::
During the class initialization the disk will not be accessed.
During the class initialization the disk will not be physically accessed.
Args:
disk_name (str): the disk name
Expand All @@ -47,7 +48,7 @@ class Disk:
This exampe shows how to create a :class:`~diskinfo.Disk` class then how to get its path and serial number::
>>> from diskinfo import Disk
>>> d = Disk("sda")
>>> d=Disk("sda")
>>> d.get_path()
'/dev/sda'
>>> d.get_serial_number()
Expand Down Expand Up @@ -86,16 +87,17 @@ class Disk:
__logical_block_size: int # Disk logical block size
__part_table_type: str # Disk partition table type
__part_table_uuid: str # Disk partition table UUID
__hwmon_path: str # Path for the HWMON temperature file
__hwmon_path: str # Path for the /sys/HWMON temperature file

def __init__(self, disk_name: str = None, serial_number: str = None, wwn: str = None,
byid_name: str = None, bypath_name: str = None,) -> None:
"""See class definition docstring above."""

# Initialize with a disk name.
# Initialization 1: disk name.
if disk_name:
self.__name = disk_name
# Initialize with a disk serial number.

# Initialization 2: disk serial number.
elif serial_number:
name = ""
for file in os.listdir("/sys/block/"):
Expand All @@ -108,7 +110,8 @@ def __init__(self, disk_name: str = None, serial_number: str = None, wwn: str =
if name == "":
raise ValueError(f"Invalid serial number ({serial_number})!")
self.__name = name
# Initialize with a disk WWN name.

# Initialization 3: disk WWN name.
elif wwn:
name = ""
for file in os.listdir("/sys/block/"):
Expand All @@ -121,13 +124,16 @@ def __init__(self, disk_name: str = None, serial_number: str = None, wwn: str =
if name == "":
raise ValueError(f"Invalid wwn identifier ({wwn})!")
self.__name = name
# Initialize with a disk `by-id` name.

# Initialization 4: disk `by-id` name.
elif byid_name:
self.__name = os.path.basename(os.readlink("/dev/disk/by-id/" + byid_name))
# Initialize with a disk `by-path` name.

# Initialization 5: disk `by-path` name.
elif bypath_name:
self.__name = os.path.basename(os.readlink("/dev/disk/by-path/" + bypath_name))
# If none of them was specified.

# Initialization error (none of them was specified).
else:
raise ValueError("Missing disk identifier, Disk() class cannot be initialized.")

Expand All @@ -139,25 +145,33 @@ def __init__(self, disk_name: str = None, serial_number: str = None, wwn: str =
if not os.path.exists(path):
raise ValueError(f"Disk path ({self.__path}) does not exist!")

# Determine disk type (HDD, SSD, NVME)
path = "/sys/block/" + self.__name + "/queue/rotational"
result = _read_file(path)
if result == "1":
self.__type = DiskType.HDD
elif result == "0":
self.__type = DiskType.SSD
else:
raise RuntimeError(f"Disk type cannot be determined based on this value ({path}={result}).")
if "nvme" in self.__name:
self.__type = DiskType.NVME

# Read attributes from /sys filesystem.
# Read disk attributes from /sys filesystem.
self.__size = int(_read_file("/sys/block/" + self.__name + "/size"))
self.__model = _read_file("/sys/block/" + self.__name + "/device/model")
self.__device_id = _read_file("/sys/block/" + self.__name + "/dev")
self.__physical_block_size = int(_read_file("/sys/block/" + self.__name + "/queue/physical_block_size"))
self.__logical_block_size = int(_read_file("/sys/block/" + self.__name + "/queue/logical_block_size"))

# Determination of the disk type (HDD, SSD, NVME or LOOP)
# Type: LOOP
if re.match(r'^7:', self.__device_id):
self.__type = DiskType.LOOP

# Type: NVME
elif "nvme" in self.__name:
self.__type = DiskType.NVME

# Type: SSD or HDD
else:
path = "/sys/block/" + self.__name + "/queue/rotational"
result = _read_file(path)
if result == "1":
self.__type = DiskType.HDD
elif result == "0":
self.__type = DiskType.SSD
else:
raise RuntimeError(f"Disk type cannot be determined based on this value ({path}={result}).")

# Read attributes from udev data.
dev_path = "/run/udev/data/b" + self.__device_id
self.__serial_number = _read_udev_property(dev_path, "ID_SERIAL_SHORT=")
Expand Down Expand Up @@ -333,6 +347,7 @@ def get_type(self) -> int:
- ``DiskType.HDD`` for hard disks (with spinning platters)
- ``DiskType.SSD`` for SDDs on SATA or USB interface
- ``DiskType.NVME`` for NVME disks
- ``DiskType.LOOP`` for LOOP disks
Example:
An example about the use of this function::
Expand Down Expand Up @@ -387,12 +402,30 @@ def is_hdd(self) -> bool:
"""
return bool(self.__type == DiskType.HDD)

def is_loop(self) -> bool:
"""Returns `True` if the disk type is LOOP, otherwise `False`.
Example:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d=Disk("loop0")
>>> d.is_loop()
True
"""
return bool(self.__type == DiskType.LOOP)

def get_type_str(self) -> str:
"""Returns the name of the disk type. See the return values in :class:`~diskinfo.DiskType` class:
- ``DiskType.HDD_STR`` for hard disks (with spinning platters)
- ``DiskType.SSD_STR`` for SDDs on SATA or USB interface
- ``DiskType.NVME_STR`` for NVME disks
- ``DiskType.LOOP_STR`` for LOOP disks
Raises:
RuntimeError: in case of unknown disk type.
Example:
An example about the use of this function::
Expand All @@ -407,7 +440,11 @@ def get_type_str(self) -> str:
return DiskType.NVME_STR
if self.is_ssd():
return DiskType.SSD_STR
return DiskType.HDD_STR
if self.is_hdd():
return DiskType.HDD_STR
if self.is_loop():
return DiskType.LOOP_STR
raise RuntimeError(f'Unknown disk type (type={self.__type})')

def get_size(self) -> int:
"""Returns the size of the disk in 512-byte units.
Expand All @@ -417,7 +454,7 @@ def get_size(self) -> int:
>>> from diskinfo import Disk
>>> d=Disk("sdc")
>>> s = d.get_size()
>>> s=d.get_size()
>>> print(f"Disk size: { s * 512 } bytes.")
Disk size: 1024209543168 bytes.
Expand All @@ -443,8 +480,8 @@ def get_size_in_hrf(self, units: int = 0) -> Tuple[float, str]:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> s, u = d.get_size_in_hrf()
>>> d=Disk("sdc")
>>> s,u=d.get_size_in_hrf()
>>> print(f"{s:.1f} {u}")
1.0 TB
Expand All @@ -458,7 +495,7 @@ def get_device_id(self) -> str:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_device_id()
'8:32'
Expand All @@ -473,7 +510,7 @@ def get_physical_block_size(self) -> int:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_physical_block_size()
512
Expand All @@ -487,7 +524,7 @@ def get_logical_block_size(self) -> int:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_logical_block_size()
512
Expand All @@ -501,7 +538,7 @@ def get_partition_table_type(self) -> str:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_partition_table_type()
'gpt'
Expand All @@ -515,7 +552,7 @@ def get_partition_table_uuid(self) -> str:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_partition_table_uuid()
'd3f932e0-7107-455e-a569-9acd5b60d204'
Expand All @@ -525,9 +562,8 @@ def get_partition_table_uuid(self) -> str:
def get_temperature(self) -> float:
"""Returns the current disk temperature. Important notes about using this function:
- SATA SSDs and HDDs require ``drivetemp`` kernel module to be loaded (available from Linux kernel version ``5.6+``). Without this the HWMON system will not provide the temperature information.
- NVME disks do not require any Linux kernel module
- SATA SSDs and HDDs require ``drivetemp`` kernel module to be loaded (available from Linux kernel version
``5.6+``). Without this the HWMON system will not provide the temperature information.
.. note::
Expand All @@ -543,14 +579,14 @@ def get_temperature(self) -> float:
An example about the use of this function::
>>> from diskinfo import Disk
>>> d = Disk("sdc")
>>> d=Disk("sdc")
>>> d.get_temperature()
28.5
"""
temp: int
temp: float

temp = -1.0
temp = 0.0
if hasattr(self, '_Disk__hwmon_path'):
if not self.__hwmon_path or not os.path.exists(self.__hwmon_path):
raise RuntimeError(f"ERROR: File does not exists (hwmon={self.__hwmon_path})")
Expand Down Expand Up @@ -592,7 +628,7 @@ def get_smart_data(self, nocheck: bool = False, sudo: str = None, smartctl_path:
The example show the use of the function::
>>> from diskinfo import Disk, DiskSmartData
>>> d = Disk("sda")
>>> d=Disk("sda")
>>> sd = d.get_smart_data()
In case of SSDs and HDDs the traditional SMART attributes can be accessed via
Expand Down Expand Up @@ -820,12 +856,9 @@ def get_partition_list(self) -> List[Partition]:
if self.is_nvme():
path += "p"
path += str(index)
# If the partition path exists.
if os.path.exists(path):
result.append(Partition(os.path.basename(path), _read_file(path + "/dev")))
# File does not exist, quit from loop.
else:
break
if not os.path.exists(path):
break # If partition path dos not exists.
result.append(Partition(os.path.basename(path), _read_file(path + "/dev")))
index += 1
return result

Expand Down Expand Up @@ -855,7 +888,7 @@ def __repr__(self):
f"size={self.__size}, "
f"device_id={self.__device_id}, "
f"physical_block_size={self.__physical_block_size}, "
f"logical_block_size={self.__logical_block_size}"
f"logical_block_size={self.__logical_block_size}, "
f"partition_table_type={self.__part_table_type}, "
f"partition_table_uuid={self.__part_table_uuid})")

Expand Down
34 changes: 18 additions & 16 deletions src/diskinfo/diskinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,25 @@


class DiskInfo:
"""At class initialization time all existing disks will be explored in the runtime system. After that,
:meth:`~diskinfo.DiskInfo.get_disk_number()` method will provide the number of identified disk and
:meth:`~diskinfo.DiskInfo.get_disk_list()` method will return the list of the identified disks.
In both cases disk type filters can be applied to get only the subset of the discovered disks. The filters are
"""This class implements disk exploration functionality. At class initialization time all existing disks
will be explored automatically. In a next step, :meth:`~diskinfo.DiskInfo.get_disk_number()` method will
provide the number of identified disk and :meth:`~diskinfo.DiskInfo.get_disk_list()` method will return
the list of the identified disks.
In both cases disk type filters can be applied to get only a subset of the discovered disks. The filters are
set of :class:`~diskinfo.DiskType` values.
Operator ``in`` is also implemented for this class. Caller can check if a :class:`~diskinfo.Disk` class instance
can be found on the list of the dicovered disks.
Example:
A code example about the basic use of the class and the ``in`` operator.
A code example about the basic use of the class and the use of the ``in`` operator.
>>> from diskinfo import Disk, DiskType, DiskInfo
>>> di = DiskInfo()
>>> n = di.get_disk_number(included={DiskType.SSD}, excluded={DiskType.NVME})
>>> di=DiskInfo()
>>> n=di.get_disk_number(included={DiskType.SSD}, excluded={DiskType.HDD})
>>> print(f"Number of SSDs: {n}")
Number of SSDs: 3
>>> d = Disk("sda")
>>> d=Disk("sda")
>>> print(d in di)
True
"""
Expand Down Expand Up @@ -62,16 +63,16 @@ def get_disk_number(self, included: set = None, excluded: set = None) -> int:
A code example about using filters: it counts the number of SSDs excluding NVME disks.
>>> from diskinfo import DiskType, DiskInfo
>>> di = DiskInfo()
>>> n = di.get_disk_number(included={DiskType.SSD}, excluded={DiskType.NVME})
>>> di=DiskInfo()
>>> n=di.get_disk_number(included={DiskType.SSD}, excluded={DiskType.HDD})
>>> print(f"Number of SSDs: {n}")
Number of SSDs: 3
"""
disk_number: int # Number of disk counted

# Set default filters if not specified.
# Set the default filter if not specified.
if not included:
included = {DiskType.HDD, DiskType.SSD, DiskType.NVME}
included = {DiskType.HDD, DiskType.SSD, DiskType.NVME, DiskType.LOOP}
if not excluded:
excluded = set()

Expand Down Expand Up @@ -112,8 +113,8 @@ def get_disk_list(self, included: set = None, excluded: set = None, sorting: boo
of the HDDs:
>>> from diskinfo import DiskType, DiskInfo
>>> di = DiskInfo()
>>> disks = di.get_disk_list(included={DiskType.HDD}, sorting=True)
>>> di=DiskInfo()
>>> disks=di.get_disk_list(included={DiskType.HDD}, sorting=True)
>>> for d in disks:
... print(d.get_path())
...
Expand All @@ -125,7 +126,7 @@ def get_disk_list(self, included: set = None, excluded: set = None, sorting: boo

# Set default filters if not specified.
if not included:
included = {DiskType.HDD, DiskType.SSD, DiskType.NVME}
included = {DiskType.HDD, DiskType.SSD, DiskType.NVME, DiskType.LOOP}
if not excluded:
excluded = set()

Expand Down Expand Up @@ -156,6 +157,7 @@ def __contains__(self, item):

def __repr__(self):
"""String representation of the DiskInfo class."""
return f"DiskInfo(number_of_disks={len(self.__disk_list)}, list_of_disks={self.__disk_list})"
return f"DiskInfo(number_of_disks={len(self.__disk_list)}, " \
f"list_of_disks={self.__disk_list})"

# End
Loading

0 comments on commit 39bc35b

Please sign in to comment.