fixing some merge issues
This commit is contained in:
commit
75580b43cc
33
TODO
33
TODO
@ -1,28 +1,24 @@
|
|||||||
- write classes/functions
|
- write classes/functions
|
||||||
- XML-based config
|
- XML-based config
|
||||||
|
-x XML syntax
|
||||||
|
--- xregex btags - case-insensitive? this can be represented in-pattern:
|
||||||
|
xhttps://stackoverflow.com/a/9655186/733214
|
||||||
|
-x configuration generator
|
||||||
|
--- xprint end result xml config to stderr for easier redirection? or print prompts to stderr and xml to stdout?
|
||||||
|
-- xXSD for validation
|
||||||
|
-- Flask app for generating config?
|
||||||
|
-- TKinter (or pygame?) GUI?
|
||||||
|
--- https://docs.python.org/3/faq/gui.html
|
||||||
|
--- https://www.pygame.org/wiki/gui
|
||||||
- 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
|
https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
|
||||||
at the very least document all the functions and such so pydoc's happy.
|
at the very least document all the functions and such so pydoc's happy.
|
||||||
- better prompt display. i might include them as constants in a separate file
|
|
||||||
and then import it for e.g. confgen. or maybe a Flask website/app?
|
|
||||||
- locking
|
- locking
|
||||||
- for docs, 3.x (as of 3.10) was 2.4M.
|
- for docs, 3.x (as of 3.10) was 2.4M.
|
||||||
- GUI? at least for generating config...
|
- xNeed ability to write/parse mtree specs (or a similar equivalent) for applying ownerships/permissions to overlay files
|
||||||
- Need ability to write/parse mtree specs (or a similar equivalent) for applying ownerships/permissions to overlay files
|
-- parsing is done. writing may? come later.
|
||||||
|
|
||||||
- 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,
|
|
||||||
https://pypi.org/project/hashID/)
|
|
||||||
|
|
||||||
- package for PyPI:
|
- package for PyPI:
|
||||||
# https://packaging.python.org/tutorials/distributing-packages/
|
# https://packaging.python.org/tutorials/distributing-packages/
|
||||||
@ -38,7 +34,6 @@ BUGS.SQUARE-R00T.NET bugs/tasks:
|
|||||||
#14: Use os.path.join() for more consistency/pythonicness
|
#14: Use os.path.join() for more consistency/pythonicness
|
||||||
#24: Run as regular user? (pychroot? fakeroot?)
|
#24: Run as regular user? (pychroot? fakeroot?)
|
||||||
#34: Build-time support for only building single phase of build
|
#34: Build-time support for only building single phase of build
|
||||||
#36: Allow parsing pkg lists with inline comments
|
|
||||||
#39: Fix UEFI
|
#39: Fix UEFI
|
||||||
#40: ISO overlay (to add e.g. memtest86+ to final ISO)
|
#40: ISO overlay (to add e.g. memtest86+ to final ISO)
|
||||||
#43: Support resuming partial tarball downloads (Accet-Ranges: bytes)
|
#43: Support resuming partial tarball downloads (Accept-Ranges: bytes)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
import jinja2
|
import jinja2
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
90
bdisk/GPG.py
90
bdisk/GPG.py
@ -1,8 +1,26 @@
|
|||||||
|
import datetime
|
||||||
import gpg
|
import gpg
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import gpg.errors
|
import gpg.errors
|
||||||
|
|
||||||
|
|
||||||
|
# This helps translate the input name from the conf to a string compatible with the gpg module.
|
||||||
|
_algmaps = {#'cv': 'cv{keysize}', # DISABLED, can't sign (only encrypt). Currently only 25519
|
||||||
|
'ed': 'ed{keysize}', # Currently only 25519
|
||||||
|
#'elg': 'elg{}', # DISABLED, can't sign (only encrypt). 1024, 2048, 4096
|
||||||
|
'nist': 'nistp{keysize}', # 256, 384, 521
|
||||||
|
'brainpool.1': 'brainpoolP{keysize}r1', # 256, 384, 512
|
||||||
|
'sec.k1': 'secp{keysize}k1', # Currently only 256
|
||||||
|
'rsa': 'rsa{keysize}', # Variable (1024 <> 4096), but we only support 1024, 2048, 4096
|
||||||
|
'dsa': 'dsa{keysize}'} # Variable (768 <> 3072), but we only support 768, 2048, 3072
|
||||||
|
|
||||||
|
# This is just a helper function to get a delta from a unix epoch.
|
||||||
|
def _epoch_helper(epoch):
|
||||||
|
d = datetime.datetime.utcfromtimestamp(epoch) - datetime.datetime.utcnow()
|
||||||
|
return(abs(int(d.total_seconds()))) # Returns a positive integer even if negative...
|
||||||
|
#return(int(d.total_seconds()))
|
||||||
|
|
||||||
# http://files.au.adversary.org/crypto/GPGMEpythonHOWTOen.html
|
# http://files.au.adversary.org/crypto/GPGMEpythonHOWTOen.html
|
||||||
# https://www.gnupg.org/documentation/manuals/gpgme.pdf
|
# https://www.gnupg.org/documentation/manuals/gpgme.pdf
|
||||||
# Support ECC? https://www.gnupg.org/faq/whats-new-in-2.1.html#ecc
|
# Support ECC? https://www.gnupg.org/faq/whats-new-in-2.1.html#ecc
|
||||||
@ -60,7 +78,7 @@ class GPGHandler(object):
|
|||||||
self._prep_home()
|
self._prep_home()
|
||||||
else:
|
else:
|
||||||
self._check_home()
|
self._check_home()
|
||||||
self.ctx = self.get_context(home_dir = self.home)
|
self.ctx = self.GetContext(home_dir = self.home)
|
||||||
|
|
||||||
def _check_home(self, home = None):
|
def _check_home(self, home = None):
|
||||||
if not home:
|
if not home:
|
||||||
@ -94,11 +112,12 @@ class GPGHandler(object):
|
|||||||
'write to')
|
'write to')
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def get_context(self, **kwargs):
|
def GetContext(self, **kwargs):
|
||||||
ctx = gpg.Context(**kwargs)
|
ctx = gpg.Context(**kwargs)
|
||||||
return(ctx)
|
return(ctx)
|
||||||
|
|
||||||
def kill_stale_agent(self):
|
def KillStaleAgent(self):
|
||||||
|
# Is this even necessary since I switched to the native gpg module instead of the gpgme one?
|
||||||
_process_list = []
|
_process_list = []
|
||||||
# TODO: optimize; can I search by proc name?
|
# TODO: optimize; can I search by proc name?
|
||||||
for p in psutil.process_iter():
|
for p in psutil.process_iter():
|
||||||
@ -113,7 +132,64 @@ class GPGHandler(object):
|
|||||||
# for p in plst:
|
# for p in plst:
|
||||||
# psutil.Process(p).terminate()
|
# psutil.Process(p).terminate()
|
||||||
|
|
||||||
def get_sigs(self, data_in):
|
def CreateKey(self, name, algo, keysize, email = None, comment = None, passwd = None, key = None, expiry = None):
|
||||||
|
algo = _algmaps[algo].format(keysize = keysize)
|
||||||
|
userid = name
|
||||||
|
userid += ' ({0})'.format(comment) if comment else ''
|
||||||
|
userid += ' <{0}>'.format(email) if email else ''
|
||||||
|
if not expiry:
|
||||||
|
expires = False
|
||||||
|
else:
|
||||||
|
expires = True
|
||||||
|
self.ctx.create_key(userid,
|
||||||
|
algorithm = algo,
|
||||||
|
expires = expires,
|
||||||
|
expires_in = _epoch_helper(expiry),
|
||||||
|
sign = True)
|
||||||
|
# Even if expires is False, it still parses the expiry...
|
||||||
|
# except OverflowError: # Only trips if expires is True and a negative expires occurred.
|
||||||
|
# raise ValueError(('Expiration epoch must be 0 (to disable) or a future time! '
|
||||||
|
# 'The specified epoch ({0}, {1}) is in the past '
|
||||||
|
# '(current time is {2}, {3}).').format(expiry,
|
||||||
|
# str(datetime.datetime.utcfromtimestamp(expiry)),
|
||||||
|
# datetime.datetime.utcnow().timestamp(),
|
||||||
|
# str(datetime.datetime.utcnow())))
|
||||||
|
return(k)
|
||||||
|
# We can't use self.ctx.create_key; it's a little limiting.
|
||||||
|
# It's a fairly thin wrapper to .op_createkey() (the C GPGME API gpgme_op_createkey) anyways.
|
||||||
|
flags = (gpg.constants.create.SIGN |
|
||||||
|
gpg.constants.create.CERT)
|
||||||
|
if not expiry:
|
||||||
|
flags = (flags | gpg.constants.create.NOEXPIRE)
|
||||||
|
if not passwd:
|
||||||
|
flags = (flags | gpg.constants.create.NOPASSWD)
|
||||||
|
else:
|
||||||
|
# Thanks, gpg/core.py#Context.create_key()!
|
||||||
|
sys_pinentry = gpg.constants.PINENTRY_MODE_DEFAULT
|
||||||
|
old_pass_cb = getattr(self, '_passphrase_cb', None)
|
||||||
|
self.ctx.pinentry_mode = gpg.constants.PINENTRY_MODE_LOOPBACK
|
||||||
|
def passphrase_cb(hint, desc, prev_bad, hook = None):
|
||||||
|
return(passwd)
|
||||||
|
self.ctx.set_passphrase_cb(passphrase_cb)
|
||||||
|
try:
|
||||||
|
if not key:
|
||||||
|
try:
|
||||||
|
self.ctx.op_createkey(userid, algo, 0, 0, flags)
|
||||||
|
k = self.ctx.get_key(self.ctx.op_genkey_result().fpr, secret = True)
|
||||||
|
else:
|
||||||
|
if not isinstance(key, gpg.gpgme._gpgme_key):
|
||||||
|
key = self.ctx.get_key(key)
|
||||||
|
if not key:
|
||||||
|
raise ValueError('Key {0} does not exist'.format())
|
||||||
|
#self.ctx.op_createsubkey(key, )
|
||||||
|
finally:
|
||||||
|
if not passwd:
|
||||||
|
self.ctx.pinentry_mode = sys_pinentry
|
||||||
|
if old_pass_cb:
|
||||||
|
self.ctx.set_passphrase_cb(*old_pass_cb[1:])
|
||||||
|
return(k)
|
||||||
|
|
||||||
|
def GetSigs(self, data_in):
|
||||||
key_ids = []
|
key_ids = []
|
||||||
# Currently as of May 13, 2018 there's no way using the GPGME API to do
|
# Currently as of May 13, 2018 there's no way using the GPGME API to do
|
||||||
# the equivalent of the CLI's --list-packets.
|
# the equivalent of the CLI's --list-packets.
|
||||||
@ -131,3 +207,9 @@ class GPGHandler(object):
|
|||||||
l = [i.strip() for i in line.split(':')]
|
l = [i.strip() for i in line.split(':')]
|
||||||
key_ids.append(l[0])
|
key_ids.append(l[0])
|
||||||
return(key_ids)
|
return(key_ids)
|
||||||
|
|
||||||
|
def CheckSigs(self, keys, sig_data):
|
||||||
|
try:
|
||||||
|
self.ctx.verify(sig_data)
|
||||||
|
except:
|
||||||
|
pass # TODO
|
||||||
|
@ -2,4 +2,11 @@ import OpenSSL
|
|||||||
# https://cryptography.io/en/latest/x509/reference/#cryptography.x509.CertificateBuilder.sign
|
# https://cryptography.io/en/latest/x509/reference/#cryptography.x509.CertificateBuilder.sign
|
||||||
# migrate old functions of bSSL to use cryptography
|
# migrate old functions of bSSL to use cryptography
|
||||||
# but still waiting on their recpipes.
|
# but still waiting on their recpipes.
|
||||||
# https://cryptography.io/en/latest/x509/tutorial/
|
# https://cryptography.io/en/latest/x509/tutorial/
|
||||||
|
#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')
|
1
bdisk/basedistro/antergos.py
Symbolic link
1
bdisk/basedistro/antergos.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
archlinux.py
|
1
bdisk/basedistro/arch.py
Symbolic link
1
bdisk/basedistro/arch.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
archlinux.py
|
96
bdisk/basedistro/archlinux.py
Normal file
96
bdisk/basedistro/archlinux.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Supported initsys values:
|
||||||
|
# systemd
|
||||||
|
# Possible future inclusions:
|
||||||
|
# openrc
|
||||||
|
# runit
|
||||||
|
# sinit
|
||||||
|
# s6
|
||||||
|
# shepherd
|
||||||
|
initsys = 'systemd'
|
||||||
|
|
||||||
|
def extern_prep(cfg, cur_arch = 'x86_64'):
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
mirrorlist = os.path.join(cfg['build']['paths']['chroot'],
|
||||||
|
cur_arch,
|
||||||
|
'etc/pacman.d/mirrorlist')
|
||||||
|
with open(mirrorlist, 'r') as f:
|
||||||
|
mirrors = []
|
||||||
|
for i in f.readlines():
|
||||||
|
m = re.sub('^\s*#.*$', '', i.strip())
|
||||||
|
if m != '':
|
||||||
|
mirrors.append(m)
|
||||||
|
if not mirrors:
|
||||||
|
# We do this as a fail-safe.
|
||||||
|
mirror = ('\n\n# Added by BDisk\n'
|
||||||
|
'Server = https://arch.mirror.square-r00t.net/'
|
||||||
|
'$repo/os/$arch\n')
|
||||||
|
with open(mirrorlist, 'a') as f:
|
||||||
|
f.write(mirror)
|
||||||
|
return()
|
||||||
|
|
||||||
|
# This will be run before the regular packages are installed. It can be
|
||||||
|
# whatever script you like, as long as it has the proper shebang and doesn't
|
||||||
|
# need additional packages installed.
|
||||||
|
# In Arch's case, we use it for initializing the keyring and installing an AUR
|
||||||
|
# helper.
|
||||||
|
pkg_mgr_prep = """#!/bin/bash
|
||||||
|
|
||||||
|
pacman -Syy
|
||||||
|
pacman-key --init
|
||||||
|
pacman-key --populate archlinux
|
||||||
|
pacman -S --noconfirm --needed base
|
||||||
|
pacman -S --noconfirm --needed base-devel multilib-devel git linux-headers \
|
||||||
|
mercurial subversion vala xorg-server-devel
|
||||||
|
cd /tmp
|
||||||
|
sqrt="https://git.square-r00t.net/BDisk/plain/external"
|
||||||
|
# Temporary until there's another AUR helper that allows dropping privs AND
|
||||||
|
# automatically importing GPG keys.
|
||||||
|
pkg="${sqrt}/apacman-current.pkg.tar.xz?h=4.x_rewrite"
|
||||||
|
curl -sL -o apacman-current.pkg.tar.xz ${pkg}
|
||||||
|
pacman -U --noconfirm apacman-current.pkg.tar.xz
|
||||||
|
rm apacman*
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Special values:
|
||||||
|
# {PACKAGE} = the package name
|
||||||
|
# {VERSION} = the version specified in the <package version= ...> attribute
|
||||||
|
# {REPO} = the repository specified in the <package repo= ...> attribute
|
||||||
|
# If check_cmds are needed to run before installing, set pre_check to True.
|
||||||
|
# Return code 0 means the package is installed already, anything else means we
|
||||||
|
# should try to install it.
|
||||||
|
#### AUR SUPPORT ####
|
||||||
|
packager = {'pre_check': False,
|
||||||
|
'sys_update': ['/usr/bin/apacman', '-S', '-u'],
|
||||||
|
'sync_cmd': ['/usr/bin/apacman', '-S', '-y', '-y'],
|
||||||
|
'check_cmds': {'versioned': ['/usr/bin/pacman',
|
||||||
|
'-Q', '-s',
|
||||||
|
'{PACKAGE}'],
|
||||||
|
'unversioned': ['/usr/bin/pacman',
|
||||||
|
'-Q', '-s',
|
||||||
|
'{PACKAGE}']
|
||||||
|
},
|
||||||
|
'update_cmds': {'versioned': ['/usr/bin/pacman',
|
||||||
|
'-S', '-u',
|
||||||
|
'{PACKAGE}'],
|
||||||
|
'unversioned': ['/usr/bin/pacman',
|
||||||
|
'-S', '-u',
|
||||||
|
'{PACKAGE}']
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# These are packages *required* to exist on the base guest, no questions asked.
|
||||||
|
# TODO: can this be trimmed down?
|
||||||
|
prereqs = ['arch-install-scripts', 'archiso', 'bzip2', 'coreutils',
|
||||||
|
'customizepkg-scripting', 'cronie', 'dhclient', 'dhcp', 'dhcpcd',
|
||||||
|
'dosfstools', 'dropbear', 'efibootmgr', 'efitools', 'efivar',
|
||||||
|
'file', 'findutils', 'iproute2', 'iputils', 'libisoburn',
|
||||||
|
'localepurge', 'lz4', 'lzo', 'lzop', 'mkinitcpio-nbd',
|
||||||
|
'mkinitcpio-nfs-utils', 'mkinitcpio-utils', 'nbd', 'ms-sys',
|
||||||
|
'mtools', 'net-tools', 'netctl', 'networkmanager', 'pv',
|
||||||
|
'python', 'python-pyroute2', 'rsync', 'sed', 'shorewall',
|
||||||
|
'squashfs-tools', 'sudo', 'sysfsutils',
|
||||||
|
'syslinux', 'traceroute', 'vi']
|
||||||
|
|
1
bdisk/basedistro/manjaro.py
Symbolic link
1
bdisk/basedistro/manjaro.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
archlinux.py
|
931
bdisk/bdisk.xsd
931
bdisk/bdisk.xsd
@ -1,6 +1,933 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||||
targetNamespace="http://bdisk.square-r00t.net"
|
targetNamespace="http://bdisk.square-r00t.net/"
|
||||||
xmlns="http://bdisk.square-r00t.net"
|
xmlns="http://bdisk.square-r00t.net/"
|
||||||
elementFormDefault="qualified">
|
elementFormDefault="qualified">
|
||||||
|
|
||||||
|
<!-- CUSTOM TYPES -->
|
||||||
|
<!-- t_btag_uri: a string that will allow btags (xpath or variable only) or a URI string (but NOT a URN). -->
|
||||||
|
<!-- We can't use xs:anyURI because it is too loose (allows things like relative paths, etc.) -->
|
||||||
|
<!-- but ALSO too restrictive in that btags fail validation ({ and } are invalid for anyURI, -->
|
||||||
|
<!-- ironically). -->
|
||||||
|
<xs:simpleType name="t_btag_uri">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="\w+:(/?/?)[^\s]+"/>
|
||||||
|
<xs:pattern value=".*\{variable%[A-Za-z0-9_]\}.*"/>
|
||||||
|
<xs:pattern value=".*\{xpath%["'A-Za-z0-9_/\(\)\.\*@\-\[\]=]+\}.*"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_btag_uri -->
|
||||||
|
|
||||||
|
<!-- t_filename: a POSIX fully-portable filename. -->
|
||||||
|
<xs:simpleType name="t_filename">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="([a-z0-9._-]+){1,255}"/>
|
||||||
|
<xs:pattern value=".*\{variable%[A-Za-z0-9_]\}.*"/>
|
||||||
|
<xs:pattern value=".*\{xpath%["'A-Za-z0-9_/\(\)\.\*@\-\[\]=]+\}.*"/>
|
||||||
|
<!-- We don't allow (string)(regex) or (regex)(string) or (string)(regex)(string) or multiple regexes -->
|
||||||
|
<!-- because that's just... not feasible to manage from a parsing perspective. -->
|
||||||
|
<xs:pattern value="\{regex%.+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_filename -->
|
||||||
|
|
||||||
|
<!-- t_gpg_keyid: a set of various patterns that match GPG key IDs. -->
|
||||||
|
<xs:simpleType name="t_gpg_keyid">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="(none|new)"/>
|
||||||
|
<xs:pattern value="(auto|default)"/>
|
||||||
|
<xs:pattern value="(0x)?[0-9A-Fa-f]{40}"/>
|
||||||
|
<xs:pattern value="(0x)?[0-9A-Fa-f]{16}"/>
|
||||||
|
<xs:pattern value="(0x)?[0-9A-Fa-f]{8}"/>
|
||||||
|
<xs:pattern value="([0-9A-Fa-f ]{4}){5} ?([0-9A-Fa-f ]{4}){4}[0-9A-Fa-f]{4}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_gpg_keyid -->
|
||||||
|
|
||||||
|
<!-- t_gpg_keyid_list: a type for a list of key IDs. -->
|
||||||
|
<xs:simpleType name="t_gpg_keyid_list">
|
||||||
|
<xs:list itemType="t_gpg_keyid"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_gpg_key_list -->
|
||||||
|
|
||||||
|
<!-- t_net_loc: a remote host. Used for PKI Subject's commonName and host for rsync. -->
|
||||||
|
<xs:simpleType name="t_net_loc">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern
|
||||||
|
value="(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_net_loc -->
|
||||||
|
|
||||||
|
<!-- t_pass_hash_algo: used for t_password. -->
|
||||||
|
<xs:simpleType name="t_pass_hash_algo">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="des"/>
|
||||||
|
<xs:enumeration value="md5"/>
|
||||||
|
<xs:enumeration value="sha256"/>
|
||||||
|
<xs:enumeration value="sha512"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_pass_hash_algo -->
|
||||||
|
|
||||||
|
<!-- t_pass_salt: used for t_password. -->
|
||||||
|
<xs:simpleType name="t_pass_salt">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="($[156]($rounds=[0-9]+)?$[a-zA-Z0-9./]{1,16}$?|auto|)"/>
|
||||||
|
<xs:pattern value="\{variable%[A-Za-z0-9_]\}"/>
|
||||||
|
<xs:pattern value="\{xpath%["'A-Za-z0-9_\(\)\.\*\-/\[\]=]+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_pass_salt -->
|
||||||
|
|
||||||
|
<!-- t_password: used for rootpass and user/password elements. -->
|
||||||
|
<xs:complexType name="t_password">
|
||||||
|
<!-- The below will need some fleshing out and testing. It may not be possible strictly via XSD. -->
|
||||||
|
<!-- TODO: restrict the value further with a union or multi-group regex that checks for a valid length? -->
|
||||||
|
<!-- des: ????? -->
|
||||||
|
<!-- md5: "[a-zA-Z0-9./]{22}" -->
|
||||||
|
<!-- sha256: "[a-zA-Z0-9./]{43}" -->
|
||||||
|
<!-- sha512: "[a-zA-Z0-9./]{86}" -->
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="hash_algo" type="t_pass_hash_algo" use="optional"/>
|
||||||
|
<xs:attribute name="hashed" type="xs:boolean" use="required"/>
|
||||||
|
<xs:attribute name="salt" type="t_pass_salt" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
<!-- END t_password -->
|
||||||
|
|
||||||
|
<!-- t_path: for specifying subdirectories (either local filesystem or remote paths). -->
|
||||||
|
<xs:simpleType name="t_path">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- We include blank to operate on default actions (or default filepaths). -->
|
||||||
|
<xs:pattern value=""/>
|
||||||
|
<xs:pattern value="(.+)/([^/]+)"/>
|
||||||
|
<xs:pattern value="((.+)/([^/]+))?\{variable%[A-Za-z0-9_]\}((.+)/([^/]+))?"/>
|
||||||
|
<xs:pattern value="((.+)/([^/]+))?\{xpath%["'A-Za-z0-9_\(\)\.\*\-/\[\]=]+\}((.+)/([^/]+))?"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_path -->
|
||||||
|
|
||||||
|
<!-- t_pki_cert: used for pki/ca/cert and pki/client/cert. -->
|
||||||
|
<xs:complexType name="t_pki_cert">
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="hash_algo" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="blake2b512"/>
|
||||||
|
<xs:enumeration value="blake2s256"/>
|
||||||
|
<xs:enumeration value="gost"/>
|
||||||
|
<xs:enumeration value="md4"/>
|
||||||
|
<xs:enumeration value="md5"/>
|
||||||
|
<xs:enumeration value="mdc2"/>
|
||||||
|
<xs:enumeration value="rmd160"/>
|
||||||
|
<xs:enumeration value="sha1"/>
|
||||||
|
<xs:enumeration value="sha224"/>
|
||||||
|
<xs:enumeration value="sha256"/>
|
||||||
|
<xs:enumeration value="sha384"/>
|
||||||
|
<xs:enumeration value="sha512"/>
|
||||||
|
<xs:enumeration value="none"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
<!-- END t_pki_cert -->
|
||||||
|
|
||||||
|
<!-- t_pki_key: used for pki/ca/key and pki/client/key -->
|
||||||
|
<xs:complexType name="t_pki_key">
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="cipher" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="aes128"/>
|
||||||
|
<xs:enumeration value="aes192"/>
|
||||||
|
<xs:enumeration value="bf"/>
|
||||||
|
<xs:enumeration value="blowfish"/>
|
||||||
|
<xs:enumeration value="camellia128"/>
|
||||||
|
<xs:enumeration value="camellia192"/>
|
||||||
|
<xs:enumeration value="camellia256"/>
|
||||||
|
<xs:enumeration value="des"/>
|
||||||
|
<xs:enumeration value="rc2"/>
|
||||||
|
<xs:enumeration value="seed"/>
|
||||||
|
<xs:enumeration value="none"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="passphrase" type="xs:string"/>
|
||||||
|
<xs:attribute name="keysize"
|
||||||
|
type="xs:positiveInteger"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
<!-- END t_pki_key -->
|
||||||
|
|
||||||
|
<!-- t_pki_subject: used for pki/ca/subject and pki/client/subject -->
|
||||||
|
<xs:complexType name="t_pki_subject">
|
||||||
|
<xs:all>
|
||||||
|
<!-- .../SUBJECT/COMMONNAME -->
|
||||||
|
<xs:element name="commonName" type="t_net_loc"/>
|
||||||
|
<!-- END .../SUBJECT/COMMONNAME -->
|
||||||
|
<!-- .../SUBJECT/COUNTRYNAME -->
|
||||||
|
<xs:element name="countryName">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- We can't validate an actual ISO-3166 ALPHA-2 code, but we can validate the format. -->
|
||||||
|
<!-- TODO: maybe cron the generation of an external namespace? -->
|
||||||
|
<xs:pattern value="[A-Z]{2}"/>
|
||||||
|
<xs:pattern value=".*\{variable%[A-Za-z0-9_]\}.*"/>
|
||||||
|
<xs:pattern value=".*\{xpath%["'A-Za-z0-9_/\(\)\.\*@\-\[\]=]+\}.*"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END .../SUBJECT/COUNTRYNAME -->
|
||||||
|
<!-- .../SUBJECT/LOCALITYNAME -->
|
||||||
|
<xs:element name="localityName" type="xs:string"/>
|
||||||
|
<!-- END .../SUBJECT/LOCALITYNAME -->
|
||||||
|
<!-- .../SUBJECT/STATEORPROVINCENAME -->
|
||||||
|
<xs:element name="stateOrProvinceName"
|
||||||
|
type="xs:string"/>
|
||||||
|
<!-- END .../SUBJECT/STATEORPROVINCENAME -->
|
||||||
|
<!-- .../SUBJECT/ORGANIZATION -->
|
||||||
|
<xs:element name="organization" type="xs:string"/>
|
||||||
|
<!-- END .../SUBJECT/ORGANIZATION -->
|
||||||
|
<!-- .../SUBJECT/ORGANIZATIONALUNITNAME -->
|
||||||
|
<xs:element name="organizationalUnitName"
|
||||||
|
type="xs:string"/>
|
||||||
|
<!-- END .../SUBJECT/ORGANIZATIONALUNITNAME -->
|
||||||
|
<!-- .../SUBJECT/EMAILADDRESS -->
|
||||||
|
<xs:element name="emailAddress" type="xs:string"/>
|
||||||
|
<!-- END .../SUBJECT/EMAILADDRESS -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<!-- END t_pki_subject -->
|
||||||
|
|
||||||
|
<!-- t_remote_file: an element that lets us define both a file pattern for remote content and flags attribute. -->
|
||||||
|
<xs:complexType name="t_remote_file">
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_filename">
|
||||||
|
<xs:attribute name="flags" type="t_remote_file_flags" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
<!-- END t_remote_file -->
|
||||||
|
|
||||||
|
<!-- t_remote_file_flags: a type to match a list of known flags. -->
|
||||||
|
<xs:simpleType name="t_remote_file_flags">
|
||||||
|
<xs:list>
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- Currently we only support two flags. -->
|
||||||
|
<xs:enumeration value="regex"/>
|
||||||
|
<xs:enumeration value="latest"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:list>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_remote_file_flags -->
|
||||||
|
|
||||||
|
<!-- t_username: enforce a POSIX-compliant username. Used for user/username elements. -->
|
||||||
|
<xs:simpleType name="t_username">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}$)"/>
|
||||||
|
<xs:pattern value="\{variable%[A-Za-z0-9_]\}"/>
|
||||||
|
<xs:pattern value="\{xpath%["'A-Za-z0-9_\(\)\.\*\-/\[\]=]+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<!-- END t_username -->
|
||||||
|
<!-- END CUSTOM TYPES -->
|
||||||
|
|
||||||
|
<!-- ROOT ELEMENT ("BDISK") -->
|
||||||
|
<xs:element name="bdisk">
|
||||||
|
<xs:complexType>
|
||||||
|
<!-- Should this be xs:sequence instead? -->
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE -->
|
||||||
|
<xs:element name="profile" maxOccurs="unbounded" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/META -->
|
||||||
|
<xs:element name="meta" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/META/NAMES -->
|
||||||
|
<xs:element name="names" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/META/NAMES/NAME -->
|
||||||
|
<xs:element name="name" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="[A-Z0-9]{1,8}"/>
|
||||||
|
<xs:pattern value="\{variable%[A-Za-z0-9_]\}"/>
|
||||||
|
<xs:pattern value="\{xpath%[A-Za-z0-9_\(\)\.\*\-/]+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/NAMES/NAME -->
|
||||||
|
<!-- BDISK/PROFILE/META/NAMES/UXNAME -->
|
||||||
|
<xs:element name="uxname" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- refer to the 2009 POSIX spec, "3.282 Portable Filename Character Set" -->
|
||||||
|
<!-- http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282 -->
|
||||||
|
<!-- (We use this string to name some files.) -->
|
||||||
|
<xs:pattern value="([A-Za-z0-9._-]+){1,255}"/>
|
||||||
|
<xs:pattern value="\{variable%[A-Za-z0-9_]\}"/>
|
||||||
|
<xs:pattern value="\{xpath%[A-Za-z0-9_\(\)\.\*\-/]+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/NAMES/UXNAME -->
|
||||||
|
<!-- BDISK/PROFILE/META/NAMES/PNAME -->
|
||||||
|
<xs:element name="pname" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- TODO: Can I use UTF-8 instead? -->
|
||||||
|
<!-- https://stackoverflow.com/a/9805789/733214 -->
|
||||||
|
<xs:pattern value="\p{IsBasicLatin}*"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/NAMES/PNAME -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/NAMES -->
|
||||||
|
<!-- BDISK/PROFILE/META/DESC -->
|
||||||
|
<xs:element name="desc" maxOccurs="1" minOccurs="1" type="xs:string"/>
|
||||||
|
<!-- END BDISK/PROFILE/META/DESC -->
|
||||||
|
<!-- BDISK/PROFILE/META/DEV -->
|
||||||
|
<xs:element name="dev" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/META/DEV/AUTHOR -->
|
||||||
|
<xs:element name="author" maxOccurs="1" minOccurs="1"
|
||||||
|
type="xs:normalizedString"/>
|
||||||
|
<!-- END BDISK/PROFILE/META/DEV/AUTHOR -->
|
||||||
|
<!-- BDISK/PROFILE/META/DEV/EMAIL -->
|
||||||
|
<!-- The following does NOT WORK. Shame, really. -->
|
||||||
|
<!-- It seems to be an invalid pattern per my XSD validator (xmllint). -->
|
||||||
|
<!--<xs:pattern value="([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\[[\t -Z^-~]*])"/>-->
|
||||||
|
<xs:element name="email" maxOccurs="1" minOccurs="1"
|
||||||
|
type="xs:normalizedString"/>
|
||||||
|
<!-- END BDISK/PROFILE/META/DEV/EMAIL -->
|
||||||
|
<!-- BDISK/PROFILE/META/DEV/WEBSITE -->
|
||||||
|
<xs:element name="website" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_btag_uri"/>
|
||||||
|
<!-- END BDISK/PROFILE/META/DEV/WEBSITE -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/DEV -->
|
||||||
|
<!-- BDISK/PROFILE/META/URI -->
|
||||||
|
<xs:element name="uri" maxOccurs="1" minOccurs="1" type="t_btag_uri"/>
|
||||||
|
<!-- END BDISK/PROFILE/META/URI -->
|
||||||
|
<!-- BDISK/PROFILE/META/VER -->
|
||||||
|
<xs:element name="ver" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:normalizedString">
|
||||||
|
<!-- Like ../names/uxname, this is also used to name certain files so, POSIX portable filename. -->
|
||||||
|
<xs:pattern value="([A-Za-z0-9._-]+){1,255}"/>
|
||||||
|
<xs:pattern value="\{variable%[A-Za-z0-9_]\}"/>
|
||||||
|
<xs:pattern value="\{xpath%[A-Za-z0-9_\(\)\.\*\-/]+\}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/VER -->
|
||||||
|
<!-- BDISK/PROFILE/META/MAX_RECURSE -->
|
||||||
|
<xs:element name="max_recurse" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:positiveInteger">
|
||||||
|
<xs:maxExclusive value="1000"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/MAX_RECURSE -->
|
||||||
|
<!-- BDISK/PROFILE/META/REGEXES -->
|
||||||
|
<xs:element name="regexes" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/META/REGEXES/PATTERN -->
|
||||||
|
<xs:element name="pattern" maxOccurs="unbounded" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="id" type="xs:string"
|
||||||
|
use="required"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/REGEXES/PATTERN -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/REGEXES -->
|
||||||
|
<!-- BDISK/PROFILE/META/VARIABLES -->
|
||||||
|
<xs:element name="variables" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/META/VARIABLES/VARIABLE -->
|
||||||
|
<xs:element name="variable" maxOccurs="unbounded" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="id" type="xs:string"
|
||||||
|
use="required"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/VARIABLES/VARIABLE -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META/VARIABLES -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/META -->
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS -->
|
||||||
|
<xs:element name="accounts" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS/ROOTPASS -->
|
||||||
|
<xs:element name="rootpass" maxOccurs="1" minOccurs="1" type="t_password"/>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS/ROOTPASS -->
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS/USER -->
|
||||||
|
<xs:element name="user" maxOccurs="unbounded" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS/USER/USERNAME -->
|
||||||
|
<xs:element name="username" type="t_username" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS/USER/USERNAME -->
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS/USER/COMMENT -->
|
||||||
|
<!-- https://en.wikipedia.org/wiki/Gecos_field -->
|
||||||
|
<!-- Through experimentation, this *seems* to cap at 990 chars. -->
|
||||||
|
<xs:element name="comment" maxOccurs="1"
|
||||||
|
minOccurs="0">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:normalizedString">
|
||||||
|
<xs:maxLength value="990"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS/USER/COMMENT -->
|
||||||
|
<!-- BDISK/PROFILE/ACCOUNTS/USER/PASSWORD -->
|
||||||
|
<xs:element name="password" type="t_password" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS/USER/PASSWORD -->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="sudo" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS/USER -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/ACCOUNTS -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES -->
|
||||||
|
<xs:element name="sources" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDisk only supports two different architectures (x86/i686 and x86_64, respectively) currently. -->
|
||||||
|
<!-- TODO: future improvements may let us include e.g. two different x86_64 environments (e.g. CentOS and Debian on the same media), but this is like, still in development stages. -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE -->
|
||||||
|
<xs:element name="source" minOccurs="1" maxOccurs="2">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- We cheat here. TECHNICALLY it should ONLY be scheme://location (no /path...), but there isn't a data type for that. -->
|
||||||
|
<!-- Currently we enforce only one item. Future BDisk versions may be able to make use of multiple <mirror>s and select best one based on speed. -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE/MIRROR -->
|
||||||
|
<xs:element name="mirror" type="t_btag_uri" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE/MIRROR -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE/ROOTPATH -->
|
||||||
|
<xs:element name="rootpath" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE/ROOTPATH -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE/TARBALL -->
|
||||||
|
<xs:element name="tarball" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_remote_file"/>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE/TARBALL -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE/CHECKSUM -->
|
||||||
|
<xs:element name="checksum" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_remote_file">
|
||||||
|
<!-- There is NO way we can validate this, because it will vary based on the algorithms supported by the build host. -->
|
||||||
|
<xs:attribute name="hash_algo" type="xs:string"
|
||||||
|
use="required"/>
|
||||||
|
<xs:attribute name="explicit" type="xs:boolean"
|
||||||
|
use="required"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE/CHECKSUM -->
|
||||||
|
<!-- BDISK/PROFILE/SOURCES/SOURCE/SIG -->
|
||||||
|
<xs:element name="sig" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_remote_file">
|
||||||
|
<!-- Required; otherwise there's no point using it. -->
|
||||||
|
<xs:attribute name="keys" type="t_gpg_keyid_list"
|
||||||
|
use="required"/>
|
||||||
|
<xs:attribute name="keyserver" type="t_btag_uri"
|
||||||
|
use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE/SIG-->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="arch" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="(i686|x86(_64)?|32|64)"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES/SOURCE -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SOURCES -->
|
||||||
|
<!-- BDISK/PROFILE/PACKAGES -->
|
||||||
|
<xs:element name="packages" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/PACKAGES/PACKAGE -->
|
||||||
|
<xs:element name="package" maxOccurs="unbounded" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="version" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="repo" type="xs:string" use="optional"/>
|
||||||
|
<!-- Default is "both" -->
|
||||||
|
<xs:attribute name="arch" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="(i686|x86(_64)?|32|64|both)"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/PACKAGES/PACKAGE -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/PACKAGES -->
|
||||||
|
<!-- BDISK/PROFILE/SERVICES -->
|
||||||
|
<xs:element name="services" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/SERVICES/SERVICE -->
|
||||||
|
<xs:element name="service" maxOccurs="unbounded" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="xs:string">
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="required"/>
|
||||||
|
<xs:attribute name="blacklisted" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SERVICES/SERVICE -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SERVICES -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD -->
|
||||||
|
<xs:element name="build" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS -->
|
||||||
|
<xs:element name="paths">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/BASE -->
|
||||||
|
<xs:element name="base" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/BASE -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/CACHE -->
|
||||||
|
<xs:element name="cache" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/CACHE -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/CHROOT -->
|
||||||
|
<xs:element name="chroot" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/CHROOT -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/OVERLAY -->
|
||||||
|
<xs:element name="overlay" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/OVERLAY -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/TEMPLATES -->
|
||||||
|
<xs:element name="templates" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/TEMPLATES -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/MOUNT -->
|
||||||
|
<xs:element name="mount" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/MOUNT -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/DISTROS -->
|
||||||
|
<xs:element name="distros" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/DISTROS -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/DEST -->
|
||||||
|
<xs:element name="dest" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/DEST -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/ISO -->
|
||||||
|
<xs:element name="iso" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/ISO -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/HTTP -->
|
||||||
|
<xs:element name="http" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/HTTP -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/PATHS/TFTP -->
|
||||||
|
<xs:element name="tftp" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/TFTP -->
|
||||||
|
<!-- EBDISK/PROFILE/BUILD/PATHS/PKI -->
|
||||||
|
<xs:element name="pki" maxOccurs="1" minOccurs="1" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS/PKI -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/PATHS -->
|
||||||
|
<!-- BDISK/PROFILE/BUILD/BASEDISTRO -->
|
||||||
|
<xs:element name="basedistro"/>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD/BASEDISTRO -->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="its_full_of_stars" type="xs:boolean"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/BUILD -->
|
||||||
|
<!-- BDISK/PROFILE/ISO -->
|
||||||
|
<xs:element name="iso" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="sign" type="xs:boolean"/>
|
||||||
|
<xs:attribute name="multi_arch">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="yes"/>
|
||||||
|
<xs:enumeration value="no"/>
|
||||||
|
<xs:enumeration value="true"/>
|
||||||
|
<xs:enumeration value="false"/>
|
||||||
|
<xs:enumeration value="x86_64"/>
|
||||||
|
<xs:enumeration value="x86"/>
|
||||||
|
<xs:enumeration value="64"/>
|
||||||
|
<xs:enumeration value="32"/>
|
||||||
|
<xs:enumeration value="i686"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/ISO -->
|
||||||
|
<!-- BDISK/PROFILE/IPXE -->
|
||||||
|
<xs:element name="ipxe" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/IPXE/URI -->
|
||||||
|
<xs:element name="uri" type="t_btag_uri" maxOccurs="1" minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/IPXE/URI -->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="sign" type="xs:boolean"/>
|
||||||
|
<xs:attribute name="iso" type="xs:boolean"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/IPXE -->
|
||||||
|
<!-- BDISK/PROFILE/GPG -->
|
||||||
|
<xs:element name="gpg" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/GPG/KEY -->
|
||||||
|
<xs:element name="key" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/GPG/KEY/NAME -->
|
||||||
|
<xs:element name="name" type="xs:normalizedString" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/GPG/KEY/NAME -->
|
||||||
|
<!-- BDISK/PROFILE/GPG/KEY/EMAIL -->
|
||||||
|
<xs:element name="email" type="xs:normalizedString" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/GPG/KEY/EMAIL -->
|
||||||
|
<!-- BDISK/PROFILE/GPG/KEY/COMMENT -->
|
||||||
|
<xs:element name="comment" type="xs:string" maxOccurs="1"
|
||||||
|
minOccurs="0"/>
|
||||||
|
<!-- END BDISK/PROFILE/GPG/KEY/COMMENT -->
|
||||||
|
<!-- BDISK/PROFILE/GPG/KEY/SUBKEY -->
|
||||||
|
<xs:element name="subkey" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<!-- See below for notes on attributes. -->
|
||||||
|
<!-- TODO: convert into shared type for parent as well? -->
|
||||||
|
<xs:attribute name="algo" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="rsa"/>
|
||||||
|
<xs:enumeration value="dsa"/>
|
||||||
|
<xs:enumeration value="ed"/>
|
||||||
|
<xs:enumeration value="nist"/>
|
||||||
|
<xs:enumeration value="brainpool.1"/>
|
||||||
|
<xs:enumeration value="sec.k1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="keysize" type="xs:positiveInteger" use="optional"/>
|
||||||
|
<xs:attribute name="expire" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:integer">
|
||||||
|
<xs:pattern value="(0|[0-9]{10})"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/GPG/KEY/SUBKEY -->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="algo" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<!-- rsa, dsa, and elgamal are "normal". Newer GnuPG supports ECC (yay!), so we have support for those in the XSD (you can get a list with gpg -with-colons -list-config curve | cut -f3 -d":" | tr ';' '\n'). -->
|
||||||
|
<!-- We test in-code if the host supports it. -->
|
||||||
|
<xs:enumeration value="rsa"/>
|
||||||
|
<xs:enumeration value="dsa"/>
|
||||||
|
<!-- The following only support encryption. The entire reason we'd be generating a key is to sign files, so we disable them. -->
|
||||||
|
<!-- <xs:enumeration value="elg"/> -->
|
||||||
|
<!-- <xs:enumeration value="cv"/> -->
|
||||||
|
<xs:enumeration value="ed"/>
|
||||||
|
<xs:enumeration value="nist"/>
|
||||||
|
<xs:enumeration value="brainpool.1"/>
|
||||||
|
<xs:enumeration value="sec.k1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<!-- We COULD constrain this further, but it's conditional upon the algo type. So we'll do that in BDisk itself. -->
|
||||||
|
<!-- But it may be possible? https://stackoverflow.com/a/39045446/733214 -->
|
||||||
|
<xs:attribute name="keysize" type="xs:positiveInteger" use="optional"/>
|
||||||
|
<!-- XSD doesn't have a datatype for Epoch vs. 0 (for no expire). -->
|
||||||
|
<xs:attribute name="expire" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<!--This is xs:integer instead of xs:positiveInteger because 0 will fail validation then. -->
|
||||||
|
<xs:restriction base="xs:integer">
|
||||||
|
<xs:pattern value="(0|[0-9]{10})"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/GPG/KEY -->
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="keyid" type="t_gpg_keyid" use="required"/>
|
||||||
|
<xs:attribute name="publish" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="prompt_passphrase" type="xs:boolean" use="required"/>
|
||||||
|
<xs:attribute name="passphrase" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern
|
||||||
|
value="[!"#$%&\\'\(\)\*\+,\-\./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\]\^_`abcdefghijklmnopqrstuvwxyz\{\|\}~ ]+"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="gnupghome" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern value="(.+)/([^/]+)"/>
|
||||||
|
<xs:pattern
|
||||||
|
value="((.+)/([^/]+))?\{variable%[A-Za-z0-9_]\}((.+)/([^/]+))?"/>
|
||||||
|
<xs:pattern
|
||||||
|
value="((.+)/([^/]+))?\{xpath%[A-Za-z0-9_\(\)\.\*\-/]+\}((.+)/([^/]+))?"/>
|
||||||
|
<xs:pattern value="(none|)"/>
|
||||||
|
<xs:pattern value="(auto|default)"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/GPG -->
|
||||||
|
<!-- BDISK/PROFILE/PKI -->
|
||||||
|
<xs:element name="pki" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA -->
|
||||||
|
<xs:element name="ca" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/CERT -->
|
||||||
|
<xs:element name="cert" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_pki_cert"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/CERT -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/CSR -->
|
||||||
|
<xs:element name="csr" maxOccurs="1" minOccurs="0" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/CSR -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/INDEX -->
|
||||||
|
<xs:element name="index" maxOccurs="1" minOccurs="0" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/INDEX -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/SERIAL -->
|
||||||
|
<xs:element name="serial" maxOccurs="1" minOccurs="0"
|
||||||
|
type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/SERIAL -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/KEY -->
|
||||||
|
<xs:element name="key" minOccurs="1" maxOccurs="1"
|
||||||
|
type="t_pki_key"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/CSR -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CA/SUBJECT -->
|
||||||
|
<xs:element name="subject" maxOccurs="1" minOccurs="0"
|
||||||
|
type="t_pki_subject"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA/SUBJECT -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CA -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CLIENT -->
|
||||||
|
<xs:element name="client" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/PKI/CLIENT/CERT -->
|
||||||
|
<xs:element name="cert" maxOccurs="1" minOccurs="1"
|
||||||
|
type="t_pki_cert"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CLIENT/CERT -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CLIENT/CSR -->
|
||||||
|
<xs:element name="csr" maxOccurs="1" minOccurs="0" type="t_path"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CLIENT/CSR -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CLIENT/KEY -->
|
||||||
|
<xs:element name="key" minOccurs="1" maxOccurs="1"
|
||||||
|
type="t_pki_key"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CLIENT/CSR -->
|
||||||
|
<!-- BDISK/PROFILE/PKI/CLIENT/SUBJECT -->
|
||||||
|
<xs:element name="subject" maxOccurs="1" minOccurs="0"
|
||||||
|
type="t_pki_subject"/>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CLIENT/SUBJECT -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/PKI/CLIENT -->
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="overwrite" type="xs:boolean" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/PKI -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC -->
|
||||||
|
<xs:element name="sync" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<!-- BDISK/PROFILE/SYNC/IPXE -->
|
||||||
|
<xs:element name="ipxe" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/IPXE -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/TFTP -->
|
||||||
|
<xs:element name="tftp" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/TFTP -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/ISO -->
|
||||||
|
<xs:element name="iso" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="optional"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/ISO -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/GPG -->
|
||||||
|
<xs:element name="gpg" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:simpleContent>
|
||||||
|
<xs:extension base="t_path">
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="optional"/>
|
||||||
|
<xs:attribute name="format" use="required">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="asc"/>
|
||||||
|
<xs:enumeration value="bin"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:simpleContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/GPG -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC -->
|
||||||
|
<xs:element name="rsync" maxOccurs="1" minOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC/USER -->
|
||||||
|
<xs:element name="user" type="t_username" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/RSYNC/USER -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC/HOST -->
|
||||||
|
<xs:element name="host" type="t_net_loc" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/RSYNC/HOST -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC/PORT -->
|
||||||
|
<xs:element name="port" maxOccurs="1" minOccurs="0">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:positiveInteger">
|
||||||
|
<xs:minInclusive value="1"/>
|
||||||
|
<xs:maxInclusive value="65535"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/RSYNC/PORT -->
|
||||||
|
<xs:choice>
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC/PUBKEY -->
|
||||||
|
<xs:element name="pubkey" type="t_path" maxOccurs="1"
|
||||||
|
minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/RSYNC/PUBKEY -->
|
||||||
|
<!-- BDISK/PROFILE/SYNC/RSYNC/PUBKEY -->
|
||||||
|
<xs:element name="password" maxOccurs="1" minOccurs="1"/>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/RSYNC/PUBKEY -->
|
||||||
|
</xs:choice>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="enabled" type="xs:boolean" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC/IPXE -->
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE/SYNC -->
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="id" type="xs:positiveInteger" use="optional"/>
|
||||||
|
<xs:attribute name="name" type="xs:string" use="optional"/>
|
||||||
|
<xs:attribute name="uuid" use="optional">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:pattern
|
||||||
|
value="[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK/PROFILE -->
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<!-- END BDISK -->
|
||||||
</xs:schema>
|
</xs:schema>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3.6
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Ironically enough, I think building a GUI for this would be *cleaner*.
|
# Ironically enough, I think building a GUI for this would be *cleaner*.
|
||||||
# Go figure.
|
# Go figure.
|
||||||
|
|
||||||
import confparse
|
import confparse
|
||||||
import crypt
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
import utils
|
import utils
|
||||||
@ -134,7 +134,12 @@ class ConfGenerator(object):
|
|||||||
self.cfg = c.xml
|
self.cfg = c.xml
|
||||||
self.append = True
|
self.append = True
|
||||||
else:
|
else:
|
||||||
self.cfg = lxml.etree.Element('bdisk')
|
_ns = {None: 'http://bdisk.square-r00t.net/',
|
||||||
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
|
||||||
|
_xsi = {
|
||||||
|
'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation':
|
||||||
|
'http://bdisk.square-r00t.net bdisk.xsd'}
|
||||||
|
self.cfg = lxml.etree.Element('bdisk', nsmap = _ns, attrib = _xsi)
|
||||||
self.append = False
|
self.append = False
|
||||||
self.profile = lxml.etree.Element('profile')
|
self.profile = lxml.etree.Element('profile')
|
||||||
self.cfg.append(self.profile)
|
self.cfg.append(self.profile)
|
||||||
@ -155,6 +160,13 @@ class ConfGenerator(object):
|
|||||||
self.get_pki()
|
self.get_pki()
|
||||||
self.get_gpg()
|
self.get_gpg()
|
||||||
self.get_sync()
|
self.get_sync()
|
||||||
|
# TODO: make this more specific (script? gui? web? etc.)
|
||||||
|
# and append comment to bdisk element
|
||||||
|
_comment = lxml.etree.Comment(
|
||||||
|
'Generated {0} by BDisk configuration generator'.format(
|
||||||
|
str(datetime.datetime.now())
|
||||||
|
)
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
exit('\n\nCaught KeyboardInterrupt; quitting...')
|
exit('\n\nCaught KeyboardInterrupt; quitting...')
|
||||||
return()
|
return()
|
||||||
@ -442,6 +454,8 @@ class ConfGenerator(object):
|
|||||||
tarball_elem.attrib['flags'] = 'latest'
|
tarball_elem.attrib['flags'] = 'latest'
|
||||||
tarball_elem.text = tarball['full_url']
|
tarball_elem.text = tarball['full_url']
|
||||||
print('\n++ SOURCES || {0} || CHECKSUM ++'.format(arch.upper()))
|
print('\n++ SOURCES || {0} || CHECKSUM ++'.format(arch.upper()))
|
||||||
|
# TODO: explicit not being set for explicitly-set sums,
|
||||||
|
# and checksum string not actually added to those. investigate.
|
||||||
chksum = lxml.etree.SubElement(source, 'checksum')
|
chksum = lxml.etree.SubElement(source, 'checksum')
|
||||||
_chksum_chk = prompt.confirm_or_no(prompt = (
|
_chksum_chk = prompt.confirm_or_no(prompt = (
|
||||||
'\nWould you like to add a checksum for the tarball? (BDisk '
|
'\nWould you like to add a checksum for the tarball? (BDisk '
|
||||||
@ -470,7 +484,7 @@ class ConfGenerator(object):
|
|||||||
print('Invalid selection. Starting over.')
|
print('Invalid selection. Starting over.')
|
||||||
continue
|
continue
|
||||||
chksum.attrib['hash_algo'] = checksum_type
|
chksum.attrib['hash_algo'] = checksum_type
|
||||||
chksum.attrib['explicit'] = "no"
|
chksum.attrib['explicit'] = "false"
|
||||||
chksum.text = checksum['full_url']
|
chksum.text = checksum['full_url']
|
||||||
else:
|
else:
|
||||||
# Maybe it's a digest string.
|
# Maybe it's a digest string.
|
||||||
@ -502,8 +516,8 @@ class ConfGenerator(object):
|
|||||||
print('Invalid selection. Starting over.')
|
print('Invalid selection. Starting over.')
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
checksum_type == checksum_type[0]
|
checksum_type = checksum_type[0]
|
||||||
chksum.attrib['explicit'] = "yes"
|
chksum.attrib['explicit'] = "true"
|
||||||
chksum.text = checksum
|
chksum.text = checksum
|
||||||
chksum.attrib['hash_algo'] = checksum_type
|
chksum.attrib['hash_algo'] = checksum_type
|
||||||
print('\n++ SOURCES || {0} || GPG ++'.format(arch.upper()))
|
print('\n++ SOURCES || {0} || GPG ++'.format(arch.upper()))
|
||||||
@ -595,7 +609,7 @@ class ConfGenerator(object):
|
|||||||
usage = (
|
usage = (
|
||||||
'{0} for yes, {1} for no...\n'))
|
'{0} for yes, {1} for no...\n'))
|
||||||
if _chk_optimizations:
|
if _chk_optimizations:
|
||||||
build.attrib['its_full_of_stars'] = 'yes'
|
build.attrib['its_full_of_stars'] = 'true'
|
||||||
print('\n++ BUILD || PATHS ++')
|
print('\n++ BUILD || PATHS ++')
|
||||||
# Thankfully, we can simplify a lot of this.
|
# Thankfully, we can simplify a lot of this.
|
||||||
_dir_strings = {'base': ('the base directory (used for files that are '
|
_dir_strings = {'base': ('the base directory (used for files that are '
|
||||||
@ -625,7 +639,7 @@ class ConfGenerator(object):
|
|||||||
'created that can be used to serve iPXE)'),
|
'created that can be used to serve iPXE)'),
|
||||||
'tftp': ('the TFTP directory (where a TFTP/'
|
'tftp': ('the TFTP directory (where a TFTP/'
|
||||||
'traditional PXE root is created)'),
|
'traditional PXE root is created)'),
|
||||||
'ssl': ('the SSL/TLS PKI directory (where we store '
|
'pki': ('the SSL/TLS PKI directory (where we store '
|
||||||
'the PKI structure we use/re-use - MAKE SURE '
|
'the PKI structure we use/re-use - MAKE SURE '
|
||||||
'it is in a path that is well-protected!)')}
|
'it is in a path that is well-protected!)')}
|
||||||
has_paths = False
|
has_paths = False
|
||||||
@ -676,9 +690,9 @@ class ConfGenerator(object):
|
|||||||
self.profile.append(iso)
|
self.profile.append(iso)
|
||||||
# We have more than one arch, so we need to ask how they want to handle
|
# We have more than one arch, so we need to ask how they want to handle
|
||||||
# it.
|
# it.
|
||||||
_ma_strings = {'yes': ('a multi-arch ISO (both architectures on one '
|
_ma_strings = {'true': ('a multi-arch ISO (both architectures on one '
|
||||||
'ISO)'),
|
'ISO)'),
|
||||||
'no': ('separate image files for '
|
'false': ('separate image files for '
|
||||||
'{0}').format(' and '.join(_arches))}
|
'{0}').format(' and '.join(_arches))}
|
||||||
for a in _arches:
|
for a in _arches:
|
||||||
_ma_strings[a] = 'only build an image file for {0}'.format(a)
|
_ma_strings[a] = 'only build an image file for {0}'.format(a)
|
||||||
@ -710,7 +724,7 @@ class ConfGenerator(object):
|
|||||||
'option to configure it a bit later).\nWould you like to sign '
|
'option to configure it a bit later).\nWould you like to sign '
|
||||||
'the ISO/USB image files with GPG?\n'), usage = (
|
'the ISO/USB image files with GPG?\n'), usage = (
|
||||||
'{0} for yes, {1} for no...\n'))
|
'{0} for yes, {1} for no...\n'))
|
||||||
_gpg_sign = ('yes' if _gpg_input else 'no')
|
_gpg_sign = ('true' if _gpg_input else 'false')
|
||||||
iso.attrib['sign'] = _gpg_sign
|
iso.attrib['sign'] = _gpg_sign
|
||||||
self.profile.append(iso)
|
self.profile.append(iso)
|
||||||
return()
|
return()
|
||||||
@ -725,21 +739,21 @@ class ConfGenerator(object):
|
|||||||
'see the manual for more information). Would you like to '
|
'see the manual for more information). Would you like to '
|
||||||
'build iPXE support?\n'), usage = (
|
'build iPXE support?\n'), usage = (
|
||||||
'{0} for yes, {1} for no...\n'))
|
'{0} for yes, {1} for no...\n'))
|
||||||
_ipxe = ('yes' if _ipxe else 'no')
|
_ipxe = ('true' if _ipxe else 'true')
|
||||||
if _ipxe == 'yes':
|
if _ipxe == 'true':
|
||||||
print('\n++ iPXE || MINI-ISO ++')
|
print('\n++ iPXE || MINI-ISO ++')
|
||||||
_iso = prompt.confirm_or_no(prompt = (
|
_iso = prompt.confirm_or_no(prompt = (
|
||||||
'\nWould you like to build a "mini-ISO" (see the manual) for '
|
'\nWould you like to build a "mini-ISO" (see the manual) for '
|
||||||
'bootstrapping iPXE booting from USB or optical media?\n'),
|
'bootstrapping iPXE booting from USB or optical media?\n'),
|
||||||
usage = ('{0} for yes, {1} for no...\n'))
|
usage = ('{0} for yes, {1} for no...\n'))
|
||||||
ipxe.attrib['iso'] = ('yes' if _iso else 'no')
|
ipxe.attrib['iso'] = ('true' if _iso else 'false')
|
||||||
print('\n++ iPXE || SIGNING ++')
|
print('\n++ iPXE || SIGNING ++')
|
||||||
_sign = prompt.confirm_or_no(prompt = (
|
_sign = prompt.confirm_or_no(prompt = (
|
||||||
'\nBDisk can sign the mini-ISO and other relevant files for '
|
'\nBDisk can sign the mini-ISO and other relevant files for '
|
||||||
'iPXE builds using GPG. Would you like to sign the iPXE build '
|
'iPXE builds using GPG. Would you like to sign the iPXE build '
|
||||||
'distributables? (You\'ll have the chance to configure GPG '
|
'distributables? (You\'ll have the chance to configure GPG '
|
||||||
'later).\n'), usage = ('{0} for yes, {1} for no...\n'))
|
'later).\n'), usage = ('{0} for yes, {1} for no...\n'))
|
||||||
ipxe.attrib['sign'] = ('yes' if _sign else 'no')
|
ipxe.attrib['sign'] = ('true' if _sign else 'false')
|
||||||
_uri = None
|
_uri = None
|
||||||
while not _uri:
|
while not _uri:
|
||||||
print('\n++ iPXE || URL ++')
|
print('\n++ iPXE || URL ++')
|
||||||
@ -754,7 +768,7 @@ class ConfGenerator(object):
|
|||||||
else:
|
else:
|
||||||
uri = lxml.etree.SubElement(ipxe, 'uri')
|
uri = lxml.etree.SubElement(ipxe, 'uri')
|
||||||
uri.text = _uri
|
uri.text = _uri
|
||||||
if _ipxe == 'yes':
|
if _ipxe == 'true':
|
||||||
self.profile.append(ipxe)
|
self.profile.append(ipxe)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
@ -780,7 +794,7 @@ class ConfGenerator(object):
|
|||||||
'wish to keep persistent keys and certs), you should '
|
'wish to keep persistent keys and certs), you should '
|
||||||
'DEFINITELY answer no here.\n'),
|
'DEFINITELY answer no here.\n'),
|
||||||
usage = ('{0} for yes, {1} for no...\n'))
|
usage = ('{0} for yes, {1} for no...\n'))
|
||||||
pki.attrib['overwrite'] = ('yes' if _overwrite else 'no')
|
pki.attrib['overwrite'] = ('true' if _overwrite else 'false')
|
||||||
for x in ('ca', 'client'):
|
for x in ('ca', 'client'):
|
||||||
print('\n++ SSL/TLS PKI || {0} ++'.format(x.upper()))
|
print('\n++ SSL/TLS PKI || {0} ++'.format(x.upper()))
|
||||||
_x = None
|
_x = None
|
||||||
@ -804,7 +818,7 @@ class ConfGenerator(object):
|
|||||||
for x in _xpaths:
|
for x in _xpaths:
|
||||||
_x = self.profile.xpath(x)
|
_x = self.profile.xpath(x)
|
||||||
for a in _x:
|
for a in _x:
|
||||||
if a == 'yes':
|
if a == 'true':
|
||||||
_sigchk = True
|
_sigchk = True
|
||||||
break
|
break
|
||||||
if _sigchk:
|
if _sigchk:
|
||||||
@ -848,7 +862,7 @@ class ConfGenerator(object):
|
|||||||
'\nWould you like to push the key to the SKS keyserver pool '
|
'\nWould you like to push the key to the SKS keyserver pool '
|
||||||
'(making it much easier for end-users to look it up)?\n'),
|
'(making it much easier for end-users to look it up)?\n'),
|
||||||
usage = ('{0} for yes, {1} for no...\n'))
|
usage = ('{0} for yes, {1} for no...\n'))
|
||||||
gpg.attrib['publish'] = ('yes' if _gpgpublish else 'no')
|
gpg.attrib['publish'] = ('true' if _gpgpublish else 'false')
|
||||||
print('\n++ GPG || PASSWORD HANDLING ++')
|
print('\n++ GPG || PASSWORD HANDLING ++')
|
||||||
_gpgpass_prompt = prompt.confirm_or_no(prompt = (
|
_gpgpass_prompt = prompt.confirm_or_no(prompt = (
|
||||||
'\nWould you like BDisk to prompt you for a passphrase? If not, '
|
'\nWould you like BDisk to prompt you for a passphrase? If not, '
|
||||||
@ -856,7 +870,8 @@ class ConfGenerator(object):
|
|||||||
'the configuration (HIGHLY unrecommended) or use a blank '
|
'the configuration (HIGHLY unrecommended) or use a blank '
|
||||||
'passphrase (also HIGHLY unrecommended).\n'),
|
'passphrase (also HIGHLY unrecommended).\n'),
|
||||||
usage = ('{0} for yes, {1} for no...\n'))
|
usage = ('{0} for yes, {1} for no...\n'))
|
||||||
gpg.attrib['prompt_passphrase'] = ('yes' if _gpgpass_prompt else 'no')
|
gpg.attrib['prompt_passphrase'] = ('true' if _gpgpass_prompt else
|
||||||
|
'false')
|
||||||
_pass = None
|
_pass = None
|
||||||
if not _gpgpass_prompt:
|
if not _gpgpass_prompt:
|
||||||
while not _pass:
|
while not _pass:
|
||||||
@ -921,7 +936,7 @@ class ConfGenerator(object):
|
|||||||
'\nWould you like to sync {0}?\n'.format(_syncs[s])),
|
'\nWould you like to sync {0}?\n'.format(_syncs[s])),
|
||||||
usage = ('{0} for yes, {1} for no...\n'))
|
usage = ('{0} for yes, {1} for no...\n'))
|
||||||
elem = lxml.etree.SubElement(sync, s)
|
elem = lxml.etree.SubElement(sync, s)
|
||||||
elem.attrib['enabled'] = ('yes' if _item_sync_chk else 'no')
|
elem.attrib['enabled'] = ('true' if _item_sync_chk else 'false')
|
||||||
if not _item_sync_chk:
|
if not _item_sync_chk:
|
||||||
continue
|
continue
|
||||||
if s == 'gpg':
|
if s == 'gpg':
|
||||||
@ -935,12 +950,12 @@ class ConfGenerator(object):
|
|||||||
'\n\t'.join(_choices)
|
'\n\t'.join(_choices)
|
||||||
))).strip().lower()
|
))).strip().lower()
|
||||||
if _export_type.startswith('a'):
|
if _export_type.startswith('a'):
|
||||||
_export_type == 'asc'
|
_export_type = 'asc'
|
||||||
elif _export_type.startswith('b'):
|
elif _export_type.startswith('b'):
|
||||||
_export_type == 'bin'
|
_export_type = 'bin'
|
||||||
else:
|
else:
|
||||||
print('Using the default.')
|
print('Using the default.')
|
||||||
_export_type == 'asc'
|
_export_type = 'asc'
|
||||||
elem.attrib['format'] = _export_type
|
elem.attrib['format'] = _export_type
|
||||||
_path = None
|
_path = None
|
||||||
while not _path:
|
while not _path:
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
import copy
|
import copy
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
import utils
|
import utils
|
||||||
import validators
|
|
||||||
import lxml.etree
|
import lxml.etree
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
etree = lxml.etree
|
etree = lxml.etree
|
||||||
|
detect = utils.detect()
|
||||||
|
generate = utils.generate()
|
||||||
|
transform = utils.transform()
|
||||||
|
valid = utils.valid()
|
||||||
|
|
||||||
class Conf(object):
|
class Conf(object):
|
||||||
def __init__(self, cfg, profile = None):
|
def __init__(self, cfg, profile = None, validate_cfg = False,
|
||||||
|
xsd_file = None):
|
||||||
"""
|
"""
|
||||||
A configuration object.
|
A configuration object.
|
||||||
|
|
||||||
@ -36,92 +41,351 @@ class Conf(object):
|
|||||||
|
|
||||||
You can provide any combination of these
|
You can provide any combination of these
|
||||||
(e.g. "profile={'id': 2, 'name' = 'some_profile'}").
|
(e.g. "profile={'id': 2, 'name' = 'some_profile'}").
|
||||||
|
Non-greedy matching (meaning ALL attributes specified
|
||||||
|
must match).
|
||||||
"""
|
"""
|
||||||
#self.raw = _detect_cfg(cfg) # no longer needed; in utils
|
if validate_cfg == 'pre':
|
||||||
|
# Validate before attempting any other operations
|
||||||
|
self.validate()
|
||||||
self.xml_suppl = utils.xml_supplicant(cfg, profile = profile)
|
self.xml_suppl = utils.xml_supplicant(cfg, profile = profile)
|
||||||
self.profile = self.xml_suppl
|
self.xml = self.xml_suppl.xml
|
||||||
self.xml = None
|
for e in self.xml_suppl.xml.iter():
|
||||||
self.profile = None
|
self.xml_suppl.substitute(e)
|
||||||
# Mad props to https://stackoverflow.com/a/12728199/733214
|
self.xml_suppl.get_profile(profile = self.xml_suppl.orig_profile)
|
||||||
self.xpath_re = re.compile('(?<=(?<!\{)\{)[^{}]*(?=\}(?!\}))')
|
with open('/tmp/parsed.xml', 'wb') as f:
|
||||||
self.substitutions = {}
|
f.write(lxml.etree.tostring(self.xml_suppl.xml))
|
||||||
self.xpaths = ['xpath']
|
self.profile = self.xml_suppl.profile
|
||||||
try:
|
self.xsd = xsd_file
|
||||||
self.xml = etree.fromstring(self.raw)
|
self.cfg = {}
|
||||||
except lxml.etree.XMLSyntaxError:
|
if validate_cfg:
|
||||||
raise ValueError('The configuration provided does not seem to be '
|
# Validation post-substitution
|
||||||
'valid')
|
self.validate(parsed = False)
|
||||||
self.xsd = None
|
# TODO: populate checksum{} with hash_algo if explicit
|
||||||
#if not self.validate(): # Need to write the XSD
|
|
||||||
# raise ValueError('The configuration did not pass XSD/schema '
|
def get_pki_obj(self, pki, pki_type):
|
||||||
# 'validation')
|
elem = {}
|
||||||
self.get_profile()
|
if pki_type not in ('ca', 'client'):
|
||||||
self.max_recurse = int(self.profile.xpath('//meta/'
|
raise ValueError('pki_type must be "ca" or "client"')
|
||||||
'max_recurse')[0].text)
|
if pki_type == 'ca':
|
||||||
|
elem['index'] = None
|
||||||
|
elem['serial'] = None
|
||||||
|
for e in pki.xpath('./*'):
|
||||||
|
# These have attribs or children.
|
||||||
|
if e.tag in ('cert', 'key', 'subject'):
|
||||||
|
elem[e.tag] = {}
|
||||||
|
if e.tag == 'subject':
|
||||||
|
for sub in e.xpath('./*'):
|
||||||
|
elem[e.tag][sub.tag] = transform.xml2py(sub.text,
|
||||||
|
attrib = False)
|
||||||
|
else:
|
||||||
|
for a in e.xpath('./@*'):
|
||||||
|
elem[e.tag][a.attrname] = transform.xml2py(a)
|
||||||
|
elem[e.tag]['path'] = e.text
|
||||||
|
else:
|
||||||
|
elem[e.tag] = e.text
|
||||||
|
return(elem)
|
||||||
|
|
||||||
|
def get_source(self, source, item, _source):
|
||||||
|
_source_item = {'flags': [], 'fname': None}
|
||||||
|
elem = source.xpath('./{0}'.format(item))[0]
|
||||||
|
if item == 'checksum':
|
||||||
|
if elem.get('explicit', False):
|
||||||
|
_explicit = transform.xml2py(
|
||||||
|
elem.attrib['explicit'])
|
||||||
|
_source_item['explicit'] = _explicit
|
||||||
|
if _explicit:
|
||||||
|
del(_source_item['fname'])
|
||||||
|
_source_item['value'] = elem.text
|
||||||
|
return(_source_item)
|
||||||
|
else:
|
||||||
|
_source_item['explicit'] = False
|
||||||
|
if elem.get('hash_algo', False):
|
||||||
|
_source_item['hash_algo'] = elem.attrib['hash_algo']
|
||||||
|
else:
|
||||||
|
_source_item['hash_algo'] = None
|
||||||
|
if item == 'sig':
|
||||||
|
if elem.get('keys', False):
|
||||||
|
_keys = [i.strip() for i in elem.attrib['keys'].split()]
|
||||||
|
_source_item['keys'] = _keys
|
||||||
|
else:
|
||||||
|
_source_item['keys'] = []
|
||||||
|
if elem.get('keyserver', False):
|
||||||
|
_source_item['keyserver'] = elem.attrib['keyserver']
|
||||||
|
else:
|
||||||
|
_source_item['keyserver'] = None
|
||||||
|
_item = elem.text
|
||||||
|
_flags = elem.get('flags', '')
|
||||||
|
if _flags:
|
||||||
|
for f in _flags.split():
|
||||||
|
if f.strip().lower() == 'none':
|
||||||
|
continue
|
||||||
|
_source_item['flags'].append(f.strip().lower())
|
||||||
|
if _source_item['flags']:
|
||||||
|
if 'regex' in _source_item['flags']:
|
||||||
|
ptrn = _item.format(**self.xml_suppl.btags['regex'])
|
||||||
|
else:
|
||||||
|
ptrn = None
|
||||||
|
_source_item['fname'] = detect.remote_files(
|
||||||
|
'/'.join((_source['mirror'],
|
||||||
|
_source['rootpath'])),
|
||||||
|
ptrn = ptrn,
|
||||||
|
flags = _source_item['flags'])
|
||||||
|
else:
|
||||||
|
_source_item['fname'] = _item
|
||||||
|
return(_source_item)
|
||||||
|
|
||||||
def get_xsd(self):
|
def get_xsd(self):
|
||||||
path = os.path.join(os.path.dirname(__file__),
|
if isinstance(self.xsd, lxml.etree.XMLSchema):
|
||||||
'bdisk.xsd')
|
return(self.xsd)
|
||||||
with open(path, 'r') as f:
|
if not self.xsd:
|
||||||
xsd = f.read()
|
path = os.path.join(os.path.dirname(__file__), 'bdisk.xsd')
|
||||||
|
else:
|
||||||
|
path = os.path.abspath(os.path.expanduser(self.xsd))
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
xsd = lxml.etree.parse(f)
|
||||||
return(xsd)
|
return(xsd)
|
||||||
|
|
||||||
def validate(self):
|
def parse_accounts(self):
|
||||||
self.xsd = etree.XMLSchema(self.get_xsd())
|
## PROFILE/ACCOUNTS
|
||||||
return(self.xsd.validate(self.xml))
|
self.cfg['users'] = []
|
||||||
|
# First we handle the root user, since it's a "special" case.
|
||||||
|
_root = self.profile.xpath('./accounts/rootpass')
|
||||||
|
self.cfg['root'] = transform.user(_root)
|
||||||
|
for user in self.profile.xpath('./accounts/user'):
|
||||||
|
_user = {'username': user.xpath('./username/text()')[0],
|
||||||
|
'sudo': transform.xml2py(user.attrib['sudo']),
|
||||||
|
'comment': None}
|
||||||
|
_comment = user.xpath('./comment/text()')
|
||||||
|
if len(_comment):
|
||||||
|
_user['comment'] = _comment[0]
|
||||||
|
_password = user.xpath('./password')
|
||||||
|
_user.update(transform.user(_password))
|
||||||
|
self.cfg['users'].append(_user)
|
||||||
|
return()
|
||||||
|
|
||||||
def get_profile(self):
|
def parse_all(self):
|
||||||
"""Get a configuration profile.
|
self.parse_profile()
|
||||||
|
self.parse_meta()
|
||||||
|
self.parse_accounts()
|
||||||
|
self.parse_sources()
|
||||||
|
self.parse_buildpaths()
|
||||||
|
self.parse_pki()
|
||||||
|
self.parse_gpg()
|
||||||
|
self.parse_sync()
|
||||||
|
return()
|
||||||
|
|
||||||
Get a configuration profile from the XML object and set that as a
|
def parse_buildpaths(self):
|
||||||
profile object. If a profile is specified, attempt to find it. If not,
|
## PROFILE/BUILD(/PATHS)
|
||||||
follow the default rules as specified in __init__.
|
self.cfg['build'] = {'paths': {}}
|
||||||
"""
|
build = self.profile.xpath('./build')[0]
|
||||||
if self.profile:
|
_optimize = build.get('its_full_of_stars', 'false')
|
||||||
# A profile identifier was provided
|
self.cfg['build']['optimize'] = transform.xml2py(_optimize)
|
||||||
if isinstance(self.profile, str):
|
for path in build.xpath('./paths/*'):
|
||||||
_profile_name = self.profile
|
self.cfg['build']['paths'][path.tag] = path.text
|
||||||
self.profile = {}
|
self.cfg['build']['basedistro'] = build.get('basedistro', 'archlinux')
|
||||||
for i in _profile_specifiers:
|
# iso and ipxe are their own basic profile elements, but we group them
|
||||||
self.profile[i] = None
|
# in here because 1.) they're related, and 2.) they're simple to
|
||||||
self.profile['name'] = _profile_name
|
# import. This may change in the future if they become more complex.
|
||||||
elif isinstance(self.profile, dict):
|
## PROFILE/ISO
|
||||||
for k in _profile_specifiers:
|
self.cfg['iso'] = {'sign': None,
|
||||||
if k not in self.profile.keys():
|
'multi_arch': None}
|
||||||
self.profile[k] = None
|
self.cfg['ipxe'] = {'sign': None,
|
||||||
else:
|
'iso': None}
|
||||||
raise TypeError('profile must be a string (name of profile), '
|
for x in ('iso', 'ipxe'):
|
||||||
'a dictionary, or None')
|
# We enable all features by default.
|
||||||
xpath = ('/bdisk/'
|
elem = self.profile.xpath('./{0}'.format(x))[0]
|
||||||
'profile{0}').format(_profile_xpath_gen(self.profile))
|
for a in self.cfg[x]:
|
||||||
self.profile = self.xml.xpath(xpath)
|
self.cfg[x][a] = transform.xml2py(elem.get(a, 'true'))
|
||||||
if not self.profile:
|
if x == 'ipxe':
|
||||||
raise RuntimeError('Could not find the profile specified in '
|
self.cfg[x]['uri'] = elem.xpath('./uri/text()')[0]
|
||||||
'the given configuration')
|
return()
|
||||||
else:
|
|
||||||
# We need to find the default.
|
def parse_gpg(self):
|
||||||
profiles = []
|
## PROFILE/GPG
|
||||||
for p in self.xml.xpath('/bdisk/profile'):
|
self.cfg['gpg'] = {'keyid': None,
|
||||||
profiles.append(p)
|
'gnupghome': None,
|
||||||
# Look for one named "default" or "DEFAULT" etc.
|
'publish': None,
|
||||||
for idx, value in enumerate([e.attrib['name'].lower() \
|
'prompt_passphrase': None,
|
||||||
for e in profiles]):
|
'keys': []}
|
||||||
if value == 'default':
|
elem = self.profile.xpath('./gpg')[0]
|
||||||
self.profile = copy.deepcopy(profiles[idx])
|
for attr in elem.xpath('./@*'):
|
||||||
break
|
self.cfg['gpg'][attr.attrname] = transform.xml2py(attr)
|
||||||
# We couldn't find a profile with a default name. Try to grab the
|
for key in elem.xpath('./key'):
|
||||||
# first profile.
|
_keytpl = {'algo': 'rsa',
|
||||||
if self.profile is None:
|
'keysize': '4096'}
|
||||||
# Grab the first profile.
|
_key = copy.deepcopy(_keytpl)
|
||||||
if profiles:
|
_key['name'] = None
|
||||||
self.profile = profile[0]
|
_key['email'] = None
|
||||||
|
_key['comment'] = None
|
||||||
|
for attr in key.xpath('./@*'):
|
||||||
|
_key[attr.attrname] = transform.xml2py(attr)
|
||||||
|
for param in key.xpath('./*'):
|
||||||
|
if param.tag == 'subkey':
|
||||||
|
# We only support one subkey (for key generation).
|
||||||
|
if 'subkey' not in _key:
|
||||||
|
_key['subkey'] = copy.deepcopy(_keytpl)
|
||||||
|
for attr in param.xpath('./@*'):
|
||||||
|
_key['subkey'][attr.attrname] = transform.xml2py(attr)
|
||||||
|
print(_key)
|
||||||
else:
|
else:
|
||||||
# No profiles found.
|
_key[param.tag] = transform.xml2py(param.text, attrib = False)
|
||||||
raise RuntimeError('Could not find any usable '
|
self.cfg['gpg']['keys'].append(_key)
|
||||||
'configuration profiles')
|
return()
|
||||||
|
|
||||||
|
def parse_meta(self):
|
||||||
|
## PROFILE/META
|
||||||
|
# Get the various meta strings. We skip regexes (we handle those
|
||||||
|
# separately since they're unique'd per id attrib) and variables (they
|
||||||
|
# are already substituted by self.xml_suppl.substitute(x)).
|
||||||
|
_meta_iters = ('dev', 'names')
|
||||||
|
for t in _meta_iters:
|
||||||
|
self.cfg[t] = {}
|
||||||
|
_xpath = './meta/{0}'.format(t)
|
||||||
|
for e in self.profile.xpath(_xpath):
|
||||||
|
for se in e:
|
||||||
|
if not isinstance(se, lxml.etree._Comment):
|
||||||
|
self.cfg[t][se.tag] = transform.xml2py(se.text,
|
||||||
|
attrib = False)
|
||||||
|
for e in ('desc', 'uri', 'ver', 'max_recurse'):
|
||||||
|
_xpath = './meta/{0}/text()'.format(e)
|
||||||
|
self.cfg[e] = transform.xml2py(self.profile.xpath(_xpath)[0],
|
||||||
|
attrib = False)
|
||||||
|
# HERE is where we would handle regex patterns.
|
||||||
|
# But we don't, because they're in self.xml_suppl.btags['regex'].
|
||||||
|
#self.cfg['regexes'] = {}
|
||||||
|
#_regexes = self.profile.xpath('./meta/regexes/pattern')
|
||||||
|
#if len(_regexes):
|
||||||
|
# for ptrn in _regexes:
|
||||||
|
# self.cfg['regexes'][ptrn.attrib['id']] = re.compile(ptrn.text)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parse_pki(self):
|
||||||
|
## PROFILE/PKI
|
||||||
|
self.cfg['pki'] = {'clients': []}
|
||||||
|
elem = self.profile.xpath('./pki')[0]
|
||||||
|
self.cfg['pki']['overwrite'] = transform.xml2py(
|
||||||
|
elem.get('overwrite', 'false'))
|
||||||
|
ca = elem.xpath('./ca')[0]
|
||||||
|
clients = elem.xpath('./client')
|
||||||
|
self.cfg['pki']['ca'] = self.get_pki_obj(ca, 'ca')
|
||||||
|
for client in clients:
|
||||||
|
self.cfg['pki']['clients'].append(self.get_pki_obj(client,
|
||||||
|
'client'))
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def parse_profile(self):
|
def parse_profile(self):
|
||||||
pass
|
## PROFILE
|
||||||
|
# The following are attributes of profiles that serve as identifiers.
|
||||||
|
self.cfg['profile'] = {'id': None,
|
||||||
|
'name': None,
|
||||||
|
'uuid': None}
|
||||||
|
for a in self.cfg['profile']:
|
||||||
|
if a in self.profile.attrib:
|
||||||
|
self.cfg['profile'][a] = transform.xml2py(
|
||||||
|
self.profile.attrib[a],
|
||||||
|
attrib = True)
|
||||||
|
# Small bug in transform.xml2py that we unfortunately can't fix, so we manually fix.
|
||||||
|
if 'id' in self.cfg['profile'] and isinstance(self.cfg['profile']['id'], bool):
|
||||||
|
self.cfg['profile']['id'] = int(self.cfg['profile']['id'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parse_sources(self):
|
||||||
|
## PROFILE/SOURCES
|
||||||
|
self.cfg['sources'] = []
|
||||||
|
for source in self.profile.xpath('./sources/source'):
|
||||||
|
_source = {}
|
||||||
|
_source['arch'] = source.attrib['arch']
|
||||||
|
_source['mirror'] = source.xpath('./mirror/text()')[0]
|
||||||
|
_source['rootpath'] = source.xpath('./rootpath/text()')[0]
|
||||||
|
# The tarball, checksum, and sig components requires some...
|
||||||
|
# special care.
|
||||||
|
for e in ('tarball', 'checksum', 'sig'):
|
||||||
|
_source[e] = self.get_source(source, e, _source)
|
||||||
|
self.cfg['sources'].append(_source)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parse_sync(self):
|
||||||
|
## PROFILE/SYNC
|
||||||
|
self.cfg['sync'] = {}
|
||||||
|
elem = self.profile.xpath('./sync')[0]
|
||||||
|
# We populate defaults in case they weren't specified.
|
||||||
|
for e in ('gpg', 'ipxe', 'iso', 'tftp'):
|
||||||
|
self.cfg['sync'][e] = {'enabled': False,
|
||||||
|
'path': None}
|
||||||
|
sub = elem.xpath('./{0}'.format(e))[0]
|
||||||
|
for a in sub.xpath('./@*'):
|
||||||
|
self.cfg['sync'][e][a.attrname] = transform.xml2py(a)
|
||||||
|
self.cfg['sync'][e]['path'] = sub.text
|
||||||
|
rsync = elem.xpath('./rsync')[0]
|
||||||
|
self.cfg['sync']['rsync'] = {'enabled': False}
|
||||||
|
for a in rsync.xpath('./@*'):
|
||||||
|
self.cfg['sync']['rsync'][a.attrname] = transform.xml2py(a)
|
||||||
|
for sub in rsync.xpath('./*'):
|
||||||
|
self.cfg['sync']['rsync'][sub.tag] = transform.xml2py(
|
||||||
|
sub.text,
|
||||||
|
attrib = False)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def validate(self, parsed = False):
|
||||||
|
xsd = self.get_xsd()
|
||||||
|
if not isinstance(xsd, lxml.etree.XMLSchema):
|
||||||
|
self.xsd = etree.XMLSchema(xsd)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# This would return a bool if it validates or not.
|
||||||
|
#self.xsd.validate(self.xml)
|
||||||
|
# We want to get a more detailed exception.
|
||||||
|
xml = etree.fromstring(self.xml_suppl.return_full())
|
||||||
|
self.xsd.assertValid(xml)
|
||||||
|
if parsed:
|
||||||
|
# We wait until after it's parsed to evaluate because otherwise we
|
||||||
|
# can't use utils.valid().
|
||||||
|
# We only bother with stuff that would hinder building, though -
|
||||||
|
# e.g. we don't check that profile's UUID is a valid UUID4.
|
||||||
|
# The XSD can catch a lot of stuff, but it's not so hot with things like URI validation,
|
||||||
|
# email validation, etc.
|
||||||
|
# URLs
|
||||||
|
for url in (self.cfg['uri'], self.cfg['dev']['website']):
|
||||||
|
if not valid.url(url):
|
||||||
|
raise ValueError('{0} is not a valid URL.'.format(url))
|
||||||
|
# Emails
|
||||||
|
for k in self.cfg['gpg']['keys']:
|
||||||
|
if not valid.email(k['email']):
|
||||||
|
raise ValueError('GPG key {0}: {1} is not a valid email address'.format(k['name'], k['email']))
|
||||||
|
if not valid.email(self.cfg['dev']['email']):
|
||||||
|
raise ValueError('{0} is not a valid email address'.format(self.cfg['dev']['email']))
|
||||||
|
if self.cfg['pki']:
|
||||||
|
if 'subject' in self.cfg['pki']['ca']:
|
||||||
|
if not valid.email(self.cfg['pki']['ca']['subject']['emailAddress']):
|
||||||
|
raise ValueError('{0} is not a valid email address'.format(
|
||||||
|
self.cfg['pki']['ca']['subject']['emailAddress']))
|
||||||
|
for cert in self.cfg['pki']['clients']:
|
||||||
|
if not cert['subject']:
|
||||||
|
continue
|
||||||
|
if not valid.email(cert['subject']['emailAddress']):
|
||||||
|
raise ValueError('{0} is not a valid email address'.format(cert['subject']['email']))
|
||||||
|
# Salts/hashes
|
||||||
|
if self.cfg['root']['salt']:
|
||||||
|
if not valid.salt_hash(self.cfg['root']['salt']):
|
||||||
|
raise ValueError('{0} is not a valid salt'.format(self.cfg['root']['salt']))
|
||||||
|
if self.cfg['root']['hashed']:
|
||||||
|
if not valid.salt_hash_full(self.cfg['root']['salt_hash'], self.cfg['root']['hash_algo']):
|
||||||
|
raise ValueError('{0} is not a valid hash of type {1}'.format(self.cfg['root']['salt_hash'],
|
||||||
|
self.cfg['root']['hash_algo']))
|
||||||
|
for u in self.cfg['users']:
|
||||||
|
if u['salt']:
|
||||||
|
if not valid.salt_hash(u['salt']):
|
||||||
|
raise ValueError('{0} is not a valid salt'.format(u['salt']))
|
||||||
|
if u['hashed']:
|
||||||
|
if not valid.salt_hash_full(u['salt_hash'], u['hash_algo']):
|
||||||
|
raise ValueError('{0} is not a valid hash of type {1}'.format(u['salt_hash'], u['hash_algo']))
|
||||||
|
# GPG Key IDs
|
||||||
|
if self.cfg['gpg']['keyid']:
|
||||||
|
if not valid.gpgkeyID(self.cfg['gpg']['keyid']):
|
||||||
|
raise ValueError('{0} is not a valid GPG Key ID/fingerprint'.format(self.cfg['gpg']['keyid']))
|
||||||
|
for s in self.cfg['sources']:
|
||||||
|
if 'sig' in s:
|
||||||
|
for k in s['sig']['keys']:
|
||||||
|
if not valid.gpgkeyID(k):
|
||||||
|
raise ValueError('{0} is not a valid GPG Key ID/fingerprint'.format(k))
|
||||||
|
return()
|
||||||
|
@ -1,3 +1,67 @@
|
|||||||
import copy
|
import hashlib
|
||||||
import importlib
|
import importlib # needed for the guest-os-specific stuff...
|
||||||
import os
|
import os
|
||||||
|
from . import utils
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
|
||||||
|
def hashsum_downloader(url, filename = None):
|
||||||
|
# TODO: support "latest" and "regex" flags? or remove from specs (since the tarball can be specified by these)?
|
||||||
|
# move that to the utils.DOwnload() class?
|
||||||
|
d = utils.Download(url, progress = False)
|
||||||
|
hashes = {os.path.basename(k):v for (v, k) in [line.split() for line in d.fetch().decode('utf-8').splitlines()]}
|
||||||
|
if filename:
|
||||||
|
if filename in hashes:
|
||||||
|
return(hashes[filename])
|
||||||
|
else:
|
||||||
|
raise KeyError('Filename {0} not in the list of hashes'.format(filename))
|
||||||
|
return(hashes)
|
||||||
|
|
||||||
|
|
||||||
|
class Prepper(object):
|
||||||
|
def __init__(self, dirs, sources, gpg = None):
|
||||||
|
# dirs is a ConfParse.cfg['build']['paths'] dict of dirs
|
||||||
|
self.CreateDirs(dirs)
|
||||||
|
# TODO: set up GPG env here so we can use it to import sig key and verify sources
|
||||||
|
for idx, s in enumerate(sources):
|
||||||
|
self._download(idx)
|
||||||
|
|
||||||
|
def CreateDirs(self, dirs):
|
||||||
|
for d in dirs:
|
||||||
|
os.makedirs(d, exist_ok = True)
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def _download(self, source_idx):
|
||||||
|
download = True
|
||||||
|
_source = self.cfg['sources'][source_idx]
|
||||||
|
_dest_dir = os.path.join(self.cfg['build']['paths']['cache'], source_idx)
|
||||||
|
_tarball = os.path.join(_dest_dir, _source['tarball']['fname'])
|
||||||
|
_remote_dir = urljoin(_source['mirror'], _source['rootpath'])
|
||||||
|
_remote_tarball = urljoin(_remote_dir + '/', _source['tarball']['fname'])
|
||||||
|
def _hash_verify(): # TODO: move to utils.valid()?
|
||||||
|
# Get a checksum.
|
||||||
|
if 'checksum' in _source:
|
||||||
|
if not _source['checksum']['explicit']:
|
||||||
|
_source['checksum']['value'] = hashsum_downloader(urljoin(_remote_dir + '/',
|
||||||
|
_source['checksum']['fname']))
|
||||||
|
if not _source['checksum']['hash_algo']:
|
||||||
|
_source['checksum']['hash_algo'] = utils.detect.any_hash(_source['checksum']['value'],
|
||||||
|
normalize = True)[0]
|
||||||
|
_hash = hashlib.new(_source['checksum']['hash_algo'])
|
||||||
|
with open(_tarball, 'rb') as f:
|
||||||
|
# It's potentially a large file, so we chunk it 64kb at a time.
|
||||||
|
_hashbuf = f.read(64000)
|
||||||
|
while len(_hashbuf) > 0:
|
||||||
|
_hash.update(_hashbuf)
|
||||||
|
_hashbuf = f.read(64000)
|
||||||
|
if _hash.hexdigest().lower() != _source['checksum']['value'].lower():
|
||||||
|
return(False)
|
||||||
|
return(True)
|
||||||
|
def _sig_verify(gpg_instance): # TODO: move to utils.valid()? or just use as part of the bdisk.GPG module?
|
||||||
|
pass
|
||||||
|
if os.path.isfile(_tarball):
|
||||||
|
download = _hash_verify()
|
||||||
|
download = _sig_verify()
|
||||||
|
if download:
|
||||||
|
d = utils.Download(_remote_tarball)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3.6
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import confparse
|
import confparse
|
||||||
@ -14,8 +14,10 @@ def parseArgs():
|
|||||||
epilog = ('https://git.square-r00t.net'))
|
epilog = ('https://git.square-r00t.net'))
|
||||||
return(args)
|
return(args)
|
||||||
|
|
||||||
def run():
|
def run(cfg):
|
||||||
pass
|
cfg = confparse.Conf(cfg, validate_cfg = True)
|
||||||
|
cfg.parse_all()
|
||||||
|
|
||||||
|
|
||||||
def run_interactive():
|
def run_interactive():
|
||||||
args = vars(parseArgs().parse_args())
|
args = vars(parseArgs().parse_args())
|
||||||
|
396
bdisk/mtree.py
Executable file
396
bdisk/mtree.py
Executable file
@ -0,0 +1,396 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import grp
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
from collections import OrderedDict
|
||||||
|
try:
|
||||||
|
import pycksum
|
||||||
|
has_cksum = True
|
||||||
|
except ImportError:
|
||||||
|
has_cksum = False
|
||||||
|
|
||||||
|
# Parse BSD mtree spec files.
|
||||||
|
# On arch, BSD mtree is ported in the AUR as nmtree.
|
||||||
|
# TODO: add a generator class as well? (in process)
|
||||||
|
# TODO: add a checking function as well?
|
||||||
|
|
||||||
|
# The format used for headers
|
||||||
|
_header_strptime_fmt = '%a %b %d %H:%M:%S %Y'
|
||||||
|
|
||||||
|
# Supported hash types (for generation). These are globally available always.
|
||||||
|
_hashtypes = ['md5', 'sha1', 'sha256', 'sha384', 'sha512']
|
||||||
|
# If RIPEMD-160 is supported, we add it (after MD5).
|
||||||
|
if 'ripemd160' in hashlib.algorithms_available:
|
||||||
|
_hashtypes.insert(1, 'rmd160')
|
||||||
|
|
||||||
|
# Iterative to determine which type an item is.
|
||||||
|
_stype_map = {'block': stat.S_ISBLK,
|
||||||
|
'char': stat.S_ISCHR,
|
||||||
|
'dir': stat.S_ISDIR,
|
||||||
|
'fifo': stat.S_ISFIFO,
|
||||||
|
'file': stat.S_ISREG,
|
||||||
|
'link': stat.S_ISLNK,
|
||||||
|
'socket': stat.S_ISSOCK}
|
||||||
|
|
||||||
|
# Regex pattern for cleaning up an octal perm mode into a string representation.
|
||||||
|
_octre = re.compile('^0o')
|
||||||
|
|
||||||
|
class MTreeGen(object):
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = pathlib.PosixPath(os.path.abspath(os.path.expanduser(path)))
|
||||||
|
# These are used to keep a cached copy of the info.
|
||||||
|
self._sysinfo = {'uids': {}, 'gids': {}}
|
||||||
|
self._build_header()
|
||||||
|
# We use this to keep track of where we are exactly in the tree so we can generate a full absolute path at
|
||||||
|
# any moment relative to the tree.
|
||||||
|
self._path_pointer = copy.deepcopy(self.path)
|
||||||
|
|
||||||
|
|
||||||
|
def paths_iterator(self):
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for f in files:
|
||||||
|
_fname = self.path.joinpath(f)
|
||||||
|
_stats = self._get_stats(_fname)
|
||||||
|
if not _stats:
|
||||||
|
print(('WARNING: {0} either disappeared while we were trying to parse it or '
|
||||||
|
'it is a broken symlink.').format(_fname))
|
||||||
|
continue
|
||||||
|
# TODO: get /set line here?
|
||||||
|
item = ' {0} \\\n'.format(f)
|
||||||
|
_type = 'file' # TODO: stat this more accurately
|
||||||
|
_cksum = self._gen_cksum(_fname)
|
||||||
|
item += ' {0} {1} {2}\\\n'.format(_stats['size'],
|
||||||
|
_stats['time'],
|
||||||
|
('{0} '.format(_cksum) if _cksum else ''))
|
||||||
|
# TODO: here's where the hashes would get added
|
||||||
|
# TODO: here's where we parse dirs. maybe do that before files?
|
||||||
|
# remember: mtree specs use ..'s to traverse upwards when done with a dir
|
||||||
|
for d in dirs:
|
||||||
|
_dname = self.path.joinpath(d)
|
||||||
|
_stats = self._get_stats(_dname)
|
||||||
|
if not _stats:
|
||||||
|
print(('WARNING: {0} either disappeared while we were trying to parse it or '
|
||||||
|
'it is a broken symlink.').format(_dname))
|
||||||
|
continue
|
||||||
|
# TODO: get /set line here?
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_cksum(self, fpath):
|
||||||
|
if not has_cksum:
|
||||||
|
return(None)
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
return(None)
|
||||||
|
# TODO: waiting on https://github.com/sobotklp/pycksum/issues/2 for byte iteration (because large files maybe?)
|
||||||
|
c = pycksum.Cksum()
|
||||||
|
with open(fpath, 'rb') as f:
|
||||||
|
c.add(f)
|
||||||
|
return(c.get_cksum())
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stats(self, path):
|
||||||
|
stats = {}
|
||||||
|
try:
|
||||||
|
_st = os.stat(path, follow_symlinks = False)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Broken symlink? Shouldn't occur since follow_symlinks is False anyways, BUT...
|
||||||
|
return(None)
|
||||||
|
# Ownership
|
||||||
|
stats['uid'] = _st.st_uid
|
||||||
|
stats['gid'] = _st.st_gid
|
||||||
|
if _st.st_uid in self._sysinfo['uids']:
|
||||||
|
stats['uname'] = self._sysinfo['uids'][_st.st_uid]
|
||||||
|
else:
|
||||||
|
_pw = pwd.getpwuid(_st.st_uid).pw_name
|
||||||
|
stats['uname'] = _pw
|
||||||
|
self._sysinfo['uids'][_st.stuid] = _pw
|
||||||
|
if _st.st_gid in self._sysinfo['gids']:
|
||||||
|
stats['gname'] = self._sysinfo['gids'][_st.st_gid]
|
||||||
|
else:
|
||||||
|
_grp = grp.getgrgid(_st.st_gid).gr_name
|
||||||
|
stats['gname'] = _grp
|
||||||
|
self._sysinfo['gids'][_st.stgid] = _grp
|
||||||
|
# Type and Mode
|
||||||
|
for t in _stype_map:
|
||||||
|
if _stype_map[t](_st.st_mode):
|
||||||
|
stats['type'] = t
|
||||||
|
# TODO: need a reliable way of parsing this.
|
||||||
|
# for instance, for /dev/autofs, _st.st_dev = 6 (os.makedev(6) confirms major is 0, minor is 6)
|
||||||
|
# but netBSD mtree (ported) says it's "0xaeb" (2795? or, as str, "®b" apparently).
|
||||||
|
# I'm guessing the kernel determines this, but where is it pulling it from/how?
|
||||||
|
# We can probably do 'format,major,minor' (or, for above, 'linux,0,6').
|
||||||
|
# if t in ('block', 'char'):
|
||||||
|
# stats['device'] = None
|
||||||
|
# Handle symlinks.
|
||||||
|
if t == 'link':
|
||||||
|
_target = path
|
||||||
|
while os.path.islink(_target):
|
||||||
|
_target = os.path.realpath(_target)
|
||||||
|
stats['link'] = _target
|
||||||
|
break
|
||||||
|
stats['mode'] = '{0:0>4}'.format(_octre.sub('', str(oct(stat.S_IMODE(_st.st_mode)))))
|
||||||
|
stats['size'] = _st.st_size
|
||||||
|
stats['time'] = str(float(_st.st_mtime))
|
||||||
|
stats['nlink'] = _st.st_nlink
|
||||||
|
# TODO: "flags" keyword? is that meaningful on linux?
|
||||||
|
stats['flags'] = 'none'
|
||||||
|
return(stats)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_hashes(self, fpath):
|
||||||
|
hashes = OrderedDict({})
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
return(hashes)
|
||||||
|
_hashnums = len(_hashtypes)
|
||||||
|
for idx, h in enumerate(_hashtypes):
|
||||||
|
# Stupid naming inconsistencies.
|
||||||
|
_hashname = (h if h is not 'rmd160' else 'ripemd160')
|
||||||
|
_hasher = hashlib.new(_hashname)
|
||||||
|
with open(fpath, 'rb') as f:
|
||||||
|
# Hash 64kb at a time in case it's a huge file. TODO: is this the most ideal chunk size?
|
||||||
|
_hashbuf = f.read(64000)
|
||||||
|
while len(_hashbuf) > 0:
|
||||||
|
_hasher.update(_hashbuf)
|
||||||
|
_hashbuf = f.read(64000)
|
||||||
|
hashes[h] = _hasher.hexdigest()
|
||||||
|
return(hashes)
|
||||||
|
# if idx + 1 < _hashnums:
|
||||||
|
# hashes += ' {0}={1} \\\n'.format(h, _hasher.hexdigest())
|
||||||
|
# else:
|
||||||
|
# hashes += ' {0}={1}\n'.format(h, _hasher.hexdigest())
|
||||||
|
# return(hashes)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_header(self):
|
||||||
|
self.spec = ''
|
||||||
|
_header = OrderedDict({})
|
||||||
|
_header['user'] = pwd.getpwuid(os.geteuid()).pw_name
|
||||||
|
_header['machine'] = platform.node()
|
||||||
|
_header['tree'] = str(self.path)
|
||||||
|
_header['date'] = datetime.datetime.utcnow().strftime(_header_strptime_fmt)
|
||||||
|
for h in _header:
|
||||||
|
self.spec += '#\t{0:>7}: {1}\n'.format(h, _header[h])
|
||||||
|
self.spec += '\n'
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MTreeParse(object):
|
||||||
|
def __init__(self, spec):
|
||||||
|
if not isinstance(spec, (str, bytes)):
|
||||||
|
raise ValueError('spec must be a raw string of the spec or a bytes object of the string')
|
||||||
|
if isinstance(spec, bytes):
|
||||||
|
try:
|
||||||
|
spec = spec.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ValueError('spec must be a utf-8 encoded set of bytes if using byte mode')
|
||||||
|
self.orig_spec = copy.deepcopy(spec) # For referencing in case someone wanted to write it out.
|
||||||
|
# We NOW need to handle the escaped linebreaking it does.
|
||||||
|
self._specdata = re.sub('\\\\\s+', '', spec).splitlines()
|
||||||
|
self._get_header()
|
||||||
|
self.spec = {'header': self.header,
|
||||||
|
'paths': {}}
|
||||||
|
# Template for an item.
|
||||||
|
# Default keywords are:
|
||||||
|
# flags, gid, link, mode, nlink, size, time, type, uid
|
||||||
|
self._tplitem = {
|
||||||
|
'type': None, # ('block', 'char', 'dir', 'fifo', 'file', 'link', 'socket')
|
||||||
|
# checksum of file (if it's a file) (int)
|
||||||
|
# On all *nix platforms, the cksum(1) utility (which is what the mtree spec uses) follows
|
||||||
|
# the POSIX standard CRC (which is NOT CRC-1/CRC-16 nor CRC32!):
|
||||||
|
# http://pubs.opengroup.org/onlinepubs/009695299/utilities/cksum.html
|
||||||
|
# For a python implementation,
|
||||||
|
# https://stackoverflow.com/questions/6835381/python-equivalent-of-unix-cksum-function
|
||||||
|
# See also crcmod (in PyPi).
|
||||||
|
'cksum': None,
|
||||||
|
# "The device number to use for block or char file types." Should be converted to a tuple of one
|
||||||
|
# of the following:
|
||||||
|
# - (format(str), major(int), minor(int))
|
||||||
|
# - (format(str), major(int), unit(str?), subunit(str?)) (only used on bsdos formats)
|
||||||
|
# - (number(int?), ) ("opaque" number)
|
||||||
|
# Valid formats are, per man page of mtree:
|
||||||
|
# native, 386bsd, 4bsd, bsdos, freebsd, hpux, isc, linux, netbsd, osf1, sco, solaris, sunos,
|
||||||
|
# svr3, svr4, ultrix
|
||||||
|
'device': None,
|
||||||
|
# File flags as symbolic name. BSD-specific thing? TODO: testing on BSD system
|
||||||
|
'flags': [],
|
||||||
|
'ignore': False, # An mtree-internal flag to ignore hierarchy under this item
|
||||||
|
'gid': None, # The group ID (int)
|
||||||
|
'gname': None, # The group name (str)
|
||||||
|
'link': None, # The link target/source, if a link.
|
||||||
|
# The MD5 checksum digest (str? hex?). "md5digest" is a synonym for this, so it's consolidated in
|
||||||
|
# as the same keyword.
|
||||||
|
'md5': None,
|
||||||
|
# The mode (in octal) (we convert it to a python-native int for os.chmod/stat, etc.)
|
||||||
|
# May also be a symbolic value; TODO: map symbolic to octal/int.
|
||||||
|
'mode': None,
|
||||||
|
'nlink': None, # Number of hard links for this item.
|
||||||
|
'optional': False, # This item may or may not be present in the compared directory for checking.
|
||||||
|
'rmd160': None, # The RMD-160 checksum of the file. "rmd160digest" is a synonym.
|
||||||
|
'sha1': None, # The SHA-1 sum. "sha1digest" is a synonym.
|
||||||
|
'sha256': None, # SHA-2 256-bit checksum; "sha256digest" is a synonym.
|
||||||
|
'sha384': None, # SHA-2 384-bit checksum; "sha384digest" is a synonym.
|
||||||
|
'sha512': None, # SHA-2 512-bit checksum; "sha512digest" is a synonym.
|
||||||
|
'size': None, # Size of the file in bytes (int).
|
||||||
|
'tags': [], # mtree-internal tags (comma-separated in the mtree spec).
|
||||||
|
'time': None, # Time the file was last modified (in Epoch fmt as float).
|
||||||
|
'uid': None, # File owner UID (int)
|
||||||
|
'uname': None # File owner username (str)
|
||||||
|
# And lastly, "children" is where the children files/directories go. We don't include it in the template;
|
||||||
|
# it's added programmatically.
|
||||||
|
# 'children': {}
|
||||||
|
}
|
||||||
|
# Global aspects are handled by "/set" directives.
|
||||||
|
# They are restored by an "/unset". Since they're global and stateful, they're handled as a class attribute.
|
||||||
|
self.settings = copy.deepcopy(self._tplitem)
|
||||||
|
self._parse_items()
|
||||||
|
del(self.settings, self._tplitem)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_header(self):
|
||||||
|
self.header = {}
|
||||||
|
_headre = re.compile('^#\s+(user|machine|tree|date):\s')
|
||||||
|
_cmtre = re.compile('^\s*#\s*')
|
||||||
|
_blklnre = re.compile('^\s*$')
|
||||||
|
for idx, line in enumerate(self._specdata):
|
||||||
|
if _headre.search(line): # We found a header item.
|
||||||
|
l = [i.lstrip() for i in _cmtre.sub('', line).split(':', 1)]
|
||||||
|
header = l[0]
|
||||||
|
val = (l[1] if l[1] is not '(null)' else None)
|
||||||
|
if header == 'date':
|
||||||
|
val = datetime.datetime.strptime(val, _header_strptime_fmt)
|
||||||
|
elif header == 'tree':
|
||||||
|
val = pathlib.PosixPath(val)
|
||||||
|
self.header[header] = val
|
||||||
|
elif _blklnre.search(line):
|
||||||
|
break # We've reached the end of the header. Otherwise...
|
||||||
|
else: # We definitely shouldn't be here, but this means the spec doesn't even have a header.
|
||||||
|
break
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_items(self):
|
||||||
|
# A pattern (compiled for performance) to match commands.
|
||||||
|
_stngsre = re.compile('^/(un)?set\s')
|
||||||
|
# Per the man page:
|
||||||
|
# "Empty lines and lines whose first non-whitespace character is a hash mark (‘#’) are ignored."
|
||||||
|
_ignre = re.compile('^(\s*(#.*)?)?$')
|
||||||
|
# The following regex is used to quickly and efficiently check for a synonymized hash name.
|
||||||
|
_hashre = re.compile('^(md5|rmd160|sha1|sha256|sha384|sha512)(digest)?$')
|
||||||
|
# The following regex is to test if we need to traverse upwards in the path.
|
||||||
|
_parentre = re.compile('^\.{,2}/?$')
|
||||||
|
# _curpath = self.header['tree']
|
||||||
|
_curpath = pathlib.PosixPath('/')
|
||||||
|
_types = ('block', 'char', 'dir', 'fifo', 'file', 'link', 'socket')
|
||||||
|
# This parses keywords. Used by both item specs and /set.
|
||||||
|
def _kwparse(kwline):
|
||||||
|
out = {}
|
||||||
|
for i in kwline:
|
||||||
|
l = i.split('=', 1)
|
||||||
|
if len(l) < 2:
|
||||||
|
l.append(None)
|
||||||
|
k, v = l
|
||||||
|
if v == 'none':
|
||||||
|
v = None
|
||||||
|
# These are represented as octals.
|
||||||
|
if k in ('mode', ):
|
||||||
|
# TODO: handle symbolic references too (e.g. rwxrwxrwx)
|
||||||
|
if v.isdigit():
|
||||||
|
v = int(v, 8) # Convert from the octal. This can then be used directly with os.chmod etc.
|
||||||
|
# These are represented as ints
|
||||||
|
elif k in ('uid', 'gid', 'cksum', 'nlink'):
|
||||||
|
if v.isdigit():
|
||||||
|
v = int(v)
|
||||||
|
# These are booleans (represented as True by their presence).
|
||||||
|
elif k in ('ignore', 'optional'):
|
||||||
|
v = True
|
||||||
|
# These are lists (comma-separated).
|
||||||
|
elif k in ('flags', 'tags'):
|
||||||
|
if v:
|
||||||
|
v = [i.strip() for i in v.split(',')]
|
||||||
|
# The following are synonyms.
|
||||||
|
elif _hashre.search(k):
|
||||||
|
k = _hashre.sub('\g<1>', k)
|
||||||
|
elif k == 'time':
|
||||||
|
v = datetime.datetime.fromtimestamp(float(v))
|
||||||
|
elif k == 'type':
|
||||||
|
if v not in _types:
|
||||||
|
raise ValueError('{0} not one of: {1}'.format(v, ', '.join(_types)))
|
||||||
|
out[k] = v
|
||||||
|
return(out)
|
||||||
|
def _unset_parse(unsetline):
|
||||||
|
out = {}
|
||||||
|
if unsetline[1] == 'all':
|
||||||
|
return(copy.deepcopy(self._tplitem))
|
||||||
|
for i in unsetline:
|
||||||
|
out[i] = self._tplitem[i]
|
||||||
|
return(out)
|
||||||
|
# The Business-End (TM)
|
||||||
|
for idx, line in enumerate(self._specdata):
|
||||||
|
_fname = copy.deepcopy(_curpath)
|
||||||
|
# Skip these lines
|
||||||
|
if _ignre.search(line):
|
||||||
|
continue
|
||||||
|
l = line.split()
|
||||||
|
if _parentre.search(line):
|
||||||
|
_curpath = _curpath.parent
|
||||||
|
elif not _stngsre.search(line):
|
||||||
|
# So it's an item, not a command.
|
||||||
|
_itemsettings = copy.deepcopy(self.settings)
|
||||||
|
_itemsettings.update(_kwparse(l[1:]))
|
||||||
|
if _itemsettings['type'] == 'dir':
|
||||||
|
# SOMEONE PLEASE let me know if there's a cleaner way to do this.
|
||||||
|
_curpath = pathlib.PosixPath(os.path.normpath(_curpath.joinpath(l[0])))
|
||||||
|
_fname = _curpath
|
||||||
|
else:
|
||||||
|
_fname = pathlib.PosixPath(os.path.normpath(_curpath.joinpath(l[0])))
|
||||||
|
self.spec['paths'][_fname] = _itemsettings
|
||||||
|
else:
|
||||||
|
# It's a command. We can safely split on whitespace since the man page specifies the
|
||||||
|
# values are not to contain whitespace.
|
||||||
|
# /set
|
||||||
|
if l[0] == '/set':
|
||||||
|
del(l[0])
|
||||||
|
self.settings.update(_kwparse(l))
|
||||||
|
# /unset
|
||||||
|
else:
|
||||||
|
self.settings.update(_unset_parse(l))
|
||||||
|
continue
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = 'An mtree parser')
|
||||||
|
# TODO: support stdin piping
|
||||||
|
args.add_argument('specfile',
|
||||||
|
help = 'The path to the spec file to parse')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
# Allow to be run as a CLI utility as well.
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
import os
|
||||||
|
with open(os.path.abspath(os.path.expanduser(args['specfile']))) as f:
|
||||||
|
mt = MTreeParse(f.read())
|
||||||
|
with open('/tmp/newspec', 'w') as f:
|
||||||
|
f.write('\n'.join(mt._specdata))
|
||||||
|
import pprint
|
||||||
|
import inspect
|
||||||
|
del(mt.orig_spec)
|
||||||
|
del(mt._specdata)
|
||||||
|
import shutil
|
||||||
|
pprint.pprint(inspect.getmembers(mt), width = shutil.get_terminal_size()[0])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
129
bdisk/prompt_strings.py
Normal file
129
bdisk/prompt_strings.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# These are *key* ciphers, for encrypting exported keys.
|
||||||
|
openssl_ciphers = ['aes128', 'aes192', 'aes256', 'bf', 'blowfish',
|
||||||
|
'camellia128', 'camellia192', 'camellia256', 'cast', 'des',
|
||||||
|
'des3', 'idea', 'rc2', 'seed']
|
||||||
|
# These are *hash algorithms* for cert digests.
|
||||||
|
openssl_digests = ['blake2b512', 'blake2s256', 'gost', 'md4', 'md5', 'mdc2',
|
||||||
|
'rmd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
|
||||||
|
|
||||||
|
class PromptStrings(object):
|
||||||
|
gpg = {
|
||||||
|
'attribs': {
|
||||||
|
'algo': {
|
||||||
|
'text': 'the subkey\'s encryption type/algorithm',
|
||||||
|
# The following can ONLY be used for encryption, not signing: elg, cv
|
||||||
|
#'choices': ['rsa', 'dsa', 'elg', 'ed', 'cv', 'nistp', 'brainpool.1', 'secp.k1'],
|
||||||
|
'choices': ['rsa', 'dsa', 'ed', 'nist', 'brainpool.1', 'sec.k1'],
|
||||||
|
#'default': 'rsa'
|
||||||
|
'default': 'ed'
|
||||||
|
},
|
||||||
|
'keysize': {
|
||||||
|
'text': 'the subkey\'s key size (in bits)',
|
||||||
|
'choices': {
|
||||||
|
'rsa': ['1024', '2048', '4096'],
|
||||||
|
'dsa': ['768', '2048', '3072'],
|
||||||
|
#'elg': ['1024', '2048', '4096'], # Invalid for signing, etc.
|
||||||
|
'ed': ['25519'],
|
||||||
|
#'cv': ['25519'],
|
||||||
|
'nistp': ['256', '384', '521'],
|
||||||
|
'brainpool.1': ['256', '384', '512'],
|
||||||
|
'sec.k1': ['256']
|
||||||
|
},
|
||||||
|
'default': {
|
||||||
|
'rsa': '4096',
|
||||||
|
'dsa': '3072',
|
||||||
|
'ed': '25519',
|
||||||
|
'nistp': '521',
|
||||||
|
'brainpool.1': '512',
|
||||||
|
'sec.k1': '256'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'params': ['name', 'email', 'comment']
|
||||||
|
}
|
||||||
|
ssl = {
|
||||||
|
'attribs': {
|
||||||
|
'cert': {
|
||||||
|
'hash_algo': {
|
||||||
|
'text': ('What hashing algorithm do you want to use? '
|
||||||
|
'(Default is sha512.)'),
|
||||||
|
'prompt': 'Hashing algorithm: ',
|
||||||
|
'options': openssl_digests,
|
||||||
|
'default': 'aes256'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'key': {
|
||||||
|
'cipher': {
|
||||||
|
'text': ('What encryption algorithm/cipher do you want to '
|
||||||
|
'use? (Default is aes256.) Use "none" to specify '
|
||||||
|
'a key without a passphrase.'),
|
||||||
|
'prompt': 'Cipher: ',
|
||||||
|
'options': openssl_ciphers + ['none'],
|
||||||
|
'default': 'aes256'
|
||||||
|
},
|
||||||
|
'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.) (If the key '
|
||||||
|
'cipher is "none", this is ignored.)'),
|
||||||
|
'prompt': 'Keysize: ',
|
||||||
|
# TODO: do all openssl_ciphers support these sizes?
|
||||||
|
'options': ['1024', '2048', '4096'],
|
||||||
|
'default': 'aes256'
|
||||||
|
},
|
||||||
|
'passphrase': {
|
||||||
|
'text': ('What passphrase do you want to use for the key? '
|
||||||
|
'If you specified the cipher as "none", this is '
|
||||||
|
'ignored (you can just hit enter).'),
|
||||||
|
'prompt': 'Passphrase (will not echo back): ',
|
||||||
|
'options': None,
|
||||||
|
'default': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'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)')
|
||||||
|
},
|
||||||
|
'paths_ca': {
|
||||||
|
'index': ('(or read from) the CA (Certificate Authority) Database '
|
||||||
|
'index file (if left blank, one will not be used)'),
|
||||||
|
'serial': ('(or read from) the CA (Certificate Authority) '
|
||||||
|
'Database serial file (if left blank, one will not be '
|
||||||
|
'used)'),
|
||||||
|
},
|
||||||
|
'subject': {
|
||||||
|
'countryName': {
|
||||||
|
'text': ('the 2-letter country abbreviation (must conform to '
|
||||||
|
'ISO3166 ALPHA-2)?\n'
|
||||||
|
'Country code: ')
|
||||||
|
},
|
||||||
|
'localityName': {
|
||||||
|
'text': ('the city/town/borough/locality name?\n'
|
||||||
|
'Locality: ')
|
||||||
|
},
|
||||||
|
'stateOrProvinceName': {
|
||||||
|
'text': ('the state/region name (full string)?\n'
|
||||||
|
'Region: ')
|
||||||
|
},
|
||||||
|
'organization': {
|
||||||
|
'text': ('your organization\'s name?\n'
|
||||||
|
'Organization: ')
|
||||||
|
},
|
||||||
|
'organizationalUnitName': {
|
||||||
|
'text': ('your department/role/team/department name?\n'
|
||||||
|
'Organizational Unit: ')
|
||||||
|
},
|
||||||
|
'emailAddress': {
|
||||||
|
'text': ('the email address to be associated with this '
|
||||||
|
'certificate/PKI object?\n'
|
||||||
|
'Email: ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
609
bdisk/utils.py
609
bdisk/utils.py
@ -1,17 +1,25 @@
|
|||||||
|
# Yes, this is messy. They doesn't belong anywhere else, leave me alone.
|
||||||
|
|
||||||
import _io
|
import _io
|
||||||
|
import copy
|
||||||
import crypt
|
import crypt
|
||||||
import GPG
|
import GPG
|
||||||
|
import getpass
|
||||||
import hashid
|
import hashid
|
||||||
import hashlib
|
import hashlib
|
||||||
import iso3166
|
import iso3166
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
|
import prompt_strings
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
import validators
|
import validators
|
||||||
import zlib
|
import zlib
|
||||||
|
import requests
|
||||||
import lxml.etree
|
import lxml.etree
|
||||||
|
import lxml.objectify
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from dns import resolver
|
from dns import resolver
|
||||||
from email.utils import parseaddr as emailparse
|
from email.utils import parseaddr as emailparse
|
||||||
@ -25,6 +33,7 @@ passlib_schemes = ['des_crypt', 'md5_crypt', 'sha256_crypt', 'sha512_crypt']
|
|||||||
# Build various hash digest name lists
|
# Build various hash digest name lists
|
||||||
digest_schemes = list(hashlib.algorithms_available)
|
digest_schemes = list(hashlib.algorithms_available)
|
||||||
# Provided by zlib
|
# Provided by zlib
|
||||||
|
# TODO?
|
||||||
digest_schemes.append('adler32')
|
digest_schemes.append('adler32')
|
||||||
digest_schemes.append('crc32')
|
digest_schemes.append('crc32')
|
||||||
|
|
||||||
@ -33,12 +42,53 @@ crypt_map = {'sha512': crypt.METHOD_SHA512,
|
|||||||
'md5': crypt.METHOD_MD5,
|
'md5': crypt.METHOD_MD5,
|
||||||
'des': crypt.METHOD_CRYPT}
|
'des': crypt.METHOD_CRYPT}
|
||||||
|
|
||||||
# These are *key* ciphers, for encrypting exported keys.
|
|
||||||
openssl_ciphers = ['aes128', 'aes192', 'aes256', 'bf', 'blowfish',
|
class Download(object):
|
||||||
'camellia128', 'camellia192', 'camellia256', 'cast', 'des',
|
def __init__(self, url, progress = True, offset = None, chunksize = 1024):
|
||||||
'des3', 'idea', 'rc2', 'seed']
|
self.cnt_len = None
|
||||||
openssl_digests = ['blake2b512', 'blake2s256', 'gost', 'md4', 'md5', 'mdc2',
|
self.head = requests.head(url, allow_redirects = True).headers
|
||||||
'rmd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
|
self.req_headers = {}
|
||||||
|
self.range = False
|
||||||
|
self.url = url
|
||||||
|
self.offset = offset
|
||||||
|
self.chunksize = chunksize
|
||||||
|
self.progress = progress
|
||||||
|
if 'accept-ranges' in self.head:
|
||||||
|
if self.head['accept-ranmges'].lower() != 'none':
|
||||||
|
self.range = True
|
||||||
|
if 'content-length' in self.head:
|
||||||
|
try:
|
||||||
|
self.cnt_len = int(self.head['content-length'])
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
if self.cnt_len and self.offset and self.range:
|
||||||
|
if not self.offset <= self.cnt_len:
|
||||||
|
raise ValueError(('The offset requested ({0}) is greater than '
|
||||||
|
'the content-length value').format(self.offset, self.cnt_len))
|
||||||
|
self.req_headers['range'] = 'bytes={0}-'.format(self.offset)
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
if not self.progress:
|
||||||
|
self.req = requests.get(self.url, allow_redirects = True, headers = self.req_headers)
|
||||||
|
self.bytes_obj = self.req.content
|
||||||
|
else:
|
||||||
|
self.req = requests.get(self.url, allow_redirects = True, stream = True, headers = self.req_headers)
|
||||||
|
self.bytes_obj = bytes()
|
||||||
|
_bytelen = 0
|
||||||
|
# TODO: better handling for logging instead of print()s?
|
||||||
|
for chunk in self.req.iter_content(chunk_size = self.chunksize):
|
||||||
|
self.bytes_obj += chunk
|
||||||
|
if self.cnt_len:
|
||||||
|
print('\033[F')
|
||||||
|
print('{0:.2f}'.format((_bytelen / float(self.head['content-length'])) * 100),
|
||||||
|
end = '%',
|
||||||
|
flush = True)
|
||||||
|
_bytelen += self.chunksize
|
||||||
|
else:
|
||||||
|
print('.', end = '')
|
||||||
|
print()
|
||||||
|
return(self.bytes_obj)
|
||||||
|
|
||||||
|
|
||||||
class XPathFmt(string.Formatter):
|
class XPathFmt(string.Formatter):
|
||||||
def get_field(self, field_name, args, kwargs):
|
def get_field(self, field_name, args, kwargs):
|
||||||
@ -51,18 +101,19 @@ class detect(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def any_hash(self, hash_str):
|
def any_hash(self, hash_str, normalize = False):
|
||||||
h = hashid.HashID()
|
h = hashid.HashID()
|
||||||
hashes = []
|
hashes = []
|
||||||
for i in h.identifyHash(hash_str):
|
for i in h.identifyHash(hash_str):
|
||||||
if i.extended:
|
if i.extended:
|
||||||
continue
|
continue
|
||||||
x = i.name
|
x = i.name
|
||||||
if x.lower() in ('crc-32', 'ripemd-160', 'sha-1', 'sha-224',
|
if x.lower() in ('crc-32', 'ripemd-160', 'sha-1', 'sha-224', 'sha-256', 'sha-384', 'sha-512'):
|
||||||
'sha-256', 'sha-384', 'sha-512'):
|
|
||||||
# Gorram you, c0re.
|
# Gorram you, c0re.
|
||||||
x = re.sub('-', '', x.lower())
|
x = re.sub('-', '', x.lower())
|
||||||
_hashes = [h.lower() for h in digest_schemes]
|
_hashes = [h.lower() for h in digest_schemes] # TODO: move this outside so we don't define it every invoke
|
||||||
|
if normalize:
|
||||||
|
x = re.sub('(-|crypt|\s+)', '', x.lower())
|
||||||
if x.lower() in sorted(list(set(_hashes))):
|
if x.lower() in sorted(list(set(_hashes))):
|
||||||
hashes.append(x)
|
hashes.append(x)
|
||||||
return(hashes)
|
return(hashes)
|
||||||
@ -76,9 +127,45 @@ class detect(object):
|
|||||||
return(None)
|
return(None)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
def password_hash_salt(self, salted_hash):
|
||||||
|
_hash_list = salted_hash.split('$')
|
||||||
|
if len(_hash_list) < 3:
|
||||||
|
return(None)
|
||||||
|
salt = _hash_list[2]
|
||||||
|
return(salt)
|
||||||
|
|
||||||
|
def remote_files(self, url_base, ptrn = None, flags = []):
|
||||||
|
soup = BeautifulSoup(Download(url_base, progress = False).fetch().decode('utf-8'),
|
||||||
|
'lxml')
|
||||||
|
urls = []
|
||||||
|
if 'regex' in flags:
|
||||||
|
if not isinstance(ptrn, str):
|
||||||
|
raise ValueError('"ptrn" must be a regex pattern to match '
|
||||||
|
'against')
|
||||||
|
else:
|
||||||
|
ptrn = re.compile(ptrn)
|
||||||
|
for u in soup.find_all('a'):
|
||||||
|
if not u.has_attr('href'):
|
||||||
|
continue
|
||||||
|
if 'regex' in flags:
|
||||||
|
if not ptrn.search(u.attrs['href']):
|
||||||
|
continue
|
||||||
|
if u.has_attr('href'):
|
||||||
|
urls.append(u.attrs['href'])
|
||||||
|
if not urls:
|
||||||
|
return(None)
|
||||||
|
# We certainly can't intelligently parse the printed timestamp since it
|
||||||
|
# varies so much and that'd be a nightmare to get consistent...
|
||||||
|
# But we CAN sort by filename.
|
||||||
|
if 'latest' in flags:
|
||||||
|
urls = sorted(list(set(urls)))
|
||||||
|
urls = urls[-1]
|
||||||
|
else:
|
||||||
|
urls = urls[0]
|
||||||
|
return(urls)
|
||||||
|
|
||||||
def gpgkeyID_from_url(self, url):
|
def gpgkeyID_from_url(self, url):
|
||||||
with urlopen(url) as u:
|
data = Download(url, progress = False).bytes_obj
|
||||||
data = u.read()
|
|
||||||
g = GPG.GPGHandler()
|
g = GPG.GPGHandler()
|
||||||
key_ids = g.get_sigs(data)
|
key_ids = g.get_sigs(data)
|
||||||
del(g)
|
del(g)
|
||||||
@ -130,7 +217,7 @@ class detect(object):
|
|||||||
# Get any easy ones out of the way first.
|
# Get any easy ones out of the way first.
|
||||||
if name in digest_schemes:
|
if name in digest_schemes:
|
||||||
return(name)
|
return(name)
|
||||||
# Otherwise grab the first one that matches, in order from the .
|
# Otherwise grab the first one that matches
|
||||||
_digest_re = re.compile('^{0}$'.format(name.strip()), re.IGNORECASE)
|
_digest_re = re.compile('^{0}$'.format(name.strip()), re.IGNORECASE)
|
||||||
for h in digest_schemes:
|
for h in digest_schemes:
|
||||||
if _digest_re.search(h):
|
if _digest_re.search(h):
|
||||||
@ -146,6 +233,9 @@ class generate(object):
|
|||||||
_salt = crypt.mksalt(algo)
|
_salt = crypt.mksalt(algo)
|
||||||
else:
|
else:
|
||||||
_salt = salt
|
_salt = salt
|
||||||
|
if not password:
|
||||||
|
# Intentionally empty password.
|
||||||
|
return('')
|
||||||
return(crypt.crypt(password, _salt))
|
return(crypt.crypt(password, _salt))
|
||||||
|
|
||||||
def hashlib_names(self):
|
def hashlib_names(self):
|
||||||
@ -156,13 +246,18 @@ class generate(object):
|
|||||||
hashes.append(h)
|
hashes.append(h)
|
||||||
return(hashes)
|
return(hashes)
|
||||||
|
|
||||||
def salt(self, algo = 'sha512'):
|
def salt(self, algo = None):
|
||||||
|
if not algo:
|
||||||
|
algo = 'sha512'
|
||||||
algo = crypt_map[algo]
|
algo = crypt_map[algo]
|
||||||
return(crypt.mksalt(algo))
|
return(crypt.mksalt(algo))
|
||||||
|
|
||||||
class prompts(object):
|
class prompts(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.promptstr = prompt_strings.PromptStrings()
|
||||||
|
|
||||||
|
# TODO: these strings should be indexed in a separate module and
|
||||||
|
# sourced/imported. we should generally just find a cleaner way to do this.
|
||||||
|
|
||||||
def confirm_or_no(self, prompt = '', invert = False,
|
def confirm_or_no(self, prompt = '', invert = False,
|
||||||
usage = '{0} to confirm, otherwise {1}...\n'):
|
usage = '{0} to confirm, otherwise {1}...\n'):
|
||||||
@ -194,21 +289,18 @@ class prompts(object):
|
|||||||
return(True)
|
return(True)
|
||||||
|
|
||||||
def gpg_keygen_attribs(self):
|
def gpg_keygen_attribs(self):
|
||||||
_attribs = {'algo': {'text': 'the subkey\'s encryption type/algorithm',
|
_strs = self.promptstr.gpg
|
||||||
'choices': ['rsa', 'dsa'],
|
|
||||||
'default': 'rsa'},
|
|
||||||
'keysize': {'text': 'the subkey\'s key size (in bits)',
|
|
||||||
'choices': {'rsa': ['1024', '2048', '4096'],
|
|
||||||
'dsa': ['768', '2048', '3072']},
|
|
||||||
'default': {'rsa': '4096',
|
|
||||||
'dsa': '3072'}}}
|
|
||||||
_params = {'name': None,
|
|
||||||
'email': None,
|
|
||||||
#'email': valid().email, # Use this to force valid email.
|
|
||||||
'comment': None}
|
|
||||||
gpg_vals = {'attribs': {},
|
gpg_vals = {'attribs': {},
|
||||||
'params': {}}
|
'params': {}}
|
||||||
for a in _attribs:
|
_checks = {
|
||||||
|
'params': {
|
||||||
|
'name': {'error': 'name cannot be empty',
|
||||||
|
'check': valid().nonempty_str},
|
||||||
|
'email': {'error': 'not a valid email address',
|
||||||
|
'check': valid().email}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for a in _strs['attribs']:
|
||||||
_a = None
|
_a = None
|
||||||
while not _a:
|
while not _a:
|
||||||
if 'algo' in gpg_vals['attribs'] and a == 'keysize':
|
if 'algo' in gpg_vals['attribs'] and a == 'keysize':
|
||||||
@ -261,6 +353,45 @@ class prompts(object):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
gpg_vals['params'][p] = _p
|
gpg_vals['params'][p] = _p
|
||||||
|
=======
|
||||||
|
_choices = _strs['attribs']['keysize']['choices'][_algo]
|
||||||
|
_dflt = _strs['attribs']['keysize']['default'][_algo]
|
||||||
|
else:
|
||||||
|
_choices = _strs['attribs'][a]['choices']
|
||||||
|
_dflt = _strs['attribs'][a]['default']
|
||||||
|
_a = (input(
|
||||||
|
('\nWhat should be {0}? (Default is {1}.)\nChoices:\n'
|
||||||
|
'\n\t{2}\n\n{3}: ').format(
|
||||||
|
_strs['attribs'][a]['text'],
|
||||||
|
_dflt,
|
||||||
|
'\n\t'.join(_choices),
|
||||||
|
a.title()
|
||||||
|
)
|
||||||
|
)).strip().lower()
|
||||||
|
if _a == '':
|
||||||
|
_a = _dflt
|
||||||
|
elif _a not in _choices:
|
||||||
|
print(
|
||||||
|
('Invalid selection; choosing default '
|
||||||
|
'({0})').format(_dflt)
|
||||||
|
)
|
||||||
|
_a = _dflt
|
||||||
|
gpg_vals['attribs'][a] = _a
|
||||||
|
for p in _strs['params']:
|
||||||
|
_p = input(
|
||||||
|
('\nWhat is the {0} for the subkey?\n'
|
||||||
|
'{1}: ').format(p, p.title())
|
||||||
|
)
|
||||||
|
if p in _checks['params']:
|
||||||
|
while not _checks['params'][p]['check'](_p):
|
||||||
|
print(
|
||||||
|
('Invalid entry ({0}). Please retry.').format(
|
||||||
|
_checks['params'][p]['error']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_p = input('{0}: '.format(_p.title()))
|
||||||
|
gpg_vals['params'][p] = _p
|
||||||
|
>>>>>>> 69b6ec60d05d64a9e23e9a0707a0323f960a2936
|
||||||
return(gpg_vals)
|
return(gpg_vals)
|
||||||
|
|
||||||
def hash_select(self, prompt = '',
|
def hash_select(self, prompt = '',
|
||||||
@ -310,116 +441,81 @@ class prompts(object):
|
|||||||
ssl_vals = {'paths': {},
|
ssl_vals = {'paths': {},
|
||||||
'attribs': {},
|
'attribs': {},
|
||||||
'subject': {}}
|
'subject': {}}
|
||||||
|
_checks = {
|
||||||
|
'subject': {
|
||||||
|
'countryName': valid().country_abbrev,
|
||||||
|
'emailAddress': valid().email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_strs = copy.deepcopy(self.promptstr.ssl)
|
||||||
# pki_role should be 'ca' or 'client'
|
# pki_role should be 'ca' or 'client'
|
||||||
if pki_role not in ('ca', 'client'):
|
if pki_role not in ('ca', 'client'):
|
||||||
raise ValueError('pki_role must be either "ca" or "client"')
|
raise ValueError('pki_role must be either "ca" or "client"')
|
||||||
_attribs = {'cert': {'hash_algo': {'text': ('What hashing algorithm '
|
# NOTE: need to validate US and email
|
||||||
'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':
|
if pki_role == 'ca':
|
||||||
_paths['index'] = ('(or read from) the CA DB index file (if left '
|
# this is getting triggered for clients?
|
||||||
'blank, one will not be used)')
|
_strs['paths'].update(_strs['paths_ca'])
|
||||||
_paths['serial'] = ('(or read from) the CA DB serial file (if '
|
for a in _strs['attribs']:
|
||||||
'left blank, one will not be used)')
|
|
||||||
for a in _attribs:
|
|
||||||
ssl_vals['attribs'][a] = {}
|
ssl_vals['attribs'][a] = {}
|
||||||
for x in _attribs[a]:
|
for x in _strs['attribs'][a]:
|
||||||
ssl_vals['attribs'][a][x] = None
|
ssl_vals['attribs'][a][x] = None
|
||||||
for p in _paths:
|
for p in _strs['paths']:
|
||||||
if p == 'csr':
|
if p == 'csr':
|
||||||
_allow_empty = True
|
_allow_empty = True
|
||||||
else:
|
else:
|
||||||
_allow_empty = False
|
_allow_empty = False
|
||||||
ssl_vals['paths'][p] = self.path(_paths[p],
|
ssl_vals['paths'][p] = self.path(_strs['paths'][p],
|
||||||
empty_passthru = _allow_empty)
|
empty_passthru = _allow_empty)
|
||||||
print()
|
print()
|
||||||
if ssl_vals['paths'][p] == '':
|
if ssl_vals['paths'][p] == '':
|
||||||
ssl_vals['paths'][p] = None
|
ssl_vals['paths'][p] = None
|
||||||
if p in _attribs:
|
if p in _strs['attribs']:
|
||||||
for x in _attribs[p]:
|
for x in _strs['attribs'][p]:
|
||||||
while not ssl_vals['attribs'][p][x]:
|
while not ssl_vals['attribs'][p][x]:
|
||||||
ssl_vals['attribs'][p][x] = (input(
|
# cipher attrib is prompted for before this.
|
||||||
('\n{0}\n\n\t{1}\n\n{2}').format(
|
if p == 'key' and x == 'passphrase':
|
||||||
_attribs[p][x]['text'],
|
if ssl_vals['attribs']['key']['cipher'] == 'none':
|
||||||
'\n\t'.join(_attribs[p][x]['options']),
|
ssl_vals['attribs'][p][x] = 'none'
|
||||||
_attribs[p][x]['prompt'])
|
continue
|
||||||
)).strip().lower()
|
ssl_vals['attribs'][p][x] = getpass.getpass(
|
||||||
if ssl_vals['attribs'][p][x] not in \
|
('{0}\n{1}').format(
|
||||||
_attribs[p][x]['options']:
|
_strs['attribs'][p][x]['text'],
|
||||||
print(('\nInvalid selection; setting default '
|
_strs['attribs'][p][x]['prompt'])
|
||||||
'({0}).').format(_attribs[p][x]['default']))
|
)
|
||||||
ssl_vals['attribs'][p][x] = \
|
if ssl_vals['attribs'][p][x] == '':
|
||||||
_attribs[p][x]['default']
|
ssl_vals['attribs'][p][x] = 'none'
|
||||||
_subject = {'countryName': {'text': ('the 2-letter country '
|
else:
|
||||||
'abbreviation (must conform to '
|
ssl_vals['attribs'][p][x] = (input(
|
||||||
'ISO3166 ALPHA-2)?\nCountry '
|
('\n{0}\n\n\t{1}\n\n{2}').format(
|
||||||
'code: '),
|
_strs['attribs'][p][x]['text'],
|
||||||
'check': 'func',
|
'\n\t'.join(
|
||||||
'func': valid().country_abbrev},
|
_strs['attribs'][p][x]['options']),
|
||||||
'localityName': {'text': ('the city/town/borough/locality '
|
_strs['attribs'][p][x]['prompt']))
|
||||||
'name?\nLocality: '),
|
).strip().lower()
|
||||||
'check': None},
|
if ssl_vals['attribs'][p][x] not in \
|
||||||
'stateOrProvinceName': {'text': ('the state/region '
|
_strs['attribs'][p][x]['options']:
|
||||||
'name (full string)?'
|
print(
|
||||||
'\nRegion: '),
|
('\nInvalid selection; setting default '
|
||||||
'check': None},
|
'({0}).').format(
|
||||||
'organization': {'text': ('your organization\'s name?'
|
_strs['attribs'][p][x]['default']
|
||||||
'\nOrganization: '),
|
)
|
||||||
'check': None},
|
)
|
||||||
'organizationalUnitName': {'text': ('your department/role/'
|
ssl_vals['attribs'][p][x] = \
|
||||||
'team/department name?'
|
_strs['attribs'][p][x]['default']
|
||||||
'\nOrganizational '
|
for s in _strs['subject']:
|
||||||
'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
|
ssl_vals['subject'][s] = None
|
||||||
for s in _subject:
|
for s in _strs['subject']:
|
||||||
while not ssl_vals['subject'][s]:
|
while not ssl_vals['subject'][s]:
|
||||||
_input = (input(
|
_input = (input(
|
||||||
('\nWhat is {0}').format(_subject[s]['text'])
|
('\nWhat is {0}').format(
|
||||||
|
_strs['subject'][s]['text'])
|
||||||
)).strip()
|
)).strip()
|
||||||
_chk = _subject[s]['check']
|
|
||||||
if _chk:
|
|
||||||
if _chk == 'func':
|
|
||||||
_chk = _subject[s]['func'](_input)
|
|
||||||
if not _chk:
|
|
||||||
print('Invalid value; retrying.')
|
|
||||||
continue
|
|
||||||
print()
|
print()
|
||||||
|
if s in _checks['subject']:
|
||||||
|
if not _checks['subject'][s](_input):
|
||||||
|
print('Invalid entry; try again.')
|
||||||
|
ssl_vals['subject'][s] = None
|
||||||
|
continue
|
||||||
ssl_vals['subject'][s] = _input
|
ssl_vals['subject'][s] = _input
|
||||||
_url = transform().url_to_dict(cn_url, no_None = True)
|
_url = transform().url_to_dict(cn_url, no_None = True)
|
||||||
ssl_vals['subject']['commonName'] = _url['host']
|
ssl_vals['subject']['commonName'] = _url['host']
|
||||||
@ -454,12 +550,12 @@ class transform(object):
|
|||||||
def py2xml(self, value, attrib = True):
|
def py2xml(self, value, attrib = True):
|
||||||
if value in (False, ''):
|
if value in (False, ''):
|
||||||
if attrib:
|
if attrib:
|
||||||
return("no")
|
return("false")
|
||||||
else:
|
else:
|
||||||
return(None)
|
return(None)
|
||||||
elif isinstance(value, bool):
|
elif isinstance(value, bool):
|
||||||
# We handle the False case above.
|
# We handle the False case above.
|
||||||
return("yes")
|
return("true")
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
return(value)
|
return(value)
|
||||||
else:
|
else:
|
||||||
@ -477,7 +573,6 @@ class transform(object):
|
|||||||
text_out = re.sub('[^\w]', '', text_out)
|
text_out = re.sub('[^\w]', '', text_out)
|
||||||
return(text_out)
|
return(text_out)
|
||||||
|
|
||||||
# noinspection PyDictCreation
|
|
||||||
def url_to_dict(self, orig_url, no_None = False):
|
def url_to_dict(self, orig_url, no_None = False):
|
||||||
def _getuserinfo(uinfo_str):
|
def _getuserinfo(uinfo_str):
|
||||||
if len(uinfo_str) == 0:
|
if len(uinfo_str) == 0:
|
||||||
@ -622,6 +717,88 @@ class transform(object):
|
|||||||
url['full_url'] += '#{0}'.format('#'.join(_f))
|
url['full_url'] += '#{0}'.format('#'.join(_f))
|
||||||
return(url)
|
return(url)
|
||||||
|
|
||||||
|
def user(self, user_elem):
|
||||||
|
_attribs = ('hashed', 'hash_algo', 'salt')
|
||||||
|
acct = {}
|
||||||
|
for a in _attribs:
|
||||||
|
acct[a] = None
|
||||||
|
if len(user_elem):
|
||||||
|
elem = user_elem[0]
|
||||||
|
for a in _attribs:
|
||||||
|
if a in elem.attrib:
|
||||||
|
acct[a] = self.xml2py(elem.attrib[a], attrib = True)
|
||||||
|
if acct['hashed']:
|
||||||
|
if not acct['hash_algo']:
|
||||||
|
_hash = detect().password_hash(elem.text)
|
||||||
|
if _hash:
|
||||||
|
acct['hash_algo'] = _hash
|
||||||
|
else:
|
||||||
|
acct['hash_algo'] = None
|
||||||
|
# We no longer raise ValueError. Per shadow(5):
|
||||||
|
#######################################################
|
||||||
|
# If the password field contains some string that is
|
||||||
|
# not a valid result of crypt(3), for instance ! or *,
|
||||||
|
# the user will not be able to use a unix password to
|
||||||
|
# log in (but the user may log in the system by other
|
||||||
|
# means).
|
||||||
|
# This field may be empty, in which case no passwords
|
||||||
|
# are required to authenticate as the specified login
|
||||||
|
# name. However, some applications which read the
|
||||||
|
# /etc/shadow file may decide not to permit any access
|
||||||
|
# at all if the password field is empty.
|
||||||
|
# A password field which starts with an exclamation
|
||||||
|
# mark means that the password is locked. The remaining
|
||||||
|
# characters on the line represent the password field
|
||||||
|
# before the password was locked.
|
||||||
|
#######################################################
|
||||||
|
# raise ValueError(
|
||||||
|
# 'Invalid salted password hash: {0}'.format(
|
||||||
|
# elem.text)
|
||||||
|
# )
|
||||||
|
acct['salt_hash'] = elem.text
|
||||||
|
acct['passphrase'] = None
|
||||||
|
else:
|
||||||
|
if not acct['hash_algo']:
|
||||||
|
acct['hash_algo'] = 'sha512'
|
||||||
|
acct['passphrase'] = elem.text
|
||||||
|
_saltre = re.compile('^\s*(auto|none|)\s*$', re.IGNORECASE)
|
||||||
|
if acct['salt']:
|
||||||
|
if _saltre.search(acct['salt']):
|
||||||
|
_salt = generate.salt(acct['hash_algo'])
|
||||||
|
acct['salt'] = _salt
|
||||||
|
else:
|
||||||
|
if not acct['hashed']:
|
||||||
|
acct['salt_hash'] = generate().hash_password(
|
||||||
|
acct['passphrase'],
|
||||||
|
algo = crypt_map[acct['hash_algo']])
|
||||||
|
acct['salt'] = detect().password_hash_salt(acct['salt_hash'])
|
||||||
|
if 'salt_hash' not in acct:
|
||||||
|
acct['salt_hash'] = generate().hash_password(
|
||||||
|
acct['passphrase'],
|
||||||
|
salt = acct['salt'],
|
||||||
|
algo = crypt_map[acct['hash_algo']])
|
||||||
|
return(acct)
|
||||||
|
|
||||||
|
def xml2py(self, value, attrib = True):
|
||||||
|
yes = re.compile('^\s*(true|1)\s*$', re.IGNORECASE)
|
||||||
|
no = re.compile('^\s*(false|0)\s*$', re.IGNORECASE)
|
||||||
|
none = re.compile('^\s*(none|)\s*$', re.IGNORECASE)
|
||||||
|
if no.search(value):
|
||||||
|
if attrib:
|
||||||
|
return(False)
|
||||||
|
else:
|
||||||
|
return(None)
|
||||||
|
elif yes.search(value):
|
||||||
|
# We handle the False case above.
|
||||||
|
return(True)
|
||||||
|
elif value.strip() == '' or none.search(value):
|
||||||
|
return(None)
|
||||||
|
elif valid().integer(value):
|
||||||
|
return(int(value))
|
||||||
|
else:
|
||||||
|
return(value)
|
||||||
|
return()
|
||||||
|
|
||||||
class valid(object):
|
class valid(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
@ -664,6 +841,11 @@ class valid(object):
|
|||||||
return(False)
|
return(False)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
def nonempty_str(self, str_in):
|
||||||
|
if str_in.strip() == '':
|
||||||
|
return(False)
|
||||||
|
return(True)
|
||||||
|
|
||||||
def password(self, passwd):
|
def password(self, passwd):
|
||||||
# https://en.wikipedia.org/wiki/ASCII#Printable_characters
|
# https://en.wikipedia.org/wiki/ASCII#Printable_characters
|
||||||
# https://serverfault.com/a/513243/103116
|
# https://serverfault.com/a/513243/103116
|
||||||
@ -693,14 +875,19 @@ class valid(object):
|
|||||||
return(True)
|
return(True)
|
||||||
|
|
||||||
def salt_hash(self, salthash):
|
def salt_hash(self, salthash):
|
||||||
_idents = ''.join([i.ident for i in crypt_map if i.ident])
|
_idents = ''.join([i.ident for i in crypt_map.values() if i.ident])
|
||||||
# noinspection PyStringFormat
|
# noinspection PyStringFormat
|
||||||
_regex = re.compile('^(\$[{0}]\$)?[./0-9A-Za-z]{{0,16}}\$?'.format(
|
_regex = re.compile('^(\$[{0}]\$)?[./0-9A-Za-z]{{0,16}}\$?'.format(_idents))
|
||||||
_idents))
|
|
||||||
if not _regex.search(salthash):
|
if not _regex.search(salthash):
|
||||||
return(False)
|
return(False)
|
||||||
return(True)
|
return(True)
|
||||||
|
|
||||||
|
def salt_hash_full(self, salthash, hash_type):
|
||||||
|
h = [re.sub('-', '', i.lower()).split()[0] for i in detect.any_hash(self, salthash, normalize = True)]
|
||||||
|
if hash_type.lower() not in h:
|
||||||
|
return(False)
|
||||||
|
return(True)
|
||||||
|
|
||||||
def plugin_name(self, name):
|
def plugin_name(self, name):
|
||||||
if len(name) == 0:
|
if len(name) == 0:
|
||||||
return(False)
|
return(False)
|
||||||
@ -748,22 +935,38 @@ class valid(object):
|
|||||||
|
|
||||||
class xml_supplicant(object):
|
class xml_supplicant(object):
|
||||||
def __init__(self, cfg, profile = None, max_recurse = 5):
|
def __init__(self, cfg, profile = None, max_recurse = 5):
|
||||||
raw = self._detect_cfg(cfg)
|
self.selector_ids = ('id', 'name', 'uuid')
|
||||||
xmlroot = lxml.etree.fromstring(raw)
|
|
||||||
self.btags = {'xpath': {},
|
self.btags = {'xpath': {},
|
||||||
'regex': {},
|
'regex': {},
|
||||||
'variable': {}}
|
'variable': {}}
|
||||||
|
raw = self._detect_cfg(cfg)
|
||||||
|
# This is changed in just a moment.
|
||||||
|
self.profile = profile
|
||||||
|
# This is retained so we can "refresh" the profile if needed.
|
||||||
|
self.orig_profile = profile
|
||||||
|
try:
|
||||||
|
self.orig_xml = lxml.etree.fromstring(raw)
|
||||||
|
# We need to strip the naked namespace for XPath to work.
|
||||||
|
self.xml = copy.deepcopy(self.orig_xml)
|
||||||
|
self.roottree = self.xml.getroottree()
|
||||||
|
self.tree = self.roottree.getroot()
|
||||||
|
self.strip_naked_ns()
|
||||||
|
except lxml.etree.XMLSyntaxError:
|
||||||
|
raise ValueError('The configuration provided does not seem to be '
|
||||||
|
'valid')
|
||||||
|
self.get_profile(profile = profile)
|
||||||
|
# This is disabled; we set it above.
|
||||||
|
#self.xml = lxml.etree.fromstring(raw)
|
||||||
self.fmt = XPathFmt()
|
self.fmt = XPathFmt()
|
||||||
self.max_recurse = max_recurse
|
self.max_recurse = int(self.profile.xpath(
|
||||||
|
'//meta/max_recurse/text()')[0])
|
||||||
# I don't have permission to credit them, but to the person who helped
|
# 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.
|
# me with this regex - thank you. You know who you are.
|
||||||
|
# Originally this pattern was the one from:
|
||||||
|
# https://stackoverflow.com/a/12728199/733214
|
||||||
self.ptrn = re.compile(('(?<=(?<!\{)\{)(?:[^{}]+'
|
self.ptrn = re.compile(('(?<=(?<!\{)\{)(?:[^{}]+'
|
||||||
'|{{[^{}]*}})*(?=\}(?!\}))'))
|
'|{{[^{}]*}})*(?=\}(?!\}))'))
|
||||||
self.root = lxml.etree.ElementTree(xmlroot)
|
self.root = lxml.etree.ElementTree(self.xml)
|
||||||
if not profile:
|
|
||||||
self.profile = xmlroot.xpath('/bdisk/profile[1]')[0]
|
|
||||||
else:
|
|
||||||
self.profile = xmlroot.xpath(profile)[0]
|
|
||||||
self._parse_regexes()
|
self._parse_regexes()
|
||||||
self._parse_variables()
|
self._parse_variables()
|
||||||
|
|
||||||
@ -794,19 +997,99 @@ class xml_supplicant(object):
|
|||||||
else:
|
else:
|
||||||
raise TypeError('Could not determine the object type.')
|
raise TypeError('Could not determine the object type.')
|
||||||
return(cfg)
|
return(cfg)
|
||||||
|
|
||||||
def _parse_regexes(self):
|
def _parse_regexes(self):
|
||||||
for regex in self.profile.xpath('//meta/regexes/pattern'):
|
for regex in self.profile.xpath('./meta/regexes/pattern'):
|
||||||
self.btags['regex'][regex.attrib['id']] = re.compile(regex.text)
|
_key = 'regex%{0}'.format(regex.attrib['id'])
|
||||||
|
self.btags['regex'][_key] = regex.text
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def _parse_variables(self):
|
def _parse_variables(self):
|
||||||
for variable in self.profile.xpath('//meta/variables/variable'):
|
for variable in self.profile.xpath('./meta/variables/variable'):
|
||||||
self.btags['variable'][
|
self.btags['variable'][
|
||||||
'variable%{0}'.format(variable.attrib['id'])
|
'variable%{0}'.format(variable.attrib['id'])
|
||||||
] = variable.text
|
] = variable.text
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def get_profile(self, profile = None):
|
||||||
|
"""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 profile:
|
||||||
|
# A profile identifier was provided
|
||||||
|
if isinstance(profile, str):
|
||||||
|
_profile_name = profile
|
||||||
|
profile = {}
|
||||||
|
for i in self.selector_ids:
|
||||||
|
profile[i] = None
|
||||||
|
profile['name'] = _profile_name
|
||||||
|
elif isinstance(profile, dict):
|
||||||
|
for k in self.selector_ids:
|
||||||
|
if k not in profile.keys():
|
||||||
|
profile[k] = None
|
||||||
|
else:
|
||||||
|
raise TypeError('profile must be a string (name of profile), '
|
||||||
|
'a dictionary, or None')
|
||||||
|
xpath = '/bdisk/profile{0}'.format(self.xpath_selector(profile))
|
||||||
|
self.profile = self.xml.xpath(xpath)
|
||||||
|
if len(self.profile) != 1:
|
||||||
|
raise ValueError('Could not determine a valid, unique '
|
||||||
|
'profile; please check your profile '
|
||||||
|
'specifier(s)')
|
||||||
|
else:
|
||||||
|
# We need the actual *profile*, not a list of matching
|
||||||
|
# profile(s).
|
||||||
|
self.profile = self.profile[0]
|
||||||
|
if not len(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])
|
||||||
|
self.profile = profiles[idx]
|
||||||
|
break
|
||||||
|
# We couldn't find a profile with a default name. Try to grab the
|
||||||
|
# first profile.
|
||||||
|
if self.profile is None:
|
||||||
|
# Grab the first profile.
|
||||||
|
if profiles:
|
||||||
|
self.profile = profiles[0]
|
||||||
|
else:
|
||||||
|
# No profiles found.
|
||||||
|
raise RuntimeError('Could not find any usable '
|
||||||
|
'configuration profiles')
|
||||||
|
return()
|
||||||
|
|
||||||
def get_path(self, element):
|
def get_path(self, element):
|
||||||
path = element
|
path = element
|
||||||
try:
|
try:
|
||||||
@ -818,6 +1101,33 @@ class xml_supplicant(object):
|
|||||||
).format(element.text))
|
).format(element.text))
|
||||||
return(path)
|
return(path)
|
||||||
|
|
||||||
|
def return_full(self):
|
||||||
|
# https://stackoverflow.com/a/22553145/733214
|
||||||
|
local_xml = lxml.etree.Element('bdisk',
|
||||||
|
nsmap = self.orig_xml.nsmap,
|
||||||
|
attrib = self.orig_xml.attrib)
|
||||||
|
local_xml.text = '\n '
|
||||||
|
for elem in self.xml.xpath('/bdisk/profile'):
|
||||||
|
local_xml.append(copy.deepcopy(elem))
|
||||||
|
return(lxml.etree.tostring(local_xml))
|
||||||
|
|
||||||
|
def return_naked_ns(self):
|
||||||
|
# It's so stupid I have to do this.
|
||||||
|
return(self.orig_xml.nsmap)
|
||||||
|
|
||||||
|
def strip_naked_ns(self):
|
||||||
|
# I cannot *believe* that LXML doesn't have this built-in, considering
|
||||||
|
# how common naked namespaces are.
|
||||||
|
# https://stackoverflow.com/a/18160164/733214
|
||||||
|
for elem in self.roottree.getiterator():
|
||||||
|
if not hasattr(elem.tag, 'find'):
|
||||||
|
continue
|
||||||
|
i = elem.tag.find('}')
|
||||||
|
if i >= 0:
|
||||||
|
elem.tag = elem.tag[i + 1:]
|
||||||
|
lxml.objectify.deannotate(self.roottree, cleanup_namespaces = True)
|
||||||
|
return()
|
||||||
|
|
||||||
def substitute(self, element, recurse_count = 0):
|
def substitute(self, element, recurse_count = 0):
|
||||||
if recurse_count >= self.max_recurse:
|
if recurse_count >= self.max_recurse:
|
||||||
return(element)
|
return(element)
|
||||||
@ -858,33 +1168,10 @@ class xml_supplicant(object):
|
|||||||
_dictmap = self.btags_to_dict(element.text)
|
_dictmap = self.btags_to_dict(element.text)
|
||||||
return(element)
|
return(element)
|
||||||
|
|
||||||
def xpath_selector(self, selectors,
|
def xpath_selector(self, selectors):
|
||||||
selector_ids = ('id', 'name', 'uuid')):
|
|
||||||
# selectors is a dict of {attrib:value}
|
# selectors is a dict of {attrib:value}
|
||||||
xpath = ''
|
xpath = ''
|
||||||
for i in selectors.items():
|
for i in selectors.items():
|
||||||
if i[1] and i[0] in selector_ids:
|
if i[1] and i[0] in self.selector_ids:
|
||||||
xpath += '[@{0}="{1}"]'.format(*i)
|
xpath += '[@{0}="{1}"]'.format(*i)
|
||||||
return(xpath)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
3
bin/xmllint.sh
Executable file
3
bin/xmllint.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
xmllint -schema /opt/dev/bdisk/bdisk/bdisk.xsd /opt/dev/bdisk/docs/examples/multi_profile.xml --noout
|
@ -1,285 +1,288 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<bdisk>
|
<bdisk xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://bdisk.square-r00t.net/" xsi:schemaLocation="http://bdisk.square-r00t.net bdisk.xsd">
|
||||||
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
|
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
|
||||||
<meta>
|
<meta>
|
||||||
<names>
|
<names>
|
||||||
<name>BDisk</name>
|
<name>BDISK</name>
|
||||||
<uxname>bdisk</uxname>
|
<!--<name>{xpath%../uxname/text()}</name>-->
|
||||||
<!-- Just like with previous versions of BDisk, you can reference other values...
|
<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.
|
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
|
See https://www.w3schools.com/xml/xpath_syntax.asp
|
||||||
If you need a literal curly brace, double them (e.g. for "{foo}", use "{{foo}}"),
|
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). -->
|
UNLESS it's in a <regexes><pattern> as part of the expression. Those are taken as literal strings. -->
|
||||||
<pname>{xpath%../name/text()}</pname>
|
<pname>{xpath%../name/text()}</pname>
|
||||||
</names>
|
</names>
|
||||||
<desc>A rescue/restore live environment.</desc>
|
<desc>A rescue/restore live environment.</desc>
|
||||||
<dev>
|
<dev>
|
||||||
<author>A. Dev Eloper</author>
|
<author>A. Dev Eloper</author>
|
||||||
<email>dev@domain.tld</email>
|
<email>dev@domain.tld</email>
|
||||||
<website>https://domain.tld/~dev</website>
|
<website>https://domain.tld/~dev</website>
|
||||||
</dev>
|
</dev>
|
||||||
<uri>https://domain.tld/projname</uri>
|
<uri>https://domain.tld/projname</uri>
|
||||||
<ver>1.0.0</ver>
|
<ver>1.0.0</ver>
|
||||||
<!-- This is the VERY FIRST value parsed, and is required. It controls how many levels of {xpath%...} to recurse. -->
|
<!-- 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. -->
|
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
|
||||||
<max_recurse>5</max_recurse>
|
<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
|
<!-- 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. -->
|
items. See the manual for more information. NO btags within the patterns is allowed. -->
|
||||||
<regexes>
|
<regexes>
|
||||||
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
|
<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="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="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>
|
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
|
||||||
</regexes>
|
</regexes>
|
||||||
<!-- You can also define variables. NO xpath or regex btags, and they can't be used within other btags! -->
|
<!-- You can also define variables. NO xpath or regex btags, and they can't be used within other btags! -->
|
||||||
<variables>
|
<variables>
|
||||||
<variable id="bdisk_root">/var/tmp/BDisk</variable>
|
<variable id="bdisk_root">/var/tmp/BDisk</variable>
|
||||||
</variables>
|
</variables>
|
||||||
</meta>
|
</meta>
|
||||||
<accounts>
|
<accounts>
|
||||||
<!-- Salted/hashed password is "test" -->
|
<!-- Salted/hashed password is "test" -->
|
||||||
<rootpass hashed="yes">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
|
<rootpass hashed="true">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
|
||||||
<user sudo="yes">
|
<user sudo="true">
|
||||||
<username>{xpath%//meta/names/uxname/text()}</username>
|
<username>{xpath%//meta/names/uxname/text()}</username>
|
||||||
<!-- You can also use substitution from different profiles in this same configuration: -->
|
<!-- You can also use substitution from different profiles in this same configuration: -->
|
||||||
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
|
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
|
||||||
<comment>{xpath%//meta/dev/author/text()}</comment>
|
<comment>{xpath%//meta/dev/author/text()}</comment>
|
||||||
<password hashed="no" hash_algo="sha512" salt="auto">testpassword</password>
|
<password hashed="false" hash_algo="sha512" salt="auto">testpassword</password>
|
||||||
</user>
|
</user>
|
||||||
<user sudo="no">
|
<user sudo="false">
|
||||||
<username>testuser</username>
|
<username>testuser</username>
|
||||||
<name>Test User</name>
|
<comment>Test User</comment>
|
||||||
<password hashed="no" hash_algo="sha512" salt="auto">anothertestpassword</password>
|
<password hashed="false" hash_algo="sha512" salt="auto">anothertestpassword</password>
|
||||||
</user>
|
</user>
|
||||||
</accounts>
|
</accounts>
|
||||||
<sources>
|
<sources>
|
||||||
<source arch="x86_64">
|
<source arch="x86_64">
|
||||||
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
|
<tarball flags="regex latest">{regex%tarball_x86_64}</tarball>
|
||||||
<checksum hash_algo="sha1" flags="none">sha1sums.txt</checksum>
|
<checksum hash_algo="sha1" explicit="false" flags="latest">sha1sums.txt</checksum>
|
||||||
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_x86_64}</sig>
|
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex latest">{regex%sig_x86_64}</sig>
|
||||||
</source>
|
</source>
|
||||||
<source arch="i686">
|
<source arch="i686">
|
||||||
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
|
<tarball flags="regex latest">{regex%tarball_i686}</tarball>
|
||||||
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
|
<checksum hash_algo="sha512" explicit="true">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
|
||||||
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_i686}</sig>
|
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex latest">{regex%sig_i686}</sig>
|
||||||
</source>
|
</source>
|
||||||
</sources>
|
</sources>
|
||||||
<build its_full_of_stars="yes">
|
<build its_full_of_stars="true">
|
||||||
<paths>
|
<paths>
|
||||||
<base>{variable%bdisk_root}/base</base>
|
<base>{variable%bdisk_root}/base</base>
|
||||||
<cache>{variable%bdisk_root}/cache</cache>
|
<cache>{variable%bdisk_root}/cache</cache>
|
||||||
<chroot>{variable%bdisk_root}/chroots</chroot>
|
<chroot>{variable%bdisk_root}/chroots</chroot>
|
||||||
<overlay>{variable%bdisk_root}/overlay</overlay>
|
<overlay>{variable%bdisk_root}/overlay</overlay>
|
||||||
<templates>{variable%bdisk_root}/templates</templates>
|
<templates>{variable%bdisk_root}/templates</templates>
|
||||||
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
|
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
|
||||||
<distros>{variable%bdisk_root}/distros</distros>
|
<distros>{variable%bdisk_root}/distros</distros>
|
||||||
<dest>{variable%bdisk_root}/results</dest>
|
<dest>{variable%bdisk_root}/results</dest>
|
||||||
<iso>{variable%bdisk_root}/iso_overlay</iso>
|
<iso>{variable%bdisk_root}/iso_overlay</iso>
|
||||||
<http>{variable%bdisk_root}/http</http>
|
<http>{variable%bdisk_root}/http</http>
|
||||||
<tftp>{variable%bdisk_root}/tftp</tftp>
|
<tftp>{variable%bdisk_root}/tftp</tftp>
|
||||||
<pki>{variable%bdisk_root}/pki</pki>
|
<pki>{variable%bdisk_root}/pki</pki>
|
||||||
</paths>
|
</paths>
|
||||||
<basedistro>archlinux</basedistro>
|
<basedistro>archlinux</basedistro>
|
||||||
</build>
|
</build>
|
||||||
<iso sign="yes" multi_arch="yes"/>
|
<iso sign="true" multi_arch="true"/>
|
||||||
<ipxe sign="yes" iso="yes">
|
<ipxe sign="true" iso="true">
|
||||||
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
||||||
</ipxe>
|
</ipxe>
|
||||||
<pki overwrite="no">
|
<pki overwrite="false">
|
||||||
<!-- http://ipxe.org/crypto -->
|
<!-- http://ipxe.org/crypto -->
|
||||||
<ca>
|
<ca>
|
||||||
<cert hash_algo="sha512">{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.
|
<!-- If csr is self-enclosed (<csr />), we'll just generate and use a CSR in-memory.
|
||||||
Assuming we need to generate a certificate, anyways.
|
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,
|
If you want to write it out to disk (for debugging, etc.) OR use one already generated,
|
||||||
then provide a path.
|
then provide a path.
|
||||||
e.g.:
|
e.g.:
|
||||||
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
|
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
|
||||||
<csr/>
|
<csr/>
|
||||||
<!-- If you use an index file (or want to) to serialize client certificates, specify it here. -->
|
<!-- 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). -->
|
<!-- 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. -->
|
<!-- You should probably also specify a serial file if so. -->
|
||||||
<!-- Both of these are entirely optional if you aren't using an existing PKI. -->
|
<!-- Both of these are entirely optional if you aren't using an existing PKI. -->
|
||||||
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
|
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
|
||||||
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
|
<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 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
|
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. -->
|
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>
|
<key cipher="none" passphrase="none" keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
|
||||||
<subject>
|
<subject>
|
||||||
<commonName>domain.tld</commonName>
|
<commonName>domain.tld</commonName>
|
||||||
<countryName>XX</countryName>
|
<countryName>XX</countryName>
|
||||||
<localityName>Some City</localityName>
|
<localityName>Some City</localityName>
|
||||||
<stateOrProvinceName>Some State</stateOrProvinceName>
|
<stateOrProvinceName>Some State</stateOrProvinceName>
|
||||||
<organization>Some Org, Inc.</organization>
|
<organization>Some Org, Inc.</organization>
|
||||||
<organizationalUnitName>Department Name</organizationalUnitName>
|
<organizationalUnitName>Department Name</organizationalUnitName>
|
||||||
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
||||||
</subject>
|
</subject>
|
||||||
</ca>
|
</ca>
|
||||||
<client>
|
<client>
|
||||||
<cert hash_algo="sha512">{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/>
|
<csr/>
|
||||||
<key cipher="none" passphrase="none" keysize="4096">{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>
|
<subject>
|
||||||
<commonName>some client name</commonName>
|
<commonName>website.tld</commonName>
|
||||||
<countryName>XX</countryName>
|
<countryName>XX</countryName>
|
||||||
<localityName>Some City</localityName>
|
<localityName>Some City</localityName>
|
||||||
<stateOrProvinceName>Some State</stateOrProvinceName>
|
<stateOrProvinceName>Some State</stateOrProvinceName>
|
||||||
<organization>Some Org, Inc.</organization>
|
<organization>Some Org, Inc.</organization>
|
||||||
<organizationalUnitName>Department Name</organizationalUnitName>
|
<organizationalUnitName>Department Name</organizationalUnitName>
|
||||||
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
||||||
</subject>
|
</subject>
|
||||||
</client>
|
</client>
|
||||||
</pki>
|
</pki>
|
||||||
<!-- If prompt_passphrase is "no" and passphrase attribute is not given for a gpg element, we will try to use a
|
<!-- If prompt_passphrase is false and passphrase attribute is not given for a gpg element, we will try to use a
|
||||||
blank passphrase for all operations. -->
|
blank passphrase for all operations. -->
|
||||||
<gpg keyid="none" gnupghome="none" publish="no" prompt_passphrase="no">
|
<gpg keyid="none" gnupghome="none" publish="false" prompt_passphrase="false">
|
||||||
<!-- The below is only used if we are generating a key (i.e. keyid="none"). -->
|
<!-- The below is only used if we are generating a key (i.e. keyid="none"). -->
|
||||||
<key type="rsa" keysize="4096" expire="0">
|
<key algo="rsa" keysize="4096" expire="0">
|
||||||
<name>{xpath%../../../../meta/dev/author/text()}</name>
|
<name>{xpath%../../../meta/dev/author/text()}</name>
|
||||||
<email>{xpath%../../../../meta/dev/email/text()}</email>
|
<email>{xpath%../../../meta/dev/email/text()}</email>
|
||||||
<comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment>
|
<!-- If present, the subkey element will create a secondary key used *only* for signing. This is good security practice. Obviously, this is only used if we are creating a new (master) key. -->
|
||||||
</key>
|
<subkey algo="ed" keysize="25519" expire="0"/>
|
||||||
</gpg>
|
<comment>for {xpath%../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../meta/uri/text()} | {xpath%../../../meta/desc/text()}</comment>
|
||||||
<sync>
|
</key>
|
||||||
<ipxe enabled="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
</gpg>
|
||||||
<tftp enabled="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
<sync>
|
||||||
<iso enabled="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
<!-- ipxe includes the http directory. or should, anyways. -->
|
||||||
<gpg enabled="yes" format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
<ipxe enabled="true">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
||||||
<rsync enabled="yes">
|
<tftp enabled="true">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
||||||
<user>root</user>
|
<iso enabled="true">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
||||||
<host>mirror.domain.tld</host>
|
<gpg enabled="true" format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
||||||
<port>22</port>
|
<rsync enabled="true">
|
||||||
<pubkey>~/.ssh/id_ed25519</pubkey>
|
<user>root</user>
|
||||||
</rsync>
|
<host>mirror.domain.tld</host>
|
||||||
</sync>
|
<port>22</port>
|
||||||
</profile>
|
<pubkey>~/.ssh/id_ed25519</pubkey>
|
||||||
<profile name="alternate" id="2" uuid="2ed07c19-2071-4d66-8569-da40475ba716">
|
</rsync>
|
||||||
<meta>
|
</sync>
|
||||||
<names>
|
</profile>
|
||||||
<name>AnotherCD</name>
|
<profile name="alternate" id="2" uuid="2ed07c19-2071-4d66-8569-da40475ba716">
|
||||||
<uxname>bdisk_alt</uxname>
|
<meta>
|
||||||
<pname>{xpath%../name/text()}</pname>
|
<names>
|
||||||
</names>
|
<name>ALTCD</name>
|
||||||
<desc>Another rescue/restore live environment.</desc>
|
<uxname>bdisk_alt</uxname>
|
||||||
<dev>
|
<pname>{xpath%../name/text()}</pname>
|
||||||
<author>Another Dev Eloper</author>
|
</names>
|
||||||
<!-- You can reference other profiles within the same configuration. -->
|
<desc>Another rescue/restore live environment.</desc>
|
||||||
<email>{xpath%//profile[@name="default"]/meta/dev/email/text()}</email>
|
<dev>
|
||||||
<website>{xpath%//profile[@name="default"]/meta/dev/website/text()}</website>
|
<author>Another Dev Eloper</author><!-- You can reference other profiles within the same configuration. -->
|
||||||
</dev>
|
<email>{xpath%//profile[@name="default"]/meta/dev/email/text()}</email>
|
||||||
<uri>https://domain.tld/projname</uri>
|
<website>{xpath%//profile[@name="default"]/meta/dev/website/text()}</website>
|
||||||
<ver>0.0.1</ver>
|
</dev>
|
||||||
<max_recurse>5</max_recurse>
|
<uri>https://domain.tld/projname</uri>
|
||||||
<regexes>
|
<ver>0.0.1</ver>
|
||||||
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
|
<max_recurse>5</max_recurse>
|
||||||
<pattern id="sig_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz\.sig$</pattern>
|
<regexes>
|
||||||
<pattern id="tarball_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz$</pattern>
|
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
|
||||||
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
|
<pattern id="sig_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz\.sig$</pattern>
|
||||||
</regexes>
|
<pattern id="tarball_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz$</pattern>
|
||||||
<variables>
|
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
|
||||||
<variable id="bdisk_root">/var/tmp/BDisk</variable>
|
</regexes>
|
||||||
</variables>
|
<variables>
|
||||||
</meta>
|
<variable id="bdisk_root">/var/tmp/BDisk</variable>
|
||||||
<accounts>
|
</variables>
|
||||||
<rootpass hashed="no">atotallyinsecurepassword</rootpass>
|
</meta>
|
||||||
<user sudo="no">
|
<accounts>
|
||||||
<username>testuser</username>
|
<rootpass hashed="false">atotallyinsecurepassword</rootpass>
|
||||||
<comment>Test User</comment>
|
<user sudo="false">
|
||||||
<password hashed="no" hash_algo="sha512" salt="auto">atestpassword</password>
|
<username>testuser</username>
|
||||||
</user>
|
<comment>Test User</comment>
|
||||||
</accounts>
|
<password hashed="false" hash_algo="sha512" salt="auto">atestpassword</password>
|
||||||
<sources>
|
</user>
|
||||||
<source arch="x86_64">
|
</accounts>
|
||||||
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
<sources>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<source arch="x86_64">
|
||||||
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
|
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
||||||
<checksum hash_algo="sha1" flags="none">sha1sums.txt</checksum>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_x86_64}</sig>
|
<tarball flags="regex latest">{regex%tarball_x86_64}</tarball>
|
||||||
</source>
|
<checksum hash_algo="sha1" explicit="false" flags="latest">sha1sums.txt</checksum>
|
||||||
<source arch="i686">
|
<sig keys="7F2D434B9741E8AC" keyserver="hkp://pool.sks-keyservers.net" flags="regex latest">{regex%sig_x86_64}</sig>
|
||||||
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
</source>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<source arch="i686">
|
||||||
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
|
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
||||||
<checksum hash_algo="sha512" explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex,latest">{regex%sig_i686}</sig>
|
<tarball flags="regex latest">{regex%tarball_i686}</tarball>
|
||||||
</source>
|
<checksum hash_algo="sha512" explicit="true">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
|
||||||
</sources>
|
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506" keyserver="hkp://pool.sks-keyservers.net" flags="regex latest">{regex%sig_i686}</sig>
|
||||||
<build its_full_of_stars="yes">
|
</source>
|
||||||
<paths>
|
</sources>
|
||||||
<base>{variable%bdisk_root}/base</base>
|
<build its_full_of_stars="true">
|
||||||
<cache>{variable%bdisk_root}/cache</cache>
|
<paths>
|
||||||
<chroot>{variable%bdisk_root}/chroots</chroot>
|
<base>{variable%bdisk_root}/base</base>
|
||||||
<overlay>{variable%bdisk_root}/overlay</overlay>
|
<cache>{variable%bdisk_root}/cache</cache>
|
||||||
<templates>{variable%bdisk_root}/templates</templates>
|
<chroot>{variable%bdisk_root}/chroots</chroot>
|
||||||
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
|
<overlay>{variable%bdisk_root}/overlay</overlay>
|
||||||
<distros>{variable%bdisk_root}/distros</distros>
|
<templates>{variable%bdisk_root}/templates</templates>
|
||||||
<dest>{variable%bdisk_root}/results</dest>
|
<mount>/mnt/{xpath%//meta/names/uxname/text()}</mount>
|
||||||
<iso>{variable%bdisk_root}/iso_overlay</iso>
|
<distros>{variable%bdisk_root}/distros</distros>
|
||||||
<http>{variable%bdisk_root}/http</http>
|
<dest>{variable%bdisk_root}/results</dest>
|
||||||
<tftp>{variable%bdisk_root}/tftp</tftp>
|
<iso>{variable%bdisk_root}/iso_overlay</iso>
|
||||||
<pki>{variable%bdisk_root}/pki</pki>
|
<http>{variable%bdisk_root}/http</http>
|
||||||
</paths>
|
<tftp>{variable%bdisk_root}/tftp</tftp>
|
||||||
<basedistro>archlinux</basedistro>
|
<pki>{variable%bdisk_root}/pki</pki>
|
||||||
</build>
|
</paths>
|
||||||
<iso sign="yes" multi_arch="yes"/>
|
<basedistro>archlinux</basedistro>
|
||||||
<ipxe sign="yes" iso="yes">
|
</build>
|
||||||
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
<iso sign="true" multi_arch="true"/>
|
||||||
</ipxe>
|
<ipxe sign="true" iso="true">
|
||||||
<pki overwrite="no">
|
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
||||||
<ca>
|
</ipxe>
|
||||||
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
|
<pki overwrite="false">
|
||||||
<csr/>
|
<ca>
|
||||||
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
|
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
|
||||||
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
|
<csr/>
|
||||||
<key cipher="none" passphrase="none" keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
|
<index>{xpath%../../../build/paths/pki/text()}/index.txt</index>
|
||||||
<subject>
|
<serial>{xpath%../../../build/paths/pki/text()}/serial</serial>
|
||||||
<commonName>domain.tld</commonName>
|
<key cipher="none" passphrase="none" keysize="4096">{xpath%../../../build/paths/pki/text()}/ca.key</key>
|
||||||
<countryName>XX</countryName>
|
<subject>
|
||||||
<localityName>Some City</localityName>
|
<commonName>domain.tld</commonName>
|
||||||
<stateOrProvinceName>Some State</stateOrProvinceName>
|
<countryName>XX</countryName>
|
||||||
<organization>Some Org, Inc.</organization>
|
<localityName>Some City</localityName>
|
||||||
<organizationalUnitName>Department Name</organizationalUnitName>
|
<stateOrProvinceName>Some State</stateOrProvinceName>
|
||||||
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
<organization>Some Org, Inc.</organization>
|
||||||
</subject>
|
<organizationalUnitName>Department Name</organizationalUnitName>
|
||||||
</ca>
|
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
||||||
<client>
|
</subject>
|
||||||
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
|
</ca>
|
||||||
<csr/>
|
<client>
|
||||||
<key cipher="none" passphrase="none" keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
|
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.crt</cert>
|
||||||
<subject>
|
<csr/>
|
||||||
<commonName>some client name</commonName>
|
<key cipher="none" passphrase="none" keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
|
||||||
<countryName>XX</countryName>
|
<subject>
|
||||||
<localityName>Some City</localityName>
|
<commonName>website.tld</commonName>
|
||||||
<stateOrProvinceName>Some State</stateOrProvinceName>
|
<countryName>XX</countryName>
|
||||||
<organization>Some Org, Inc.</organization>
|
<localityName>Some City</localityName>
|
||||||
<organizationalUnitName>Department Name</organizationalUnitName>
|
<stateOrProvinceName>Some State</stateOrProvinceName>
|
||||||
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
<organization>Some Org, Inc.</organization>
|
||||||
</subject>
|
<organizationalUnitName>Department Name</organizationalUnitName>
|
||||||
</client>
|
<emailAddress>{xpath%../../../../meta/dev/email/text()}</emailAddress>
|
||||||
</pki>
|
</subject>
|
||||||
<gpg keyid="none" gnupghome="none" publish="no" prompt_passphrase="no">
|
</client>
|
||||||
<key type="rsa" keysize="4096" expire="0">
|
</pki>
|
||||||
<name>{xpath%../../../../meta/dev/author/text()}</name>
|
<gpg keyid="none" gnupghome="none" publish="false" prompt_passphrase="false">
|
||||||
<email>{xpath%../../../../meta/dev/email/text()}</email>
|
<key algo="rsa" keysize="4096" expire="0">
|
||||||
<comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment>
|
<name>{xpath%../../../meta/dev/author/text()}</name>
|
||||||
</key>
|
<email>{xpath%../../../meta/dev/email/text()}</email>
|
||||||
</gpg>
|
<comment>for {xpath%../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../meta/uri/text()} | {xpath%../../../meta/desc/text()}</comment>
|
||||||
<sync>
|
</key>
|
||||||
<ipxe enabled="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
</gpg>
|
||||||
<tftp enabled="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
<sync>
|
||||||
<iso enabled="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
<ipxe enabled="true">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
||||||
<gpg enabled="yes" format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
<tftp enabled="true">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
||||||
<rsync enabled="yes">
|
<iso enabled="true">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
||||||
<user>root</user>
|
<gpg enabled="true" format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
||||||
<host>mirror.domain.tld</host>
|
<rsync enabled="true">
|
||||||
<port>22</port>
|
<user>root</user>
|
||||||
<pubkey>~/.ssh/id_ed25519</pubkey>
|
<host>mirror.domain.tld</host>
|
||||||
</rsync>
|
<port>22</port>
|
||||||
</sync>
|
<pubkey>~/.ssh/id_ed25519</pubkey>
|
||||||
</profile>
|
</rsync>
|
||||||
|
</sync>
|
||||||
|
</profile>
|
||||||
</bdisk>
|
</bdisk>
|
||||||
|
@ -1,13 +1,34 @@
|
|||||||
#!/usr/bin/env python3.6
|
#!/usr/bin/env python3.6
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from lxml import etree
|
from lxml import etree, objectify
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text = True)
|
#parser = etree.XMLParser(remove_blank_text = True)
|
||||||
|
parser = etree.XMLParser(remove_blank_text = False)
|
||||||
|
|
||||||
|
# We need to append to a new root because you can't edit nsmap, and you can't
|
||||||
|
# xpath on an element with a naked namespace (e.g. 'xlmns="..."').
|
||||||
|
ns = {None: 'http://bdisk.square-r00t.net/',
|
||||||
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
|
||||||
|
xsi = {'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation':
|
||||||
|
'http://bdisk.square-r00t.net bdisk.xsd'}
|
||||||
|
new_cfg = etree.Element('bdisk', nsmap = ns, attrib = xsi)
|
||||||
|
new_cfg.text = '\n '
|
||||||
|
|
||||||
with open('single_profile.xml', 'rb') as f:
|
with open('single_profile.xml', 'rb') as f:
|
||||||
xml = etree.fromstring(f.read(), parser)
|
xml = etree.fromstring(f.read(), parser)
|
||||||
|
|
||||||
|
|
||||||
|
roottree = xml.getroottree()
|
||||||
|
for elem in roottree.getiterator():
|
||||||
|
if not hasattr(elem.tag, 'find'):
|
||||||
|
continue
|
||||||
|
i = elem.tag.find('}')
|
||||||
|
if i >= 0:
|
||||||
|
elem.tag = elem.tag[i + 1:]
|
||||||
|
objectify.deannotate(roottree, cleanup_namespaces = True)
|
||||||
|
|
||||||
|
|
||||||
single_profile = xml.xpath('/bdisk/profile[1]')[0]
|
single_profile = xml.xpath('/bdisk/profile[1]')[0]
|
||||||
alt_profile = copy.deepcopy(single_profile)
|
alt_profile = copy.deepcopy(single_profile)
|
||||||
for c in alt_profile.xpath('//comment()'):
|
for c in alt_profile.xpath('//comment()'):
|
||||||
@ -19,7 +40,7 @@ alt_profile.attrib['name'] = 'alternate'
|
|||||||
alt_profile.attrib['id'] = '2'
|
alt_profile.attrib['id'] = '2'
|
||||||
alt_profile.attrib['uuid'] = '2ed07c19-2071-4d66-8569-da40475ba716'
|
alt_profile.attrib['uuid'] = '2ed07c19-2071-4d66-8569-da40475ba716'
|
||||||
|
|
||||||
meta_tags = {'name': 'AnotherCD',
|
meta_tags = {'name': 'ALTCD',
|
||||||
'uxname': 'bdisk_alt',
|
'uxname': 'bdisk_alt',
|
||||||
'pname': '{xpath%../name/text()}',
|
'pname': '{xpath%../name/text()}',
|
||||||
'desc': 'Another rescue/restore live environment.',
|
'desc': 'Another rescue/restore live environment.',
|
||||||
@ -42,18 +63,22 @@ for e in accounts.iter():
|
|||||||
if e.tag in accounts_tags:
|
if e.tag in accounts_tags:
|
||||||
e.text = accounts_tags[e.tag]
|
e.text = accounts_tags[e.tag]
|
||||||
if e.tag == 'rootpass':
|
if e.tag == 'rootpass':
|
||||||
e.attrib['hashed'] = 'no'
|
e.attrib['hashed'] = 'false'
|
||||||
elif e.tag == 'user':
|
elif e.tag == 'user':
|
||||||
e.attrib['sudo'] = 'no'
|
e.attrib['sudo'] = 'false'
|
||||||
# Delete the second user
|
# Delete the second user
|
||||||
accounts.remove(accounts[2])
|
accounts.remove(accounts[2])
|
||||||
author = alt_profile.xpath('/profile/meta/dev/author')[0]
|
author = alt_profile.xpath('/profile/meta/dev/author')[0]
|
||||||
author.addnext(etree.Comment(
|
author.addnext(etree.Comment(
|
||||||
' You can reference other profiles within the same configuration. '))
|
' You can reference other profiles within the same configuration. '))
|
||||||
xml.append(alt_profile)
|
#xml.append(alt_profile)
|
||||||
|
|
||||||
|
for child in xml.xpath('/bdisk/profile'):
|
||||||
|
new_cfg.append(copy.deepcopy(child))
|
||||||
|
new_cfg.append(alt_profile)
|
||||||
|
|
||||||
with open('multi_profile.xml', 'wb') as f:
|
with open('multi_profile.xml', 'wb') as f:
|
||||||
f.write(etree.tostring(xml,
|
f.write(etree.tostring(new_cfg,
|
||||||
pretty_print = True,
|
pretty_print = True,
|
||||||
encoding = 'UTF-8',
|
encoding = 'UTF-8',
|
||||||
xml_declaration = True))
|
xml_declaration = True))
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<bdisk>
|
<bdisk xmlns="http://bdisk.square-r00t.net/"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://bdisk.square-r00t.net bdisk.xsd">
|
||||||
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
|
<profile name="default" id="1" uuid="8cdd6bcb-c147-4a63-9779-b5433c510dbc">
|
||||||
<meta>
|
<meta>
|
||||||
<names>
|
<names>
|
||||||
<name>BDisk</name>
|
<name>BDISK</name>
|
||||||
|
<!--<name>{xpath%../uxname/text()}</name>-->
|
||||||
<uxname>bdisk</uxname>
|
<uxname>bdisk</uxname>
|
||||||
<!-- Just like with previous versions of BDisk, you can reference other values...
|
<!-- 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.
|
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
|
See https://www.w3schools.com/xml/xpath_syntax.asp
|
||||||
If you need a literal curly brace, double them (e.g. for "{foo}", use "{{foo}}"),
|
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). -->
|
UNLESS it's in a <regexes><pattern> as part of the expression. Those are taken as literal strings. -->
|
||||||
<pname>{xpath%../name/text()}</pname>
|
<pname>{xpath%../name/text()}</pname>
|
||||||
</names>
|
</names>
|
||||||
<desc>A rescue/restore live environment.</desc>
|
<desc>A rescue/restore live environment.</desc>
|
||||||
@ -24,10 +27,11 @@
|
|||||||
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
|
<!-- If the maximum level is reached, the substitution will evaluate as blank. -->
|
||||||
<max_recurse>5</max_recurse>
|
<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
|
<!-- 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. -->
|
items. See the manual for more information. NO btags within the patterns is allowed. -->
|
||||||
<regexes>
|
<regexes>
|
||||||
<pattern id="tarball_x86_64">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64\.tar\.gz$</pattern>
|
<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="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="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>
|
<pattern id="sig_i686">archlinux-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-i686\.tar\.gz\.sig$</pattern>
|
||||||
</regexes>
|
</regexes>
|
||||||
@ -38,20 +42,20 @@
|
|||||||
</meta>
|
</meta>
|
||||||
<accounts>
|
<accounts>
|
||||||
<!-- Salted/hashed password is "test" -->
|
<!-- Salted/hashed password is "test" -->
|
||||||
<rootpass hashed="yes">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
|
<rootpass hashed="true">$6$7KfIdtHTcXwVrZAC$LZGNeMNz7v5o/cYuA48FAxtZynpIwO5B1CPGXnOW5kCTVpXVt4SypRqfM.AoKkFt/O7MZZ8ySXJmxpELKmdlF1</rootpass>
|
||||||
<user sudo="yes">
|
<user sudo="true">
|
||||||
<username>{xpath%//meta/names/uxname/text()}</username>
|
<username>{xpath%../../../meta/names/uxname/text()}</username>
|
||||||
<!-- You can also use substitution from different profiles in this same configuration: -->
|
<!-- You can also use substitution from different profiles in this same configuration: -->
|
||||||
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
|
<!-- <username>{xpath%//profile[@name='another_profile']/meta/names/uxname"}</username> -->
|
||||||
<comment>{xpath%//meta/dev/author/text()}</comment>
|
<comment>{xpath%../../../meta/dev/author/text()}</comment>
|
||||||
<password hashed="no"
|
<password hashed="false"
|
||||||
hash_algo="sha512"
|
hash_algo="sha512"
|
||||||
salt="auto">testpassword</password>
|
salt="auto">testpassword</password>
|
||||||
</user>
|
</user>
|
||||||
<user sudo="no">
|
<user sudo="false">
|
||||||
<username>testuser</username>
|
<username>testuser</username>
|
||||||
<name>Test User</name>
|
<comment>Test User</comment>
|
||||||
<password hashed="no"
|
<password hashed="false"
|
||||||
hash_algo="sha512"
|
hash_algo="sha512"
|
||||||
salt="auto">anothertestpassword</password>
|
salt="auto">anothertestpassword</password>
|
||||||
</user>
|
</user>
|
||||||
@ -60,25 +64,29 @@
|
|||||||
<source arch="x86_64">
|
<source arch="x86_64">
|
||||||
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
<mirror>http://archlinux.mirror.domain.tld</mirror>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<tarball flags="regex,latest">{regex%tarball_x86_64}</tarball>
|
<tarball flags="regex latest">{regex%tarball_x86_64}</tarball>
|
||||||
<checksum hash_algo="sha1"
|
<checksum hash_algo="sha1"
|
||||||
flags="none">sha1sums.txt</checksum>
|
explicit="false"
|
||||||
<sig keys="7F2D434B9741E8AC"
|
flags="latest">sha1sums.txt</checksum>
|
||||||
|
<sig keys="7F2D434B9741E8AC"
|
||||||
keyserver="hkp://pool.sks-keyservers.net"
|
keyserver="hkp://pool.sks-keyservers.net"
|
||||||
flags="regex,latest">{regex%sig_x86_64}</sig>
|
flags="regex latest">{regex%sig_x86_64}</sig>
|
||||||
</source>
|
</source>
|
||||||
<source arch="i686">
|
<source arch="i686">
|
||||||
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
<mirror>http://archlinux32.mirror.domain.tld</mirror>
|
||||||
<rootpath>/iso/latest</rootpath>
|
<rootpath>/iso/latest</rootpath>
|
||||||
<tarball flags="regex,latest">{regex%tarball_i686}</tarball>
|
<tarball flags="regex latest">{regex%tarball_i686}</tarball>
|
||||||
<checksum hash_algo="sha512"
|
<checksum hash_algo="sha512"
|
||||||
explicit="yes">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e</checksum>
|
explicit="true">cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e </checksum>
|
||||||
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506"
|
<sig keys="248BF41F9BDD61D41D060AE774EDA3C6B06D0506"
|
||||||
keyserver="hkp://pool.sks-keyservers.net"
|
keyserver="hkp://pool.sks-keyservers.net"
|
||||||
flags="regex,latest">{regex%sig_i686}</sig>
|
flags="regex latest">{regex%sig_i686}</sig>
|
||||||
</source>
|
</source>
|
||||||
</sources>
|
</sources>
|
||||||
<build its_full_of_stars="yes">
|
<packages>
|
||||||
|
<package repo="core">openssh</package>
|
||||||
|
</packages>
|
||||||
|
<build its_full_of_stars="true">
|
||||||
<paths>
|
<paths>
|
||||||
<base>{variable%bdisk_root}/base</base>
|
<base>{variable%bdisk_root}/base</base>
|
||||||
<cache>{variable%bdisk_root}/cache</cache>
|
<cache>{variable%bdisk_root}/cache</cache>
|
||||||
@ -95,11 +103,11 @@
|
|||||||
</paths>
|
</paths>
|
||||||
<basedistro>archlinux</basedistro>
|
<basedistro>archlinux</basedistro>
|
||||||
</build>
|
</build>
|
||||||
<iso sign="yes" multi_arch="yes" />
|
<iso sign="true" multi_arch="true"/>
|
||||||
<ipxe sign="yes" iso="yes">
|
<ipxe sign="true" iso="true">
|
||||||
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
<uri>{xpath%//meta/dev/website/text()}/ipxe</uri>
|
||||||
</ipxe>
|
</ipxe>
|
||||||
<pki overwrite="no">
|
<pki overwrite="false">
|
||||||
<!-- http://ipxe.org/crypto -->
|
<!-- http://ipxe.org/crypto -->
|
||||||
<ca>
|
<ca>
|
||||||
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
|
<cert hash_algo="sha512">{xpath%../../../build/paths/pki/text()}/ca.crt</cert>
|
||||||
@ -109,7 +117,7 @@
|
|||||||
then provide a path.
|
then provide a path.
|
||||||
e.g.:
|
e.g.:
|
||||||
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
|
<csr>{xpath%build/paths/ssl/text()}/ca.csr</csr> -->
|
||||||
<csr />
|
<csr/>
|
||||||
<!-- If you use an index file (or want to) to serialize client certificates, specify it here. -->
|
<!-- 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). -->
|
<!-- 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. -->
|
<!-- You should probably also specify a serial file if so. -->
|
||||||
@ -134,12 +142,12 @@
|
|||||||
</ca>
|
</ca>
|
||||||
<client>
|
<client>
|
||||||
<cert hash_algo="sha512">{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 />
|
<csr/>
|
||||||
<key cipher="none"
|
<key cipher="none"
|
||||||
passphrase="none"
|
passphrase="none"
|
||||||
keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
|
keysize="4096">{xpath%//build/paths/pki/text()}/{xpath%../../../meta/names/uxname/text()}.key</key>
|
||||||
<subject>
|
<subject>
|
||||||
<commonName>some client name</commonName>
|
<commonName>website.tld</commonName>
|
||||||
<countryName>XX</countryName>
|
<countryName>XX</countryName>
|
||||||
<localityName>Some City</localityName>
|
<localityName>Some City</localityName>
|
||||||
<stateOrProvinceName>Some State</stateOrProvinceName>
|
<stateOrProvinceName>Some State</stateOrProvinceName>
|
||||||
@ -149,26 +157,27 @@
|
|||||||
</subject>
|
</subject>
|
||||||
</client>
|
</client>
|
||||||
</pki>
|
</pki>
|
||||||
<!-- If prompt_passphrase is "no" and passphrase attribute is not given for a gpg element, we will try to use a
|
<!-- If prompt_passphrase is "false" and passphrase attribute is not given for a gpg element, we will try to use a
|
||||||
blank passphrase for all operations. -->
|
blank passphrase for all operations. -->
|
||||||
<gpg keyid="none"
|
<gpg keyid="none"
|
||||||
gnupghome="none"
|
gnupghome="none"
|
||||||
publish="no"
|
publish="false"
|
||||||
prompt_passphrase="no">
|
prompt_passphrase="false">
|
||||||
<!-- The below is only used if we are generating a key (i.e. keyid="none"). -->
|
<!-- The below is only used if we are generating a key (i.e. keyid="none"). -->
|
||||||
<key algo="rsa" keysize="4096" expire="0">
|
<key algo="rsa" keysize="4096" expire="0">
|
||||||
<name>{xpath%../../../../meta/dev/author/text()}</name>
|
<name>{xpath%../../../meta/dev/author/text()}</name>
|
||||||
<email>{xpath%../../../../meta/dev/email/text()}</email>
|
<email>{xpath%../../../meta/dev/email/text()}</email>
|
||||||
<comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment>
|
<comment>for {xpath%../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../meta/uri/text()} | {xpath%../../../meta/desc/text()}</comment>
|
||||||
</key>
|
</key>
|
||||||
</gpg>
|
</gpg>
|
||||||
<sync>
|
<sync>
|
||||||
<ipxe enabled="yes">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
<!-- ipxe includes the http directory. or should, anyways. -->
|
||||||
<tftp enabled="yes">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
<ipxe enabled="true">/srv/http/{xpath%../../meta/names/uxname/text()}</ipxe>
|
||||||
<iso enabled="yes">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
<tftp enabled="true">/tftproot/{xpath%../../meta/names/uxname/text()}</tftp>
|
||||||
<gpg enabled="yes"
|
<iso enabled="true">/srv/http/isos/{xpath%../../meta/names/uxname/text()}</iso>
|
||||||
|
<gpg enabled="true"
|
||||||
format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
format="asc">/srv/http/{xpath%../../meta/names/uxname/text()}/pubkey.asc</gpg>
|
||||||
<rsync enabled="yes">
|
<rsync enabled="true">
|
||||||
<user>root</user>
|
<user>root</user>
|
||||||
<host>mirror.domain.tld</host>
|
<host>mirror.domain.tld</host>
|
||||||
<port>22</port>
|
<port>22</port>
|
||||||
|
@ -7,4 +7,7 @@
|
|||||||
|
|
||||||
- in faq/ISOBIG.adoc and the doc section it references, make sure we reference that the package lists are now in the environment plugin!
|
- in faq/ISOBIG.adoc and the doc section it references, make sure we reference that the package lists are now in the environment plugin!
|
||||||
|
|
||||||
- change all references to build.ini to something like "BDisk configuration file"
|
- change all references to build.ini to something like "BDisk configuration file"
|
||||||
|
|
||||||
|
- reminder: users can specify a local file source for <sources><source> items by using "file:///absolute/path/to/file"
|
||||||
|
-- todo: add http auth, ftp, ftps
|
8
examples/README
Normal file
8
examples/README
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This directory contains example files/data that you may see referenced in documentation/code.
|
||||||
|
|
||||||
|
- mtree.spec
|
||||||
|
This file is an example mtree spec sheet that one may use for an overlay. It was generated by the command "mtree -c -K all -p /home/bts".
|
||||||
|
If you're on Arch, a port of mtree can be found in the AUR under the package name "nmtree" (it's maintained by the same author as BDisk!).
|
||||||
|
If you're on Debian or Ubuntu (or forks thereof), you can find it in the "freebsd-buildutils" package. (The executable is called "fmtree").
|
||||||
|
If you're on Gentoo, it's in sys-apps/mtree.
|
||||||
|
If you're on RHEL/CentOS, the "extras" repository has gomtree, which (although written in Go) should be able to produce mtree spec files (but this is unknown for certain).
|
1191
examples/mtree.spec
Normal file
1191
examples/mtree.spec
Normal file
File diff suppressed because it is too large
Load Diff
BIN
external/apacman-current.pkg.tar.xz
vendored
Normal file
BIN
external/apacman-current.pkg.tar.xz
vendored
Normal file
Binary file not shown.
BIN
external/aurman-current.pkg.tar.xz
vendored
Normal file
BIN
external/aurman-current.pkg.tar.xz
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user