Development
PySphere script to clone a template into multiple VMs with post processing
*There is a better/newer version of this script, for more details, check this post: https://dellaert.org/2014/02/03/multi-clone-py-multi-threaded-cloning-of-a-template-to-multiple-vms/ – Or via github: https://github.com/pdellaert/vSphere-Python *
For an internal test setup i needed to be able to deploy multiple VMs from a template without to much hassle. So i started thinking that the best way to approach this, would be using a script. At first i thought i had to do this using PowerCLI as this is the preferred VMware way of scripting.
Luckily i came across the wonderful site of PySphere, which is a Python library that provides tools to access and administer a VMware vSphere setup. As Python wasn’t my strong suite, i was in a bit of a dilemma, i had almost no experience with either of those languages, so which to go for. Altho PowerCLI/Powershell has a lot more possibilities, as it is maintained and developed by VMware itself, Python had the great advantage i could do it all in a more familiar environment (Linux). It’s also closer to the languages i know than Powershell is. So i decided to just go for Python and see if it got me where i wanted to go.
This script deploys multiple VMs from a single template, you can specify how many and what basename the VMs should have. Each VM gets a name starting with the basename and a number which increments with each new VM. You are able to specify at what number it should start. You can also specify in which resource pool the VMs should be placed.
And as a final feature, you can specify a script which should be called after the VM has successfully booted and the guest OS has initiated it’s network interface. This script will be called with two arguments: the VM name and it’s IP. You can even specify it may only return a valid IPv6 address (i needed this to deploy VMs in an IPv6-only test environment).
The output if run with the help argument (-h):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
philippe@Draken-Korin:~/VMware/vSphere-Python$ ./multi-clone.py -h usage: multi-clone.py [-h] [-6] -b BASENAME [-c COUNT] [-n AMOUNT] [-p POST_SCRIPT] [-r RESOURCE_POOL] -s SERVER -t TEMPLATE -u USERNAME [-v] [-w MAXWAIT] Deploy a template into multiple VM's optional arguments: -h, --help show this help message and exit -6, --six Get IPv6 address for VMs instead of IPv4 -b BASENAME, --basename BASENAME Basename of the newly deployed VMs -c COUNT, --count COUNT Starting count, the name of the first VM deployed will be -, the second will be - (default=1) -n AMOUNT, --number AMOUNT Amount of VMs to deploy (default=1) -p POST_SCRIPT, --post-script POST_SCRIPT Script to be called after each VM is created and booted. Arguments passed: name ip-address -r RESOURCE_POOL, --resource-pool RESOURCE_POOL The resource pool in which the new VMs should reside -s SERVER, --server SERVER The vCenter or ESXi server to connect to -t TEMPLATE, --template TEMPLATE Template to deploy -u USERNAME, --user USERNAME The username with which to connect to the server -v, --verbose Enable verbose output -w MAXWAIT, --wait-max MAXWAIT Maximum amount of seconds to wait when gathering information (default 120) |
To run the script, the command should look something like this:
1 |
philippe@Draken-Korin:~/VMware/scripts$ ./multi-clone.py -s vcenter.server.domain.tld -u DOMAIN\\USER -t VM-Template -b Deployed-VM -c 1 -n 10 -r TestRP -p ./post-process.sh -6 -v -w 120 |
And finally, the script itself, you can also download it on Github:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
#!/usr/bin/python import sys, re, getpass, argparse, subprocess from time import sleep from pysphere import MORTypes, VIServer, VITask, VIProperty, VIMor, VIException from pysphere.vi_virtual_machine import VIVirtualMachine def print_verbose(message): if verbose: print message def find_vm(name): try: vm = con.get_vm_by_name(name) return vm except VIException: return None def find_resource_pool(name): rps = con.get_resource_pools() for mor, path in rps.iteritems(): print_verbose('Parsing RP %s' % path) if re.match('.*%s' % name,path): return mor return None def run_post_script(name,ip): print_verbose('Running post script: %s %s %s' % (post_script,name,ip)) retcode = subprocess.call([post_script,name,ip]) if retcode < 0: print 'ERROR: %s %s %s : Returned a non-zero result' % (post_script,name,ip) sys.exit(1) def find_ip(vm,ipv6=False): net_info = None waitcount = 0 while net_info is None: if waitcount > maxwait: break net_info = vm.get_property('net',False) print_verbose('Waiting 5 seconds ...') waitcount += 5 sleep(5) if net_info: for ip in net_info[0]['ip_addresses']: if ipv6 and re.match('\d{1,4}\:.*',ip) and not re.match('fe83\:.*',ip): print_verbose('IPv6 address found: %s' % ip) return ip elif not ipv6 and re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',ip) and ip != '127.0.0.1': print_verbose('IPv4 address found: %s' % ip) return ip print_verbose('Timeout expired: No IP address found') return None parser = argparse.ArgumentParser(description="Deploy a template into multiple VM's") parser.add_argument('-6', '--six', required=False, help='Get IPv6 address for VMs instead of IPv4', dest='ipv6', action='store_true') parser.add_argument('-b', '--basename', nargs=1, required=True, help='Basename of the newly deployed VMs', dest='basename', type=str) parser.add_argument('-c', '--count', nargs=1, required=False, help='Starting count, the name of the first VM deployed will be <basename>-<count>, the second will be <basename>-<count+1> (default=1)', dest='count', type=int, default=[1]) parser.add_argument('-n', '--number', nargs=1, required=False, help='Amount of VMs to deploy (default=1)', dest='amount', type=int, default=[1]) parser.add_argument('-p', '--post-script', nargs=1, required=False, help='Script to be called after each VM is created and booted. Arguments passed: name ip-address', dest='post_script', type=str) parser.add_argument('-r', '--resource-pool', nargs=1, required=False, help='The resource pool in which the new VMs should reside', dest='resource_pool', type=str) parser.add_argument('-s', '--server', nargs=1, required=True, help='The vCenter or ESXi server to connect to', dest='server', type=str) parser.add_argument('-t', '--template', nargs=1, required=True, help='Template to deploy', dest='template', type=str) parser.add_argument('-u', '--user', nargs=1, required=True, help='The username with which to connect to the server', dest='username', type=str) parser.add_argument('-v', '--verbose', required=False, help='Enable verbose output', dest='verbose', action='store_true') parser.add_argument('-w', '--wait-max', nargs=1, required=False, help='Maximum amount of seconds to wait when gathering information (default 120)', dest='maxwait', type=int, default=[120]) args = parser.parse_args() ipv6 = args.ipv6 amount = args.amount[0] basename = args.basename[0] count = args.count[0] post_script = None if args.post_script: post_script = args.post_script[0] resource_pool = None if args.resource_pool: resource_pool = args.resource_pool[0] server = args.server[0] template = args.template[0] username = args.username[0] verbose = args.verbose maxwait = args.maxwait[0] # Asking Users password for server password=getpass.getpass(prompt='Enter password for vCenter %s for user %s: ' % (server,username)) # Connecting to server print_verbose('Connecting to server %s with username %s' % (server,username)) con = VIServer() con.connect(server,username,password) print_verbose('Connected to server %s' % server) print_verbose('Server type: %s' % con.get_server_type()) print_verbose('API version: %s' % con.get_api_version()) # Verify the template exists print_verbose('Finding template %s' % template) template_vm = find_vm(template) if template_vm is None: print 'ERROR: %s not found' % template sys.exit(1) print_verbose('Template %s found' % template) # Verify the target Resource Pool exists print_verbose('Finding resource pool %s' % resource_pool) resource_pool_mor = find_resource_pool(resource_pool) if resource_pool_mor is None: print 'ERROR: %s not found' % resource_pool sys.exit(1) print_verbose('Resource pool %s found' % resource_pool) # List with VM name elements for post script processing vms_to_ps = [] # Looping through amount that needs to be created for a in range(1,amount+1): print_verbose('================================================================================') vm_name = '%s-%i' % (basename,count) print_verbose('Trying to clone %s to VM %s' % (template,vm_name)) if find_vm(vm_name): print 'ERROR: %s already exists' % vm_name else: clone = template_vm.clone(vm_name, True, None, resource_pool_mor, None, None, False) print_verbose('VM %s created' % vm_name) print_verbose('Booting VM %s' % vm_name) clone.power_on() if post_script: vms_to_ps.append(vm_name) count += 1 # Looping through post scripting if necessary if post_script: for name in vms_to_ps: vm = find_vm(name) if vm: ip = find_ip(vm,ipv6) if ip: run_post_script(name,ip) else: print 'ERROR: No IP found for VM %s, post processing disabled' % name else: print 'ERROR: VM %s not found, post processing disabled' % name # Disconnecting from server con.disconnect() |
The script is probably a work in progress, as there are a lot of possibilities for improvement, if you have any requests feel free to contact me!
This is the first Python script i’ve ever written, so forgive me if i made some basic mistakes against best practices. Feel free to submit a patch.
PHP 5.3.2 DateTime diff() issue
It seems that PHP 5.3.2 ( 5.3.2-1ubuntu4.10 ) has a small issue when using the DateTime diff() method.
Got this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
< ?php $mode = 'D'; $back = 1; $start = new \DateTime(); $start->setTime(0, 0); $start->sub(new \DateInterval('P'.$back.$mode)); $end = new \DateTime(); $end->setTime(0, 0); if( $back > 0 ) { $end->sub(new \DateInterval('P'.($back-1).$mode)); } else { $end->add(new \DateInterval('P1'.$mode)); } echo $start->format('Y-m-d H:i:s') . "\n"; echo $end->format('Y-m-d H:i:s') . "\n"; $dt = $start->diff($end); echo $start->format('Y-m-d H:i:s') . "\n"; echo $end->format('Y-m-d H:i:s') . "\n"; $differenceDays = $dt->format('%a'); echo $differenceDays."\n"; ?> |
This code results in:
1 2 3 4 5 |
2011-12-03 00:00:00 2011-12-04 00:00:00 2011-12-02 00:00:00 2011-12-04 00:00:00 2 |
It seems the diff statement changes the starting date to a date before. This is kind of an issue, i need that difference in a project between days, weeks, months and even years. And the issue get’s worse if you use weeks (the difference becomes 14 days instead of 7) in the interval or months (61 instead of 30 or 31). So i decided to use a quickfix to calculate the difference, cause this was not working…
The workaround needed to change depending on the mode variable (day, week, month or year). For day and week it could just be 1 and 7 days, but the amount of days in a month and years changes per month and per year (leap years). So i came up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
< ?php switch($mode) { case "day": $differenceDays = 1; break; case "week": $differenceDays = 7; break; case "month": $differenceDays = date('t',$start->getTimestamp()); break; case "year": $differenceDays = 365; if( date('L',mktime(0,0,0,1,1,$start->format('Y'))) == 1 ) { $differenceDays++; } break; } ?> |
I decided to use the mktime function, because the getTimestamp() gave some issues concerning timezones (i think ;))
Update
Dries Verachtert pointed me to a comment on the page of the DateTime sub method. The short of it:
If you use diff() after sub(), the effects of the sub() will be repeated on the date object.
It doesn’t matter if the object is the one diffed or doing the diffing (i.e. which object you call diff() from).
Note that using add() instead of sub() does NOT have the same effect.
This is particularly undesirable — in this example you make a datetime, use sub() to make it a relative time in the past, and then date->diff() to confirm the difference. But the diff() inadvertendly makes the difference 2x.
Strange issue, but i can tell you it’s fixed in 5.3.3, as i have running that on an internal test server, and the code does a perfect job on that.