422 lines
12 KiB
Python
422 lines
12 KiB
Python
#!/usr/bin/python2
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# TODO:
|
|
# Ability to set CPU/Memory reservations
|
|
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: vsphere_client
|
|
short_description: Creates a virtual guest on vsphere.
|
|
description:
|
|
- Communicates with vsphere, creating a new virtual guest OS based on
|
|
the specifications you specify to the module.
|
|
version_added: "1.1"
|
|
options:
|
|
vcenter_hostname:
|
|
description:
|
|
- The hostname of the vcenter server the module will connect to, to create the guest.
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
user:
|
|
description:
|
|
- username of the user to connect to vcenter as.
|
|
required: true
|
|
default: null
|
|
password:
|
|
description:
|
|
- password of the user to connect to vcenter as.
|
|
required: true
|
|
default: null
|
|
resource_pool:
|
|
description:
|
|
- The name of the resource_pool to create the VM in.
|
|
required: false
|
|
default: None
|
|
cluster:
|
|
description:
|
|
- The name of the cluster to create the VM in. By default this is derived from the host you tell the module to build the guest on.
|
|
required: false
|
|
default: None
|
|
datacenter:
|
|
description:
|
|
- The name of the datacenter to create the VM in.
|
|
required: true
|
|
default: null
|
|
datastore:
|
|
description:
|
|
- The datastore to store the VMs config files in. (Hard-disk locations are specified separately.)
|
|
required: true
|
|
default: null
|
|
esxi_hostname:
|
|
description:
|
|
- The hostname of the esxi host you want the VM to be created on.
|
|
required: true
|
|
default: null
|
|
power_on:
|
|
description:
|
|
- Whether or not to power on the VM after creation.
|
|
required: false
|
|
default: no
|
|
choices: [yes, no]
|
|
vm_name:
|
|
description:
|
|
- The name you want to call the VM.
|
|
required: true
|
|
default: null
|
|
vm_memory_mb:
|
|
description:
|
|
- How much memory in MB to give the VM.
|
|
required: false
|
|
default: 1024
|
|
vm_num_cpus:
|
|
description:
|
|
- How many vCPUs to give the VM.
|
|
required: false
|
|
default: 1
|
|
vm_scsi:
|
|
description:
|
|
- The type of scsi controller to add to the VM.
|
|
required: false
|
|
default: "paravirtual"
|
|
choices: [paravirtual, lsi, lsi_sas, bus_logic]
|
|
vm_disk:
|
|
description:
|
|
- A key, value list of disks and their sizes and which datastore to keep it in.
|
|
required: false
|
|
default: null
|
|
vm_nic:
|
|
description:
|
|
- A key, value list of nics, their types and what network to put them on.
|
|
required: false
|
|
default: null
|
|
choices: [vmxnet3, vmxnet2, vmxnet, e1000, e1000e, pcnet32]
|
|
vm_notes:
|
|
description:
|
|
- Any notes that you want to show up in the VMs Annotations field.
|
|
required: false
|
|
default: null
|
|
vm_cdrom:
|
|
description:
|
|
- A path, including datastore, to an ISO you want the CDROM device on the VM to have.
|
|
required: false
|
|
default: null
|
|
vm_extra_config:
|
|
description:
|
|
- A key, value pair of any extra values you want set or changed in the vmx file of the VM. Useful to set advanced options on the VM.
|
|
required: false
|
|
default: null
|
|
guestosid:
|
|
description:
|
|
- "A vmware guest needs to have a specific OS identifier set on it
|
|
during creation. You can find your os guestosid at the following URL:
|
|
http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html"
|
|
required: true
|
|
default: null
|
|
# informational: requirements for nodes
|
|
requirements: [ pysphere ]
|
|
author: Romeo Theriault
|
|
'''
|
|
|
|
|
|
def power_state(vm, state, force):
|
|
|
|
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):
|
|
|
|
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):
|
|
|
|
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
|
|
}
|
|
|
|
proto_vm_disk = {
|
|
'disk1': {
|
|
'size_gb': int,
|
|
'type': basestring
|
|
}
|
|
}
|
|
|
|
proto_vm_nic = {
|
|
'nic1': {
|
|
'type': basestring,
|
|
'network': basestring,
|
|
'network_type': basestring
|
|
}
|
|
}
|
|
|
|
proto_esxi = {
|
|
'datastore': basestring,
|
|
'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_boot_state=dict(
|
|
required=False,
|
|
choices=[
|
|
'powered_on',
|
|
'powered_off',
|
|
],
|
|
default='powered_on'),
|
|
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={}),
|
|
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_boot_state',
|
|
'vm_nic',
|
|
'vm_hardware',
|
|
'esxi'
|
|
]
|
|
],
|
|
)
|
|
|
|
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
|
|
except ImportError, e:
|
|
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_boot_state = module.params['vm_boot_state']
|
|
vm_nic = module.params['vm_nic']
|
|
vm_hardware = module.params['vm_hardware']
|
|
vm_extra_config = module.params['vm_extra_config']
|
|
esxi = module.params['esxi']
|
|
|
|
# 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 not in ['absent', 'present']:
|
|
module.fail_json(
|
|
msg="No such VM %s. States [powered_on, powered_off, "
|
|
"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 == 'present':
|
|
|
|
# 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)
|
|
|
|
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
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
main()
|