diff --git a/aif/disk/lvm.py b/aif/disk/lvm.py index deba1d9..a13c617 100644 --- a/aif/disk/lvm.py +++ b/aif/disk/lvm.py @@ -1,4 +1,7 @@ -import uuid +import logging +# import uuid +## +from lxml import etree ## from . import _common import aif.utils @@ -7,204 +10,25 @@ import aif.disk.luks as luks import aif.disk.mdadm as mdadm +_logger = logging.getLogger(__name__) + + _BlockDev = _common.BlockDev -class PV(object): - def __init__(self, pv_xml, partobj): - self.xml = pv_xml - self.id = self.xml.attrib('id') - self.source = self.xml.attrib('source') - self.device = partobj - if not isinstance(self.device, (block.Disk, - block.Partition, - luks.LUKS, - mdadm.Array)): - raise ValueError(('partobj must be of type ' - 'aif.disk.block.Disk, ' - 'aif.disk.block.Partition, ' - 'aif.disk.luks.LUKS, or' - 'aif.disk.mdadm.Array')) - _common.addBDPlugin('lvm') - self.devpath = self.device.devpath - self.is_pooled = False - self.meta = None - self._parseMeta() - - def _parseMeta(self): - # Note, the "UUID" for LVM is *not* a true UUID (RFC4122) so we don't convert it. - # https://unix.stackexchange.com/questions/173722/what-is-the-uuid-format-used-by-lvm - # TODO: parity with lvm_fallback.PV._parseMeta - # key names currently (probably) don't match and need to confirm the information's all present - meta = {} - try: - _meta = _BlockDev.lvm.pvinfo(self.devpath) - except _BlockDev.LVMError: - self.meta = None - self.is_pooled = False - return(None) - for k in dir(_meta): - if k.startswith('_'): - continue - elif k in ('copy',): - continue - v = getattr(_meta, k) - meta[k] = v - self.meta = meta - self.is_pooled = True - return(None) - - def prepare(self): - try: - if not self.meta: - self._parseMeta() - if self.meta: - vg = self.meta['vg_name'] - # LVM is SO. DUMB. - # If you're using LVM, seriously - just switch your model to mdadm. It lets you do things like - # remove disks live without restructuring the entire thing. - # That said, because the config references partitions/disks/arrays/etc. created *in the same config*, - # and it's all dependent on block devices defined in the thing, we can be reckless here. - # I'd like to take the time now to remind you to NOT RUN AIF-NG ON A "LIVE" MACHINE. - # At least until I can maybe find a better way to determine which LVs to reduce on multi-LV VGs - # so I can *then* use lvresize in a balanced manner, vgreduce, and pvmove/pvremove and not kill - # everything. - # TODO. - for lv in _BlockDev.lvm.lvs(): - if lv.vg_name == vg: - _BlockDev.lvm.lvremove(vg, lv.lv_name) - _BlockDev.lvm.vgreduce(vg) - _BlockDev.lvm.vgremove(vg) # This *shouldn't* fail. In theory. But LVM is lel. - _BlockDev.lvm.pvremove(self.devpath) - # Or if I can get this working properly. Shame it isn't automagic. - # Seems to kill the LV by dropping a PV under it. Makes sense, but STILL. LVM IS SO DUMB. - # _BlockDev.lvm.vgdeactivate(vg) - # _BlockDev.lvm.pvremove(self.devpath) - # _BlockDev.lvm.vgreduce(vg) - # _BlockDev.lvm.vgactivate(vg) - ## - self.meta = None - self.is_pooled = False - except _BlockDev.LVMError: - self.meta = None - self.is_pooled = False - opts = [_BlockDev.ExtraArg.new('--reportformat', 'json')] - # FUCK. LVM. You can't *specify* a UUID. - # u = uuid.uuid4() - # opts.append(_BlockDev.ExtraArg.new('--uuid', str(u))) - _BlockDev.lvm.pvcreate(self.devpath, - 0, - 0, - opts) - self._parseMeta() - return(None) - - -class VG(object): - def __init__(self, vg_xml): - self.xml = vg_xml - self.id = self.xml.attrib('id') - self.name = self.xml.attrib('name') - self.pe_size = self.xml.attrib.get('extentSize', 0) - if self.pe_size: - x = dict(zip(('from_bgn', 'size', 'type'), - aif.utils.convertSizeUnit(self.pe_size))) - if x['type']: - self.pe_size = aif.utils.size.convertStorage(self.pe_size, - x['type'], - target = 'B') - if not aif.utils.isPowerofTwo(self.pe_size): - raise ValueError('The PE size must be a power of two (in bytes)') - self.lvs = [] - self.pvs = [] - # self.tags = [] - # for te in self.xml.findall('tags/tag'): - # self.tags.append(te.text) - _common.addBDPlugin('lvm') - self.devpath = '/dev/{0}'.format(self.name) - self.info = None - self.created = False - - def addPV(self, pvobj): - if not isinstance(pvobj, PV): - raise ValueError('pvobj must be of type aif.disk.lvm.PV') - pvobj.prepare() - self.pvs.append(pvobj) - return(None) - - def create(self): - if not self.pvs: - raise RuntimeError('Cannot create a VG with no PVs') - opts = [_BlockDev.ExtraArg.new('--reportformat', 'json')] - # FUCK. LVM. You can't *specify* a UUID. - # u = uuid.uuid4() - # opts.append(_BlockDev.ExtraArg.new('--uuid', str(u))) - # for t in self.tags: - # opts.append(_BlockDev.ExtraArg.new('--addtag', t)) - _BlockDev.lvm.vgcreate(self.name, - [p.devpath for p in self.pvs], - self.pe_size, - opts) - for pv in self.pvs: - pv._parseMeta() - self.created = True - self.updateInfo() - return(None) - - def createLV(self, lv_xml = None): - if not self.created: - raise RuntimeError('VG must be created before LVs can be added') - # If lv_xml is None, we loop through our own XML. - if lv_xml: - lv = LV(lv_xml, self) - lv.create() - # self.lvs.append(lv) - else: - for le in self.xml.findall('logicalVolumes/lv'): - lv = LV(le, self) - lv.create() - # self.lvs.append(lv) - self.updateInfo() - return(None) - - def start(self): - _BlockDev.lvm.vgactivate(self.name) - self.updateInfo() - return(None) - - def stop(self): - _BlockDev.lvm.vgdeactivate(self.name) - self.updateInfo() - return(None) - - def updateInfo(self): - if not self.created: - return(None) - _info = _BlockDev.lvm.vginfo(self.name) - # TODO: parity with lvm_fallback.VG.updateInfo - # key names currently (probably) don't match and need to confirm the information's all present - info = {} - for k in dir(_info): - if k.startswith('_'): - continue - elif k in ('copy',): - continue - v = getattr(_info, k) - info[k] = v - self.info = info - return(None) - - class LV(object): def __init__(self, lv_xml, vgobj): self.xml = lv_xml + _logger.debug('lv_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) self.id = self.xml.attrib('id') self.name = self.xml.attrib('name') self.vg = vgobj self.qualified_name = '{0}/{1}'.format(self.vg.name, self.name) + _logger.debug('Qualified name: {0}'.format(self.qualified_name)) self.pvs = [] if not isinstance(self.vg, VG): - raise ValueError('vgobj must be of type aif.disk.lvm.VG') + _logger.debug('vgobj must be of type aif.disk.lvm.VG') + raise ValueError('Invalid vgobj type') _common.addBDPlugin('lvm') self.info = None self.devpath = '/dev/{0}/{1}'.format(self.vg.name, self.name) @@ -216,10 +40,12 @@ class LV(object): self.pvs = [] _indexed_pvs = {i.id: i for i in self.vg.pvs} for pe in self.xml.findall('pvMember'): + _logger.debug('Found PV element: {0}'.format(etree.tostring(pe, with_tail = False).decode('utf-8'))) pv_id = pe.attrib('source') if pv_id in _indexed_pvs.keys(): self.pvs.append(_indexed_pvs[pv_id]) if not self.pvs: # We get all in the VG instead since none were explicitly assigned + _logger.debug('No PVs explicitly designated to VG; adding all.') self.pvs = self.vg.pvs # Size processing. We have to do this after indexing PVs. # If not x['type'], assume *extents*, not sectors @@ -251,7 +77,8 @@ class LV(object): def create(self): if not self.pvs: - raise RuntimeError('Cannot create LV with no associated LVs') + _logger.error('Cannot create LV with no associated PVs') + raise RuntimeError('Missing PVs') opts = [_BlockDev.ExtraArg.new('--reportformat', 'json')] # FUCK. LVM. You can't *specify* a UUID. # u = uuid.uuid4() @@ -271,6 +98,7 @@ class LV(object): return(None) def start(self): + _logger.info('Activating LV {0} in VG {1}.'.format(self.name, self.vg.name)) _BlockDev.lvm.lvactivate(self.vg.name, self.name, True, @@ -279,6 +107,7 @@ class LV(object): return(None) def stop(self): + _logger.info('Deactivating LV {0} in VG {1}.'.format(self.name, self.vg.name)) _BlockDev.lvm.lvdeactivate(self.vg.name, self.name, None) @@ -287,6 +116,7 @@ class LV(object): def updateInfo(self): if not self.created: + _logger.warning('Attempted to updateInfo on an LV not created yet.') return(None) _info = _BlockDev.lvm.lvinfo(self.vg.name, self.name) # TODO: parity with lvm_fallback.LV.updateInfo @@ -300,4 +130,211 @@ class LV(object): v = getattr(_info, k) info[k] = v self.info = info + _logger.debug('Rendered info: {0}'.format(info)) + return(None) + + +class PV(object): + def __init__(self, pv_xml, partobj): + self.xml = pv_xml + _logger.debug('pv_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) + self.id = self.xml.attrib('id') + self.source = self.xml.attrib('source') + self.device = partobj + if not isinstance(self.device, (block.Disk, + block.Partition, + luks.LUKS, + mdadm.Array)): + _logger.error(('partobj must be of type ' + 'aif.disk.block.Disk, ' + 'aif.disk.block.Partition, ' + 'aif.disk.luks.LUKS, or' + 'aif.disk.mdadm.Array.')) + raise ValueError('Invalid partobj type') + _common.addBDPlugin('lvm') + self.devpath = self.device.devpath + self.is_pooled = False + self.meta = None + self._parseMeta() + + def _parseMeta(self): + # Note, the "UUID" for LVM is *not* a true UUID (RFC4122) so we don't convert it. + # https://unix.stackexchange.com/questions/173722/what-is-the-uuid-format-used-by-lvm + # TODO: parity with lvm_fallback.PV._parseMeta + # key names currently (probably) don't match and need to confirm the information's all present + meta = {} + try: + _meta = _BlockDev.lvm.pvinfo(self.devpath) + except _BlockDev.LVMError: + _logger.debug('PV device is not a PV yet.') + self.meta = None + self.is_pooled = False + return(None) + for k in dir(_meta): + if k.startswith('_'): + continue + elif k in ('copy', ): + continue + v = getattr(_meta, k) + meta[k] = v + self.meta = meta + _logger.debug('Rendered meta: {0}'.format(meta)) + self.is_pooled = True + return(None) + + def prepare(self): + try: + if not self.meta: + self._parseMeta() + if self.meta: + vg = self.meta['vg_name'] + # LVM is SO. DUMB. + # If you're using LVM, seriously - just switch your model to mdadm. It lets you do things like + # remove disks live without restructuring the entire thing. + # That said, because the config references partitions/disks/arrays/etc. created *in the same config*, + # and it's all dependent on block devices defined in the thing, we can be reckless here. + # I'd like to take the time now to remind you to NOT RUN AIF-NG ON A "PRODUCTION"-STATE MACHINE. + # At least until I can maybe find a better way to determine which LVs to reduce on multi-LV VGs + # so I can *then* use lvresize in a balanced manner, vgreduce, and pvmove/pvremove and not kill + # everything. + # TODO. + for lv in _BlockDev.lvm.lvs(): + if lv.vg_name == vg: + _logger.info('Removing LV {0} from VG {1}.'.format(lv.lv_name, vg)) + _BlockDev.lvm.lvremove(vg, lv.lv_name) + _logger.debug('Reducing VG {0}.'.format(vg)) + _BlockDev.lvm.vgreduce(vg) + _logger.info('Removing VG {0}.'.format(vg)) + _BlockDev.lvm.vgremove(vg) # This *shouldn't* fail. In theory. But LVM is lel. + _logger.info('Removing PV {0}.'.format(self.devpath)) + _BlockDev.lvm.pvremove(self.devpath) + # Or if I can get this working properly. Shame it isn't automagic. + # Seems to kill the LV by dropping a PV under it. Makes sense, but STILL. LVM IS SO DUMB. + # _BlockDev.lvm.vgdeactivate(vg) + # _BlockDev.lvm.pvremove(self.devpath) + # _BlockDev.lvm.vgreduce(vg) + # _BlockDev.lvm.vgactivate(vg) + ## + self.meta = None + self.is_pooled = False + except _BlockDev.LVMError: + self.meta = None + self.is_pooled = False + opts = [_BlockDev.ExtraArg.new('--reportformat', 'json')] + # FUCK. LVM. You can't *specify* a UUID. + # u = uuid.uuid4() + # opts.append(_BlockDev.ExtraArg.new('--uuid', str(u))) + _BlockDev.lvm.pvcreate(self.devpath, + 0, + 0, + opts) + _logger.info('Created PV {0} with opts {1}'.format(self.devpath, opts)) + self._parseMeta() + return(None) + + +class VG(object): + def __init__(self, vg_xml): + self.xml = vg_xml + _logger.debug('vg_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) + self.id = self.xml.attrib('id') + self.name = self.xml.attrib('name') + self.pe_size = self.xml.attrib.get('extentSize', 0) + if self.pe_size: + x = dict(zip(('from_bgn', 'size', 'type'), + aif.utils.convertSizeUnit(self.pe_size))) + if x['type']: + self.pe_size = aif.utils.size.convertStorage(self.pe_size, + x['type'], + target = 'B') + if not aif.utils.isPowerofTwo(self.pe_size): + _logger.error('The PE size must be a power of two (in bytes).') + raise ValueError('Invalid PE value') + self.lvs = [] + self.pvs = [] + # self.tags = [] + # for te in self.xml.findall('tags/tag'): + # self.tags.append(te.text) + _common.addBDPlugin('lvm') + self.devpath = '/dev/{0}'.format(self.name) + self.info = None + self.created = False + + def addPV(self, pvobj): + if not isinstance(pvobj, PV): + _logger.error('pvobj must be of type aif.disk.lvm.PV.') + raise ValueError('Invalid pvbobj type') + pvobj.prepare() + self.pvs.append(pvobj) + return(None) + + def create(self): + if not self.pvs: + _logger.error('Cannot create a VG with no PVs.') + raise RuntimeError('Missing PVs') + opts = [_BlockDev.ExtraArg.new('--reportformat', 'json')] + # FUCK. LVM. You can't *specify* a UUID. + # u = uuid.uuid4() + # opts.append(_BlockDev.ExtraArg.new('--uuid', str(u))) + # for t in self.tags: + # opts.append(_BlockDev.ExtraArg.new('--addtag', t)) + _BlockDev.lvm.vgcreate(self.name, + [p.devpath for p in self.pvs], + self.pe_size, + opts) + for pv in self.pvs: + pv._parseMeta() + self.created = True + self.updateInfo() + return(None) + + def createLV(self, lv_xml = None): + if not self.created: + _logger.info('Attempted to add an LV to a VG before it was created.') + raise RuntimeError('LV before VG creation') + # If lv_xml is None, we loop through our own XML. + if lv_xml: + _logger.debug('Explicit lv_xml specified: {0}'.format(etree.tostring(lv_xml, + with_tail = False).decode('utf-8'))) + lv = LV(lv_xml, self) + lv.create() + # self.lvs.append(lv) + else: + for le in self.xml.findall('logicalVolumes/lv'): + _logger.debug('Found lv element: {0}'.format(etree.tostring(le, with_tail = False).decode('utf-8'))) + lv = LV(le, self) + lv.create() + # self.lvs.append(lv) + self.updateInfo() + return(None) + + def start(self): + _logger.info('Activating VG: {0}.'.format(self.name)) + _BlockDev.lvm.vgactivate(self.name) + self.updateInfo() + return(None) + + def stop(self): + _logger.info('Deactivating VG: {0}.'.format(self.name)) + _BlockDev.lvm.vgdeactivate(self.name) + self.updateInfo() + return(None) + + def updateInfo(self): + if not self.created: + _logger.warning('Attempted to updateInfo on a VG not created yet.') + return(None) + _info = _BlockDev.lvm.vginfo(self.name) + # TODO: parity with lvm_fallback.VG.updateInfo + # key names currently (probably) don't match and need to confirm the information's all present + info = {} + for k in dir(_info): + if k.startswith('_'): + continue + elif k in ('copy',): + continue + v = getattr(_info, k) + info[k] = v + self.info = info + _logger.debug('Rendered info: {0}'.format(info)) return(None) diff --git a/aif/disk/lvm_fallback.py b/aif/disk/lvm_fallback.py index 4e61719..1d21e79 100644 --- a/aif/disk/lvm_fallback.py +++ b/aif/disk/lvm_fallback.py @@ -1,214 +1,32 @@ import datetime import json +import logging import subprocess ## +from lxml import etree +## import aif.utils import aif.disk.block_fallback as block import aif.disk.luks_fallback as luks import aif.disk.mdadm_fallback as mdadm -class PV(object): - def __init__(self, pv_xml, partobj): - self.xml = pv_xml - self.id = self.xml.attrib('id') - self.source = self.xml.attrib('source') - self.device = partobj - if not isinstance(self.device, (block.Disk, - block.Partition, - luks.LUKS, - mdadm.Array)): - raise ValueError(('partobj must be of type ' - 'aif.disk.block.Disk, ' - 'aif.disk.block.Partition, ' - 'aif.disk.luks.LUKS, or' - 'aif.disk.mdadm.Array')) - self.devpath = self.device.devpath - self.is_pooled = False - self.meta = None - self._parseMeta() - - def _parseMeta(self): - # Note, the "UUID" for LVM is *not* a true UUID (RFC4122) so we don't convert it. - # https://unix.stackexchange.com/questions/173722/what-is-the-uuid-format-used-by-lvm - meta = {} - cmd = ['pvs', - '--binary', - '--nosuffix', - '--units', 'b', - '--options', '+pvall', - '--reportformat', 'json', - self.devpath] - _meta = subprocess.run(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE) - if _meta.returncode != 0: - self.meta = None - self.is_pooled = False - return(None) - _meta = json.loads(_meta.stdout.decode('utf-8'))['report'][0]['pv'][0] - for k, v in _meta.items(): - # We *could* regex this but the pattern would be a little more complex than idea, - # especially for such predictable strings. - # These are ints. - if k in ('dev_size', 'pe_start', 'pv_ba_size', 'pv_ba_start', 'pv_ext_vsn', 'pv_free', 'pv_major', - 'pv_mda_count', 'pv_mda_free', 'pv_mda_size', 'pv_mda_used_count', 'pv_minor', 'pv_pe_alloc_count', - 'pv_pe_alloc_count', 'pv_size', 'pv_used'): - v = int(v) - # These are boolean. - elif k in ('pv_allocatable', 'pv_duplicate', 'pv_exported', 'pv_in_use', 'pv_missing'): - v = (True if int(v) == 1 else False) - # This is a list. - elif k == 'pv_tags': - v = [i.strip() for i in v.split(',') if i.strip() != ''] - elif v.strip() == '': - v = None - meta[k] = v - self.meta = meta - self.is_pooled = True - return(None) - - def prepare(self): - if not self.meta: - self._parseMeta() - # *Technically*, we should vgreduce before pvremove, but eff it. - cmd = ['pvremove', - '--force', '--force', - '--reportformat', 'json', - self.devpath] - subprocess.run(cmd) - cmd = ['pvcreate', - '--reportformat', 'json', - self.devpath] - subprocess.run(cmd) - self._parseMeta() - return(None) - - -class VG(object): - def __init__(self, vg_xml): - self.xml = vg_xml - self.id = self.xml.attrib('id') - self.name = self.xml.attrib('name') - self.pe_size = self.xml.attrib.get('extentSize', 0) - if self.pe_size: - x = dict(zip(('from_bgn', 'size', 'type'), - aif.utils.convertSizeUnit(self.pe_size))) - if x['type']: - self.pe_size = aif.utils.size.convertStorage(self.pe_size, - x['type'], - target = 'B') - if not aif.utils.isPowerofTwo(self.pe_size): - raise ValueError('The PE size must be a power of two (in bytes)') - self.lvs = [] - self.pvs = [] - # self.tags = [] - # for te in self.xml.findall('tags/tag'): - # self.tags.append(te.text) - self.devpath = None - self.devpath = self.name - self.info = None - self.created = False - - def addPV(self, pvobj): - if not isinstance(pvobj, PV): - raise ValueError('pvobj must be of type aif.disk.lvm.PV') - pvobj.prepare() - self.pvs.append(pvobj) - return(None) - - def create(self): - if not self.pvs: - raise RuntimeError('Cannot create a VG with no PVs') - cmd = ['vgcreate', - '--reportformat', 'json', - '--physicalextentsize', '{0}b'.format(self.pe_size), - self.name] - for pv in self.pvs: - cmd.append(pv.devpath) - subprocess.run(cmd) - for pv in self.pvs: - pv._parseMeta() - self.created = True - self.updateInfo() - return(None) - - def createLV(self, lv_xml = None): - if not self.created: - raise RuntimeError('VG must be created before LVs can be added') - # If lv_xml is None, we loop through our own XML. - if lv_xml: - lv = LV(lv_xml, self) - lv.create() - # self.lvs.append(lv) - else: - for le in self.xml.findall('logicalVolumes/lv'): - lv = LV(le, self) - lv.create() - # self.lvs.append(lv) - self.updateInfo() - return(None) - - def start(self): - cmd = ['vgchange', - '--activate', 'y', - '--reportformat', 'json', - self.name] - subprocess.run(cmd) - self.updateInfo() - return(None) - - def stop(self): - cmd = ['vgchange', - '--activate', 'n', - '--reportformat', 'json', - self.name] - subprocess.run(cmd) - self.updateInfo() - return(None) - - def updateInfo(self): - info = {} - cmd = ['vgs', - '--binary', - '--nosuffix', - '--units', 'b', - '--options', '+vgall', - '--reportformat', 'json', - self.name] - _info = subprocess.run(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE) - if _info.returncode != 0: - self.info = None - self.created = False - return(None) - _info = json.loads(_info.stdout.decode('utf-8'))['report'][0]['vg'][0] - for k, v in _info.items(): - # ints - if k in ('lv_count', 'max_lv', 'max_pv', 'pv_count', 'snap_count', 'vg_extent_count', 'vg_extent_size', - 'vg_free', 'vg_free_count', 'vg_mda_count', 'vg_mda_free', 'vg_mda_size', 'vg_mda_used_count', - 'vg_missing_pv_count', 'vg_seqno', 'vg_size'): - v = int(v) - # booleans - elif k in ('vg_clustered', 'vg_exported', 'vg_extendable', 'vg_partial', 'vg_shared'): - v = (True if int(v) == 1 else False) - # lists - elif k in ('vg_lock_args', 'vg_permissions', 'vg_tags'): # not 100% sure about vg_permissions... - v = [i.strip() for i in v.split(',') if i.strip() != ''] - elif v.strip() == '': - v = None - info[k] = v - self.info = info - return(None) +_logger = logging.getLogger(__name__) class LV(object): def __init__(self, lv_xml, vgobj): self.xml = lv_xml + _logger.debug('lv_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) self.id = self.xml.attrib('id') self.name = self.xml.attrib('name') self.vg = vgobj self.qualified_name = '{0}/{1}'.format(self.vg.name, self.name) + _logger.debug('Qualified name: {0}'.format(self.qualified_name)) self.pvs = [] if not isinstance(self.vg, VG): - raise ValueError('vgobj must be of type aif.disk.lvm.VG') + _logger.debug('vgobj must be of type aif.disk.lvm.VG') + raise ValueError('Invalid vgobj type') self.info = None self.devpath = '/dev/{0}/{1}'.format(self.vg.name, self.name) self.created = False @@ -219,10 +37,12 @@ class LV(object): self.pvs = [] _indexed_pvs = {i.id: i for i in self.vg.pvs} for pe in self.xml.findall('pvMember'): + _logger.debug('Found PV element: {0}'.format(etree.tostring(pe, with_tail = False).decode('utf-8'))) pv_id = pe.attrib('source') if pv_id in _indexed_pvs.keys(): self.pvs.append(_indexed_pvs[pv_id]) if not self.pvs: # We get all in the VG instead since none were explicitly assigned + _logger.debug('No PVs explicitly designated to VG; adding all.') self.pvs = self.vg.pvs # Size processing. We have to do this after indexing PVs. # If not x['type'], assume *extents*, not sectors @@ -254,15 +74,26 @@ class LV(object): def create(self): if not self.pvs: - raise RuntimeError('Cannot create LV with no associated LVs') - cmd = ['lvcreate', - '--reportformat', 'json'] + _logger.error('Cannot create LV with no associated PVs') + raise RuntimeError('Missing PVs') + cmd_str = ['lvcreate', + '--reportformat', 'json'] if self.size > 0: - cmd.extend(['--size', self.size]) + cmd_str.extend(['--size', self.size]) elif self.size == 0: - cmd.extend(['--extents', '100%FREE']) - cmd.extend([self.name, - self.vg.name]) + cmd_str.extend(['--extents', '100%FREE']) + cmd_str.extend([self.name, + self.vg.name]) + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to create LV successfully') self.vg.lvs.append(self) self.created = True self.updateInfo() @@ -270,24 +101,47 @@ class LV(object): return(None) def start(self): - cmd = ['lvchange', - '--activate', 'y', - '--reportformat', 'json', - self.qualified_name] - subprocess.run(cmd) + _logger.info('Activating LV {0} in VG {1}.'.format(self.name, self.vg.name)) + cmd_str = ['lvchange', + '--activate', 'y', + '--reportformat', 'json', + self.qualified_name] + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to activate LV successfully') self.updateInfo() return(None) def stop(self): - cmd = ['lvchange', - '--activate', 'n', - '--reportformat', 'json', - self.qualified_name] - subprocess.run(cmd) + _logger.info('Deactivating LV {0} in VG {1}.'.format(self.name, self.vg.name)) + cmd_str = ['lvchange', + '--activate', 'n', + '--reportformat', 'json', + self.qualified_name] + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to deactivate successfully') self.updateInfo() return(None) def updateInfo(self): + if not self.created: + _logger.warning('Attempted to updateInfo on an LV not created yet.') + return(None) info = {} cmd = ['lvs', '--binary', @@ -297,7 +151,14 @@ class LV(object): '--reportformat', 'json', self.qualified_name] _info = subprocess.run(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(_info.args))) if _info.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(_info.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) self.info = None self.created = False return(None) @@ -330,4 +191,271 @@ class LV(object): v = None info[k] = v self.info = info + _logger.debug('Rendered info: {0}'.format(info)) + return(None) + + +class PV(object): + def __init__(self, pv_xml, partobj): + self.xml = pv_xml + _logger.debug('pv_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) + self.id = self.xml.attrib('id') + self.source = self.xml.attrib('source') + self.device = partobj + if not isinstance(self.device, (block.Disk, + block.Partition, + luks.LUKS, + mdadm.Array)): + _logger.error(('partobj must be of type ' + 'aif.disk.block.Disk, ' + 'aif.disk.block.Partition, ' + 'aif.disk.luks.LUKS, or' + 'aif.disk.mdadm.Array.')) + raise ValueError('Invalid partobj type') + self.devpath = self.device.devpath + self.is_pooled = False + self.meta = None + self._parseMeta() + + def _parseMeta(self): + # Note, the "UUID" for LVM is *not* a true UUID (RFC4122) so we don't convert it. + # https://unix.stackexchange.com/questions/173722/what-is-the-uuid-format-used-by-lvm + meta = {} + cmd = ['pvs', + '--binary', + '--nosuffix', + '--units', 'b', + '--options', '+pvall', + '--reportformat', 'json', + self.devpath] + _meta = subprocess.run(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(_meta.args))) + if _meta.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(_meta.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + self.meta = None + self.is_pooled = False + return(None) + _meta = json.loads(_meta.stdout.decode('utf-8'))['report'][0]['pv'][0] + for k, v in _meta.items(): + # We *could* regex this but the pattern would be a little more complex than idea, + # especially for such predictable strings. + # These are ints. + if k in ('dev_size', 'pe_start', 'pv_ba_size', 'pv_ba_start', 'pv_ext_vsn', 'pv_free', 'pv_major', + 'pv_mda_count', 'pv_mda_free', 'pv_mda_size', 'pv_mda_used_count', 'pv_minor', 'pv_pe_alloc_count', + 'pv_pe_alloc_count', 'pv_size', 'pv_used'): + v = int(v) + # These are boolean. + elif k in ('pv_allocatable', 'pv_duplicate', 'pv_exported', 'pv_in_use', 'pv_missing'): + v = (True if int(v) == 1 else False) + # This is a list. + elif k == 'pv_tags': + v = [i.strip() for i in v.split(',') if i.strip() != ''] + elif v.strip() == '': + v = None + meta[k] = v + self.meta = meta + self.is_pooled = True + _logger.debug('Rendered meta: {0}'.format(meta)) + return(None) + + def prepare(self): + if not self.meta: + self._parseMeta() + # *Technically*, we should vgreduce before pvremove, but eff it. + cmd_str = ['pvremove', + '--force', '--force', + '--reportformat', 'json', + self.devpath] + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to remove PV successfully') + cmd_str = ['pvcreate', + '--reportformat', 'json', + self.devpath] + cmd = subprocess.run(cmd_str) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to format successfully') + self._parseMeta() + return(None) + + +class VG(object): + def __init__(self, vg_xml): + self.xml = vg_xml + _logger.debug('vg_xml: {0}'.format(etree.tostring(self.xml, with_tail = False).decode('utf-8'))) + self.id = self.xml.attrib('id') + self.name = self.xml.attrib('name') + self.pe_size = self.xml.attrib.get('extentSize', 0) + if self.pe_size: + x = dict(zip(('from_bgn', 'size', 'type'), + aif.utils.convertSizeUnit(self.pe_size))) + if x['type']: + self.pe_size = aif.utils.size.convertStorage(self.pe_size, + x['type'], + target = 'B') + if not aif.utils.isPowerofTwo(self.pe_size): + _logger.error('The PE size must be a power of two (in bytes).') + raise ValueError('Invalid PE value') + self.lvs = [] + self.pvs = [] + # self.tags = [] + # for te in self.xml.findall('tags/tag'): + # self.tags.append(te.text) + self.devpath = self.name + self.info = None + self.created = False + + def addPV(self, pvobj): + if not isinstance(pvobj, PV): + _logger.error('pvobj must be of type aif.disk.lvm.PV.') + raise ValueError('Invalid pvbobj type') + pvobj.prepare() + self.pvs.append(pvobj) + return(None) + + def create(self): + if not self.pvs: + _logger.error('Cannot create a VG with no PVs.') + raise RuntimeError('Missing PVs') + cmd_str = ['vgcreate', + '--reportformat', 'json', + '--physicalextentsize', '{0}b'.format(self.pe_size), + self.name] + for pv in self.pvs: + cmd_str.append(pv.devpath) + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to create VG successfully') + for pv in self.pvs: + pv._parseMeta() + self.created = True + self.updateInfo() + return(None) + + def createLV(self, lv_xml = None): + if not self.created: + _logger.info('Attempted to add an LV to a VG before it was created.') + raise RuntimeError('LV before VG creation') + # If lv_xml is None, we loop through our own XML. + if lv_xml: + _logger.debug('Explicit lv_xml specified: {0}'.format(etree.tostring(lv_xml, + with_tail = False).decode('utf-8'))) + lv = LV(lv_xml, self) + lv.create() + # self.lvs.append(lv) + else: + for le in self.xml.findall('logicalVolumes/lv'): + _logger.debug('Found lv element: {0}'.format(etree.tostring(le, with_tail = False).decode('utf-8'))) + lv = LV(le, self) + lv.create() + # self.lvs.append(lv) + self.updateInfo() + return(None) + + def start(self): + _logger.info('Activating VG: {0}.'.format(self.name)) + cmd_str = ['vgchange', + '--activate', 'y', + '--reportformat', 'json', + self.name] + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to activate VG successfully') + self.updateInfo() + return(None) + + def stop(self): + _logger.info('Deactivating VG: {0}.'.format(self.name)) + cmd_str = ['vgchange', + '--activate', 'n', + '--reportformat', 'json', + self.name] + cmd = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(cmd.args))) + if cmd.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(cmd.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(cmd, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + raise RuntimeError('Failed to deactivate VG successfully') + self.updateInfo() + return(None) + + def updateInfo(self): + if not self.created: + _logger.warning('Attempted to updateInfo on a VG not created yet.') + return(None) + info = {} + cmd_str = ['vgs', + '--binary', + '--nosuffix', + '--units', 'b', + '--options', '+vgall', + '--reportformat', 'json', + self.name] + _info = subprocess.run(cmd_str, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + _logger.info('Executed: {0}'.format(' '.join(_info.args))) + if _info.returncode != 0: + _logger.warning('Command returned non-zero status') + _logger.debug('Exit status: {0}'.format(str(_info.returncode))) + for a in ('stdout', 'stderr'): + x = getattr(_info, a) + if x: + _logger.debug('{0}: {1}'.format(a.upper(), x.decode('utf-8').strip())) + self.info = None + self.created = False + return(None) + _info = json.loads(_info.stdout.decode('utf-8'))['report'][0]['vg'][0] + for k, v in _info.items(): + # ints + if k in ('lv_count', 'max_lv', 'max_pv', 'pv_count', 'snap_count', 'vg_extent_count', 'vg_extent_size', + 'vg_free', 'vg_free_count', 'vg_mda_count', 'vg_mda_free', 'vg_mda_size', 'vg_mda_used_count', + 'vg_missing_pv_count', 'vg_seqno', 'vg_size'): + v = int(v) + # booleans + elif k in ('vg_clustered', 'vg_exported', 'vg_extendable', 'vg_partial', 'vg_shared'): + v = (True if int(v) == 1 else False) + # lists + elif k in ('vg_lock_args', 'vg_permissions', 'vg_tags'): # not 100% sure about vg_permissions... + v = [i.strip() for i in v.split(',') if i.strip() != ''] + elif v.strip() == '': + v = None + info[k] = v + self.info = info + _logger.debug('Rendered info: {0}'.format(info)) return(None)