aif-ng/aif/disk/block_fallback.py

222 lines
10 KiB
Python

# To reproduce sgdisk behaviour in v1 of AIF-NG:
# https://gist.github.com/herry13/5931cac426da99820de843477e41e89e
# https://github.com/dcantrell/pyparted/blob/master/examples/query_device_capacity.py
# TODO: Remember to replicate genfstab behaviour.
import logging
import os
try:
# https://stackoverflow.com/a/34812552/733214
# https://github.com/karelzak/util-linux/blob/master/libmount/python/test_mount_context.py#L6
import libmount as mount
except ImportError:
# We should never get here. util-linux is part of core (base) in Arch and uses "libmount".
import pylibmount as mount
##
import blkinfo
import parted # https://www.gnu.org/software/parted/api/index.html
from lxml import etree
##
import aif.constants
import aif.utils
# TODO: https://serverfault.com/questions/356534/ssd-erase-block-size-lvm-pv-on-raw-device-alignment
# TODO: caveats? https://gist.github.com/leodutra/8779d468e9062058a3e90008295d3ca6
# https://unix.stackexchange.com/questions/325886/bios-gpt-do-we-need-a-boot-flag
_logger = logging.getLogger(__name__)
class Disk(object):
def __init__(self, disk_xml):
self.xml = disk_xml
_logger.debug('disk_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8')))
self.id = self.xml.attrib['id']
self.devpath = os.path.realpath(self.xml.attrib['device'])
self.is_lowformatted = None
self.is_hiformatted = None
self.is_partitioned = None
self.partitions = None
self._initDisk()
def _initDisk(self):
if self.devpath == 'auto':
self.devpath = '/dev/{0}'.format(blkinfo.BlkDiskInfo().get_disks()[0]['kname'])
if not os.path.isfile(self.devpath):
_logger.error('Disk {0} does not exist; please specify an explicit device path'.format(self.devpath))
raise ValueError('Disk not found')
self.table_type = self.xml.attrib.get('diskFormat', 'gpt').lower()
if self.table_type in ('bios', 'mbr', 'dos'):
self.table_type = 'msdos'
validlabels = parted.getLabels()
if self.table_type not in validlabels:
_logger.error(('Disk format ({0}) is not valid for this architecture;'
'must be one of: {1}'.format(self.table_type, ', '.join(list(validlabels)))))
raise ValueError('Invalid disk format')
self.device = parted.getDevice(self.devpath)
self.disk = parted.freshDisk(self.device, self.table_type)
_logger.debug('Configured parted device for {0}.'.format(self.devpath))
self.is_lowformatted = False
self.is_hiformatted = False
self.is_partitioned = False
self.partitions = []
return(None)
def diskFormat(self):
if self.is_lowformatted:
return(None)
# This is a safeguard. We do *not* want to low-format a disk that is mounted.
aif.utils.checkMounted(self.devpath)
self.disk.deleteAllPartitions()
self.disk.commit()
self.is_lowformatted = True
self.is_partitioned = False
return(None)
def getPartitions(self):
# For GPT, this *technically* should be 34 -- or, more precisely, 2048 (see FAQ in manual), but the alignment
# optimizer fixes it for us automatically.
# But for DOS tables, it's required.
_logger.info('Establishing partitions for {0}'.format(self.devpath))
if self.table_type == 'msdos':
start_sector = 2048
else:
start_sector = 0
self.partitions = []
xml_partitions = self.xml.findall('part')
for idx, part in enumerate(xml_partitions):
partnum = idx + 1
if self.table_type == 'gpt':
p = Partition(part, self.disk, start_sector, partnum, self.table_type)
else:
parttype = 'primary'
if len(xml_partitions) > 4:
if partnum == 4:
parttype = 'extended'
elif partnum > 4:
parttype = 'logical'
p = Partition(part, self.disk, start_sector, partnum, self.table_type, part_type = parttype)
start_sector = p.end + 1
self.partitions.append(p)
_logger.debug('Added partition {0}'.format(p.id))
return(None)
def partFormat(self):
if self.is_partitioned:
return(None)
if not self.is_lowformatted:
self.diskFormat()
# This is a safeguard. We do *not* want to partition a disk that is mounted.
aif.utils.checkMounted(self.devpath)
if not self.partitions:
self.getPartitions()
if not self.partitions:
return(None)
for p in self.partitions:
self.disk.addPartition(partition = p, constraint = self.device.optimalAlignedConstraint)
self.disk.commit()
p.devpath = p.partition.path
p.is_hiformatted = True
self.is_partitioned = True
return(None)
class Partition(object):
def __init__(self, part_xml, diskobj, start_sector, partnum, tbltype, part_type = None):
if tbltype not in ('gpt', 'msdos'):
_logger.error('Invalid tabletype specified: {0}. Must be one of: gpt,msdos.'.format(tbltype))
raise ValueError('Invalid tbltype.')
if tbltype == 'msdos' and part_type not in ('primary', 'extended', 'logical'):
_logger.error(('Table type msdos requires the part_type to be specified and must be one of: primary,'
'extended,logical (instead of: {0}).').format(part_type))
raise ValueError('The part_type must be specified for msdos tables')
self.xml = part_xml
_logger.debug('part_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8')))
_logger.debug('Partition number: {0}'.format(partnum))
_logger.debug('Partition table type: {0}.'.format(tbltype))
self.id = self.xml.attrib['id']
self.flags = set()
for f in self.xml.findall('partitionFlag'):
if f.text in aif.constants.PARTED_FLAGS:
self.flags.add(f.text)
self.flags = sorted(list(self.flags))
self.partnum = partnum
if tbltype == 'msdos':
if partnum > 4:
self.part_type = parted.PARTITION_LOGICAL
else:
if part_type == 'extended':
self.part_type = parted.PARTITION_EXTENDED
elif part_type == 'logical':
self.part_type = parted.PARTITION_LOGICAL
else:
self.part_type = parted.PARTITION_NORMAL
self.fs_type = self.xml.attrib['fsType'].lower()
if self.fs_type not in aif.constants.PARTED_FSTYPES:
_logger.error(('{0} is not a valid partition filesystem type; must be one of: '
'{1}').format(self.xml.attrib['fsType'],
', '.join(sorted(aif.constants.PARTED_FSTYPES))))
raise ValueError('Invalid partition filesystem type')
self.disk = diskobj
self.device = self.disk.device
self.devpath = '{0}{1}'.format(self.device.path, self.partnum)
_logger.debug('Assigned to disk: {0} ({1}) at path {2}'.format(self.disk.id, self.device, self.devpath))
self.is_hiformatted = False
sizes = {}
for s in ('start', 'stop'):
x = dict(zip(('from_bgn', 'size', 'type'),
aif.utils.convertSizeUnit(self.xml.attrib[s])))
sectors = x['size']
if x['type'] == '%':
sectors = int(self.device.getLength() * (0.01 * x['size']))
else:
sectors = int(aif.utils.size.convertStorage(x['size'],
x['type'],
target = 'B') / self.device.sectorSize)
sizes[s] = (sectors, x['from_bgn'])
if sizes['start'][1] is not None:
if sizes['start'][1]:
self.begin = sizes['start'][0] + 0
else:
self.begin = self.device.getLength() - sizes['start'][0]
else:
self.begin = sizes['start'][0] + start_sector
if sizes['stop'][1] is not None:
if sizes['stop'][1]:
self.end = sizes['stop'][0] + 0
else:
# This *technically* should be - 34, at least for gpt, but the alignment optimizer fixes it for us.
self.end = (self.device.getLength() - 1) - sizes['stop'][0]
else:
self.end = self.begin + sizes['stop'][0]
_logger.debug('Size: sector {0} to {1}.'.format(self.begin, self.end))
# TECHNICALLY we could craft the Geometry object with "length = ...", but it doesn't let us be explicit
# in configs. So we manually crunch the numbers and do it all at the end.
self.geometry = parted.Geometry(device = self.device,
start = self.begin,
end = self.end)
self.filesystem = parted.FileSystem(type = self.fs_type,
geometry = self.geometry)
self.partition = parted.Partition(disk = diskobj,
type = self.part_type,
geometry = self.geometry,
fs = self.filesystem)
for f in self.flags[:]:
flag_id = aif.constants.PARTED_FLAG_IDX[f]
if self.partition.isFlagAvailable(flag_id):
self.partition.setFlag(flag_id)
else:
self.flags.remove(f)
if tbltype == 'gpt' and self.xml.attrib.get('name'):
# The name attribute setting is b0rk3n, so we operate on the underlying PedPartition object.
# https://github.com/dcantrell/pyparted/issues/49#issuecomment-540096687
# https://github.com/dcantrell/pyparted/issues/65
# self.partition.name = self.xml.attrib.get('name')
_pedpart = self.partition.getPedPartition()
_pedpart.set_name(self.xml.attrib['name'])
_logger.debug('Partition name: {0}'.format(self.xml.attrib['name']))
#
# def detect(self):
# pass # TODO; blkinfo?