2017-05-07 23:50:11 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2017-05-15 02:47:04 -04:00
|
|
|
xmldebug = True
|
|
|
|
|
|
|
|
if not xmldebug:
|
|
|
|
try:
|
|
|
|
from lxml import etree
|
|
|
|
lxml_avail = True
|
|
|
|
except ImportError:
|
|
|
|
import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html
|
|
|
|
lxml_avail = False
|
|
|
|
else:
|
|
|
|
# debugging
|
|
|
|
import xml.etree.ElementTree as etree
|
2017-05-07 23:50:11 -04:00
|
|
|
lxml_avail = False
|
2017-05-15 02:47:04 -04:00
|
|
|
# end debugging
|
2017-05-07 23:50:11 -04:00
|
|
|
import argparse
|
2017-05-12 23:00:17 -04:00
|
|
|
import crypt
|
2017-05-07 23:50:11 -04:00
|
|
|
import datetime
|
|
|
|
import errno
|
|
|
|
import ipaddress
|
2017-05-14 01:21:15 -04:00
|
|
|
import json
|
2017-05-12 23:00:17 -04:00
|
|
|
import getpass
|
2017-05-07 23:50:11 -04:00
|
|
|
import os
|
|
|
|
import re
|
2017-05-12 23:00:17 -04:00
|
|
|
import readline
|
2017-05-07 23:50:11 -04:00
|
|
|
import sys
|
|
|
|
import urllib.request as urlrequest
|
|
|
|
import urllib.parse as urlparse
|
|
|
|
import urllib.response as urlresponse
|
|
|
|
from ftplib import FTP_TLS
|
|
|
|
|
|
|
|
xsd = 'https://aif.square-r00t.net/aif.xsd'
|
|
|
|
|
2017-05-12 23:00:17 -04:00
|
|
|
# Ugh. You kids and your colors and bolds and crap.
|
|
|
|
class color(object):
|
|
|
|
PURPLE = '\033[95m'
|
|
|
|
CYAN = '\033[96m'
|
|
|
|
DARKCYAN = '\033[36m'
|
|
|
|
BLUE = '\033[94m'
|
|
|
|
GREEN = '\033[92m'
|
|
|
|
YELLOW = '\033[93m'
|
|
|
|
RED = '\033[91m'
|
|
|
|
BOLD = '\033[1m'
|
|
|
|
UNDERLINE = '\033[4m'
|
|
|
|
END = '\033[0m'
|
|
|
|
|
2017-05-07 23:50:11 -04:00
|
|
|
class aifgen(object):
|
|
|
|
def __init__(self, args):
|
|
|
|
self.args = args
|
|
|
|
|
2017-05-13 07:27:58 -04:00
|
|
|
def webFetch(self, uri, auth = False): # TODO: add commandline args support for extra auth?
|
|
|
|
# Sanitize the user specification and find which protocol to use
|
|
|
|
prefix = uri.split(':')[0].lower()
|
|
|
|
if uri.startswith('/'):
|
2017-05-13 08:11:59 -04:00
|
|
|
uri = 'file://{0}'.format(uri)
|
2017-05-13 07:27:58 -04:00
|
|
|
prefix = 'file'
|
|
|
|
# Use the urllib module
|
|
|
|
if prefix in ('http', 'https', 'file', 'ftp'):
|
|
|
|
if auth:
|
|
|
|
if 'user' in auth.keys() and 'password' in auth.keys():
|
|
|
|
# Set up Basic or Digest auth.
|
|
|
|
passman = urlrequest.HTTPPasswordMgrWithDefaultRealm()
|
|
|
|
if not 'realm' in auth.keys():
|
|
|
|
passman.add_password(None, uri, auth['user'], auth['password'])
|
|
|
|
else:
|
|
|
|
passman.add_password(auth['realm'], uri, auth['user'], auth['password'])
|
|
|
|
if auth['type'] == 'digest':
|
|
|
|
httpauth = urlrequest.HTTPDigestAuthHandler(passman)
|
|
|
|
else:
|
|
|
|
httpauth = urlrequest.HTTPBasicAuthHandler(passman)
|
|
|
|
httpopener = urlrequest.build_opener(httpauth)
|
|
|
|
urlrequest.install_opener(httpopener)
|
|
|
|
with urlrequest.urlopen(uri) as f:
|
|
|
|
data = f.read()
|
|
|
|
elif prefix == 'ftps':
|
|
|
|
if auth:
|
|
|
|
if 'user' in auth.keys():
|
|
|
|
username = auth['user']
|
|
|
|
else:
|
|
|
|
username = 'anonymous'
|
|
|
|
if 'password' in auth.keys():
|
|
|
|
password = auth['password']
|
|
|
|
else:
|
|
|
|
password = 'anonymous'
|
|
|
|
filepath = '/'.join(uri.split('/')[3:])
|
|
|
|
server = uri.split('/')[2]
|
|
|
|
content = StringIO()
|
|
|
|
ftps = FTP_TLS(server)
|
|
|
|
ftps.login(username, password)
|
|
|
|
ftps.prot_p()
|
|
|
|
ftps.retrlines("RETR " + filepath, content.write)
|
|
|
|
data = content.getvalue()
|
|
|
|
else:
|
|
|
|
exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix))
|
|
|
|
return(data)
|
|
|
|
|
2017-05-07 23:50:11 -04:00
|
|
|
def getXSD(self):
|
2017-05-13 08:11:59 -04:00
|
|
|
xsdobj = etree.fromstring(self.webFetch(xsd))
|
2017-05-13 07:27:58 -04:00
|
|
|
return(xsdobj)
|
2017-05-07 23:50:11 -04:00
|
|
|
|
|
|
|
def getXML(self):
|
2017-05-13 08:11:59 -04:00
|
|
|
xmlobj = etree.fromstring(self.webFetch(self.args['cfgfile']))
|
2017-05-13 07:27:58 -04:00
|
|
|
return(xmlobj)
|
2017-05-07 23:50:11 -04:00
|
|
|
|
|
|
|
def getOpts(self):
|
2017-05-13 08:11:59 -04:00
|
|
|
# Before anything else... a disclaimer.
|
|
|
|
print('\nWARNING: This tool is not guaranteed to generate a working configuration file,\n' +
|
|
|
|
'\t but for most basic cases it should work. I strongly encourage you to generate your own\n' +
|
|
|
|
'\t configuration file instead by reading the documentation: https://aif.square-r00t.net/#writing_an_xml_configuration_file\n\n')
|
2017-05-11 18:46:36 -04:00
|
|
|
# This whole thing is ugly. Really, really ugly. Patches 100% welcome.
|
2017-05-07 23:50:11 -04:00
|
|
|
def chkPrompt(prompt, urls):
|
|
|
|
txtin = None
|
|
|
|
txtin = input(prompt)
|
2017-05-11 18:46:36 -04:00
|
|
|
if txtin == 'wikihelp':
|
2017-05-07 23:50:11 -04:00
|
|
|
print('\n Articles/pages that you may find helpful for this option are:')
|
|
|
|
for h in urls:
|
|
|
|
print(' * {0}'.format(h))
|
|
|
|
print()
|
|
|
|
txtin = input(prompt)
|
|
|
|
else:
|
|
|
|
return(txtin)
|
2017-05-09 08:17:07 -04:00
|
|
|
def sizeChk(startsize):
|
|
|
|
try:
|
2017-05-11 18:46:36 -04:00
|
|
|
startn = int(re.sub('[%\-+KMGTP]', '', startsize))
|
2017-05-09 08:17:07 -04:00
|
|
|
modifier = re.sub('^(\+|-)?.*$', '\g<1>', startsize)
|
2017-05-11 18:46:36 -04:00
|
|
|
if re.match('^(\+|-)?[0-9]+%$', startsize):
|
2017-05-09 08:17:07 -04:00
|
|
|
sizetype = 'percentage'
|
|
|
|
elif re.match('^(\+|-)?[0-9]+[KMGTP]$', n):
|
|
|
|
sizetype = 'fixed'
|
|
|
|
else:
|
|
|
|
exit(' !! ERROR: The input you provided does not match a valid pattern.')
|
|
|
|
if sizetype == 'percentage':
|
2017-05-11 18:46:36 -04:00
|
|
|
if not (0 <= startn <= 100):
|
2017-05-09 08:17:07 -04:00
|
|
|
exit(' !! ERROR: You must provide a percentage or a size.')
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: You did not provide a valid size specifier!')
|
|
|
|
return(startsize)
|
|
|
|
def ifacePrompt(nethelp):
|
|
|
|
ifaces = {}
|
|
|
|
moreIfaces = True
|
2017-05-15 02:47:04 -04:00
|
|
|
print('\tNOTE: You must specify the "persistent device naming" name of the device when configuring.\n' +
|
|
|
|
'\tYou can instead specify \'auto\' for automatic configuration of the first found interface\n' +
|
|
|
|
'\twith an active link. (You can only specify one auto device per system, and all other\n'
|
|
|
|
'\tinterface entries will be ignored by AIF-NG.)\n')
|
2017-05-09 08:17:07 -04:00
|
|
|
while moreIfaces:
|
2017-05-14 12:07:39 -04:00
|
|
|
ifacein = chkPrompt('* Interface device: ', nethelp)
|
|
|
|
addrin = chkPrompt(('** Address for {0} in CIDR format (can be an IPv4 or IPv6 address; ' +
|
2017-05-11 23:27:22 -04:00
|
|
|
'use \'auto\' for DHCP/DHCPv6): ').format(ifacein), nethelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
if addrin == 'auto':
|
2017-05-11 18:46:36 -04:00
|
|
|
addrtype = 'auto'
|
2017-05-14 12:07:39 -04:00
|
|
|
ipver = (chkPrompt('** Would you like \'ipv4\', \'ipv6\', or \'both\' to be auto-configured? ', nethelp)).lower()
|
2017-05-11 18:46:36 -04:00
|
|
|
if ipver not in ('ipv4', 'ipv6', 'both'):
|
|
|
|
exit(' !! ERROR: Must be one of ipv4, ipv6, or both.')
|
2017-05-09 08:17:07 -04:00
|
|
|
else:
|
|
|
|
addrtype = 'static'
|
|
|
|
try:
|
2017-05-11 23:27:22 -04:00
|
|
|
ipaddress.ip_network(addrin, strict = False)
|
|
|
|
try:
|
|
|
|
ipaddress.IPv4Address(addrin.split('/')[0])
|
|
|
|
ipver = 'ipv4'
|
|
|
|
except ipaddress.AddressValueError:
|
|
|
|
ipver = 'ipv6'
|
2017-05-09 08:17:07 -04:00
|
|
|
except ValueError:
|
|
|
|
exit(' !! ERROR: You did not enter a valid IPv4/IPv6 address.')
|
|
|
|
if addrtype == 'static':
|
2017-05-14 12:07:39 -04:00
|
|
|
gwin = chkPrompt('*** What is the gateway address for {0}? '.format(addrin), nethelp)
|
2017-05-11 18:46:36 -04:00
|
|
|
try:
|
|
|
|
ipaddress.ip_address(gwin)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: You did not enter a valid IPv4/IPv6 address.')
|
2017-05-11 23:27:22 -04:00
|
|
|
ifaces[ifacein] = {'address': addrin, 'proto': ipver, 'gw': gwin, 'resolvers': []}
|
2017-05-14 12:07:39 -04:00
|
|
|
resolversin = chkPrompt('*** What DNS resolvers should we use? Can accept a comma-separated list: ', nethelp)
|
2017-05-11 18:46:36 -04:00
|
|
|
for rslv in resolversin.split(','):
|
|
|
|
rslvaddr = rslv.strip()
|
|
|
|
ifaces[ifacein]['resolvers'].append(rslvaddr)
|
|
|
|
try:
|
|
|
|
ipaddress.ip_address(rslvaddr)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: {0} is not a valid resolver address.'.format(rslvaddr))
|
|
|
|
else:
|
|
|
|
ifaces[ifacein] = {'address': 'auto', 'proto': ipver, 'gw': False, 'resolvers': False}
|
2017-05-14 12:07:39 -04:00
|
|
|
moreIfacesin = input('* Would you like to add more interfaces? (y/{0}n{1}) '.format(color.BOLD, color.END))
|
2017-05-11 18:46:36 -04:00
|
|
|
if not re.match('^y(es)?$', moreIfacesin.lower()):
|
|
|
|
moreIfaces = False
|
2017-05-09 08:17:07 -04:00
|
|
|
return(ifaces)
|
2017-05-12 23:00:17 -04:00
|
|
|
def genPassHash(user):
|
2017-05-14 01:21:15 -04:00
|
|
|
# https://bugs.python.org/issue30360 - keep this disabled until we're ready for primetime.
|
2017-05-14 12:07:39 -04:00
|
|
|
passin = getpass.getpass('* Please enter the password you want to use for {0} (will not echo back): '.format(user))
|
|
|
|
#passin = input('* Please enter the password you want to use for {0}: '.format(user))
|
2017-05-12 23:00:17 -04:00
|
|
|
if passin not in ('', '!'):
|
|
|
|
salt = crypt.mksalt(crypt.METHOD_SHA512)
|
|
|
|
salthash = crypt.crypt(passin, salt)
|
|
|
|
else:
|
|
|
|
salthash = passin
|
|
|
|
return(salthash)
|
|
|
|
def userPrompt(syshelp):
|
|
|
|
users = {}
|
2017-05-14 01:21:15 -04:00
|
|
|
moreusers = True
|
|
|
|
while moreusers:
|
2017-05-14 12:07:39 -04:00
|
|
|
user = chkPrompt('* What username would you like to add? ', syshelp)
|
2017-05-12 23:00:17 -04:00
|
|
|
if len(user) > 32:
|
|
|
|
exit(' !! ERROR: Usernames must be less than 32 characters.')
|
|
|
|
if not re.match('^[a-z_][a-z0-9_-]*[$]?$', user):
|
|
|
|
exit(' !! ERROR: Your username does not match a valid pattern. See the man page for useradd (\'CAVEATS\').')
|
|
|
|
users[user] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
sudoin = chkPrompt('** Should {0} have (full!) sudo access? (y/{1}n{2}) '.format(user, color.BOLD, color.END), syshelp)
|
2017-05-12 23:00:17 -04:00
|
|
|
if re.match('^y(es)?$', sudoin.lower()):
|
|
|
|
users[user]['sudo'] = True
|
|
|
|
else:
|
|
|
|
users[user]['sudo'] = False
|
|
|
|
users[user]['password'] = genPassHash(user)
|
2017-05-14 12:07:39 -04:00
|
|
|
users[user]['comment'] = chkPrompt(('** What comment should {0} have? ' +
|
2017-05-12 23:00:17 -04:00
|
|
|
'(Typically this is the user\'s full name) ').format(user), syshelp)
|
2017-05-14 12:07:39 -04:00
|
|
|
uidin = chkPrompt(('** What UID should {0} have? Leave this blank if you don\'t care ' +
|
2017-05-12 23:00:17 -04:00
|
|
|
'(should be fine for most cases): ').format(user), syshelp)
|
|
|
|
if uidin != '':
|
|
|
|
try:
|
|
|
|
users[user]['uid'] = int(uidin)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: The UID must be an integer.')
|
|
|
|
else:
|
|
|
|
users[user]['uid'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
grpin = chkPrompt(('** What group name would you like to use for {0}\'s primary group? ' +
|
2017-05-12 23:00:17 -04:00
|
|
|
'(You\'ll be able to add additional groups in a moment.)\n' +
|
|
|
|
'\tThe default, if left blank, is to simply create a group named {0} ' +
|
|
|
|
'(which is what you probably want): ').format(user), syshelp)
|
2017-05-14 01:21:15 -04:00
|
|
|
if grpin != '':
|
|
|
|
if len(grpin) > 32:
|
|
|
|
exit(' !! ERROR: Group names must be less than 32 characters.')
|
|
|
|
if not re.match('^[a-z_][a-z0-9_-]*[$]?$', grpin):
|
|
|
|
exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').')
|
|
|
|
users[user]['group'] = grpin
|
|
|
|
else:
|
|
|
|
users[user]['group'] = False
|
|
|
|
if grpin != '':
|
2017-05-14 12:07:39 -04:00
|
|
|
gidin = chkPrompt(('** What GID should {0} have? Leave this blank if you don\'t care ' +
|
2017-05-14 01:21:15 -04:00
|
|
|
'(should be fine for most cases): ').format(grpin), syshelp)
|
|
|
|
if gidin != '':
|
|
|
|
try:
|
|
|
|
users[user]['gid'] = int(gidin)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: The GID must be an integer.')
|
|
|
|
else:
|
|
|
|
users[user]['gid'] = False
|
2017-05-12 23:00:17 -04:00
|
|
|
else:
|
|
|
|
users[user]['gid'] = False
|
|
|
|
syshelp.append('https://aif.square-r00t.net/#code_home_code')
|
2017-05-14 12:07:39 -04:00
|
|
|
homein = chkPrompt(('** What directory should {0} use for its home? Leave blank if you don\'t care ' +
|
2017-05-12 23:00:17 -04:00
|
|
|
'(should be fine for most cases): ').format(user), syshelp)
|
|
|
|
if homein != '':
|
|
|
|
if not re.match('^/([^/\x00\s]+(/)?)+)$', homein):
|
|
|
|
exit('!! ERROR: Path {0} does not seem to be valid.'.format(homein))
|
|
|
|
users[user]['home'] = homein
|
2017-05-14 12:07:39 -04:00
|
|
|
homecrt = chkPrompt('*** Do we need to create {0}? (y/{1}n{2}) '.format(homein, color.BOLD, color.END), syshelp)
|
2017-05-14 01:21:15 -04:00
|
|
|
if re.match('^y(es)?$', homecrt):
|
|
|
|
users[user]['homecreate'] = True
|
|
|
|
else:
|
|
|
|
users[user]['homecreate'] = False
|
2017-05-12 23:00:17 -04:00
|
|
|
else:
|
|
|
|
users[user]['home'] = False
|
|
|
|
del(syshelp[-1])
|
|
|
|
xgrouphelp = 'https://aif.square-r00t.net/#code_xgroup_code'
|
|
|
|
if xgrouphelp not in syshelp:
|
|
|
|
syshelp.append(xgrouphelp)
|
2017-05-14 12:07:39 -04:00
|
|
|
xgroupin = chkPrompt('** Would you like to add extra groups for {0}? (y/{1}n{2}) '.format(user, color.BOLD, color.END), syshelp)
|
2017-05-12 23:00:17 -04:00
|
|
|
if re.match('^y(es)?$', xgroupin.lower()):
|
|
|
|
morexgroups = True
|
|
|
|
users[user]['xgroups'] = {}
|
|
|
|
else:
|
|
|
|
morexgroups = False
|
|
|
|
users[user]['xgroups'] = False
|
|
|
|
while morexgroups:
|
2017-05-14 12:07:39 -04:00
|
|
|
xgrp = chkPrompt('*** What is the name of the group you would like to add to {0}? '.format(user), syshelp)
|
2017-05-12 23:00:17 -04:00
|
|
|
if len(xgrp) > 32:
|
|
|
|
exit(' !! ERROR: Group names must be less than 32 characters.')
|
|
|
|
if not re.match('^[a-z_][a-z0-9_-]*[$]?$', xgrp):
|
|
|
|
exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').')
|
|
|
|
users[user]['xgroups'][xgrp] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
xgrpcrt = chkPrompt('*** Does the group \'{0}\' need to be created? (y/{1}n{2}) '.format(xgrp, color.BOLD, color.END), syshelp)
|
2017-05-12 23:00:17 -04:00
|
|
|
if re.match('^y(es)?$', xgrpcrt.lower()):
|
|
|
|
users[user]['xgroups'][xgrp]['create'] = True
|
2017-05-14 12:07:39 -04:00
|
|
|
xgrpgid = chkPrompt(('*** What GID should {0} be? If the group will already exist on the new system or ' +
|
2017-05-14 01:21:15 -04:00
|
|
|
'don\'t care,\nleave this blank (should be fine for most cases): ').format(xgrp), syshelp)
|
|
|
|
if xgrpgid != '':
|
|
|
|
try:
|
|
|
|
users[user]['xgroups'][xgrp]['gid'] = int(xgrpgid)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: The GID must be an integer.')
|
|
|
|
else:
|
|
|
|
users[user]['xgroups'][xgrp]['gid'] = False
|
2017-05-12 23:00:17 -04:00
|
|
|
else:
|
|
|
|
users[user]['xgroups'][xgrp]['create'] = False
|
|
|
|
users[user]['xgroups'][xgrp]['gid'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
morexgrpsin = input('** Would you like to add additional extra groups for {0}? (y/{1}n{2}) '.format(user,
|
|
|
|
color.BOLD,
|
|
|
|
color.END))
|
2017-05-14 01:21:15 -04:00
|
|
|
if not re.match('^y(es)?$', morexgrpsin.lower()):
|
2017-05-12 23:00:17 -04:00
|
|
|
morexgroups = False
|
2017-05-14 01:21:15 -04:00
|
|
|
moreusersin = chkPrompt('* Would you like to add additional users? (y/{0}n{1}) '.format(color.BOLD, color.END), syshelp)
|
|
|
|
if not re.match('^y(es)?$', moreusersin.lower()):
|
|
|
|
moreusers = False
|
2017-05-12 23:00:17 -04:00
|
|
|
return(users)
|
2017-05-13 03:59:31 -04:00
|
|
|
def svcsPrompt(svchelp):
|
|
|
|
svcs = {}
|
|
|
|
moresvcs = True
|
|
|
|
while moresvcs:
|
|
|
|
svc = chkPrompt('** What is the name of the service? If it\'s a .service unit, you can leave the .service off: ', svchelp)
|
|
|
|
if not re.match('^[A-Za-z0-9\-@]+(\.(service|timer|target|socket|mount|slice))?$', svc):
|
|
|
|
exit(' !! ERROR: You seem to have specified an invalid service name.')
|
|
|
|
svcstatusin = chkPrompt('** Should {0} be enabled? ({1}y{2}/n) '.format(svc, color.BOLD, color.END), svchelp)
|
|
|
|
if re.match('^no?$', svcstatusin.lower()):
|
|
|
|
svcs[svc] = False
|
|
|
|
else:
|
2017-05-14 01:21:15 -04:00
|
|
|
svcs[svc] = True
|
2017-05-13 03:59:31 -04:00
|
|
|
moreservices = input('* Would you like to manage another service? (y/{0}n{1}) '.format(color.BOLD, color.END))
|
|
|
|
if not re.match('^y(es)?$', moreservices.lower()):
|
|
|
|
moresvcs = False
|
|
|
|
return(svcs)
|
|
|
|
def repoPrompt(repohelp):
|
|
|
|
# The default pacman.conf's repo setup
|
|
|
|
repos = {'core': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': True},
|
|
|
|
'extra': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': True},
|
|
|
|
'community-testing': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': False},
|
|
|
|
'community': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': True},
|
|
|
|
'multilib-testing': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': False},
|
|
|
|
'multilib': {'mirror': 'file:///etc/pacman.d/mirrorlist',
|
|
|
|
'siglevel': 'default',
|
|
|
|
'enabled': False}}
|
2017-05-14 12:07:39 -04:00
|
|
|
chkdefs = chkPrompt(('* Would you like to review the default repository configuration ' +
|
2017-05-13 03:59:31 -04:00
|
|
|
'(and possibly edit it)? ({0}y{1}/n) ').format(color.BOLD, color.END), repohelp)
|
2017-05-15 02:47:04 -04:00
|
|
|
fmtstr = '\t{0} {1:<20} {2:^10} {3:^10} {4}' # ('#', 'REPO', 'ENABLED', 'SIGLEVEL', 'URI')
|
2017-05-13 03:59:31 -04:00
|
|
|
if not re.match('^no?$', chkdefs.lower()):
|
|
|
|
print('{0}{1}{2}'.format(color.BOLD, fmtstr.format('#', 'REPO', 'ENABLED', 'SIGLEVEL', 'URI'), color.END))
|
|
|
|
rcnt = 1
|
|
|
|
for r in repos.keys():
|
|
|
|
print(fmtstr.format(rcnt, r, str(repos[r]['enabled']), repos[r]['siglevel'], repos[r]['mirror']))
|
|
|
|
rcnt += 1
|
2017-05-14 12:07:39 -04:00
|
|
|
editdefs = chkPrompt('** Would you like to edit any of this? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if re.match('^y(es)?$', editdefs.lower()):
|
|
|
|
repokeys = list(repos.keys())
|
|
|
|
moreedits = True
|
|
|
|
while moreedits:
|
2017-05-14 12:07:39 -04:00
|
|
|
rnum = input('** What repository # would you like to edit? ')
|
2017-05-13 03:59:31 -04:00
|
|
|
try:
|
|
|
|
rnum = int(rnum)
|
|
|
|
rname = repokeys[rnum - 1]
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: You did not specify a valid repository #.')
|
2017-05-14 12:07:39 -04:00
|
|
|
enableedit = chkPrompt('*** Should {0} be enabled? (y/n/{1}nochange{2}) '.format(rname, color.BOLD, color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if re.match('^y(es)?$', enableedit.lower()):
|
|
|
|
repos[rname]['enabled'] = True
|
|
|
|
elif re.match('^no?$', enableedit.lower()):
|
|
|
|
repos[rname]['enabled'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
siglvledit = chkPrompt('*** What siglevel should {0} use? Leave blank for no change: '.format(rname), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if siglvledit != '':
|
|
|
|
grp1 = re.compile('^((Package|Database)?(Never|Optional|Required)|default)$')
|
|
|
|
grp2 = re.compile('^(Package|Database)?Trust(edOnly|All)$')
|
|
|
|
siglst = siglvledit.split()
|
|
|
|
if len(siglist) > 2:
|
|
|
|
exit(' !! ERROR: That is not a valid SigLevel string. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').')
|
|
|
|
if not grp1.match(siglist[0]):
|
|
|
|
exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[0]))
|
|
|
|
if len(siglist) == 1:
|
|
|
|
if not grp2.match(siglist[1]):
|
|
|
|
exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[1]))
|
|
|
|
repos[rname]['siglevel'] = siglvledit
|
2017-05-14 12:07:39 -04:00
|
|
|
uriedit = chkPrompt('*** What should the URI be?\n' +
|
|
|
|
'\tUse \'file:///absolute/path/to/file\' to use an Include directive. Leave blank for no change: ', repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if uriedit != '':
|
|
|
|
repos[rname]['mirror'] = uriedit
|
|
|
|
moreeditsin = chkPrompt(('** Would you like to edit another ' +
|
|
|
|
'repository? (y/{0}n{1}) ').format(color.BOLD, color.END), repohelp)
|
|
|
|
if not re.match('^y(es)?$', moreeditsin.lower()):
|
|
|
|
moreedits = False
|
|
|
|
addreposin = chkPrompt('* Would you like to add any additional repositories? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp)
|
|
|
|
if re.match('^y(es)?$', addreposin.lower()):
|
|
|
|
addrepos = True
|
|
|
|
while addrepos:
|
|
|
|
reponamein = chkPrompt('** What should this repository be named? (Must match the repository name on the mirror): ', repohelp)
|
|
|
|
reponame = re.sub('(^\[|]$)', '', reponamein)
|
|
|
|
if not re.match('^[a-z0-9]', reponame.lower()):
|
|
|
|
exit(' !! ERROR: That is not a valid repository name.')
|
|
|
|
repos[reponame] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
enablein = chkPrompt('** Should {0}{1}{2} be enabled? ({0}y{2}/n) '.format(color.BOLD, reponame, color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if not re.match('^no?$', enablein.lower()):
|
|
|
|
repos[reponame]['enabled'] = True
|
|
|
|
else:
|
|
|
|
repos[reponame]['enabled'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
siglvlin = chkPrompt(('** What SigLevel string should we use for {0}{1}{2}? ' +
|
|
|
|
'Leave blank for default: ').format(color.BOLD, reponame, color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if siglvlin != '':
|
|
|
|
grp1 = re.compile('^((Package|Database)?(Never|Optional|Required)|default)$')
|
|
|
|
grp2 = re.compile('^(Package|Database)?Trust(edOnly|All)$')
|
|
|
|
siglst = siglvlin.split()
|
|
|
|
if len(siglist) > 2:
|
|
|
|
exit(' !! ERROR: That is not a valid SigLevel string. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').')
|
|
|
|
if not grp1.match(siglist[0]):
|
|
|
|
exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[0]))
|
|
|
|
if len(siglist) == 1:
|
|
|
|
if not grp2.match(siglist[1]):
|
|
|
|
exit((' !! ERROR: {0} is not valid. See the manpage for pacman.conf ' +
|
|
|
|
'(\'PACKAGE AND DATABASE SIGNATURE CHECKING\').').format(siglist[1]))
|
|
|
|
repos[reponame]['siglevel'] = siglvlin
|
|
|
|
else:
|
|
|
|
repos[reponame]['siglevel'] = 'default'
|
2017-05-14 12:07:39 -04:00
|
|
|
uriin = chkPrompt(('** What URI should be used for {0}{1}{2}?\n' +
|
|
|
|
'\tUse \'file:///absolute/path/to/file\' to use an Include directive: ').format(color.BOLD,
|
|
|
|
reponame,
|
|
|
|
color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if uriin == '':
|
|
|
|
exit(' !! ERROR: You cannot specify a blank repository URI.')
|
|
|
|
else:
|
|
|
|
repos[reponame]['mirror'] = uriin
|
|
|
|
morereposin = chkPrompt('* Would you like to add another repository? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp)
|
|
|
|
if not re.match('^y(es)?$', morereposin.lower()):
|
|
|
|
addrepos = False
|
|
|
|
return(repos)
|
|
|
|
def pkgsPrompt(repohelp):
|
|
|
|
pkgs = {}
|
|
|
|
morepkgs = True
|
|
|
|
while morepkgs:
|
|
|
|
pkgname = chkPrompt('** What is the name of the package? ', repohelp)
|
|
|
|
if pkgname == '':
|
|
|
|
exit(' !! ERROR: You must specify a package name.')
|
|
|
|
reponame = chkPrompt(('** What repository should we install {0} from? ' +
|
|
|
|
'({1}optional{2}, leave blank to skip) ').format(pkgname, color.BOLD, color.END), repohelp)
|
|
|
|
if reponame == '':
|
|
|
|
pkgs[pkgname] = None
|
|
|
|
else:
|
|
|
|
pkgs[pkgname] = reponame
|
|
|
|
morepkgsin = chkPrompt('** Would you like to add another package? (y/{0}n{1}) '.format(color.BOLD, color.END), repohelp)
|
|
|
|
if not re.match('^y(es)?$', morepkgsin.lower()):
|
|
|
|
morepkgs = False
|
|
|
|
return(pkgs)
|
2017-05-13 06:06:53 -04:00
|
|
|
def scrptPrompt(scrpthlp):
|
|
|
|
scrpts = {'pre': False, 'pkg': False, 'post': False}
|
|
|
|
morescrpts = True
|
|
|
|
while morescrpts:
|
|
|
|
hookin = chkPrompt('** What type of script is this? (pre/pkg/post) ', scrpthlp)
|
|
|
|
if not re.match('^p(re|kg|ost)$', hookin.lower()):
|
|
|
|
exit(' !! ERROR: The hook must be one of pre, pkg, or post.')
|
|
|
|
else:
|
|
|
|
hook = hookin.lower()
|
|
|
|
if not scrpts[hook]:
|
|
|
|
scrpts[hook] = {}
|
|
|
|
scrptin = chkPrompt('** What is the URI for this script? Can be an http://, https://, ftp://, ftps://, or file:// URI: ', scrpthlp)
|
|
|
|
if not re.match('^(https?|ftps?|file)://', scrptin.lower()):
|
|
|
|
exit(' !! ERROR: That is not a valid URI.')
|
|
|
|
orderin = chkPrompt(('** What order should this script be executed in during the {0} hook?\n' +
|
|
|
|
'\tMust be a unique integer ' +
|
|
|
|
'(lower numbers execute before higher numbers): ').format(hook), scrpthlp)
|
|
|
|
try:
|
|
|
|
order = int(ordrin)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: Must be an integer')
|
|
|
|
if order in scrpts[hook].keys():
|
|
|
|
exit(' !! ERROR: You already have a {0} script at that order number.'.format(order))
|
|
|
|
scrpts[hook][order] = {'uri': scrptin}
|
|
|
|
if re.match('^(https?|ftps?)://', scriptin.lower()):
|
|
|
|
authin = chkPrompt('** Does this script URI require auth? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp)
|
|
|
|
if re.match('^y(es)?$', authin.lower()):
|
|
|
|
if re.match('^https?://', scriptin.lower()):
|
|
|
|
authtype = chkPrompt(('*** What type of auth does this URI require? ' +
|
|
|
|
'({0}basic{1}/digest) ').format(color.BOLD, color.END), scrpthlp)
|
|
|
|
if authtype == '':
|
|
|
|
scrpts[hook][order]['auth'] = 'basic'
|
|
|
|
elif not re.match('^(basic|digest)$', authtype.lower()):
|
|
|
|
scrpts[hook][order]['auth'] = authtype.lower()
|
|
|
|
else:
|
|
|
|
exit(' !! ERROR: That is not a valid auth type.')
|
|
|
|
if authtype.lower() == 'digest':
|
|
|
|
realmin = chkPrompt('*** Do you know the realm needed for authentication?\n' +
|
|
|
|
'\tIf not, just leave this blank and AIF-NG will try to guess: ', scrpthlp)
|
|
|
|
if realmin != '':
|
|
|
|
scrpts[hook][order]['realm'] = realmin
|
|
|
|
scrpts[hook][order]['user'] = chkPrompt('*** What user should we use for auth? ', scrpthlp)
|
|
|
|
scrpts[hook][order]['password'] = chkPrompt('*** What password should we use for auth? ', scrpthlp)
|
|
|
|
else:
|
|
|
|
scrpts[hook][order][auth] = False
|
|
|
|
morescrptsin = chkPrompt('* Would you like to add another hook script? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp)
|
|
|
|
if not re.match('^y(es)?$', morescrptsin.lower()):
|
|
|
|
morescrpts = False
|
|
|
|
return(scrpts)
|
2017-05-07 23:50:11 -04:00
|
|
|
conf = {}
|
|
|
|
print('[{0}] Beginning configuration...'.format(datetime.datetime.now()))
|
2017-05-15 02:47:04 -04:00
|
|
|
print('\n\tYou may reply with \'wikihelp\' on the first prompt of a question for the relevant link(s) in the Arch wiki ' +
|
2017-05-14 01:21:15 -04:00
|
|
|
'(and other resources).')
|
2017-05-07 23:50:11 -04:00
|
|
|
# https://aif.square-r00t.net/#code_disk_code
|
|
|
|
diskhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Partition_the_disks']
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}= DISKS ={1}'.format(color.BOLD, color.END))
|
2017-05-15 02:47:04 -04:00
|
|
|
diskin = chkPrompt('* What disk(s) would you like to be configured on the target system?\n' +
|
2017-05-14 12:07:39 -04:00
|
|
|
'\tIf you have multiple disks, separate with a comma (e.g. \'/dev/sda,/dev/sdb\'): ', diskhelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
# NOTE: the following is a dict of fstype codes to their description.
|
|
|
|
fstypes = {'0700': 'Microsoft basic data', '0c01': 'Microsoft reserved', '2700': 'Windows RE', '3000': 'ONIE config', '3900': 'Plan 9', '4100': 'PowerPC PReP boot', '4200': 'Windows LDM data', '4201': 'Windows LDM metadata', '4202': 'Windows Storage Spaces', '7501': 'IBM GPFS', '7f00': 'ChromeOS kernel', '7f01': 'ChromeOS root', '7f02': 'ChromeOS reserved', '8200': 'Linux swap', '8300': 'Linux filesystem', '8301': 'Linux reserved', '8302': 'Linux /home', '8303': 'Linux x86 root (/)', '8304': 'Linux x86-64 root (/', '8305': 'Linux ARM64 root (/)', '8306': 'Linux /srv', '8307': 'Linux ARM32 root (/)', '8400': 'Intel Rapid Start', '8e00': 'Linux LVM', 'a500': 'FreeBSD disklabel', 'a501': 'FreeBSD boot', 'a502': 'FreeBSD swap', 'a503': 'FreeBSD UFS', 'a504': 'FreeBSD ZFS', 'a505': 'FreeBSD Vinum/RAID', 'a580': 'Midnight BSD data', 'a581': 'Midnight BSD boot', 'a582': 'Midnight BSD swap', 'a583': 'Midnight BSD UFS', 'a584': 'Midnight BSD ZFS', 'a585': 'Midnight BSD Vinum', 'a600': 'OpenBSD disklabel', 'a800': 'Apple UFS', 'a901': 'NetBSD swap', 'a902': 'NetBSD FFS', 'a903': 'NetBSD LFS', 'a904': 'NetBSD concatenated', 'a905': 'NetBSD encrypted', 'a906': 'NetBSD RAID', 'ab00': 'Recovery HD', 'af00': 'Apple HFS/HFS+', 'af01': 'Apple RAID', 'af02': 'Apple RAID offline', 'af03': 'Apple label', 'af04': 'AppleTV recovery', 'af05': 'Apple Core Storage', 'bc00': 'Acronis Secure Zone', 'be00': 'Solaris boot', 'bf00': 'Solaris root', 'bf01': 'Solaris /usr & Mac ZFS', 'bf02': 'Solaris swap', 'bf03': 'Solaris backup', 'bf04': 'Solaris /var', 'bf05': 'Solaris /home', 'bf06': 'Solaris alternate sector', 'bf07': 'Solaris Reserved 1', 'bf08': 'Solaris Reserved 2', 'bf09': 'Solaris Reserved 3', 'bf0a': 'Solaris Reserved 4', 'bf0b': 'Solaris Reserved 5', 'c001': 'HP-UX data', 'c002': 'HP-UX service', 'ea00': 'Freedesktop $BOOT', 'eb00': 'Haiku BFS', 'ed00': 'Sony system partition', 'ed01': 'Lenovo system partition', 'ef00': 'EFI System', 'ef01': 'MBR partition scheme', 'ef02': 'BIOS boot partition', 'f800': 'Ceph OSD', 'f801': 'Ceph dm-crypt OSD', 'f802': 'Ceph journal', 'f803': 'Ceph dm-crypt journal', 'f804': 'Ceph disk in creation', 'f805': 'Ceph dm-crypt disk in creation', 'fb00': 'VMWare VMFS', 'fb01': 'VMWare reserved', 'fc00': 'VMWare kcore crash protection', 'fd00': 'Linux RAID'}
|
2017-05-07 23:50:11 -04:00
|
|
|
conf['disks'] = {}
|
|
|
|
for d in diskin.split(','):
|
|
|
|
disk = d.strip()
|
|
|
|
if not re.match('^/dev/[A-Za-z0]+', disk):
|
|
|
|
exit('!! ERROR: Disk {0} does not seem to be a valid device path.'.format(disk))
|
|
|
|
conf['disks'][disk] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
print('\n{0}== DISK: {1} =={2}'.format(color.BOLD, disk, color.END))
|
|
|
|
fmtin = chkPrompt('* What format should this disk use (gpt/bios)? ', diskhelp)
|
2017-05-07 23:50:11 -04:00
|
|
|
fmt = fmtin.lower()
|
|
|
|
if fmt not in ('gpt', 'bios'):
|
|
|
|
exit(' !! ERROR: Must be one of \'gpt\' or \'bios\'.')
|
|
|
|
conf['disks'][disk]['fmt'] = fmt
|
2017-05-09 08:17:07 -04:00
|
|
|
conf['disks'][disk]['parts'] = {}
|
2017-05-07 23:50:11 -04:00
|
|
|
if fmt == 'gpt':
|
|
|
|
maxpart = '256'
|
|
|
|
else:
|
2017-05-09 08:17:07 -04:00
|
|
|
maxpart = '4' # yeah, extended volumes can do more, but that's not supported in AIF-NG. yet?
|
2017-05-14 12:07:39 -04:00
|
|
|
partnumsin = chkPrompt('* How many partitions should this disk have? (Maximum: {0}) '.format(maxpart), diskhelp)
|
2017-05-11 18:46:36 -04:00
|
|
|
try:
|
|
|
|
int(partnumsin)
|
|
|
|
except:
|
2017-05-07 23:50:11 -04:00
|
|
|
exit(' !! ERROR: Must be an integer.')
|
2017-05-11 18:46:36 -04:00
|
|
|
if int(partnumsin) < 1:
|
2017-05-07 23:50:11 -04:00
|
|
|
exit(' !! ERROR: Must be a positive integer.')
|
2017-05-11 18:46:36 -04:00
|
|
|
if int(partnumsin) > int(maxpart):
|
2017-05-07 23:50:11 -04:00
|
|
|
exit(' !! ERROR: Must be less than {0}'.format(maxpart))
|
|
|
|
parthelp = diskhelp + ['https://wiki.archlinux.org/index.php/installation_guide#Format_the_partitions',
|
2017-05-09 08:17:07 -04:00
|
|
|
'https://aif.square-r00t.net/#code_part_code']
|
2017-05-11 18:46:36 -04:00
|
|
|
for partn in range(1, int(partnumsin) + 1):
|
2017-05-09 08:17:07 -04:00
|
|
|
# https://aif.square-r00t.net/#code_part_code
|
|
|
|
conf['disks'][disk]['parts'][partn] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}=== PARTITION: {1}{2}==={3}'.format(color.BOLD, disk, partn, color.END))
|
2017-05-09 08:17:07 -04:00
|
|
|
for s in ('start', 'stop'):
|
|
|
|
conf['disks'][disk]['parts'][partn][s] = None
|
2017-05-14 12:07:39 -04:00
|
|
|
sizein = chkPrompt(('* Where should partition {0} {1}? Can be percentage [n%] ' +
|
2017-05-09 08:17:07 -04:00
|
|
|
'or size [(+/-)n(K/M/G/T/P)]: ').format(partn, s), parthelp)
|
|
|
|
conf['disks'][disk]['parts'][partn][s] = sizeChk(sizein)
|
|
|
|
newhelp = 'https://aif.square-r00t.net/#fstypes'
|
|
|
|
if newhelp not in parthelp:
|
|
|
|
parthelp.append(newhelp)
|
2017-05-14 12:07:39 -04:00
|
|
|
fstypein = chkPrompt(('* What filesystem type should partition {0} be? ' +
|
2017-05-11 18:46:36 -04:00
|
|
|
'See wikihelp for valid fstypes: ').format(partn), parthelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
if fstypein not in fstypes.keys():
|
|
|
|
exit(' !! ERROR: {0} is not a valid filesystem type.'.format(fstypein))
|
|
|
|
else:
|
2017-05-14 01:21:15 -04:00
|
|
|
print('\t(Selected {0})'.format(fstypes[fstypein]))
|
2017-05-15 02:47:04 -04:00
|
|
|
conf['disks'][disk]['parts'][partn]['fstype'] = fstypein
|
2017-05-09 08:17:07 -04:00
|
|
|
mnthelp = ['https://wiki.archlinux.org/index.php/installation_guide#Mount_the_file_systems',
|
|
|
|
'https://aif.square-r00t.net/#code_mount_code']
|
2017-05-15 02:47:04 -04:00
|
|
|
print('\n{0}= MOUNTS ={1}'.format(color.BOLD, color.END))
|
|
|
|
mntin = chkPrompt('* What mountpoint(s) would you like to be configured on the target system?\n' +
|
|
|
|
'\tIf you have multiple mountpoints, separate with a comma (e.g. \'/mnt/aif,/mnt/aif/boot\').\n' +
|
|
|
|
'\t(NOTE: Can be \'swap\' for swapspace.): ', mnthelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
conf['mounts'] = {}
|
|
|
|
for m in mntin.split(','):
|
|
|
|
mount = m.strip()
|
|
|
|
if not re.match('^(/([^/\x00\s]+(/)?)+|swap)$', mount):
|
|
|
|
exit('!! ERROR: Mountpoint {0} does not seem to be a valid path/specifier.'.format(mount))
|
2017-05-15 02:47:04 -04:00
|
|
|
print('\n{0}== MOUNT: {1} =={2}'.format(color.BOLD, mount, color.END))
|
2017-05-14 12:07:39 -04:00
|
|
|
dvcin = chkPrompt('* What device/partition should be mounted here? ', mnthelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
if not re.match('^/dev/[A-Za-z0]+', dvcin):
|
|
|
|
exit(' !! ERROR: Must be a full path to a device/partition.')
|
2017-05-14 12:07:39 -04:00
|
|
|
ordrin = chkPrompt('* What order should this mount occur in relation to others?\n\t'+
|
2017-05-09 08:17:07 -04:00
|
|
|
'Must be a unique integer (lower numbers mount before higher numbers): ', mnthelp)
|
|
|
|
try:
|
|
|
|
order = int(ordrin)
|
|
|
|
except:
|
|
|
|
exit(' !! ERROR: Must be an integer')
|
|
|
|
if order in conf['mounts'].keys():
|
|
|
|
exit(' !! ERROR: You already have a mountpoint at that order number.')
|
|
|
|
conf['mounts'][order] = {}
|
|
|
|
conf['mounts'][order]['target'] = mount
|
|
|
|
conf['mounts'][order]['device'] = dvcin
|
2017-05-14 01:21:15 -04:00
|
|
|
if mount != 'swap':
|
2017-05-14 12:07:39 -04:00
|
|
|
fstypein = chkPrompt('* What filesystem type should this be mounted as (i.e. mount\'s -t option)? This is optional,\n\t' +
|
2017-05-14 01:21:15 -04:00
|
|
|
'but may be required for more exotic filesystem types. If you don\'t have to specify one,\n\t' +
|
|
|
|
'just leave this blank: ', mnthelp)
|
|
|
|
if fstypein == '':
|
|
|
|
conf['mounts'][order]['fstype'] = False
|
|
|
|
elif not re.match('^[a-z]+([0-9]+)?$', fstypein): # Not 100%, but should catch most faulty entries
|
|
|
|
exit(' !! ERROR: {0} does not seem to be a valid filesystem type.'.format(fstypein))
|
|
|
|
else:
|
|
|
|
conf['mounts'][order]['fstype'] = fstypein
|
|
|
|
mntoptsin = chkPrompt('** What, if any, mount option(s) (mount\'s -o option) do you require? (Multiple options should be separated\n' +
|
|
|
|
'\twith a comma). If none, leave this blank: ', mnthelp)
|
|
|
|
if mntoptsin == '':
|
|
|
|
conf['mounts'][order]['opts'] = False
|
|
|
|
elif not re.match('^[A-Za-z0-9_\.\-=]+(,[A-Za-z0-9_\.\-=]+)*', re.sub('\s', '', mntoptsin)): # TODO: shlex split this instead?
|
|
|
|
exit(' !! ERROR: You seem to have not specified valid mount options.')
|
|
|
|
else:
|
|
|
|
# TODO: slex this instead? is it possible for mount opts to contain whitespace?
|
|
|
|
conf['mounts'][order]['opts'] = re.sub('\s', '', mntoptsin)
|
2017-05-09 08:17:07 -04:00
|
|
|
else:
|
2017-05-14 01:21:15 -04:00
|
|
|
conf['mounts'][order]['fstype'] = False
|
2017-05-09 08:17:07 -04:00
|
|
|
conf['mounts'][order]['opts'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
print(('\n{0}= NETWORK ={1}\n' +
|
|
|
|
'\tNOTE: At this time, wireless/more exotic networking is not supported by AIF-NG.').format(color.BOLD, color.END))
|
2017-05-09 08:17:07 -04:00
|
|
|
conf['network'] = {}
|
|
|
|
nethelp = ['https://wiki.archlinux.org/index.php/installation_guide#Network_configuration',
|
|
|
|
'https://aif.square-r00t.net/#code_network_code']
|
2017-05-14 01:21:15 -04:00
|
|
|
hostnamein = chkPrompt('* What should the newly-installed system\'s hostname be?\n' +
|
|
|
|
'\tIt must be in FQDN format, but can be a non-existent domain: ', nethelp)
|
2017-05-09 08:17:07 -04:00
|
|
|
hostname = hostnamein.lower()
|
|
|
|
if len(hostname) > 253:
|
|
|
|
exit(' !! ERROR: A FQDN cannot be more than 253 characters (RFC 1035, 2.3.4)')
|
|
|
|
hostnamelst = hostname.split('.')
|
|
|
|
for c in hostnamelst:
|
|
|
|
if len(c) > 63:
|
|
|
|
exit(' !! ERROR: No component of an FQDN can be more than 63 characters (RFC 1035, 2.3.4)')
|
|
|
|
if not re.match('^[a-zA-Z\d-]{,63}(\.[a-zA-Z\d-]{,63})*', hostname):
|
|
|
|
exit(' !! ERROR: That does not seem to be a valid FQDN.')
|
|
|
|
else:
|
|
|
|
conf['network']['hostname'] = hostname
|
|
|
|
conf['network']['ifaces'] = {}
|
|
|
|
nethelp.append('https://aif.square-r00t.net/#code_iface_code')
|
2017-05-11 18:46:36 -04:00
|
|
|
conf['network']['ifaces'] = ifacePrompt(nethelp)
|
2017-05-14 12:07:39 -04:00
|
|
|
print('\n{0}= SYSTEM ={1}'.format(color.BOLD, color.END))
|
2017-05-11 18:46:36 -04:00
|
|
|
syshelp = ['https://aif.square-r00t.net/#code_system_code']
|
|
|
|
syshelp.append('https://wiki.archlinux.org/index.php/installation_guide#Time_zone')
|
|
|
|
tzin = chkPrompt('* What timezone should the newly installed system use? (Default is UTC): ', syshelp)
|
|
|
|
if tzin == '':
|
|
|
|
tzin = 'UTC'
|
|
|
|
syshelp[1] = 'https://wiki.archlinux.org/index.php/installation_guide#Locale'
|
|
|
|
localein = chkPrompt('* What locale should the new system use? (Default is en_US.UTF-8): ', syshelp)
|
|
|
|
if localein == '':
|
|
|
|
localein = 'en_US.UTF-8'
|
|
|
|
syshelp[1] = 'https://aif.square-r00t.net/#code_mount_code'
|
|
|
|
chrootpathin = chkPrompt('* What chroot path should the host use? This should be one of the mounts you specified above: ', syshelp)
|
|
|
|
if not re.match('^/([^/\x00\s]+(/)?)+$', chrootpathin):
|
|
|
|
exit('!! ERROR: Your chroot path does not seem to be a valid path/specifier.')
|
|
|
|
syshelp[1] = 'https://wiki.archlinux.org/index.php/installation_guide#Set_the_keyboard_layout'
|
|
|
|
kbdin = chkPrompt('* What keyboard layout should the newly installed system use? (Default is US): ', syshelp)
|
|
|
|
if kbdin == '':
|
|
|
|
kbdin = 'US'
|
|
|
|
del(syshelp[1])
|
2017-05-12 23:00:17 -04:00
|
|
|
rbtin = chkPrompt('* Would you like to reboot the host system after installation completes? ({0}y{1}/n): '.format(color.BOLD, color.END), syshelp)
|
2017-05-11 18:46:36 -04:00
|
|
|
if not re.match('^no?$', rbtin.lower()):
|
|
|
|
rebootme = True
|
|
|
|
else:
|
|
|
|
rebootme = False
|
2017-05-11 23:27:22 -04:00
|
|
|
conf['system'] = {'timezone': tzin, 'locale': localein, 'chrootpath': chrootpathin, 'kbd': kbdin, 'reboot': rebootme}
|
2017-05-14 01:21:15 -04:00
|
|
|
syshelp.append('https://aif.square-r00t.net/#code_users_code')
|
2017-05-14 12:07:39 -04:00
|
|
|
print(('\n{0}== USERS =={1}\n\tNOTE: For passwords, you can either enter the password you want to use,\n' +
|
|
|
|
'\ta \'!\' (in which case TTY login will be disabled but e.g. SSH will still work), or just hit enter to leave it blank\n' +
|
|
|
|
'\t(which is HIGHLY not recommended - it means anyone can login by just pressing enter at the login!)').format(color.BOLD, color.END))
|
|
|
|
print('{0}=== ROOT ==={1}'.format(color.BOLD, color.END))
|
2017-05-14 01:21:15 -04:00
|
|
|
conf['system']['rootpass'] = genPassHash('root')
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}=== REGULAR USERS ==={1}'.format(color.BOLD, color.END))
|
|
|
|
moreusers = input('* Would you like to add regular user(s)? (y/{0}n{1}) '.format(color.BOLD, color.END))
|
2017-05-12 23:00:17 -04:00
|
|
|
if re.match('^y(es)?$', moreusers.lower()):
|
|
|
|
syshelp.append('https://aif.square-r00t.net/#code_user_code')
|
|
|
|
conf['system']['users'] = userPrompt(syshelp)
|
|
|
|
else:
|
|
|
|
conf['system']['users'] = False
|
2017-05-13 03:59:31 -04:00
|
|
|
svchelp = ['https://wiki.archlinux.org/index.php/Systemd',
|
|
|
|
'https://aif.square-r00t.net/#code_service_code']
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}== SERVICES =={1}'.format(color.BOLD, color.END))
|
|
|
|
svcin = chkPrompt('* Would you like to configure (enable/disable) services? (y/{0}n{1}) '.format(color.BOLD, color.END), svchelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if re.match('^y(es)?$', svcin.lower()):
|
|
|
|
conf['system']['services'] = svcsPrompt(svchelp)
|
|
|
|
else:
|
|
|
|
conf['system']['services'] = False
|
2017-05-14 12:07:39 -04:00
|
|
|
print('\n{0}== PACKAGES/SOFTWARE =={1}'.format(color.BOLD, color.END))
|
2017-05-13 03:59:31 -04:00
|
|
|
conf['software'] = {}
|
|
|
|
pkgrhelp = ['https://wiki.archlinux.org/index.php/Pacman',
|
|
|
|
'https://wiki.archlinux.org/index.php/AUR_helpers',
|
|
|
|
'https://aif.square-r00t.net/#code_pacman_code']
|
2017-05-14 12:07:39 -04:00
|
|
|
pkgrcmd = chkPrompt('* If you won\'t be using pacman for a package manager, what command should be used to install packages?\n' +
|
2017-05-13 03:59:31 -04:00
|
|
|
'\t(Remember that you would need to install/configure it in a \'pkg\' hook script.)\n' +
|
|
|
|
'\tLeave blank if you\'ll only be using pacman: ', pkgrhelp)
|
|
|
|
if pkgrcmd == '':
|
|
|
|
conf['software']['pkgr'] = False
|
|
|
|
else:
|
|
|
|
conf['software']['pkgr'] = pkgrcmd
|
2017-05-14 12:07:39 -04:00
|
|
|
print('\n{0}=== REPOSITORIES/PACKAGES ==={1}'.format(color.BOLD, color.END))
|
2017-05-13 03:59:31 -04:00
|
|
|
repohelp = ['https://aif.square-r00t.net/#code_repos_code']
|
|
|
|
conf['software']['repos'] = repoPrompt(repohelp)
|
2017-05-14 12:07:39 -04:00
|
|
|
if pkgrcmd == '':
|
|
|
|
pkgrcmd = 'pacman --needed --noconfirm -S'
|
|
|
|
pkgsin = chkPrompt(('* Would you like to install extra packages?\n' +
|
2017-05-13 03:59:31 -04:00
|
|
|
'\t(Note that they must be available in your configured repositories or\n' +
|
2017-05-14 12:07:39 -04:00
|
|
|
'\tinstallable via "{0} <package name>".) (y/{1}n{2}) ').format(pkgrcmd, color.BOLD, color.END), repohelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if re.match('^y(es)?$', pkgsin.lower()):
|
|
|
|
repohelp.append('https://aif.square-r00t.net/#code_package_code')
|
|
|
|
conf['software']['packages'] = pkgsPrompt(repohelp)
|
|
|
|
else:
|
|
|
|
conf['software']['packages'] = False
|
|
|
|
btldrhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Boot_loader',
|
|
|
|
'https://aif.square-r00t.net/#code_bootloader_code']
|
|
|
|
conf['boot'] = {}
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}== BOOTLOADER =={1}'.format(color.BOLD, color.END))
|
|
|
|
btldrin = chkPrompt('* Please choose a bootloader. ({0}grub{1}/systemd) '.format(color.BOLD, color.END), btldrhelp)
|
2017-05-14 01:21:15 -04:00
|
|
|
if btldrin == '':
|
|
|
|
btldrin = 'grub'
|
|
|
|
elif not re.match('^(grub|systemd)$', btldrin.lower()):
|
2017-05-13 03:59:31 -04:00
|
|
|
exit(' !! ERROR: You must choose a bootloader between grub or systemd.')
|
|
|
|
else:
|
|
|
|
conf['boot']['bootloader'] = btldrin.lower()
|
|
|
|
bttgtstr = 'boot partition/disk'
|
|
|
|
btrgx = re.compile('^/dev/[A-Za-z0]+')
|
|
|
|
if btldrin.lower() == 'grub':
|
|
|
|
efienable = chkPrompt('** Is this system (U)EFI-capable? ({0}y{1}/n) '.format(color.BOLD, color.END), btldrhelp)
|
|
|
|
if re.match('^no?$', efienable.lower()):
|
|
|
|
conf['boot']['efi'] = False
|
|
|
|
else:
|
|
|
|
conf['boot']['efi'] = True
|
|
|
|
bttgtstr = 'ESP (EFI System Partition)'
|
|
|
|
btrgx = re.compile('^/([^/\x00\s]+(/)?)+$')
|
2017-05-14 01:21:15 -04:00
|
|
|
bttgtin = chkPrompt('** What is the target for {0}? That is, the path to the {1} (within the chroot): '.format(btldrin.lower(), bttgtstr), btldrhelp)
|
2017-05-13 03:59:31 -04:00
|
|
|
if not btrgx.match(bttgtin):
|
|
|
|
exit(' !! ERROR: That doesn\'t seem to be a valid {0}.'.format(bttgtstr))
|
|
|
|
else:
|
|
|
|
conf['boot']['target'] = bttgtin
|
|
|
|
scrpthlp = ['https://aif.square-r00t.net/#code_script_code']
|
2017-05-14 12:07:39 -04:00
|
|
|
print('{0}= HOOK SCRIPTS ={1}'.format(color.BOLD, color.END))
|
|
|
|
scrptsin = chkPrompt('* Do you have any hook scripts you\'d like to add? (y/{0}n{1}) '.format(color.BOLD, color.END), scrpthlp)
|
2017-05-13 06:06:53 -04:00
|
|
|
if re.match('^y(es)?$', scrptsin.lower()):
|
|
|
|
conf['scripts'] = scrptPrompt(scrpthlp)
|
2017-05-15 02:47:04 -04:00
|
|
|
else:
|
|
|
|
conf['scripts'] = False
|
|
|
|
print('\n\n[{0}] {1}ALL DONE!{2} Whew. You can find your configuration file at: {3}{4}{2}\n'.format(datetime.datetime.now(),
|
|
|
|
color.BOLD,
|
|
|
|
color.END,
|
|
|
|
color.BLUE,
|
|
|
|
self.args['cfgfile']))
|
2017-05-11 18:46:36 -04:00
|
|
|
if self.args['verbose']:
|
2017-05-09 08:17:07 -04:00
|
|
|
import pprint
|
|
|
|
pprint.pprint(conf)
|
2017-05-14 01:21:15 -04:00
|
|
|
if self.args['verbose_raw']:
|
|
|
|
print(conf)
|
2017-05-09 08:17:07 -04:00
|
|
|
return(conf)
|
2017-05-11 23:27:22 -04:00
|
|
|
|
|
|
|
def convertJSON(self):
|
2017-05-14 01:21:15 -04:00
|
|
|
with open(self.args['inputfile'], 'r') as f:
|
2017-05-11 23:27:22 -04:00
|
|
|
try:
|
2017-05-14 01:21:15 -04:00
|
|
|
conf = json.load(f)
|
2017-05-11 23:27:22 -04:00
|
|
|
except:
|
|
|
|
exit(' !! ERROR: {0} does not seem to be a strict JSON file.'.format(args['inputfile']))
|
|
|
|
return(conf)
|
2017-05-07 23:50:11 -04:00
|
|
|
|
|
|
|
def validateXML(self):
|
2017-05-13 08:11:59 -04:00
|
|
|
# First we validate the XSD.
|
|
|
|
if not lxml_avail:
|
2017-05-15 02:47:04 -04:00
|
|
|
exit('\nXML validation is only supported by LXML.\n' +
|
|
|
|
'If you want to validate the XML, install the lxml python module (python-lxml) ' +
|
|
|
|
'and run:\n\t{0} validate -f {1}.\n'.format(sys.argv[0], self.args['cfgfile']))
|
2017-05-13 08:11:59 -04:00
|
|
|
try:
|
|
|
|
xsd = etree.XMLSchema(self.getXSD())
|
|
|
|
print('\nXSD: {0}PASSED{1}'.format(color.BOLD, color.END))
|
|
|
|
except Exception as e:
|
|
|
|
exit('\nXSD: {0}FAILED{1}: {2}'.format(color.BOLD, color.END, e))
|
|
|
|
# Then we can validate the XML.
|
|
|
|
try:
|
|
|
|
xml = xsd.validate(self.getXML())
|
|
|
|
print('XML: {0}PASSED{1}\n'.format(color.BOLD, color.END))
|
|
|
|
except Exception as e:
|
|
|
|
print('XML: {0}FAILED{1}: {2}\n'.format(color.BOLD, color.END, e))
|
2017-05-14 12:07:39 -04:00
|
|
|
|
|
|
|
def genXMLFile(self, conf):
|
2017-05-15 02:47:04 -04:00
|
|
|
namespaces = {'aif': 'http://aif.square-r00t.net/', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
|
|
|
|
xsi = {'{http://www.w3.org/2001/XMLSchema-instance}schemaLocation' : 'http://aif.square-r00t.net aif.xsd'}
|
|
|
|
#for ns in namespaces.keys():
|
|
|
|
# etree.register_namespace(ns, namespaces[ns])
|
2017-05-14 12:07:39 -04:00
|
|
|
if lxml_avail:
|
2017-05-15 02:47:04 -04:00
|
|
|
genname = 'LXML (http://lxml.de/)'
|
|
|
|
root = etree.Element('aif', nsmap = namespaces, attrib = xsi)
|
|
|
|
#xml = etree.ElementTree(root)
|
|
|
|
else:
|
|
|
|
genname = 'Python stdlib "xml" module'
|
|
|
|
for ns in namespaces.keys():
|
|
|
|
etree.register_namespace(ns, namespaces[ns])
|
2017-05-14 12:07:39 -04:00
|
|
|
root = etree.Element('aif')
|
2017-05-15 02:47:04 -04:00
|
|
|
if self.args['oper'] == 'convert':
|
|
|
|
fromstr = self.args['inputfile']
|
|
|
|
else:
|
|
|
|
fromstr = 'interactive commandline'
|
|
|
|
root.append(etree.Comment('Generated by {0} on {1} from {2} via {3}'.format(sys.argv[0], datetime.datetime.now(), fromstr, genname)))
|
|
|
|
root.append(etree.Comment('THIS FILE CONTAINS SENSITIVE INFORMATION. SHARE/SCRUB WISELY.'))
|
|
|
|
# /aif/ required sections
|
|
|
|
for e in ('storage', 'network', 'system', 'pacman', 'bootloader'):
|
|
|
|
root.append(etree.Element(e))
|
|
|
|
# /aif/ optional sections
|
|
|
|
if conf['scripts']:
|
|
|
|
root.append(etree.Element('scripts'))
|
|
|
|
# /aif/storage
|
|
|
|
strg = root.find('storage')
|
|
|
|
for d in conf['disks'].keys():
|
|
|
|
# /aif/storage/disk
|
|
|
|
disk = etree.Element('disk', device = d, diskfmt = conf['disks'][d]['fmt'])
|
|
|
|
for p in conf['disks'][d]['parts'].keys():
|
|
|
|
# /aif/storage/disk/part
|
|
|
|
start = conf['disks'][d]['parts'][p]['start']
|
|
|
|
stop = conf['disks'][d]['parts'][p]['stop']
|
|
|
|
fstype = conf['disks'][d]['parts'][p]['fstype']
|
|
|
|
disk.append(etree.Element('part', num = p, start = start, stop = stop, fstype = fstype))
|
|
|
|
strg.append(disk)
|
|
|
|
# /aif/storage/mount
|
|
|
|
for m in conf['mounts'].keys():
|
|
|
|
mnt = {}
|
|
|
|
mnt['order'] = m
|
|
|
|
mnt['source'] = conf['mounts'][m]['device']
|
|
|
|
mnt['target'] = conf['mounts'][m]['target']
|
|
|
|
# These are optional, hence the splat and mnt dict.
|
|
|
|
for o in ('fstype', 'opts'):
|
|
|
|
if o in conf['mounts'][m].keys() and conf['mounts'][m][o]:
|
|
|
|
mnt[o] = conf['mounts'][m][o]
|
|
|
|
mount = etree.Element('mount', **mnt)
|
|
|
|
strg.append(mount)
|
|
|
|
# /aif/network
|
|
|
|
ntwk = root.find('network')
|
|
|
|
ntwk.set('hostname', conf['network']['hostname'])
|
|
|
|
for i in conf['network']['ifaces'].keys():
|
|
|
|
# /aif/network/iface
|
|
|
|
optmap = {'gw': 'gateway', 'proto': 'netproto', 'resolvers': 'resolvers'}
|
|
|
|
iface = {}
|
|
|
|
iface['device'] = i
|
|
|
|
iface['address'] = conf['network']['ifaces'][i]['address']
|
|
|
|
for o in optmap.keys():
|
|
|
|
if conf['network']['ifaces'][i][o]:
|
|
|
|
if o == 'resolvers':
|
|
|
|
iface[optmap[o]] = ','.join(conf['network']['ifaces'][i][o])
|
|
|
|
else:
|
|
|
|
iface[optmap[o]] = conf['network']['ifaces'][i][o]
|
|
|
|
interface = etree.Element('iface', **iface)
|
|
|
|
ntwk.append(interface)
|
|
|
|
# /aif/system
|
|
|
|
systm = root.find('system')
|
|
|
|
for a in ('timezone', 'locale', 'chrootpath', 'kbd', 'reboot'):
|
|
|
|
if isinstance(conf['system'][a], bool):
|
|
|
|
val = str(conf['system'][a]).lower()
|
|
|
|
else:
|
|
|
|
val = conf['system'][a]
|
|
|
|
systm.set(a, val)
|
|
|
|
# /aif/system/users
|
|
|
|
usrs = etree.Element('users', rootpass = conf['system']['rootpass'])
|
|
|
|
subs = ('home', 'xgroups')
|
|
|
|
optional = ('uid', 'group', 'gid')
|
|
|
|
if conf['system']['users']:
|
|
|
|
for u in conf['system']['users'].keys():
|
|
|
|
# /aif/system/users/user
|
|
|
|
o = {}
|
|
|
|
o['name'] = u
|
|
|
|
for i in conf['system']['users'][u].keys():
|
|
|
|
if isinstance(conf['system']['users'][u][i], bool):
|
|
|
|
val = str(conf['system']['users'][u][i]).lower()
|
|
|
|
else:
|
|
|
|
val = conf['system']['users'][u][i]
|
|
|
|
if i not in subs: # we handle "subs" as subelements
|
|
|
|
if i in optional: # and we only add optional attribs if they're populated
|
|
|
|
if conf['system']['users'][u][i]:
|
|
|
|
o[i] = val
|
|
|
|
else:
|
|
|
|
o[i] = val
|
|
|
|
user = etree.Element('user', **o)
|
|
|
|
# /aif/system/users/user/home
|
|
|
|
if conf['system']['users'][u]['home']:
|
|
|
|
o = {}
|
|
|
|
o['create'] = str(conf['system']['users'][u]['home']['create']).lower()
|
|
|
|
if 'path' in conf['system']['users'][u]['home'].keys():
|
|
|
|
o['path'] = conf['system']['users'][u]['home']['path']
|
|
|
|
home = etree.Element('home', **o)
|
|
|
|
user.append(home)
|
|
|
|
# /aig/system/users/user/xgroup
|
|
|
|
if conf['system']['users'][u]['xgroups']:
|
|
|
|
for g in conf['system']['users'][u]['xgroups'].keys():
|
|
|
|
o = {}
|
|
|
|
o['name'] = g
|
|
|
|
o['create'] = str(conf['system']['users'][u]['xgroups'][g]['create']).lower()
|
|
|
|
if 'gid' in conf['system']['users'][u]['xgroups'][g].keys() and conf['system']['users'][u]['xgroups'][g]['gid']:
|
|
|
|
o['gid'] = conf['system']['users'][u]['xgroups'][g]['gid']
|
|
|
|
xgrp = etree.Element('xgroup', **o)
|
|
|
|
user.append(xgrp)
|
|
|
|
usrs.append(user)
|
|
|
|
systm.append(usrs)
|
|
|
|
# /aif/system/service
|
|
|
|
if conf['system']['services']:
|
|
|
|
for s in conf['system']['services'].keys():
|
|
|
|
o = {}
|
|
|
|
o['name'] = s
|
|
|
|
o['status'] = str(conf['system']['services'][s]).lower()
|
|
|
|
svc = etree.Element('service', **o)
|
|
|
|
systm.append(svc)
|
|
|
|
# /aif/pacman
|
|
|
|
pcmn = root.find('pacman')
|
|
|
|
if conf['software']['pkgr']:
|
|
|
|
pcmn.set('command', conf['software']['pkgr'])
|
|
|
|
# /aif/pacman/repo
|
|
|
|
repos = etree.Element('repos')
|
|
|
|
for r in conf['software']['repos'].keys():
|
|
|
|
o = {}
|
|
|
|
o['name'] = r
|
|
|
|
o['enabled'] = str(conf['software']['repos'][r]['enabled']).lower()
|
|
|
|
o['siglevel'] = conf['software']['repos'][r]['siglevel']
|
|
|
|
o['mirror'] = conf['software']['repos'][r]['mirror']
|
|
|
|
repo = etree.Element('repo', **o)
|
|
|
|
repos.append(repo)
|
|
|
|
pcmn.append(repos)
|
|
|
|
# debugging
|
|
|
|
if lxml_avail:
|
|
|
|
# LXML
|
|
|
|
#print(etree.tostring(root).decode('utf-8'))
|
|
|
|
print(etree.tostring(root, xml_declaration = True, encoding = 'utf-8', pretty_print = True).decode('utf-8'))
|
2017-05-14 12:07:39 -04:00
|
|
|
else:
|
2017-05-15 02:47:04 -04:00
|
|
|
# XML
|
|
|
|
import xml.dom.minidom
|
|
|
|
xmlstr = etree.tostring(root, encoding = 'utf-8')
|
|
|
|
# holy cats, the xml module sucks.
|
|
|
|
nsstr = ''
|
|
|
|
for ns in namespaces.keys():
|
|
|
|
nsstr += ' xmlns:{0}="{1}"'.format(ns, namespaces[ns])
|
|
|
|
for x in xsi.keys():
|
|
|
|
xsiname = x.split('}')[1]
|
|
|
|
nsstr += ' xsi:{0}="{1}"'.format(xsiname, xsi[x])
|
|
|
|
outstr = xml.dom.minidom.parseString(xmlstr).toprettyxml(indent = ' ').splitlines()
|
|
|
|
outstr[0] = '<?xml version=\'1.0\' encoding=\'utf-8\'?>'
|
|
|
|
outstr[1] = '<aif{0}>'.format(nsstr)
|
|
|
|
print('\n'.join(outstr))
|
|
|
|
# end debugging
|
|
|
|
# https://stackoverflow.com/questions/4886189/python-namespaces-in-xml-elementtree-or-lxml
|
|
|
|
#if lxml_avail:
|
|
|
|
#xml.write(..., xml_declaration = True, encoding='utf-8')
|
|
|
|
#else:
|
|
|
|
#import xml.dom.minidom
|
|
|
|
#xmlstr = etree.tostring(root, encoding = 'utf-8')
|
|
|
|
#with open(self.args['cfgfile'], 'w') as f: # TODO: test this. print() wrap it necessary?
|
|
|
|
#f.write(xml.dom.minidom.parseString(xmlstr).toprettyxml(indent = ' '))
|
|
|
|
return()
|
2017-05-14 12:07:39 -04:00
|
|
|
|
2017-05-07 23:50:11 -04:00
|
|
|
def main(self):
|
|
|
|
if self.args['oper'] == 'create':
|
2017-05-11 18:46:36 -04:00
|
|
|
conf = self.getOpts()
|
2017-05-11 23:27:22 -04:00
|
|
|
elif self.args['oper'] == 'convert':
|
|
|
|
conf = self.convertJSON()
|
2017-05-14 12:07:39 -04:00
|
|
|
if self.args['oper'] in ('create', 'convert'):
|
|
|
|
self.genXMLFile(conf)
|
2017-05-14 01:21:15 -04:00
|
|
|
if self.args['oper'] in ('create', 'convert', 'validate'):
|
2017-05-07 23:50:11 -04:00
|
|
|
self.validateXML()
|
|
|
|
|
|
|
|
def parseArgs():
|
|
|
|
args = argparse.ArgumentParser(description = 'AIF-NG Configuration Generator',
|
2017-05-09 08:17:07 -04:00
|
|
|
epilog = 'TIP: this program has context-specific help. e.g. try:\n\t%(prog)s create --help',
|
|
|
|
formatter_class = argparse.RawTextHelpFormatter)
|
2017-05-07 23:50:11 -04:00
|
|
|
commonargs = argparse.ArgumentParser(add_help = False)
|
|
|
|
commonargs.add_argument('-f',
|
|
|
|
'--file',
|
|
|
|
dest = 'cfgfile',
|
2017-05-13 08:11:59 -04:00
|
|
|
help = 'The file to create/validate. If not specified, defaults to ./aif.xml',
|
2017-05-07 23:50:11 -04:00
|
|
|
default = '{0}/aif.xml'.format(os.getcwd()))
|
|
|
|
subparsers = args.add_subparsers(help = 'Operation to perform',
|
|
|
|
dest = 'oper')
|
|
|
|
createargs = subparsers.add_parser('create',
|
|
|
|
help = 'Create an AIF-NG XML configuration file.',
|
|
|
|
parents = [commonargs])
|
|
|
|
validateargs = subparsers.add_parser('validate',
|
|
|
|
help = 'Validate an AIF-NG XML configuration file.',
|
|
|
|
parents = [commonargs])
|
2017-05-11 23:27:22 -04:00
|
|
|
convertargs = subparsers.add_parser('convert',
|
|
|
|
help = 'Convert a "more" human-readable JSON configuration file to AIF-NG-compatible XML.',
|
|
|
|
parents = [commonargs])
|
2017-05-09 08:17:07 -04:00
|
|
|
createargs.add_argument('-v',
|
|
|
|
'--verbose',
|
|
|
|
dest = 'verbose',
|
|
|
|
action = 'store_true',
|
|
|
|
help = 'Print the dict of raw values used to create the XML. Mostly/only useful for debugging.')
|
2017-05-14 01:21:15 -04:00
|
|
|
createargs.add_argument('-v:r',
|
|
|
|
'--verbose-raw',
|
|
|
|
dest = 'verbose_raw',
|
|
|
|
action = 'store_true',
|
|
|
|
help = 'Like -v, but prints the unformatted dict.')
|
2017-05-11 23:27:22 -04:00
|
|
|
convertargs.add_argument('-i',
|
|
|
|
'--input',
|
|
|
|
dest = 'inputfile',
|
|
|
|
required = True,
|
|
|
|
help = 'The JSON file to import and convert into XML.')
|
2017-05-07 23:50:11 -04:00
|
|
|
return(args)
|
|
|
|
|
|
|
|
def verifyArgs(args):
|
|
|
|
args['cfgfile'] = os.path.normpath(os.path.abspath(os.path.expanduser(args['cfgfile'])))
|
|
|
|
args['cfgfile'] = re.sub('^/+', '/', args['cfgfile'])
|
|
|
|
# Path/file handling - make sure we can create the parent dir if it doesn't exist,
|
|
|
|
# check that we can write to the file, etc.
|
2017-05-11 23:27:22 -04:00
|
|
|
if args['oper'] in ('create', 'convert'):
|
2017-05-07 23:50:11 -04:00
|
|
|
args['cfgbak'] = '{0}.bak.{1}'.format(args['cfgfile'], int(datetime.datetime.utcnow().timestamp()))
|
|
|
|
try:
|
|
|
|
temp = True
|
|
|
|
if os.path.lexists(args['cfgfile']):
|
|
|
|
temp = False
|
|
|
|
os.makedirs(os.path.dirname(args['cfgfile']), exist_ok = True)
|
|
|
|
with open(args['cfgfile'], 'a') as f:
|
|
|
|
f.write('')
|
|
|
|
if temp:
|
|
|
|
os.remove(args['cfgfile'])
|
|
|
|
except OSError as e:
|
|
|
|
print('\nERROR: {0}: {1}'.format(e.strerror, e.filename))
|
|
|
|
exit(('\nWe encountered an error when trying to use path {0}.\n' +
|
|
|
|
'Please review the output and address any issues present.').format(args['cfgfile']))
|
2017-05-11 23:27:22 -04:00
|
|
|
if args['oper'] == 'convert':
|
|
|
|
# And we need to make sure we have read perms to the JSON input file.
|
|
|
|
try:
|
|
|
|
with open(args['inputfile'], 'r') as f:
|
|
|
|
f.read()
|
|
|
|
except OSError as e:
|
|
|
|
print('\nERROR: {0}: {1}'.format(e.strerror, e.filename))
|
|
|
|
exit(('\nWe encountered an error when trying to read path {0}.\n' +
|
|
|
|
'Please review the output and address any issues present.').format(args['inputfile']))
|
2017-05-07 23:50:11 -04:00
|
|
|
return(args)
|
|
|
|
|
|
|
|
def main():
|
|
|
|
args = vars(parseArgs().parse_args())
|
|
|
|
if not args['oper']:
|
|
|
|
parseArgs().print_help()
|
|
|
|
else:
|
|
|
|
aif = aifgen(verifyArgs(args))
|
2017-05-14 01:21:15 -04:00
|
|
|
aif.main()
|
2017-05-07 23:50:11 -04:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2017-05-11 18:46:36 -04:00
|
|
|
main()
|