├── .gitignore ├── Makefile ├── README.md ├── esxi-vm-create ├── esxi-vm-destroy └── esxi_vm_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | esxi_vm_functions.pyc 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo "make install" 3 | 4 | install: 5 | install -m 755 ./esxi-vm-create /usr/local/bin/ 6 | install -m 755 ./esxi-vm-destroy /usr/local/bin/ 7 | install -m 755 ./esxi_vm_functions.py /usr/local/bin/ 8 | @echo "Install Success." 9 | 10 | uninstall: 11 | rm -fr /usr/local/bin/esxi-vm-create 12 | rm -fr /usr/local/bin/esxi-vm-destroy 13 | rm -fr /usr/local/bin/esxi_vm_functions.py 14 | rm -fr /usr/local/bin/esxi_vm_functions.pyc 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | 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. This tool is an easy way to automate building VM's from command line or from other testing or automation tools such as Bamboo, Puppet, Saltstack, or Chef. 5 | 6 | 7 | Usage 8 | ----- 9 | 10 | Get help using --help option. The defaults will be displayed in the help output. 11 | 12 | The only command line required paramater is the VM name (-n), all other command line arguments are optional. 13 | 14 | 15 | Defaults are stored in your home directory in ~/.esxi-vm.yml. You can edit this file directly, or you can use the tool to update most the defaults by specifying --updateDefaults. 16 | 17 | One of the first settings to set and save as defaults is the --Host (-H), --User (-U) and --Password (-P). 18 | 19 | Some basic sanity checks are done on the ESXi host before creating the VM. The --verbose (-V) option will give you a little more details in the creation process. If an invalid Disk Stores or Network Interface is specified, the available devices will be shown in the error message. The tool will not show the list of available ISO images, and Guest OS types. CPU, Memory, Virtual Disk sizes are based on ESXi 6.0 limitations. 20 | 21 | The --dry (-d) option will go through the sanity checks, but will not create the VM. 22 | 23 | By default the Disk Store is set to "LeastUsed". This will use the Disk Store with the most free space (in bytes). 24 | 25 | By default the ISO is set to "None". Specify the full path to the ISO image. If you specify just the ISO image filename (no path), the system will attempt to find the ISO image on your DataStores. 26 | 27 | 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". 28 | 29 | 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. 30 | 31 | 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. 32 | 33 | 34 | Requirements 35 | ------------ 36 | 37 | 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. 38 | 39 | 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. 40 | 41 | Python and paramiko is a software requirement. 42 | 43 | ``` 44 | yum -y install python python-paramiko 45 | ``` 46 | 47 | 48 | Command Line Args 49 | ----------------- 50 | 51 | ``` 52 | ./esxi-vm-create --help 53 | 54 | usage: esxi-vm-create [-h] [-d] [-H HOST] [-U USER] [-P PASSWORD] [-n NAME] 55 | [-c CPU] [-m MEM] [-v HDISK] [-i ISO] [-N NET] [-M MAC] 56 | [-S STORE] [-g GUESTOS] [-o VMXOPTS] [-V] [--summary] 57 | [-u] 58 | 59 | ESXi Create VM utility. 60 | 61 | optional arguments: 62 | -h, --help show this help message and exit 63 | -d, --dry Enable Dry Run mode (False) 64 | -H HOST, --Host HOST ESXi Host/IP (esxi) 65 | -U USER, --User USER ESXi Host username (root) 66 | -P PASSWORD, --Password PASSWORD 67 | ESXi Host password (*****) 68 | -n NAME, --name NAME VM name 69 | -c CPU, --cpu CPU Number of vCPUS (2) 70 | -m MEM, --mem MEM Memory in GB (4) 71 | -v HDISK, --vdisk HDISK 72 | Size of virt hdisk (20) 73 | -i ISO, --iso ISO CDROM ISO Path | None (None) 74 | -N NET, --net NET Network Interface | None (None) 75 | -M MAC, --mac MAC MAC address 76 | -S STORE, --store STORE 77 | vmfs Store | LeastUsed (LeastUsed) 78 | -g GUESTOS, --guestos GUESTOS 79 | Guest OS. (centos-64) 80 | -o VMXOPTS, --options VMXOPTS 81 | Comma list of VMX Options. 82 | -V, --verbose Enable Verbose mode (False) 83 | --summary Display Summary (False) 84 | -u, --updateDefaults Update Default VM settings stored in ~/.esxi-vm.yml 85 | ``` 86 | 87 | 88 | Example Usage 89 | ------------- 90 | 91 | Running the script for the first time it's recommended to specify your defaults. (ESXi HOST, PASSWORD) 92 | 93 | ``` 94 | ./esxi-vm-create -H esxi -P MySecurePassword -u 95 | Saving new Defaults to ~/.esxi-vm.yml 96 | ``` 97 | 98 | 99 | Create a new VM named testvm01 using all defaults from ~/.esxi-vm.yml. 100 | ``` 101 | ./esxi-vm-create -n testvm01 --summary 102 | 103 | Create VM Success: 104 | VM NAME: testvm01 105 | vCPU: 2 106 | Memory: 4GB 107 | VM Disk: 20GB 108 | DS Store: DS_4TB 109 | Network: None 110 | 111 | ``` 112 | 113 | Change default number of vCPUs to 4, Memory to 8GB and vDisk size to 40GB. 114 | ``` 115 | ./esxi-vm-create -c 4 -m 8 -s 40 -u 116 | Saving new Defaults to ~/.esxi-vm.yml 117 | ``` 118 | 119 | Create a new VM named testvm02 using new defaults from ~/.esxi-vm.yml and specifying a Network interface and partial MAC. 120 | ``` 121 | ./esxi-vm-create -n testvm02 -N 192.168.1 -M 01:02:03 122 | 00:50:56:01:02:03 123 | ``` 124 | 125 | Available Network Interfaces and Available Disk Storage volumes will be listed if an invalid option is specified. 126 | 127 | ``` 128 | ./esxi-vm-create -n testvm03 -N BadNet -S BadDS 129 | ERROR: Disk Storage BadDS doesn't exist. 130 | Available Disk Stores: ['DS_SSD500s', 'DS_SSD500c', 'DS_SSD250', 'DS_4TB', 'DS_3TB_m'] 131 | LeastUsed Disk Store : DS_4TB 132 | ERROR: Virtual NIC BadNet doesn't exist. 133 | Available VM NICs: ['192.168.1', '192.168.0', 'VM Network test'] or 'None' 134 | ``` 135 | 136 | Create a new VM named testvm03 using a valid Network Interface, valid Disk Storage volume, summary and verbose enabled. Save as default. 137 | ``` 138 | ./esxi-vm-create -n testvm03 -N 192.168.1 -S DS_3TB_m --summary --verbose --updateDefaults 139 | Saving new Defaults to ~/.esxi-vm.yml 140 | Create testvm03.vmx file 141 | Create testvm03.vmdk file 142 | Register VM 143 | Power ON VM 144 | 145 | Create VM Success: 146 | ESXi Host: esxi 147 | VM NAME: testvm03 148 | vCPU: 4 149 | Memory: 8GB 150 | VM Disk: 40GB 151 | Format: thin 152 | DS Store: DS_3TB_m 153 | Network: 192.168.1 154 | Guest OS: centos-64 155 | MAC: 00:0c:29:32:63:92 156 | 00:0c:29:32:63:92 157 | ``` 158 | 159 | Create a new VM named testvm04 specifying an ISO file to boot. 160 | ``` 161 | ./esxi-vm-create -n testvm04 --summary --iso CentOS-7-x86_64-Minimal-1611.iso 162 | FoundISOPath: /vmfs/volumes/5430094d-5a4fa180-4962-0017a45127e2/ISO/CentOS-7-x86_64-Minimal-1611.iso 163 | Create testvm04.vmx file 164 | Create testvm04.vmdk file 165 | Register VM 166 | Power ON VM 167 | 168 | Create VM Success: 169 | ESXi Host: esxi 170 | VM NAME: testvm04 171 | vCPU: 4 172 | Memory: 8GB 173 | VM Disk: 40GB 174 | Format: thin 175 | DS Store: DS_3TB_m 176 | Network: 192.168.1 177 | ISO: /vmfs/volumes/5430094d-5a4fa180-4962-0017a45127e2/ISO/CentOS-7-x86_64-Minimal-1611.iso 178 | Guest OS: centos-64 179 | MAC: 00:0c:29:ea:a0:42 180 | 00:0c:29:ea:a0:42 181 | 182 | ``` 183 | 184 | Merge/Add extra VMX options, saved as default. 185 | ``` 186 | ./esxi-vm-create -o 'floppy0.present = "TRUE",svga.autodetect = "TRUE",svga.present = "TRUE"' -u 187 | Saving new Defaults to ~/.esxi-vm.yml 188 | ``` 189 | 190 | License 191 | ------- 192 | 193 | Copyright (C) 2017 Jonathan Senkerik 194 | 195 | This program is free software: you can redistribute it and/or modify 196 | it under the terms of the GNU General Public License as published by 197 | the Free Software Foundation, either version 3 of the License, or 198 | (at your option) any later version. 199 | 200 | This program is distributed in the hope that it will be useful, 201 | but WITHOUT ANY WARRANTY; without even the implied warranty of 202 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 203 | GNU General Public License for more details. 204 | 205 | You should have received a copy of the GNU General Public License 206 | along with this program. If not, see . 207 | 208 | 209 | Support 210 | ------- 211 | Website : http://www.jintegrate.co 212 | github : http://github.com/josenk/ 213 | bitcoin : 1HCTVkmSXepQahJfvuxQRhUMem1HpHq3Mq 214 | paypal : josenk@jintegrate.co 215 | -------------------------------------------------------------------------------- /esxi-vm-create: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | import argparse # Argument parser 5 | import datetime # For current Date/Time 6 | import os.path # To check if file exists 7 | import sys # For args 8 | import re # For regex 9 | import paramiko # For remote ssh 10 | import yaml 11 | import warnings 12 | 13 | from esxi_vm_functions import * 14 | 15 | # Defaults and Variable setup 16 | ConfigData = setup_config() 17 | NAME = "" 18 | LOG = ConfigData['LOG'] 19 | isDryRun = ConfigData['isDryRun'] 20 | isVerbose = ConfigData['isVerbose'] 21 | isSummary = ConfigData['isSummary'] 22 | HOST = ConfigData['HOST'] 23 | USER = ConfigData['USER'] 24 | PASSWORD = ConfigData['PASSWORD'] 25 | CPU = ConfigData['CPU'] 26 | MEM = ConfigData['MEM'] 27 | HDISK = int(ConfigData['HDISK']) 28 | DISKFORMAT = ConfigData['DISKFORMAT'] 29 | VIRTDEV = ConfigData['VIRTDEV'] 30 | STORE = ConfigData['STORE'] 31 | NET = ConfigData['NET'] 32 | ISO = ConfigData['ISO'] 33 | GUESTOS = ConfigData['GUESTOS'] 34 | VMXOPTS = ConfigData['VMXOPTS'] 35 | 36 | ErrorMessages = "" 37 | MAC = "" 38 | GeneratedMAC = "" 39 | ISOfound = False 40 | CheckHasErrors = False 41 | LeastUsedDS = "" 42 | DSPATH="" 43 | DSSTORE="" 44 | FullPathExists = False 45 | 46 | # 47 | # Process Arguments 48 | # 49 | parser = argparse.ArgumentParser(description='ESXi Create VM utility.') 50 | 51 | parser.add_argument('-d', '--dry', dest='isDryRunarg', action='store_true', help="Enable Dry Run mode (" + str(isDryRun) + ")") 52 | parser.add_argument("-H", "--Host", dest='HOST', type=str, help="ESXi Host/IP (" + str(HOST) + ")") 53 | parser.add_argument("-U", "--User", dest='USER', type=str, help="ESXi Host username (" + str(USER) + ")") 54 | parser.add_argument("-P", "--Password", dest='PASSWORD', type=str, help="ESXi Host password (*****)") 55 | parser.add_argument("-n", "--name", dest='NAME', type=str, help="VM name") 56 | parser.add_argument("-c", "--cpu", dest='CPU', type=int, help="Number of vCPUS (" + str(CPU) + ")") 57 | parser.add_argument("-m", "--mem", type=int, help="Memory in GB (" + str(MEM) + ")") 58 | parser.add_argument("-v", "--vdisk", dest='HDISK', type=str, help="Size of virt hdisk (" + str(HDISK) + ")") 59 | parser.add_argument("-i", "--iso", dest='ISO', type=str, help="CDROM ISO Path | None (" + str(ISO) + ")") 60 | parser.add_argument("-N", "--net", dest='NET', type=str, help="Network Interface | None (" + str(NET) + ")") 61 | parser.add_argument("-M", "--mac", dest='MAC', type=str, help="MAC address") 62 | parser.add_argument("-S", "--store", dest='STORE', type=str, help="vmfs Store | LeastUsed (" + str(STORE) + ")") 63 | parser.add_argument("-g", "--guestos", dest='GUESTOS', type=str, help="Guest OS. (" + str(GUESTOS) + ")") 64 | parser.add_argument("-o", "--options", dest='VMXOPTS', type=str, default='NIL', help="Comma list of VMX Options.") 65 | parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")") 66 | parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")") 67 | parser.add_argument("-u", "--updateDefaults", dest='UPDATE', action='store_true', help="Update Default VM settings stored in ~/.esxi-vm.yml") 68 | #parser.add_argument("--showDefaults", dest='SHOW', action='store_true', help="Show Default VM settings stored in ~/.esxi-vm.yml") 69 | 70 | 71 | args = parser.parse_args() 72 | 73 | if args.isDryRunarg: 74 | isDryRun = True 75 | if args.isVerbosearg: 76 | isVerbose = True 77 | if args.isSummaryarg: 78 | isSummary = True 79 | if args.HOST: 80 | HOST=args.HOST 81 | if args.USER: 82 | USER=args.USER 83 | if args.PASSWORD: 84 | PASSWORD=args.PASSWORD 85 | if args.NAME: 86 | NAME=args.NAME 87 | if args.CPU: 88 | CPU=int(args.CPU) 89 | if args.mem: 90 | MEM=int(args.mem) 91 | if args.HDISK: 92 | HDISK=int(args.HDISK) 93 | if args.ISO: 94 | ISO=args.ISO 95 | if args.NET: 96 | NET=args.NET 97 | if args.MAC: 98 | MAC=args.MAC 99 | if args.STORE: 100 | STORE=args.STORE 101 | if STORE == "": 102 | STORE = "LeastUsed" 103 | if args.GUESTOS: 104 | GUESTOS=args.GUESTOS 105 | if args.VMXOPTS == '' and VMXOPTS != '': 106 | VMXOPTS='' 107 | if args.VMXOPTS and args.VMXOPTS != 'NIL': 108 | VMXOPTS=args.VMXOPTS.split(",") 109 | 110 | 111 | if args.UPDATE: 112 | print "Saving new Defaults to ~/.esxi-vm.yml" 113 | ConfigData['isDryRun'] = isDryRun 114 | ConfigData['isVerbose'] = isVerbose 115 | ConfigData['isSummary'] = isSummary 116 | ConfigData['HOST'] = HOST 117 | ConfigData['USER'] = USER 118 | ConfigData['PASSWORD'] = PASSWORD 119 | ConfigData['CPU'] = CPU 120 | ConfigData['MEM'] = MEM 121 | ConfigData['HDISK'] = HDISK 122 | ConfigData['DISKFORMAT'] = DISKFORMAT 123 | ConfigData['VIRTDEV'] = VIRTDEV 124 | ConfigData['STORE'] = STORE 125 | ConfigData['NET'] = NET 126 | ConfigData['ISO'] = ISO 127 | ConfigData['GUESTOS'] = GUESTOS 128 | ConfigData['VMXOPTS'] = VMXOPTS 129 | SaveConfig(ConfigData) 130 | if NAME == "": 131 | sys.exit(0) 132 | 133 | # 134 | # main() 135 | # 136 | LogOutput = '{' 137 | LogOutput += '"datetime":"' + str(theCurrDateTime()) + '",' 138 | 139 | if NAME == "": 140 | print "ERROR: Missing required option --name" 141 | sys.exit(1) 142 | 143 | try: 144 | ssh = paramiko.SSHClient() 145 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 146 | ssh.connect(HOST, username=USER, password=PASSWORD) 147 | 148 | (stdin, stdout, stderr) = ssh.exec_command("esxcli system version get |grep Version") 149 | type(stdin) 150 | if re.match("Version", str(stdout.readlines())) is not None: 151 | print "Unable to determine if this is a ESXi Host: %s, username: %s" % (HOST, USER) 152 | sys.exit(1) 153 | except: 154 | print "The Error is " + str(sys.exc_info()[0]) 155 | print "Unable to access ESXi Host: %s, username: %s" % (HOST, USER) 156 | sys.exit(1) 157 | 158 | # 159 | # Get list of DataStores, store in VOLUMES 160 | # 161 | try: 162 | (stdin, stdout, stderr) = ssh.exec_command("esxcli storage filesystem list |grep '/vmfs/volumes/.*true VMFS' |sort -nk7") 163 | type(stdin) 164 | VOLUMES = {} 165 | for line in stdout.readlines(): 166 | splitLine = line.split() 167 | VOLUMES[splitLine[0]] = splitLine[1] 168 | LeastUsedDS = splitLine[1] 169 | except: 170 | print "The Error is " + str(sys.exc_info()[0]) 171 | sys.exit(1) 172 | 173 | if STORE == "LeastUsed": 174 | STORE = LeastUsedDS 175 | 176 | 177 | # 178 | # Get list of Networks available, store in VMNICS 179 | # 180 | try: 181 | (stdin, stdout, stderr) = ssh.exec_command("esxcli network vswitch standard list|grep Portgroups|sed 's/^ Portgroups: //g'") 182 | type(stdin) 183 | VMNICS = [] 184 | for line in stdout.readlines(): 185 | splitLine = re.split(',|\n', line) 186 | VMNICS.append(splitLine[0]) 187 | except: 188 | print "The Error is " + str(sys.exc_info()[0]) 189 | sys.exit(1) 190 | 191 | # 192 | # Check MAC address 193 | # 194 | MACarg = MAC 195 | if MAC != "": 196 | MACregex = '^([a-fA-F0-9]{2}[:|\-]){5}[a-fA-F0-9]{2}$' 197 | if re.compile(MACregex).search(MAC): 198 | # Full MAC found. OK 199 | MAC = MAC.replace("-",":") 200 | elif re.compile(MACregex).search("00:50:56:" + MAC): 201 | MAC="00:50:56:" + MAC.replace("-",":") 202 | else: 203 | print "ERROR: " + MAC + " Invalid MAC address." 204 | ErrorMessages += " " + MAC + " Invalid MAC address." 205 | CheckHasErrors = True 206 | 207 | 208 | # 209 | # Get from ESXi host if ISO exists 210 | # 211 | ISOarg = ISO 212 | if ISO == "None": 213 | ISO = "" 214 | if ISO != "": 215 | try: 216 | # If ISO has no "/", try to find the ISO 217 | if not re.match('/', ISO): 218 | (stdin, stdout, stderr) = ssh.exec_command("find /vmfs/volumes/ -type f -name " + ISO + " -exec sh -c 'echo $1; kill $PPID' sh {} 2>/dev/null \;") 219 | type(stdin) 220 | FoundISOPath = str(stdout.readlines()[0]).strip('\n') 221 | if isVerbose: 222 | print "FoundISOPath: " + str(FoundISOPath) 223 | ISO = str(FoundISOPath) 224 | 225 | (stdin, stdout, stderr) = ssh.exec_command("ls " + str(ISO)) 226 | type(stdin) 227 | if stdout.readlines() and not stderr.readlines(): 228 | ISOfound = True 229 | 230 | except: 231 | print "The Error is " + str(sys.exc_info()[0]) 232 | sys.exit(1) 233 | 234 | # 235 | # Check if VM already exists 236 | # 237 | VMID = -1 238 | try: 239 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/getallvms") 240 | type(stdin) 241 | for line in stdout.readlines(): 242 | splitLine = line.split() 243 | if NAME == splitLine[1]: 244 | VMID = splitLine[0] 245 | print "ERROR: VM " + NAME + " already exists." 246 | ErrorMessages += " VM " + NAME + " already exists." 247 | CheckHasErrors = True 248 | except: 249 | print "The Error is " + str(sys.exc_info()[0]) 250 | sys.exit(1) 251 | 252 | # 253 | # Do checks here 254 | # 255 | 256 | # Check CPU 257 | if CPU < 1 or CPU > 128: 258 | print str(CPU) + " CPU out of range. [1-128]." 259 | ErrorMessages += " " + str(CPU) + " CPU out of range. [1-128]." 260 | CheckHasErrors = True 261 | 262 | # Check MEM 263 | if MEM < 1 or MEM > 4080: 264 | print str(MEM) + "GB Memory out of range. [1-4080]." 265 | ErrorMessages += " " + str(MEM) + "GB Memory out of range. [1-4080]." 266 | CheckHasErrors = True 267 | 268 | # Check HDISK 269 | if HDISK < 1 or HDISK > 63488: 270 | print "Virtual Disk size " + str(HDISK) + "GB out of range. [1-63488]." 271 | ErrorMessages += " Virtual Disk size " + str(HDISK) + "GB out of range. [1-63488]." 272 | CheckHasErrors = True 273 | 274 | # Convert STORE to path and visa-versa 275 | V = [] 276 | for Path in VOLUMES: 277 | V.append(VOLUMES[Path]) 278 | if STORE == Path or STORE == VOLUMES[Path]: 279 | DSPATH = Path 280 | DSSTORE = VOLUMES[Path] 281 | 282 | if DSSTORE not in V: 283 | print "ERROR: Disk Storage " + STORE + " doesn't exist. " 284 | print " Available Disk Stores: " + str([str(item) for item in V]) 285 | print " LeastUsed Disk Store : " + str(LeastUsedDS) 286 | ErrorMessages += " Disk Storage " + STORE + " doesn't exist. " 287 | CheckHasErrors = True 288 | 289 | # Check NIC (NIC record) 290 | if (NET not in VMNICS) and (NET != "None"): 291 | print "ERROR: Virtual NIC " + NET + " doesn't exist." 292 | print " Available VM NICs: " + str([str(item) for item in VMNICS]) + " or 'None'" 293 | ErrorMessages += " Virtual NIC " + NET + " doesn't exist." 294 | CheckHasErrors = True 295 | 296 | # Check ISO exists 297 | if ISO != "" and not ISOfound: 298 | print "ERROR: ISO " + ISO + " not found. Use full path to ISO" 299 | ErrorMessages += " ISO " + ISO + " not found. Use full path to ISO" 300 | CheckHasErrors = True 301 | 302 | # Check if DSPATH/NAME aready exists 303 | try: 304 | FullPath = DSPATH + "/" + NAME 305 | (stdin, stdout, stderr) = ssh.exec_command("ls -d " + FullPath) 306 | type(stdin) 307 | if stdout.readlines() and not stderr.readlines(): 308 | print "ERROR: Directory " + FullPath + " already exists." 309 | ErrorMessages += " Directory " + FullPath + " already exists." 310 | CheckHasErrors = True 311 | except: 312 | pass 313 | 314 | # 315 | # Create the VM 316 | # 317 | VMX = [] 318 | VMX.append('config.version = "8"') 319 | VMX.append('virtualHW.version = "8"') 320 | VMX.append('vmci0.present = "TRUE"') 321 | VMX.append('displayName = "' + NAME + '"') 322 | VMX.append('floppy0.present = "FALSE"') 323 | VMX.append('numvcpus = "' + str(CPU) + '"') 324 | VMX.append('scsi0.present = "TRUE"') 325 | VMX.append('scsi0.sharedBus = "none"') 326 | VMX.append('scsi0.virtualDev = "pvscsi"') 327 | VMX.append('memsize = "' + str(MEM * 1024) + '"') 328 | VMX.append('scsi0:0.present = "TRUE"') 329 | VMX.append('scsi0:0.fileName = "' + NAME + '.vmdk"') 330 | VMX.append('scsi0:0.deviceType = "scsi-hardDisk"') 331 | if ISO == "": 332 | VMX.append('ide1:0.present = "TRUE"') 333 | VMX.append('ide1:0.fileName = "emptyBackingString"') 334 | VMX.append('ide1:0.deviceType = "atapi-cdrom"') 335 | VMX.append('ide1:0.startConnected = "FALSE"') 336 | VMX.append('ide1:0.clientDevice = "TRUE"') 337 | else: 338 | VMX.append('ide1:0.present = "TRUE"') 339 | VMX.append('ide1:0.fileName = "' + ISO + '"') 340 | VMX.append('ide1:0.deviceType = "cdrom-image"') 341 | VMX.append('pciBridge0.present = "TRUE"') 342 | VMX.append('pciBridge4.present = "TRUE"') 343 | VMX.append('pciBridge4.virtualDev = "pcieRootPort"') 344 | VMX.append('pciBridge4.functions = "8"') 345 | VMX.append('pciBridge5.present = "TRUE"') 346 | VMX.append('pciBridge5.virtualDev = "pcieRootPort"') 347 | VMX.append('pciBridge5.functions = "8"') 348 | VMX.append('pciBridge6.present = "TRUE"') 349 | VMX.append('pciBridge6.virtualDev = "pcieRootPort"') 350 | VMX.append('pciBridge6.functions = "8"') 351 | VMX.append('pciBridge7.present = "TRUE"') 352 | VMX.append('pciBridge7.virtualDev = "pcieRootPort"') 353 | VMX.append('pciBridge7.functions = "8"') 354 | VMX.append('guestOS = "' + GUESTOS + '"') 355 | if NET != "None": 356 | VMX.append('ethernet0.virtualDev = "vmxnet3"') 357 | VMX.append('ethernet0.present = "TRUE"') 358 | VMX.append('ethernet0.networkName = "' + NET + '"') 359 | if MAC == "": 360 | VMX.append('ethernet0.addressType = "generated"') 361 | else: 362 | VMX.append('ethernet0.addressType = "static"') 363 | VMX.append('ethernet0.address = "' + MAC + '"') 364 | 365 | # 366 | # Merge extra VMX options 367 | for VMXopt in VMXOPTS: 368 | try: 369 | k,v = VMXopt.split("=") 370 | except: 371 | k="" 372 | v="" 373 | key = k.lstrip().strip() 374 | value = v.lstrip().strip() 375 | for i in VMX: 376 | try: 377 | ikey,ivalue = i.split("=") 378 | except: 379 | break 380 | if ikey.lstrip().strip().lower() == key.lower(): 381 | index = VMX.index(i) 382 | VMX[index] = ikey + " = " + value 383 | break 384 | else: 385 | if key != '' and value != '': 386 | VMX.append(key + " = " + value) 387 | 388 | if isVerbose and VMXOPTS != '': 389 | print "VMX file:" 390 | for i in VMX: 391 | print i 392 | 393 | MyVM = FullPath + "/" + NAME 394 | if CheckHasErrors: 395 | Result = "Errors" 396 | else: 397 | Result = "Success" 398 | 399 | if not isDryRun and not CheckHasErrors: 400 | try: 401 | 402 | # Create NAME.vmx 403 | if isVerbose: 404 | print "Create " + NAME + ".vmx file" 405 | (stdin, stdout, stderr) = ssh.exec_command("mkdir " + FullPath ) 406 | type(stdin) 407 | for line in VMX: 408 | (stdin, stdout, stderr) = ssh.exec_command("echo " + line + " >>" + MyVM + ".vmx") 409 | type(stdin) 410 | 411 | # Create vmdk 412 | if isVerbose: 413 | print "Create " + NAME + ".vmdk file" 414 | (stdin, stdout, stderr) = ssh.exec_command("vmkfstools -c " + str(HDISK) + "G -d " + DISKFORMAT + " " + MyVM + ".vmdk") 415 | type(stdin) 416 | 417 | # Register VM 418 | if isVerbose: 419 | print "Register VM" 420 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd solo/registervm " + MyVM + ".vmx") 421 | type(stdin) 422 | VMID = int(stdout.readlines()[0]) 423 | 424 | # Power on VM 425 | if isVerbose: 426 | print "Power ON VM" 427 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.on " + str(VMID)) 428 | type(stdin) 429 | if stderr.readlines(): 430 | print "Error Power.on VM." 431 | Result="Fail" 432 | 433 | # Get Generated MAC 434 | if NET != "None": 435 | (stdin, stdout, stderr) = ssh.exec_command( 436 | "grep -i 'ethernet0.*ddress = ' " + MyVM + ".vmx |tail -1|awk '{print $NF}'") 437 | type(stdin) 438 | GeneratedMAC = str(stdout.readlines()[0]).strip('\n"') 439 | 440 | except: 441 | print "There was an error creating the VM." 442 | ErrorMessages += " There was an error creating the VM." 443 | Result = "Fail" 444 | 445 | # Print Summary 446 | 447 | # 448 | # The output log string 449 | LogOutput += '"Host":"' + HOST + '",' 450 | LogOutput += '"Name":"' + NAME + '",' 451 | LogOutput += '"CPU":"' + str(CPU) + '",' 452 | LogOutput += '"Mem":"' + str(MEM) + '",' 453 | LogOutput += '"Hdisk":"' + str(HDISK) + '",' 454 | LogOutput += '"DiskFormat":"' + DISKFORMAT + '",' 455 | LogOutput += '"Virtual Device":"' + VIRTDEV + '",' 456 | LogOutput += '"Store":"' + STORE + '",' 457 | LogOutput += '"Store Used":"' + DSPATH + '",' 458 | LogOutput += '"Network":"' + NET + '",' 459 | LogOutput += '"ISO":"' + ISOarg + '",' 460 | LogOutput += '"ISO used":"' + ISO + '",' 461 | LogOutput += '"Guest OS":"' + GUESTOS + '",' 462 | LogOutput += '"MAC":"' + MACarg + '",' 463 | LogOutput += '"MAC Used":"' + GeneratedMAC + '",' 464 | LogOutput += '"Dry Run":"' + str(isDryRun) + '",' 465 | LogOutput += '"Verbose":"' + str(isVerbose) + '",' 466 | if ErrorMessages != "": 467 | LogOutput += '"Error Message":"' + ErrorMessages + '",' 468 | LogOutput += '"Result":"' + Result + '",' 469 | LogOutput += '"Completion Time":"' + str(theCurrDateTime()) + '"' 470 | LogOutput += '}\n' 471 | try: 472 | with open(LOG, "a+w") as FD: 473 | FD.write(LogOutput) 474 | except: 475 | print "Error writing to log file: " + LOG 476 | 477 | if isSummary: 478 | if isDryRun: 479 | print "\nDry Run summary:" 480 | else: 481 | print "\nCreate VM Success:" 482 | 483 | if isVerbose: 484 | print "ESXi Host: " + HOST 485 | print "VM NAME: " + NAME 486 | print "vCPU: " + str(CPU) 487 | print "Memory: " + str(MEM) + "GB" 488 | print "VM Disk: " + str(HDISK) + "GB" 489 | if isVerbose: 490 | print "Format: " + DISKFORMAT 491 | print "DS Store: " + DSSTORE 492 | print "Network: " + NET 493 | if ISO: 494 | print "ISO: " + ISO 495 | if isVerbose: 496 | print "Guest OS: " + GUESTOS 497 | print "MAC: " + GeneratedMAC 498 | else: 499 | pass 500 | 501 | if CheckHasErrors: 502 | if isDryRun: 503 | print "Dry Run: Failed." 504 | sys.exit(1) 505 | else: 506 | if isDryRun: 507 | print "Dry Run: Success." 508 | else: 509 | print GeneratedMAC 510 | sys.exit(0) 511 | -------------------------------------------------------------------------------- /esxi-vm-destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | import argparse # Argument parser 5 | import datetime # For current Date/Time 6 | import time 7 | import os.path # To check if file exists 8 | import sys # For args 9 | import re # For regex 10 | import paramiko # For remote ssh 11 | import yaml 12 | import warnings 13 | 14 | from esxi_vm_functions import * 15 | 16 | # Defaults and Variable setup 17 | ConfigData = setup_config() 18 | NAME = "" 19 | LOG = ConfigData['LOG'] 20 | isDryRun = ConfigData['isDryRun'] 21 | isVerbose = ConfigData['isVerbose'] 22 | isSummary = ConfigData['isSummary'] 23 | HOST = ConfigData['HOST'] 24 | USER = ConfigData['USER'] 25 | PASSWORD = ConfigData['PASSWORD'] 26 | CPU = ConfigData['CPU'] 27 | MEM = ConfigData['MEM'] 28 | HDISK = int(ConfigData['HDISK']) 29 | DISKFORMAT = ConfigData['DISKFORMAT'] 30 | VIRTDEV = ConfigData['VIRTDEV'] 31 | STORE = ConfigData['STORE'] 32 | NET = ConfigData['NET'] 33 | ISO = ConfigData['ISO'] 34 | GUESTOS = ConfigData['GUESTOS'] 35 | 36 | ErrorMessages = "" 37 | CheckHasErrors = False 38 | DSPATH="" 39 | DSSTORE="" 40 | 41 | # 42 | # Process Arguments 43 | # 44 | parser = argparse.ArgumentParser(description='ESXi Create VM utility.') 45 | 46 | parser.add_argument("-H", "--Host", dest='HOST', type=str, help="ESXi Host/IP (" + str(HOST) + ")") 47 | parser.add_argument("-U", "--User", dest='USER', type=str, help="ESXi Host username (" + str(USER) + ")") 48 | parser.add_argument("-P", "--Password", dest='PASSWORD', type=str, help="ESXi Host password (*****)") 49 | parser.add_argument("-n", "--name", dest='NAME', type=str, help="VM name") 50 | parser.add_argument('-V', '--verbose', dest='isVerbosearg', action='store_true', help="Enable Verbose mode (" + str(isVerbose) + ")") 51 | parser.add_argument('--summary', dest='isSummaryarg', action='store_true', help="Display Summary (" + str(isSummary) + ")") 52 | 53 | 54 | args = parser.parse_args() 55 | 56 | if args.isVerbosearg: 57 | isVerbose = True 58 | if args.isSummaryarg: 59 | isSummary = True 60 | if args.HOST: 61 | HOST=args.HOST 62 | if args.USER: 63 | USER=args.USER 64 | if args.PASSWORD: 65 | PASSWORD=args.PASSWORD 66 | if args.NAME: 67 | NAME=args.NAME 68 | 69 | # 70 | # main() 71 | # 72 | LogOutput = '{' 73 | LogOutput += '"datetime":"' + str(theCurrDateTime()) + '",' 74 | 75 | if NAME == "": 76 | print "ERROR: Missing required option --name" 77 | sys.exit(1) 78 | 79 | try: 80 | ssh = paramiko.SSHClient() 81 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 82 | ssh.connect(HOST, username=USER, password=PASSWORD) 83 | 84 | (stdin, stdout, stderr) = ssh.exec_command("esxcli system version get |grep Version") 85 | type(stdin) 86 | if re.match("Version", str(stdout.readlines())) is not None: 87 | print "Unable to determine if this is a ESXi Host: %s, username: %s" % (HOST, USER) 88 | sys.exit(1) 89 | except: 90 | print "The Error is " + str(sys.exc_info()[0]) 91 | print "Unable to access ESXi Host: %s, username: %s" % (HOST, USER) 92 | sys.exit(1) 93 | 94 | # 95 | # Check if VM exists 96 | # 97 | VMID = -1 98 | try: 99 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/getallvms") 100 | type(stdin) 101 | for line in stdout.readlines(): 102 | splitLine = line.split() 103 | if NAME == splitLine[1]: 104 | VMID = splitLine[0] 105 | JNK = line.split('[')[1] 106 | STORE = JNK.split(']')[0] 107 | VMDIR = splitLine[3] 108 | 109 | if VMID == -1: 110 | print "Warning: VM " + NAME + " doesn't exists." 111 | ErrorMessages += " VM " + NAME + " doesn't exists." 112 | CheckHasErrors = True 113 | CheckHasWarnings = True 114 | except: 115 | print "The Error is " + str(sys.exc_info()[0]) 116 | sys.exit(1) 117 | 118 | # Get List of Volumes, 119 | try: 120 | (stdin, stdout, stderr) = ssh.exec_command("esxcli storage filesystem list |grep '/vmfs/volumes/.*true VMFS' |sort -nk7") 121 | type(stdin) 122 | VOLUMES = {} 123 | for line in stdout.readlines(): 124 | splitLine = line.split() 125 | VOLUMES[splitLine[0]] = splitLine[1] 126 | except: 127 | print "The Error is " + str(sys.exc_info()[0]) 128 | sys.exit(1) 129 | 130 | 131 | # Convert STORE to path and visa-versa 132 | V = [] 133 | for Path in VOLUMES: 134 | V.append(VOLUMES[Path]) 135 | if STORE == Path or STORE == VOLUMES[Path]: 136 | DSPATH = Path 137 | DSSTORE = VOLUMES[Path] 138 | 139 | 140 | if CheckHasErrors: 141 | Result = "Errors" 142 | else: 143 | Result = "Success" 144 | 145 | if not CheckHasErrors: 146 | try: 147 | 148 | CurrentState = "" 149 | CurrentStateCounter = 0 150 | while CurrentState != "off": 151 | if isVerbose: 152 | print "Get state VM" 153 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.getstate " + str(VMID)) 154 | type(stdin) 155 | lines = str(stdout.readlines()) + str(stderr.readlines()) 156 | if isVerbose: 157 | print "power.getstate: " + lines 158 | if re.search("Powered off", lines): 159 | CurrentState = "off" 160 | 161 | # Power off VM 162 | if isVerbose: 163 | print "Power OFF VM" 164 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/power.off " + str(VMID) + " ||echo") 165 | type(stdin) 166 | lines = str(stdout.readlines()) + str(stderr.readlines()) 167 | if isVerbose: 168 | print "power.off: " + str(lines) 169 | 170 | CurrentStateCounter += 1 171 | if CurrentStateCounter >10: 172 | break 173 | time.sleep(1) 174 | 175 | # destroy VM 176 | if isVerbose: 177 | print "Destroy VM" 178 | (stdin, stdout, stderr) = ssh.exec_command("vim-cmd vmsvc/destroy " + str(VMID)) 179 | type(stdin) 180 | lines = str(stdout.readlines()) + str(stderr.readlines()) 181 | if isVerbose: 182 | print "destroy: " + str(lines) 183 | 184 | except: 185 | print "There was an error destroying the VM." 186 | ErrorMessages += " There was an error destroying the VM." 187 | CheckHasErrors = True 188 | Result = "Fail" 189 | 190 | # Print Summary 191 | 192 | # 193 | # The output log string 194 | LogOutput += '"Host":"' + HOST + '",' 195 | LogOutput += '"Name":"' + NAME + '",' 196 | LogOutput += '"Store Used":"' + DSPATH + '",' 197 | LogOutput += '"Verbose":"' + str(isVerbose) + '",' 198 | if ErrorMessages != "": 199 | LogOutput += '"Error Message":"' + ErrorMessages + '",' 200 | LogOutput += '"Result":"' + Result + '",' 201 | LogOutput += '"Completion Time":"' + str(theCurrDateTime()) + '"' 202 | LogOutput += '}\n' 203 | try: 204 | with open(LOG, "a+w") as FD: 205 | FD.write(LogOutput) 206 | except: 207 | print "Error writing to log file: " + LOG 208 | 209 | if isSummary: 210 | if isVerbose: 211 | print "ESXi Host: " + HOST 212 | print "VM NAME: " + NAME 213 | print "Path: " + DSSTORE 214 | else: 215 | pass 216 | 217 | if CheckHasErrors and not CheckHasWarnings: 218 | print "Failed" 219 | sys.exit(1) 220 | else: 221 | print "Success" 222 | sys.exit(0) 223 | 224 | 225 | -------------------------------------------------------------------------------- /esxi_vm_functions.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import yaml 3 | import datetime # For current Date/Time 4 | import paramiko # For remote ssh 5 | from math import log 6 | 7 | 8 | # 9 | # 10 | # Functions 11 | # 12 | # 13 | 14 | 15 | def setup_config(): 16 | 17 | # 18 | # System wide defaults 19 | # 20 | ConfigData = dict( 21 | 22 | # Your logfile 23 | LOG= os.path.expanduser("~") + "/esxi-vm.log", 24 | 25 | # Enable/Disable dryrun by default 26 | isDryRun=False, 27 | 28 | # Enable/Disable Verbose output by default 29 | isVerbose=False, 30 | 31 | # Enable/Disable exit summary by default 32 | isSummary=False, 33 | 34 | # ESXi host/IP, root login & password 35 | HOST="esxi", 36 | USER="root", 37 | PASSWORD="", 38 | 39 | # Default number of vCPU's, GB Mem, & GB boot disk 40 | CPU=2, 41 | MEM=4, 42 | HDISK=20, 43 | 44 | # Default Disk format thin, zeroedthick, eagerzeroedthick 45 | DISKFORMAT="thin", 46 | 47 | # Virtual Disk device type 48 | VIRTDEV="pvscsi", 49 | 50 | # Specify default Disk store to "LeastUsed" 51 | STORE="LeastUsed", 52 | 53 | # Default Network Interface (vswitch) 54 | NET="None", 55 | 56 | # Default ISO 57 | ISO="None", 58 | 59 | # Default GuestOS type. (See VMware documentation for all available options) 60 | GUESTOS="centos-64", 61 | 62 | # Extra VMX options 63 | VMXOPTS="" 64 | ) 65 | 66 | ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml" 67 | 68 | # 69 | # Get ConfigData from ConfigDataFile, then merge. 70 | # 71 | if os.path.exists(ConfigDataFileLocation): 72 | FromFileConfigData = yaml.safe_load(open(ConfigDataFileLocation)) 73 | ConfigData.update(FromFileConfigData) 74 | 75 | try: 76 | with open(ConfigDataFileLocation, 'w') as FD: 77 | yaml.dump(ConfigData, FD, default_flow_style=False) 78 | FD.close() 79 | except: 80 | print "Unable to create/update config file " + ConfigDataFileLocation 81 | e = sys.exc_info()[0] 82 | print "The Error is " + str(e) 83 | sys.exit(1) 84 | return ConfigData 85 | 86 | def SaveConfig(ConfigData): 87 | ConfigDataFileLocation = os.path.expanduser("~") + "/.esxi-vm.yml" 88 | try: 89 | with open(ConfigDataFileLocation, 'w') as FD: 90 | yaml.dump(ConfigData, FD, default_flow_style=False) 91 | FD.close() 92 | except: 93 | print "Unable to create/update config file " + ConfigDataFileLocation 94 | e = sys.exc_info()[0] 95 | print "The Error is " + str(e) 96 | return 1 97 | return 0 98 | 99 | 100 | def theCurrDateTime(): 101 | i = datetime.datetime.now() 102 | return str(i.isoformat()) 103 | 104 | 105 | unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], [0, 0, 1, 2, 2, 2]) 106 | def float2human(num): 107 | """Integer to Human readable""" 108 | if num > 1: 109 | exponent = min(int(log(float(num), 1024)), len(unit_list) - 1) 110 | quotient = float(num) / 1024**exponent 111 | unit, num_decimals = unit_list[exponent] 112 | format_string = '{:.%sf} {}' % (num_decimals) 113 | return format_string.format(quotient, unit) 114 | if num == 0: 115 | return '0 bytes' 116 | if num == 1: 117 | return '1 byte' 118 | --------------------------------------------------------------------------------