checking in some work

This commit is contained in:
brent s. 2018-05-18 08:13:46 -04:00
parent 559789ffe5
commit b2498ba98d
8 changed files with 789 additions and 424 deletions

9
TODO
View File

@ -9,6 +9,15 @@
- for docs, 3.x (as of 3.10) was 2.4M.
- GUI? at least for generating config...

- SSL key gen:
import OpenSSL
k = OpenSSL.crypto.PKey()
k.generate_key(OpenSSL.crypto.TYPE_RSA, 4096)
x = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
k,
cipher = 'aes256',
passphrase = 'test')

- need to package:
python-hashid (https://psypanda.github.io/hashID/,
https://github.com/psypanda/hashID,

View File

@ -3,6 +3,9 @@ import os
import psutil
import gpg.errors

# http://files.au.adversary.org/crypto/GPGMEpythonHOWTOen.html
# https://www.gnupg.org/documentation/manuals/gpgme.pdf

class GPGHandler(object):
def __init__(self, gnupg_homedir = None, key_id = None, keyservers = None):
self.home = gnupg_homedir

View File

@ -1 +1,5 @@
import OpenSSL
# https://cryptography.io/en/latest/x509/reference/#cryptography.x509.CertificateBuilder.sign
# migrate old functions of bSSL to use cryptography
# but still waiting on their recpipes.
# https://cryptography.io/en/latest/x509/tutorial/

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python3.6

# Ironically enough, I think building a GUI for this would be *cleaner*.
# Go figure.

import confparse
import crypt
import getpass
@ -15,7 +18,7 @@ transform = utils.transform()
valid = utils.valid()

# TODO: convert the restarts for prompts to continue's instead of letting them
# continue on to the next prompt.
# continue on to the next prompt.

def pass_prompt(user):
# This isn't in utils.prompts() because we need to use an instance of
@ -147,6 +150,9 @@ class ConfGenerator(object):
self.get_accounts()
self.get_sources()
self.get_build()
self.get_iso()
self.get_ipxe()
self.get_pki()
except KeyboardInterrupt:
exit('\n\nCaught KeyboardInterrupt; quitting...')
return()
@ -290,6 +296,8 @@ class ConfGenerator(object):
'\nWhat is YOUR name?\nName: ')).strip()
meta_items['dev']['email'] = (input('\nWhat is your email address?'
'\nemail: ')).strip()
# TODO: this always returns invalid?? and doesn't seem to trigger
# the redo
if not valid.email(meta_items['dev']['email']):
print('Invalid; skipping...')
meta_items['dev']['email'] = None
@ -395,7 +403,8 @@ class ConfGenerator(object):
'x86_64': ('(Also referred to by distros as '
'"64-bit")')}
while more_sources:
if len(_arches) == len(_supported_arches):
# this doesn't trigger? maybe?
if len(_arches) >= len(_supported_arches):
# All supported arches have been added. We currently don't
# support mirror-balancing. TODO?
print('\nCannot add more sources; all supported architectures '
@ -403,7 +412,7 @@ class ConfGenerator(object):
more_sources = False
break
if len(_arches) > 0:
print('\n(Currently added arches: {0})'.format(
print('\n\t(Currently added arches: {0})'.format(
', '.join(_arches)))
_print_arches = '\n\t'.join(
['{0}:\t{1}'.format(*i) for i in _supported_arches.items()])
@ -531,7 +540,7 @@ class ConfGenerator(object):
continue
sig.attrib['keys'] = sigkeys
else:
sigkeys = detect.gpgkeyID_from_url(gpgsig)
sigkeys = detect.gpgkeyID_from_url(gpgsig['full_url'])
if not isinstance(sigkeys, list):
print('Could not properly parse any keys in the '
'signature file. Restarting.')
@ -553,7 +562,8 @@ class ConfGenerator(object):
print('\t\t{0}'.format(_uid['Name']))
for k in _uid:
if k != 'Name':
print('\t\t\t{0}:\t{1}'.format(k, _uid[k]))
print('\t\t\t{0:<9} {1}'.format(
'{0}:'.format(k), _uid[k]))
_key_chk = prompt.confirm_or_no(prompt = (
'\n{0} look correct?\n').format(_s))
if not _key_chk:
@ -633,22 +643,157 @@ class ConfGenerator(object):
distro_path = self.profile.xpath('//paths/distros/text()')[0]
except IndexError:
distro_path = 'your "distros" path'
distro = (input('\nWhich distro plugin/distro base are you using? '
'See the manual for more information. A matching '
'plugin MUST exist in {0} for a build to '
'complete successfully! The default (Arch Linux, '
'"archlinux") will be used if left blank.'
'\nDistro base: ').format(
distro_path)).strip()
distro = (input(('\nWhich distro plugin/distro base are you '
'using? See the manual for more information. A '
'matching plugin MUST exist in {0} for a build '
'to complete successfully! The default (Arch '
'Linux, "archlinux") will be used if left blank.'
'\nDistro base: ').format(distro_path))).strip()
if distro == '':
distro = 'archlinux'
if not valid.plugin_name(distro):
print('That is not a valid name. See the manual for examples '
'and shipped plugins. Retrying.')
continue
else:
has_distro = True
distro_elem = lxml.etree.SubElement(build, 'distro')
distro_elem.text = distro
return()
def get_iso(self):
print('\n++ ISO ++')
# We don't need to ask if it's multiarch if we only have one arch.
iso = lxml.etree.Element('iso')
_arches = []
for _source in self.profile.xpath('sources/source'):
_arches.append(_source.attrib['arch'])
if len(_arches) < 2:
iso.attrib['multi_arch'] = _arches[0]
self.profile.append(iso)
# We have more than one arch, so we need to ask how they want to handle
# it.
_ma_strings = {'yes': ('a multi-arch ISO (both architectures on one '
'ISO)'),
'no': ('separate image files for '
'{0}').format(' and '.join(_arches))}
for a in _arches:
_ma_strings[a] = 'only build an image file for {0}'.format(a)
if len(_arches) > 1:
_multi_arch_input = None
while not _multi_arch_input:
print('\n++ ISO || MULTI-ARCH ++')
_multi_arch = (input((
'\nYou have defined mutliple architecture sources. BDisk '
'allows you to build an ISO image (USB image, etc.) that '
'will support both architectures using the same file. '
'Please consult the manual if you need further '
'information.\nPossible values:\n'
'\n\t{0}\n\nMulti-arch: ').format(
'\n\t'.join(
['{0}:\t{1}'.format(k, v) for k, v in _ma_strings.items()]
)))).strip().lower()
if _multi_arch not in _ma_strings.keys():
print('Invalid selection; retrying.')
continue
else:
_multi_arch_input = _multi_arch
iso.attrib['multi_arch'] = _multi_arch_input
_gpg_sign = None
while not _gpg_sign:
print('\n++ ISO || SIGNING ++')
_gpg_input = prompt.confirm_or_no(prompt = (
'\nWe can sign ISO image files using GPG (we\'ll give the '
'option to configure it a bit later).\nWould you like to sign '
'the ISO/USB image files with GPG?\n'), usage = (
'{0} for yes, {1} for no...\n'))
_gpg_sign = ('yes' if _gpg_input else 'no')
iso.attrib['sign'] = _gpg_sign
self.profile.append(iso)
return()
def get_ipxe(self):
print('\n++ iPXE ++')
ipxe = lxml.etree.Element('ipxe')
_ipxe = None
while not _ipxe:
_ipxe = prompt.confirm_or_no(prompt = (
'\nBDisk has built-in support for iPXE (https://ipxe.org/, '
'see the manual for more information). Would you like to '
'build iPXE support?\n'), usage = (
'{0} for yes, {1} for no...\n'))
_ipxe = ('yes' if _ipxe else 'no')
if _ipxe == 'yes':
print('\n++ iPXE || MINI-ISO ++')
_iso = prompt.confirm_or_no(prompt = (
'\nWould you like to build a "mini-ISO" (see the manual) for '
'bootstrapping iPXE booting from USB or optical media?\n'),
usage = ('{0} for yes, {1} for no...\n'))
ipxe.attrib['iso'] = ('yes' if _iso else 'no')
print('\n++ iPXE || SIGNING ++')
_sign = prompt.confirm_or_no(prompt = (
'\nBDisk can sign the mini-ISO and other relevant files for '
'iPXE builds using GPG. Would you like to sign the iPXE build '
'distributables? (You\'ll have the chance to configure GPG '
'later).\n'), usage = ('{0} for yes, {1} for no...\n'))
ipxe.attrib['sign'] = ('yes' if _sign else 'no')
_uri = None
while not _uri:
print('\n++ iPXE || URL ++')
_uri = (input(
'\niPXE uses a remote URI to boot. What URI should this '
'profile\'s iPXE use? (Consult the manual for more '
'information.)\niPXE Boot URL: ')).strip()
if not valid.url(_uri):
print('Invalid URL, retrying...')
_uri = None
continue
else:
uri = lxml.etree.SubElement(ipxe, 'uri')
uri.text = _uri
if _ipxe == 'yes':
self.profile.append(ipxe)
return()
def get_pki(self):
print('\n++ SSL/TLS PKI ++')
pki = lxml.etree.Element('pki')
_pki = None
while not _pki:
_pki = prompt.confirm_or_no(prompt = (
'\nWould you like to support SSL/TLS transport for various '
'functions? Currently this is only used for iPXE, but future '
'applications may be possible.\n'),
usage = ('{0} for yes, {1} for no...\n'))
if _pki:
_pki_url_chk = self.profile.xpath('ipxe/uri/text()')
_pki_url = (_pki_url_chk[0] if _pki_url_chk else None)
print('\n++ SSL/TLS PKI || OVERWRITE ++')
_overwrite = prompt.confirm_or_no(prompt = (
'\nYou\'ll have the opportunity in a moment to configure '
'paths for the various files, but do you want BDisk to '
're-generate (and thus overwrite) any of the files it finds? '
'If you use these files for anything OTHER than BDisk (or '
'wish to keep persistent keys and certs), you should '
'DEFINITELY answer no here.\n'),
usage = ('{0} for yes, {1} for no...\n'))
pki.attrib['overwrite'] = ('yes' if _overwrite else 'no')
for x in ('ca', 'client'):
print('\n++ SSL/TLS PKI || {0} ++'.format(x.upper()))
_x = None
while not _x:
_x = prompt.ssl_object(x, _pki_url)
elem = lxml.etree.SubElement(pki, x)
_elems = {}
for e in _x['paths']:
_elems[e] = lxml.etree.SubElement(elem, e)
_elems[e].text = _x['paths'][e]
for e in _x['attribs']:
for a in _x['attribs'][e]:
_elems[e].attrib[a] = _x['attribs'][e][a]
if _pki:
self.profile.append(pki)
return()

def main():
cg = ConfGenerator()

View File

@ -3,11 +3,11 @@ import crypt
import GPG
import hashid
import hashlib
import iso3166
import os
import pprint
import re
import string
import textwrap
import uuid
import validators
import zlib
@ -17,6 +17,7 @@ from dns import resolver
from email.utils import parseaddr as emailparse
from passlib.context import CryptContext as cryptctx
from urllib.parse import urlparse
from urllib.request import urlopen

# Supported by all versions of GNU/Linux shadow
passlib_schemes = ['des_crypt', 'md5_crypt', 'sha256_crypt', 'sha512_crypt']
@ -26,19 +27,21 @@ digest_schemes = list(hashlib.algorithms_available)
# Provided by zlib
digest_schemes.append('adler32')
digest_schemes.append('crc32')
#clean_digest_schemes = sorted(list(set(digest_schemes)))

crypt_map = {'sha512': crypt.METHOD_SHA512,
'sha256': crypt.METHOD_SHA256,
'md5': crypt.METHOD_MD5,
'des': crypt.METHOD_CRYPT}

class XPathFmt(string.Formatter):
def __init__(self):
print('foo')
# These are *key* ciphers, for encrypting exported keys.
openssl_ciphers = ['aes128', 'aes192', 'aes256', 'bf', 'blowfish',
'camellia128', 'camellia192', 'camellia256', 'cast', 'des',
'des3', 'idea', 'rc2', 'seed']
openssl_digests = ['blake2b512', 'blake2s256', 'gost', 'md4', 'md5', 'mdc2',
'rmd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']

class XPathFmt(string.Formatter):
def get_field(self, field_name, args, kwargs):
# custom arg to specify if it's a regex pattern or not
vals = self.get_value(field_name, args, kwargs), field_name
if not vals[0]:
vals = ('{{{0}}}'.format(vals[1]), vals[1])
@ -51,7 +54,7 @@ class detect(object):
def any_hash(self, hash_str):
h = hashid.HashID()
hashes = []
for i in h.IdentifyHash(hash_str):
for i in h.identifyHash(hash_str):
if i.extended:
continue
x = i.name
@ -74,7 +77,7 @@ class detect(object):
return()

def gpgkeyID_from_url(self, url):
with urlparse(url) as u:
with urlopen(url) as u:
data = u.read()
g = GPG.GPGHandler()
key_ids = g.get_sigs(data)
@ -85,7 +88,7 @@ class detect(object):
def _get_key():
key = None
try:
key = g.get_key(keyID, secret = secret)
key = g.ctx.get_key(keyID, secret = secret)
except GPG.gpg.errors.KeyNotFound:
return(None)
except Exception:
@ -93,16 +96,16 @@ class detect(object):
return(key)
uids = {}
g = GPG.GPGHandler()
_orig_kl_mode = g.get_keylist_mode()
_orig_kl_mode = g.ctx.get_keylist_mode()
if _orig_kl_mode != GPG.gpg.constants.KEYLIST_MODE_EXTERN:
_key = _get_key()
if not _key:
g.set_keylist_mode(GPG.gpg.constants.KEYLIST_MODE_EXTERN)
g.ctx.set_keylist_mode(GPG.gpg.constants.KEYLIST_MODE_EXTERN)
_key = _get_key()
else:
_key = _get_key()
if not _key:
g.set_keylist_mode(_orig_kl_mode)
g.ctx.set_keylist_mode(_orig_kl_mode)
del(g)
return(None)
else:
@ -119,7 +122,7 @@ class detect(object):
_u['Invalid'] = (True if _uid.invalid else False)
_u['Revoked'] = (True if _uid.revoked else False)
uids['User IDs'].append(_u)
g.set_keylist_mode(_orig_kl_mode)
g.ctx.set_keylist_mode(_orig_kl_mode)
del(g)
return(uids)

@ -163,7 +166,7 @@ class prompts(object):

def confirm_or_no(self, prompt = '', invert = False,
usage = '{0} to confirm, otherwise {1}...\n'):
# A simplified version of multiline_input, really.
# A simplified version of multiline_input(), really.
# By default, Enter confirms (and returns True) and CTRL-d returns
# False unless - you guessed it - invert is True.
# usage is a string appended to prompt that explains which keys to use.
@ -224,11 +227,135 @@ class prompts(object):
print(end_str)
return('\n'.join(_lines))

def path(self, path_desc):
def path(self, path_desc, empty_passthru = False):
path = input(('\nWhere would you like to put {0}?\n'
'Path: ').format(path_desc))
if empty_passthru:
if path.strip() == '':
return('')
path = transform().full_path(path)
return(path)
def ssl_object(self, pki_role, cn_url):
ssl_vals = {'paths': {},
'attribs': {},
'subject': {}}
# pki_role should be 'ca' or 'client'
if pki_role not in ('ca', 'client'):
raise ValueError('pki_role must be either "ca" or "client"')
_attribs = {'cert': {'hash_algo': {'text': ('What hashing algorithm '
'do you want to use? (Default is sha512.)'),
'prompt': 'Hashing algorithm: ',
'options': openssl_digests,
'default': 'sha512'}},
'key': {'cipher': {'text': ('What encryption algorithm/'
'cipher do you want to use? (Default is '
'aes256.)'),
'prompt': 'Cipher: ',
'options': openssl_ciphers,
'default': 'aes256'},
# This can actually theoretically be anywhere from
# 512 to... who knows how high. I couldn't find the
# upper bound. So we just set it to sensible
# defaults. If they want something higher, they can
# edit the XML when they're done.
'keysize': {'text': ('What keysize/length (in '
'bits) do you want the key to be? (Default is '
'4096; much higher values are possible but '
'are untested and thus not supported by this '
'tool; feel free to edit the generated '
'configuration by hand.)'),
'prompt': 'Keysize: ',
'options': ['1024', '2048', '4096'],
'default': '4096'}}}
_paths = {'cert': '(or read from) the certificate',
'key': '(or read from) the key',
'csr': ('(or read from) the certificate signing request (if '
'blank, we won\'t write to disk - the operation '
'will occur entirely in memory assuming we need to '
'generate/sign)')}
if pki_role == 'ca':
_paths['index'] = ('(or read from) the CA DB index file (if left '
'blank, one will not be used)')
_paths['serial'] = ('(or read from) the CA DB serial file (if '
'left blank, one will not be used)')
for a in _attribs:
ssl_vals['attribs'][a] = {}
for x in _attribs[a]:
ssl_vals['attribs'][a][x] = None
for p in _paths:
if p == 'csr':
_allow_empty = True
else:
_allow_empty = False
ssl_vals['paths'][p] = self.path(_paths[p],
empty_passthru = _allow_empty)
print()
if ssl_vals['paths'][p] == '':
ssl_vals['paths'][p] = None
if p in _attribs:
for x in _attribs[p]:
while not ssl_vals['attribs'][p][x]:
ssl_vals['attribs'][p][x] = (input(
('\n{0}\n\n\t{1}\n\n{2}').format(
_attribs[p][x]['text'],
'\n\t'.join(_attribs[p][x]['options']),
_attribs[p][x]['prompt'])
)).strip().lower()
if ssl_vals['attribs'][p][x] not in \
_attribs[p][x]['options']:
print(('\nInvalid selection; setting default '
'({0}).').format(_attribs[p][x]['default']))
ssl_vals['attribs'][p][x] = \
_attribs[p][x]['default']
_subject = {'countryName': {'text': ('the 2-letter country '
'abbreviation (must conform to '
'ISO3166 ALPHA-2)?\nCountry '
'code: '),
'check': 'func',
'func': valid().country_abbrev},
'localityName': {'text': ('the city/town/borough/locality '
'name?\nLocality: '),
'check': None},
'stateOrProvinceName': {'text': ('the state/region '
'name (full string)?'
'\nRegion: '),
'check': None},
'organization': {'text': ('your organization\'s name?'
'\nOrganization: '),
'check': None},
'organizationalUnitName': {'text': ('your department/role/'
'team/department name?'
'\nOrganizational '
'Unit: '),
'check': None},
'emailAddress': {'text': ('the email address to be '
'associated with this '
'certificate/PKI object?\n'
'Email: '),
'check': 'func',
'func': valid().email}}
for s in _subject:
ssl_vals['subject'][s] = None
for s in _subject:
while not ssl_vals['subject'][s]:
_input = (input(
('\nWhat is {0}').format(_subject[s]['text'])
)).strip()
_chk = _subject[s]['check']
if _chk:
if _chk == 'func':
_chk = _subject[s]['func'](_input)
if not _chk:
print('Invalid value; retrying.')
continue
print()
ssl_vals['subject'][s] = _input
_url = transform().url_to_dict(cn_url, no_None = True)
ssl_vals['subject']['commonName'] = _url['host']
if pki_role == 'client':
ssl_vals['subject']['commonName'] += ' (Client)'
return(ssl_vals)

class transform(object):
def __init__(self):
@ -280,6 +407,7 @@ class transform(object):
text_out = re.sub('[^\w]', '', text_out)
return(text_out)

# noinspection PyDictCreation
def url_to_dict(self, orig_url, no_None = False):
def _getuserinfo(uinfo_str):
if len(uinfo_str) == 0:
@ -332,25 +460,25 @@ class transform(object):
return(None)
else:
return('')
params = {}
_params = {}
for i in in_str.split(split_char):
p = [x.strip() for x in i.split('=')]
params[p[0]] = p[1]
if not params:
_params[p[0]] = p[1]
if not _params:
if not no_None:
return(None)
else:
return('')
if not params and not no_None:
if not _params and not no_None:
return(None)
return(params)
return(_params)
_dflt_ports = _getdfltport()
scheme = None
_scheme_re = re.compile('^([\w+\.-]+)(://.*)', re.IGNORECASE)
_scheme_re = re.compile('^([\w+.-]+)(://.*)', re.IGNORECASE)
if not _scheme_re.search(orig_url):
# They probably didn't prefix a URI signifier (RFC3986 § 3.1).
# We'll add one for them.
url = 'http://' + url
url = 'http://' + orig_url
scheme = 'http'
else:
# urlparse's .scheme? Total trash.
@ -407,7 +535,7 @@ class transform(object):
'url': orig_url}
url['full_url'] = '{0}://'.format(scheme)
if userinfo not in (None, ''):
url['full_url'] += '{user}:{password}@'.format(userinfo)
url['full_url'] += '{user}:{password}@'.format(**userinfo)
url['full_url'] += host
if port not in (None, ''):
url['full_url'] += ':{0}'.format(port)
@ -424,145 +552,15 @@ class transform(object):
url['full_url'] += '#{0}'.format('#'.join(_f))
return(url)

class xml_supplicant(object):
def __init__(self, cfg, profile = None, max_recurse = 5):
raw = self._detect_cfg(cfg)
xmlroot = lxml.etree.fromstring(raw)
self.btags = {'xpath': {},
'regex': {}}
self.fmt = XPathFmt()
self.max_recurse = max_recurse
#self.ptrn = re.compile('(?<=(?<!\{)\{)[^{}]*(?=\}(?!\}))')
# I don't have permission to credit them, but to the person who helped
# me with this regex - thank you. You know who you are.
self.ptrn = re.compile(('(?<=(?<!\{)\{)(?:[^{}]+'
'|{{[^{}]*}})*(?=\}(?!\}))'))
self.root = lxml.etree.ElementTree(xmlroot)
if not profile:
self.profile = xmlroot.xpath('/bdisk/profile[1]')[0]
else:
self.profile = xmlroot.xpath(profile)[0]
def _detect_cfg(self, cfg):
if isinstance(cfg, str):
try:
lxml.etree.fromstring(cfg.encode('utf-8'))
return(cfg.encode('utf-8'))
except lxml.etree.XMLSyntaxError:
path = os.path.abspath(os.path.expanduser(cfg))
try:
with open(path, 'rb') as f:
cfg = f.read()
except FileNotFoundError:
raise ValueError('Could not open {0}'.format(path))
elif isinstance(cfg, _io.TextIOWrapper):
_cfg = cfg.read().encode('utf-8')
cfg.close()
cfg = _cfg
elif isinstance(cfg, _io.BufferedReader):
_cfg = cfg.read()
cfg.close()
cfg = _cfg
elif isinstance(cfg, lxml.etree._Element):
return(lxml.etree.tostring(cfg))
elif isinstance(cfg, bytes):
return(cfg)
else:
raise TypeError('Could not determine the object type.')
return(cfg)

def get_path(self, element):
path = element
try:
path = self.root.getpath(element)
except ValueError:
raise ValueError(
(
'Could not find a path for the expression {0}'
).format(element.text))
return(path)

def substitute(self, element, recurse_count = 0):
if recurse_count >= self.max_recurse:
return(element)
if isinstance(element, lxml.etree._Element):
if isinstance(element, lxml.etree._Comment):
return(element)
# if len(element) == 0:
# print(element.text)
if element.text:
_dictmap = self.xpath_to_dict(element.text)
while _dictmap:
for elem in _dictmap:
if isinstance(_dictmap[elem], str):
try:
newpath = element.xpath(_dictmap[elem])
except (AttributeError, IndexError, TypeError):
newpath = element
except lxml.etree.XPathEvalError:
return(element)
try:
self.btags['xpath'][elem] = self.substitute(
newpath, (recurse_count + 1))[0]
except (IndexError, TypeError):
raise ValueError(
('Encountered an error while trying to '
'substitute {0} at {1}').format(
elem, self.get_path(element)
))
print(element.text)
element.text = self.fmt.format(
element.text,
{**self.btags['xpath'],
**self.btags['regex']})
# element.text = self.fmt.vformat(
# element.text,
# [],
# {**self.btags['xpath'],
# **self.btags['regex']})
# element.text = (element.text).format(
# {**self.btags['xpath'],
# **self.btags['regex']})
_dictmap = self.xpath_to_dict(element.text)
return(element)

def xpath_selector(self, selectors,
selector_ids = ('id', 'name', 'uuid')):
# selectors is a dict of {attrib:value}
xpath = ''
for i in selectors.items():
if i[1] and i[0] in selector_ids:
xpath += '[@{0}="{1}"]'.format(*i)
return(xpath)

def xpath_to_dict(self, text_in):
d = None
ptrn_id = self.ptrn.findall(text_in)
if len(ptrn_id) >= 1:
for item in ptrn_id:
if not isinstance(d, dict):
d = {}
try:
_, xpath_expr = item.split('%', 1)
if _ not in self.btags:
continue
if item not in self.btags[_]:
self.btags[_][item] = None
if _ == 'regex':
_re = re.sub('^regex%', '', item)
_re = re.sub('{{(.*)}}', '\g<1>', _re)
# We use a native python object
self.btags['regex'][item] = re.compile(_re)
d[item] = xpath_expr
except ValueError:
return(None)
return(d)


class valid(object):
def __init__(self):
pass

def country_abbrev(self, country_code):
if country_code not in iso3166.countries_by_alpha2:
return(False)
return(True)

def dns(self, addr):
pass

@ -572,7 +570,7 @@ class valid(object):

def email(self, addr):
return(
isinstance(validators.email(emailparse(addr)[1]),
not isinstance(validators.email(emailparse(addr)[1]),
validators.utils.ValidationFailure))

def gpgkeyID(self, key_id):
@ -626,9 +624,10 @@ class valid(object):

def salt_hash(self, salthash):
_idents = ''.join([i.ident for i in crypt_map if i.ident])
_regex = re.compile('^(\$[{0}]\$)?[./0-9A-Za-z]{0,16}\$?'.format(
# noinspection PyStringFormat
_regex = re.compile('^(\$[{0}]\$)?[./0-9A-Za-z]{{0,16}}\$?'.format(
_idents))
if not regex.search(salthash):
if not _regex.search(salthash):
return(False)
return(True)

@ -650,7 +649,7 @@ class valid(object):
return(True)

def url(self, url):
if not re.search('^[\w+\.-]+://', url):
if not re.search('^[\w+.-]+://', url):
# They probably didn't prefix a URI signifier (RFC3986 § 3.1).
# We'll add one for them.
url = 'http://' + url
@ -670,9 +669,152 @@ class valid(object):
def uuid(self, uuid_str):
is_uuid = True
try:
u = uuid.UUID(uuid_in)
u = uuid.UUID(uuid_str)
except ValueError:
return(False)
if not uuid_in == str(u):
if not uuid_str == str(u):
return(False)
return(is_uuid)

class xml_supplicant(object):
def __init__(self, cfg, profile = None, max_recurse = 5):
raw = self._detect_cfg(cfg)
xmlroot = lxml.etree.fromstring(raw)
self.btags = {'xpath': {},
'regex': {},
'variable': {}}
self.fmt = XPathFmt()
self.max_recurse = max_recurse
# I don't have permission to credit them, but to the person who helped
# me with this regex - thank you. You know who you are.
self.ptrn = re.compile(('(?<=(?<!\{)\{)(?:[^{}]+'
'|{{[^{}]*}})*(?=\}(?!\}))'))
self.root = lxml.etree.ElementTree(xmlroot)
if not profile:
self.profile = xmlroot.xpath('/bdisk/profile[1]')[0]
else:
self.profile = xmlroot.xpath(profile)[0]
self._parse_regexes()
self._parse_variables()
def _detect_cfg(self, cfg):
if isinstance(cfg, str):
try:
lxml.etree.fromstring(cfg.encode('utf-8'))
return(cfg.encode('utf-8'))
except lxml.etree.XMLSyntaxError:
path = os.path.abspath(os.path.expanduser(cfg))
try:
with open(path, 'rb') as f:
cfg = f.read()
except FileNotFoundError:
raise ValueError('Could not open {0}'.format(path))
elif isinstance(cfg, _io.TextIOWrapper):
_cfg = cfg.read().encode('utf-8')
cfg.close()
cfg = _cfg
elif isinstance(cfg, _io.BufferedReader):
_cfg = cfg.read()
cfg.close()
cfg = _cfg
elif isinstance(cfg, lxml.etree._Element):
return(lxml.etree.tostring(cfg))
elif isinstance(cfg, bytes):
return(cfg)
else:
raise TypeError('Could not determine the object type.')
return(cfg)
def _parse_regexes(self):
for regex in self.profile.xpath('//meta/regexes/pattern'):
self.btags['regex'][regex.attrib['id']] = re.compile(regex.text)
return()

def _parse_variables(self):
for variable in self.profile.xpath('//meta/variables/variable'):
self.btags['variable'][
'variable%{0}'.format(variable.attrib['id'])
] = variable.text
return()

def get_path(self, element):
path = element
try:
path = self.root.getpath(element)
except ValueError:
raise ValueError(
(
'Could not find a path for the expression {0}'
).format(element.text))
return(path)

def substitute(self, element, recurse_count = 0):
if recurse_count >= self.max_recurse:
return(element)
if isinstance(element, lxml.etree._Element):
if element.tag == 'regex':
return(element)
if isinstance(element, lxml.etree._Comment):
return(element)
if element.text:
_dictmap = self.btags_to_dict(element.text)
for elem in _dictmap:
# This is needed because _dictmap gets replaced below
if not _dictmap:
return(element)
_btag, _value = _dictmap[elem]
if isinstance(_value, str):
if _btag == 'xpath':
try:
newpath = element.xpath(_dictmap[elem][1])
except (AttributeError, IndexError, TypeError):
newpath = element
except lxml.etree.XPathEvalError:
return(element)
try:
self.btags['xpath'][elem] = self.substitute(
newpath, (recurse_count + 1))[0]
except (IndexError, TypeError):
raise ValueError(
('Encountered an error while trying to '
'substitute {0} at {1}').format(
elem, self.get_path(element)
))
element.text = self.fmt.vformat(
element.text,
[],
{**self.btags['xpath'],
**self.btags['variable']})
_dictmap = self.btags_to_dict(element.text)
return(element)

def xpath_selector(self, selectors,
selector_ids = ('id', 'name', 'uuid')):
# selectors is a dict of {attrib:value}
xpath = ''
for i in selectors.items():
if i[1] and i[0] in selector_ids:
xpath += '[@{0}="{1}"]'.format(*i)
return(xpath)

def btags_to_dict(self, text_in):
d = {}
ptrn_id = self.ptrn.findall(text_in)
if len(ptrn_id) >= 1:
for item in ptrn_id:
try:
btag, expr = item.split('%', 1)
if btag not in self.btags:
continue
if item not in self.btags[btag]:
self.btags[btag][item] = None
#self.btags[btag][item] = expr # remove me?
if btag == 'xpath':
d[item] = (btag, expr)
elif btag == 'variable':
d[item] = (btag, self.btags['variable'][item])
except ValueError:
return(d)
return(d)



View File

@ -1,233 +1,266 @@
<?xml version='1.0' encoding='UTF-8'?>
<bdisk>
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
<meta>
<names>
<name>BDisk</name>
<uxname>bdisk</uxname>
<!-- Just like with previous versions of BDisk, you can reference other values...
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
<meta>
<names>
<name>BDisk</name>
<uxname>bdisk</uxname>
<!-- Just like with previous versions of BDisk, you can reference other values...
but now with the neat benefits of XPath! Everything you could do in build.ini's and more.
See https://www.w3schools.com/xml/xpath_syntax.asp
If you need a literal curly brace, double them (e.g. for "{foo}", use "{{foo}}"),
UNLESS it's in a {regex%...} placeholder/filter (as part of the expression). -->
<pname>{xpath%../name/text()}</pname>
</names>
<desc>A rescue/restore live environment.</desc>
<dev>
<author>A. Dev Eloper</author>
<email>dev@domain.tld</email>
<website>https://domain.tld/~dev</website>
</dev>
<uri>https://domain.tld/projname</uri>
<ver>1.0.0</ver>
<!-- This is the VERY FIRST value parsed, and is required. It controls how many levels of {xpath%...} to recurse. -->
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
<max_recurse>5</max_recurse>
</meta>
<accounts>
<!-- Salted/hashed password is "test" -->
<rootpass hashed="yes">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
<user sudo="yes">
<username>{xpath%//meta/names/uxname/text()}</username>
<!-- You can also use substitution from different profiles: -->
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
<comment>{xpath%//meta/dev/author/text()}</comment>
<password hashed="no" hash_algo="sha512" salt="auto">testpassword</password>
</user>
<user sudo="no">
<username>testuser</username>
<name>Test User</name>
<password hashed="no" hash_algo="sha512" salt="auto">anothertestpassword</password>
</user>
</accounts>
<sources>
<source arch="x86_64">
<mirror>http://archlinux.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flags="regex,latest">{xpath%../mirror/text()}{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz}</tarball>
<checksum hash_algo="sha1" flags="none">{xpath%../mirror/text()}{xpath%../webroot/text()}/sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="latest">{xpath%../tarball/text()}.sig</sig>
</source>
<source arch="i686">
<mirror>http://archlinux32.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flag="regex,latest">{xpath%../mirror/text()}/{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz}</tarball>
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net">{xpath%../tarball/text()}.sig</sig>
</source>
</sources>
<build its_full_of_stars="yes">
<paths>
<cache>/var/tmp/{xpath%//meta/names/uxname/text()}</cache>
<chroot>/var/tmp/chroots/{xpath%//meta/names/uxname/text()}</chroot>
<overlay>{xpath%../cache/text()}/overlay</overlay>
<templates>~/{xpath%//meta/names/uxname/text()}/templates</templates>
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
<distros>~/{xpath%//meta/names/uxname/text()}/distros</distros>
<dest>~/{xpath%//meta/names/uxname/text()}/results</dest>
<iso>{xpath%../dest/text()}/iso</iso>
<http>{xpath%../dest/text()}/http</http>
<tftp>{xpath%../dest/text()}/tftp</tftp>
<pki>{xpath%../dest/text()}/pki</pki>
</paths>
<basedistro>archlinux</basedistro>
</build>
<iso sign="yes" multiarch="yes"/>
<ipxe sign="yes" iso="yes">
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
</ipxe>
<pki overwrite="no">
<!-- http://ipxe.org/crypto -->
<ca>
<cert>{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<!-- If csr is self-enclosed (<csr />), we'll just generate and use a CSR in-memory.
<pname>{xpath%../name/text()}</pname>
</names>
<desc>A rescue/restore live environment.</desc>
<dev>
<author>A. Dev Eloper</author>
<email>dev@domain.tld</email>
<website>https://domain.tld/~dev</website>
</dev>
<uri>https://domain.tld/projname</uri>
<ver>1.0.0</ver>
<!-- This is the VERY FIRST value parsed, and is required. It controls how many levels of {xpath%...} to recurse. -->
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
<max_recurse>5</max_recurse>
<!-- You need to store regex patterns here and reference them in a special way later, and it's only valid for certain
items. See the manual for more information. -->
<regexes>
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
<pattern id="sig_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz\.sig$</pattern>
<pattern id="tarball_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz$</pattern>
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
</regexes>
<!-- You can also define variables. NO xpath or regex btags, and they can't be used within other btags! -->
<variables>
<variable id="bdisk_root">/var/tmp/BDisk</variable>
</variables>
</meta>
<accounts>
<!-- Salted/hashed password is "test" -->
<rootpass hashed="yes">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
<user sudo="yes">
<username>{xpath%//meta/names/uxname/text()}</username>
<!-- You can also use substitution from different profiles in this same configuration: -->
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
<comment>{xpath%//meta/dev/author/text()}</comment>
<password hashed="no" hash_algo="sha512" salt="auto">testpassword</password>
</user>
<user sudo="no">
<username>testuser</username>
<name>Test User</name>
<password hashed="no" hash_algo="sha512" salt="auto">anothertestpassword</password>
</user>
</accounts>
<sources>
<source arch="x86_64">
<mirror>http://archlinux.mirror.domain.tld</mirror>
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
<checksum hash_algo="sha1" flags="none">sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_x86_64}</sig>
</source>
<source arch="i686">
<mirror>http://archlinux32.mirror.domain.tld</mirror>
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_i686}</sig>
</source>
</sources>
<build its_full_of_stars="yes">
<paths>
<cache>{variable%bdisk_root}/cache</cache>
<chroot>{variable%bdisk_root}/chroots</chroot>
<overlay>{variable%bdisk_root}/overlay</overlay>
<templates>{variable%bdisk_root}/templates</templates>
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
<distros>{variable%bdisk_root}/distros</distros>
<dest>{variable%bdisk_root}/results</dest>
<iso>{variable%bdisk_root}/iso_overlay</iso>
<http>{variable%bdisk_root}/http</http>
<tftp>{variable%bdisk_root}/tftp</tftp>
<pki>{variable%bdisk_root}/pki</pki>
</paths>
<basedistro>archlinux</basedistro>
</build>
<iso sign="yes" multi_arch="yes"/>
<ipxe sign="yes" iso="yes">
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
</ipxe>
<pki overwrite="no">
<!-- http://ipxe.org/crypto -->
<ca>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<!-- If csr is self-enclosed (<csr />), we'll just generate and use a CSR in-memory.
Assuming we need to generate a certificate, anyways.
If you want to write it out to disk (for debugging, etc.) OR use one already generated,
then provide a path.
e.g.:
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
<csr/>
<key des="no" passphrase="none">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<subject>
<commonName>domain.tld</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</ca>
<client>
<cert>{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<csr/>
<key des="no" passphrase="none">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<subject>
<commonName>some client name</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</client>
</pki>
<gpg keyid="none" gnupghome="none" publish="no" sync="yes"/>
<sync>
<ipxe enabled="yes" rsync="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
<tftp enabled="yes" rsync="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
<iso enabled="yes" rsync="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
<rsync enabled="yes">
<user>root</user>
<host>mirror.domain.tld</host>
<port>22</port>
<pubkey>~/.ssh/id_ed25519</pubkey>
</rsync>
</sync>
</profile>
<profile name="alternate" id="2" uuid="2ed07c19-2071-4d66-8569-da40475ba716">
<meta>
<names>
<name>AnotherCD</name>
<uxname>bdisk_alt</uxname>
<pname>{xpath%../name/text()}</pname>
</names>
<desc>Another rescue/restore live environment.</desc>
<dev>
<author>Another Dev Eloper</author>
<email>{xpath%//profile[@name="default"]/meta/dev/email/text()}</email>
<website>{xpath%//profile[@name="default"]/meta/dev/website/text()}</website>
</dev>
<uri>https://domain.tld/projname</uri>
<ver>0.0.1</ver>
<max_recurse>5</max_recurse>
</meta>
<accounts>
<rootpass hashed="no">atotallyinsecurepassword</rootpass>
<user sudo="no">
<username>testuser</username>
<comment>Test User</comment>
<password hashed="no" hash_algo="sha512" salt="auto">atestpassword</password>
</user>
</accounts>
<sources>
<source arch="x86_64">
<mirror>http://archlinux.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flags="regex,latest">{xpath%../mirror/text()}{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz}</tarball>
<checksum hash_algo="sha1" flags="none">{xpath%../mirror/text()}{xpath%../webroot/text()}/sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="latest">{xpath%../tarball/text()}.sig</sig>
</source>
<source arch="i686">
<mirror>http://archlinux32.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flag="regex,latest">{xpath%../mirror/text()}/{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz}</tarball>
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net">{xpath%../tarball/text()}.sig</sig>
</source>
</sources>
<build its_full_of_stars="yes">
<paths>
<cache>/var/tmp/{xpath%//meta/names/uxname/text()}</cache>
<chroot>/var/tmp/chroots/{xpath%//meta/names/uxname/text()}</chroot>
<overlay>{xpath%../cache/text()}/overlay</overlay>
<templates>~/{xpath%//meta/names/uxname/text()}/templates</templates>
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
<distros>~/{xpath%//meta/names/uxname/text()}/distros</distros>
<dest>~/{xpath%//meta/names/uxname/text()}/results</dest>
<iso>{xpath%../dest/text()}/iso</iso>
<http>{xpath%../dest/text()}/http</http>
<tftp>{xpath%../dest/text()}/tftp</tftp>
<pki>{xpath%../dest/text()}/pki</pki>
</paths>
<basedistro>archlinux</basedistro>
</build>
<iso sign="yes" multiarch="yes"/>
<ipxe sign="yes" iso="yes">
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
</ipxe>
<pki overwrite="no">
<ca>
<cert>{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<csr/>
<key des="no" passphrase="none">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<subject>
<commonName>domain.tld</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</ca>
<client>
<cert>{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<csr/>
<key des="no" passphrase="none">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<subject>
<commonName>some client name</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</client>
</pki>
<gpg keyid="none" gnupghome="none" publish="no" sync="yes"/>
<sync>
<ipxe enabled="yes" rsync="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
<tftp enabled="yes" rsync="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
<iso enabled="yes" rsync="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
<rsync enabled="yes">
<user>root</user>
<host>mirror.domain.tld</host>
<port>22</port>
<pubkey>~/.ssh/id_ed25519</pubkey>
</rsync>
</sync>
</profile>
<csr/>
<!-- If you use an index file (or want to) to serialize client certificates, specify it here. -->
<!-- It must conform to CADB spec (https://pki-tutorial.readthedocs.io/en/latest/cadb.html). -->
<!-- You should probably also specify a serial file if so. -->
<!-- Both of these are entirely optional if you aren't using an existing PKI. -->
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
<!-- If you specify a cipher, the key will be encrypted to the passphrase provided by the passphrase attribute.
If the key is encrypted (either a pre-existing or a created one) but passphrase is not provided, you will
be (securely) prompted for the passphrase to unlock it/add a passphrase to it. -->
<key cipher="none" passphrase="none" keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<subject>
<commonName>domain.tld</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</ca>
<client>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<csr/>
<key cipher="none" passphrase="none" keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<subject>
<commonName>some client name</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</client>
</pki>
<gpg keyid="none" gnupghome="none" publish="no" sync="yes"/>
<sync>
<ipxe enabled="yes" rsync="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
<tftp enabled="yes" rsync="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
<iso enabled="yes" rsync="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
<rsync enabled="yes">
<user>root</user>
<host>mirror.domain.tld</host>
<port>22</port>
<pubkey>~/.ssh/id_ed25519</pubkey>
</rsync>
</sync>
</profile>
<profile name="alternate" id="2" uuid="2ed07c19-2071-4d66-8569-da40475ba716">
<meta>
<names>
<name>AnotherCD</name>
<uxname>bdisk_alt</uxname>
<pname>{xpath%../name/text()}</pname>
</names>
<desc>Another rescue/restore live environment.</desc>
<dev>
<author>Another Dev Eloper</author>
<!-- You can reference other profiles within the same configuration. -->
<email>{xpath%//profile[@name="default"]/meta/dev/email/text()}</email>
<website>{xpath%//profile[@name="default"]/meta/dev/website/text()}</website>
</dev>
<uri>https://domain.tld/projname</uri>
<ver>0.0.1</ver>
<max_recurse>5</max_recurse>
<regexes>
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
<pattern id="sig_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz\.sig$</pattern>
<pattern id="tarball_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz$</pattern>
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
</regexes>
<variables>
<variable id="bdisk_root">/var/tmp/BDisk</variable>
</variables>
</meta>
<accounts>
<rootpass hashed="no">atotallyinsecurepassword</rootpass>
<user sudo="no">
<username>testuser</username>
<comment>Test User</comment>
<password hashed="no" hash_algo="sha512" salt="auto">atestpassword</password>
</user>
</accounts>
<sources>
<source arch="x86_64">
<mirror>http://archlinux.mirror.domain.tld</mirror>
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
<checksum hash_algo="sha1" flags="none">sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_x86_64}</sig>
</source>
<source arch="i686">
<mirror>http://archlinux32.mirror.domain.tld</mirror>
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_i686}</sig>
</source>
</sources>
<build its_full_of_stars="yes">
<paths>
<cache>{variable%bdisk_root}/cache</cache>
<chroot>{variable%bdisk_root}/chroots</chroot>
<overlay>{variable%bdisk_root}/overlay</overlay>
<templates>{variable%bdisk_root}/templates</templates>
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
<distros>{variable%bdisk_root}/distros</distros>
<dest>{variable%bdisk_root}/results</dest>
<iso>{variable%bdisk_root}/iso_overlay</iso>
<http>{variable%bdisk_root}/http</http>
<tftp>{variable%bdisk_root}/tftp</tftp>
<pki>{variable%bdisk_root}/pki</pki>
</paths>
<basedistro>archlinux</basedistro>
</build>
<iso sign="yes" multi_arch="yes"/>
<ipxe sign="yes" iso="yes">
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
</ipxe>
<pki overwrite="no">
<ca>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<csr/>
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
<key cipher="none" passphrase="none" keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<subject>
<commonName>domain.tld</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</ca>
<client>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<csr/>
<key cipher="none" passphrase="none" keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<subject>
<commonName>some client name</commonName>
<countryName>XX</countryName>
<localityName>Some City</localityName>
<stateOrProvinceName>Some State</stateOrProvinceName>
<organization>Some Org, Inc.</organization>
<organizationalUnitName>Department Name</organizationalUnitName>
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
</subject>
</client>
</pki>
<gpg keyid="none" gnupghome="none" publish="no" sync="yes"/>
<sync>
<ipxe enabled="yes" rsync="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
<tftp enabled="yes" rsync="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
<iso enabled="yes" rsync="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
<rsync enabled="yes">
<user>root</user>
<host>mirror.domain.tld</host>
<port>22</port>
<pubkey>~/.ssh/id_ed25519</pubkey>
</rsync>
</sync>
</profile>
</bdisk>

View File

@ -3,8 +3,10 @@
import copy
from lxml import etree

parser = etree.XMLParser(remove_blank_text = True)

with open('single_profile.xml', 'rb') as f:
xml = etree.fromstring(f.read())
xml = etree.fromstring(f.read(), parser)

single_profile = xml.xpath('/bdisk/profile[1]')[0]
alt_profile = copy.deepcopy(single_profile)
@ -45,6 +47,9 @@ for e in accounts.iter():
e.attrib['sudo'] = 'no'
# Delete the second user
accounts.remove(accounts[2])
author = alt_profile.xpath('/profile/meta/dev/author')[0]
author.addnext(etree.Comment(
' You can reference other profiles within the same configuration. '))
xml.append(alt_profile)

with open('multi_profile.xml', 'wb') as f:

View File

@ -23,13 +23,25 @@
<!-- This is the VERY FIRST value parsed, and is required. It controls how many levels of {xpath%...} to recurse. -->
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
<max_recurse>5</max_recurse>
<!-- You need to store regex patterns here and reference them in a special way later, and it's only valid for certain
items. See the manual for more information. -->
<regexes>
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
<pattern id="sig_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz\.sig$</pattern>
<pattern id="tarball_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz$</pattern>
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
</regexes>
<!-- You can also define variables. NO xpath or regex btags, and they can't be used within other btags! -->
<variables>
<variable id="bdisk_root">/var/tmp/BDisk</variable>
</variables>
</meta>
<accounts>
<!-- Salted/hashed password is "test" -->
<rootpass hashed="yes">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
<user sudo="yes">
<username>{xpath%//meta/names/uxname/text()}</username>
<!-- You can also use substitution from different profiles: -->
<!-- You can also use substitution from different profiles in this same configuration: -->
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
<comment>{xpath%//meta/dev/author/text()}</comment>
<password hashed="no"
@ -47,48 +59,47 @@
<sources>
<source arch="x86_64">
<mirror>http://archlinux.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flags="regex,latest">{xpath%../mirror/text()}{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{{4}}\.[0-9]{{2}}\.[0-9]{{2}}-x86_64\.tar\.gz}</tarball>
<!-- <tarball flags="regex,latest">{xpath%../mirror/text()}{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz}</tarball> -->
<checksum hash_algo="sha1" flags="none" >{xpath%../mirror/text()}{xpath%../webroot/text()}/sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC"
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
<checksum hash_algo="sha1" flags="none">sha1sums.txt</checksum>
<sig keys="7F2D434B9741E8AC"
keyserver="hkp://pool.sks-keyservers.net"
flags="latest">{xpath%../tarball/text()}.sig</sig>
flags="regex,latest">{regex%sig_x86_64}</sig>
</source>
<source arch="i686">
<mirror>http://archlinux32.mirror.domain.tld</mirror>
<webroot>/iso/latest</webroot>
<tarball flag="regex,latest">{xpath%../mirror/text()}/{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{{4}}\.[0-9]{{2}}\.[0-9]{{2}}-i686\.tar\.gz}</tarball>
<!-- <tarball flag="regex,latest">{xpath%../mirror/text()}/{xpath%../webroot/text()}/{regex%archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz}</tarball> -->
<rootpath>/iso/latest</rootpath>
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506"
keyserver="hkp://pool.sks-keyservers.net">{xpath%../tarball/text()}.sig</sig>
keyserver="hkp://pool.sks-keyservers.net"
flags="regex,latest">{regex%sig_i686}</sig>
</source>
</sources>
<build its_full_of_stars="yes">
<paths>
<cache>/var/tmp/{xpath%//meta/names/uxname/text()}</cache>
<chroot>/var/tmp/chroots/{xpath%//meta/names/uxname/text()}</chroot>
<overlay>{xpath%../cache/text()}/overlay</overlay>
<templates>~/{xpath%//meta/names/uxname/text()}/templates</templates>
<cache>{variable%bdisk_root}/cache</cache>
<chroot>{variable%bdisk_root}/chroots</chroot>
<overlay>{variable%bdisk_root}/overlay</overlay>
<templates>{variable%bdisk_root}/templates</templates>
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
<distros>~/{xpath%//meta/names/uxname/text()}/distros</distros>
<dest>~/{xpath%//meta/names/uxname/text()}/results</dest>
<iso>{xpath%../dest/text()}/iso</iso>
<http>{xpath%../dest/text()}/http</http>
<tftp>{xpath%../dest/text()}/tftp</tftp>
<pki>{xpath%../dest/text()}/pki</pki>
<distros>{variable%bdisk_root}/distros</distros>
<dest>{variable%bdisk_root}/results</dest>
<iso>{variable%bdisk_root}/iso_overlay</iso>
<http>{variable%bdisk_root}/http</http>
<tftp>{variable%bdisk_root}/tftp</tftp>
<pki>{variable%bdisk_root}/pki</pki>
</paths>
<basedistro>archlinux</basedistro>
</build>
<iso sign="yes" multiarch="yes" />
<iso sign="yes" multi_arch="yes" />
<ipxe sign="yes" iso="yes">
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
</ipxe>
<pki overwrite="no">
<!-- http://ipxe.org/crypto -->
<ca>
<cert>{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
<!-- If csr is self-enclosed (<csr />), we'll just generate and use a CSR in-memory.
Assuming we need to generate a certificate, anyways.
If you want to write it out to disk (for debugging, etc.) OR use one already generated,
@ -96,7 +107,18 @@
e.g.:
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
<csr />
<key des="no" passphrase="none">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<!-- If you use an index file (or want to) to serialize client certificates, specify it here. -->
<!-- It must conform to CADB spec (https://pki-tutorial.readthedocs.io/en/latest/cadb.html). -->
<!-- You should probably also specify a serial file if so. -->
<!-- Both of these are entirely optional if you aren't using an existing PKI. -->
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
<!-- If you specify a cipher, the key will be encrypted to the passphrase provided by the passphrase attribute.
If the key is encrypted (either a pre-existing or a created one) but passphrase is not provided, you will
be (securely) prompted for the passphrase to unlock it/add a passphrase to it. -->
<key cipher="none"
passphrase="none"
keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
<subject>
<commonName>domain.tld</commonName>
<countryName>XX</countryName>
@ -108,9 +130,11 @@
</subject>
</ca>
<client>
<cert>{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
<csr />
<key des="no" passphrase="none">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<key cipher="none"
passphrase="none"
keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
<subject>
<commonName>some client name</commonName>
<countryName>XX</countryName>