diff --git a/aif-config.py b/aif-config.py index 82a42d7..b0a5714 100755 --- a/aif-config.py +++ b/aif-config.py @@ -11,6 +11,7 @@ import crypt import datetime import errno import ipaddress +import json import getpass import os import re @@ -176,7 +177,9 @@ class aifgen(object): moreIfaces = False return(ifaces) def genPassHash(user): - passin = getpass.getpass('* Please enter the password you want to use for {0} (will not echo back): '.format(user)) + # https://bugs.python.org/issue30360 - keep this disabled until we're ready for primetime. + #passin = getpass.getpass('* Please enter the password you want to use for {0} (will not echo back): '.format(user)) + passin = input('* Please enter the password you want to use for {0}: '.format(user)) if passin not in ('', '!'): salt = crypt.mksalt(crypt.METHOD_SHA512) salthash = crypt.crypt(passin, salt) @@ -185,15 +188,15 @@ class aifgen(object): return(salthash) def userPrompt(syshelp): users = {} - moreUsers = True - while moreUsers: + moreusers = True + while moreusers: user = chkPrompt('What username would you like to add? ', syshelp) if len(user) > 32: exit(' !! ERROR: Usernames must be less than 32 characters.') if not re.match('^[a-z_][a-z0-9_-]*[$]?$', user): exit(' !! ERROR: Your username does not match a valid pattern. See the man page for useradd (\'CAVEATS\').') users[user] = {} - sudoin = chkPrompt('* Should {0} have (full!) sudo access? (y/{0}n{1}) '.format(user, color.BOLD, color.END), syshelp) + sudoin = chkPrompt('* Should {0} have (full!) sudo access? (y/{1}n{2}) '.format(user, color.BOLD, color.END), syshelp) if re.match('^y(es)?$', sudoin.lower()): users[user]['sudo'] = True else: @@ -214,18 +217,24 @@ class aifgen(object): '(You\'ll be able to add additional groups in a moment.)\n' + '\tThe default, if left blank, is to simply create a group named {0} ' + '(which is what you probably want): ').format(user), syshelp) - if len(grpin) > 32: - exit(' !! ERROR: Group names must be less than 32 characters.') - if not re.match('^[a-z_][a-z0-9_-]*[$]?$', grpin): - exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').') - users[user]['group'] = grpin - gidin = chkPrompt(('* What GID should {0} have? Leave this blank if you don\'t care ' + - '(should be fine for most cases): ').format(grpin), syshelp) - if gidin != '': - try: - users[user]['gid'] = int(gidin) - except: - exit(' !! ERROR: The GID must be an integer.') + if grpin != '': + if len(grpin) > 32: + exit(' !! ERROR: Group names must be less than 32 characters.') + if not re.match('^[a-z_][a-z0-9_-]*[$]?$', grpin): + exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').') + users[user]['group'] = grpin + else: + users[user]['group'] = False + if grpin != '': + gidin = chkPrompt(('* What GID should {0} have? Leave this blank if you don\'t care ' + + '(should be fine for most cases): ').format(grpin), syshelp) + if gidin != '': + try: + users[user]['gid'] = int(gidin) + except: + exit(' !! ERROR: The GID must be an integer.') + else: + users[user]['gid'] = False else: users[user]['gid'] = False syshelp.append('https://aif.square-r00t.net/#code_home_code') @@ -235,13 +244,13 @@ class aifgen(object): if not re.match('^/([^/\x00\s]+(/)?)+)$', homein): exit('!! ERROR: Path {0} does not seem to be valid.'.format(homein)) users[user]['home'] = homein + homecrt = chkPrompt('* Do we need to create {0}? (y/{1}n{2}) '.format(homein, color.BOLD, color.END), syshelp) + if re.match('^y(es)?$', homecrt): + users[user]['homecreate'] = True + else: + users[user]['homecreate'] = False else: users[user]['home'] = False - homecrt = chkPrompt('* Do we need to create {0}? (y/{1}n{2}) '.format(homein, color.BOLD, color.END), syshelp) - if re.match('^y(es)?$', homecrt): - users[user]['homecreate'] = True - else: - users[user]['homecreate'] = False del(syshelp[-1]) xgrouphelp = 'https://aif.square-r00t.net/#code_xgroup_code' if xgrouphelp not in syshelp: @@ -254,29 +263,35 @@ class aifgen(object): morexgroups = False users[user]['xgroups'] = False while morexgroups: - xgrp = chkPrompt('** What is the name of the group you would like to add? ', syshelp) + xgrp = chkPrompt('** What is the name of the group you would like to add to {0}? '.format(user), syshelp) if len(xgrp) > 32: exit(' !! ERROR: Group names must be less than 32 characters.') if not re.match('^[a-z_][a-z0-9_-]*[$]?$', xgrp): exit(' !! ERROR: Your group name does not match a valid pattern. See the man page for groupadd (\'CAVEATS\').') users[user]['xgroups'][xgrp] = {} - xgrpcrt = chkPrompt('** Does {0} need to be created? (y/{1}n{2} '.format(xgrp, color.BOLD, color.END), syshelp) + xgrpcrt = chkPrompt('** Does the group \'{0}\' need to be created? (y/{1}n{2}) '.format(xgrp, color.BOLD, color.END), syshelp) if re.match('^y(es)?$', xgrpcrt.lower()): users[user]['xgroups'][xgrp]['create'] = True + xgrpgid = chkPrompt(('** What GID should {0} be? If the group will already exist on the new system or ' + + 'don\'t care,\nleave this blank (should be fine for most cases): ').format(xgrp), syshelp) + if xgrpgid != '': + try: + users[user]['xgroups'][xgrp]['gid'] = int(xgrpgid) + except: + exit(' !! ERROR: The GID must be an integer.') + else: + users[user]['xgroups'][xgrp]['gid'] = False else: users[user]['xgroups'][xgrp]['create'] = False - xgrpgid = chkPrompt(('** What GID should {0} be? If the group will already exist on the new system or ' + - 'don\'t care,\nleave this blank (should be fine for most cases): ').format(xgrp), syshelp) - if xrpgid != '': - try: - users[user]['xgroups'][xgrp]['gid'] = int(xgrpid) - except: - exit(' !! ERROR: The GID must be an integer.') - else: users[user]['xgroups'][xgrp]['gid'] = False - moreusersin = input('* Would you like to add additional extra groups for {0}? (y/{1}n{2}) '.format(user, color.BOLD, color.END)) - if not re.match('^y(es)?$', moreusersin.lower()): + morexgrpsin = input('* Would you like to add additional extra groups for {0}? (y/{1}n{2}) '.format(user, + color.BOLD, + color.END)) + if not re.match('^y(es)?$', morexgrpsin.lower()): morexgroups = False + moreusersin = chkPrompt('* Would you like to add additional users? (y/{0}n{1}) '.format(color.BOLD, color.END), syshelp) + if not re.match('^y(es)?$', moreusersin.lower()): + moreusers = False return(users) def svcsPrompt(svchelp): svcs = {} @@ -289,7 +304,7 @@ class aifgen(object): if re.match('^no?$', svcstatusin.lower()): svcs[svc] = False else: - svcs[svc] - True + svcs[svc] = True moreservices = input('* Would you like to manage another service? (y/{0}n{1}) '.format(color.BOLD, color.END)) if not re.match('^y(es)?$', moreservices.lower()): moresvcs = False @@ -474,10 +489,10 @@ class aifgen(object): conf = {} print('[{0}] Beginning configuration...'.format(datetime.datetime.now())) print('You may reply with \'wikihelp\' on the first prompt of a question for the relevant link(s) in the Arch wiki ' + - '(and other resources).\n') + '(and other resources).') # https://aif.square-r00t.net/#code_disk_code diskhelp = ['https://wiki.archlinux.org/index.php/installation_guide#Partition_the_disks'] - diskin = chkPrompt('\nWhat disk(s) would you like to be configured on the target system?\n' + + diskin = chkPrompt('\n* What disk(s) would you like to be configured on the target system?\n' + '\tIf you have multiple disks, separate with a comma (e.g. \'/dev/sda,/dev/sdb\'): ', diskhelp) # NOTE: the following is a dict of fstype codes to their description. fstypes = {'0700': 'Microsoft basic data', '0c01': 'Microsoft reserved', '2700': 'Windows RE', '3000': 'ONIE config', '3900': 'Plan 9', '4100': 'PowerPC PReP boot', '4200': 'Windows LDM data', '4201': 'Windows LDM metadata', '4202': 'Windows Storage Spaces', '7501': 'IBM GPFS', '7f00': 'ChromeOS kernel', '7f01': 'ChromeOS root', '7f02': 'ChromeOS reserved', '8200': 'Linux swap', '8300': 'Linux filesystem', '8301': 'Linux reserved', '8302': 'Linux /home', '8303': 'Linux x86 root (/)', '8304': 'Linux x86-64 root (/', '8305': 'Linux ARM64 root (/)', '8306': 'Linux /srv', '8307': 'Linux ARM32 root (/)', '8400': 'Intel Rapid Start', '8e00': 'Linux LVM', 'a500': 'FreeBSD disklabel', 'a501': 'FreeBSD boot', 'a502': 'FreeBSD swap', 'a503': 'FreeBSD UFS', 'a504': 'FreeBSD ZFS', 'a505': 'FreeBSD Vinum/RAID', 'a580': 'Midnight BSD data', 'a581': 'Midnight BSD boot', 'a582': 'Midnight BSD swap', 'a583': 'Midnight BSD UFS', 'a584': 'Midnight BSD ZFS', 'a585': 'Midnight BSD Vinum', 'a600': 'OpenBSD disklabel', 'a800': 'Apple UFS', 'a901': 'NetBSD swap', 'a902': 'NetBSD FFS', 'a903': 'NetBSD LFS', 'a904': 'NetBSD concatenated', 'a905': 'NetBSD encrypted', 'a906': 'NetBSD RAID', 'ab00': 'Recovery HD', 'af00': 'Apple HFS/HFS+', 'af01': 'Apple RAID', 'af02': 'Apple RAID offline', 'af03': 'Apple label', 'af04': 'AppleTV recovery', 'af05': 'Apple Core Storage', 'bc00': 'Acronis Secure Zone', 'be00': 'Solaris boot', 'bf00': 'Solaris root', 'bf01': 'Solaris /usr & Mac ZFS', 'bf02': 'Solaris swap', 'bf03': 'Solaris backup', 'bf04': 'Solaris /var', 'bf05': 'Solaris /home', 'bf06': 'Solaris alternate sector', 'bf07': 'Solaris Reserved 1', 'bf08': 'Solaris Reserved 2', 'bf09': 'Solaris Reserved 3', 'bf0a': 'Solaris Reserved 4', 'bf0b': 'Solaris Reserved 5', 'c001': 'HP-UX data', 'c002': 'HP-UX service', 'ea00': 'Freedesktop $BOOT', 'eb00': 'Haiku BFS', 'ed00': 'Sony system partition', 'ed01': 'Lenovo system partition', 'ef00': 'EFI System', 'ef01': 'MBR partition scheme', 'ef02': 'BIOS boot partition', 'f800': 'Ceph OSD', 'f801': 'Ceph dm-crypt OSD', 'f802': 'Ceph journal', 'f803': 'Ceph dm-crypt journal', 'f804': 'Ceph disk in creation', 'f805': 'Ceph dm-crypt disk in creation', 'fb00': 'VMWare VMFS', 'fb01': 'VMWare reserved', 'fc00': 'VMWare kcore crash protection', 'fd00': 'Linux RAID'} @@ -487,8 +502,8 @@ class aifgen(object): if not re.match('^/dev/[A-Za-z0]+', disk): exit('!! ERROR: Disk {0} does not seem to be a valid device path.'.format(disk)) conf['disks'][disk] = {} - print('\nConfiguring disk {0} ...'.format(disk)) - fmtin = chkPrompt('* What format should this disk use (gpt/bios)? ', diskhelp) + print('\n* Configuring disk {0} ...'.format(disk)) + fmtin = chkPrompt('** What format should this disk use (gpt/bios)? ', diskhelp) fmt = fmtin.lower() if fmt not in ('gpt', 'bios'): exit(' !! ERROR: Must be one of \'gpt\' or \'bios\'.') @@ -498,7 +513,7 @@ class aifgen(object): maxpart = '256' else: maxpart = '4' # yeah, extended volumes can do more, but that's not supported in AIF-NG. yet? - partnumsin = chkPrompt('* How many partitions should this disk have? (Maximum: {0}) '.format(maxpart), diskhelp) + partnumsin = chkPrompt('** How many partitions should this disk have? (Maximum: {0}) '.format(maxpart), diskhelp) try: int(partnumsin) except: @@ -525,10 +540,10 @@ class aifgen(object): if fstypein not in fstypes.keys(): exit(' !! ERROR: {0} is not a valid filesystem type.'.format(fstypein)) else: - print('\tSelected {0}'.format(fstypes[fstypein])) + print('\t(Selected {0})'.format(fstypes[fstypein])) mnthelp = ['https://wiki.archlinux.org/index.php/installation_guide#Mount_the_file_systems', 'https://aif.square-r00t.net/#code_mount_code'] - mntin = chkPrompt('\nWhat mountpoint(s) would you like to be configured on the target system?\n' + + mntin = chkPrompt('\n* What mountpoint(s) would you like to be configured on the target system?\n' + '\tIf you have multiple mountpoints, separate with a comma (e.g. \'/mnt/aif,/mnt/aif/boot\').\n' + '\t(NOTE: Can be \'swap\' for swapspace.): ', mnthelp) conf['mounts'] = {} @@ -536,11 +551,11 @@ class aifgen(object): mount = m.strip() if not re.match('^(/([^/\x00\s]+(/)?)+|swap)$', mount): exit('!! ERROR: Mountpoint {0} does not seem to be a valid path/specifier.'.format(mount)) - print('\nConfiguring mountpoint {0} ...'.format(mount)) - dvcin = chkPrompt('* What device/partition should be mounted here? ', mnthelp) + print('\n* Configuring mountpoint {0} ...'.format(mount)) + dvcin = chkPrompt('** What device/partition should be mounted here? ', mnthelp) if not re.match('^/dev/[A-Za-z0]+', dvcin): exit(' !! ERROR: Must be a full path to a device/partition.') - ordrin = chkPrompt('* What order should this mount occur in relation to others?\n\t'+ + ordrin = chkPrompt('** What order should this mount occur in relation to others?\n\t'+ 'Must be a unique integer (lower numbers mount before higher numbers): ', mnthelp) try: order = int(ordrin) @@ -551,30 +566,35 @@ class aifgen(object): conf['mounts'][order] = {} conf['mounts'][order]['target'] = mount conf['mounts'][order]['device'] = dvcin - fstypein = chkPrompt('* What filesystem type should this be mounted as (i.e. mount\'s -t option)? This is optional,\n\t' + - 'but may be required for more exotic filesystem types. If you don\'t have to specify one,\n\t' + - 'just leave this blank: ', mnthelp) - if fstypein == '': + if mount != 'swap': + fstypein = chkPrompt('** What filesystem type should this be mounted as (i.e. mount\'s -t option)? This is optional,\n\t' + + 'but may be required for more exotic filesystem types. If you don\'t have to specify one,\n\t' + + 'just leave this blank: ', mnthelp) + if fstypein == '': + conf['mounts'][order]['fstype'] = False + elif not re.match('^[a-z]+([0-9]+)?$', fstypein): # Not 100%, but should catch most faulty entries + exit(' !! ERROR: {0} does not seem to be a valid filesystem type.'.format(fstypein)) + else: + conf['mounts'][order]['fstype'] = fstypein + mntoptsin = chkPrompt('** What, if any, mount option(s) (mount\'s -o option) do you require? (Multiple options should be separated\n' + + '\twith a comma). If none, leave this blank: ', mnthelp) + if mntoptsin == '': + conf['mounts'][order]['opts'] = False + elif not re.match('^[A-Za-z0-9_\.\-=]+(,[A-Za-z0-9_\.\-=]+)*', re.sub('\s', '', mntoptsin)): # TODO: shlex split this instead? + exit(' !! ERROR: You seem to have not specified valid mount options.') + else: + # TODO: slex this instead? is it possible for mount opts to contain whitespace? + conf['mounts'][order]['opts'] = re.sub('\s', '', mntoptsin) + else: conf['mounts'][order]['fstype'] = False - elif not re.match('^[a-z]+([0-9]+)?$', fstypein): # Not 100%, but should catch most faulty entries - exit(' !! ERROR: {0} does not seem to be a valid filesystem type.'.format(fstypein)) - else: - conf['mounts'][order]['fstype'] = fstypein - mntoptsin = chkPrompt('* What, if any, mount option(s) (mount\'s -o option) do you require? (Multiple options should be separated\n' + - '\twith a comma). If none, leave this blank: ', mnthelp) - if mntoptsin == '': conf['mounts'][order]['opts'] = False - elif not re.match('^[A-Za-z0-9_\.\-=]+(,[A-Za-z0-9_\.\-=]+)*', re.sub('\s', '', mntoptsin)): # TODO: shlex split this instead? - exit(' !! ERROR: You seem to have not specified valid mount options.') - else: - # TODO: slex this instead? is it possible for mount opts to contain whitespace? - conf['mounts'][order]['opts'] = re.sub('\s', '', mntoptsin) - print('\nNow, let\'s configure the network. Note that at this time, wireless/more exotic networking is not supported by AIF-NG.\n') + print('\nNow, let\'s configure the network. Note that at this time,' + + '\twireless/more exotic networking is not supported by AIF-NG.\n') conf['network'] = {} nethelp = ['https://wiki.archlinux.org/index.php/installation_guide#Network_configuration', 'https://aif.square-r00t.net/#code_network_code'] - hostnamein = chkPrompt('What should the newly-installed system\'s hostname be?\n\t' + - 'It must be in FQDN format, but can be a non-existent domain: ', nethelp) + hostnamein = chkPrompt('* What should the newly-installed system\'s hostname be?\n' + + '\tIt must be in FQDN format, but can be a non-existent domain: ', nethelp) hostname = hostnamein.lower() if len(hostname) > 253: exit(' !! ERROR: A FQDN cannot be more than 253 characters (RFC 1035, 2.3.4)') @@ -614,12 +634,12 @@ class aifgen(object): else: rebootme = False conf['system'] = {'timezone': tzin, 'locale': localein, 'chrootpath': chrootpathin, 'kbd': kbdin, 'reboot': rebootme} - syshelp[1] = 'https://aif.square-r00t.net/#code_users_code' + syshelp.append('https://aif.square-r00t.net/#code_users_code') print('\nNow let\'s handle some user accounts. For passwords, you can either enter the password you want to use,\n' + 'a \'!\' (in which case TTY login will be disabled but e.g. SSH will still work), or just hit enter to leave it blank\n' + '(which is HIGHLY not recommended - it means anyone can login by just pressing enter at the login!)\n') print('Let\'s configure the root user.') - conf['system']['rootpass'] = genPassHash(root) + conf['system']['rootpass'] = genPassHash('root') moreusers = input('Would you like to add one or more regular user(s)? (y/{0}n{1}) '.format(color.BOLD, color.END)) if re.match('^y(es)?$', moreusers.lower()): syshelp.append('https://aif.square-r00t.net/#code_user_code') @@ -662,7 +682,9 @@ class aifgen(object): 'https://aif.square-r00t.net/#code_bootloader_code'] conf['boot'] = {} btldrin = chkPrompt('* Almost done! Please choose a bootloader. ({0}grub{1}/systemd) '.format(color.BOLD, color.END), btldrhelp) - if not re.match('^(grub|systemd)$', btldrin.lower()): + if btldrin == '': + btldrin = 'grub' + elif not re.match('^(grub|systemd)$', btldrin.lower()): exit(' !! ERROR: You must choose a bootloader between grub or systemd.') else: conf['boot']['bootloader'] = btldrin.lower() @@ -676,7 +698,7 @@ class aifgen(object): conf['boot']['efi'] = True bttgtstr = 'ESP (EFI System Partition)' btrgx = re.compile('^/([^/\x00\s]+(/)?)+$') - bttgtin = chkPrompt('** What is the target for {0}? That is, the path to the {1}: '.format(btldrin.lower(), bttgtstr), btldrhelp) + bttgtin = chkPrompt('** What is the target for {0}? That is, the path to the {1} (within the chroot): '.format(btldrin.lower(), bttgtstr), btldrhelp) if not btrgx.match(bttgtin): exit(' !! ERROR: That doesn\'t seem to be a valid {0}.'.format(bttgtstr)) else: @@ -692,12 +714,14 @@ class aifgen(object): if self.args['verbose']: import pprint pprint.pprint(conf) + if self.args['verbose_raw']: + print(conf) return(conf) def convertJSON(self): - with open(args['inputfile'], 'r') as f: + with open(self.args['inputfile'], 'r') as f: try: - conf = json.loads(f.read()) + conf = json.load(f) except: exit(' !! ERROR: {0} does not seem to be a strict JSON file.'.format(args['inputfile'])) return(conf) @@ -723,7 +747,7 @@ class aifgen(object): conf = self.getOpts() elif self.args['oper'] == 'convert': conf = self.convertJSON() - if self.args['oper'] in ('create', 'convert'): + if self.args['oper'] in ('create', 'convert', 'validate'): self.validateXML() def parseArgs(): @@ -752,6 +776,11 @@ def parseArgs(): dest = 'verbose', action = 'store_true', help = 'Print the dict of raw values used to create the XML. Mostly/only useful for debugging.') + createargs.add_argument('-v:r', + '--verbose-raw', + dest = 'verbose_raw', + action = 'store_true', + help = 'Like -v, but prints the unformatted dict.') convertargs.add_argument('-i', '--input', dest = 'inputfile', @@ -798,12 +827,7 @@ def main(): # Once aifgen.main() is complete, we only need to call that. # That should handle all the below logic. aif = aifgen(verifyArgs(args)) - if args['oper'] == 'create': - aif.getOpts() - if args['oper'] == 'validate': - aif.validateXML() - elif args['oper'] == 'convert': - aif.convertJSON() + aif.main() if __name__ == '__main__': main() diff --git a/docs/examples/aif-sample-intermediate.json b/docs/examples/aif-sample-intermediate.json index c06aabc..f15cae5 100644 --- a/docs/examples/aif-sample-intermediate.json +++ b/docs/examples/aif-sample-intermediate.json @@ -1,97 +1,156 @@ { -"disks": - { - "/dev/sda": - {"fmt": "gpt", - "parts": - { - "1": - { - "start": "0%", - "stop": "90%" - }, - "2": - { - "start": "90%", - "stop": "100%" - } - } - }, - "/dev/sdb": - { - "fmt": "gpt", - "parts": - { - "1": - { - "start": "0%", - "stop": "10%" - }, - "2": - { - "start": "10%", - "stop": "100%" - } - } - } - }, - "mounts": - { - "1": - { - "device": "/dev/sda1", - "fstype": "ext4", - "opts": "rw,noatime,errors=remount-ro", - "target": "/mnt/aif" - }, - "2": - { - "device": "/dev/sda2", - "fstype": "vfat", - "opts": "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro,auto", - "target": "/mnt/aif/boot" - }, - "3": - { - "device": "/dev/sdb1", - "fstype": "swap", - "opts": false, - "target": "swap"}, - "4": - { - "device": "/dev/sdb2", - "fstype": false, - "opts": false, - "target": "/mnt/aif/mnt/data" - } - }, - "network": - { - "hostname": "aif.loc.lan", - "ifaces": - { - "ens3": - { - "address": "auto", - "gw": false, - "proto": "ipv4", - "resolvers": false - }, - "ens4": - { - "address": "192.168.1.2/24", - "gw": "192.168.1.1", - "proto": "ipv4", - "resolvers": ["4.2.2.1", "4.2.2.2", "8.8.8.8"] - } - } - }, - "system": - { - "chrootpath": "/mnt/aif", - "kbd": "US", - "locale": "en_US.UTF-8", - "reboot": true, - "timezone": "UTC" - } + "boot": { + "efi": true, + "target": "/boot" + }, + "disks": { + "/dev/sda": { + "fmt": "gpt", + "parts": { + "1": { + "start": "0%", + "stop": "95%" + }, + "2": { + "start": "95%", + "stop": "100%" + } + } + }, + "/dev/sdb": { + "fmt": "gpt", + "parts": { + "1": { + "start": "0%", + "stop": "47%" + }, + "2": { + "start": "47%", + "stop": "95%" + }, + "3": { + "start": "95%", + "stop": "100%" + } + } + } + }, + "mounts": { + "1": { + "device": "/dev/sda1", + "fstype": "ext4", + "opts": "defaults", + "target": "/mnt/aif" + }, + "2": { + "device": "/dev/sda2", + "fstype": "vfat", + "opts": "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro", + "target": "/mnt/aif/boot" + }, + "3": { + "device": "/dev/sdb1", + "fstype": "ext4", + "opts": "defaults", + "target": "/mnt/aif/home" + }, + "4": { + "device": "/dev/sdb2", + "fstype": "ext4", + "opts": "defaults", + "target": "/mnt/aif/mnt/data" + }, + "5": { + "device": "/dev/sdb3", + "fstype": false, + "opts": false, + "target": "swap" + } + }, + "network": { + "hostname": "aif.loc.lan", + "ifaces": { + "ens3": { + "address": "auto", + "gw": false, + "proto": "ipv4", + "resolvers": false + }, + "ens4": { + "address": "192.168.1.2/24", + "gw": "192.168.1.1", + "proto": "ipv4", + "resolvers": [ + "4.2.2.1", + "4.2.2.2" + ] + } + } + }, + "software": { + "packages": { + "openssh": "None" + }, + "pkgr": false, + "repos": { + "community": { + "enabled": true, + "mirror": "file:///etc/pacman.d/mirrorlist", + "siglevel": "default" + }, + "community-testing": { + "enabled": false, + "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" + } + } + }, + "system": { + "chrootpath": "/mnt/aif", + "kbd": "US", + "locale": "en_US.UTF-8", + "reboot": true, + "rootpass": "$6$0jk/xhwahQHTi5QP$VWTgGlHNdSBDbQmJXUwJPZqajfL3JqYYF7Ghxk3ZSKi12WWXb49KsjR7q0bigvgBBBk5A/mvYES3/qareytFS0", + "services": { + "sshd": true + }, + "timezone": "UTC", + "users": { + "aifusr": { + "comment": "A Test User", + "gid": false, + "group": false, + "home": false, + "password": "$6$IlEwDkNmZRuTrT97$vKHjREGspspApBd8aQ/y1S43yRmGMjAzqOmdjNRLWaZyNKqGPrIjMHV9CJc7BzQgU12pRz3cwC6yyc8BDFARu/", + "sudo": true, + "uid": false, + "xgroups": { + "users": { + "create": false, + "gid": false + } + } + } + } + } } diff --git a/docs/examples/aif-sample-intermediate.json.txt b/docs/examples/aif-sample-intermediate.json.txt new file mode 100644 index 0000000..1aae677 --- /dev/null +++ b/docs/examples/aif-sample-intermediate.json.txt @@ -0,0 +1,73 @@ +{'boot': {'efi': True, 'target': '/boot'}, + 'disks': {'/dev/sda': {'fmt': 'gpt', + 'parts': {1: {'start': '0%', 'stop': '95%'}, + 2: {'start': '95%', 'stop': '100%'}}}, + '/dev/sdb': {'fmt': 'gpt', + 'parts': {1: {'start': '0%', 'stop': '47%'}, + 2: {'start': '47%', 'stop': '95%'}, + 3: {'start': '95%', 'stop': '100%'}}}}, + 'mounts': {1: {'device': '/dev/sda1', + 'fstype': 'ext4', + 'opts': 'defaults', + 'target': '/mnt/aif'}, + 2: {'device': '/dev/sda2', + 'fstype': 'vfat', + 'opts': 'rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro', + 'target': '/mnt/aif/boot'}, + 3: {'device': '/dev/sdb1', + 'fstype': 'ext4', + 'opts': 'defaults', + 'target': '/mnt/aif/home'}, + 4: {'device': '/dev/sdb2', + 'fstype': 'ext4', + 'opts': 'defaults', + 'target': '/mnt/aif/mnt/data'}, + 5: {'device': '/dev/sdb3', + 'fstype': False, + 'opts': False, + 'target': 'swap'}}, + 'network': {'hostname': 'aif.loc.lan', + 'ifaces': {'ens3': {'address': 'auto', + 'gw': False, + 'proto': 'ipv4', + 'resolvers': False}, + 'ens4': {'address': '192.168.1.2/24', + 'gw': '192.168.1.1', + 'proto': 'ipv4', + 'resolvers': ['4.2.2.1', '4.2.2.2']}}}, + 'software': {'packages': {'openssh': 'None'}, + 'pkgr': False, + 'repos': {'community': {'enabled': True, + 'mirror': 'file:///etc/pacman.d/mirrorlist', + 'siglevel': 'default'}, + 'community-testing': {'enabled': False, + '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'}}}, + 'system': {'chrootpath': '/mnt/aif', + 'kbd': 'US', + 'locale': 'en_US.UTF-8', + 'reboot': True, + 'rootpass': '$6$0jk/xhwahQHTi5QP$VWTgGlHNdSBDbQmJXUwJPZqajfL3JqYYF7Ghxk3ZSKi12WWXb49KsjR7q0bigvgBBBk5A/mvYES3/qareytFS0', + 'services': {'sshd': True}, + 'timezone': 'UTC', + 'users': {'aifusr': {'comment': 'A Test User', + 'gid': False, + 'group': False, + 'home': False, + 'password': '$6$IlEwDkNmZRuTrT97$vKHjREGspspApBd8aQ/y1S43yRmGMjAzqOmdjNRLWaZyNKqGPrIjMHV9CJc7BzQgU12pRz3cwC6yyc8BDFARu/', + 'sudo': True, + 'uid': False, + 'xgroups': {'users': {'create': False, + 'gid': False}}}}}} diff --git a/extras/createtest.expect b/extras/createtest.expect new file mode 100755 index 0000000..8c40f06 --- /dev/null +++ b/extras/createtest.expect @@ -0,0 +1,167 @@ +#!/usr/bin/expect -f + +log_file -noappend /tmp/expect.log +set force_conservative 0 ;# set to 1 to force conservative mode even if + ;# script wasn't run conservatively originally +if {$force_conservative} { + set send_slow {1 .1} + proc send {ignore arg} { + sleep .1 + exp_send -s -- $arg + } +} + +set send_slow {10 .001} + +set timeout -1 +spawn ./aif-config.py create -v:r -f /tmp/aif.xml +## disks +send -- "/dev/sda,/dev/sdb\r" +# sda +send -- "gpt\r" +send -- "2\r" +# sda1 +send -- "0%\r" +send -- "95%\r" +send -- "8300\r" +# sda2 +send -- "95%\r" +send -- "100%\r" +send -- "ef00\r" +# sdb +send -- "gpt\r" +send -- "3\r" +# sdb1 +send -- "0%\r" +send -- "47%\r" +send -- "8300\r" +# sdb2 +send -- "47%\r" +send -- "95%\r" +send -- "8300\r" +# sdb3 +send -- "95%\r" +send -- "100%\r" +send -- "8200\r" +## mounts +send -- "/mnt/aif,/mnt/aif/boot,/mnt/aif/home,/mnt/aif/mnt/data,swap\r" +# /mnt/aif +send -- "/dev/sda1\r" +send -- "1\r" +send -- "ext4\r" +send -- "defaults\r" +# /mnt/aif/boot +send -- "/dev/sda2\r" +send -- "2\r" +send -- "vfat\r" +send -- "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro\r" +# /mnt/aif/home +send -- "/dev/sdb1\r" +send -- "3\r" +send -- "ext4\r" +send -- "defaults\r" +# /mnt/aif/mnt/data +send -- "/dev/sdb2\r" +send -- "4\r" +send -- "ext4\r" +send -- "defaults\r" +# swap +send -- "/dev/sdb3\r" +send -- "5\r" +## network +# hostname +send -- "aif.loc.lan\r" +# interface +send -- "ens3\r" +send -- "auto\r" +send -- "ipv4\r" +# add another interface? +send -- "y\r" +# second interface +send -- "ens4\r" +send -- "192.168.1.2/24\r" +send -- "192.168.1.1\r" +send -- "4.2.2.1,4.2.2.2\r" +# add another interface? default is no +send -- "\r" +## system +# timezone (default is UTC) +send -- "\r" +# locale (default is en_US.UTF-8 +send -- "\r" +# chroot path +send -- "/mnt/aif\r" +# kbd (default is US) +send -- "\r" +# reboot host after install? default is yes +send -- "\r" +# root password +send -- "test\r" +# add user? +send -- "y\r" +# user +send -- "aifusr\r" +# sudo access +send -- "y\r" +# password +send -- "test\r" +send -- "A Test User\r" +# uid (default is autogen) +send -- "\r" +# primary group (default is autogen'd based on username) +send -- "\r" +# home dir (default is e.g. /home/username) +send -- "\r" +# add exta groups? +send -- "y\r" +# extra group +send -- "users\r" +# need to be created? default is no +send -- "\r" +# add another extra group? default is no +send -- "\r" +# add more users? default is no +send -- "\r" +# enable/disable services +send -- "y\r" +# service +send -- "sshd\r" +# enable? default is yes +send -- "\r" +# manage another service? default is no +send -- "\r" +# packager (default is pacman) +send -- "\r" +# review default repos? default is yes +send -- "\r" +# edit any of them? +send -- "y\r" +# edit the 6th repo (multilib) +send -- "6\r" +# enabled? +send -- "y\r" +# siglevel (default is unchanged) +send -- "\r" +# mirror URI (default is unchanged) +send -- "\r" +# edit another repo? default is no +send -- "\r" +# add additional repositories? default is no +send -- "\r" +# install extra software? +send -- "y\r" +# software +send -- "openssh\r" +# repository (optional) +send -- "\r" +# add another package? +send -- "\r" +# bootloader (default is grub) +send -- "\r" +# system supports UEFI? default is yes +send -- "\r" +# ESP/EFI system partition +send -- "/boot\r" +# any hook scripts? default is no +send -- "\r" +expect eof diff --git a/extras/txttojson.py b/extras/txttojson.py new file mode 100755 index 0000000..c6c6012 --- /dev/null +++ b/extras/txttojson.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import pprint +#import re +try: + import yaml +except: + exit('You need pyYAML.') + +def parseArgs(): + args = argparse.ArgumentParser() + args.add_argument('-i', + '--in', + dest = 'infile', + required = True, + help = 'The plaintext representation of a python dict') + args.add_argument('-o', + '--out', + dest = 'outfile', + required = True, + help = 'The JSON file to create') + return(args) + +def main(): + args = vars(parseArgs().parse_args()) + infile = os.path.abspath(os.path.normpath(args['infile'])) + outfile = os.path.abspath(os.path.normpath(args['outfile'])) + if not os.path.lexists(infile): + exit('Input file doesn\'t exist.') +#try: + with open(outfile, 'w') as outgoing: + with open(infile, 'r') as incoming: + #data = re.sub("'", '"', incoming.read()) + #outgoing.write(data) + #d = json.dumps(data, ensure_ascii = False) + #d = json.dumps(incoming.read().replace("'", '"')) + d = yaml.load(incoming.read()) + pprint.pprint(d) + j = json.dumps(d, indent = 4) + outgoing.write(j) +#except: + #exit('Error when trying to read/write file(s).') + return() + +if __name__ == '__main__': + main()