basic bootloader support (grub and systemd-boot), script support. untested, as per usual.

This commit is contained in:
brent s 2017-04-26 04:55:34 -04:00
parent 20b5850a94
commit 7330579d9d
4 changed files with 274 additions and 15 deletions

2
TODO
View File

@ -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 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: docs:
http://lxml.de/parsing.html http://lxml.de/parsing.html

View File

@ -111,7 +111,7 @@
<xs:simpleType name="bootloaders"> <xs:simpleType name="bootloaders">
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:pattern value="(grub|lilo|syslinux)" /> <xs:pattern value="(grub|systemd|syslinux)" />
</xs:restriction> </xs:restriction>
</xs:simpleType> </xs:simpleType>
@ -321,6 +321,7 @@
<xs:attribute name="uri" type="scripturi" use="required" /> <xs:attribute name="uri" type="scripturi" use="required" />
<xs:attribute name="lang" type="devlang" /> <xs:attribute name="lang" type="devlang" />
<xs:attribute name="order" type="xs:integer" use="required" /> <xs:attribute name="order" type="xs:integer" use="required" />
<xs:attribute name="bootstrap" type="xs:boolean" use="required" />
</xs:complexType> </xs:complexType>
</xs:element> </xs:element>
</xs:sequence> </xs:sequence>

View File

@ -17,6 +17,7 @@ except ImportError:
import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html import xml.etree.ElementTree as etree # https://docs.python.org/3/library/xml.etree.elementtree.html
lxml_avail = False lxml_avail = False
import shlex import shlex
import fileinput
import os import os
import re import re
import socket 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)) exit('{0} is not a recognised URI type specifier. Must be one of http, https, file, ftp, or ftps.'.format(prefix))
return(conf) 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): def getXML(self, confobj = False):
if not confobj: if not confobj:
confobj = self.getConfig() confobj = self.getConfig()
@ -117,11 +161,13 @@ class aif(object):
aifdict = {} aifdict = {}
for i in ('disk', 'mount', 'network', 'system', 'users', 'software', 'scripts'): for i in ('disk', 'mount', 'network', 'system', 'users', 'software', 'scripts'):
aifdict[i] = {} 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('.') i = i.split('.')
dictname = i[0] dictname = i[0]
keyname = i[1] keyname = i[1]
aifdict[dictname][keyname] = {} aifdict[dictname][keyname] = {}
aifdict['scripts']['pre'] = False
aifdict['scripts']['post'] = False
aifdict['users']['root']['password'] = False aifdict['users']['root']['password'] = False
for i in ('repos', 'mirrors', 'packages'): for i in ('repos', 'mirrors', 'packages'):
aifdict['software'][i] = {} aifdict['software'][i] = {}
@ -278,6 +324,11 @@ class aif(object):
# The bootloader setup... # The bootloader setup...
for x in xmlobj.find('bootloader').attrib: for x in xmlobj.find('bootloader').attrib:
aifdict['system']['bootloader'][x] = xmlobj.find('bootloader').attrib[x] 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) return(aifdict)


