still need to set up users, software, chroot stuff, network..

This commit is contained in:
brent s 2017-03-21 15:13:15 -04:00
parent 2ed67d2401
commit 2bc7003b21
4 changed files with 194 additions and 9 deletions

3
TODO
View File

@ -1,6 +1,9 @@
- config layout - config layout
-- need to apply defaults and annotate/document -- need to apply defaults and annotate/document


find out where to run aif-pre.sh (runs on host) (rename to aif-pre.script)
and get a way to insert that and aif-post(.script) from the kernel params, etc.
remember to uncomment the functions in main() when ready to test


- use sgdisk? scripting (generated by python) for disk partitioning (part types listed at http://www.rodsbooks.com/gdisk/walkthrough.html ) - use sgdisk? scripting (generated by python) for disk partitioning (part types listed at http://www.rodsbooks.com/gdisk/walkthrough.html )
-- actually, might want to use parted --script instead? then we can do percentages. https://www.gnu.org/software/parted/manual/parted.html -- actually, might want to use parted --script instead? then we can do percentages. https://www.gnu.org/software/parted/manual/parted.html

View File

@ -5,15 +5,17 @@
<storage> <storage>
<disk device="/dev/sda" diskfmt="gpt"> <disk device="/dev/sda" diskfmt="gpt">
<part num="1" start="0%" size="10%" fstype="ef00" /> <part num="1" start="0%" size="10%" fstype="ef00" />
<part num="2" start="10%" size="90%" fstype="8300" /> <part num="2" start="10%" size="80%" fstype="8300" />
<part num="3" start="80%" size="10%" fstype="8200" />
</disk> </disk>
<mount source="/dev/sda2" target="/mnt" order="1" /> <mount source="/dev/sda2" target="/mnt" order="1" />
<mount source="/dev/sda1" target="/mnt/boot" order="2" /> <mount source="/dev/sda1" target="/mnt/boot" order="2" />
<mount source="/dev/sda3" target="swap" order="3" />
</storage> </storage>
<network hostname="aiftest.square-r00t.net"> <network hostname="aiftest.square-r00t.net">
<iface device="auto" address="auto" netproto="ipv4" /> <iface device="auto" address="auto" netproto="ipv4" />
</network> </network>
<system timezone="EST5EDT" locale="en_US.UTF-8"> <system timezone="EST5EDT" locale="en_US.UTF-8" chrootpath="/mnt">
<!-- note: all password hashes below are "test"; don't waste your time trying to crack. :) --> <!-- note: all password hashes below are "test"; don't waste your time trying to crack. :) -->
<users rootpass="$6$3YPpiS.l3SQC6ELe$NQ4qMvcDpv5j1cCM6AGNc5Hyg.rsvtzCt2VWlSbuZXCGg2GB21CMUN8TMGS35tdUezZ/n9y3UFGlmLRVWXvZR."> <users rootpass="$6$3YPpiS.l3SQC6ELe$NQ4qMvcDpv5j1cCM6AGNc5Hyg.rsvtzCt2VWlSbuZXCGg2GB21CMUN8TMGS35tdUezZ/n9y3UFGlmLRVWXvZR.">
<user name="aifusr" <user name="aifusr"

View File

