diff --git a/ezmomi/cli.py b/ezmomi/cli.py index 078ee05..f7028f5 100644 --- a/ezmomi/cli.py +++ b/ezmomi/cli.py @@ -2,6 +2,7 @@ Command line definitions for ezmomi ''' import argparse +from params import add_common_params from params import add_params from ezmomi import EZMomi @@ -11,6 +12,7 @@ def cli(): parser = argparse.ArgumentParser( description='Perform common vSphere API tasks' ) + subparsers = parser.add_subparsers(help='Command', dest='mode') # set up each command section diff --git a/ezmomi/ezmomi.py b/ezmomi/ezmomi.py index 5868b7d..0b255f5 100644 --- a/ezmomi/ezmomi.py +++ b/ezmomi/ezmomi.py @@ -64,10 +64,14 @@ def get_configs(self, kwargs): 'specify the config file path by setting the EZMOMI_CONFIG ' \ 'environment variable.' sys.exit(1) - except Exception: - print 'Unable to read config file. YAML syntax issue, perhaps?' + except Exception, e: + print 'Unable to read config file. YAML syntax issue, perhaps? \n %s' % e sys.exit(1) + # Handle empty configs + if not config: + config = {} + # Check all required values were supplied either via command line # or config. override defaults from config.yml with any supplied # command line arguments @@ -75,7 +79,7 @@ def get_configs(self, kwargs): for key, value in kwargs.items(): if value: config[key] = value - elif (value is None) and (key not in config): + elif (value is None) and (key not in config) and key is not "ips" and key is not "domain": # compile list of parameters that were not set notset.append(key) @@ -130,6 +134,7 @@ def list_objects(self): print "{0:<20} {1:<20}".format(c._moId, c.name) def clone(self): + self.config['hostname'] = self.config['hostname'].lower() self.config['mem'] = self.config['mem'] * 1024 # convert GB to MB @@ -142,42 +147,62 @@ def clone(self): ip_settings = list() # Get network settings for each IP - for key, ip_string in enumerate(self.config['ips']): - - # convert ip from string to the 'IPAddress' type - ip = IPAddress(ip_string) - - # determine network this IP is in - for network in self.config['networks']: - if ip in IPNetwork(network): - self.config['networks'][network]['ip'] = ip - ipnet = IPNetwork(network) - self.config['networks'][network]['subnet_mask'] = str( - ipnet.netmask - ) - ip_settings.append(self.config['networks'][network]) - - # throw an error if we couldn't find a network for this ip - if not any(d['ip'] == ip for d in ip_settings): - print "I don't know what network %s is in. You can supply " \ - "settings for this network in config.yml." % ip_string - sys.exit(1) + if 'ips' in self.config: + for key, ip_string in enumerate(self.config['ips']): + + # convert ip from string to the 'IPAddress' type + ip = IPAddress(ip_string) + + # determine network this IP is in + for network in self.config['networks']: + if ip in IPNetwork(network): + self.config['networks'][network]['ip'] = ip + ipnet = IPNetwork(network) + self.config['networks'][network]['subnet_mask'] = str( + ipnet.netmask + ) + ip_settings.append(self.config['networks'][network]) + + # throw an error if we couldn't find a network for this ip + if not any(d['ip'] == ip for d in ip_settings): + print "I don't know what network %s is in. You can supply " \ + "settings for this network in config.yml." % ip_string + sys.exit(1) # network to place new VM in - self.get_obj([vim.Network], ip_settings[0]['network']) - datacenter = self.get_obj([vim.Datacenter], - ip_settings[0]['datacenter'] - ) + if not self.config['dhcp']: + self.get_obj([vim.Network], ip_settings[0]['network']) + + cluster = self.get_obj([vim.ClusterComputeResource], + ip_settings[0]['cluster'] + ) + + datacenter = self.get_obj([vim.Datacenter], + ip_settings[0]['datacenter'] + ) + + datastore = self.get_obj([vim.Datastore], + ip_settings[0]['datastore'] + ) + else: + cluster = self.get_obj([vim.ClusterComputeResource], + self.config['cluster'] + ) + + datacenter = self.get_obj([vim.Datacenter], + self.config['datacenter'] + ) + + datastore = self.get_obj([vim.Datastore], + self.config['datastore'] + ) + # get the folder where VMs are kept for this datacenter destfolder = datacenter.vmFolder - cluster = self.get_obj([vim.ClusterComputeResource], - ip_settings[0]['cluster'] - ) # use same root resource pool that my desired cluster uses resource_pool = cluster.resourcePool - datastore = self.get_obj([vim.Datastore], ip_settings[0]['datastore']) template_vm = self.get_obj([vim.VirtualMachine], self.config['template'] ) @@ -250,12 +275,15 @@ def clone(self): # DNS settings globalip = vim.vm.customization.GlobalIPSettings() - globalip.dnsServerList = self.config['dns_servers'] - globalip.dnsSuffixList = self.config['domain'] + + if not self.config['dhcp']: + globalip.dnsServerList = self.config['dns_servers'] + globalip.dnsSuffixList = self.config['domain'] # Hostname settings ident = vim.vm.customization.LinuxPrep() - ident.domain = self.config['domain'] + if "domain" in self.config: + ident.domain = self.config['domain'] ident.hostName = vim.vm.customization.FixedName() ident.hostName.name = self.config['hostname'] @@ -268,25 +296,115 @@ def clone(self): clonespec = vim.vm.CloneSpec() clonespec.location = relospec clonespec.config = vmconf - clonespec.customization = customspec + + if not self.config['dhcp']: + clonespec.customization = customspec clonespec.powerOn = True clonespec.template = False - # fire the clone task - tasks = [template_vm.Clone(folder=destfolder, - name=self.config['hostname'], - spec=clonespec - )] + # Override the destination folder if user defined + if "folder" in self.config: + foldermap = self._get_folder_map() + if self.config['folder'] in foldermap: + folder = self.config['folder'] + destfolder = foldermap[folder] + + tasks = [] + if self.config['dhcp'] and self.config['count']: + for x in range(0, self.config['count']): + if '%s' in self.config['hostname']: + hostname = self.config['hostname'] % x + else: + hostname = self.config['hostname'] + "-%s" % x + tasks.append(template_vm.Clone(folder=destfolder, + name=hostname, + spec=clonespec)) + else: + # fire the clone task + tasks = [template_vm.Clone(folder=destfolder, + name=self.config['hostname'], + spec=clonespec + )] + result = self.WaitForTasks(tasks) + vmobjs = [x.info.result for x in tasks] + + if 'waitforip' in self.config: + if self.config['waitforip']: + for vmobj in vmobjs: + self._wait_for_ip(vmobj) + print "{0:<20} {1:<20} {2:<20}".format("Name", "IP", "UUID") + for vmobj in vmobjs: + ip = str(vmobj.summary.guest.ipAddress) + uuid = str(vmobj.config.uuid) + print "{0:<20} {1:<20} {2:<20}".format(vmobj.name, ip, uuid) + + if "mailfrom" in self.config: + self.send_email() + + def _get_folder_map(self): + + """ Return a mapping of full folder paths to folder objects """ + + def buildpath(folder_map, folder): + """ Recursively build out a folderpath """ + fullpath = folder + + if hasattr(folder_map[folder], "parent"): + parentname = folder_map[folder].parent.name + if not parentname in folder_map: + pass + else: + tpath = buildpath(folder_map, parentname) + fullpath = os.path.join(tpath, fullpath) + + return fullpath + + folder_map = {} + container = self.content.viewManager.CreateContainerView( + self.content.rootFolder, [eval("vim.Folder")], True) + + # make first pass map + for vf in container.view: + name = vf.name + folder_map[str(name)] = vf + + # make final map + fmap = {} + for k,v in folder_map.iteritems(): + fullpath = buildpath(folder_map, k) + if not fullpath.startswith('/'): + fullpath = '/' + fullpath + fmap[fullpath] = v + + return fmap + + def _wait_for_ip(self, vmobj): + + """ Poll a VirtualMachine object until it registers an IP address """ + + ip = None + count = 0 + + while count <= 300: + + hostname = vmobj.name + ip = vmobj.summary.guest.ipAddress + + if str(ip) != "None": + break + else: + print "Waiting for %s to obtain an ip address" % hostname + time.sleep(2) + count += 1 - self.send_email() def destroy(self): tasks = list() print "Finding VM named %s..." % self.config['name'] vm = self.get_obj([vim.VirtualMachine], self.config['name']) - # need to shut the VM down before destorying it + # need to shut the VM down before destroying it if vm.runtime.powerState == vim.VirtualMachinePowerState.poweredOn: tasks.append(vm.PowerOff()) diff --git a/ezmomi/params.py b/ezmomi/params.py index dfd2edc..50fde05 100644 --- a/ezmomi/params.py +++ b/ezmomi/params.py @@ -3,6 +3,29 @@ ''' +def add_common_params(parser): + parser.add_argument( + '--server', + type=str, + help='vCenter server', + ) + parser.add_argument( + '--port', + type=str, + default='443', + help='vCenter server port', + ) + parser.add_argument( + '--username', + type=str, + help='vCenter username', + ) + parser.add_argument( + '--password', + type=str, + help='vCenter password', + ) + def add_params(subparsers): # list list_parser = subparsers.add_parser( @@ -10,6 +33,8 @@ def add_params(subparsers): help='List VMware objects on your VMware server' ) + add_common_params(list_parser) + list_parser.add_argument( '--type', required=True, @@ -21,26 +46,9 @@ def add_params(subparsers): 'clone', help='Clone a VM template to a new VM' ) - clone_parser.add_argument( - '--server', - type=str, - help='vCenter server', - ) - clone_parser.add_argument( - '--port', - type=str, - help='vCenter server port', - ) - clone_parser.add_argument( - '--username', - type=str, - help='vCenter username', - ) - clone_parser.add_argument( - '--password', - type=str, - help='vCenter password', - ) + + add_common_params(clone_parser) + clone_parser.add_argument( '--template', type=str, @@ -54,11 +62,25 @@ def add_params(subparsers): ) clone_parser.add_argument( '--ips', + required=False, type=str, help='Static IPs of new host, separated by a space. ' 'List primary IP first.', nargs='+', ) + clone_parser.add_argument( + '--dhcp', + action="store_true", + required=False, + help='Use DHCP instead of static IPs', + ) + clone_parser.add_argument( + '--waitforip', + action="store_true", + required=False, + default=False, + help='Wait for the system to obtain and IP address', + ) clone_parser.add_argument( '--cpus', type=int, @@ -69,17 +91,45 @@ def add_params(subparsers): type=int, help='Memory in GB' ) + clone_parser.add_argument( + '--folder', + type=str, + required=False, + default='/', + help='Destination folder for the new VM' + ) + clone_parser.add_argument( + '--count', + type=int, + help='Number of VMs to launch [dhcp only]' + ) clone_parser.add_argument( '--domain', type=str, help='Domain, e.g. "example.com"' ) + clone_parser.add_argument( + '--datacenter', + type=str, + help='Datacenter' + ) + clone_parser.add_argument( + '--cluster', + type=str, + help='Cluster' + ) + clone_parser.add_argument( + '--datastore', + type=str, + help='Datastore' + ) # destroy destroy_parser = subparsers.add_parser( 'destroy', help='Destroy/delete a Virtual Machine' ) + add_common_params(destroy_parser) destroy_parser.add_argument( '--name', required=True,