class archInstall(object): class archInstall(object):
@ -442,6 +493,7 @@ class archInstall(object):
hostscript.append(['timedatectl', 'set-ntp', 'true']) hostscript.append(['timedatectl', 'set-ntp', 'true'])
hostscript.append(['pacstrap', self.system['chrootpath'], 'base']) hostscript.append(['pacstrap', self.system['chrootpath'], 'base'])
with open('{0}/etc/fstab'.format(self.system['chrootpath']), 'a') as f: 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')) 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. # 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 # https://stackoverflow.com/questions/15453917/get-all-available-timezones
@ -476,20 +528,24 @@ class archInstall(object):
if v.startswith('#{0}'.format(x)): if v.startswith('#{0}'.format(x)):
localeraw[i] = x + '\n' localeraw[i] = x + '\n'
with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'w') as f: with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'w') as f:
f.write('# Modified by AIF-NG.\n')
f.write(''.join(localeraw)) f.write(''.join(localeraw))
with open('{0}/etc/locale.conf', 'a') as f: 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])) f.write('LANG={0}\n'.format(locale[0].split()[0]))
chrootcmds.append(['locale-gen']) chrootcmds.append(['locale-gen'])
# Set up the kbd layout. # Set up the kbd layout.
# Currently there is NO validation on this. TODO. # Currently there is NO validation on this. TODO.
if self.system['kbd']: if self.system['kbd']:
with open('{0}/etc/vconsole.conf'.format(self.system['chrootpath']), 'a') as f: 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. # Set up the hostname.
with open('{0}/etc/hostname'.format(self.system['chrootpath']), 'w') as f: 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') f.write(self.network['hostname'] + '\n')
with open('{0}/etc/hosts'.format(self.system['chrootpath']), 'a') as f: 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. # Set up networking.
ifaces = [] ifaces = []
# Ideally we'd find a better way to do... all of this. Patches welcome. TODO. # 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) 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. # 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: with open(filename, 'w') as f:
f.write('# Generated by AIF-NG.\n')
f.write(netprofile) f.write(netprofile)
with open('{0}/etc/systemd/system/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev)) as f: 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' + f.write(('.include /usr/lib/systemd/system/netctl@.service\n\n[Unit]\n' +
'Description=A basic {0} ethernet connection\n' + 'Description=A basic {0} ethernet connection\n' +
'BindsTo=sys-subsystem-net-devices-{1}.device\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)) '{0}/etc/systemd/system/multi-user.target.wants/netctl@{1}.service'.format(self.system['chrootpath'], ifacedev))
os.symlink('/usr/lib/systemd/system/netctl.service', os.symlink('/usr/lib/systemd/system/netctl.service',
'{0}/etc/systemd/system/multi-user.target.wants/netctl.service'.format(self.system['chrootpath'])) '{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. # Base configuration- initcpio, etc.
chrootcmds.append(['mkinitcpio', '-p', 'linux']) chrootcmds.append(['mkinitcpio', '-p', 'linux'])
# Run the basic host prep
with open(os.devnull, 'w') as DEVNULL: with open(os.devnull, 'w') as DEVNULL:
for c in hostscript: for c in hostscript:
subprocess.call(c, stdout = DEVNULL, stderr = subprocess.STDOUT) subprocess.call(c, stdout = DEVNULL, stderr = subprocess.STDOUT)
return(chrootcmds) 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: if not chrootcmds:
chrootcmds = self.setup() chrootcmds = self.setup()
chrootscript = """#!/bin/bash if not bootcmds:
# https://aif.square-r00t.net/ 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: #with open('{0}/root/aif.sh'.format(self.system['chrootpath']), 'w') as f:
f.write(chrootscript) # f.write(chrootscript)
os.chmod('{0}/root/aif.sh'.format(self.system['chrootpath']), 0o700) #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) real_root = os.open("/", os.O_RDONLY)
os.chroot(self.system['chrootpath']) os.chroot(self.system['chrootpath'])
# Does this even work with an os.chroot()? Let's hope so! # Does this even work with an os.chroot()? Let's hope so!
with open(os.devnull, 'w') as DEVNULL: with open(os.devnull, 'w') as DEVNULL:
for c in chrootcmds: for c in chrootcmds:
subprocess.call(c, stdout = DEVNULL, stderr = subprocess.STDOUT) 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.system('{0}/root/aif-post.sh'.format(self.system['chrootpath']))
os.fchdir(real_root) os.fchdir(real_root)
os.chroot('.') os.chroot('.')
@ -602,8 +782,7 @@ def runInstall(confdict):
install = archInstall(confdict) install = archInstall(confdict)
#install.format() #install.format()
#install.mounts() #install.mounts()
##chrootcmds = install.setup() #install.bootloader()
##install.chroot(chrootcmds)
#install.chroot() #install.chroot()
#install.unmount() #install.unmount()



77
sampledict Normal file
View File

@ -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.'}}}