665 lines
34 KiB
Python
Executable File
665 lines
34 KiB
Python
Executable File
#!/usr/bin/env python3.6
|
|
|
|
import confparse
|
|
import crypt
|
|
import getpass
|
|
import os
|
|
import utils
|
|
import uuid
|
|
import lxml.etree
|
|
|
|
detect = utils.detect()
|
|
generate = utils.generate()
|
|
prompt = utils.prompts()
|
|
transform = utils.transform()
|
|
valid = utils.valid()
|
|
|
|
# TODO: convert the restarts for prompts to continue's instead of letting them
|
|
# continue on to the next prompt.
|
|
|
|
def pass_prompt(user):
|
|
# This isn't in utils.prompts() because we need to use an instance of
|
|
# utils.valid() and it feels like it belongs here, since it's only usable
|
|
# for configuration generation.
|
|
passwd = {'hashed': None,
|
|
'password': None,
|
|
'hash_algo': None,
|
|
'salt': None}
|
|
_special_password_values = ('BLANK', '')
|
|
_passwd_is_special = False
|
|
_need_input_type = True
|
|
while _need_input_type:
|
|
_input_type = input('\nWill you be entering a password or a salted '
|
|
'hash? (If using a "special" value per the '
|
|
'manual, use password entry):\n\n'
|
|
'\t\t1: password\n'
|
|
'\t\t2: salted hash\n\n'
|
|
'Choice: ').strip()
|
|
if not valid.integer(_input_type):
|
|
print('You must enter 1 or 2.')
|
|
else:
|
|
if int(_input_type) == 1:
|
|
_input_type = 'password'
|
|
_need_input_type = False
|
|
passwd['hashed'] = False
|
|
elif int(input_type) == 2:
|
|
_input_type = 'salted hash'
|
|
_need_input_type = False
|
|
passwd['hashed'] = True
|
|
else:
|
|
print('You must enter 1 or 2.')
|
|
_prompt = ('\nWhat do you want {0}\'s {1} to be?\n').format(user,
|
|
_input_type)
|
|
if passwd['hashed']:
|
|
passwd['password'] = input('{0}\n{1}: '.format(_prompt,
|
|
_input_type.title()))
|
|
if not valid.password_hash:
|
|
print('This is not a valid password hash. Re-running.')
|
|
pass_prompt(user)
|
|
else:
|
|
passwd['password'] = getpass.getpass(_prompt + ('See the manual for '
|
|
'special values.\nYour input will NOT '
|
|
'echo back (unless it\'s a special value).\n'
|
|
'{0}: ').format(_input_type.title()))
|
|
if passwd['password'] in _special_password_values:
|
|
_passwd_is_special = True
|
|
# 'BLANK' => '' => <(root)password></(root)password>
|
|
if passwd['password'] == 'BLANK':
|
|
passwd['password'] == ''
|
|
# '' => None => <(root)password />
|
|
elif passwd['password'] == '':
|
|
passwd['password'] == None
|
|
if not valid.password(passwd['password']):
|
|
print('As a safety precaution, we are refusing to use this '
|
|
'password. It should entirely consist of the 95 printable '
|
|
'ASCII characters. Consult the manual\'s section on '
|
|
'passwords for more information.\nLet\'s try this again, '
|
|
'shall we?')
|
|
pass_prompt(user)
|
|
_salt = input('\nEnter the salt to use. If left blank, one will be '
|
|
'automatically generated. See the manual for special '
|
|
'values.\nSalt: ').strip()
|
|
if _salt == '':
|
|
pass
|
|
elif _salt == 'auto':
|
|
passwd['salt'] = 'auto'
|
|
elif not valid.salt_hash():
|
|
print('This is not a valid salt. Let\'s try this again.')
|
|
pass_prompt(user)
|
|
else:
|
|
passwd['salt'] = _salt
|
|
_algo = input(('\nWhat algorithm should we use to hash the password? '
|
|
'The default is sha512. You can choose from the '
|
|
'following:\n\n'
|
|
'\t\t{0}\n\nAlgorithm: ').format(
|
|
'\n\t\t'.join(list(utils.crypt_map.keys()))
|
|
)).strip().lower()
|
|
if _algo == '':
|
|
_algo = 'sha512'
|
|
if _algo not in utils.crypt_map:
|
|
print('Algorithm not found; let\'s try this again.')
|
|
pass_prompt(user)
|
|
else:
|
|
passwd['hash_algo'] = _algo
|
|
if _salt == '':
|
|
passwd['salt'] = generate.salt(_algo)
|
|
if not _passwd_is_special:
|
|
_gen_now = prompt.confirm_or_no(prompt = '\nGenerate a password '
|
|
'hash now? This is HIGHLY recommended; otherwise, '
|
|
'the plaintext password will be stored in the '
|
|
'configuration and that is no bueno.\n')
|
|
if _gen_now:
|
|
passwd['password'] = generate.hash_password(
|
|
passwd['password'],
|
|
salt = passwd['salt'],
|
|
algo = passwd['hash_algo'])
|
|
passwd['hashed'] = True
|
|
return(passwd)
|
|
|
|
class ConfGenerator(object):
|
|
def __init__(self, cfgfile = None, append_config = False):
|
|
if append_config:
|
|
if not cfgfile:
|
|
raise RuntimeError('You have specified config appending but '
|
|
'did not provide a configuration file')
|
|
if cfgfile:
|
|
self.cfgfile = os.path.abspath(os.path.expanduser(cfgfile))
|
|
else:
|
|
# Write to STDOUT
|
|
self.cfgfile = None
|
|
c = confparse.Conf(cfgfile)
|
|
self.cfg = c.xml
|
|
self.append = True
|
|
else:
|
|
self.cfg = lxml.etree.Element('bdisk')
|
|
self.append = False
|
|
self.profile = lxml.etree.Element('profile')
|
|
self.cfg.append(self.profile)
|
|
|
|
def main(self):
|
|
print(('\n\tPlease consult the manual at {manual_site} if you have '
|
|
'any questions.'
|
|
'\n\tYou can hit CTRL-c at any time to quit.\n'
|
|
).format(manual_site = 'https://bdisk.square-r00t.net/'))
|
|
try:
|
|
self.get_profile_attribs()
|
|
self.get_meta()
|
|
self.get_accounts()
|
|
self.get_sources()
|
|
self.get_build()
|
|
except KeyboardInterrupt:
|
|
exit('\n\nCaught KeyboardInterrupt; quitting...')
|
|
return()
|
|
|
|
def get_profile_attribs(self):
|
|
print('++ PROFILE ATTRIBUTES ++')
|
|
id_attrs = {'name': None,
|
|
'id': None,
|
|
'uuid': None}
|
|
while not any(tuple(id_attrs.values())):
|
|
print('\nThese are used to uniquely identify the profile you are '
|
|
'creating. To ensure compatibility with other processes, '
|
|
'each profile MUST be unique (even if you\'re only storing '
|
|
'one profile per file). That means at least ONE of these '
|
|
'attributes must be populated. You can hit enter to leave '
|
|
'the attribute blank - you don\'t need to provide ALL '
|
|
'attributes (though it\'s certainly recommended).')
|
|
id_attrs['name'] = transform.sanitize_input(
|
|
(input(
|
|
'\nWhat name should this profile be? (It will '
|
|
'be transformed to a safe string if '
|
|
'necessary.)\nName: ')
|
|
))
|
|
id_attrs['id'] = transform.sanitize_input(
|
|
(input(
|
|
'\nWhat ID number should this profile have? It MUST be a '
|
|
'positive integer.\nID: ')
|
|
).strip())
|
|
if id_attrs['id']:
|
|
if not valid.integer(id_attrs['id']):
|
|
print('Invalid; skipping...')
|
|
id_attrs['id'] = None
|
|
# We don't sanitize this because it'd break. UUID4 requires hyphen
|
|
# separators. We still validate, though.
|
|
id_attrs['uuid'] = input(
|
|
'\nWhat UUID should this profile have? '
|
|
'It MUST be a UUID4 (RFC4122 § 4.4). e.g.:\n'
|
|
'\t333d7287-3caa-45fe-b954-2da15dad1212\n'
|
|
'If you use the special value "auto" (without quotes), then '
|
|
'one will be automatically generated for you.\nUUID: ').strip()
|
|
if id_attrs['uuid'].lower() == 'auto':
|
|
id_attrs['uuid'] = str(uuid.uuid4())
|
|
print('\n\tGenerated a UUID: {0}\n'.format(id_attrs['uuid']))
|
|
else:
|
|
if not valid.uuid(id_attrs['uuid']):
|
|
print('Invalid; skipping...')
|
|
id_attrs['uuid'] = None
|
|
# This causes a looping if none of the answers are valid.
|
|
for i in id_attrs:
|
|
if id_attrs[i] == '':
|
|
id_attrs[i] = None
|
|
for i in id_attrs:
|
|
if id_attrs[i]:
|
|
self.profile.attrib[i] = id_attrs[i]
|
|
print()
|
|
return()
|
|
|
|
def get_meta(self):
|
|
print('\n++ META ITEMS ++')
|
|
meta_items = {'names': {'name': None,
|
|
'uxname': None,
|
|
'pname': None},
|
|
'desc': None,
|
|
'uri': None,
|
|
'ver': None,
|
|
'dev': {'author': None,
|
|
'email': None,
|
|
'website': None},
|
|
'max_recurse': None}
|
|
while (not transform.flatten_recurse(meta_items) or \
|
|
(None in transform.flatten_recurse(meta_items))):
|
|
print('\nThese are used primarily for branding (with the '
|
|
'exception of recursion level, which is used '
|
|
'operationally).\n*All* items are REQUIRED (and if any are '
|
|
'blank or invalid, the entire section will restart), but '
|
|
'you may want to tweak the VERSION_INFO.txt.j2 template if '
|
|
'you don\'t want this information exposed to your users '
|
|
'(see the manual for more detail).')
|
|
print('\n++ META ITEMS || NAMES ++')
|
|
# https://en.wikipedia.org/wiki/8.3_filename
|
|
meta_items['names']['name'] = transform.sanitize_input(
|
|
input(
|
|
'\nWhat 8.3 filename should be used as the name of this '
|
|
'project/live distro? Refer to the manual\'s Configuration '
|
|
'section for path /bdisk/profile/meta/names/name for '
|
|
'restrictions (there are quite a few).\n8.3 Name: ').strip(),
|
|
no_underscores = True).upper()
|
|
if (len(meta_items['names']['name']) > 8) or (
|
|
meta_items['names']['name'] == ''):
|
|
print('Invalid; skipping...')
|
|
meta_items['names']['name'] = None
|
|
# Note: 2009 spec
|
|
# http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
|
|
meta_items['names']['uxname'] = input(
|
|
'\nWhat name should be used as the "human-readable" name of '
|
|
'this project/live distro? Refer to the manual\'s '
|
|
'Configuration section for path '
|
|
'/bdisk/profile/meta/names/uxname for restrictions, but in a '
|
|
'nutshell it must be compatible with the "POSIX Portable '
|
|
'Filename Character Set" specification (the manual has a '
|
|
'link).\nName: ').strip()
|
|
if not valid.posix_filename(meta_items['names']['uxname']):
|
|
print('Invalid; skipping...')
|
|
meta_items['names']['uxname'] = None
|
|
meta_items['names']['pname'] = input(
|
|
'\nWhat name should be used as the "pretty" name of this '
|
|
'project/live distro? Refer to the manual\'s Configuration '
|
|
'section for path /bdisk/profile/meta/names/uxname for '
|
|
'restrictions, but this is by far the most lax naming. It '
|
|
'should be used for your actual branding.\nName: ').strip()
|
|
if meta_items['names']['pname'] == '':
|
|
meta_items['names']['pname'] = None
|
|
print('\n++ META ITEMS || PROJECT INFORMATION ++')
|
|
meta_items['uri'] = input('\nWhat is your project\'s URI/URL?'
|
|
'\nURL: ').strip()
|
|
if not valid.url(meta_items['uri']):
|
|
print('Invalid; skipping...')
|
|
meta_items['uri'] = None
|
|
meta_items['ver'] = input(
|
|
'\nWhat version is this project? It follows the same rules as '
|
|
'the POSIX filename specification mentioned earlier (as we '
|
|
'use it to name certain files).\nVersion: ')
|
|
while not meta_items['desc']:
|
|
print('\nWhat is your project\'s description?'
|
|
'\nAccepts multiple lines, etc.'
|
|
'\nPress CTRL-d (on *nix/macOS) or CTRL-z (on Windows) '
|
|
'on an empty line when done.'
|
|
'\nIt will be echoed back for confirmation after it is '
|
|
'entered (with the option to re-enter if '
|
|
'desired/needed - this will NOT restart the entire Meta '
|
|
'section).')
|
|
meta_items['desc'] = prompt.multiline_input(
|
|
prompt = '\nDescription: ')
|
|
print('-----\n{0}\n-----'.format(meta_items['desc']))
|
|
_confirm = prompt.confirm_or_no(
|
|
prompt = 'Does this look okay?\n')
|
|
if not _confirm:
|
|
meta_items['desc'] = None
|
|
print('\n++ META ITEMS || DEVELOPER INFORMATION ++')
|
|
meta_items['dev']['author'] = (input(
|
|
'\nWhat is YOUR name?\nName: ')).strip()
|
|
meta_items['dev']['email'] = (input('\nWhat is your email address?'
|
|
'\nemail: ')).strip()
|
|
if not valid.email(meta_items['dev']['email']):
|
|
print('Invalid; skipping...')
|
|
meta_items['dev']['email'] = None
|
|
meta_items['dev']['website'] = (input('\nWhat is your website?\n'
|
|
'Website: ')).strip()
|
|
if not valid.url(meta_items['dev']['website']):
|
|
print('Invalid; skipping...')
|
|
meta_items['dev']['website'] = None
|
|
print('\n++ META ITEMS || OPERATIONAL CONFIGURATION ++')
|
|
meta_items['max_recurse'] = transform.sanitize_input(input(
|
|
'\nAs of the 4.x branch, BDisk configuration files support '
|
|
'cross-document substitution via XPath references, even '
|
|
'recursively. How many levels of recursion do you want this '
|
|
'profile to support? Note that the default limit for Python '
|
|
'is 1000 (and CAN be changed, but is not recommended) and '
|
|
'each level of recursion you add can POTENTIALLY add '
|
|
'additional CPU/RAM strain. HOWEVER, chances are if your '
|
|
'machine\'s good enough to run BDisk, it\'s good enough for '
|
|
'whatever you set. I recommend setting it to 5, because any '
|
|
'more than that and your configuration becomes cumbersome to '
|
|
'maintain.\nMax recursion: ').strip())
|
|
if not valid.integer(meta_items['max_recurse']):
|
|
print('Invalid; skipping...')
|
|
meta_items['dev']['website'] = None
|
|
meta = lxml.etree.SubElement(self.profile, 'meta')
|
|
for e in meta_items:
|
|
elem = lxml.etree.SubElement(meta, e)
|
|
# These have nested items.
|
|
if isinstance(meta_items[e], dict):
|
|
for s in meta_items[e]:
|
|
subelem = lxml.etree.SubElement(elem, s)
|
|
subelem.text = meta_items[e][s]
|
|
else:
|
|
elem.text = meta_items[e]
|
|
print()
|
|
return()
|
|
|
|
def get_accounts(self):
|
|
print('\n++ ACCOUNTS ++')
|
|
accounts = lxml.etree.SubElement(self.profile, 'accounts')
|
|
pass_attribs = ('hashed', 'hash_algo', 'salt')
|
|
rootpass = None
|
|
print('\n++ ACCOUNTS || ROOT ++')
|
|
if not rootpass:
|
|
prompt_attribs = pass_prompt('root')
|
|
rootpass = lxml.etree.Element('rootpass')
|
|
for i in pass_attribs:
|
|
rootpass.attrib[i] = transform.py2xml(prompt_attribs[i])
|
|
rootpass.text = prompt_attribs['password']
|
|
accounts.append(rootpass)
|
|
print('\n++ ACCOUNTS || USERS ++')
|
|
more_accounts = prompt.confirm_or_no(prompt = ('\nWould you like to '
|
|
'add a non-root/regular user?\n'),
|
|
usage = ('{0} for yes, {1} for no...\n'))
|
|
users = lxml.etree.SubElement(accounts, 'users')
|
|
while more_accounts:
|
|
user = None
|
|
_user_invalid = True
|
|
_user_text = {'username': None,
|
|
'password': None,
|
|
'comment': None}
|
|
while _user_invalid:
|
|
_username = (input('\nWhat should the username be?'
|
|
'\nUsername: ')).strip()
|
|
if not valid.username(_username):
|
|
print('\nThat username string is invalid. Consult the '
|
|
'manual and the man page for useradd(8). Let\'s '
|
|
'have another go.')
|
|
else:
|
|
_user_text['username'] = _username
|
|
_user_invalid = False
|
|
_sudo = prompt.confirm_or_no(prompt = ('\nGive {0} full sudo '
|
|
'access?\n').format(_username))
|
|
_pass_attr = pass_prompt(_username)
|
|
_user_text['password'] = _pass_attr['password']
|
|
_user_text['comment'] = transform.no_newlines(
|
|
(input('\nWhat do you want the GECOS comment to be? This is '
|
|
'USUALLY the full "real" name of the user (or a '
|
|
'description of the service, etc.). You can leave it '
|
|
'blank if you want.\nGECOS: ')).strip())
|
|
user = lxml.etree.Element('user')
|
|
user.attrib['sudo'] = transform.py2xml(_sudo)
|
|
_elems = {}
|
|
for elem in _user_text:
|
|
_elems[elem] = lxml.etree.SubElement(user, elem)
|
|
_elems[elem].text = _user_text[elem]
|
|
for i in pass_attribs:
|
|
_elems['password'].attrib[i] = transform.py2xml(_pass_attr[i])
|
|
users.append(user)
|
|
more_accounts = prompt.confirm_or_no(prompt = ('\nWould you like '
|
|
'to add another user?\n'),
|
|
usage = ('{0} for yes, {1} '
|
|
'for no...\n'))
|
|
return()
|
|
|
|
def get_sources(self):
|
|
print('\n++ SOURCES ++')
|
|
sources = lxml.etree.SubElement(self.profile, 'sources')
|
|
more_sources = True
|
|
_arches = []
|
|
_supported_arches = {'x86': ('(Also referred to by distros as "i386", '
|
|
'"i486", "i686", and "32-bit")'),
|
|
'x86_64': ('(Also referred to by distros as '
|
|
'"64-bit")')}
|
|
while more_sources:
|
|
if len(_arches) == len(_supported_arches):
|
|
# All supported arches have been added. We currently don't
|
|
# support mirror-balancing. TODO?
|
|
print('\nCannot add more sources; all supported architectures '
|
|
'have been used. Moving on.')
|
|
more_sources = False
|
|
break
|
|
if len(_arches) > 0:
|
|
print('\n(Currently added arches: {0})'.format(
|
|
', '.join(_arches)))
|
|
_print_arches = '\n\t'.join(
|
|
['{0}:\t{1}'.format(*i) for i in _supported_arches.items()])
|
|
source = lxml.etree.Element('source')
|
|
arch = (input((
|
|
'\nWhat hardware architecture is this source for?\n(Note: '
|
|
'BDisk currently only supports the listed architectures).\n'
|
|
'\n\t{0}\n\nArch: ').format(_print_arches))).strip().lower()
|
|
if arch not in _supported_arches.keys():
|
|
print('That is not a supported architecture. Trying again.')
|
|
continue
|
|
source.attrib['arch'] = arch
|
|
print('\n++ SOURCES || {0} ++'.format(arch.upper()))
|
|
print('\n++ SOURCES || {0} || TARBALL ++'.format(arch.upper()))
|
|
tarball = (input('\nWhat URL should be used for the tarball? '
|
|
'(Note that this is ONLY tested for syntax, we '
|
|
'don\'t confirm it\'s downloadable when running '
|
|
'through the configuration generator wizard - '
|
|
'so please make sure you enter the correct URL!)'
|
|
'\nTarball: ')).strip()
|
|
if not valid.url(tarball):
|
|
print('That isn\'t a valid URL. Please double-check and try '
|
|
'again.')
|
|
continue
|
|
tarball = transform.url_to_dict(tarball, no_None = True)
|
|
tarball_elem = lxml.etree.SubElement(source, 'tarball')
|
|
tarball_elem.attrib['flags'] = 'latest'
|
|
tarball_elem.text = tarball['full_url']
|
|
print('\n++ SOURCES || {0} || CHECKSUM ++'.format(arch.upper()))
|
|
chksum = lxml.etree.SubElement(source, 'checksum')
|
|
_chksum_chk = prompt.confirm_or_no(prompt = (
|
|
'\nWould you like to add a checksum for the tarball? (BDisk '
|
|
'can fetch a checksum file from a remote URL at build-time or '
|
|
'you can hardcode an explicit checksum in.)\n'),
|
|
usage = ('{0} for yes, {1} '
|
|
'for no...\n'))
|
|
if not _chksum_chk:
|
|
checksum = None
|
|
else:
|
|
checksum = (input(
|
|
'\nPlease enter the URL to the checksum file OR the '
|
|
'explicit checksum you wish to use.\nChecksum (remote URL '
|
|
'or checksum hash): ')).strip()
|
|
if valid.url(checksum):
|
|
checksum = transform.url_to_dict(checksum)
|
|
checksum_type = prompt.hash_select(prompt = (
|
|
'\nPlease select the digest type (by number) of the '
|
|
'checksums contained in this file.\n'
|
|
'Can be one of:\n\n\t{0}'
|
|
'\n\nChecksum type: '))
|
|
if checksum_type is False:
|
|
print('Select by NUMBER. Starting over.')
|
|
continue
|
|
elif checksum_type is None:
|
|
print('Invalid selection. Starting over.')
|
|
continue
|
|
chksum.attrib['hash_algo'] = checksum_type
|
|
chksum.attrib['explicit'] = "no"
|
|
chksum.text = checksum['full_url']
|
|
else:
|
|
# Maybe it's a digest string.
|
|
checksum_type = detect.any_hash(checksum)
|
|
if not checksum_type:
|
|
print('\nCould not detect which hash type this digest '
|
|
'is.')
|
|
checksum_type = prompt.hash_select(
|
|
prompt = ('\nPlease select from the following '
|
|
'list (by numer):\n\n\t{0}'
|
|
'\n\nChecksum type: '))
|
|
if checksum_type is False:
|
|
print('Select by NUMBER. Starting over.')
|
|
continue
|
|
elif checksum_type is None:
|
|
print('Invalid selection. Starting over.')
|
|
continue
|
|
elif len(checksum_type) > 1:
|
|
checksum_type = prompt.hash_select(
|
|
prompt = (
|
|
'\nWe found several algorithms that can match '
|
|
'your provided digest.\nPlease select the '
|
|
'appropriate digest method from the list below '
|
|
'(by number):\n\n\t{0}\n\nChecksum type: '))
|
|
if checksum_type is False:
|
|
print('Select by NUMBER. Starting over.')
|
|
continue
|
|
elif checksum_type is None:
|
|
print('Invalid selection. Starting over.')
|
|
continue
|
|
else:
|
|
checksum_type == checksum_type[0]
|
|
chksum.attrib['explicit'] = "yes"
|
|
chksum.text = checksum
|
|
chksum.attrib['hash_algo'] = checksum_type
|
|
print('\n++ SOURCES || {0} || GPG ++'.format(arch.upper()))
|
|
sig = lxml.etree.SubElement(source, 'sig')
|
|
_gpg_chk = prompt.confirm_or_no(prompt = (
|
|
'\nWould you like to add a GPG(/GnuPG/PGP) signature for the '
|
|
'tarball?\n'))
|
|
if _gpg_chk:
|
|
gpgsig = (input(
|
|
'\nPlease enter the remote URL for the GPG signature '
|
|
'file.\nGPG Signature File URL: ')
|
|
).strip()
|
|
if not valid.url(gpgsig):
|
|
print('Invalid URL. Starting over.')
|
|
continue
|
|
else:
|
|
gpgsig = transform.url_to_dict(gpgsig)
|
|
sig.text = gpgsig['full_url']
|
|
sigkeys = prompt.confirm_or_no(prompt = (
|
|
'\nDo you know the key ID of the authorized/valid '
|
|
'signer? (If not, we will fetch the GPG signature file '
|
|
'now and try to parse it for key IDs.)\n'),
|
|
usage = ('{0} for yes, {1} '
|
|
'for no...\n'))
|
|
if sigkeys:
|
|
sigkeys = (input('\nWhat is the key ID? You can use the '
|
|
'fingerprint, full 40-character key ID '
|
|
'(preferred), 16-character "long" ID, or '
|
|
'the 8-character "short" ID '
|
|
'(HIGHLY unrecommended!).\nKey ID: ')
|
|
).strip().upper()
|
|
if not valid.gpgkeyID(sigkeys):
|
|
print('That is not a valid GPG key ID. Restarting')
|
|
continue
|
|
sig.attrib['keys'] = sigkeys
|
|
else:
|
|
sigkeys = detect.gpgkeyID_from_url(gpgsig)
|
|
if not isinstance(sigkeys, list):
|
|
print('Could not properly parse any keys in the '
|
|
'signature file. Restarting.')
|
|
continue
|
|
elif len(sigkeys) == 0:
|
|
print('We didn\'t find any key IDs embedded in the '
|
|
'given signature file. Restarting.')
|
|
continue
|
|
elif len(sigkeys) == 1:
|
|
_s = 'Does this key'
|
|
else:
|
|
_s = 'Do these keys'
|
|
_key_info = [detect.gpgkey_info(k) for k in sigkeys]
|
|
print('\nWe found the following key ID information:\n\n')
|
|
for _key in _key_info:
|
|
print('\t{0}\n'.format(_key['Full key']))
|
|
for _uid in _key['User IDs']:
|
|
# COULD flatten this to just one level.
|
|
print('\t\t{0}'.format(_uid['Name']))
|
|
for k in _uid:
|
|
if k != 'Name':
|
|
print('\t\t\t{0}:\t{1}'.format(k, _uid[k]))
|
|
_key_chk = prompt.confirm_or_no(prompt = (
|
|
'\n{0} look correct?\n').format(_s))
|
|
if not _key_chk:
|
|
print('Something must have gotten futzed, then.'
|
|
'Restarting!')
|
|
continue
|
|
sig.attrib['keys'] = ','.join(sigkeys)
|
|
elems = {}
|
|
for s in ('mirror', 'webroot'):
|
|
elems[s] = lxml.etree.SubElement(source, s)
|
|
elems['mirror'].text = '{scheme}://{host}'.format(**tarball)
|
|
if tarball['port'] != '':
|
|
elems['mirror'].text += ':{0}'.format(tarball['port'])
|
|
elems['webroot'].text = '{path}'.format(**tarball)
|
|
sources.append(source)
|
|
_arches.append(arch)
|
|
more_sources = prompt.confirm_or_no(prompt = ('\nWould you like '
|
|
'to add another '
|
|
'source?\n'),
|
|
usage = ('{0} for yes, {1} '
|
|
'for no...\n'))
|
|
return()
|
|
|
|
def get_build(self):
|
|
print('\n++ BUILD ++')
|
|
build = lxml.etree.SubElement(self.profile, 'build')
|
|
_chk_optimizations = prompt.confirm_or_no(prompt = (
|
|
'\nWould you like to enable experimental optimizations?\n'),
|
|
usage = (
|
|
'{0} for yes, {1} for no...\n'))
|
|
if _chk_optimizations:
|
|
build.attrib['its_full_of_stars'] = 'yes'
|
|
print('\n++ BUILD || PATHS ++')
|
|
# Thankfully, we can simplify a lot of this.
|
|
_dir_strings = {'cache': ('the caching directory (used for temporary '
|
|
'files, temporary downloads, etc.)'),
|
|
'chroot': ('the chroot directory (where we store '
|
|
'the root filesystems that are converted '
|
|
'into the live environment'),
|
|
'overlay': ('the overlay directory (allowing for '
|
|
'injecting files into the live '
|
|
'environment\'s filesystem)'),
|
|
'templates': ('the template directory (for templating '
|
|
'configuration files in the live '
|
|
'environment)'),
|
|
'mount': ('the mount directory (where chroots are '
|
|
'mounted to perform preparation tasks)'),
|
|
'distros': ('the distro plugin directory (where '
|
|
'plugins supporting other guest Linux '
|
|
'distributions are put)'),
|
|
'dest': ('the destination directory (where finished '
|
|
'products like ISO image files go)'),
|
|
'iso': ('the iso directory (the overlay directory for '
|
|
'the "outer" layer of media)'),
|
|
'http': ('the HTTP directory (where a webroot is '
|
|
'created that can be used to serve iPXE)'),
|
|
'tftp': ('the TFTP directory (where a TFTP/'
|
|
'traditional PXE root is created)'),
|
|
'ssl': ('the SSL/TLS PKI directory (where we store '
|
|
'the PKI structure we use/re-use - MAKE SURE '
|
|
'it is in a path that is well-protected!)')}
|
|
has_paths = False
|
|
# Get the paths
|
|
while not has_paths:
|
|
paths = lxml.etree.Element('paths')
|
|
_paths_elems = {}
|
|
for _dir in _dir_strings:
|
|
_paths_elems[_dir] = lxml.etree.SubElement(paths, _dir)
|
|
path = prompt.path(_dir_strings[_dir])
|
|
_paths_elems[_dir].text = path
|
|
build.append(paths)
|
|
has_paths = True
|
|
print('\n++ BUILD || ENVIRONMENT DISTRO ++')
|
|
has_distro = False
|
|
while not has_distro:
|
|
try:
|
|
distro_path = self.profile.xpath('//paths/distros/text()')[0]
|
|
except IndexError:
|
|
distro_path = 'your "distros" path'
|
|
distro = (input('\nWhich distro plugin/distro base are you using? '
|
|
'See the manual for more information. A matching '
|
|
'plugin MUST exist in {0} for a build to '
|
|
'complete successfully! The default (Arch Linux, '
|
|
'"archlinux") will be used if left blank.'
|
|
'\nDistro base: ').format(
|
|
distro_path)).strip()
|
|
if distro == '':
|
|
distro = 'archlinux'
|
|
if not valid.plugin_name(distro):
|
|
print('That is not a valid name. See the manual for examples '
|
|
'and shipped plugins. Retrying.')
|
|
continue
|
|
distro_elem = lxml.etree.SubElement(build, 'distro')
|
|
distro_elem.text = distro
|
|
return()
|
|
|
|
def main():
|
|
cg = ConfGenerator()
|
|
cg.main()
|
|
print()
|
|
print(lxml.etree.tostring(cg.cfg,
|
|
pretty_print = True,
|
|
encoding = 'UTF-8',
|
|
xml_declaration = True
|
|
).decode('utf-8'))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|