diff --git a/TODO b/TODO
index f892a54..e031ef4 100644
--- a/TODO
+++ b/TODO
@@ -1,6 +1,9 @@
- config layout
-- 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 )
-- actually, might want to use parted --script instead? then we can do percentages. https://www.gnu.org/software/parted/manual/parted.html
diff --git a/aif.xml b/aif.xml
index 47114f4..855abea 100644
--- a/aif.xml
+++ b/aif.xml
@@ -5,15 +5,17 @@
-
+
+
+
-
+
- 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.
@@ -45,7 +45,7 @@
- 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.
@@ -157,6 +157,7 @@
+
@@ -250,6 +251,7 @@
+
diff --git a/aifclient.py b/aifclient.py
index d8732cd..7b12ac4 100755
--- a/aifclient.py
+++ b/aifclient.py
@@ -1,5 +1,15 @@
#!/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:
from lxml import etree
lxml_avail = True
@@ -205,7 +215,8 @@ class aif(object):
aifdict['system']['timezone'] = False
aifdict['system']['locale'] = 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:
aifdict['system'][i] = xmlobj.find('system').attrib[i]
# And now services...
@@ -332,20 +343,187 @@ class archInstall(object):
for p in cmds:
subprocess.call(p, stdout = DEVNULL, stderr = subprocess.STDOUT)
- def mount(self):
- pass
+ def mounts(self):
+ 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):
install = archInstall(confdict)
#install.format()
- install.mount()
+ #install.mounts()
+ ##chrootcmds = install.setup()
+ ##install.chroot(chrootcmds)
+ #install.chroot()
+ #install.unmount()
def main():
if os.getuid() != 0:
exit('This must be run as root.')
conf = aif()
+ import pprint
instconf = conf.buildDict()
+ pprint.pprint(instconf)
runInstall(instconf)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()