#!/usr/bin/python2 # -*- coding: utf-8 -*- # TODO: # Ability to set CPU/Memory reservations try: import json except ImportError: import simplejson as json HAS_PYSPHERE = False try: from pysphere import VIServer, VIProperty, MORTypes from pysphere.resources import VimService_services as VI from pysphere.vi_task import VITask from pysphere import VIException, VIApiException, FaultTypes HAS_PYSPHERE = True except ImportError: pass def add_scsi_controller(module, s, config, devices, type="paravirtual", bus_num=0, disk_ctrl_key=1): # add a scsi controller scsi_ctrl_spec = config.new_deviceChange() scsi_ctrl_spec.set_element_operation('add') if type == "lsi": # For RHEL5 scsi_ctrl = VI.ns0.VirtualLsiLogicController_Def("scsi_ctrl").pyclass() elif type == "paravirtual": # For RHEL6 scsi_ctrl = VI.ns0.ParaVirtualSCSIController_Def("scsi_ctrl").pyclass() elif type == "lsi_sas": scsi_ctrl = VI.ns0.VirtualLsiLogicSASController_Def( "scsi_ctrl").pyclass() elif type == "bus_logic": scsi_ctrl = VI.ns0.VirtualBusLogicController_Def("scsi_ctrl").pyclass() else: s.disconnect() module.fail_json( msg="Error adding scsi controller to vm spec. No scsi controller" " type of: %s" % (type)) scsi_ctrl.set_element_busNumber(bus_num) scsi_ctrl.set_element_key(disk_ctrl_key) scsi_ctrl.set_element_sharedBus("noSharing") scsi_ctrl_spec.set_element_device(scsi_ctrl) # Add the scsi controller to the VM spec. devices.append(scsi_ctrl_spec) return disk_ctrl_key def add_disk(module, s, config_target, config, devices, datastore, type="thin", size=200000, disk_ctrl_key=1, disk_number=0, key=0): # add a vmdk disk # Verify the datastore exists datastore_name, ds = find_datastore(module, s, datastore, config_target) # create a new disk - file based - for the vm disk_spec = config.new_deviceChange() disk_spec.set_element_fileOperation("create") disk_spec.set_element_operation("add") disk_ctlr = VI.ns0.VirtualDisk_Def("disk_ctlr").pyclass() disk_backing = VI.ns0.VirtualDiskFlatVer2BackingInfo_Def( "disk_backing").pyclass() disk_backing.set_element_fileName(datastore_name) disk_backing.set_element_diskMode("persistent") if type != "thick": disk_backing.set_element_thinProvisioned(1) disk_ctlr.set_element_key(key) disk_ctlr.set_element_controllerKey(disk_ctrl_key) disk_ctlr.set_element_unitNumber(disk_number) disk_ctlr.set_element_backing(disk_backing) disk_ctlr.set_element_capacityInKB(size) disk_spec.set_element_device(disk_ctlr) devices.append(disk_spec) def add_cdrom(module, s, config_target, config, devices, default_devs, type="client", vm_cd_iso_path=None): # Add a cd-rom # Make sure the datastore exists. if vm_cd_iso_path: iso_location = vm_cd_iso_path.split('/', 1) datastore, ds = find_datastore( module, s, iso_location[0], config_target) iso_path = iso_location[1] # find ide controller ide_ctlr = None for dev in default_devs: if dev.typecode.type[1] == "VirtualIDEController": ide_ctlr = dev # add a cdrom based on a physical device if ide_ctlr: cd_spec = config.new_deviceChange() cd_spec.set_element_operation('add') cd_ctrl = VI.ns0.VirtualCdrom_Def("cd_ctrl").pyclass() if type == "iso": iso = VI.ns0.VirtualCdromIsoBackingInfo_Def("iso").pyclass() ds_ref = iso.new_datastore(ds) ds_ref.set_attribute_type(ds.get_attribute_type()) iso.set_element_datastore(ds_ref) iso.set_element_fileName("%s %s" % (datastore, iso_path)) cd_ctrl.set_element_backing(iso) cd_ctrl.set_element_key(20) cd_ctrl.set_element_controllerKey(ide_ctlr.get_element_key()) cd_ctrl.set_element_unitNumber(0) cd_spec.set_element_device(cd_ctrl) elif type == "client": client = VI.ns0.VirtualCdromRemoteAtapiBackingInfo_Def( "client").pyclass() client.set_element_deviceName("") cd_ctrl.set_element_backing(client) cd_ctrl.set_element_key(20) cd_ctrl.set_element_controllerKey(ide_ctlr.get_element_key()) cd_ctrl.set_element_unitNumber(0) cd_spec.set_element_device(cd_ctrl) else: s.disconnect() module.fail_json( msg="Error adding cdrom of type %s to vm spec. " " cdrom type can either be iso or client" % (type)) devices.append(cd_spec) def add_nic(module, s, nfmor, config, devices, nic_type="vmxnet3", network_name="VM Network", network_type="standard"): # add a NIC # Different network card types are: "VirtualE1000", # "VirtualE1000e","VirtualPCNet32", "VirtualVmxnet", "VirtualNmxnet2", # "VirtualVmxnet3" nic_spec = config.new_deviceChange() nic_spec.set_element_operation("add") if nic_type == "e1000": nic_ctlr = VI.ns0.VirtualE1000_Def("nic_ctlr").pyclass() elif nic_type == "e1000e": nic_ctlr = VI.ns0.VirtualE1000e_Def("nic_ctlr").pyclass() elif nic_type == "pcnet32": nic_ctlr = VI.ns0.VirtualPCNet32_Def("nic_ctlr").pyclass() elif nic_type == "vmxnet": nic_ctlr = VI.ns0.VirtualVmxnet_Def("nic_ctlr").pyclass() elif nic_type == "vmxnet2": nic_ctlr = VI.ns0.VirtualVmxnet2_Def("nic_ctlr").pyclass() elif nic_type == "vmxnet3": nic_ctlr = VI.ns0.VirtualVmxnet3_Def("nic_ctlr").pyclass() else: s.disconnect() module.fail_json( msg="Error adding nic to vm spec. No nic type of: %s" % (nic_type)) if network_type == "standard": nic_backing = VI.ns0.VirtualEthernetCardNetworkBackingInfo_Def( "nic_backing").pyclass() nic_backing.set_element_deviceName(network_name) elif network_type == "dvs": # Get the portgroup key portgroupKey = find_portgroup_key(module, s, nfmor, network_name) # Get the dvswitch uuid dvswitch_uuid = find_dvswitch_uuid(module, s, nfmor, portgroupKey) nic_backing_port = VI.ns0.DistributedVirtualSwitchPortConnection_Def( "nic_backing_port").pyclass() nic_backing_port.set_element_switchUuid(dvswitch_uuid) nic_backing_port.set_element_portgroupKey(portgroupKey) nic_backing = VI.ns0.VirtualEthernetCardDistributedVirtualPortBackingInfo_Def( "nic_backing").pyclass() nic_backing.set_element_port(nic_backing_port) else: s.disconnect() module.fail_json( msg="Error adding nic backing to vm spec. No network type of:" " %s" % (network_type)) nic_ctlr.set_element_addressType("generated") nic_ctlr.set_element_backing(nic_backing) nic_ctlr.set_element_key(4) nic_spec.set_element_device(nic_ctlr) devices.append(nic_spec) def find_datastore(module, s, datastore, config_target): # Verify the datastore exists and put it in brackets if it does. ds = None for d in config_target.Datastore: if (d.Datastore.Accessible and (datastore and d.Datastore.Name == datastore) or (not datastore)): ds = d.Datastore.Datastore datastore = d.Datastore.Name break if not ds: s.disconnect() module.fail_json(msg="Datastore: %s does not appear to exist" % (datastore)) datastore_name = "[%s]" % datastore return datastore_name, ds def find_portgroup_key(module, s, nfmor, network_name): # Find a portgroups key given the portgroup name. # Grab all the distributed virtual portgroup's names and key's. dvpg_mors = s._retrieve_properties_traversal( property_names=['name', 'key'], from_node=nfmor, obj_type='DistributedVirtualPortgroup') # Get the correct portgroup managed object. dvpg_mor = None for dvpg in dvpg_mors: if dvpg_mor: break for p in dvpg.PropSet: if p.Name == "name" and p.Val == network_name: dvpg_mor = dvpg if dvpg_mor: break # If dvpg_mor is empty we didn't find the named portgroup. if dvpg_mor is None: s.disconnect() module.fail_json( msg="Could not find the distributed virtual portgroup named" " %s" % network_name) # Get the portgroup key portgroupKey = None for p in dvpg_mor.PropSet: if p.Name == "key": portgroupKey = p.Val return portgroupKey def find_dvswitch_uuid(module, s, nfmor, portgroupKey): # Find a dvswitch's uuid given a portgroup key. # Function searches all dvswitches in the datacenter to find the switch # that has the portgroup key. # Grab the dvswitch uuid and portgroup properties dvswitch_mors = s._retrieve_properties_traversal( property_names=['uuid', 'portgroup'], from_node=nfmor, obj_type='DistributedVirtualSwitch') dvswitch_mor = None # Get the dvswitches managed object for dvswitch in dvswitch_mors: if dvswitch_mor: break for p in dvswitch.PropSet: if p.Name == "portgroup": pg_mors = p.Val.ManagedObjectReference for pg_mor in pg_mors: if dvswitch_mor: break key_mor = s._get_object_properties( pg_mor, property_names=['key']) for key in key_mor.PropSet: if key.Val == portgroupKey: dvswitch_mor = dvswitch # Get the switches uuid dvswitch_uuid = None for p in dvswitch_mor.PropSet: if p.Name == "uuid": dvswitch_uuid = p.Val return dvswitch_uuid def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, vm_extra_config, vm_hardware, vm_disk, vm_nic, state): datacenter = esxi['datacenter'] esxi_hostname = esxi['hostname'] # Datacenter managed object reference dcmor = [k for k, v in vsphere_client.get_datacenters().items() if v == datacenter][0] if dcmor is None: vsphere_client.disconnect() module.fail_json(msg="Cannot find datacenter named: %s" % datacenter) dcprops = VIProperty(vsphere_client, dcmor) # hostFolder managed reference hfmor = dcprops.hostFolder._obj # virtualmachineFolder managed object reference vmfmor = dcprops.vmFolder._obj # networkFolder managed object reference nfmor = dcprops.networkFolder._obj # Grab the computerResource name and host properties crmors = vsphere_client._retrieve_properties_traversal( property_names=['name', 'host'], from_node=hfmor, obj_type='ComputeResource') # Grab the host managed object reference of the esxi_hostname try: hostmor = [k for k, v in vsphere_client.get_hosts().items() if v == esxi_hostname][0] except IndexError, e: vsphere_client.disconnect() module.fail_json(msg="Cannot find esx host named: %s" % esxi_hostname) # Grab the computerResource managed object reference of the host we are # creating the VM on. crmor = None for cr in crmors: if crmor: break for p in cr.PropSet: if p.Name == "host": for h in p.Val.get_element_ManagedObjectReference(): if h == hostmor: crmor = cr.Obj break if crmor: break crprops = VIProperty(vsphere_client, crmor) # Get resource pool managed reference # Requires that a cluster name be specified. if resource_pool: try: cluster = [k for k, v in vsphere_client.get_clusters().items() if v == cluster_name][0] except IndexError, e: vsphere_client.disconnect() module.fail_json(msg="Cannot find Cluster named: %s" % cluster_name) try: rpmor = [k for k, v in vsphere_client.get_resource_pools( from_mor=cluster).items() if v == resource_pool][0] except IndexError, e: vsphere_client.disconnect() module.fail_json(msg="Cannot find Resource Pool named: %s" % resource_pool) else: rpmor = crprops.resourcePool._obj # CREATE VM CONFIGURATION # get config target request = VI.QueryConfigTargetRequestMsg() _this = request.new__this(crprops.environmentBrowser._obj) _this.set_attribute_type( crprops.environmentBrowser._obj.get_attribute_type()) request.set_element__this(_this) h = request.new_host(hostmor) h.set_attribute_type(hostmor.get_attribute_type()) request.set_element_host(h) config_target = vsphere_client._proxy.QueryConfigTarget(request)._returnval # get default devices request = VI.QueryConfigOptionRequestMsg() _this = request.new__this(crprops.environmentBrowser._obj) _this.set_attribute_type( crprops.environmentBrowser._obj.get_attribute_type()) request.set_element__this(_this) h = request.new_host(hostmor) h.set_attribute_type(hostmor.get_attribute_type()) request.set_element_host(h) config_option = vsphere_client._proxy.QueryConfigOption(request)._returnval default_devs = config_option.DefaultDevice # add parameters to the create vm task create_vm_request = VI.CreateVM_TaskRequestMsg() config = create_vm_request.new_config() vmfiles = config.new_files() datastore_name, ds = find_datastore( module, vsphere_client, vm_disk['disk1']['datastore'], config_target) vmfiles.set_element_vmPathName(datastore_name) config.set_element_files(vmfiles) config.set_element_name(guest) if vm_extra_config['notes'] is not None: config.set_element_annotation(vm_extra_config['notes']) config.set_element_memoryMB(vm_hardware['memory_mb']) config.set_element_numCPUs(vm_hardware['num_cpus']) config.set_element_guestId(vm_hardware['osid']) devices = [] # Attach all the hardware we want to the VM spec. # Add a scsi controller to the VM spec. disk_ctrl_key = add_scsi_controller( module, vsphere_client, config, devices, vm_hardware['scsi']) if vm_disk: disk_num = 0 disk_key = 0 for disk in sorted(vm_disk.iterkeys()): try: datastore = vm_disk[disk]['datastore'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. datastore needs to be" " specified." % disk) try: disksize = vm_disk[disk]['size_gb'] # Convert the disk size to kiloboytes disksize = disksize * 1024 * 1024 except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. size needs to be" " specified." % disk) try: disktype = vm_disk[disk]['type'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. type needs to be" " specified." % disk) # Add the disk to the VM spec. add_disk( module, vsphere_client, config_target, config, devices, datastore, disktype, disksize, disk_ctrl_key, disk_num, disk_key) disk_num = disk_num + 1 disk_key = disk_key + 1 if 'vm_cdrom' in vm_hardware: cdrom_iso_path = None cdrom_type = None try: cdrom_type = vm_hardware['vm_cdrom']['type'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. cdrom type needs to be" " specified." % vm_hardware['vm_cdrom']) if cdrom_type == 'iso': try: cdrom_iso_path = vm_hardware['vm_cdrom']['iso_path'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. cdrom iso_path needs" " to be specified." % vm_hardware['vm_cdrom']) # Add a CD-ROM device to the VM. add_cdrom(module, vsphere_client, config_target, config, devices, default_devs, cdrom_type, cdrom_iso_path) if vm_nic: for nic in sorted(vm_nic.iterkeys()): try: nictype = vm_nic[nic]['type'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. type needs to be " " specified." % nic) try: network = vm_nic[nic]['network'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. network needs to be " " specified." % nic) try: network_type = vm_nic[nic]['network_type'] except KeyError: vsphere_client.disconnect() module.fail_json( msg="Error on %s definition. network_type needs to be " " specified." % nic) # Add the nic to the VM spec. add_nic(module, vsphere_client, nfmor, config, devices, nictype, network, network_type) config.set_element_deviceChange(devices) create_vm_request.set_element_config(config) folder_mor = create_vm_request.new__this(vmfmor) folder_mor.set_attribute_type(vmfmor.get_attribute_type()) create_vm_request.set_element__this(folder_mor) rp_mor = create_vm_request.new_pool(rpmor) rp_mor.set_attribute_type(rpmor.get_attribute_type()) create_vm_request.set_element_pool(rp_mor) host_mor = create_vm_request.new_host(hostmor) host_mor.set_attribute_type(hostmor.get_attribute_type()) create_vm_request.set_element_host(host_mor) # CREATE THE VM taskmor = vsphere_client._proxy.CreateVM_Task(create_vm_request)._returnval task = VITask(taskmor, vsphere_client) task.wait_for_state([task.STATE_SUCCESS, task.STATE_ERROR]) if task.get_state() == task.STATE_ERROR: vsphere_client.disconnect() module.fail_json(msg="Error creating vm: %s" % task.get_error_message()) else: vm = None if vm_extra_config or state in ['powered_on', 'powered_off']: vm = vsphere_client.get_vm_by_name(guest) # VM was created. If there is any extra config options specified, set # them here , disconnect from vcenter, then exit. if vm_extra_config: vm.set_extra_config(vm_extra_config) # Power on the VM if it was requested power_state(vm, state, True) def power_state(vm, state, force): """ Correctly set the power status for a VM determined by the current and requested states. force is forceful """ power_status = vm.get_status() check_status = ' '.join(state.split("_")).upper() # Need Force if not force and power_status in [ 'SUSPENDED', 'POWERING ON', 'RESETTING', 'BLOCKED ON MSG' ]: return "VM is in %s power state. Force is required!" % power_status # State is already true if power_status == check_status: return False else: try: if state == 'powered_off': vm.power_off(sync_run=True) elif state == 'powered_on': vm.power_on(sync_run=True) elif state == 'restarted': if power_status in ('POWERED ON', 'POWERING ON', 'RESETTING'): vm.reset(sync_run=False) else: return "Cannot restart VM in the current state %s" \ % power_status return True except Exception, e: return e return False def gather_facts(vm): """ Gather facts for VM directly from vsphere. """ vm.get_properties() facts = { 'module_hw': True, 'hw_name': vm.properties.name, 'hw_guest_full_name': vm.properties.config.guestFullName, 'hw_guest_id': vm.properties.config.guestId, 'hw_product_uuid': vm.properties.config.uuid, 'hw_processor_count': vm.properties.config.hardware.numCPU, 'hw_memtotal_mb': vm.properties.config.hardware.memoryMB, } ifidx = 0 for entry in vm.properties.config.hardware.device: if not hasattr(entry, 'macAddress'): continue factname = 'hw_eth' + str(ifidx) facts[factname] = { 'addresstype': entry.addressType, 'label': entry.deviceInfo.label, 'macaddress': entry.macAddress, 'macaddress_dash': entry.macAddress.replace(':', '-'), 'summary': entry.deviceInfo.summary, } ifidx += 1 return facts class DefaultVMConfig(object): """ Shallow and deep dict comparison for interfaces """ def __init__(self, check_dict, interface_dict): self.check_dict, self.interface_dict = check_dict, interface_dict self.set_current, self.set_past = set( check_dict.keys()), set(interface_dict.keys()) self.intersect = self.set_current.intersection(self.set_past) self.recursive_missing = None def shallow_diff(self): return self.set_past - self.intersect def recursive_diff(self): if not self.recursive_missing: self.recursive_missing = [] for key, value in self.interface_dict.items(): if isinstance(value, dict): for k, v in value.items(): if k in self.check_dict[key]: if not isinstance(self.check_dict[key][k], v): self.recursive_missing.append((k, v)) else: self.recursive_missing.append((k, v)) return self.recursive_missing def config_check(name, passed, default, module): """ Checks that the dict passed for VM configuration matches the required interface declared at the top of __main__ """ diff = DefaultVMConfig(passed, default) if len(diff.shallow_diff()): module.fail_json( msg="Missing required key/pair [%s]. %s must contain %s" % (', '.join(diff.shallow_diff()), name, default)) if diff.recursive_diff(): module.fail_json( msg="Config mismatch for %s on %s" % (name, diff.recursive_diff())) return True def main(): vm = None proto_vm_hardware = { 'memory_mb': int, 'num_cpus': int, 'scsi': basestring, 'osid': basestring } proto_vm_disk = { 'disk1': { 'datastore': basestring, 'size_gb': int, 'type': basestring } } proto_vm_nic = { 'nic1': { 'type': basestring, 'network': basestring, 'network_type': basestring } } proto_esxi = { 'datacenter': basestring, 'hostname': basestring } module = AnsibleModule( argument_spec=dict( vcenter_hostname=dict(required=True, type='str'), username=dict(required=True, type='str'), password=dict(required=True, type='str'), state=dict( required=False, choices=[ 'powered_on', 'powered_off', 'present', 'absent', 'restarted', 'reconfigured' ], default='present'), vmware_guest_facts=dict(required=False, choices=BOOLEANS), guest=dict(required=True, type='str'), vm_disk=dict(required=False, type='dict', default={}), vm_nic=dict(required=False, type='dict', default={}), vm_hardware=dict(required=False, type='dict', default={}), vm_extra_config=dict(required=False, type='dict', default={}), resource_pool=dict(required=False, default=None, type='str'), cluster=dict(required=False, default=None, type='str'), force=dict(required=False, choices=BOOLEANS, default=False), esxi=dict(required=False, type='dict', default={}), ), supports_check_mode=False, mutually_exclusive=[['state', 'vmware_guest_facts']], required_together=[ ['state', 'force'], [ 'state', 'vm_disk', 'vm_nic', 'vm_hardware', 'esxi' ], ['resource_pool', 'cluster'] ], ) if not HAS_PYSPHERE: module.fail_json(msg='pysphere module required') vcenter_hostname = module.params['vcenter_hostname'] username = module.params['username'] password = module.params['password'] vmware_guest_facts = module.params['vmware_guest_facts'] state = module.params['state'] guest = module.params['guest'] force = module.params['force'] vm_disk = module.params['vm_disk'] vm_nic = module.params['vm_nic'] vm_hardware = module.params['vm_hardware'] vm_extra_config = module.params['vm_extra_config'] esxi = module.params['esxi'] resource_pool = module.params['resource_pool'] cluster = module.params['cluster'] # CONNECT TO THE SERVER viserver = VIServer() try: viserver.connect(vcenter_hostname, username, password) except VIApiException, err: module.fail_json(msg="Cannot connect to %s: %s" % (vcenter_hostname, err)) # Check if the VM exists before continuing try: vm = viserver.get_vm_by_name(guest) # Run for facts only if vmware_guest_facts: try: module.exit_json(ansible_facts=gather_facts(vm)) except Exception, e: module.fail_json( msg="Fact gather failed with exception %s" % e) # Power Changes elif state in ['powered_on', 'powered_off', 'restarted']: state_result = power_state(vm, state, force) # Failure if isinstance(state_result, basestring): module.fail_json(msg=state_result) else: module.exit_json(changed=state_result) # Just check if there elif state == 'present': module.exit_json(changed=False) # Fail on reconfig without params elif state == 'reconfigured': pass # VM doesn't exist except Exception: # Fail for fact gather task if vmware_guest_facts: module.fail_json( msg="No such VM %s. Fact gathering requires an existing vm" % guest) if state in ['restarted', 'reconfigured']: module.fail_json( msg="No such VM %s. States [" "restarted, reconfigured] required an existing VM" % guest) elif state == 'absent': module.exit_json(changed=False, msg="vm %s not present" % guest) # Create the VM elif state in ['present', 'powered_off', 'powered_on']: # Check the guest_config config_check("vm_disk", vm_disk, proto_vm_disk, module) config_check("vm_nic", vm_nic, proto_vm_nic, module) config_check("vm_hardware", vm_hardware, proto_vm_hardware, module) config_check("esxi", esxi, proto_esxi, module) create_vm( vsphere_client=viserver, module=module, esxi=esxi, resource_pool=resource_pool, cluster_name=cluster, guest=guest, vm_extra_config=vm_extra_config, vm_hardware=vm_hardware, vm_disk=vm_disk, vm_nic=vm_nic, state=state ) if vm: # If the vm already exists, lets get some info from it, pass back the # vm's vmware_guest_facts and then exit. viserver.disconnect() module.exit_json( changed=False, vcenter=vcenter_hostname) # this is magic, see lib/ansible/module_common.py #<> main()