@ -23,7 +23,7 @@
<xs:simpleType name="diskfmt"> <xs:simpleType name="diskfmt">
<xs:annotation> <xs:annotation>
<xs:documentation> <xs:documentation>
This element specifies a type to validate what kind of disk formatting. Accept either GPT or BIOS only. This element specifies a type to validate what kind of disk formatting. Accepts either GPT or BIOS (for MBR systems) only.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:restriction base="xs:string"> <xs:restriction base="xs:string">
@ -45,7 +45,7 @@
<xs:simpleType name="fstype"> <xs:simpleType name="fstype">
<xs:annotation> <xs:annotation>
<xs:documentation> <xs:documentation>
This element validates a filesystem type to be specified for formatting a partition. Recommended is ext3, ext4, btrfs, vfat (for UEFI ESP). This element validates a filesystem type to be specified for formatting a partition. See sgdisk -L (or the table at http://www.rodsbooks.com/gdisk/walkthrough.html) for valid filesystem codes.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
@ -157,6 +157,7 @@
<xs:attribute name="target" type="xs:token" use="required" /> <xs:attribute name="target" type="xs:token" use="required" />
<xs:attribute name="fstype" type="fstype" /> <xs:attribute name="fstype" type="fstype" />
<xs:attribute name="opts" type="mntopts" /> <xs:attribute name="opts" type="mntopts" />
<xs:attribute name="swap" type="xs:bool" />
</xs:complexType> </xs:complexType>
<xs:unique name="unique-mnts"> <xs:unique name="unique-mnts">
<xs:selector xpath="mount" /> <xs:selector xpath="mount" />
@ -250,6 +251,7 @@
</xs:sequence> </xs:sequence>
<xs:attribute name="timezone" type="xs:string" use="required" /> <xs:attribute name="timezone" type="xs:string" use="required" />
<xs:attribute name="locale" type="xs:string" use="required" /> <xs:attribute name="locale" type="xs:string" use="required" />
<xs:attribute name="chrootpath" type="xs:string" user="required" />
<xs:attribute name="kbd" type="xs:token" /> <xs:attribute name="kbd" type="xs:token" />
</xs:complexType> </xs:complexType>
</xs:element> </xs:element>

View File

@ -1,5 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3


## REQUIRES: ##
# parted
# sgdisk (yes, both)
# python 3 with standard library
# (OPTIONAL) lxml
# pacman in the host environment
# arch-install-scripts: https://www.archlinux.org/packages/extra/any/arch-install-scripts/
# a network connection
# the proper kernel arguments.

try: try:
from lxml import etree from lxml import etree
lxml_avail = True lxml_avail = True
@ -205,7 +215,8 @@ class aif(object):
aifdict['system']['timezone'] = False aifdict['system']['timezone'] = False
aifdict['system']['locale'] = False aifdict['system']['locale'] = False
aifdict['system']['kbd'] = False aifdict['system']['kbd'] = False
for i in ('locale', 'timezone', 'kbd'): aifdict['system']['chrootpath'] = False
for i in ('locale', 'timezone', 'kbd', 'chrootpath'):
if i in xmlobj.find('system').attrib: if i in xmlobj.find('system').attrib:
aifdict['system'][i] = xmlobj.find('system').attrib[i] aifdict['system'][i] = xmlobj.find('system').attrib[i]
# And now services... # And now services...
@ -332,20 +343,187 @@ class archInstall(object):
for p in cmds: for p in cmds:
subprocess.call(p, stdout = DEVNULL, stderr = subprocess.STDOUT) subprocess.call(p, stdout = DEVNULL, stderr = subprocess.STDOUT)


def mount(self): def mounts(self):
pass mntorder = list(self.mount.keys())
mntorder.sort()
for m in mntorder:
mnt = self.mount[m]
if mnt['mountpt'].lower() == 'swap':
cmd = ['swapon', mnt['device']]
else:
cmd = ['mount', mnt['device'], mnt['mountpt']]
if mnt['opts']:
cmd.insert(1, '-o {0}'.format(mnt['opts']))
if mnt['fstype']:
cmd.insert(1, '-t {0}'.format(mnt['fstype']))
# with open(os.devnull, 'w') as DEVNULL:
# for p in cmd:
# subprocess.call(p, stdout = DEVNULL, stderr = subprocess.STDOUT)
# And we need to add some extra mounts to support a chroot. We also need to know what was mounted before.
with open('/proc/mounts', 'r') as f:
procmounts = f.read()
mountlist = {}
for i in procmounts.splitlines():
mountlist[i.split()[1]] = i
cmounts = {}
for m in ('chroot', 'resolv', 'proc', 'sys', 'efi', 'dev', 'pts', 'shm', 'run', 'tmp'):
cmounts[m] = None
chrootdir = self.system['chrootpath']
# chroot (bind mount... onto itself. it's so stupid, i know. see https://bugs.archlinux.org/task/46169)
if chrootdir not in mountlist.keys():
cmounts['chroot'] = ['mount', '--bind', chrootdir, chrootdir]
# resolv.conf (for DNS resolution in the chroot)
if (chrootdir + '/etc/resolv.conf') not in mountlist.keys():
cmounts['resolv'] = ['/bin/mount', '--bind', '-o', 'ro', '/etc/resolv.conf', chrootdir + '/etc/resolv.conf']
# proc
if (chrootdir + '/proc') not in mountlist.keys():
cmounts['proc'] = ['/bin/mount', '-t', 'proc', '-o', 'nosuid,noexec,nodev', 'proc', chrootdir + '/proc']
# sys
if (chrootdir + '/sys') not in mountlist.keys():
cmounts['sys'] = ['/bin/mount', '-t', 'sysfs', '-o', 'nosuid,noexec,nodev,ro', 'sys', chrootdir + '/sys']
# efi (if it exists on the host)
if '/sys/firmware/efi/efivars' in mountlist.keys():
if (chrootdir + '/sys/firmware/efi/efivars') not in mountlist.keys():
cmounts['efi'] = ['/bin/mount', '-t', 'efivarfs', '-o', 'nosuid,noexec,nodev', 'efivarfs', chrootdir + '/sys/firmware/efi/efivars']
# dev
if (chrootdir + '/dev') not in mountlist.keys():
cmounts['dev'] = ['/bin/mount', '-t', 'devtmpfs', '-o', 'mode=0755,nosuid', 'udev', chrootdir + '/dev']
# pts
if (chrootdir + '/dev/pts') not in mountlist.keys():
cmounts['pts'] = ['/bin/mount', '-t', 'devpts', '-o', 'mode=0620,gid=5,nosuid,noexec', 'devpts', chrootdir + '/dev/pts']
# shm (if it exists on the host)
if '/dev/shm' in mountlist.keys():
if (chrootdir + '/dev/shm') not in mountlist.keys():
cmounts['shm'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'mode=1777,nosuid,nodev', 'shm', chrootdir + '/dev/shm']
# run (if it exists on the host)
if '/run' in mountlist.keys():
if (chrootdir + '/run') not in mountlist.keys():
cmounts['run'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'nosuid,nodev,mode=0755', 'run', chrootdir + '/run']
# tmp (if it exists on the host)
if '/tmp' in mountlist.keys():
if (chrootdir + '/tmp') not in mountlist.keys():
cmounts['tmp'] = ['/bin/mount', '-t', 'tmpfs', '-o', 'mode=1777,strictatime,nodev,nosuid', 'tmp', chrootdir + '/tmp']
# Because the order of these mountpoints is so ridiculously important, we hardcode it. Yeah, python 3.6 has ordered dicts, but do we really want to risk it?
with open(os.devnull, 'w') as DEVNULL:
for m in ('chroot', 'resolv', 'proc', 'sys', 'efi', 'dev', 'pts', 'shm', 'run', 'tmp'):
if cmounts[m]:
subprocess.call(cmounts[m], stdout = DEVNULL, stderr = subprocess.STDOUT)
# Okay. So we finally have all the mounts bound. Whew.
def setup(self):
# TODO: could we leverage https://github.com/hartwork/image-bootstrap somehow? I want to keep this close
# to standard Python libs, though, to reduce dependency requirements.
hostscript = []
chrootcmds = []
locales = []
locale = []
# Get the necessary fstab additions for the guest
chrootfstab = subprocess.check_output(['genfstab', '-U', self.system['chrootpath']])
# Set up the time, and then kickstart the guest install.
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(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
tzlist = subprocess.check_output(['timedatectl', 'list-timezones']).decode('utf-8').splitlines()
if self.system['timezone'] not in tzlist:
print('WARNING (non-fatal): {0} does not seem to be a valid timezone, but we\'re continuing anyways.'.format(self.system['timezone']))
os.symlink('/usr/share/zoneinfo/{0}'.format(self.system['timezone']),
'{0}/etc/localtime'.format(self.system['chrootpath']))
# This is an ugly hack. TODO: find a better way of determining if the host is set to UTC in the RTC. maybe the datetime module can do it.
utccheck = subprocess.check_output(['timedatectl', 'status']).decode('utf-8').splitlines()
utccheck = [x.strip(' ') for x in utccheck]
for i, v in enumerate(utccheck):
if v.startswith('RTC in local'):
utcstatus = (v.split(': ')[1]).lower() in ('yes')
break
if utcstatus:
chrootcmds.append(['hwclock', '--systohc'])
# We need to check the locale, and set up locale.gen.
with open('{0}/etc/locale.gen'.format(self.system['chrootpath']), 'r') as f:
localeraw = f.readlines()
for line in localeraw:
if not line.startswith('# '): # Comments, thankfully, have a space between the leading octothorpe and the comment. Locales have no space.
i = line.strip().strip('#')
if i != '': # We also don't want blank entries. Keep it clean, folks.
locales.append(i)
for i in locales:
localelst = i.split()
if localelst[0].lower().startswith(self.system['locale'].lower()):
locale.append(' '.join(localelst).strip())
for i, v in enumerate(localeraw):
for x in locale:
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(''.join(localeraw))
with open('{0}/etc/locale.conf', 'a') as f:
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']))
# Set up the hostname.
with open('{0}/etc/hostname'.format(self.system['chrootpath']), 'w') as f:
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]))
# SET UP NETWORKING HERE
########################
chrootcmds.append(['mkinitcpio', '-p', 'linux'])
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):
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)
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']))
os.system('{0}/root/aif-post.sh'.format(self.system['chrootpath']))
os.fchdir(real_root)
os.chroot('.')
os.close(real_root)
if not os.path.isfile('{0}/sbin/init'.format(chrootdir)):
os.symlink('../lib/systemd/systemd', '{0}/sbin/init'.format(chrootdir))
def unmount(self):
with open(os.devnull, 'w') as DEVNULL:
subprocess.call(['unmount', '-lR', self.system['chrootpath']], stdout = DEVNULL, stderr = subprocess.STDOUT)
def runInstall(confdict): def runInstall(confdict):
install = archInstall(confdict) install = archInstall(confdict)
#install.format() #install.format()
install.mount() #install.mounts()
##chrootcmds = install.setup()
##install.chroot(chrootcmds)
#install.chroot()
#install.unmount()


def main(): def main():
if os.getuid() != 0: if os.getuid() != 0:
exit('This must be run as root.') exit('This must be run as root.')
conf = aif() conf = aif()
import pprint
instconf = conf.buildDict() instconf = conf.buildDict()
pprint.pprint(instconf)
runInstall(instconf) runInstall(instconf)


if __name__ == "__main__": if __name__ == "__main__":
main() main()