import os import shutil import re import subprocess import jinja2 import git import patch import datetime import humanize import hashlib def buildIPXE(conf): build = conf['build'] bdisk = conf['bdisk'] ipxe = conf['ipxe'] mini = ipxe['iso'] prepdir = conf['build']['prepdir'] templates_dir = build['basedir'] + '/extra/templates' ipxe_tpl = templates_dir + '/iPXE' srcdir = build['srcdir'] embedscript = build['dlpath'] + '/EMBED' ipxe_src = srcdir + '/ipxe' #ipxe_git_uri = 'git://git.ipxe.org/ipxe.git' ipxe_git_uri = 'http://git.ipxe.org/ipxe.git' print('{0}: [IPXE] Prep/fetch sources...'.format( datetime.datetime.now())) # Get the source if os.path.isdir(ipxe_src): shutil.rmtree(ipxe_src) ipxe_repo = git.Repo.clone_from(ipxe_git_uri, ipxe_src) # Generate patches tpl_loader = jinja2.FileSystemLoader(ipxe_tpl) env = jinja2.Environment(loader = tpl_loader) tpl = env.get_template('EMBED.j2') tpl_out = tpl.render(ipxe = ipxe) with open(embedscript, 'w+') as f: f.write(tpl_out) # Feature enabling # In config/general.h with open('{0}/src/config/general.h'.format(ipxe_src), 'r') as f: generalconf = f.read() # And in config/console.h with open('{0}/src/config/console.h'.format(ipxe_src), 'r') as f: consoleconf = f.read() patterns = (('^#undef(\s*NET_PROTO_IPV6.*)$','#define\g<1>'), # enable IPv6 ('^#undef(\s*DOWNLOAD_PROTO_HTTPS)','#define\g<1>'), # enable HTTPS ('^//(#define\s*IMAGE_TRUST_CMD)','\g<1>'), # moar HTTPS ('^#undef(\s*DOWNLOAD_PROTO_FTP)','#define\g<1>')) # enable FTP #('^//(#define\s*CONSOLE_CMD)','\g<1>'), # BROKEN in EFI? TODO. if enable, replace } with , above etc. #('^//(#define\s*IMAGE_PNG','\g<1>'), # SAME, broken in EFI? TODO. #console = ('^//(#define\s*CONSOLE_VESAFB)','\g<1>') # BROKEN in EFI? TODO. # https://stackoverflow.com/a/4427835 # https://emilics.com/notebook/enblog/p869.html # The above methods don't seem to work. it craps out on the pattern matchings # so we use tuples instead. for x in patterns: generalconf = re.sub(x[0], x[1], generalconf, flags=re.MULTILINE) with open('{0}/src/config/general.h'.format(ipxe_src), 'w') as f: f.write(generalconf) # Uncomment when we want to test the above consdict etc. #for x in patterns: # generalconf = re.sub(x[0], x[1], generalconf, flags=re.MULTILINE) #with open('{0}/src/config/console.h'.format(ipxe_src), 'w') as f: # f.write(console) # Now we make! cwd = os.getcwd() os.chdir(ipxe_src + '/src') modenv = os.environ.copy() modenv['EMBED'] = embedscript #modenv['TRUST'] = ipxe_ssl_ca # TODO: test these #modenv['CERT'] = '{0},{1}'.format(ipxe_ssl_ca, ipxe_ssl_crt) # TODO: test these #modenv['PRIVKEY'] = ipxe_ssl_ckey # TODO: test these build_cmd = {} build_cmd['base'] = ['/usr/bin/make', 'all', 'EMBED={0}'.format(embedscript)] # TODO: copy the UNDI stuff/chainloader to tftpboot, if enabled build_cmd['undi'] = ['/usr/bin/make', 'bin/ipxe.pxe', 'EMBED={0}'.format(embedscript)] build_cmd['efi'] = ['/usr/bin/make', 'bin-i386-efi/ipxe.efi', 'bin-x86_64-efi/ipxe.efi', 'EMBED={0}'.format(embedscript)] # Now we call the commands. DEVNULL = open(os.devnull, 'w') if os.path.isfile(build['dlpath'] + '/ipxe.log'): os.remove(build['dlpath'] + '/ipxe.log') print(('{0}: [IPXE] Building iPXE ({1}). PROGRESS: tail -f {2}/ipxe.log ...').format( datetime.datetime.now(), ipxe_src, build['dlpath'])) with open('{0}/ipxe.log'.format(build['dlpath']), 'a') as f: subprocess.call(build_cmd['base'], stdout = f, stderr = subprocess.STDOUT, env=modenv) subprocess.call(build_cmd['undi'], stdout = f, stderr = subprocess.STDOUT, env=modenv) subprocess.call(build_cmd['efi'], stdout = f, stderr = subprocess.STDOUT, env=modenv) print('{0}: [IPXE] Built iPXE image(s) successfully.'.format(datetime.datetime.now())) os.chdir(cwd) def genISO(conf): build = conf['build'] bdisk = conf['bdisk'] ipxe = conf['ipxe'] arch = build['arch'] dlpath = build['dlpath'] ver = bdisk['ver'] isodir = build['isodir'] isofile = '{0}-{1}-{2}.mini.iso'.format(bdisk['uxname'], bdisk['ver'], build['buildnum']) isopath = '{0}/{1}'.format(isodir, isofile) prepdir = build['prepdir'] chrootdir = build['chrootdir'] mini = ipxe['iso'] iso = {} srcdir = build['srcdir'] ipxe_src = srcdir + '/ipxe' mountpt = build['mountpt'] templates_dir = build['basedir'] + '/extra/templates/iPXE/' tpl_loader = jinja2.FileSystemLoader(templates_dir) env = jinja2.Environment(loader = tpl_loader) bootdir = '{0}/ipxe_mini'.format(dlpath) efiboot_img = '{0}/EFI/{1}/efiboot.img'.format(bootdir, bdisk['name']) innerefi64 = '{0}/src/bin-x86_64-efi/ipxe.efi'.format(ipxe_src) efi = False # this shouldn't be necessary... if it is, we can revisit this in the future. see "Inner dir" below. #innerefi32 = '{0}/src/bin-i386-efi/ipxe.efi'.format(ipxe_src) # We only need to do EFI prep if we have UEFI/x86_64 support. See above, but IA64 is dead, Zed. if mini and (('x86_64') in arch): efi = True # EFI prep/building print('{0}: [IPXE] UEFI support for Mini ISO...'.format(datetime.datetime.now())) if os.path.isdir(bootdir): shutil.rmtree(bootdir) os.makedirs(os.path.dirname(efiboot_img), exist_ok = True) # FAT32 embedded EFI dir os.makedirs('{0}/EFI/boot'.format(bootdir), exist_ok = True) # EFI bootloader binary dir # Inner dir (miniboot.img file) #sizetotal = 2097152 # 2MB wiggle room. increase this if we add IA64. sizetotal = 34603008 # 33MB wiggle room. increase this if we add IA64. sizetotal += os.path.getsize(innerefi64) sizefiles = ['HashTool', 'PreLoader'] for f in sizefiles: sizetotal += os.path.getsize('{0}/root.x86_64/usr/share/efitools/efi/{1}.efi'.format( chrootdir, f)) # These won't be *quite* accurate since it's before the template substitution, # but it'll be close enough. for (path, dirs, files) in os.walk(templates_dir): for file in files: fname = os.path.join(path, file) sizetotal += os.path.getsize(fname) print("{0}: [IPXE] Creating EFI ESP image {1} ({2})...".format( datetime.datetime.now(), efiboot_img, humanize.naturalsize(sizetotal))) if os.path.isfile(efiboot_img): os.remove(efiboot_img) with open(efiboot_img, 'wb+') as f: f.truncate(sizetotal) DEVNULL = open(os.devnull, 'w') cmd = ['/sbin/mkfs.fat', '-F', '32', '-n', 'iPXE_EFI', efiboot_img] subprocess.call(cmd, stdout = DEVNULL, stderr = subprocess.STDOUT) cmd = ['/bin/mount', efiboot_img, mountpt] subprocess.call(cmd) os.makedirs(mountpt + '/EFI/boot', exist_ok = True) # "Inner" (EFI image) #os.makedirs('{0}/EFI/{1}'.format(mountpt, bdisk['name']), exist_ok = True) # "Inner" (EFI image) os.makedirs('{0}/boot'.format(bootdir), exist_ok = True) # kernel(s) os.makedirs('{0}/loader/entries'.format(bootdir), exist_ok = True) # EFI for d in (mountpt, bootdir): shutil.copy2(innerefi64,'{0}/EFI/boot/ipxe.efi'.format(d)) for f in ('PreLoader.efi', 'HashTool.efi'): if f == 'PreLoader.efi': fname = 'bootx64.efi' else: fname = f with open('{0}/root.x86_64/usr/share/efitools/efi/{1}'.format( chrootdir,f), 'rb') as r: with open('{0}/EFI/boot/{1}'.format(mountpt, fname), 'wb') as file: file.write(r.read()) with open('{0}/root.x86_64/usr/share/efitools/efi/{1}'.format( chrootdir, f), 'rb') as r: with open('{0}/EFI/boot/{1}'.format(bootdir, fname), 'wb+') as file: file.write(r.read()) # And the systemd efi bootloader. with open('{0}/root.x86_64/usr/lib/systemd/boot/efi/systemd-bootx64.efi'.format( chrootdir), 'rb') as r: with open('{0}/EFI/boot/loader.efi'.format(mountpt), 'wb+') as f: f.write(r.read()) # And loader entries. os.makedirs('{0}/loader/entries'.format(mountpt, exist_ok = True)) for t in ('loader', 'base'): if t == 'base': name = bdisk['uxname'] tplpath = '{0}/loader/entries'.format(mountpt) else: name = t tplpath = '{0}/loader'.format(mountpt) tpl = env.get_template('EFI/{0}.conf.j2'.format(t)) tpl_out = tpl.render(build = build, bdisk = bdisk) with open('{0}/{1}.conf'.format(tplpath, name), "w+") as f: f.write(tpl_out) cmd = ['/bin/umount', mountpt] subprocess.call(cmd) # Outer dir outerdir = True os.makedirs('{0}/isolinux'.format(bootdir), exist_ok = True) # BIOS # Loader entries (outer) for t in ('loader','base'): if t == 'base': name = bdisk['uxname'] tplpath = '{0}/loader/entries'.format(bootdir) else: name = t tplpath = '{0}/loader'.format(bootdir) tpl = env.get_template('EFI/{0}.conf.j2'.format(t)) tpl_out = tpl.render(build = build, bdisk = bdisk, outerdir = outerdir) with open('{0}/{1}.conf'.format(tplpath, name), "w+") as f: f.write(tpl_out) if mini: # BIOS prepping shutil.copy2('{0}/src/bin/ipxe.lkrn'.format(ipxe_src), '{0}/boot/ipxe.krn'.format(bootdir)) isolinux_filelst = ['isolinux.bin', 'ldlinux.c32'] os.makedirs('{0}/isolinux'.format(bootdir), exist_ok = True) for f in isolinux_filelst: shutil.copy2('{0}/root.{1}/usr/lib/syslinux/bios/{2}'.format(chrootdir, arch[0], f), '{0}/isolinux/{1}'.format(bootdir, f)) tpl = env.get_template('BIOS/isolinux.cfg.j2') tpl_out = tpl.render(build = build, bdisk = bdisk) with open('{0}/isolinux/isolinux.cfg'.format(bootdir), "w+") as f: f.write(tpl_out) print("{0}: [IPXE] Building Mini ISO ({1})...".format(datetime.datetime.now(), isopath)) if efi: cmd = ['/usr/bin/xorriso', '-as', 'mkisofs', '-iso-level', '3', '-full-iso9660-filenames', '-volid', bdisk['name'] + '_MINI', '-appid', bdisk['desc'], '-publisher', bdisk['dev'], '-preparer', 'prepared by ' + bdisk['dev'], '-eltorito-boot', 'isolinux/isolinux.bin', '-eltorito-catalog', 'isolinux/boot.cat', '-no-emul-boot', '-boot-load-size', '4', '-boot-info-table', '-isohybrid-mbr', '{0}/root.{1}/usr/lib/syslinux/bios/isohdpfx.bin'.format(chrootdir, arch[0]), '-eltorito-alt-boot', '-e', 'EFI/{0}/{1}'.format(bdisk['name'], os.path.basename(efiboot_img)), '-no-emul-boot', '-isohybrid-gpt-basdat', '-output', isopath, bootdir] else: # UNTESTED. TODO. # I think i want to also get rid of: -boot-load-size 4, # -boot-info-table, ... possiblyyy -isohybrid-gpt-basedat... # https://wiki.archlinux.org/index.php/Unified_Extensible_Firmware_Interface#Remove_UEFI_boot_support_from_Optical_Media cmd = ['/usr/bin/xorriso', '-as', 'mkisofs', '-iso-level', '3', '-full-iso9660-filenames', '-volid', bdisk['name'] + '_MINI', '-appid', bdisk['desc'], '-publisher', bdisk['dev'], '-preparer', 'prepared by ' + bdisk['dev'], '-eltorito-boot', 'isolinux/isolinux.bin', '-eltorito-catalog', 'isolinux/boot.cat', '-no-emul-boot', '-boot-load-size', '4', '-boot-info-table', '-isohybrid-mbr', '{0}/root.{1}/usr/lib/syslinux/bios/isohdpfx.bin'.format(chrootdir, arch[0]), '-no-emul-boot', '-isohybrid-gpt-basdat', '-output', isopath, bootdir] DEVNULL = open(os.devnull, 'w') subprocess.call(cmd, stdout = DEVNULL, stderr = subprocess.STDOUT) # Get size of ISO iso['name'] = ['Mini'] iso['Mini'] = {} iso['Mini']['sha'] = hashlib.sha256() with open(isopath, 'rb') as f: while True: stream = f.read(65536) # 64kb chunks if not stream: break iso['Mini']['sha'].update(stream) iso['Mini']['sha'] = iso['Mini']['sha'].hexdigest() iso['Mini']['file'] = isopath iso['Mini']['size'] = humanize.naturalsize(os.path.getsize(isopath)) iso['Mini']['type'] = 'Mini' iso['Mini']['fmt'] = 'Hybrid ISO' return(iso) def tftpbootEnv(conf): build = conf['build'] ipxe = conf['ipxe'] sync = conf['sync'] if sync['tftp']: pass # TODO: generate a pxelinux.cfg in bdisk/tftp.py (to write) and sync in the ipxe chainloader here