commit 390d04b57b7f9fd2bf4cc851a32b4d2d612745f8 Author: Jonathan Senkerik Date: Wed May 3 16:00:03 2017 -0400 Init commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc3cf71 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +Introduction +------------ + + This utility is a simple to use comand line tool to create VMs on an ESXi host from from a system running python and ssh. vCenter is not required. You need to enable ssh access on your ESXi server. It's HIGHLY RECOMMENDED to use password-less authentication by copying your ssh public keys to the ESXi host. + + +Command Line Args +----------------- + +``` + ./esxi-vm-create -h + usage: esxi-vm-create [-h] [-d] [-v] [-H HOST] [-U USER] [-P PASSWORD] + [-n NAME] [-c CPU] [-m MEM] [-s SIZE] [-i ISO] [-N NET] + [-S STORE] [-g GUESTOS] [-u] + + ESXi Create VM utility. + + optional arguments: + -h, --help show this help message and exit + -d, --dry Enable Dry Run mode (False) + -v, --verbose Enable Verbose mode (False) + -H HOST, --Host HOST ESXi Host (esxi) + -U USER, --User USER ESXi Host username (root) + -P PASSWORD, --Password PASSWORD + ESXi Host password (*****) + -n NAME, --name NAME VM name + -c CPU, --cpu CPU Number of vCPUS (2) + -m MEM, --mem MEM Memory in GB (4) + -s SIZE, --size SIZE Size of virt disk (20) + -i ISO, --iso ISO CDROM ISO Path | None (None) + -N NET, --net NET Network Interface | None (None) + -S STORE, --store STORE + vmfs Store | LeastUsed (DS_3TB_m) + -g GUESTOS, --guestos GUESTOS + Guest OS. (centos-64) + -u, --updateDefaults Update Default VM settings stored in ~/.esxi-vm.yml + +``` + + +Examples +-------- + + + Create a new VM named testvm01 using all defaults from ~/.esxi-vm.yml. +``` + ./esxi-vm-create -n testvm01 + + Create VM Success + ESXi Host: esxi + VM NAME: testvm01 + vCPU: 2 + Memory: 4GB + VM Disk: 20GB + DS Store: DS_4TB + Network: None + +``` + + Change default number of vCPUs to 4, Memory to 8GB and vDisk size to 40GB. +``` + ./esxi-vm-create -c 4 -m 8 -s 40 -u + Saving new Defaults to ~/.esxi-vm.yml +``` + + Create a new VM named testvm02 using new defaults from ~/.esxi-vm.yml and specifying a Network interface. +``` + ./esxi-vm-create -n testvm02 -N 192.168.1 + + Create VM Success + ESXi Host: esxi + VM NAME: testvm02 + vCPU: 4 + Memory: 8GB + VM Disk: 40GB + DS Store: DS_4TB + Network: 192.168.1 +``` + + Available Network Interfaces and Available Disk Storage volumes will be listed if an invalid entry is made. + +``` + ./esxi-vm-create -n testvm03 -N BadNet -S BadDS + ERROR: Disk Storage BadDS doesn't exist. + Available Disk Stores: ['DS_SSD500s', 'DS_SSD500c', 'DS_SSD250', 'DS_4TB', 'DS_3TB_m'] + LeastUsed Disk Store : DS_4TB + ERROR: Virtual NIC BadNet doesn't exist. + Available VM NICs: ['192.168.1', '192.168.0', 'VM Network test'] or 'None' +``` + + Create a new VM named testvm03 using a valid Network Interface, valid Disk Storage volume, enabled verbose and saving the settings as default. +``` + ./esxi-vm-create -n testvm03 -N 192.168.1 -S DS_3TB_m -v -u + Saving new Defaults to ~/.esxi-vm.yml + Create testvm03.vmx file + Create testvm03.vmdk file + Register VM + Power ON VM + + Create VM Success + ESXi Host: esxi + VM NAME: testvm03 + vCPU: 4 + Memory: 8GB + VM Disk: 40GB + Format: thin + DS Store: DS_3TB_m + Network: 192.168.1 + Guest OS: centos-64 +``` + +License +------- + +Copyright (C) 2017 Jonathan Senkerik + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + + +Support +------- + Website : http://www.jintegrate.co + + github : http://github.com/josenk/ + diff --git a/esxi-vm-create b/esxi-vm-create new file mode 100755 index 0000000..611da84 --- /dev/null +++ b/esxi-vm-create @@ -0,0 +1,369 @@ +#!/usr/bin/python + + +import argparse # Argument parser +import datetime # For current Date/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'] +HOST = ConfigData['HOST'] +USER = ConfigData['USER'] +PASSWORD = ConfigData['PASSWORD'] +CPU = ConfigData['CPU'] +MEM = ConfigData['MEM'] +SIZE = int(ConfigData['SIZE']) +DISKFORMAT = ConfigData['DISKFORMAT'] +VIRTDEV = ConfigData['VIRTDEV'] +STORE = ConfigData['STORE'] +NET = ConfigData['NET'] +ISO = ConfigData['ISO'] +GUESTOS = ConfigData['GUESTOS'] + + + +# +# Process Arguments +# +parser = argparse.ArgumentParser(description='ESXi Create VM utility.') + +parser.add_argument('-d', '--dry', dest='isDryRunarg', action='store_true', help="Enable Dry Run mode (" + str(isDryRun) + ")") +parser.add_argument('-v', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")") +parser.add_argument("-H", "--Host", dest='HOST', type=str, help="ESXi Host (" + 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("-c", "--cpu", dest='CPU', type=int, help="Number of vCPUS (" + str(CPU) + ")") +parser.add_argument("-m", "--mem", type=int, help="Memory in GB (" + str(MEM) + ")") +parser.add_argument("-s", "--size", dest='SIZE', type=str, help="Size of virt disk (" + str(SIZE) + ")") +parser.add_argument("-i", "--iso", dest='ISO', type=str, help="CDROM ISO Path | None (" + str(ISO) + ")") +parser.add_argument("-N", "--net", dest='NET', type=str, help="Network Interface | None (" + str(NET) + ")") +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("-u", "--updateDefaults", dest='UPDATE', action='store_true', help="Update Default VM settings stored in ~/.esxi-vm.yml") + +args = parser.parse_args() + +if args.isDryRunarg: + isDryRun = True +if args.isVerbosearg: + isVerbose = 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 +if args.CPU: + CPU=int(args.CPU) +if args.mem: + MEM=int(args.mem) +if args.SIZE: + SIZE=int(args.SIZE) +if args.ISO: + ISO=args.ISO +if args.NET: + NET=args.NET +if args.STORE: + STORE=args.STORE +if STORE == "": + STORE = "LeastUsed" +if args.GUESTOS: + GUESTOS=args.GUESTOS + +if args.UPDATE: + print "Saving new Defaults to ~/.esxi-vm.yml" + ConfigData['isDryRun'] = isDryRun + ConfigData['isVerbose'] = isVerbose + ConfigData['HOST'] = HOST + ConfigData['USER'] = USER + ConfigData['PASSWORD'] = PASSWORD + ConfigData['CPU'] = CPU + ConfigData['MEM'] = MEM + ConfigData['SIZE'] = SIZE + ConfigData['DISKFORMAT'] = DISKFORMAT + ConfigData['VIRTDEV'] = VIRTDEV + ConfigData['STORE'] = STORE + ConfigData['NET'] = NET + ConfigData['ISO'] = ISO + ConfigData['GUESTOS'] = GUESTOS + SaveConfig(ConfigData) + if NAME == "": + sys.exit(0) + +# +# main() +# +# print "Current Date " + theCurrDateTime() +CheckHasErrors = False + +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", stdout.readlines()) is not None: + print "Unable to access ESXi Host: %s, username: %s" % (HOST, USER) + sys.exit(1) +except: + pass + +# +# Get list of DataStores, store in VOLUMES +# +LeastUsedDS = "" +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] + LeastUsedDS = splitLine[1] +except: + e = sys.exc_info()[0] + print "The Error is " + str(e) + sys.exit(1) + +if STORE == "LeastUsed": + STORE = LeastUsedDS + + +# +# Get list of Networks available, store in VMNICS +# +try: + (stdin, stdout, stderr) = ssh.exec_command("esxcli network vswitch standard list|grep Portgroups|sed 's/^ Portgroups: //g'") + type(stdin) + VMNICS = [] + for line in stdout.readlines(): + splitLine = re.split(',|\n', line) + VMNICS.append(splitLine[0]) +except: + e = sys.exc_info()[0] + print "The Error is " + str(e) + sys.exit(1) + +# +# Get from ESXi host if ISO exists +# +ISOfound = False +if ISO == "None": + ISO = "" +if ISO != "": + try: + (stdin, stdout, stderr) = ssh.exec_command("ls " + str(ISO)) + type(stdin) + if not stdout.readlines() and stderr.readlines(): + ISOfound = True + except: + e = sys.exc_info()[0] + print "The Error is " + str(e) + sys.exit(1) +# +# Check if VM already 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] + print "ERROR: VM " + NAME + " already exists." + CheckHasErrors = True +except: + e = sys.exc_info()[0] + print "The Error is " + str(e) + sys.exit(1) + +# +# Do checks here +# + +# Check CPU +if CPU < 1 or CPU > 128: + print str(CPU) + " CPU out of range. [1-128]" + CheckHasErrors = True + +# Check MEM +if MEM < 1 or MEM > 4080: + print str(MEM) + "GB Memory out of range. [1-4080]" + CheckHasErrors = True + +# Check SIZE +if SIZE < 1 or SIZE > 63488: + print "Virtual Disk size " + str(SIZE) + "GB out of range. [1-63488]" + CheckHasErrors = True + +# Check STORE +V = [] +DSPATH="" +DSSTORE="" +for Path in VOLUMES: + V.append(VOLUMES[Path]) + if STORE == Path or STORE == VOLUMES[Path]: + DSPATH = Path + DSSTORE = VOLUMES[Path] + +if DSSTORE not in V: + print "ERROR: Disk Storage " + STORE + " doesn't exist. " + print " Available Disk Stores: " + str(V) + print " LeastUsed Disk Store : " + str(LeastUsedDS) + CheckHasErrors = True + +# Check NIC (NIC record) +if (NET not in VMNICS) and (NET != "None"): + print "ERROR: Virtual NIC " + NET + " doesn't exist." + print " Available VM NICs: " + str(VMNICS) + " or 'None'" + CheckHasErrors = True + +# Check ISO exists +if ISO != "" and not ISOfound: + print "ERROR: ISO " + ISO + " not found. Use full path to ISO" + CheckHasErrors = True + +# Check if DSPATH/NAME aready exists +FullPathExists = False +try: + FullPath = DSPATH + "/" + NAME + (stdin, stdout, stderr) = ssh.exec_command("ls -d " + FullPath) + type(stdin) + if stdout.readlines() and not stderr.readlines(): + print "ERROR: Directory " + FullPath + " already exists." + CheckHasErrors = True +except: + pass + +# +# Exit if there are any errors +# +if CheckHasErrors: + sys.exit(1) + +# +# Create the VM +# +VMX = [] +VMX.append('config.version = "8"') +VMX.append('virtualHW.version = "8"') +VMX.append('vmci0.present = "TRUE"') +VMX.append('displayName = "' + NAME + '"') +VMX.append('floppy0.present = "FALSE"') +VMX.append('numvcpus = "' + str(CPU) + '"') +VMX.append('scsi0.present = "TRUE"') +VMX.append('scsi0.sharedBus = "none"') +VMX.append('scsi0.virtualDev = "pvscsi"') +VMX.append('memsize = "' + str(MEM * 1024) + '"') +VMX.append('scsi0:0.present = "TRUE"') +VMX.append('scsi0:0.fileName = "' + NAME + '.vmdk"') +VMX.append('scsi0:0.deviceType = "scsi-hardDisk"') +if ISO == "": + VMX.append('ide1:0.present = "TRUE"') + VMX.append('ide1:0.fileName = "emptyBackingString"') + VMX.append('ide1:0.deviceType = "atapi-cdrom"') + VMX.append('ide1:0.startConnected = "FALSE"') + VMX.append('ide1:0.clientDevice = "TRUE"') +else: + VMX.append('ide1:0.present = "TRUE"') + VMX.append('ide1:0.fileName = "' + ISO + '"') + VMX.append('ide1:0.deviceType = "cdrom-image"') +VMX.append('pciBridge0.present = "TRUE"') +VMX.append('pciBridge4.present = "TRUE"') +VMX.append('pciBridge4.virtualDev = "pcieRootPort"') +VMX.append('pciBridge4.functions = "8"') +VMX.append('pciBridge5.present = "TRUE"') +VMX.append('pciBridge5.virtualDev = "pcieRootPort"') +VMX.append('pciBridge5.functions = "8"') +VMX.append('pciBridge6.present = "TRUE"') +VMX.append('pciBridge6.virtualDev = "pcieRootPort"') +VMX.append('pciBridge6.functions = "8"') +VMX.append('pciBridge7.present = "TRUE"') +VMX.append('pciBridge7.virtualDev = "pcieRootPort"') +VMX.append('pciBridge7.functions = "8"') +VMX.append('guestOS = "' + GUESTOS + '"') +if NET != "None": + VMX.append('ethernet0.virtualDev = "vmxnet3"') + VMX.append('ethernet0.present = "TRUE"') + VMX.append('ethernet0.networkName = "' + NET + '"') + VMX.append('ethernet0.addressType = "generated"') + +if isDryRun: + print "Dry Run Enabled. No VM created..." + sys.exit(0) +else: + try: + + # Create NAME.vmx + if isVerbose: + print "Create " + NAME + ".vmx file" + MyVM = FullPath + "/" + NAME + (stdin, stdout, stderr) = ssh.exec_command("mkdir " + FullPath ) + type(stdin) + for line in VMX: + (stdin, stdout, stderr) = ssh.exec_command("echo " + line + " >>" + MyVM + ".vmx") + type(stdin) + + # Create vmdk + if isVerbose: + print "Create " + NAME + ".vmdk file" + (stdin, stdout, stderr) = ssh.exec_command("vmkfstools -c " + str(SIZE) + "G -d " + DISKFORMAT + " " + MyVM + ".vmdk") + type(stdin) + + # Register VM + if isVerbose: + print "Register VM" + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd solo/registervm " + MyVM + ".vmx") + type(stdin) + Jnk = stdout.readlines() + VMID = int(Jnk[0]) + + # Power on VM + if isVerbose: + print "Power ON VM" + (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.on " + str(VMID)) + type(stdin) + if stderr.readlines(): + print "Error Power.on VM." + sys.exit(1) + except: + print "There was an error creating the VM!" + sys.exit(1) + +# Print Summary +print "\nCreate VM Success" +print "ESXi Host: " + HOST +print "VM NAME: " + NAME +print "vCPU: " + str(CPU) +print "Memory: " + str(MEM) + "GB" +print "VM Disk: " + str(SIZE) + "GB" +if isVerbose: + print "Format: " + DISKFORMAT +print "DS Store: " + DSSTORE +print "Network: " + NET +if ISO: + print "ISO: " + ISO +if isVerbose: + print "Guest OS: " + GUESTOS + + diff --git a/esxi_vm_functions.py b/esxi_vm_functions.py new file mode 100755 index 0000000..b9ac4ee --- /dev/null +++ b/esxi_vm_functions.py @@ -0,0 +1,89 @@ +import os.path +import yaml +import datetime # For current Date/Time +import paramiko # For remote ssh +from math import log + + +# +# +# Functions +# +# + + +def setup_config(): + + # + # System wide defaults + # + ConfigData = dict( + LOG="~/esxi-vm.log", + isDryRun=False, + isVerbose=False, + HOST="esxi", + USER="root", + PASSWORD="", + CPU=2, + MEM=4, + SIZE=20, + DISKFORMAT="thin", + VIRTDEV="pvscsi", + STORE="LeastUsed", + NET="None", + ISO="None", + GUESTOS="centos-64" + ) + + ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml" + + # + # Get ConfigData from ConfigDataFile, then merge. + if os.path.exists(ConfigDataFileLocation): + FromFileConfigData = yaml.safe_load(open(ConfigDataFileLocation)) + ConfigData.update(FromFileConfigData) + + try: + with open(ConfigDataFileLocation, 'w') as FD: + yaml.dump(ConfigData, FD, default_flow_style=False) + FD.close() + except: + print "Unable to create/update config file " + ConfigDataFileLocation + e = sys.exc_info()[0] + print "The Error is " + str(e) + sys.exit(1) + return ConfigData + +def SaveConfig(ConfigData): + ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml" + try: + with open(ConfigDataFileLocation, 'w') as FD: + yaml.dump(ConfigData, FD, default_flow_style=False) + FD.close() + except: + print "Unable to create/update config file " + ConfigDataFileLocation + e = sys.exc_info()[0] + print "The Error is " + str(e) + return 1 + return 0 + + +def theCurrDateTime(): + i = datetime.datetime.now() + return str(i.isoformat()) + + +unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], [0, 0, 1, 2, 2, 2]) +def float2human(num): + """Integer to Human readable""" + if num > 1: + exponent = min(int(log(float(num), 1024)), len(unit_list) - 1) + quotient = float(num) / 1024**exponent + unit, num_decimals = unit_list[exponent] + format_string = '{:.%sf} {}' % (num_decimals) + return format_string.format(quotient, unit) + if num == 0: + return '0 bytes' + if num == 1: + return '1 byte' +