#!/build/toolchain/lin64/python-3.5.1-openssl1.0.2/bin/python ''' Copyright 2018-2020 VMware, Inc. All rights reserved. -- VMware Confidential ''' ''' The imageCreator module of the ESX Packaging Kit module is meant to be the utility used to create an ESX image deliverable, such as an iso, raw disk image or pxe setup. This module will act as a high level orchestrator that will make calls into other image libraries that perform various operations like ISO creation, raw image creation, etc... ''' from argparse import ArgumentParser import glob import json import os import sys import shutil EPK_LIB_DIR = os.path.join(os.path.dirname(os.path.dirname( os.path.realpath(os.path.abspath(__file__)))), 'epkLib') # Add epkCommon to the path and use it to setup paths and load shared libs sys.path.append(EPK_LIB_DIR) from epkCommon import (CheckVendorName, CheckVendorCode, ConfigureDirs, epkBaseDir, GetVibConfigSchemas, ExitWithMsg, GetErrorMsg, PromptWithChoice, LoadSharedLibs, SetupSysPath) SetupSysPath() LoadSharedLibs() from vmware.esximage import ImageProfile, OfflineBundle from vmware.esximage.ImageBuilder import EsxIsoImage, EsxRawImage, EsxPxeImage from vmware.esximage.ImageManager import DepotMgr, SoftwareSpecMgr from vmware.esximage.Utils.BootCfg import BootCfg from vmware.esximage import Bulletin, ReleaseCollection, VibCollection ISO_PARSER_OPTION = 'iso' RAW_PARSER_OPTION = 'raw' PXE_PARSER_OPTION = 'pxe' PXE_TIMEOUT = 15 PXE_NOHALT = 1 PXE_LABEL = 'VMvisor' PXE_IPAPPEND = 2 PXEMENU_TXT = """DEFAULT %(menuProg)s\n MENU TITLE %(menuTitle)s TIMEOUT %(timeout)d NOHALT %(noHalt)d\n LABEL %(label)s MENU DEFAULT MENU LABEL %(menuLabel)s\n KERNEL %(mbootProg)s APPEND -c %(bootCfg)s IPAPPEND %(ipAppend)d""" IPXEMENU_TXT = """DEFAULT %(menuProg)s\n LABEL nextmenu MENU HIDE KERNEL %(kernelMenu)s\n MENU TITLE %(menuTitle)s TIMEOUT %(timeout)d NOHALT %(noHalt)d\n LABEL %(label)s MENU DEFAULT MENU LABEL %(menuLabel)s\n KERNEL %(mbootProg)s APPEND -c %(bootCfg)s IPAPPEND %(ipAppend)d""" PXE_PAYLOAD_SUBDIR = 'payload' def CreateArgumentParser(): """Creates the argument parser and image subparsers and then parsers the command line. Returns: * Command line argument parser """ parser = ArgumentParser() subParser = parser.add_subparsers(dest='formatOption') # Create a parser for the format sub options formatGroup = subParser.add_parser('format', help='Image format creation' ' options') formatGroup.set_defaults(func=CreateImage) formatSubParsers = formatGroup.add_subparsers(dest='format') # Create image, raw, and pxe sub parsers isoParser = formatSubParsers.add_parser(ISO_PARSER_OPTION, help='Create an ISO image') rawParser = formatSubParsers.add_parser(RAW_PARSER_OPTION, help='Create a raw (DD) image') pxeParser = formatSubParsers.add_parser(PXE_PARSER_OPTION, help='Create a PXE image') for formatSubParser in [isoParser, rawParser, pxeParser]: formatSubParser.add_argument('-s', '--software-spec', dest='softwareSpecFile', required=True, metavar='.json', help='Software Specification File') formatSubParser.add_argument('-d', '--depot', dest='depots', required=True, action='append', metavar='.zip', help='Depots which contains the payloads' ' to create the image. This parameter' ' can be used multiple times if' ' multiple depots are needed.') formatSubParser.add_argument('-k', '--kernel-opts', dest='kernelOpts', help='Additional kernel options other than ' 'feature states, as a string ' 'separated by space(s) containg ' 'key=value pairs. Example: ' '"abc def=1 xyz=\'abc\'".') formatSubParser.add_argument('-f', '--no-acceptance-check', dest='noAcceptanceCheck', action='store_true', default=False, help='Skip the acceptance validation of' ' each VIB. An error occurs when a' ' VIB fails to pass the check.' ' Defaults to False.') for formatSubParser in [isoParser, rawParser]: formatSubParser.add_argument('-o', '--output-image-file', dest='outputImageFile', required=True, metavar='.', help='Output Image File') # The ISO/PXE option will typically be used to create the installer image # as that is what is needed by a user under normal circumstances. If # for whatever reason a user wants a live image, it can # be created with the option below. This doesn't apply to # raw images since we are actually writing the OS disk image to file. for formatSubParser in [isoParser, pxeParser]: formatSubParser.add_argument('-l', '--live-image', dest='liveImage', action='store_true', default=False, help='Creates the live image instead of' ' the installer image. Defaults to' ' False.') pxeParser.add_argument('-p', '--pxeDir', dest='pxeDir', required=True, help='Directory to store pxe config and payload') pxeParser.add_argument('-i', '--iPxeUrl', dest='iPxeUrl', help='URL to use for iPXE. Setting this option' ' creates a PXE configuration that' ' uses iPXE instead of the standard TFTP PXE') for formatSubParser in [isoParser, rawParser, pxeParser]: formatSubParser.add_argument('-z', '--output-depot-file', dest='outputDepotFilename', metavar='.zip', help='An output depot containing the' ' contents of the image') formatSubParser.add_argument('-n', '--vendor-name', dest='vendorName', default='', help='The vendor name of the output depot') formatSubParser.add_argument('-c', '--vendor-code', dest='vendorCode', default='', help='The vendor code of the output depot') formatSubParser.add_argument('-r', '--rollup-bulletin', dest='createRollupBulletin', default=False, action='store_true', help='Set to true to create a Rollup' ' Bulletin in the output depot') global formatHelpStr formatHelpStr = formatGroup.format_help() return parser def GetJsonFromFile(filename): """Opens up a file and attempts extract a json structure from it. Throws an exception on failure. Parameters: * filename - The filename to extact JSON from Returns: * JSON structure loaded from the file Raises: IOError: If there was an error parsing JSON from the file """ try: with open(filename, 'r') as f: return json.load(f) except Exception as e: raise IOError('Failed to parse file: %s, error: %s.' % (filename, e)) def CreatePxeBootMenu(bootMenuDir, pxePayloadDir, iPxeUrl=None): """The function creates the pxe boot menu file named "default" in the bootMenuDir. Parameters: * bootMenuDir: The python file object referring to the pxe boot menu dir ("pxelinux.cfg"). * pxePayloadDir: The python file object referring to pxe payload directory containing VIBs and pxe boot binaries. * iPxeUrl: The URL to uses for ipxe mode """ menuTitle = 'ESXi PXE Boot Menu' menuLabel = 'ESXi' bootMenuFile = os.path.join(bootMenuDir, 'default') if iPxeUrl: ifgPxe = os.path.join(pxePayloadDir, 'ifgpxe.c32') shutil.copyfile(glob.glob(os.path.join(EPK_LIB_DIR, 'VMware-ifgpxe-*.c32'))[0], ifgPxe) iPxeUndi = os.path.join(pxePayloadDir, 'ipxe-undionly.0') shutil.copyfile(os.path.join(EPK_LIB_DIR, 'undionly.kpxe.nomcast'), iPxeUndi) menuProg = '%s nextmenu -- %s' % (ifgPxe, iPxeUndi) mbootProg = iPxeUrl + '/' + os.path.join(PXE_PAYLOAD_SUBDIR, 'mboot.c32') bootCfg = iPxeUrl + '/' + os.path.join(PXE_PAYLOAD_SUBDIR, 'boot.cfg') kernelMenu = iPxeUrl + '/' + os.path.join(PXE_PAYLOAD_SUBDIR, 'menu.c32') ipxeMenuArgs = { 'menuProg': menuProg, 'kernelMenu': kernelMenu, 'menuTitle': menuTitle, 'timeout': PXE_TIMEOUT, 'noHalt': PXE_NOHALT, 'menuLabel': menuLabel, 'mbootProg': mbootProg, 'bootCfg': bootCfg, 'label': PXE_LABEL, 'ipAppend': PXE_IPAPPEND, } with open(bootMenuFile, 'w') as f: f.write(IPXEMENU_TXT % ipxeMenuArgs) else: menuProg = os.path.join(pxePayloadDir, 'menu.c32') mbootProg = os.path.join(pxePayloadDir, 'mboot.c32') bootCfg = os.path.join(pxePayloadDir, 'boot.cfg') for f in [menuProg, mbootProg, bootCfg]: if not os.path.exists(f): sys.exit('Failed to create pxe boot menu.\n' 'Reason: The file %s is missing in pxe payload ' 'dir %s.\n' % (f, pxePayloadDir)) pxeMenuArgs = { 'menuProg': menuProg, 'menuTitle': menuTitle, 'timeout': PXE_TIMEOUT, 'noHalt': PXE_NOHALT, 'menuLabel': menuLabel, 'mbootProg': mbootProg, 'bootCfg': bootCfg, 'label': PXE_LABEL, 'ipAppend': PXE_IPAPPEND, } with open(bootMenuFile, 'w') as f: f.write(PXEMENU_TXT % pxeMenuArgs) def CreatePxeImage(imageProfile, pxeDir, kernelOpts, installer, checkAcceptance, iPxeUrl=None): """The function creates the pxe image. It will prompt user for confirming whether to overwrite existing pxe image in pxe directory. Parameters: * imageProfile: The imageProfile that contains metadata and VIBs to create the pxe image * pxeDir: The directory to write the PXE image files * kernelOpts: Kernel boot options, this a 'name: value', dictionary containing kernel options and their values. * installer: True to create an installer image, false for the live image * checkAcceptance: True to run tests that check the vib's acceptance level * iPxeUrl: The iPXE URL to use """ pxePayloadDir = os.path.join(pxeDir, PXE_PAYLOAD_SUBDIR) pxeLinuxCfgDir = os.path.join(pxeDir, 'pxelinux.cfg') if os.path.exists(pxeLinuxCfgDir) or os.path.exists(pxePayloadDir): # Prompt user to confirm before overwrite existing image. prompt = '\nWarning: The command will overwrite the existing\n'\ '\t pxe image in the directory %s.\n\t Do you to '\ 'want to continue?' % (pxeDir) overwriteAccepted = PromptWithChoice(prompt) if not overwriteAccepted: sys.exit(0) try: shutil.rmtree(pxeDir) except Exception as err: ExitWithMsg('Failed to clean pxe directory. Reason: ' '%s' % GetErrorMsg(err)) try: os.makedirs(pxePayloadDir, exist_ok=True) except Exception as err: ExitWithMsg('Failed to create pxe payload directory.' ' Reason: %s' % GetErrorMsg(err)) try: pxeImg = EsxPxeImage.EsxPxeImage(imageProfile) if iPxeUrl: bootCfgPrefix = iPxeUrl + '/' + PXE_PAYLOAD_SUBDIR else: bootCfgPrefix = os.path.abspath(pxePayloadDir) # This ensures the bootCfgPrefix ends with a '/' bootCfgPrefix = os.path.join(bootCfgPrefix, '') pxeImg.Write(pxePayloadDir, pxeUrl=None, installer=installer, kernelopts=kernelOpts, checkacceptance=checkAcceptance, bootCfgPrefix=bootCfgPrefix) except Exception as err: ExitWithMsg('Failed to create pxe image. Reason: %s' % GetErrorMsg(err)) try: os.mkdir(pxeLinuxCfgDir) except Exception as err: ExitWithMsg('Failed to create pxe config directory: %s.' ' Reason: %s' % (pxeLinuxCfgDir, GetErrorMsg(err))) # Create a boot menu cfg file. CreatePxeBootMenu(pxeLinuxCfgDir, pxePayloadDir, iPxeUrl) def CreateOutputDepot(imageProfile, vendorName, vendorCode, createRollupBulletin, checkAcceptance, outputDepotFilename, softwareSpecMgr, depotMgr): """Creates an output depot based upon an ImageProfile Parameters: * ImageProfile - The ImageProfile to create the depot from * vendorName - The vendor name for the depot * vendorCode - The vendor code for the depot * createRollupBulletin - Set to true to create a Rollup Bulletin based upon the input image profile * checkAcceptance - True to run tests that check the vibs' acceptance level * outputDepotFilename - The name for the output depot * softwareSpecMgr - The software spec mgr that contains components * depotMgr - The depot mgr that contains vibs """ profiles = ImageProfile.ImageProfileCollection() profiles.AddProfile(imageProfile) vibs = VibCollection.VibCollection() components = Bulletin.ComponentCollection() addons = ReleaseCollection.AddonCollection() if imageProfile.addon: addons[imageProfile.addon.releaseID] = imageProfile.addon addonComponents, addonVibs = GetReleaseUnitComponentsAndVibs(imageProfile.addon, softwareSpecMgr, depotMgr) components += addonComponents vibs += addonVibs manifests = imageProfile.manifests for manifest in manifests.values(): manifestComponents, manifestVibs = GetReleaseUnitComponentsAndVibs(manifest, softwareSpecMgr, depotMgr) vibs += manifestVibs components += manifestComponents baseimages = ReleaseCollection.BaseImageCollection() if imageProfile.baseimage: baseimages[imageProfile.baseimage.releaseID] = imageProfile.baseimage baseImageComponents, baseImageVibs = GetReleaseUnitComponentsAndVibs(imageProfile.baseimage, softwareSpecMgr, depotMgr) vibs += baseImageVibs components += baseImageComponents vibConfigSchemas = GetVibConfigSchemas(vibs, depotMgr._dc.configSchemas) components += imageProfile.components vibs += imageProfile.vibs CheckVendorName(vendorName) CheckVendorCode(vendorCode) version = imageProfile.baseimage.versionSpec.version.versionstring platformVer = version[0:3] bulletins = Bulletin.BulletinCollection() description = '%s Rollup Bulletin for ESXi %s' % (imageProfile.creator, version) if createRollupBulletin: bullArgs = {'id': 'Rollup-Bulletin-For-%s' % (imageProfile.name), 'vendor': imageProfile.creator, 'summary': description, 'severity': 'general', 'category': 'bugfix', 'urgency': 'moderate', 'releasetype': 'rollup', 'description': description, 'releasedate': imageProfile.creationtime, 'platforms': [(platformVer, '', 'embeddedEsx')], 'vibids': imageProfile.vibIDs, } bulletin = Bulletin.Bulletin(**bullArgs) bulletins.AddBulletin(bulletin) OfflineBundle.WriteOfflineBundle(outputDepotFilename, vendorName, vendorCode, baseimages, addons, manifests, None, profiles, components, vibs, configSchemas=vibConfigSchemas, legacyBulletins=bulletins, checkAcceptance=checkAcceptance) def CreateImage(opts): """Creates an Image, either ISO or Raw based upon command line options Parameters: * opts - Command line options including software spec file name depot file name, image type and image output name """ if not opts.format: ExitWithMsg(formatHelpStr, False) # These two variables provided from the command line input are # opposite to the parameters of the ImageBuilder classes. # We convert these variables to what the class expects by # default for readablity. checkAcceptance = not opts.noAcceptanceCheck if hasattr(opts, 'liveImage'): installer = not opts.liveImage # Extract the software spec file from the command line, and create a depot # specification based upon the depots passed in. softwareSpec = GetJsonFromFile(opts.softwareSpecFile) depotSpecsList = [] i = 1 for depotPath in opts.depots: depotSpec = {} depotSpec['name'] = 'depot' + str(i) depotSpec['url'] = depotPath i += 1 depotSpecsList.append(depotSpec) # Configures schema, and certs dirs for the esximage library ConfigureDirs() # Use Software Spec manager to turn the software specification and # depot specitication into an image profile. depotMgr = DepotMgr.DepotMgr(depotSpecs=depotSpecsList, connect=True) softwareSpecMgr = SoftwareSpecMgr.SoftwareSpecMgr(depotMgr, softwareSpec) # The default is to ignore not found for VC, but this isn't the case for # the EPK softwareSpecMgr.hasManifest = True try: imageProfileName = 'VMware Lifecycle Manager Generated Image' imageProfileVendor = 'VMware, Inc.' addon = softwareSpecMgr._getAddon() baseImage = softwareSpecMgr._getBaseImage() if addon: name = addon.nameSpec.name version = addon.versionSpec.version.versionstring imageProfileName = '%s_%s' % (name, version) imageProfileVendor = addon.vendor elif baseImage: version = baseImage.versionSpec.version.versionstring imageProfileName = 'ESXi_%s' % version imageProfileVendor = baseImage.vendor imageProfile = softwareSpecMgr.validateAndReturnImageProfile( name=imageProfileName, creator=imageProfileVendor, checkAcceptance=checkAcceptance) except Exception as err: ExitWithMsg('Failed to generate the image. Error: %s' % GetErrorMsg(err)) kernelOpts = dict() if opts.kernelOpts: try: kernelOpts = BootCfg.kerneloptToDict(opts.kernelOpts) except Exception as err: sys.exit('Failed to extract kernel options. ' 'Reason: %s, input kernelOpts: %s' % (GetErrorMsg(err), str(opts.kernelOpts))) if opts.format == ISO_PARSER_OPTION: esxIsoImage = EsxIsoImage.EsxIsoImage(imageProfile) esxIsoImage.Write(opts.outputImageFile, checkacceptance=checkAcceptance, kernelopts=kernelOpts, installer=installer) elif opts.format == RAW_PARSER_OPTION: esxRawImage = EsxRawImage.EsxRawImage(imageProfile, os.path.join(EPK_LIB_DIR, 'mmd'), os.path.join(EPK_LIB_DIR, 'mcopy'), os.path.join(epkBaseDir, 'lib64', 'libvfat.so')) esxRawImage.Write(opts.outputImageFile, checkacceptance=checkAcceptance, kernelopts=kernelOpts) elif opts.format == PXE_PARSER_OPTION: CreatePxeImage(imageProfile, opts.pxeDir, kernelOpts, installer, checkAcceptance, opts.iPxeUrl) if opts.outputDepotFilename: CreateOutputDepot(imageProfile, opts.vendorName, opts.vendorCode, opts.createRollupBulletin, checkAcceptance, opts.outputDepotFilename, softwareSpecMgr, depotMgr) sys.stdout.write('Successfully created %s image\n' % str(opts.format)) return 0 def GetReleaseUnitComponentsAndVibs(releaseUnit, softwareSpecMgr, depotMgr): """Gets the components and vibs of a release unit Parameters: * releaseUnit - The release unit to get the components and vibs from * softwareSpecMgr - The software spec mgr that contains the components * depotMgr - The depot mgr that contains the vibs Returns: The component and vibs of a release unit """ vibs = VibCollection.VibCollection() components = Bulletin.ComponentCollection() for name, version in releaseUnit.components.items(): component = softwareSpecMgr.getComponent(name, version) components.AddComponent(component) vibs += component.GetVibCollection(depotMgr.vibs) return components, vibs def main(argv): try: argParser = CreateArgumentParser() opts = argParser.parse_args() if not opts.formatOption: argParser.print_help() sys.exit(1) sys.exit(opts.func(opts)) except Exception as e: sys.stderr.write('%s\n' % GetErrorMsg(e)) sys.exit(1) if __name__ == "__main__": main(sys.argv)