diff --git a/TODO b/TODO
index e031ef4..45f8d50 100644
--- a/TODO
+++ b/TODO
@@ -19,6 +19,8 @@ would yield the *client* sending info via URL params, e.g.
parser: make sure to use https://mikeknoop.com/lxml-xxe-exploit/ fix
+left off at network config- i think i just have software/packages/etc. next, unless i already did that
+
docs:
http://lxml.de/parsing.html
diff --git a/aif.xsd b/aif.xsd
index 085c3fa..cbb98ef 100644
--- a/aif.xsd
+++ b/aif.xsd
@@ -111,7 +111,7 @@
-
+
@@ -321,6 +321,7 @@
+
diff --git a/aifclient.py b/aifclient.py
index 39d0612..9f97d1a 100755
--- a/aifclient.py
+++ b/aifclient.py
@@ -17,6 +17,7 @@ except ImportError:
import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html
lxml_avail = False
import shlex
+import fileinput
import os
import re
import socket
@@ -104,6 +105,49 @@ class aif(object):
exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix))
return(conf)
+ def webFetch(self, uri, auth = False):
+ # Sanitize the user specification and find which protocol to use
+ prefix = uri.split(':')[0].lower()
+ # 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)
+
def getXML(self, confobj = False):
if not confobj:
confobj = self.getConfig()
@@ -117,11 +161,13 @@ class aif(object):
aifdict = {}
for i in ('disk', 'mount', 'network', 'system', 'users', 'software', 'scripts'):
aifdict[i] = {}
- for i in ('network.ifaces', 'system.bootloader', 'system.services', 'users.root', 'scripts.pre', 'scripts.post'):
+ for i in ('network.ifaces', 'system.bootloader', 'system.services', 'users.root'):
i = i.split('.')
dictname = i[0]
keyname = i[1]
aifdict[dictname][keyname] = {}
+ aifdict['scripts']['pre'] = False
+ aifdict['scripts']['post'] = False
aifdict['users']['root']['password'] = False
for i in ('repos', 'mirrors', 'packages'):
aifdict['software'][i] = {}
@@ -278,6 +324,11 @@ class aif(object):
# The bootloader setup...
for x in xmlobj.find('bootloader').attrib:
aifdict['system']['bootloader'][x] = xmlobj.find('bootloader').attrib[x]
+ # The script setup...
+ for x in xmlobj.find('scripts'):
+ scripttype =
+ if not aifdict['scripts'][scripttype]:
+ aifdict['scripts'][scripttype] = {}
return(aifdict)
class archInstall(object):
@@ -442,6 +493,7 @@ class archInstall(object):
hostscript.append(['timedatectl', 'set-ntp', 'true'])
hostscript.append(['pacstrap', self.system['chrootpath'], 'base'])
with open('{0}/etc/fstab'.format(self.system['chrootpath']), 'a') as f:
+ f.write('# Generated by AIF-NG.\n')
f.write(chrootfstab.decode('utf-8'))
# Validating this would be better with pytz, but it's not stdlib. dateutil would also work, but same problem.
# https://stackoverflow.com/questions/15453917/get-all-available-timezones
@@ -476,20 +528,24 @@ class archInstall(object):
if v.startswith('#{0}'.format(x)):
localeraw[i] = x + '\n'
with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'w') as f:
+ f.write('# Modified by AIF-NG.\n')
f.write(''.join(localeraw))
with open('{0}/etc/locale.conf', 'a') as f:
+ f.write('# Added by AIF-NG.\n')
f.write('LANG={0}\n'.format(locale[0].split()[0]))
chrootcmds.append(['locale-gen'])
# Set up the kbd layout.
# Currently there is NO validation on this. TODO.
if self.system['kbd']:
with open('{0}/etc/vconsole.conf'.format(self.system['chrootpath']), 'a') as f:
- f.write('KEYMAP={0}\n'.format(self.system['kbd']))
+ f.write('# Generated by AIF-NG.\nKEYMAP={0}\n'.format(self.system['kbd']))
# Set up the hostname.
with open('{0}/etc/hostname'.format(self.system['chrootpath']), 'w') as f:
+ f.write('# Generated by AIF-NG.\n')
f.write(self.network['hostname'] + '\n')
with open('{0}/etc/hosts'.format(self.system['chrootpath']), 'a') as f:
- f.write('127.0.0.1\t{0}\t{1}\n'.format(self.network['hostname'], (self.network['hostname']).split('.')[0]))
+ f.write('# Added by AIF-NG.\n127.0.0.1\t{0}\t{1}\n'.format(self.network['hostname'],
+ (self.network['hostname']).split('.')[0]))
# Set up networking.
ifaces = []
# Ideally we'd find a better way to do... all of this. Patches welcome. TODO.
@@ -553,8 +609,10 @@ class archInstall(object):
filename = '{0}/etc/netctl/{1}'.format(self.system['chrootpath'], ifacedev)
# The good news is since it's a clean install, we only have to account for our own data, not pre-existing.
with open(filename, 'w') as f:
+ f.write('# Generated by AIF-NG.\n')
f.write(netprofile)
with open('{0}/etc/systemd/system/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev)) as f:
+ f.write('# Generated by AIF-NG.\n')
f.write(('.include /usr/lib/systemd/system/netctl@.service\n\n[Unit]\n' +
'Description=A basic {0} ethernet connection\n' +
'BindsTo=sys-subsystem-net-devices-{1}.device\n' +
@@ -563,30 +621,152 @@ class archInstall(object):
'{0}/etc/systemd/system/multi-user.target.wants/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev))
os.symlink('/usr/lib/systemd/system/netctl.service',
'{0}/etc/systemd/system/multi-user.target.wants/netctl.service'.format(self.system['chrootpath']))
+ # Root password
+ if self.users['root']['password']:
+ roothash = self.users['root']['password']
+ else:
+ roothash = '!'
+ with fileinput.input('{0}/etc/shadow'.format(self.system['chrootpath']), inplace = True) as f:
+ for line in f:
+ linelst = line.split(':')
+ if linelst[0] == 'root':
+ linelst[1] = roothash
+ print(':'.join(linelst), end = '')
+ # Add users
+ for user in self.users.keys():
+ # We already handled root user
+ if user != 'root':
+ cmd = ['useradd']
+ if self.users[user]['home']['create']:
+ cmd.append('-m')
+ if self.users[user]['home']['path']:
+ cmd.append('-d {0}'.format(self.users[user]['home']['path']))
+ if self.users[user]['comment']:
+ cmd.append('-c "{0}"'.format(self.users[user]['comment']))
+ if self.users[user]['gid']:
+ cmd.append('-g {0}'.format(self.users[user]['gid']))
+ if self.users[user]['uid']:
+ cmd.append('-u {0}'.format(self.users[user]['uid']))
+ if self.users[user]['password']:
+ cmd.append('-p "{0}"'.format(self.users[user]['password']))
+ cmd.append(user)
+ chrootcmds.append(cmd)
+ # Add groups
+ if self.users[user]['xgroup']:
+ for group in self.users[user]['xgroup'].keys():
+ gcmd = False
+ if self.users[user]['xgroup'][group]['create']:
+ gcmd = ['groupadd']
+ if self.users[user]['xgroup'][group]['gid']:
+ gcmd.append('-g {0}'.format(self.users[user]['xgroup'][group]['gid']))
+ gcmd.append(group)
+ chrootcmds.append(gcmd)
+ chrootcmds.append(['usermod', '-aG', '{0}'.format(','.join(self.users[user]['xgroup'].keys())), user])
+ # Handle sudo
+ if self.users[user]['sudo']:
+ os.makedirs('{0}/etc/sudoers.d'.format(self.system['chrootpath']), exist_ok = True)
+ os.chmod('{0}/etc/sudoers.d'.format(self.system['chrootpath']), 0o750)
+ with open('{0}/etc/sudoers.d/{1}'.format(self.system['chrootpath'], user), 'w') as f:
+ f.write('# Generated by AIF-NG.\nDefaults:{0} !lecture\n{0} ALL=(ALL) ALL\n'.format(user))
# Base configuration- initcpio, etc.
chrootcmds.append(['mkinitcpio', '-p', 'linux'])
+ # Run the basic host prep
with open(os.devnull, 'w') as DEVNULL:
for c in hostscript:
subprocess.call(c, stdout = DEVNULL, stderr = subprocess.STDOUT)
return(chrootcmds)
- def chroot(self, chrootcmds = False):
+ def bootloader(self):
+ # Bootloader configuration
+ btldr = self.system['bootloader']['type']
+ bootcmds = []
+ chrootpath = self.system['chrootpath']
+ bttarget = self.system['bootloader']['target']
+ if btldr == 'grub':
+ bootcmds.append(['grub-install'])
+ if self.system['bootloader']['efi']:
+ bootcmds[0].extend(['--target=x86_64-efi', '--efi-directory={0}'.format(bttarget), '--bootloader-id="Arch Linux"'])
+ else:
+ bootcmds[0].extend(['--target=i386-pc', bttarget])
+ bootcmds.append(['grub-mkconfig', '-o', '/{0}/grub/grub.cfg'.format(chrootpath, bttarget)])
+ if btldr == 'systemd':
+ if self.system['bootloader']['target'] != '/boot':
+ shutil.copy2('{0}/boot/vmlinuz-linux'.format(chrootpath),
+ '{0}/{1}/vmlinuz-linux'.format(chrootpath, bttarget))
+ shutil.copy2('{0}/boot/initramfs-linux.img'.format(chrootpath),
+ '{0}/{1}/initramfs-linux.img'.format(chrootpath, bttarget))
+ with open('{0}/{1}/loader/loader.conf'.format(chrootpath, bttarget), 'w') as f:
+ f.write('# Generated by AIF-NG.\ndefault arch\ntimeout 4\neditor 0\n')
+ # Gorram, I wish there was a better way to get the partition UUID in stdlib.
+ majmindev = os.lstat('{0}/{1}'.format(chrootpath, bttarget)).st_dev
+ majdev = os.major(majmindev)
+ mindev = os.minor(majmindev)
+ btdev = os.path.basename(os.readlink('/sys/dev/block/{0}:{1}'.format(majdev, mindev)))
+ partuuid = False
+ for d in os.listdir('/dev/disk/by-uuid'):
+ linktarget = os.path.basename(os.readlink(d))
+ if linktarget == btdev:
+ partuuid = linktarget
+ break
+ if not partuuid:
+ exit('ERROR: Cannot determine PARTUUID for /dev/{0}.'.format(btdev))
+ with open('{0}/{1}/loader/entries/arch.conf'.format(chrootpath, bttarget)) as f:
+ f.write(('# Generated by AIF-NG.\ntitle\t\tArch Linux\nlinux /vmlinuz-linux\n') +
+ ('initrd /initramfs-linux.img\noptions root=PARTUUID={0} rw\n').format(partuuid))
+ bootcmds.append(['bootctl', '--path={0}', 'install'])
+ return(bootcmds)
+
+ def scriptcmds(self):
+ if xmlobj.find('scripts') is not None:
+ self.scripts['pre'] = []
+ self.scripts['post'] = []
+ tempscriptdict = {'pre': {}, 'post': {}}
+ for x in xmlobj.find('scripts'):
+ if all(keyname in list(x.attrib.keys()) for keyname in ('user', 'password')):
+ auth = {}
+ auth['user'] = x.attrib['user']
+ auth['password'] = x.attrib['password']
+ if 'realm' in x.attrib.keys():
+ auth['realm'] = x.attrib['realm']
+ if 'authtype' in x.attrib.keys():
+ auth['type'] = x.attrib['authtype']
+ scriptcontents = self.webFetch(x.attrib['uri']).decode('utf-8')
+ else:
+ scriptcontents = self.webFetch(x.attrib['uri']).decode('utf-8')
+ if x.attrib['bootstrap'].lower() in ('true', '1'):
+ tempscriptdict['pre'][x.attrib['order']] = scriptcontents
+ else:
+ tempscriptdict['post'][x.attrib['order']] = scriptcontents
+ for d in ('pre', 'post'):
+ keylst = list(tempscriptdict[d].keys())
+ keylst.sort()
+ for s in keylst:
+ aifdict['scripts'][d].append(tempscriptdict[d][s])
+
+ def chroot(self, chrootcmds = False, bootcmds = False):
if not chrootcmds:
chrootcmds = self.setup()
- chrootscript = """#!/bin/bash
-# https://aif.square-r00t.net/
-
-"""
- with open('{0}/root/aif.sh'.format(self.system['chrootpath']), 'w') as f:
- f.write(chrootscript)
- os.chmod('{0}/root/aif.sh'.format(self.system['chrootpath']), 0o700)
+ if not bootcmds:
+ bootcmds = self.bootloader()
+ # We don't need this currently, but we might down the road.
+ #chrootscript = '#!/bin/bash\n# https://aif.square-r00t.net/\n\n'
+ #with open('{0}/root/aif.sh'.format(self.system['chrootpath']), 'w') as f:
+ # f.write(chrootscript)
+ #os.chmod('{0}/root/aif.sh'.format(self.system['chrootpath']), 0o700)
+ with open('{0}/root/aif-pre.sh'.format(self.system['chrootpath']), 'w') as f:
+ f.write(self.scripts['pre'])
+ with open('{0}/root/aif-post.sh'.format(self.system['chrootpath']), 'w') as f:
+ f.write(self.scripts['post'])
real_root = os.open("/", os.O_RDONLY)
os.chroot(self.system['chrootpath'])
# Does this even work with an os.chroot()? Let's hope so!
with open(os.devnull, 'w') as DEVNULL:
for c in chrootcmds:
subprocess.call(c, stdout = DEVNULL, stderr = subprocess.STDOUT)
- os.system('{0}/root/aif.sh'.format(self.system['chrootpath']))
+ for b in bootcmds:
+ subprocess.call(b, stdout = DEVNULL, stderr = subprocess.STDOUT)
+ os.system('{0}/root/aif-pre.sh'.format(self.system['chrootpath']))
+ #os.system('{0}/root/aif.sh'.format(self.system['chrootpath']))
os.system('{0}/root/aif-post.sh'.format(self.system['chrootpath']))
os.fchdir(real_root)
os.chroot('.')
@@ -602,8 +782,7 @@ def runInstall(confdict):
install = archInstall(confdict)
#install.format()
#install.mounts()
- ##chrootcmds = install.setup()
- ##install.chroot(chrootcmds)
+ #install.bootloader()
#install.chroot()
#install.unmount()
diff --git a/sampledict b/sampledict
new file mode 100644
index 0000000..e576197
--- /dev/null
+++ b/sampledict
@@ -0,0 +1,77 @@
+{'disk': {'/dev/sda': {'fmt': 'gpt',
+ 'parts': {'1': {'fstype': 'ef00',
+ 'num': '1',
+ 'size': '10%',
+ 'start': '0%'},
+ '2': {'fstype': '8300',
+ 'num': '2',
+ 'size': '80%',
+ 'start': '10%'},
+ '3': {'fstype': '8200',
+ 'num': '3',
+ 'size': '10%',
+ 'start': '80%'}}}},
+ 'mount': {'1': {'device': '/dev/sda2',
+ 'fstype': None,
+ 'mountpt': '/mnt',
+ 'opts': None},
+ '2': {'device': '/dev/sda1',
+ 'fstype': None,
+ 'mountpt': '/mnt/boot',
+ 'opts': None},
+ '3': {'device': '/dev/sda3',
+ 'fstype': None,
+ 'mountpt': 'swap',
+ 'opts': None}},
+ 'network': {'hostname': 'aiftest.square-r00t.net',
+ 'ifaces': {'auto': {'ipv4': {'addresses': ['auto'],
+ 'gw': False,
+ 'resolvers': False},
+ 'resolvers': []}}},
+ 'scripts': {'post': {}, 'pre': {}},
+ 'software': {'mirrors': ['http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch',
+ 'http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch',
+ 'http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch',
+ 'http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch',
+ 'http://arch.mirrors.ionfish.org/$repo/os/$arch',
+ 'http://mirrors.gigenet.com/archlinux/$repo/os/$arch',
+ 'http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch'],
+ 'packages': {'sed': {'repo': 'core'}},
+ 'repos': {'archlinuxfr': {'enabled': False,
+ 'mirror': 'http://repo.archlinux.fr/$arch',
+ 'siglevel': 'Optional TrustedOnly'},
+ 'community': {'enabled': True,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'},
+ 'core': {'enabled': True,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'},
+ 'extra': {'enabled': True,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'},
+ 'multilib': {'enabled': True,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'},
+ 'multilib-testing': {'enabled': False,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'},
+ 'testing': {'enabled': False,
+ 'mirror': 'file:///etc/pacman.d/mirrorlist',
+ 'siglevel': 'default'}}},
+ 'system': {'bootloader': {'efi': 'true', 'target': '/boot', 'type': 'grub'},
+ 'chrootpath': '/mnt',
+ 'kbd': False,
+ 'locale': 'en_US.UTF-8',
+ 'services': False,
+ 'timezone': 'EST5EDT'},
+ 'users': {'aifusr': {'comment': 'A test user for AIF.',
+ 'gid': None,
+ 'group': None,
+ 'home': {'create': True, 'path': '/opt/aifusr'},
+ 'password': '$6$WtxZKOyaahvvWQRG$TUys60kQhF0ffBdnDSJVTA.PovwCOajjMz8HEHL2H0ZMi0bFpDTQvKA7BqzM3nA.ZMAUxNjpJP1dG/eA78Zgw0',
+ 'sudo': True,
+ 'uid': None,
+ 'xgroup': {'admins': {'create': True, 'gid': False},
+ 'users': {'create': False, 'gid': False},
+ 'wheel': {'create': False, 'gid': False}}},
+ 'root': {'password': '$6$3YPpiS.l3SQC6ELe$NQ4qMvcDpv5j1cCM6AGNc5Hyg.rsvtzCt2VWlSbuZXCGg2GB21CMUN8TMGS35tdUezZ/n9y3UFGlmLRVWXvZR.'}}}