basic bootloader support (grub and systemd-boot), script support. untested, as per usual.
This commit is contained in:
parent
20b5850a94
commit
7330579d9d
2
TODO
2
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
|
||||
|
3
aif.xsd
3
aif.xsd
@ -111,7 +111,7 @@
|
||||
|
||||
<xs:simpleType name="bootloaders">
|
||||
<xs:restriction base="xs:token">
|
||||
<xs:pattern value="(grub|lilo|syslinux)" />
|
||||
<xs:pattern value="(grub|systemd|syslinux)" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
@ -321,6 +321,7 @@
|
||||
<xs:attribute name="uri" type="scripturi" use="required" />
|
||||
<xs:attribute name="lang" type="devlang" />
|
||||
<xs:attribute name="order" type="xs:integer" use="required" />
|
||||
<xs:attribute name="bootstrap" type="xs:boolean" use="required" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
|
207
aifclient.py
207
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()
|
||||
|
||||
|
77
sampledict
Normal file
77
sampledict
Normal 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.'}}}
|
Loading…
Reference in New Issue
Block a user