Fix 1669 | Refactor display of sizes in tables (#2100)

* Use sector as default display

* Display tables in sector size

* Refactor size

* Update

* Update

* fix flake8

---------

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
This commit is contained in:
Daniel Girtler 2023-09-24 19:47:38 +10:00 committed by GitHub
parent 9e3e4a5df5
commit b141609990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 122 deletions

View File

@ -14,6 +14,7 @@ from .device_model import (
PartitionTable,
Unit,
Size,
SectorSize,
SubvolumeModification,
DeviceGeometry,
PartitionType,

View File

@ -93,7 +93,7 @@ class DiskLayoutConfiguration:
status=ModificationStatus(partition['status']),
fs_type=FilesystemType(partition['fs_type']),
start=Size.parse_args(partition['start']),
length=Size.parse_args(partition['length']),
length=Size.parse_args(partition['size']),
mount_options=partition['mount_options'],
mountpoint=Path(partition['mountpoint']) if partition['mountpoint'] else None,
dev_path=Path(partition['dev_path']) if partition['dev_path'] else None,
@ -138,80 +138,89 @@ class Unit(Enum):
sectors = 'sectors' # size in sector
Percent = '%' # size in percentile
@staticmethod
def get_all_units() -> List[str]:
return [u.name for u in Unit]
@staticmethod
def get_si_units() -> List[Unit]:
return [u for u in Unit if 'i' not in u.name and u.name != 'sectors']
@dataclass
class SectorSize:
value: int
unit: Unit
def __post_init__(self):
match self.unit:
case Unit.sectors:
raise ValueError('Unit type sector not allowed for SectorSize')
@staticmethod
def default() -> SectorSize:
return SectorSize(512, Unit.B)
def json(self) -> Dict[str, Any]:
return {
'value': self.value,
'unit': self.unit.name,
}
@classmethod
def parse_args(cls, arg: Dict[str, Any]) -> SectorSize:
return SectorSize(
arg['value'],
Unit[arg['unit']]
)
def normalize(self) -> int:
"""
will normalize the value of the unit to Byte
"""
return int(self.value * self.unit.value) # type: ignore
@dataclass
class Size:
value: int
unit: Unit
sector_size: Optional[Size] = None # only required when unit is sector
total_size: Optional[Size] = None # required when operating on percentages
sector_size: SectorSize
def __post_init__(self):
if self.unit == Unit.sectors and self.sector_size is None:
raise ValueError('Sector size is required when unit is sectors')
elif self.unit == Unit.Percent:
if self.value < 0 or self.value > 100:
raise ValueError('Percentage must be between 0 and 100')
elif self.total_size is None:
raise ValueError('Total size is required when unit is percentage')
@property
def _total_size(self) -> Size:
"""
Save method to get the total size, mainly to satisfy mypy
This shouldn't happen as the Size object fails instantiation on missing total size
"""
if self.unit == Unit.Percent and self.total_size is None:
raise ValueError('Percent unit size must specify a total size')
return self.total_size # type: ignore
if not isinstance(self.sector_size, SectorSize):
raise ValueError('sector size must be of type SectorSize')
def json(self) -> Dict[str, Any]:
return {
'value': self.value,
'unit': self.unit.name,
'sector_size': self.sector_size.json() if self.sector_size else None,
'total_size': self._total_size.json() if self._total_size else None
'sector_size': self.sector_size.json() if self.sector_size else None
}
@classmethod
def parse_args(cls, size_arg: Dict[str, Any]) -> Size:
sector_size = size_arg['sector_size']
total_size = size_arg['total_size']
return Size(
size_arg['value'],
Unit[size_arg['unit']],
Size.parse_args(sector_size) if sector_size else None,
Size.parse_args(total_size) if total_size else None
SectorSize.parse_args(sector_size),
)
def convert(
self,
target_unit: Unit,
sector_size: Optional[Size] = None,
total_size: Optional[Size] = None
sector_size: Optional[SectorSize] = None
) -> Size:
if target_unit == Unit.sectors and sector_size is None:
raise ValueError('If target has unit sector, a sector size must be provided')
# not sure why we would ever wanna convert to percentages
if target_unit == Unit.Percent and total_size is None:
raise ValueError('Missing parameter total size to be able to convert to percentage')
if self.unit == target_unit:
return self
elif self.unit == Unit.Percent:
amount = int(self._total_size._normalize() * (self.value / 100))
return Size(amount, Unit.B)
elif self.unit == Unit.sectors:
norm = self._normalize()
return Size(norm, Unit.B).convert(target_unit, sector_size)
return Size(norm, Unit.B, self.sector_size).convert(target_unit, sector_size)
else:
if target_unit == Unit.sectors and sector_size is not None:
norm = self._normalize()
@ -219,7 +228,7 @@ class Size:
return Size(sectors, Unit.sectors, sector_size)
else:
value = int(self._normalize() / target_unit.value) # type: ignore
return Size(value, target_unit)
return Size(value, target_unit, self.sector_size)
def as_text(self) -> str:
return self.format_size(
@ -230,31 +239,45 @@ class Size:
def format_size(
self,
target_unit: Unit,
sector_size: Optional[Size] = None,
sector_size: Optional[SectorSize] = None,
include_unit: bool = True
) -> str:
if self.unit == Unit.Percent:
return f'{self.value}%'
else:
target_size = self.convert(target_unit, sector_size)
if include_unit:
return f'{target_size.value} {target_unit.name}'
return f'{target_size.value}'
target_size = self.convert(target_unit, sector_size)
if include_unit:
return f'{target_size.value} {target_unit.name}'
return f'{target_size.value}'
def format_highest(self, include_unit: bool = True) -> str:
si_units = Unit.get_si_units()
all_si_values = [self.convert(si) for si in si_units]
filtered = filter(lambda x: x.value >= 1, all_si_values)
# we have to get the max by the unit value as we're interested
# in getting the value in the highest possible unit without floats
si_value = max(filtered, key=lambda x: x.unit.value)
if include_unit:
return f'{si_value.value} {si_value.unit.name}'
return f'{si_value.value}'
def _normalize(self) -> int:
"""
will normalize the value of the unit to Byte
"""
if self.unit == Unit.Percent:
return self.convert(Unit.B).value
elif self.unit == Unit.sectors and self.sector_size is not None:
return self.value * self.sector_size._normalize()
if self.unit == Unit.sectors and self.sector_size is not None:
return self.value * self.sector_size.normalize()
return int(self.value * self.unit.value) # type: ignore
def __sub__(self, other: Size) -> Size:
src_norm = self._normalize()
dest_norm = other._normalize()
return Size(abs(src_norm - dest_norm), Unit.B)
return Size(abs(src_norm - dest_norm), Unit.B, self.sector_size)
def __add__(self, other: Size) -> Size:
src_norm = self._normalize()
dest_norm = other._normalize()
return Size(abs(src_norm + dest_norm), Unit.B, self.sector_size)
def __lt__(self, other):
return self._normalize() < other._normalize()
@ -296,14 +319,22 @@ class _PartitionInfo:
mountpoints: List[Path]
btrfs_subvol_infos: List[_BtrfsSubvolumeInfo] = field(default_factory=list)
@property
def sector_size(self) -> SectorSize:
sector_size = self.partition.geometry.device.sectorSize
return SectorSize(sector_size, Unit.B)
def table_data(self) -> Dict[str, Any]:
end = self.start + self.length
part_info = {
'Name': self.name,
'Type': self.type.value,
'Filesystem': self.fs_type.value if self.fs_type else str(_('Unknown')),
'Path': str(self.path),
'Start': self.start.format_size(Unit.MiB),
'Length': self.length.format_size(Unit.MiB),
'Start': self.start.format_size(Unit.sectors, self.sector_size, include_unit=False),
'End': end.format_size(Unit.sectors, self.sector_size, include_unit=False),
'Size': self.length.format_highest(),
'Flags': ', '.join([f.name for f in self.flags])
}
@ -327,10 +358,14 @@ class _PartitionInfo:
start = Size(
partition.geometry.start,
Unit.sectors,
Size(partition.disk.device.sectorSize, Unit.B)
SectorSize(partition.disk.device.sectorSize, Unit.B)
)
length = Size(int(partition.getLength(unit='B')), Unit.B)
length = Size(
int(partition.getLength(unit='B')),
Unit.B,
SectorSize(partition.disk.device.sectorSize, Unit.B)
)
return _PartitionInfo(
partition=partition,
@ -355,7 +390,7 @@ class _DeviceInfo:
type: str
total_size: Size
free_space_regions: List[DeviceGeometry]
sector_size: Size
sector_size: SectorSize
read_only: bool
dirty: bool
@ -365,7 +400,7 @@ class _DeviceInfo:
'Model': self.model,
'Path': str(self.path),
'Type': self.type,
'Size': self.total_size.format_size(Unit.MiB),
'Size': self.total_size.format_highest(),
'Free space': int(total_free_space),
'Sector size': self.sector_size.value,
'Read only': self.read_only
@ -379,15 +414,17 @@ class _DeviceInfo:
else:
device_type = parted.devices[device.type]
sector_size = Size(device.sectorSize, Unit.B)
sector_size = SectorSize(device.sectorSize, Unit.B)
free_space = [DeviceGeometry(g, sector_size) for g in disk.getFreeSpaceRegions()]
sector_size = SectorSize(device.sectorSize, Unit.B)
return _DeviceInfo(
model=device.model.strip(),
path=Path(device.path),
type=device_type,
sector_size=sector_size,
total_size=Size(int(device.getLength(unit='B')), Unit.B),
total_size=Size(int(device.getLength(unit='B')), Unit.B, sector_size),
free_space_regions=free_space,
read_only=device.readOnly,
dirty=device.dirty
@ -470,7 +507,7 @@ class SubvolumeModification:
class DeviceGeometry:
def __init__(self, geometry: Geometry, sector_size: Size):
def __init__(self, geometry: Geometry, sector_size: SectorSize):
self._geometry = geometry
self._sector_size = sector_size
@ -498,7 +535,7 @@ class DeviceGeometry:
'Sector size': self._sector_size.value,
'Start (sector/B)': start_str,
'End (sector/B)': end_str,
'Length (sectors/B)': length_str
'Size (sectors/B)': length_str
}
@ -751,7 +788,7 @@ class PartitionModification:
'status': self.status.value,
'type': self.type.value,
'start': self.start.json(),
'length': self.length.json(),
'size': self.length.json(),
'fs_type': self.fs_type.value if self.fs_type else '',
'mountpoint': str(self.mountpoint) if self.mountpoint else None,
'mount_options': self.mount_options,
@ -764,12 +801,15 @@ class PartitionModification:
"""
Called for displaying data in table format
"""
end = self.start + self.length
part_mod = {
'Status': self.status.value,
'Device': str(self.dev_path) if self.dev_path else '',
'Type': self.type.value,
'Start': self.start.format_size(Unit.MiB),
'Length': self.length.format_size(Unit.MiB),
'Start': self.start.format_size(Unit.sectors, self.start.sector_size, include_unit=False),
'End': end.format_size(Unit.sectors, self.start.sector_size, include_unit=False),
'Size': self.length.format_highest(),
'FS type': self.fs_type.value if self.fs_type else 'Unknown',
'Mountpoint': self.mountpoint if self.mountpoint else '',
'Mount options': ', '.join(self.mount_options),
@ -938,7 +978,7 @@ class LsblkInfo:
name: str = ''
path: Path = Path()
pkname: str = ''
size: Size = field(default_factory=lambda: Size(0, Unit.B))
size: Size = field(default_factory=lambda: Size(0, Unit.B, SectorSize.default()))
log_sec: int = 0
pttype: str = ''
ptuuid: str = ''
@ -1017,7 +1057,8 @@ class LsblkInfo:
if isinstance(getattr(lsblk_info, data_field), Path):
val = Path(blockdevice[lsblk_field])
elif isinstance(getattr(lsblk_info, data_field), Size):
val = Size(blockdevice[lsblk_field], Unit.B)
sector_size = SectorSize(blockdevice['log-sec'], Unit.B)
val = Size(blockdevice[lsblk_field], Unit.B, sector_size)
else:
val = blockdevice[lsblk_field]

View File

@ -5,7 +5,7 @@ from pathlib import Path
from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple
from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \
ModificationStatus, DeviceGeometry
ModificationStatus, DeviceGeometry, SectorSize
from ..menu import Menu, ListManager, MenuSelection, TextInput
from ..output import FormattedOutput, warn
from .subvolume_menu import SubvolumeMenu
@ -194,42 +194,47 @@ class PartitioningList(ListManager):
def _validate_value(
self,
sector_size: Size,
sector_size: SectorSize,
total_size: Size,
value: str
text: str,
start: Optional[Size]
) -> Optional[Size]:
match = re.match(r'([0-9]+)([a-zA-Z|%]*)', value, re.I)
match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I)
if match:
value, unit = match.groups()
str_value, unit = match.groups()
if unit == '%':
unit = Unit.Percent.name
if unit == '%' and start:
available = total_size - start
value = int(available.value * (int(str_value) / 100))
unit = available.unit.name
else:
value = int(str_value)
if unit and unit not in Unit.get_all_units():
return None
unit = Unit[unit] if unit else Unit.sectors
return Size(int(value), unit, sector_size, total_size)
return Size(value, unit, sector_size)
return None
def _enter_size(
self,
sector_size: Size,
sector_size: SectorSize,
total_size: Size,
prompt: str,
default: Size
default: Size,
start: Optional[Size],
) -> Size:
while True:
value = TextInput(prompt).run().strip()
size: Optional[Size] = None
if not value:
size = default
else:
size = self._validate_value(sector_size, total_size, value)
size = self._validate_value(sector_size, total_size, value, start)
if size:
return size
@ -247,7 +252,7 @@ class PartitioningList(ListManager):
total_bytes = device_info.total_size.format_size(Unit.B)
prompt += str(_('Total: {} / {}')).format(total_sectors, total_bytes) + '\n\n'
prompt += str(_('All entered values can be suffixed with a unit: B, KB, KiB, MB, MiB...')) + '\n'
prompt += str(_('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...')) + '\n'
prompt += str(_('If no unit is provided, the value is interpreted as sectors')) + '\n'
print(prompt)
@ -260,13 +265,14 @@ class PartitioningList(ListManager):
device_info.sector_size,
device_info.total_size,
start_prompt,
default_start
default_start,
None
)
if start_size.value == largest_free_area.start:
end_size = Size(largest_free_area.end, Unit.sectors, device_info.sector_size)
else:
end_size = Size(100, Unit.Percent, total_size=device_info.total_size)
end_size = device_info.total_size
# prompt until valid end sector was entered
end_prompt = str(_('Enter end (default: {}): ')).format(end_size.as_text())
@ -274,7 +280,8 @@ class PartitioningList(ListManager):
device_info.sector_size,
device_info.total_size,
end_prompt,
end_size
end_size,
start_size
)
return start_size, end_size

View File

@ -163,7 +163,7 @@ class Installer:
lsblk_info = disk.get_lsblk_by_mountpoint(boot_mount)
if len(lsblk_info) > 0:
if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB):
if lsblk_info[0].size < disk.Size(200, disk.Unit.MiB, disk.SectorSize.default()):
raise DiskError(
f'The boot partition mounted at {boot_mount} is not large enough to install a boot loader. '
f'Please resize it to at least 200MiB and re-run the installation.'

View File

@ -170,13 +170,13 @@ def select_disk_config(
return None
def _boot_partition() -> disk.PartitionModification:
def _boot_partition(sector_size: disk.SectorSize) -> disk.PartitionModification:
if SysInfo.has_uefi():
start = disk.Size(1, disk.Unit.MiB)
size = disk.Size(512, disk.Unit.MiB)
start = disk.Size(1, disk.Unit.MiB, sector_size)
size = disk.Size(512, disk.Unit.MiB, sector_size)
else:
start = disk.Size(3, disk.Unit.MiB)
size = disk.Size(203, disk.Unit.MiB)
start = disk.Size(3, disk.Unit.MiB, sector_size)
size = disk.Size(203, disk.Unit.MiB, sector_size)
# boot partition
return disk.PartitionModification(
@ -215,8 +215,9 @@ def suggest_single_disk_layout(
if not filesystem_type:
filesystem_type = select_main_filesystem_format(advanced_options)
min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB)
root_partition_size = disk.Size(20, disk.Unit.GiB)
sector_size = device.device_info.sector_size
min_size_to_allow_home_part = disk.Size(40, disk.Unit.GiB, sector_size)
root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size)
using_subvolumes = False
using_home_partition = False
compression = False
@ -244,7 +245,7 @@ def suggest_single_disk_layout(
# Also re-align the start to 1MiB since we don't need the first sectors
# like we do in MBR layouts where the boot loader is installed traditionally.
boot_partition = _boot_partition()
boot_partition = _boot_partition(sector_size)
device_modification.add_partition(boot_partition)
if not using_subvolumes:
@ -259,11 +260,11 @@ def suggest_single_disk_layout(
using_home_partition = False
# root partition
start = disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB)
start = disk.Size(513, disk.Unit.MiB, sector_size) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB, sector_size)
# Set a size for / (/root)
if using_subvolumes or device_size_gib < min_size_to_allow_home_part or not using_home_partition:
length = disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size)
length = device.device_info.total_size - start
else:
length = min(device.device_info.total_size, root_partition_size)
@ -294,11 +295,14 @@ def suggest_single_disk_layout(
# If we don't want to use subvolumes,
# But we want to be able to re-use data between re-installs..
# A second partition for /home would be nice if we have the space for it
start = root_partition.length
length = device.device_info.total_size - root_partition.length
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=root_partition.length,
length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size),
start=start,
length=length,
mountpoint=Path('/home'),
fs_type=filesystem_type,
mount_options=['compress=zstd'] if compression else []
@ -319,9 +323,9 @@ def suggest_multi_disk_layout(
# Not really a rock solid foundation of information to stand on, but it's a start:
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
min_home_partition_size = disk.Size(40, disk.Unit.GiB)
min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default())
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
desired_root_partition_size = disk.Size(20, disk.Unit.GiB)
desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default())
compression = False
if not filesystem_type:
@ -362,28 +366,41 @@ def suggest_multi_disk_layout(
root_device_modification = disk.DeviceModification(root_device, wipe=True)
home_device_modification = disk.DeviceModification(home_device, wipe=True)
root_device_sector_size = root_device_modification.device.device_info.sector_size
home_device_sector_size = home_device_modification.device.device_info.sector_size
# add boot partition to the root device
boot_partition = _boot_partition()
boot_partition = _boot_partition(root_device_sector_size)
root_device_modification.add_partition(boot_partition)
if SysInfo.has_uefi():
root_start = disk.Size(513, disk.Unit.MiB, root_device_sector_size)
else:
root_start = disk.Size(206, disk.Unit.MiB, root_device_sector_size)
root_length = root_device.device_info.total_size - root_start
# add root partition to the root device
root_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(513, disk.Unit.MiB) if SysInfo.has_uefi() else disk.Size(206, disk.Unit.MiB),
length=disk.Size(100, disk.Unit.Percent, total_size=root_device.device_info.total_size),
start=root_start,
length=root_length,
mountpoint=Path('/'),
mount_options=['compress=zstd'] if compression else [],
fs_type=filesystem_type
)
root_device_modification.add_partition(root_partition)
start = disk.Size(1, disk.Unit.MiB, home_device_sector_size)
length = home_device.device_info.total_size - start
# add home partition to home device
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(1, disk.Unit.MiB),
length=disk.Size(100, disk.Unit.Percent, total_size=home_device.device_info.total_size),
start=start,
length=length,
mountpoint=Path('/home'),
mount_options=['compress=zstd'] if compression else [],
fs_type=filesystem_type,

View File

@ -17,9 +17,8 @@
"Boot"
],
"fs_type": "fat32",
"length": {
"size": {
"sector_size": null,
"total_size": null,
"unit": "MiB",
"value": 512
},
@ -28,7 +27,6 @@
"obj_id": "2c3fa2d5-2c79-4fab-86ec-22d0ea1543c0",
"start": {
"sector_size": null,
"total_size": null,
"unit": "MiB",
"value": 1
},
@ -39,9 +37,8 @@
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"length": {
"size": {
"sector_size": null,
"total_size": null,
"unit": "GiB",
"value": 20
},
@ -50,7 +47,6 @@
"obj_id": "3e7018a0-363b-4d05-ab83-8e82d13db208",
"start": {
"sector_size": null,
"total_size": null,
"unit": "MiB",
"value": 513
},
@ -61,14 +57,8 @@
"btrfs": [],
"flags": [],
"fs_type": "ext4",
"length": {
"size": {
"sector_size": null,
"total_size": {
"sector_size": null,
"total_size": null,
"unit": "B",
"value": 250148290560
},
"unit": "Percent",
"value": 100
},
@ -77,7 +67,6 @@
"obj_id": "ce58b139-f041-4a06-94da-1f8bad775d3f",
"start": {
"sector_size": null,
"total_size": null,
"unit": "GiB",
"value": 20
},

View File

@ -23,8 +23,8 @@ device_modification = disk.DeviceModification(device, wipe=True)
boot_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(1, disk.Unit.MiB),
length=disk.Size(512, disk.Unit.MiB),
start=disk.Size(1, disk.Unit.MiB, device.device_info.sector_size),
length=disk.Size(512, disk.Unit.MiB, device.device_info.sector_size),
mountpoint=Path('/boot'),
fs_type=disk.FilesystemType.Fat32,
flags=[disk.PartitionFlag.Boot]
@ -35,20 +35,23 @@ device_modification.add_partition(boot_partition)
root_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=disk.Size(513, disk.Unit.MiB),
length=disk.Size(20, disk.Unit.GiB),
start=disk.Size(513, disk.Unit.MiB, device.device_info.sector_size),
length=disk.Size(20, disk.Unit.GiB, device.device_info.sector_size),
mountpoint=None,
fs_type=fs_type,
mount_options=[],
)
device_modification.add_partition(root_partition)
start_home = root_partition.length
length_home = device.device_info.total_size - start_home
# create a new home partition
home_partition = disk.PartitionModification(
status=disk.ModificationStatus.Create,
type=disk.PartitionType.Primary,
start=root_partition.length,
length=disk.Size(100, disk.Unit.Percent, total_size=device.device_info.total_size),
start=start_home,
length=length_home,
mountpoint=Path('/home'),
fs_type=fs_type,
mount_options=[]