diff --git a/aif.xsd b/aif.xsd index ac992ac..fdc1b91 100644 --- a/aif.xsd +++ b/aif.xsd @@ -75,6 +75,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -283,6 +318,11 @@ + + + + + + + diff --git a/aif/config.py b/aif/config.py index 135a7f7..f31e8e0 100644 --- a/aif/config.py +++ b/aif/config.py @@ -1,8 +1,9 @@ +import copy import os import re ## import requests -from lxml import etree +from lxml import etree, objectify _patterns = {'raw': re.compile(r'^\s*(?P<(\?xml|aif)\s+.*)\s*$', re.DOTALL|re.MULTILINE), 'remote': re.compile(r'^(?P(?P(https?|ftps?)://)(?P.*))\s*$'), @@ -19,6 +20,7 @@ class Config(object): self.raw = None self.xsd = None self.defaultsParser = None + self.obj = None def main(self, validate = True, populate_defaults = True): self.fetch() @@ -27,6 +29,7 @@ class Config(object): self.populateDefaults() if validate: self.validate() + self.pythonize() return() def fetch(self): # Just a fail-safe; this is overridden by specific subclasses. @@ -53,7 +56,7 @@ class Config(object): if len(split_url) == 2: # a properly defined schemaLocation schemaURL = split_url[1] else: - schemaURL = split_url[0] + schemaURL = split_url[0] # a LAZY schemaLocation req = requests.get(schemaURL) if not req.ok: # TODO: logging! @@ -82,21 +85,63 @@ class Config(object): self.parseRaw(parser = self.defaultsParser) return() + def pythonize(self, stripped = True, obj = 'tree'): + # https://bugs.launchpad.net/lxml/+bug/1850221 + strobj = self.toString(stripped = stripped, obj = obj) + self.obj = objectify.fromstring(strobj) + objectify.annotate(self.obj) + objectify.xsiannotate(self.obj) + return() + def removeDefaults(self): self.parseRaw() return() - def stripNS(self): + def stripNS(self, obj = None): # https://stackoverflow.com/questions/30232031/how-can-i-strip-namespaces-out-of-an-lxml-tree/30233635#30233635 - for x in (self.tree, self.xml): - for e in x.xpath("descendant-or-self::*[namespace-uri()!='']"): + xpathq = "descendant-or-self::*[namespace-uri()!='']" + if not obj: + for x in (self.tree, self.xml): + for e in x.xpath(xpathq): + e.tag = etree.QName(e).localname + elif isinstance(obj, (etree._Element, etree._ElementTree)): + obj = copy.deepcopy(obj) + for e in obj.xpath(xpathq): e.tag = etree.QName(e).localname + return(obj) + else: + raise ValueError('Did not know how to parse obj parameter') return() + def toString(self, stripped = False, obj = None): + if isinstance(obj, (etree._Element, etree._ElementTree)): + if stripped: + obj = self.stripNS(obj) + elif obj in ('tree', None): + if not stripped: + obj = self.namespaced_tree + else: + obj = self.tree + elif obj == 'xml': + if not stripped: + obj = self.namespaced_xml + else: + obj = self.xml + else: + raise ValueError(('obj parameter must be "tree", "xml", or of type ' + 'lxml.etree._Element or lxml.etree._ElementTree')) + obj = copy.deepcopy(obj) + strxml = etree.tostring(obj, + encoding = 'utf-8', + xml_declaration = True, + pretty_print = True, + with_tail = True, + inclusive_ns_prefixes = True) + return(strxml) + def validate(self): if not self.xsd: self.getXSD() - self.xsd.assertValid(self.tree) self.xsd.assertValid(self.namespaced_tree) return() diff --git a/aif/disk/mdadm.py b/aif/disk/mdadm.py index a59b751..4c60c22 100644 --- a/aif/disk/mdadm.py +++ b/aif/disk/mdadm.py @@ -1,4 +1,6 @@ import copy +import math +import re import subprocess ## import mdstat @@ -8,6 +10,30 @@ from aif.disk.block import Partition SUPPORTED_LEVELS = (0, 1, 4, 5, 6) +SUPPORTED_METADATA = ('0', '0.90', '1', '1.0', '1.1', '1.2', 'default', 'ddf', 'imsm') +SUPPORTED_LAYOUTS = {5: (re.compile(r'^((left|right)-a?symmetric|[lr][as]|' + r'parity-(fir|la)st|' + r'ddf-(N|zero)-restart|ddf-N-continue)$'), + 'left-symmetric'), + 6: (re.compile(r'^((left|right)-a?symmetric(-6)?|[lr][as]|' + r'parity-(fir|la)st|' + r'ddf-(N|zero)-restart|ddf-N-continue|' + r'parity-first-6)$'), + None), + 10: (re.compile(r'^[nof][0-9]+$'), + None)} + + +def _itTakesTwo(n): + # So dumb. + isPowerOf2 = math.ceil(math.log(n, 2)) == math.floor(math.log(n, 2)) + return(isPowerOf2) + +def _safeChunks(n): + if (n % 4) != 0: + return(False) + return(True) + class Member(object): def __init__(self, member_xml, partobj): @@ -24,27 +50,79 @@ class Member(object): return() class Array(object): - def __init__(self, array_xml): + def __init__(self, array_xml, homehost): self.xml = array_xml self.id = array_xml.attrib['id'] - self.level = int(array_xml.attrib['level']) + self.level = int(self.xml.attrib['level']) if self.level not in SUPPORTED_LEVELS: - raise ValueError('RAID level must be one of: {0}'.format(', '.join(SUPPORTED_LEVELS))) + raise ValueError('RAID level must be one of: {0}'.format(', '.join([str(i) for i in SUPPORTED_LEVELS]))) + self.metadata = self.xml.attrib.get('meta', '1.2') + if self.metadata not in SUPPORTED_METADATA: + raise ValueError('Metadata version must be one of: {0}'.format(', '.join(SUPPORTED_METADATA))) + self.chunksize = int(self.xml.attrib.get('chunkSize', 512)) + if self.level in (4, 5, 6, 10): + if not _itTakesTwo(self.chunksize): + # TODO: log.warn instead of raise exception? Will mdadm lose its marbles if it *isn't* a proper number? + raise ValueError('chunksize must be a power of 2 for the RAID level you specified') + if self.level in (0, 4, 5, 6, 10): + if not _safeChunks(self.chunksize): + # TODO: log.warn instead of raise exception? Will mdadm lose its marbles if it *isn't* a proper number? + raise ValueError('chunksize must be divisible by 4 for the RAID level you specified') + self.layout = self.xml.attrib.get('layout', 'none') + if self.level in SUPPORTED_LAYOUTS.keys(): + matcher, layout_default = SUPPORTED_LAYOUTS[self.level] + if not matcher.search(self.layout): + if layout_default: + self.layout = layout_default + else: + self.layout = None # TODO: log.warn? + else: + self.layout = None self.devname = self.xml.attrib['name'] self.devpath = '/dev/md/{0}'.format(self.devname) self.updateStatus() self.members = [] + self.state = None def addMember(self, memberobj): if not isinstance(memberobj, Member): raise ValueError('memberobj must be of type aif.disk.mdadm.Member') - def assemble(self): + pass + return() + + def assemble(self, scan = False): cmd = ['mdadm', '--assemble', self.devpath] + if not scan: + for m in self.members: + cmd.append(m.devpath) + else: + cmd.extend(['']) + # TODO: logging! + subprocess.run(cmd) + + pass + return() + + def create(self): + if not self.members: + raise RuntimeError('Cannot create an array with no members') + cmd = ['mdadm', '--create', + '--level={0}'.format(self.level), + '--metadata={0}'.format(self.metadata), + '--chunk={0}'.format(self.chunksize), + '--raid-devices={0}'.format(len(self.members))] + if self.layout: + cmd.append('--layout={0}'.format(self.layout)) + cmd.append(self.devpath) for m in self.members: cmd.append(m.devpath) + # TODO: logging! subprocess.run(cmd) + pass + return() + def stop(self): # TODO: logging subprocess.run(['mdadm', '--stop', self.devpath]) @@ -57,3 +135,6 @@ class Array(object): del(_info['devices'][k]) self.info = copy.deepcopy(_info) return() + + def writeConf(self, conf = '/etc/mdadm.conf'): + pass diff --git a/examples/aif.xml b/examples/aif.xml index 2f256d9..27f62dd 100644 --- a/examples/aif.xml +++ b/examples/aif.xml @@ -8,12 +8,24 @@ - - - - - - + + esp + + + root + + + lvm + + + raid + + + raid + + + swap +