diff --git libcloud/compute/drivers/vcloud.py libcloud/compute/drivers/vcloud.py index 10cdb9b..217b4d2 100644 --- libcloud/compute/drivers/vcloud.py +++ libcloud/compute/drivers/vcloud.py @@ -15,9 +15,11 @@ """ VMware vCloud driver. """ +import copy import sys import re import base64 +import os from libcloud.utils.py3 import httplib from libcloud.utils.py3 import urlparse from libcloud.utils.py3 import b @@ -31,7 +33,7 @@ from xml.etree.ElementTree import _ElementInterface from xml.parsers.expat import ExpatError from libcloud.common.base import XmlResponse, ConnectionUserAndKey -from libcloud.common.types import InvalidCredsError +from libcloud.common.types import InvalidCredsError, LibcloudError from libcloud.compute.providers import Provider from libcloud.compute.types import NodeState from libcloud.compute.base import Node, NodeDriver, NodeLocation @@ -47,6 +49,14 @@ DEFAULT_TASK_COMPLETION_TIMEOUT = 600 DEFAULT_API_VERSION = '0.8' +""" +Valid vCloud API v1.5 input values. +""" +VIRTUAL_CPU_VALS_1_5 = [i for i in range(1, 9)] +VIRTUAL_MEMORY_VALS_1_5 = [2 ** i for i in range(2, 19)] +FENCE_MODE_VALS_1_5 = ['bridged', 'isolated', 'natRouted'] +IP_MODE_VALS_1_5 = ['POOL', 'DHCP', 'MANUAL', 'NONE'] + def fixxpath(root, xpath): """ElementTree wants namespaces in its xpaths, so here we add them.""" @@ -67,6 +77,10 @@ class Vdc: self.name = name self.driver = driver + def __repr__(self): + return (('') + % (self.id, self.name, self.driver.name)) + class InstantiateVAppXML(object): @@ -326,6 +340,19 @@ class VCloudNodeDriver(NodeDriver): ] return self._vdcs + def _get_vdc(self, vdc_name): + vdc = None + if not vdc_name: + # Return the first organisation VDC found + vdc = self.vdcs[0] + else: + for v in self.vdcs: + if v.name == vdc_name: + vdc = v + if vdc is None: + raise ValueError('%s virtual data centre could not be found', vdc_name) + return vdc + @property def networks(self): networks = [] @@ -577,7 +604,7 @@ class VCloudNodeDriver(NodeDriver): @keyword ex_network: link to a "Network" e.g., "https://services.vcloudexpress.terremark.com/api/v0.8/network/7" @type ex_network: C{string} - @keyword ex_vdc: link to a "VDC" e.g., "https://services.vcloudexpress.terremark.com/api/v0.8/vdc/1" + @keyword ex_vdc: Name of organisation's virtual data center where vApp VMs will be deployed. @type ex_vdc: C{string} @keyword ex_cpus: number of virtual cpus (limit depends on provider) @@ -618,10 +645,10 @@ class VCloudNodeDriver(NodeDriver): group=kwargs.get('ex_group', None) ) + vdc = self._get_vdc(kwargs.get('ex_vdc', None)) # Instantiate VM and get identifier. res = self.connection.request( - '%s/action/instantiateVAppTemplate' - % kwargs.get('vdc', self.vdcs[0].id), + '%s/action/instantiateVAppTemplate' % vdc.id, data=instantiate_xml.tostring(), method='POST', headers={ @@ -712,7 +739,7 @@ class VCloud_1_5_Connection(VCloudConnection): # Get the URL of the Organization body = ET.XML(resp.read()) - org_name = body.get('org') + self.org_name = body.get('org') org_list_url = get_url_path( next((link for link in body.findall(fixxpath(body, 'Link')) if link.get('type') == 'application/vnd.vmware.vcloud.orgList+xml')).get('href') @@ -723,7 +750,7 @@ class VCloud_1_5_Connection(VCloudConnection): body = ET.XML(conn.getresponse().read()) self.driver.org = get_url_path( next((org for org in body.findall(fixxpath(body, 'Org')) - if org.get('name') == org_name)).get('href') + if org.get('name') == self.org_name)).get('href') ) def add_default_headers(self, headers): @@ -733,10 +760,12 @@ class VCloud_1_5_Connection(VCloudConnection): class Instantiate_1_5_VAppXML(object): - def __init__(self, name, template, network): + def __init__(self, name, template, network, vm_network=None, vm_fence=None): self.name = name self.template = template self.network = network + self.vm_network = vm_network + self.vm_fence = vm_fence self._build_xmltree() def tostring(self): @@ -773,10 +802,19 @@ class Instantiate_1_5_VAppXML(object): ) def _add_network_association(self, parent): - parent.set('networkName', self.network.get('name')) + if self.vm_network is None: + # Don't set a custom vApp VM network name + parent.set('networkName', self.network.get('name')) + else: + # Set a custom vApp VM network name + parent.set('networkName', self.vm_network) configuration = ET.SubElement(parent, 'Configuration') ET.SubElement(configuration, 'ParentNetwork', {'href': self.network.get('href')}) - configuration.append(self.network.find(fixxpath(self.network, 'Configuration/FenceMode'))) + if self.vm_fence is None: + fencemode = self.network.find(fixxpath(self.network, 'Configuration/FenceMode')).text + else: + fencemode = self.vm_fence + ET.SubElement(configuration, 'FenceMode').text = fencemode class VCloud_1_5_NodeDriver(VCloudNodeDriver): @@ -917,67 +955,109 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): @type image: L{NodeImage} or L{Node} Non-standard optional keyword arguments: - @keyword ex_network: link to a "Network" e.g., "https://services.vcloudexpress.terremark.com/api/network/7" + @keyword ex_network: Organisation's network name for attaching vApp VMs to. @type ex_network: C{string} - @keyword ex_vdc: link to a "VDC" e.g., "https://services.vcloudexpress.terremark.com/api/vdc/1" + @keyword ex_vdc: Name of organisation's virtual data center where vApp VMs will be deployed. @type ex_vdc: C{string} @keyword ex_vm_names: list of names to be used as a VM and computer name. The name must be max. 15 characters - long and follow the host name requirements + long and follow the host name requirements. @type ex_vm_names: C{list} of L{string} + @keyword ex_vm_cpu: number of virtual CPUs/cores to allocate for each vApp VM. + @type ex_vm_cpu: C{number} + + @keyword ex_vm_memory: amount of memory in MB to allocate for each vApp VM. + @type ex_vm_memory: C{number} + + @keyword ex_vm_script: full path to file containing guest customisation script for each vApp VM. + Useful for creating users & pushing out public SSH keys etc. + @type ex_vm_script: C{string} + + @keyword ex_vm_network: Override default vApp VM network name. Useful for when you've imported an OVF + originating from outside of the vCloud. + @type ex_vm_network: C{string} + + @keyword ex_vm_fence: Fence mode for connecting the vApp VM network (ex_vm_network) to the parent + organisation network (ex_network). + @type ex_vm_fence: C{string} + + @keyword ex_vm_ipmode: IP address allocation mode for all vApp VM network connections. + @type ex_vm_ipmode: C{string} + + @keyword ex_deploy: set to False if the node shouldn't be deployed (started) after creation + @type ex_deploy: C{bool} """ name = kwargs['name'] image = kwargs['image'] size = kwargs.get('size', None) ex_vm_names = kwargs.get('ex_vm_names') + ex_vm_cpu = kwargs.get('ex_vm_cpu') + ex_vm_memory = kwargs.get('ex_vm_memory') + ex_vm_script = kwargs.get('ex_vm_script') + ex_vm_fence = kwargs.get('ex_vm_fence', None) + ex_network = kwargs.get('ex_network', None) + ex_vm_network = kwargs.get('ex_vm_network', None) + ex_vm_ipmode = kwargs.get('ex_vm_ipmode', None) + ex_deploy = kwargs.get('ex_deploy', True) + ex_vdc = kwargs.get('ex_vdc', None) self._validate_vm_names(ex_vm_names) + self._validate_vm_cpu(ex_vm_cpu) + self._validate_vm_memory(ex_vm_memory) + self._validate_vm_fence(ex_vm_fence) + self._validate_vm_ipmode(ex_vm_ipmode) + ex_vm_script = self._validate_vm_script(ex_vm_script) # Some providers don't require a network link - network_href = kwargs.get('ex_network', None) - if network_href: + if ex_network: + network_href = self._get_network_href(ex_network) network_elem = self.connection.request(network_href).object else: network_elem = None - vdc = kwargs.get('ex_vdc', self.vdcs[0]) - if not vdc: - vdc = self.vdcs[0] + vdc = self._get_vdc(ex_vdc) if self._is_node(image): vapp_name, vapp_href = self._clone_node(name, image, vdc) else: - vapp_name, vapp_href = self._instantiate_node(name, image, network_elem, vdc) + vapp_name, vapp_href = self._instantiate_node(name, image, network_elem, + vdc, ex_vm_network, ex_vm_fence) self._change_vm_names(vapp_href, ex_vm_names) + self._change_vm_cpu(vapp_href, ex_vm_cpu) + self._change_vm_memory(vapp_href, ex_vm_memory) + self._change_vm_script(vapp_href, ex_vm_script) + self._change_vm_ipmode(vapp_href, ex_vm_ipmode) # Power on the VM. - # Retry 3 times: when instantiating large number of VMs at the same time some may fail on resource allocation - retry = 3 - while True: - try: - res = self.connection.request('%s/power/action/powerOn' % vapp_href, - method='POST') - self._wait_for_task_completion(res.object.get('href')) - break - except Exception: - if retry <= 0: - raise - retry -= 1 - time.sleep(10) + if ex_deploy: + # Retry 3 times: when instantiating large number of VMs at the same time some may fail on resource allocation + retry = 3 + while True: + try: + res = self.connection.request('%s/power/action/powerOn' % vapp_href, + method='POST') + self._wait_for_task_completion(res.object.get('href')) + break + except Exception: + if retry <= 0: + raise + retry -= 1 + time.sleep(10) res = self.connection.request(vapp_href) node = self._to_node(res.object) - return node - def _instantiate_node(self, name, image, network_elem, vdc): + def _instantiate_node(self, name, image, network_elem, vdc, vm_network, vm_fence): instantiate_xml = Instantiate_1_5_VAppXML( name=name, template=image.id, - network=network_elem + network=network_elem, + vm_network=vm_network, + vm_fence=vm_fence ) # Instantiate VM and get identifier. @@ -1057,6 +1137,58 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): return vapp_name, vapp_href + def ex_set_vm_cpu(self, vapp_or_vm_id, vm_cpu): + """ + Sets the number of virtual CPUs for the specified VM or VMs under the vApp. If the vapp_or_vm_id param + represents a link to an vApp all VMs that are attached to this vApp will be modified. + + Please ensure that hot-adding a virtual CPU is enabled for the powered on virtual machines. + Otherwise use this method on undeployed vApp. + + @keyword vapp_or_vm_id: vApp or VM ID that will be modified. If a vApp ID is used here all attached VMs + will be modified + @type vapp_or_vm_id: C{string} + + @keyword vm_cpu: number of virtual CPUs/cores to allocate for specified VMs + @type vm_cpu: C{number} + """ + self._validate_vm_cpu(vm_cpu) + self._change_vm_cpu(vapp_or_vm_id, vm_cpu) + + def ex_set_vm_memory(self, vapp_or_vm_id, vm_memory): + """ + Sets the virtual memory in MB to allocate for the specified VM or VMs under the vApp. + If the vapp_or_vm_id param represents a link to an vApp all VMs that are attached to + this vApp will be modified. + + Please ensure that hot-change of virtual memory is enabled for the powered on virtual machines. + Otherwise use this method on undeployed vApp. + + @keyword vapp_or_vm_id: vApp or VM ID that will be modified. If a vApp ID is used here all attached VMs + will be modified + @type vapp_or_vm_id: C{string} + + @keyword vm_memory: virtual memory in MB to allocate for the specified VM or VMs + @type vm_memory: C{number} + """ + self._validate_vm_memory(vm_memory) + self._change_vm_memory(vapp_or_vm_id, vm_memory) + + def ex_add_vm_disk(self, vapp_or_vm_id, vm_disk_size): + """ + Adds a virtual disk to the specified VM or VMs under the vApp. If the vapp_or_vm_id param + represents a link to an vApp all VMs that are attached to this vApp will be modified. + + @keyword vapp_or_vm_id: vApp or VM ID that will be modified. If a vApp ID is used here all attached VMs + will be modified + @type vapp_or_vm_id: C{string} + + @keyword vm_disk_size: the disk capacity in GB that will be added to the specified VM or VMs + @type vm_disk_size: C{number} + """ + self._validate_vm_disk_size(vm_disk_size) + self._add_vm_disk(vapp_or_vm_id, vm_disk_size) + @staticmethod def _validate_vm_names(names): if names is None: @@ -1068,13 +1200,64 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): if not hname_re.match(name): raise ValueError('The VM name "' + name + '" can not be used. "' + name + '" is not a valid computer name for the VM.') - def _change_vm_names(self, vapp_href, vm_names): - if vm_names is None: + @staticmethod + def _validate_vm_memory(vm_memory): + if vm_memory is None: return + elif vm_memory not in VIRTUAL_MEMORY_VALS_1_5: + raise ValueError('%s is not a valid vApp VM memory value' % vm_memory) - res = self.connection.request(vapp_href) - vms = res.object.findall(fixxpath(res.object, "Children/Vm")) + @staticmethod + def _validate_vm_cpu(vm_cpu): + if vm_cpu is None: + return + elif vm_cpu not in VIRTUAL_CPU_VALS_1_5: + raise ValueError('%s is not a valid vApp VM CPU value' % vm_cpu) + @staticmethod + def _validate_vm_disk_size(vm_disk): + if vm_disk is None: + return + elif int(vm_disk) < 0: + raise ValueError('%s is not a valid vApp VM disk space value', vm_disk) + + @staticmethod + def _validate_vm_script(vm_script): + if vm_script is None: + return + # Try to locate the script file + if not os.path.isabs(vm_script): + vm_script = os.path.expanduser(vm_script) + vm_script = os.path.abspath(vm_script) + if not os.path.isfile(vm_script): + raise LibcloudError("%s the VM script file does not exist" % vm_script) + try: + open(vm_script).read() + except: + raise + return vm_script + + @staticmethod + def _validate_vm_fence(vm_fence): + if vm_fence is None: + return + elif vm_fence not in FENCE_MODE_VALS_1_5: + raise ValueError('%s is not a valid fencing mode value' % vm_fence) + + @staticmethod + def _validate_vm_ipmode(vm_ipmode): + if vm_ipmode is None: + return + elif vm_ipmode == 'MANUAL': + raise NotImplementedError('MANUAL IP mode: The interface for supplying IPAddress does not exist yet') + elif vm_ipmode not in IP_MODE_VALS_1_5: + raise ValueError('%s is not a valid IP address allocation mode value' % vm_ipmode) + + def _change_vm_names(self, vapp_or_vm_id, vm_names): + if vm_names is None: + return + + vms = self._get_vm_elements(vapp_or_vm_id) for i, vm in enumerate(vms): if len(vm_names) <= i: return @@ -1106,5 +1289,212 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): ) self._wait_for_task_completion(res.object.get('href')) + def _change_vm_cpu(self, vapp_or_vm_id, vm_cpu): + if vm_cpu is None: + return + + vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: + # Get virtualHardwareSection/cpu section + res = self.connection.request('%s/virtualHardwareSection/cpu' % get_url_path(vm.get('href'))) + + # Update VirtualQuantity field + res.object.find( + '{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}VirtualQuantity' + ).text = str(vm_cpu) + res = self.connection.request('%s/virtualHardwareSection/cpu' % get_url_path(vm.get('href')), + data=ET.tostring(res.object), + method='PUT', + headers={'Content-Type': 'application/vnd.vmware.vcloud.rasdItem+xml'} + ) + self._wait_for_task_completion(res.object.get('href')) + + def _change_vm_memory(self, vapp_or_vm_id, vm_memory): + if vm_memory is None: + return + + vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: + # Get virtualHardwareSection/memory section + res = self.connection.request('%s/virtualHardwareSection/memory' % get_url_path(vm.get('href'))) + + # Update VirtualQuantity field + res.object.find( + '{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}VirtualQuantity' + ).text = str(vm_memory) + res = self.connection.request('%s/virtualHardwareSection/memory' % get_url_path(vm.get('href')), + data=ET.tostring(res.object), + method='PUT', + headers={'Content-Type': 'application/vnd.vmware.vcloud.rasdItem+xml'} + ) + self._wait_for_task_completion(res.object.get('href')) + + def _add_vm_disk(self, vapp_or_vm_id, vm_disk): + if vm_disk is None: + return + + rasd_ns = '{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}' + + vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: + # Get virtualHardwareSection/disks section + res = self.connection.request('%s/virtualHardwareSection/disks' % vm.get('href')) + + existing_ids = [] + new_disk = None + for item in res.object.findall(fixxpath(res.object, 'Item')): + # Clean Items from unnecessary stuff + for elem in item: + if elem.tag == '%sInstanceID' % rasd_ns: + existing_ids.append(int(elem.text)) + if elem.tag in ['%sAddressOnParent' % rasd_ns, '%sParent' % rasd_ns]: + item.remove(elem) + if item.find('%sHostResource' % rasd_ns) is not None: + new_disk = item + + new_disk = copy.deepcopy(new_disk) + disk_id = max(existing_ids) + 1 + new_disk.find('%sInstanceID' % rasd_ns).text = str(disk_id) + new_disk.find('%sElementName' % rasd_ns).text = 'Hard Disk ' + str(disk_id) + new_disk.find('%sHostResource' % rasd_ns).set(fixxpath(new_disk, 'capacity'), str(int(vm_disk) * 1024)) + res.object.append(new_disk) + + res = self.connection.request('%s/virtualHardwareSection/disks' % vm.get('href'), + data=ET.tostring(res.object), + method='PUT', + headers={'Content-Type': 'application/vnd.vmware.vcloud.rasditemslist+xml'} + ) + self._wait_for_task_completion(res.object.get('href')) + + def _change_vm_script(self, vapp_or_vm_id, vm_script): + if vm_script is None: + return + + vms = self._get_vm_elements(vapp_or_vm_id) + try: + script = open(vm_script).read() + except: + return + + # ElementTree escapes script characters automatically. Escape requirements: + # http://www.vmware.com/support/vcd/doc/rest-api-doc-1.5-html/types/GuestCustomizationSectionType.html + for vm in vms: + # Get GuestCustomizationSection + res = self.connection.request('%s/guestCustomizationSection' % get_url_path(vm.get('href'))) + + # Attempt to update any existing CustomizationScript element + try: + res.object.find(fixxpath(res.object, 'CustomizationScript')).text = script + except: + # CustomizationScript section does not exist, insert it just before ComputerName + for i, e in enumerate(res.object): + if e.tag == '{http://www.vmware.com/vcloud/v1.5}ComputerName': + break + e = ET.Element('{http://www.vmware.com/vcloud/v1.5}CustomizationScript') + e.text = script + res.object.insert(i, e) + + # Remove AdminPassword from customization section due to an API quirk + admin_pass = res.object.find(fixxpath(res.object, 'AdminPassword')) + if admin_pass is not None: + res.object.remove(admin_pass) + + # Update VM's GuestCustomizationSection + res = self.connection.request('%s/guestCustomizationSection' % get_url_path(vm.get('href')), + data=ET.tostring(res.object), + method='PUT', + headers={'Content-Type': + 'application/vnd.vmware.vcloud.guestCustomizationSection+xml' + } + ) + self._wait_for_task_completion(res.object.get('href')) + + def _change_vm_ipmode(self, vapp_or_vm_id, vm_ipmode): + if vm_ipmode is None: + return + + vms = self._get_vm_elements(vapp_or_vm_id) + + for vm in vms: + res = self.connection.request('%s/networkConnectionSection' % get_url_path(vm.get('href'))) + net_conns = res.object.findall(fixxpath(res.object, 'NetworkConnection')) + for c in net_conns: + c.find(fixxpath(c, 'IpAddressAllocationMode')).text = vm_ipmode + + res = self.connection.request('%s/networkConnectionSection' % get_url_path(vm.get('href')), + data=ET.tostring(res.object), + method='PUT', + headers={'Content-Type': + 'application/vnd.vmware.vcloud.networkConnectionSection+xml'} + ) + self._wait_for_task_completion(res.object.get('href')) + + def _get_network_href(self, network_name): + network_href = None + + # Find the organisation's network href + res = self.connection.request(self.org) + links = res.object.findall(fixxpath(res.object, 'Link')) + for l in links: + if l.attrib['type'] == 'application/vnd.vmware.vcloud.orgNetwork+xml'\ + and l.attrib['name'] == network_name: + network_href = l.attrib['href'] + + if network_href is None: + raise ValueError('%s is not a valid organisation network name' % network_name) + else: + return network_href + + def _get_vm_elements(self, vapp_or_vm_id): + res = self.connection.request(vapp_or_vm_id) + if res.object.tag.endswith('VApp'): + vms = res.object.findall(fixxpath(res.object, 'Children/Vm')) + elif res.object.tag.endswith('Vm'): + vms = [res.object] + else: + raise ValueError('Specified ID value is not a valid VApp or Vm identifier.') + return vms + def _is_node(self, node_or_image): return isinstance(node_or_image, Node) + + def _to_node(self, node_elm): + + # Parse VMs as extra field + vms = [] + for vm_elem in node_elm.findall(fixxpath(node_elm, 'Children/Vm')): + public_ips = [] + private_ips = [] + for connection in vm_elem.findall(fixxpath(vm_elem, 'NetworkConnectionSection/NetworkConnection')): + ip = connection.find(fixxpath(connection, "IpAddress")) + if ip is not None: + private_ips.append(ip.text) + external_ip = connection.find(fixxpath(connection, "ExternalIpAddress")) + if external_ip is not None: + public_ips.append(external_ip.text) + elif ip is not None: + public_ips.append(ip.text) + vm = { + 'id': vm_elem.get('href'), + 'name': vm_elem.get('name'), + 'state': self.NODE_STATE_MAP[vm_elem.get('status')], + 'public_ips': public_ips, + 'private_ips': private_ips + } + vms.append(vm) + + # Take the node IP addresses from all VMs + public_ips = [] + private_ips = [] + for vm in vms: + public_ips.extend(vm['public_ips']) + private_ips.extend(vm['private_ips']) + + node = Node(id=node_elm.get('href'), + name=node_elm.get('name'), + state=self.NODE_STATE_MAP[node_elm.get('status')], + public_ips=public_ips, + private_ips=private_ips, + driver=self.connection.driver, + extra={'vms': vms}) + return node diff --git test/compute/fixtures/vcloud_1_5/api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml test/compute/fixtures/vcloud_1_5/api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml index 7060d31..641fcad 100644 --- test/compute/fixtures/vcloud_1_5/api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml +++ test/compute/fixtures/vcloud_1_5/api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml @@ -1,5 +1,5 @@ - + diff --git test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a.xml test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a.xml index 0a6512e..a4de57e 100644 --- test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a.xml +++ test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a.xml @@ -111,7 +111,7 @@ false - + diff --git test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b.xml test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b.xml new file mode 100644 index 0000000..78192f8 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + Lease settings section + + 0 + 0 + + + VApp startup section + + + + + The list of logical networks + + + + + + The configuration parameters for logical networks + + + + + + + true + 65.41.64.1 + 255.255.252.0 + 65.41.42.113 + 65.41.42.114 + vm.myorg.com + + + 65.41.67.1 + 65.41.67.254 + + + + + bridged + false + + + false + 3600 + 7200 + + 65.41.64.2 + 65.41.67.0 + + + + true + drop + false + + true + Allow all outgoing traffic + allow + + true + + -1 + Any + -1 + Any + out + false + + + + true + ipTranslation + allowTraffic + + + automatic + ScrumVM_Master + 0 + + + + + false + + + + + true + + + + + + false + + + + + + + + + + + + + + + + + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + mgalet-test2 + vmx-07 + + + 00:50:56:01:00:99 + 0 + true + vCloud - Default + PCNet32 ethernet adapter + Network adapter 0 + 1 + PCNet32 + 10 + + + 0 + SCSI Controller + SCSI Controller 0 + 2 + lsilogic + 6 + + + 0 + Hard disk + Hard disk 1 + + 2000 + 2 + 17 + + + 0 + IDE Controller + IDE Controller 0 + 3 + 5 + + + 0 + false + CD/DVD Drive + CD/DVD Drive 1 + + 3002 + 3 + 15 + + + 0 + false + Floppy Drive + Floppy Drive 1 + + 8000 + 14 + + + hertz * 10^6 + Number of Virtual CPUs + 2 virtual CPU(s) + 4 + 0 + 3 + 2 + 0 + + + + byte * 2^20 + Memory Size + 4096 MB of memory + 5 + 0 + 4 + 4096 + 0 + + + + + + + + + + + + + + + + + Specifies the operating system installed + Red Hat Enterprise Linux 5 (64-bit) + + + + Specifies the available VM network connections + 0 + + 0 + 192.168.0.100 + 192.168.0.103 + true + 00:50:56:01:00:d9 + POOL + + + + Specifies Guest OS Customization Settings + true + false + dd75d1d3-5b7b-48f0-aff3-69622ab7e045 + false + false + true + true + sN#9QH9# + false + mgalet-test2 + + + + Specifies Runtime info + + + ScrumVM_Master + + + VMware ESXi + 5.0.0 + VMware, Inc. + en + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/api_vApp_vm_test.xml test/compute/fixtures/vcloud_1_5/api_vApp_vm_test.xml new file mode 100644 index 0000000..bc9260f --- /dev/null +++ test/compute/fixtures/vcloud_1_5/api_vApp_vm_test.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + ScrumVM_Master + vmx-07 + + + 00:50:56:01:00:d9 + 0 + true + vCloud - Default + PCNet32 ethernet adapter + Network adapter 0 + 1 + PCNet32 + 10 + + + 0 + SCSI Controller + SCSI Controller 0 + 2 + lsilogic + 6 + + + 0 + Hard disk + Hard disk 1 + + 2000 + 2 + 17 + + + 1 + Hard disk + Hard disk 2 + + 2001 + 2 + 17 + + + 2 + Hard disk + Hard disk 3 + + 2002 + 2 + 17 + + + 3 + Hard disk + Hard disk 4 + + 2003 + 2 + 17 + + + 0 + IDE Controller + IDE Controller 0 + 3 + 5 + + + 0 + false + CD/DVD Drive + CD/DVD Drive 1 + + 3002 + 3 + 15 + + + 0 + false + Floppy Drive + Floppy Drive 1 + + 8000 + 14 + + + hertz * 10^6 + Number of Virtual CPUs + 2 virtual CPU(s) + 4 + 0 + 3 + 2 + 0 + + + + byte * 2^20 + Memory Size + 4096 MB of memory + 5 + 0 + 4 + 4096 + 0 + + + + + + + + + + + + + + + + + Specifies the operating system installed + Red Hat Enterprise Linux 5 (64-bit) + + + + Specifies the available VM network connections + 0 + + 0 + 65.33.65.9 + true + 00:50:56:01:00:d9 + POOL + + + + + Specifies Guest OS Customization Settings + true + false + cbfe57d5-7362-482b-b313-e5b5bcff3309 + false + false + true + true + jW!4$$2i + false + #/bin/sh + /usr/local/sbin/ns-guest-customization.sh "$@" + ScrumVMMast-001 + + + + Specifies Runtime info + + + ScrumVM_Master + + + VMware ESXi + 5.0.0 + VMware, Inc. + en + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0.xml test/compute/fixtures/vcloud_1_5/api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0.xml index 1542ad5..e31f340 100644 --- test/compute/fixtures/vcloud_1_5/api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0.xml +++ test/compute/fixtures/vcloud_1_5/api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0.xml @@ -35,7 +35,8 @@ - + + diff --git test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_cpu.xml test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_cpu.xml new file mode 100644 index 0000000..1cc9c3c --- /dev/null +++ test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_cpu.xml @@ -0,0 +1,11 @@ + + hertz * 10^6 + Number of Virtual CPUs + 1 virtual CPU(s) + 4 + 0 + 3 + 1 + 0 + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_disks.xml test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_disks.xml new file mode 100644 index 0000000..69d9d28 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_disks.xml @@ -0,0 +1,54 @@ + + + + 0 + SCSI Controller + SCSI Controller 0 + 2 + lsilogic + 6 + + + 0 + Hard disk + Hard disk 1 + + 2000 + 2 + 17 + + + 1 + Hard disk + Hard disk 2 + + 2001 + 2 + 17 + + + 2 + Hard disk + Hard disk 3 + + 2002 + 2 + 17 + + + 3 + Hard disk + Hard disk 4 + + 2003 + 2 + 17 + + + 0 + IDE Controller + IDE Controller 0 + 3 + 5 + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_memory.xml test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_memory.xml new file mode 100644 index 0000000..1d23916 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/get_api_vApp_vm_test_virtualHardwareSection_memory.xml @@ -0,0 +1,11 @@ + + byte * 2^20 + Memory Size + 4096 MB of memory + 5 + 0 + 4 + 4096 + 0 + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_cpu.xml test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_cpu.xml new file mode 100644 index 0000000..ab55770 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_cpu.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_disks.xml test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_disks.xml new file mode 100644 index 0000000..ab55770 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_disks.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_memory.xml test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_memory.xml new file mode 100644 index 0000000..ab55770 --- /dev/null +++ test/compute/fixtures/vcloud_1_5/put_api_vApp_vm_test_virtualHardwareSection_memory.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git test/compute/test_vcloud.py test/compute/test_vcloud.py index ec73594..d8b8977 100644 --- test/compute/test_vcloud.py +++ test/compute/test_vcloud.py @@ -15,7 +15,6 @@ import sys import unittest - from xml.etree import ElementTree as ET from libcloud.utils.py3 import httplib, b @@ -31,6 +30,7 @@ from test.file_fixtures import ComputeFileFixtures from test.secrets import VCLOUD_PARAMS + class TerremarkTests(unittest.TestCase, TestCaseMixin): def setUp(self): @@ -81,6 +81,7 @@ class TerremarkTests(unittest.TestCase, TestCaseMixin): ret = self.driver.destroy_node(node) self.assertTrue(ret) + class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): def setUp(self): @@ -108,8 +109,8 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): name='testNode', image=image, size=size, - ex_vdc=Vdc('https://vm-vcloud/api/vdc/3d9ae28c-1de9-4307-8107-9356ff8ba6d0', 'MyVdc', self.driver), - ex_network='https://vm-vcloud/api/network/dca8b667-6c8f-4c3e-be57-7a9425dba4f4', + ex_vdc='MyVdc', + ex_network='vCloud - Default', cpus=2, ) self.assertTrue(isinstance(node, Node)) @@ -130,7 +131,27 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): self.assertEqual(node.name, 'testNode') self.assertEqual(node.state, NodeState.RUNNING) self.assertEqual(node.public_ips, ['65.41.67.2']) - self.assertEqual(node.private_ips, []) + self.assertEqual(node.private_ips, ['65.41.67.2']) + self.assertEqual(node.extra, {'vms': [{ + 'id': 'https://vm-vcloud/api/vApp/vm-dd75d1d3-5b7b-48f0-aff3-69622ab7e045', + 'name': 'testVm', + 'state': NodeState.RUNNING, + 'public_ips': ['65.41.67.2'], + 'private_ips': ['65.41.67.2'], + }]}) + node = ret[1] + self.assertEqual(node.id, 'https://vm-vcloud/api/vApp/vapp-8c57a5b6-e61b-48ca-8a78-3b70ee65ef6b') + self.assertEqual(node.name, 'testNode2') + self.assertEqual(node.state, NodeState.RUNNING) + self.assertEqual(node.public_ips, ['192.168.0.103']) + self.assertEqual(node.private_ips, ['192.168.0.100']) + self.assertEqual(node.extra, {'vms': [{ + 'id': 'https://vm-vcloud/api/vApp/vm-dd75d1d3-5b7b-48f0-aff3-69622ab7e046', + 'name': 'testVm2', + 'state': NodeState.RUNNING, + 'public_ips': ['192.168.0.103'], + 'private_ips': ['192.168.0.100'], + }]}) def test_reboot_node(self): node = self.driver.list_nodes()[0] @@ -176,6 +197,27 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): node = self.driver.ex_find_node('testNonExisting', self.driver.vdcs[0]) self.assertEqual(node, None) + def test_ex_add_vm_disk__with_invalid_values(self): + self.assertRaises(ValueError, self.driver.ex_add_vm_disk, 'dummy', 'invalid value') + self.assertRaises(ValueError, self.driver.ex_add_vm_disk, 'dummy', '-1') + + def test_ex_add_vm_disk(self): + self.driver.ex_add_vm_disk('https://test/api/vApp/vm-test', '20') + + def test_ex_set_vm_cpu__with_invalid_values(self): + self.assertRaises(ValueError, self.driver.ex_set_vm_cpu, 'dummy', 50) + self.assertRaises(ValueError, self.driver.ex_set_vm_cpu, 'dummy', -1) + + def test_ex_set_vm_cpu(self): + self.driver.ex_set_vm_cpu('https://test/api/vApp/vm-test', 4) + + def test_ex_set_vm_memory__with_invalid_values(self): + self.assertRaises(ValueError, self.driver.ex_set_vm_memory, 'dummy', 777) + self.assertRaises(ValueError, self.driver.ex_set_vm_memory, 'dummy', -1024) + + def test_ex_set_vm_memory(self): + self.driver.ex_set_vm_memory('https://test/api/vApp/vm-test', 1024) + class TerremarkMockHttp(MockHttp): @@ -290,6 +332,10 @@ class VCloud_1_5_MockHttp(MockHttp): status = httplib.ACCEPTED return status, body, headers, httplib.responses[status] + def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b(self, method, url, body, headers): + body = self.fixtures.load('api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b.xml') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + def _api_vApp_vm_dd75d1d3_5b7b_48f0_aff3_69622ab7e045(self, method, url, body, headers): body = self.fixtures.load('put_api_vApp_vm_dd75d1d3_5b7b_48f0_aff3_69622ab7e045_guestCustomizationSection.xml') return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] @@ -349,5 +395,36 @@ class VCloud_1_5_MockHttp(MockHttp): def _api_vApp_vapp_access_to_resource_forbidden(self, method, url, body, headers): raise Exception(ET.fromstring(self.fixtures.load('api_vApp_vapp_access_to_resource_forbidden.xml'))) + def _api_vApp_vm_test(self, method, url, body, headers): + body = self.fixtures.load('api_vApp_vm_test.xml') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + + def _api_vApp_vm_test_virtualHardwareSection_disks(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('get_api_vApp_vm_test_virtualHardwareSection_disks.xml') + status = httplib.OK + else: + body = self.fixtures.load('put_api_vApp_vm_test_virtualHardwareSection_disks.xml') + status = httplib.ACCEPTED + return status, body, headers, httplib.responses[status] + + def _api_vApp_vm_test_virtualHardwareSection_cpu(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('get_api_vApp_vm_test_virtualHardwareSection_cpu.xml') + status = httplib.OK + else: + body = self.fixtures.load('put_api_vApp_vm_test_virtualHardwareSection_cpu.xml') + status = httplib.ACCEPTED + return status, body, headers, httplib.responses[status] + + def _api_vApp_vm_test_virtualHardwareSection_memory(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('get_api_vApp_vm_test_virtualHardwareSection_memory.xml') + status = httplib.OK + else: + body = self.fixtures.load('put_api_vApp_vm_test_virtualHardwareSection_memory.xml') + status = httplib.ACCEPTED + return status, body, headers, httplib.responses[status] + if __name__ == '__main__': sys.exit(unittest.main())