still need to set up users, software, chroot stuff, network..
This commit is contained in:
parent
2ed67d2401
commit
2bc7003b21
3
TODO
3
TODO
@ -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
|
||||||
|
6
aif.xml
6
aif.xml
@ -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"
|
||||||
|
6
aif.xsd
6
aif.xsd
@ -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>
|
||||||
|
188
aifclient.py
188
aifclient.py
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user