This is an automated email from the ASF dual-hosted git repository. tomaz pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/libcloud.git
The following commit(s) were added to refs/heads/trunk by this push: new 809331c54 Use a more secure hostname validation regex without expensive backtracking in the vcloud driver. Also reformat code with black. 809331c54 is described below commit 809331c5445bbbd050950aae285049a38556726b Author: Tomaz Muraus <to...@tomaz.me> AuthorDate: Sun Jun 16 11:56:14 2024 +0200 Use a more secure hostname validation regex without expensive backtracking in the vcloud driver. Also reformat code with black. --- libcloud/compute/drivers/vcloud.py | 133 +++++++++++++++++++++++++++++++++-- libcloud/test/compute/test_vcloud.py | 65 +++++++++++++++++ 2 files changed, 194 insertions(+), 4 deletions(-) diff --git a/libcloud/compute/drivers/vcloud.py b/libcloud/compute/drivers/vcloud.py index ad455cbe6..f65f0f681 100644 --- a/libcloud/compute/drivers/vcloud.py +++ b/libcloud/compute/drivers/vcloud.py @@ -52,11 +52,15 @@ VIRTUAL_CPU_VALS_1_5 = [i for i in range(1, 9)] FENCE_MODE_VALS_1_5 = ["bridged", "isolated", "natRouted"] IP_MODE_VALS_1_5 = ["POOL", "DHCP", "MANUAL", "NONE"] +# Regular expression for validating node / VM name +HOSTNAME_RE = re.compile(r"^(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE) + def fixxpath(root, xpath): """ElementTree wants namespaces in its xpaths, so here we add them.""" namespace, root_tag = root.tag[1:].split("}", 1) fixed_xpath = "/".join(["{{{}}}{}".format(namespace, e) for e in xpath.split("/")]) + return fixed_xpath @@ -126,6 +130,7 @@ class ControlAccess: def __init__(self, node, everyone_access_level, subjects=None): self.node = node self.everyone_access_level = everyone_access_level + if not subjects: subjects = [] self.subjects = subjects @@ -238,6 +243,7 @@ class Lease: calculate :rtype: ``datetime.datetime`` or ``None`` """ + if self.deployment_lease is not None and self.deployment_lease_expiration is not None: return self.deployment_lease_expiration - datetime.timedelta( seconds=self.deployment_lease @@ -409,6 +415,7 @@ class InstantiateVAppXML: }, ) elm.text = id + return elm def _add_resource_type(self, parent, type): @@ -421,6 +428,7 @@ class InstantiateVAppXML: }, ) elm.text = type + return elm def _add_virtual_quantity(self, parent, amount): @@ -433,6 +441,7 @@ class InstantiateVAppXML: }, ) elm.text = amount + return elm def _add_network_association(self, parent): @@ -460,6 +469,7 @@ class VCloudConnection(ConnectionUserAndKey): def request(self, *args, **kwargs): self._get_auth_token() + return super().request(*args, **kwargs) def check_org(self): @@ -468,6 +478,7 @@ class VCloudConnection(ConnectionUserAndKey): def _get_auth_headers(self): """Some providers need different headers than others""" + return { "Authorization": "Basic %s" % base64.b64encode(b("{}:{}".format(self.user_id, self.key))).decode("utf-8"), @@ -495,6 +506,7 @@ class VCloudConnection(ConnectionUserAndKey): def add_default_headers(self, headers): headers["Cookie"] = self.token headers["Accept"] = "application/*+xml" + return headers @@ -543,6 +555,7 @@ class VCloudNodeDriver(NodeDriver): raise NotImplementedError( "No VCloudNodeDriver found for API version %s" % (api_version) ) + return super().__new__(cls) @property @@ -553,6 +566,7 @@ class VCloudNodeDriver(NodeDriver): :return: list of vDC objects :rtype: ``list`` of :class:`Vdc` """ + if not self._vdcs: self.connection.check_org() # make sure the org is set. res = self.connection.request(self.org) @@ -561,6 +575,7 @@ class VCloudNodeDriver(NodeDriver): for i in res.object.findall(fixxpath(res.object, "Link")) if i.get("type") == "application/vnd.vmware.vcloud.vdc+xml" ] + return self._vdcs def _to_vdc(self, vdc_elm): @@ -568,6 +583,7 @@ class VCloudNodeDriver(NodeDriver): def _get_vdc(self, vdc_name): vdc = None + if not vdc_name: # Return the first organisation VDC found vdc = self.vdcs[0] @@ -575,13 +591,16 @@ class VCloudNodeDriver(NodeDriver): for v in self.vdcs: if v.name == vdc_name or v.id == 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 = [] + for vdc in self.vdcs: res = self.connection.request(get_url_path(vdc.id)).object networks.extend( @@ -594,6 +613,7 @@ class VCloudNodeDriver(NodeDriver): image = NodeImage( id=image.get("href"), name=image.get("name"), driver=self.connection.driver ) + return image def _to_node(self, elm): @@ -610,6 +630,7 @@ class VCloudNodeDriver(NodeDriver): fixxpath(elm, "NetworkConnection"), ) ) + if not connections: connections = elm.findall( fixxpath(elm, "Children/Vm/NetworkConnectionSection/NetworkConnection") @@ -617,6 +638,7 @@ class VCloudNodeDriver(NodeDriver): for connection in connections: ips = [ip.text for ip in connection.findall(fixxpath(elm, "IpAddress"))] + if connection.get("Network") == "Internal": private_ips.extend(ips) else: @@ -647,18 +669,22 @@ class VCloudNodeDriver(NodeDriver): start_time = time.time() res = self.connection.request(get_url_path(task_href)) status = res.object.get("status") + while status != "success": if status == "error": # Get error reason from the response body error_elem = res.object.find(fixxpath(res.object, "Error")) error_msg = "Unknown error" + if error_elem is not None: error_msg = error_elem.get("message") raise Exception( "Error status returned by task {}.: {}".format(task_href, error_msg) ) + if status == "canceled": raise Exception("Canceled status returned by task %s." % task_href) + if time.time() - start_time >= timeout: raise Exception( "Timeout ({} sec) while waiting for task {}.".format(timeout, task_href) @@ -689,12 +715,14 @@ class VCloudNodeDriver(NodeDriver): pass res = self.connection.request(node_path, method="DELETE") + return res.status == httplib.ACCEPTED def reboot_node(self, node): res = self.connection.request( "%s/power/action/reset" % get_url_path(node.id), method="POST" ) + return res.status in [httplib.ACCEPTED, httplib.NO_CONTENT] def list_nodes(self): @@ -711,11 +739,14 @@ class VCloudNodeDriver(NodeDriver): :rtype: ``list`` of :class:`Node` """ + if not vdcs: vdcs = self.vdcs + if not isinstance(vdcs, (list, tuple)): vdcs = [vdcs] nodes = [] + for vdc in vdcs: res = self.connection.request(get_url_path(vdc.id)) elms = res.object.findall(fixxpath(res.object, "ResourceEntities/ResourceEntity")) @@ -736,6 +767,7 @@ class VCloudNodeDriver(NodeDriver): # The vApp was probably removed since the previous vDC # query, ignore # pylint: disable=no-member + if not ( e.args[0].tag.endswith("Error") and e.args[0].get("minorErrorCode") == "ACCESS_TO_RESOURCE_IS_FORBIDDEN" @@ -754,10 +786,12 @@ class VCloudNodeDriver(NodeDriver): price=None, driver=self.connection.driver, ) + return ns def list_sizes(self, location=None): sizes = [self._to_size(i) for i in VIRTUAL_MEMORY_VALS] + return sizes def _get_catalogitems_hrefs(self, catalog): @@ -787,6 +821,7 @@ class VCloudNodeDriver(NodeDriver): def list_images(self, location=None): images = [] + for vdc in self.vdcs: res = self.connection.request(get_url_path(vdc.id)).object res_ents = res.findall(fixxpath(res, "ResourceEntities/ResourceEntity")) @@ -819,12 +854,15 @@ class VCloudNodeDriver(NodeDriver): seen = {} result = [] + for item in seq: marker = idfun(item) + if marker in seen: continue seen[marker] = 1 result.append(item) + return result def create_node( @@ -916,6 +954,7 @@ class HostingComConnection(VCloudConnection): def _get_auth_headers(self): """hosting.com doesn't follow the standard vCloud authentication API""" + return { "Authentication": base64.b64encode(b("{}:{}".format(self.user_id, self.key))), "Content-Length": "0", @@ -952,6 +991,7 @@ class TerremarkDriver(VCloudNodeDriver): class VCloud_1_5_Connection(VCloudConnection): def _get_auth_headers(self): """Compatibility for using v1.5 API under vCloud Director 5.1""" + return { "Authorization": "Basic %s" % base64.b64encode(b("{}:{}".format(self.user_id, self.key))).decode("utf-8"), @@ -1007,6 +1047,7 @@ class VCloud_1_5_Connection(VCloudConnection): def add_default_headers(self, headers): headers["Accept"] = "application/*+xml;version=1.5" headers["x-vcloud-authorization"] = self.token + return headers @@ -1015,11 +1056,13 @@ class VCloud_5_5_Connection(VCloud_1_5_Connection): """Compatibility for using v5.5 of the API""" auth_headers = super()._get_auth_headers() auth_headers["Accept"] = "application/*+xml;version=5.5" + return auth_headers def add_default_headers(self, headers): headers["Accept"] = "application/*+xml;version=5.5" headers["x-vcloud-authorization"] = self.token + return headers @@ -1134,21 +1177,27 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :return: node instance or None if not found :rtype: :class:`Node` or ``None`` """ + if not vdcs: vdcs = self.vdcs + if not getattr(vdcs, "__iter__", False): vdcs = [vdcs] + for vdc in vdcs: res = self.connection.request(get_url_path(vdc.id)) xpath = fixxpath(res.object, "ResourceEntities/ResourceEntity") entity_elems = res.object.findall(xpath) + for entity_elem in entity_elems: if ( entity_elem.get("type") == "application/vnd.vmware.vcloud.vApp+xml" and entity_elem.get("name") == node_name ): path = entity_elem.get("href") + return self._ex_get_node(path) + return None def ex_find_vm_nodes(self, vm_name, max_results=50): @@ -1170,6 +1219,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): page=1, page_size=max_results, ) + return [self._ex_get_node(vm["container"]) for vm in vms] def destroy_node(self, node, shutdown=True): @@ -1181,14 +1231,17 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): pass res = self.connection.request(get_url_path(node.id), method="DELETE") + return res.status == httplib.ACCEPTED def reboot_node(self, node): res = self.connection.request( "%s/power/action/reset" % get_url_path(node.id), method="POST" ) + if res.status in [httplib.ACCEPTED, httplib.NO_CONTENT]: self._wait_for_task_completion(res.object.get("href")) + return True else: return False @@ -1207,14 +1260,17 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: :class:`Node` """ + if ex_force_customization: vms = self._get_vm_elements(node.id) + for vm in vms: self._ex_deploy_node_or_vm(vm.get("href"), ex_force_customization=True) else: self._ex_deploy_node_or_vm(node.id) res = self.connection.request(get_url_path(node.id)) + return self._to_node(res.object) def _ex_deploy_node_or_vm(self, vapp_or_vm_path, ex_force_customization=False): @@ -1272,6 +1328,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): undeploy("powerOff") res = self.connection.request(get_url_path(node.id)) + return self._to_node(res.object) def ex_power_off_node(self, node): @@ -1284,6 +1341,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: :class:`Node` """ + return self._perform_power_operation(node, "powerOff") def ex_power_on_node(self, node): @@ -1296,6 +1354,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: :class:`Node` """ + return self._perform_power_operation(node, "powerOn") def ex_shutdown_node(self, node): @@ -1308,6 +1367,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: :class:`Node` """ + return self._perform_power_operation(node, "shutdown") def ex_suspend_node(self, node): @@ -1320,6 +1380,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: :class:`Node` """ + return self._perform_power_operation(node, "suspend") def _perform_power_operation(self, node, operation): @@ -1328,6 +1389,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): ) self._wait_for_task_completion(res.object.get("href")) res = self.connection.request(get_url_path(node.id)) + return self._to_node(res.object) def ex_get_control_access(self, node): @@ -1342,6 +1404,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): res = self.connection.request("%s/controlAccess" % get_url_path(node.id)) everyone_access_level = None is_shared_elem = res.object.find(fixxpath(res.object, "IsSharedToEveryone")) + if is_shared_elem is not None and is_shared_elem.text == "true": everyone_access_level = res.object.find( fixxpath(res.object, "EveryoneAccessLevel") @@ -1350,9 +1413,11 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): # Parse all subjects subjects = [] xpath = fixxpath(res.object, "AccessSettings/AccessSetting") + for elem in res.object.findall(xpath): access_level = elem.find(fixxpath(res.object, "AccessLevel")).text subject_elem = elem.find(fixxpath(res.object, "Subject")) + if subject_elem.get("type") == "application/vnd.vmware.admin.group+xml": subj_type = "group" else: @@ -1385,6 +1450,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): """ xml = ET.Element("ControlAccessParams", {"xmlns": "http://www.vmware.com/vcloud/v1.5"}) shared_to_everyone = ET.SubElement(xml, "IsSharedToEveryone") + if control_access.everyone_access_level: shared_to_everyone.text = "true" everyone_access_level = ET.SubElement(xml, "EveryoneAccessLevel") @@ -1393,14 +1459,18 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): shared_to_everyone.text = "false" # Set subjects + if control_access.subjects: access_settings_elem = ET.SubElement(xml, "AccessSettings") + for subject in control_access.subjects: setting = ET.SubElement(access_settings_elem, "AccessSetting") + if subject.id: href = subject.id else: res = self.ex_query(type=subject.type, filter="name==" + subject.name) + if not res: raise LibcloudError( 'Specified subject "{} {}" not found '.format(subject.type, subject.name) @@ -1507,12 +1577,15 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): "pageSize": page_size, "page": page, } + if sort_asc: params["sortAsc"] = sort_asc + if sort_desc: params["sortDesc"] = sort_desc url = "/api/query?" + urlencode(params) + if filter: if not filter.startswith("("): filter = "(" + filter + ")" @@ -1520,11 +1593,13 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): results = [] res = self.connection.request(url) + for elem in res.object: if not elem.tag.endswith("Link"): result = elem.attrib result["type"] = elem.tag.split("}")[1] results.append(result) + return results def create_node(self, **kwargs): @@ -1639,6 +1714,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): ex_vm_script = self._validate_vm_script(ex_vm_script) # Some providers don't require a network link + if ex_network: network_href = self._get_network_href(ex_network) network_elem = self.connection.request(get_url_path(network_href)).object @@ -1671,15 +1747,18 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): self.ex_change_vm_admin_password(vapp_href, ex_admin_password) # Power on the VM. + if ex_deploy: res = self.connection.request(get_url_path(vapp_href)) node = self._to_node(res.object) # Retry 3 times: when instantiating large number of VMs at the same # time some may fail on resource allocation retry = 3 + while True: try: self.ex_deploy_node(node, ex_force_customization) + break except Exception: if retry <= 0: @@ -1689,6 +1768,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): res = self.connection.request(get_url_path(vapp_href)) node = self._to_node(res.object) + return node def _instantiate_node( @@ -1726,6 +1806,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): task_href = res.object.find(fixxpath(res.object, "Tasks/Task")).get("href") self._wait_for_task_completion(task_href, instantiate_timeout) + return vapp_name, vapp_href def _clone_node(self, name, sourceNode, vdc, clone_timeout): @@ -1760,6 +1841,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): vms = res.object.findall(fixxpath(res.object, "Children/Vm")) # Fix the networking for VMs + for i, vm in enumerate(vms): # Remove network network_xml = ET.Element( @@ -1873,22 +1955,23 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): def _validate_vm_names(names): if names is None: return - hname_re = re.compile( - r"^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9]*)[\-])*([A-Za-z]|[A-Za-z][A-Za-z0-9]*[A-Za-z0-9])$" - ) # NOQA + for name in names: if len(name) > 15: raise ValueError( 'The VM name "' + name + '" is too long for the computer ' "name (max 15 chars allowed)." ) - if not hname_re.match(name): + + if not HOSTNAME_RE.match(name): raise ValueError( 'The VM name "' + name + '" can not be ' 'used. "' + name + '" is not a valid ' "computer name for the VM." ) + return True + @staticmethod def _validate_vm_memory(vm_memory): if vm_memory is None: @@ -1915,15 +1998,18 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): 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 Exception: raise + return vm_script @staticmethod @@ -1949,6 +2035,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): return vms = self._get_vm_elements(vapp_or_vm_id) + for i, vm in enumerate(vms): if len(vm_names) <= i: return @@ -1993,6 +2080,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): return vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: # Get virtualHardwareSection/cpu section res = self.connection.request( @@ -2020,6 +2108,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): return vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: # Get virtualHardwareSection/memory section res = self.connection.request( @@ -2052,6 +2141,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): ) vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: # Get virtualHardwareSection/disks section res = self.connection.request( @@ -2060,16 +2150,20 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): 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 @@ -2110,6 +2204,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): # 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( @@ -2122,6 +2217,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): except Exception: # 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 @@ -2156,6 +2252,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): "%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 @@ -2193,6 +2290,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): admin_pass = guest_customization_section.find( fixxpath(guest_customization_section, "AdminPassword") ) + if admin_pass is not None and ( admin_pass_enabled is None or admin_pass_enabled.text != "true" @@ -2207,13 +2305,16 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): except Exception: # "section" section does not exist, insert it just # before "prev_section" + for i, e in enumerate(res.object): tag = "{http://www.vmware.com/vcloud/v1.5}%s" % prev_section + if e.tag == tag: break e = ET.Element("{http://www.vmware.com/vcloud/v1.5}%s" % section) e.text = text res.object.insert(i, e) + return res def ex_change_vm_admin_password(self, vapp_or_vm_id, ex_admin_password): @@ -2232,10 +2333,12 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): :rtype: ``None`` """ + if ex_admin_password is None: return vms = self._get_vm_elements(vapp_or_vm_id) + for vm in vms: # Get GuestCustomizationSection res = self.connection.request( @@ -2252,6 +2355,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): # it might have AdminAutoLogonCount==1 when requesting it for # the first time. auto_logon = res.object.find(fixxpath(res.object, "AdminAutoLogonEnabled")) + if auto_logon is not None and auto_logon.text == "false": self._update_or_insert_section( res, "AdminAutoLogonCount", "ResetPasswordRequired", "0" @@ -2283,6 +2387,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): # Find the organisation's network href res = self.connection.request(self.org) links = res.object.findall(fixxpath(res.object, "Link")) + for link in links: if ( link.attrib["type"] == "application/vnd.vmware.vcloud.orgNetwork+xml" @@ -2309,16 +2414,19 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): get_url_path(node_id), headers={"Content-Type": "application/vnd.vmware.vcloud.vApp+xml"}, ) + return self._to_node(res.object) def _get_vm_elements(self, vapp_or_vm_id): res = self.connection.request(get_url_path(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): @@ -2326,10 +2434,12 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): def _to_node(self, node_elm): # Parse snapshots and VMs as extra + if node_elm.find(fixxpath(node_elm, "SnapshotSection")) is None: snapshots = None else: snapshots = [] + for snapshot_elem in node_elm.findall(fixxpath(node_elm, "SnapshotSection/Snapshot")): snapshots.append( { @@ -2340,16 +2450,20 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): ) vms = [] + for vm_elem in node_elm.findall(fixxpath(node_elm, "Children/Vm")): public_ips = [] private_ips = [] xpath = fixxpath(vm_elem, "NetworkConnectionSection/NetworkConnection") + for connection in vm_elem.findall(xpath): 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: @@ -2357,6 +2471,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): xpath = "{http://schemas.dmtf.org/ovf/envelope/1}" "OperatingSystemSection" os_type_elem = vm_elem.find(xpath) + if os_type_elem is not None: os_type = os_type_elem.get("{http://www.vmware.com/schema/ovf}osType") else: @@ -2374,6 +2489,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): # 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"]) @@ -2389,12 +2505,14 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): extra = {"vdc": vdc.name, "vms": vms} description = node_elm.find(fixxpath(node_elm, "Description")) + if description is not None: extra["description"] = description.text else: extra["description"] = "" lease_settings = node_elm.find(fixxpath(node_elm, "LeaseSettingsSection")) + if lease_settings is not None: extra["lease_settings"] = Lease.to_lease(lease_settings) else: @@ -2412,6 +2530,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): driver=self.connection.driver, extra=extra, ) + return node def _to_vdc(self, vdc_elm): @@ -2421,6 +2540,7 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): limit = int(capacity_elm.findtext(fixxpath(capacity_elm, "Limit"))) used = int(capacity_elm.findtext(fixxpath(capacity_elm, "Used"))) units = capacity_elm.findtext(fixxpath(capacity_elm, "Units")) + return Capacity(limit, used, units) cpu = get_capacity_values(vdc_elm.find(fixxpath(vdc_elm, "ComputeCapacity/Cpu"))) @@ -2481,6 +2601,7 @@ class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver): ET.SubElement(snapshot_xml, "Description").text = "Description" content_type = "application/vnd.vmware.vcloud.createSnapshotParams+xml" headers = {"Content-Type": content_type} + return self._perform_snapshot_operation(node, "createSnapshot", snapshot_xml, headers) def ex_remove_snapshots(self, node): @@ -2492,6 +2613,7 @@ class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver): :rtype: :class:`Node` """ + return self._perform_snapshot_operation(node, "removeAllSnapshots", None, None) def ex_revert_to_snapshot(self, node): @@ -2503,6 +2625,7 @@ class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver): :rtype: :class:`Node` """ + return self._perform_snapshot_operation(node, "revertToCurrentSnapshot", None, None) def _perform_snapshot_operation(self, node, operation, xml_data, headers): @@ -2514,6 +2637,7 @@ class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver): ) self._wait_for_task_completion(res.object.get("href")) res = self.connection.request(get_url_path(node.id)) + return self._to_node(res.object) def ex_acquire_mks_ticket(self, vapp_or_vm_id, vm_num=0): @@ -2551,6 +2675,7 @@ class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver): "ticket": res.object.find(fixxpath(res.object, "Ticket")).text, "port": res.object.find(fixxpath(res.object, "Port")).text, } + return output except Exception: return None diff --git a/libcloud/test/compute/test_vcloud.py b/libcloud/test/compute/test_vcloud.py index 9053074ca..34af00584 100644 --- a/libcloud/test/compute/test_vcloud.py +++ b/libcloud/test/compute/test_vcloud.py @@ -271,6 +271,10 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): self.assertRaises(ValueError, self.driver._validate_vm_names, ["inv-alid.host"]) self.assertRaises(ValueError, self.driver._validate_vm_names, ["hostnametoooolong"]) self.assertRaises(ValueError, self.driver._validate_vm_names, ["host$name"]) + self.assertRaises(ValueError, self.driver._validate_vm_names, ["hostname-"]) + self.assertRaises(ValueError, self.driver._validate_vm_names, ["hostname."]) + self.assertRaises(ValueError, self.driver._validate_vm_names, [".hostname"]) + self.assertRaises(ValueError, self.driver._validate_vm_names, ["-hostname"]) def test_change_vm_names(self): self.driver._change_vm_names( @@ -611,6 +615,7 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): admin_pass_element = guest_customization_section.find( fixxpath(guest_customization_section, "AdminPassword") ) + if pass_exists: self.assertIsNotNone(admin_pass_element) else: @@ -1005,38 +1010,47 @@ class TerremarkMockHttp(MockHttp): def _api_v0_8_login(self, method, url, body, headers): headers["set-cookie"] = "vcloud-token=testtoken" body = self.fixtures.load("api_v0_8_login.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_org_240(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_org_240.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_vdc_224(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vdc_224.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_vdc_224_catalog(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vdc_224_catalog.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_catalogItem_5(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_catalogItem_5.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_vdc_224_action_instantiateVAppTemplate(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vdc_224_action_instantiateVAppTemplate.xml") + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) def _api_v0_8_vapp_14031_action_deploy(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vapp_14031_action_deploy.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_task_10496(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_task_10496.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_vapp_14031_power_action_powerOn(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vapp_14031_power_action_powerOn.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_vapp_14031(self, method, url, body, headers): @@ -1044,18 +1058,22 @@ class TerremarkMockHttp(MockHttp): body = self.fixtures.load("api_v0_8_vapp_14031_get.xml") elif method == "DELETE": body = "" + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_vapp_14031_power_action_reset(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vapp_14031_power_action_reset.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_vapp_14031_power_action_poweroff(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_vapp_14031_power_action_poweroff.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) def _api_v0_8_task_11001(self, method, url, body, headers): body = self.fixtures.load("api_v0_8_task_11001.xml") + return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED]) @@ -1093,26 +1111,32 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): def _api_sessions(self, method, url, body, headers): headers["x-vcloud-authorization"] = "testtoken" body = self.fixtures.load("api_sessions.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_org(self, method, url, body, headers): body = self.fixtures.load("api_org.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a(self, method, url, body, headers): body = self.fixtures.load("api_org_96726c78_4ae3_402f_b08b_7a78c6903d2a.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_network_dca8b667_6c8f_4c3e_be57_7a9425dba4f4(self, method, url, body, headers): body = self.fixtures.load("api_network_dca8b667_6c8f_4c3e_be57_7a9425dba4f4.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0(self, method, url, body, headers): body = self.fixtures.load("api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vdc_brokenVdc(self, method, url, body, headers): body = self.fixtures.load("api_vdc_brokenVdc.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_errorRaiser(self, method, url, body, headers): @@ -1125,6 +1149,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load( "api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0_action_instantiateVAppTemplate.xml" ) + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_power_action_powerOn( @@ -1141,36 +1166,43 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load( "api_vdc_3d9ae28c_1de9_4307_8107_9356ff8ba6d0_action_cloneVApp.xml" ) + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_vApp_vm_dd75d1d3_5b7b_48f0_aff3_69622ab7e045_networkConnectionSection( self, method, url, body, headers ): body = self.fixtures.load("api_task_b034df55_fe81_4798_bc81_1f0fd0ead450.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a(self, method, url, body, headers): status = httplib.OK + if method == "GET": body = self.fixtures.load("api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a.xml") status = httplib.OK elif method == "DELETE": body = self.fixtures.load("api_task_b034df55_fe81_4798_bc81_1f0fd0ead450.xml") 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_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6c(self, method, url, body, headers): body = self.fixtures.load("api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6c.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] def _api_vApp_vm_dd75d1d3_5b7b_48f0_aff3_69622ab7e045_guestCustomizationSection( @@ -1186,6 +1218,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): "put_api_vApp_vm_dd75d1d3_5b7b_48f0_aff3_69622ab7e045_guestCustomizationSection.xml" ) status = httplib.ACCEPTED + return status, body, headers, httplib.responses[status] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_power_action_reset( @@ -1197,44 +1230,54 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): def _api_task_b034df55_fe81_4798_bc81_1f0fd0ead450(self, method, url, body, headers): body = self.fixtures.load("api_task_b034df55_fe81_4798_bc81_1f0fd0ead450.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_catalog_cddb3cb2_3394_4b14_b831_11fbc4028da4(self, method, url, body, headers): body = self.fixtures.load("api_catalog_cddb3cb2_3394_4b14_b831_11fbc4028da4.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_catalogItem_3132e037_759b_4627_9056_ca66466fa607(self, method, url, body, headers): body = self.fixtures.load("api_catalogItem_3132e037_759b_4627_9056_ca66466fa607.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_deployTest(self, method, url, body, headers): body = self.fixtures.load("api_task_deploy.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_action_deploy( self, method, url, body, headers ): body = self.fixtures.load("api_task_deploy.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_task_deploy(self, method, url, body, headers): body = self.fixtures.load("api_task_deploy.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_vApp_undeployTest(self, method, url, body, headers): body = self.fixtures.load("api_vApp_undeployTest.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_undeployTest_action_undeploy(self, method, url, body, headers): body = self.fixtures.load("api_task_undeploy.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_task_undeploy(self, method, url, body, headers): body = self.fixtures.load("api_task_undeploy.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_undeployErrorTest(self, method, url, body, headers): body = self.fixtures.load("api_vApp_undeployTest.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_undeployErrorTest_action_undeploy(self, method, url, body, headers): @@ -1242,10 +1285,12 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load("api_task_undeploy_error.xml") else: body = self.fixtures.load("api_task_undeploy.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_task_undeployError(self, method, url, body, headers): body = self.fixtures.load("api_task_undeploy_error.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_undeployPowerOffTest(self, method, url, body, headers): @@ -1253,6 +1298,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): def _api_vApp_undeployPowerOffTest_action_undeploy(self, method, url, body, headers): self.assertIn(b("powerOff"), b(body)) + return self._api_vApp_undeployTest_action_undeploy(method, url, body, headers) def _api_vApp_vapp_access_to_resource_forbidden(self, method, url, body, headers): @@ -1262,6 +1308,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): 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): @@ -1271,6 +1318,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): 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): @@ -1280,6 +1328,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): 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): @@ -1289,6 +1338,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): else: body = self.fixtures.load("put_api_vApp_vm_test_virtualHardwareSection_memory.xml") status = httplib.ACCEPTED + return status, body, headers, httplib.responses[status] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_power_action_powerOff( @@ -1305,10 +1355,12 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_power_action_all.xml" ) + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] def _api_query(self, method, url, body, headers): assert method == "GET" + if "type=user" in url: self.assertTrue("page=2" in url) self.assertTrue("filter=(name==jrambo)" in url) @@ -1320,6 +1372,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load("api_query_vm.xml") else: raise AssertionError("Unexpected query type") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_metadata( @@ -1327,9 +1380,11 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): ): if method == "POST": body = self.fixtures.load("api_vapp_post_metadata.xml") + return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] else: body = self.fixtures.load("api_vapp_get_metadata.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_controlAccess( @@ -1338,6 +1393,7 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml" ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_action_controlAccess( @@ -1354,14 +1410,17 @@ class VCloud_1_5_MockHttp(MockHttp, unittest.TestCase): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml" ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413(self, method, url, body, headers): body = self.fixtures.load("api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6d(self, method, url, body, headers): body = self.fixtures.load("api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6d.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] @@ -1375,10 +1434,12 @@ class VCloud_5_5_MockHttp(VCloud_1_5_MockHttp): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_create_snapshot.xml" ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_task_fab4b26f_4f2e_4d49_ad01_ae9324bbfe48(self, method, url, body, headers): body = self.fixtures.load("api_task_b034df55_fe81_4798_bc81_1f0fd0ead450.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_action_removeAllSnapshots( @@ -1388,10 +1449,12 @@ class VCloud_5_5_MockHttp(VCloud_1_5_MockHttp): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_remove_snapshots.xml" ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_task_2518935e_b315_4d8e_9e99_9275f751877c(self, method, url, body, headers): body = self.fixtures.load("api_task_2518935e_b315_4d8e_9e99_9275f751877c.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_action_revertToCurrentSnapshot( @@ -1401,10 +1464,12 @@ class VCloud_5_5_MockHttp(VCloud_1_5_MockHttp): body = self.fixtures.load( "api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_revert_snapshot.xml" ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] def _api_task_fe75d3af_f5a3_44a5_b016_ae0bdadfc32b(self, method, url, body, headers): body = self.fixtures.load("api_task_fe75d3af_f5a3_44a5_b016_ae0bdadfc32b.xml") + return httplib.OK, body, headers, httplib.responses[httplib.OK]