diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93b8f73 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +esxi_vm_functions.pyc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..17fab91 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +all: + @echo "make install" + +install: + install -m 755 ./esxi-vm-create /usr/local/bin/ + install -m 755 ./esxi-vm-destroy /usr/local/bin/ + install -m 755 ./esxi_vm_functions.py /usr/local/bin/ + @echo "Install Success." + +uninstall: + rm -fr /usr/local/bin/esxi-vm-create + rm -fr /usr/local/bin/esxi-vm-destroy + rm -fr /usr/local/bin/esxi_vm_functions.py + rm -fr /usr/local/bin/esxi_vm_functions.pyc diff --git a/README.md b/README.md index 577398d..2985c98 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Usage By default the Network set set to "None". A full or partial MAC address can be specified. A partial MAC address argument would be 3 Hex pairs which would then be prepended by VMware's OEM "00:50:56". - By default the VM is powered on. If an ISO was specified, then it will boot the ISO image. Otherwise, the VM will attempt a PXE boot if a Network Interface was specified. You could customize the ISO image to specify the kickstart file, or PXE boot using COBBLER, Foreman, Razor, or your favorite provisioning tool. + By default the VM is powered on. If an ISO was specified, then it will boot the ISO image. Otherwise, the VM will attempt a PXE boot if a Network Interface was specified. You could customize the ISO image to specify the kickstart file, or PXE boot using COBBLER, Foreman, Razor, or your favorite provisioning tool. To help with automated provisioning, the script will output the full MAC address and exit code 0 on success. You can specify --summary to get a more detailed summary of the VM that was created. @@ -34,12 +34,12 @@ Usage Requirements ------------ - You must enable ssh access on your ESXi server. The VMware VIX API tools are not required. + You must enable ssh access on your ESXi server. Google 'how to enable ssh access on esxi' for instructions. The VMware VIX API tools are not required. It's HIGHLY RECOMMENDED to use password-less authentication by copying your ssh public keys to the ESXi host, otherwise your ESXi root password could be stored in clear-text in your home directory. Python and paramiko is a software requirement. - + ``` yum -y install python python-paramiko ``` @@ -50,9 +50,11 @@ Command Line Args ``` ./esxi-vm-create --help + usage: esxi-vm-create [-h] [-d] [-H HOST] [-U USER] [-P PASSWORD] [-n NAME] [-c CPU] [-m MEM] [-v HDISK] [-i ISO] [-N NET] [-M MAC] - [-S STORE] [-g GUESTOS] [-V] [--summary] [-u] + [-S STORE] [-g GUESTOS] [-o VMXOPTS] [-V] [--summary] + [-u] ESXi Create VM utility. @@ -65,20 +67,21 @@ optional arguments: ESXi Host password (*****) -n NAME, --name NAME VM name -c CPU, --cpu CPU Number of vCPUS (2) - -m MEM, --mem MEM Memory in GB (2) + -m MEM, --mem MEM Memory in GB (4) -v HDISK, --vdisk HDISK - Size of virt hdisk (12) + Size of virt hdisk (20) -i ISO, --iso ISO CDROM ISO Path | None (None) - -N NET, --net NET Network Interface | None (192.168.1) + -N NET, --net NET Network Interface | None (None) -M MAC, --mac MAC MAC address -S STORE, --store STORE - vmfs Store | LeastUsed (DS_3TB_m) + vmfs Store | LeastUsed (LeastUsed) -g GUESTOS, --guestos GUESTOS Guest OS. (centos-64) + -o VMXOPTS, --options VMXOPTS + Comma list of VMX Options. -V, --verbose Enable Verbose mode (False) --summary Display Summary (False) -u, --updateDefaults Update Default VM settings stored in ~/.esxi-vm.yml - ``` @@ -176,6 +179,12 @@ Guest OS: centos-64 MAC: 00:0c:29:ea:a0:42 00:0c:29:ea:a0:42 +``` + + Merge/Add extra VMX options, saved as default. +``` +./esxi-vm-create -o 'floppy0.present = "TRUE",svga.autodetect = "TRUE",svga.present = "TRUE"' -u +Saving new Defaults to ~/.esxi-vm.yml ``` License @@ -202,4 +211,3 @@ Support Website : http://www.jintegrate.co github : http://github.com/josenk/ - diff --git a/esxi-vm-create b/esxi-vm-create index ed9a4b3..818c6b5 100755 --- a/esxi-vm-create +++ b/esxi-vm-create @@ -31,6 +31,7 @@ STORE = ConfigData['STORE'] NET = ConfigData['NET'] ISO = ConfigData['ISO'] GUESTOS = ConfigData['GUESTOS'] +VMXOPTS = ConfigData['VMXOPTS'] ErrorMessages = "" MAC = "" @@ -60,6 +61,7 @@ parser.add_argument("-N", "--net", dest='NET', type=str, help="Network Interface parser.add_argument("-M", "--mac", dest='MAC', type=str, help="MAC address") parser.add_argument("-S", "--store", dest='STORE', type=str, help="vmfs Store | LeastUsed (" + str(STORE) + ")") parser.add_argument("-g", "--guestos", dest='GUESTOS', type=str, help="Guest OS. (" + str(GUESTOS) + ")") +parser.add_argument("-o", "--options", dest='VMXOPTS', type=str, default='NIL', help="Comma list of VMX Options.") parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")") parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")") parser.add_argument("-u", "--updateDefaults", dest='UPDATE', action='store_true', help="Update Default VM settings stored in ~/.esxi-vm.yml") @@ -100,11 +102,17 @@ if STORE == "": STORE = "LeastUsed" if args.GUESTOS: GUESTOS=args.GUESTOS +if args.VMXOPTS == '' and VMXOPTS != '': + VMXOPTS='' +if args.VMXOPTS and args.VMXOPTS != 'NIL': + VMXOPTS=args.VMXOPTS.split(",") + if args.UPDATE: print "Saving new Defaults to ~/.esxi-vm.yml" ConfigData['isDryRun'] = isDryRun ConfigData['isVerbose'] = isVerbose + ConfigData['isSummary'] = isSummary ConfigData['HOST'] = HOST ConfigData['USER'] = USER ConfigData['PASSWORD'] = PASSWORD @@ -117,6 +125,7 @@ if args.UPDATE: ConfigData['NET'] = NET ConfigData['ISO'] = ISO ConfigData['GUESTOS'] = GUESTOS + ConfigData['VMXOPTS'] = VMXOPTS SaveConfig(ConfigData) if NAME == "": sys.exit(0) @@ -194,7 +203,7 @@ if MAC != "": print "ERROR: " + MAC + " Invalid MAC address." ErrorMessages += " " + MAC + " Invalid MAC address." CheckHasErrors = True - + # # Get from ESXi host if ISO exists @@ -353,6 +362,33 @@ if NET != "None": VMX.append('ethernet0.addressType = "static"') VMX.append('ethernet0.address = "' + MAC + '"') +# +# Merge extra VMX options +for VMXopt in VMXOPTS: + try: + k,v = VMXopt.split("=") + except: + k="" + v="" + key = k.lstrip().strip() + value = v.lstrip().strip() + for i in VMX: + try: + ikey,ivalue = i.split("=") + except: + break + if ikey.lstrip().strip().lower() == key.lower(): + index = VMX.index(i) + VMX[index] = ikey + " = " + value + break + else: + if key != '' and value != '': + VMX.append(key + " = " + value) + +if isVerbose and VMXOPTS != '': + print "VMX file:" + for i in VMX: + print i MyVM = FullPath + "/" + NAME if CheckHasErrors: @@ -384,7 +420,7 @@ if not isDryRun and not CheckHasErrors: (stdin, stdout, stderr) = ssh.exec_command("vim-cmd solo/registervm " + MyVM + ".vmx") type(stdin) VMID = int(stdout.readlines()[0]) - + # Power on VM if isVerbose: print "Power ON VM" @@ -453,7 +489,7 @@ if isSummary: if isVerbose: print "Format: " + DISKFORMAT print "DS Store: " + DSSTORE - print "Network: " + NET + print "Network: " + NET if ISO: print "ISO: " + ISO if isVerbose: @@ -472,5 +508,3 @@ else: else: print GeneratedMAC sys.exit(0) - - diff --git a/esxi-vm-destroy b/esxi-vm-destroy new file mode 100755 index 0000000..1ba42c9 --- /dev/null +++ b/esxi-vm-destroy @@ -0,0 +1,224 @@ +#!/usr/bin/python + + +import argparse # Argument parser +import datetime # For current Date/Time +import time +import os.path # To check if file exists +import sys # For args +import re # For regex +import paramiko # For remote ssh +import yaml +import warnings + +from esxi_vm_functions import * + +# Defaults and Variable setup +ConfigData = setup_config() +NAME = "" +LOG = ConfigData['LOG'] +isDryRun = ConfigData['isDryRun'] +isVerbose = ConfigData['isVerbose'] +isSummary = ConfigData['isSummary'] +HOST = ConfigData['HOST'] +USER = ConfigData['USER'] +PASSWORD = ConfigData['PASSWORD'] +CPU = ConfigData['CPU'] +MEM = ConfigData['MEM'] +HDISK = int(ConfigData['HDISK']) +DISKFORMAT = ConfigData['DISKFORMAT'] +VIRTDEV = ConfigData['VIRTDEV'] +STORE = ConfigData['STORE'] +NET = ConfigData['NET'] +ISO = ConfigData['ISO'] +GUESTOS = ConfigData['GUESTOS'] + +ErrorMessages = "" +CheckHasErrors = False +DSPATH="" +DSSTORE="" + +# +# Process Arguments +# +parser = argparse.ArgumentParser(description='ESXi Create VM utility.') + +parser.add_argument("-H", "--Host", dest='HOST', type=str, help="ESXi Host/IP (" + str(HOST) + ")") +parser.add_argument("-U", "--User", dest='USER', type=str, help="ESXi Host username (" + str(USER) + ")") +parser.add_argument("-P", "--Password", dest='PASSWORD', type=str, help="ESXi Host password (*****)") +parser.add_argument("-n", "--name", dest='NAME', type=str, help="VM name") +parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")") +parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")") + + +args = parser.parse_args() + +if args.isVerbosearg: + isVerbose = True +if args.isSummaryarg: + isSummary = True +if args.HOST: + HOST=args.HOST +if args.USER: + USER=args.USER +if args.PASSWORD: + PASSWORD=args.PASSWORD +if args.NAME: + NAME=args.NAME + +# +# main() +# +LogOutput = '{' +LogOutput += '"datetime":"' + str(theCurrDateTime()) + '",' + +if NAME == "": + print "ERROR: Missing required option --name" + sys.exit(1) + +try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(HOST, username=USER, password=PASSWORD) + + (stdin, stdout, stderr) = ssh.exec_command("esxcli system version get |grep Version") + type(stdin) + if re.match("Version", str(stdout.readlines())) is not None: + print "Unable to determine if this is a ESXi Host: %s, username: %s" % (HOST, USER) + sys.exit(1) +except: + print "The Error is " + str(sys.exc_info()[0]) + print "Unable to access ESXi Host: %s, username: %s" % (HOST, USER) + sys.exit(1) + +# +# Check if VM exists +# +VMID = -1 +try: + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/getallvms") + type(stdin) + for line in stdout.readlines(): + splitLine = line.split() + if NAME == splitLine[1]: + VMID = splitLine[0] + JNK = line.split('[')[1] + STORE = JNK.split(']')[0] + VMDIR = splitLine[3] + + if VMID == -1: + print "Warning: VM " + NAME + " doesn't exists." + ErrorMessages += " VM " + NAME + " doesn't exists." + CheckHasErrors = True + CheckHasWarnings = True +except: + print "The Error is " + str(sys.exc_info()[0]) + sys.exit(1) + +# Get List of Volumes, +try: + (stdin, stdout, stderr) = ssh.exec_command("esxcli storage filesystem list |grep '/vmfs/volumes/.*true VMFS' |sort -nk7") + type(stdin) + VOLUMES = {} + for line in stdout.readlines(): + splitLine = line.split() + VOLUMES[splitLine[0]] = splitLine[1] +except: + print "The Error is " + str(sys.exc_info()[0]) + sys.exit(1) + + +# Convert STORE to path and visa-versa +V = [] +for Path in VOLUMES: + V.append(VOLUMES[Path]) + if STORE == Path or STORE == VOLUMES[Path]: + DSPATH = Path + DSSTORE = VOLUMES[Path] + + +if CheckHasErrors: + Result = "Errors" +else: + Result = "Success" + +if not CheckHasErrors: + try: + + CurrentState = "" + CurrentStateCounter = 0 + while CurrentState != "off": + if isVerbose: + print "Get state VM" + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.getstate " + str(VMID)) + type(stdin) + lines = str(stdout.readlines()) + str(stderr.readlines()) + if isVerbose: + print "power.getstate: " + lines + if re.search("Powered off", lines): + CurrentState = "off" + + # Power off VM + if isVerbose: + print "Power OFF VM" + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.off " + str(VMID) + " ||echo") + type(stdin) + lines = str(stdout.readlines()) + str(stderr.readlines()) + if isVerbose: + print "power.off: " + str(lines) + + CurrentStateCounter += 1 + if CurrentStateCounter >10: + break + time.sleep(1) + + # destroy VM + if isVerbose: + print "Destroy VM" + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/destroy " + str(VMID)) + type(stdin) + lines = str(stdout.readlines()) + str(stderr.readlines()) + if isVerbose: + print "destroy: " + str(lines) + + except: + print "There was an error destroying the VM." + ErrorMessages += " There was an error destroying the VM." + CheckHasErrors = True + Result = "Fail" + +# Print Summary + +# +# The output log string +LogOutput += '"Host":"' + HOST + '",' +LogOutput += '"Name":"' + NAME + '",' +LogOutput += '"Store Used":"' + DSPATH + '",' +LogOutput += '"Verbose":"' + str(isVerbose) + '",' +if ErrorMessages != "": + LogOutput += '"Error Message":"' + ErrorMessages + '",' +LogOutput += '"Result":"' + Result + '",' +LogOutput += '"Completion Time":"' + str(theCurrDateTime()) + '"' +LogOutput += '}\n' +try: + with open(LOG, "a+w") as FD: + FD.write(LogOutput) +except: + print "Error writing to log file: " + LOG + +if isSummary: + if isVerbose: + print "ESXi Host: " + HOST + print "VM NAME: " + NAME + print "Path: " + DSSTORE +else: + pass + +if CheckHasErrors and not CheckHasWarnings: + print "Failed" + sys.exit(1) +else: + print "Success" + sys.exit(0) + + diff --git a/esxi_vm_functions.py b/esxi_vm_functions.py index f973b21..bdd165c 100755 --- a/esxi_vm_functions.py +++ b/esxi_vm_functions.py @@ -57,7 +57,10 @@ def setup_config(): ISO="None", # Default GuestOS type. (See VMware documentation for all available options) - GUESTOS="centos-64" + GUESTOS="centos-64", + + # Extra VMX options + VMXOPTS="" ) ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml" @@ -112,4 +115,3 @@ def float2human(num): return '0 bytes' if num == 1: return '1 byte' -