From 7330579d9ded971b3147e5b81b9ae9ebc6eb5ef9 Mon Sep 17 00:00:00 2001 From: brent s Date: Wed, 26 Apr 2017 04:55:34 -0400 Subject: [PATCH] basic bootloader support (grub and systemd-boot), script support. untested, as per usual. --- TODO | 2 + aif.xsd | 3 +- aifclient.py | 207 +++++++++++++++++++++++++++++++++++++++++++++++---- sampledict | 77 +++++++++++++++++++ 4 files changed, 274 insertions(+), 15 deletions(-) create mode 100644 sampledict 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.'}}}