diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..d24842f --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +COPYING \ No newline at end of file diff --git a/TODO b/TODO index f4947e4..a02dcff 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,8 @@ - write classes/functions - XML-based config -- ensure we use docstrings in a Sphinx-compatible manner. +- ensure we use docstrings in a Sphinx-compatible manner? https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html + at the very least document all the functions and such so pydoc's happy. - package for PyPI: # https://packaging.python.org/tutorials/distributing-packages/ diff --git a/bdisk/__init__.py b/bdisk/__init__.py index 9f0a3c3..a85970a 100644 --- a/bdisk/__init__.py +++ b/bdisk/__init__.py @@ -2,6 +2,9 @@ import os import platform import sys +""" +BDisk - An easy liveCD creator built in python. +""" # BDisk is only supported on Python 3.4 and up. if sys.version_info.major != 3: diff --git a/bdisk/bdisk.xsd b/bdisk/bdisk.xsd new file mode 100644 index 0000000..e69de29 diff --git a/bdisk/confparse.py b/bdisk/confparse.py index 9f83931..82a7f64 100644 --- a/bdisk/confparse.py +++ b/bdisk/confparse.py @@ -1,29 +1,158 @@ +import _io +import copy +import os import validators from urllib.parse import urlparse -try: - from lxml import etree - has_lxml = True -except ImportError: - import xml.etree.ElementTree as etree - has_lxml = False +import lxml.etree +import lxml.objectify as objectify -"""Read a configuration file, parse it, and make it available to the rest of -BDisk.""" +etree = lxml.etree + +_profile_specifiers = ('id', 'name', 'uuid') + +def _detect_cfg(cfg): + if isinstance(cfg, str): + # check for path or string + try: + etree.fromstring(cfg) + except lxml.etree.XMLSyntaxError: + path = os.path.abspath(os.path.expanduser(cfg)) + try: + with open(path, 'r') as f: + cfg = f.read() + except FileNotFoundError: + raise ValueError('Could not open {0}'.format(path)) + elif isinstance(cfg, _io.TextIOWrapper): + _cfg = cfg.read() + cfg.close() + cfg = _cfg + elif isinstance(self.cfg, _io.BufferedReader): + _cfg = cfg.read().decode('utf-8') + cfg.close() + cfg = _cfg + elif isinstance(cfg, bytes): + cfg = cfg.decode('utf-8') + else: + raise TypeError('Could not determine the object type.') + return(cfg) + +def _profile_xpath_gen(selector): + xpath = '' + for i in selector.items(): + if i[1] and i[0] in _profile_specifiers: + xpath += '[@{0}="{1}"]'.format(*i) + return(xpath) class Conf(object): - def __init__(self, cfg, profile = None, id_type = 'name'): - """Conf classes accept the following parameters: - cfg - The configuration. Can be a filesystem path, a string, bytes, - or a stream + def __init__(self, cfg, profile = None): + """ + A configuration object. - profile (optional) - A sub-profile in the configuration. If None is - provided, we'll first look for a profile named - 'default'. If one isn't found, then the first - profile found will be used - id_type (optional) - The type of identifer to use for profile=. - Valid values are: + Read a configuration file, parse it, and make it available to the rest + of BDisk. - id - name - uuid""" + Args: + + cfg The configuration. Can be a filesystem path, a string, + bytes, or a stream. If bytes or a bytestream, it must be + in UTF-8 format. + + profile (optional) A sub-profile in the configuration. If None + is provided, we'll first look for the first profile + named 'default' (case-insensitive). If one isn't found, + then the first profile found will be used. Can be a + string (in which we'll automatically search for the + given value in the "name" attribute) or a dict for more + fine-grained profile identification, such as: + + {'name': 'PROFILE_NAME', + 'id': 1, + 'uuid': '00000000-0000-0000-0000-000000000000'} + + You can provide any combination of these + (e.g. "profile={'id': 2, 'name' = 'some_profile'}"). + """ + self.raw = _detect_cfg(cfg) + self.profile = profile + self.xml = None + self.profile = None + self.xml = etree.from_string(self.cfg) + self.xsd = None + #if not self.validate(): # Need to write the XSD + # raise ValueError('The configuration did not pass XSD/schema ' + # 'validation') + self.get_profile() + self.max_recurse = int(self.profile.xpath('//meta/' + 'max_recurse')[0].text) + + def get_xsd(self): + path = os.path.join(os.path.dirname(__file__), + 'bdisk.xsd') + with open(path, 'r') as f: + xsd = f.read() + return(xsd) + + def validate(self): + self.xsd = etree.XMLSchema(self.get_xsd()) + return(self.xsd.validate(self.xml)) + + def get_profile(self): + """Get a configuration profile. + + Get a configuration profile from the XML object and set that as a + profile object. If a profile is specified, attempt to find it. If not, + follow the default rules as specified in __init__. + """ + if self.profile: + # A profile identifier was provided + if isinstance(self.profile, str): + _profile_name = self.profile + self.profile = {} + for i in _profile_specifiers: + self.profile[i] = None + self.profile['name'] = _profile_name + elif isinstance(self.profile, dict): + for k in _profile_specifiers: + if k not in self.profile.keys(): + self.profile[k] = None + else: + raise TypeError('profile must be a string (name of profile), ' + 'a dictionary, or None') + xpath = ('/bdisk/' + 'profile{0}').format(_profile_xpath_gen(self.profile)) + self.profile = self.xml.xpath(xpath) + if not self.profile: + raise RuntimeError('Could not find the profile specified in ' + 'the given configuration') + else: + # We need to find the default. + profiles = [] + for p in self.xml.xpath('/bdisk/profile'): + profiles.append(p) + # Look for one named "default" or "DEFAULT" etc. + for idx, value in enumerate([e.attrib['name'].lower() \ + for e in profiles]): + if value == 'default': + self.profile = copy.deepcopy(profiles[idx]) + break + # We couldn't find a profile with a default name. Try to grab the + # first profile. + if not self.profile: + # Grab the first profile. + if profiles: + self.profile = profile[0] + else: + # No profiles found. + raise RuntimeError('Could not find any usable ' + 'configuration profiles') + return() + + def parse_profile(self): pass + + def _xpath_ref(self, element): + data = None + # This is incremented each recursive call until we reach + # self.max_recurse + recurse_cnt = 1 + return(data) diff --git a/bdisk/main.py b/bdisk/main.py index 182dd28..40af6a6 100644 --- a/bdisk/main.py +++ b/bdisk/main.py @@ -1,15 +1,30 @@ #!/usr/bin/env python3.6 import argparse +import confparse """The primary user interface for BDisk. If we are running interactively, parse arguments first, then initiate a BDisk session.""" def parseArgs(): - pass + args = argparse.ArgumentParser(description = ('An easy liveCD creator ' + 'built in python. Supports ' + 'hybrid ISOs/USB, iPXE, and ' + 'UEFI.'), + epilog = ('https://git.square-r00t.net')) + return(args) def run(): pass def run_interactive(): - pass + args = vars(parseArgs().parse_args()) + args['profile'] = {} + for i in ('name', 'id', 'uuid'): + args['profile'][i] = args[i] + del(args[i]) + run(args) + return() + +if __name__ == '__main__': + main() diff --git a/docs/examples/example.xml b/docs/examples/example.xml index 63b08fc..d5b633d 100644 --- a/docs/examples/example.xml +++ b/docs/examples/example.xml @@ -110,4 +110,100 @@ + + + + Another Disk + livecd2 + + + Some other rescue/restore live environment. + + Another Dev Eloper + dev2@domain.tld + https://domain.tld/~dev2 + + https://domain.tld/projname + 0.0.1 + 3 + + + + + $6$yR0lsi68GZ.8oAuV$juLOanZ6IGD6caxJFo5knnXwFZRi65Q58a1XfSWBX7R97EpHrVgpzdXfA3ysAfAg4bs1d6wBv7su2rURkg2rn. + + + + + http://archlinux.mirror.domain.tld + /iso/latest + + //archlinux-bootstrap-*-x86_64.tar.gz + + + //sha1sums.txt + + + .sig + + + + http://archlinux32.mirror.domain.tld + /iso/latest + + //archlinux-bootstrap-*-i686.tar.gz + + + //sha512sums.txt + + + .sig + + + + + + /var/tmp/ + /var/tmp/chroots/ + ~//templates + /mnt/ + ~//distros + ~//results + /iso + /http + /tftp + /pki + + archlinux + + + + /ca.crt + /ca.key + + /.crt + + + /.key + + + /ipxe + + + + + + + root + /srv/http/ + mirror.domain.tld + 22 + ~/.ssh/id_ed25519 + + + +