| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """ |
| VMware vCloud driver. |
| """ |
| import copy |
| import sys |
| import re |
| import base64 |
| import os |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import urlencode |
| from libcloud.utils.py3 import urlparse |
| from libcloud.utils.py3 import b |
| from libcloud.utils.py3 import next |
| |
| urlparse = urlparse.urlparse |
| |
| import time |
| |
| from xml.etree import ElementTree as ET |
| from xml.parsers.expat import ExpatError |
| |
| from libcloud.common.base import XmlResponse, ConnectionUserAndKey |
| 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 |
| from libcloud.compute.base import NodeSize, NodeImage, NodeAuthPassword |
| |
| """ |
| From vcloud api "The VirtualQuantity element defines the number of MB |
| of memory. This should be either 512 or a multiple of 1024 (1 GB)." |
| """ |
| VIRTUAL_MEMORY_VALS = [512] + [1024 * i for i in range(1, 9)] |
| |
| 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)] |
| 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.""" |
| namespace, root_tag = root.tag[1:].split("}", 1) |
| fixed_xpath = "/".join(["{%s}%s" % (namespace, e) |
| for e in xpath.split("/")]) |
| return fixed_xpath |
| |
| |
| def get_url_path(url): |
| return urlparse(url.strip()).path |
| |
| |
| class Vdc(object): |
| """ |
| Virtual datacenter (vDC) representation |
| """ |
| |
| def __init__(self, id, name, driver, allocation_model=None, cpu=None, |
| memory=None, storage=None): |
| self.id = id |
| self.name = name |
| self.driver = driver |
| self.allocation_model = allocation_model |
| self.cpu = cpu |
| self.memory = memory |
| self.storage = storage |
| |
| def __repr__(self): |
| return ('<Vdc: id=%s, name=%s, driver=%s ...>' |
| % (self.id, self.name, self.driver.name)) |
| |
| |
| class Capacity(object): |
| """ |
| Represents CPU, Memory or Storage capacity of vDC. |
| """ |
| def __init__(self, limit, used, units): |
| self.limit = limit |
| self.used = used |
| self.units = units |
| |
| def __repr__(self): |
| return ('<Capacity: limit=%s, used=%s, units=%s>' |
| % (self.limit, self.used, self.units)) |
| |
| |
| class ControlAccess(object): |
| """ |
| Represents control access settings of a node |
| """ |
| |
| class AccessLevel(object): |
| READ_ONLY = 'ReadOnly' |
| CHANGE = 'Change' |
| FULL_CONTROL = 'FullControl' |
| |
| 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 |
| |
| def __repr__(self): |
| return ('<ControlAccess: node=%s, everyone_access_level=%s, subjects=%s>' |
| % (self.node, self.everyone_access_level, self.subjects)) |
| |
| |
| class Subject(object): |
| """ |
| User or group subject |
| """ |
| def __init__(self, type, name, access_level, id=None): |
| self.type = type |
| self.name = name |
| self.access_level = access_level |
| self.id = id |
| |
| def __repr__(self): |
| return ('<Subject: type=%s, name=%s, access_level=%s>' |
| % (self.type, self.name, self.access_level)) |
| |
| |
| class InstantiateVAppXML(object): |
| def __init__(self, name, template, net_href, cpus, memory, |
| password=None, row=None, group=None): |
| self.name = name |
| self.template = template |
| self.net_href = net_href |
| self.cpus = cpus |
| self.memory = memory |
| self.password = password |
| self.row = row |
| self.group = group |
| |
| self._build_xmltree() |
| |
| def tostring(self): |
| return ET.tostring(self.root) |
| |
| def _build_xmltree(self): |
| self.root = self._make_instantiation_root() |
| |
| self._add_vapp_template(self.root) |
| instantiation_params = ET.SubElement(self.root, |
| "InstantiationParams") |
| |
| # product and virtual hardware |
| self._make_product_section(instantiation_params) |
| self._make_virtual_hardware(instantiation_params) |
| |
| network_config_section = ET.SubElement(instantiation_params, |
| "NetworkConfigSection") |
| |
| network_config = ET.SubElement(network_config_section, |
| "NetworkConfig") |
| self._add_network_association(network_config) |
| |
| def _make_instantiation_root(self): |
| return ET.Element( |
| "InstantiateVAppTemplateParams", |
| {'name': self.name, |
| 'xml:lang': 'en', |
| 'xmlns': "http://www.vmware.com/vcloud/v0.8", |
| 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"} |
| ) |
| |
| def _add_vapp_template(self, parent): |
| return ET.SubElement( |
| parent, |
| "VAppTemplate", |
| {'href': self.template} |
| ) |
| |
| def _make_product_section(self, parent): |
| prod_section = ET.SubElement( |
| parent, |
| "ProductSection", |
| {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8", |
| 'xmlns:ovf': "http://schemas.dmtf.org/ovf/envelope/1"} |
| ) |
| |
| if self.password: |
| self._add_property(prod_section, 'password', self.password) |
| |
| if self.row: |
| self._add_property(prod_section, 'row', self.row) |
| |
| if self.group: |
| self._add_property(prod_section, 'group', self.group) |
| |
| return prod_section |
| |
| def _add_property(self, parent, ovfkey, ovfvalue): |
| return ET.SubElement( |
| parent, |
| "Property", |
| {'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1', |
| 'ovf:key': ovfkey, |
| 'ovf:value': ovfvalue} |
| ) |
| |
| def _make_virtual_hardware(self, parent): |
| vh = ET.SubElement( |
| parent, |
| "VirtualHardwareSection", |
| {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8"} |
| ) |
| |
| self._add_cpu(vh) |
| self._add_memory(vh) |
| |
| return vh |
| |
| def _add_cpu(self, parent): |
| cpu_item = ET.SubElement( |
| parent, |
| "Item", |
| {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"} |
| ) |
| self._add_instance_id(cpu_item, '1') |
| self._add_resource_type(cpu_item, '3') |
| self._add_virtual_quantity(cpu_item, self.cpus) |
| |
| return cpu_item |
| |
| def _add_memory(self, parent): |
| mem_item = ET.SubElement( |
| parent, |
| 'Item', |
| {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"} |
| ) |
| self._add_instance_id(mem_item, '2') |
| self._add_resource_type(mem_item, '4') |
| self._add_virtual_quantity(mem_item, self.memory) |
| |
| return mem_item |
| |
| def _add_instance_id(self, parent, id): |
| elm = ET.SubElement( |
| parent, |
| 'InstanceID', |
| {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/' |
| 'CIM_ResourceAllocationSettingData'} |
| ) |
| elm.text = id |
| return elm |
| |
| def _add_resource_type(self, parent, type): |
| elm = ET.SubElement( |
| parent, |
| 'ResourceType', |
| {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/' |
| 'CIM_ResourceAllocationSettingData'} |
| ) |
| elm.text = type |
| return elm |
| |
| def _add_virtual_quantity(self, parent, amount): |
| elm = ET.SubElement( |
| parent, |
| 'VirtualQuantity', |
| {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/' |
| 'CIM_ResourceAllocationSettingData'} |
| ) |
| elm.text = amount |
| return elm |
| |
| def _add_network_association(self, parent): |
| return ET.SubElement( |
| parent, |
| 'NetworkAssociation', |
| {'href': self.net_href} |
| ) |
| |
| |
| class VCloudResponse(XmlResponse): |
| def success(self): |
| return self.status in (httplib.OK, httplib.CREATED, |
| httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| |
| class VCloudConnection(ConnectionUserAndKey): |
| """ |
| Connection class for the vCloud driver |
| """ |
| |
| responseCls = VCloudResponse |
| token = None |
| host = None |
| |
| def request(self, *args, **kwargs): |
| self._get_auth_token() |
| return super(VCloudConnection, self).request(*args, **kwargs) |
| |
| def check_org(self): |
| # the only way to get our org is by logging in. |
| self._get_auth_token() |
| |
| def _get_auth_headers(self): |
| """Some providers need different headers than others""" |
| return { |
| 'Authorization': "Basic %s" % base64.b64encode( |
| b('%s:%s' % (self.user_id, self.key))).decode('utf-8'), |
| 'Content-Length': '0', |
| 'Accept': 'application/*+xml' |
| } |
| |
| def _get_auth_token(self): |
| if not self.token: |
| conn = self.conn_classes[self.secure](self.host, |
| self.port) |
| conn.request(method='POST', url='/api/v0.8/login', |
| headers=self._get_auth_headers()) |
| |
| resp = conn.getresponse() |
| headers = dict(resp.getheaders()) |
| body = ET.XML(resp.read()) |
| |
| try: |
| self.token = headers['set-cookie'] |
| except KeyError: |
| raise InvalidCredsError() |
| |
| self.driver.org = get_url_path( |
| body.find(fixxpath(body, 'Org')).get('href') |
| ) |
| |
| def add_default_headers(self, headers): |
| headers['Cookie'] = self.token |
| headers['Accept'] = 'application/*+xml' |
| return headers |
| |
| |
| class VCloudNodeDriver(NodeDriver): |
| """ |
| vCloud node driver |
| """ |
| |
| type = Provider.VCLOUD |
| name = 'vCloud' |
| website = 'http://www.vmware.com/products/vcloud/' |
| connectionCls = VCloudConnection |
| org = None |
| _vdcs = None |
| |
| NODE_STATE_MAP = {'0': NodeState.PENDING, |
| '1': NodeState.PENDING, |
| '2': NodeState.PENDING, |
| '3': NodeState.PENDING, |
| '4': NodeState.RUNNING} |
| |
| def __new__(cls, key, secret=None, secure=True, host=None, port=None, |
| api_version=DEFAULT_API_VERSION, **kwargs): |
| if cls is VCloudNodeDriver: |
| if api_version == '0.8': |
| cls = VCloudNodeDriver |
| elif api_version == '1.5': |
| cls = VCloud_1_5_NodeDriver |
| elif api_version == '5.1': |
| cls = VCloud_1_5_NodeDriver |
| else: |
| raise NotImplementedError( |
| "No VCloudNodeDriver found for API version %s" % |
| (api_version)) |
| return super(VCloudNodeDriver, cls).__new__(cls) |
| |
| @property |
| def vdcs(self): |
| """ |
| vCloud virtual data centers (vDCs). |
| |
| @return: list of vDC objects |
| @rtype: C{list} of L{Vdc} |
| """ |
| if not self._vdcs: |
| self.connection.check_org() # make sure the org is set. # pylint: disable-msg=E1101 |
| res = self.connection.request(self.org) |
| self._vdcs = [ |
| self._to_vdc( |
| self.connection.request(get_url_path(i.get('href'))).object |
| ) |
| 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): |
| return Vdc(vdc_elm.get('href'), vdc_elm.get('name'), self) |
| |
| 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 = [] |
| for vdc in self.vdcs: |
| res = self.connection.request(get_url_path(vdc.id)).object |
| networks.extend( |
| [network |
| for network in res.findall( |
| fixxpath(res, 'AvailableNetworks/Network') |
| |
| )] |
| ) |
| |
| return networks |
| |
| def _to_image(self, image): |
| image = NodeImage(id=image.get('href'), |
| name=image.get('name'), |
| driver=self.connection.driver) |
| return image |
| |
| def _to_node(self, elm): |
| state = self.NODE_STATE_MAP[elm.get('status')] |
| name = elm.get('name') |
| public_ips = [] |
| private_ips = [] |
| |
| # Following code to find private IPs works for Terremark |
| connections = elm.findall('%s/%s' % ( |
| '{http://schemas.dmtf.org/ovf/envelope/1}NetworkConnectionSection', |
| fixxpath(elm, 'NetworkConnection')) |
| ) |
| if not connections: |
| connections = elm.findall( |
| fixxpath( |
| elm, |
| 'Children/Vm/NetworkConnectionSection/NetworkConnection')) |
| |
| 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: |
| public_ips.extend(ips) |
| |
| node = Node(id=elm.get('href'), |
| name=name, |
| state=state, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self.connection.driver) |
| |
| return node |
| |
| def _get_catalog_hrefs(self): |
| res = self.connection.request(self.org) |
| catalogs = [ |
| i.get('href') |
| for i in res.object.findall(fixxpath(res.object, "Link")) |
| if i.get('type') == 'application/vnd.vmware.vcloud.catalog+xml' |
| ] |
| |
| return catalogs |
| |
| def _wait_for_task_completion(self, task_href, |
| timeout=DEFAULT_TASK_COMPLETION_TIMEOUT): |
| 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 %s.: %s" |
| % (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 (%s sec) while waiting for task %s." |
| % (timeout, task_href)) |
| time.sleep(5) |
| res = self.connection.request(get_url_path(task_href)) |
| status = res.object.get('status') |
| |
| def destroy_node(self, node): |
| node_path = get_url_path(node.id) |
| # blindly poweroff node, it will throw an exception if already off |
| try: |
| res = self.connection.request('%s/power/action/poweroff' |
| % node_path, |
| method='POST') |
| self._wait_for_task_completion(res.object.get('href')) |
| except Exception: |
| pass |
| |
| try: |
| res = self.connection.request('%s/action/undeploy' % node_path, |
| method='POST') |
| self._wait_for_task_completion(res.object.get('href')) |
| except ExpatError: |
| # The undeploy response is malformed XML atm. |
| # We can remove this whent he providers fix the problem. |
| pass |
| except Exception: |
| # Some vendors don't implement undeploy at all yet, |
| # so catch this and move on. |
| 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): |
| return self.ex_list_nodes() |
| |
| def ex_list_nodes(self, vdcs=None): |
| """ |
| List all nodes across all vDCs. Using 'vdcs' you can specify which vDCs |
| should be queried. |
| |
| @param vdcs: None, vDC or a list of vDCs to query. If None all vDCs |
| will be queried. |
| @type vdcs: L{Vdc} |
| |
| @rtype: C{list} of L{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") |
| ) |
| vapps = [ |
| (i.get('name'), i.get('href')) |
| for i in elms |
| if i.get('type') |
| == 'application/vnd.vmware.vcloud.vApp+xml' |
| and i.get('name') |
| ] |
| |
| for vapp_name, vapp_href in vapps: |
| try: |
| res = self.connection.request( |
| get_url_path(vapp_href), |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.vApp+xml'} |
| ) |
| nodes.append(self._to_node(res.object)) |
| except Exception: |
| # The vApp was probably removed since the previous vDC query, ignore |
| e = sys.exc_info()[1] |
| if not (e.args[0].tag.endswith('Error') and |
| e.args[0].get('minorErrorCode') == 'ACCESS_TO_RESOURCE_IS_FORBIDDEN'): |
| raise |
| |
| return nodes |
| |
| def _to_size(self, ram): |
| ns = NodeSize( |
| id=None, |
| name="%s Ram" % ram, |
| ram=ram, |
| disk=None, |
| bandwidth=None, |
| 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): |
| """Given a catalog href returns contained catalog item hrefs""" |
| res = self.connection.request( |
| get_url_path(catalog), |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.catalog+xml' |
| } |
| ).object |
| |
| cat_items = res.findall(fixxpath(res, "CatalogItems/CatalogItem")) |
| cat_item_hrefs = [i.get('href') |
| for i in cat_items |
| if i.get('type') == |
| 'application/vnd.vmware.vcloud.catalogItem+xml'] |
| |
| return cat_item_hrefs |
| |
| def _get_catalogitem(self, catalog_item): |
| """Given a catalog item href returns elementree""" |
| res = self.connection.request( |
| get_url_path(catalog_item), |
| headers={ |
| 'Content-Type': |
| 'application/vnd.vmware.vcloud.catalogItem+xml' |
| } |
| ).object |
| |
| return res |
| |
| 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") |
| ) |
| images += [ |
| self._to_image(i) |
| for i in res_ents |
| if i.get('type') == |
| 'application/vnd.vmware.vcloud.vAppTemplate+xml' |
| ] |
| |
| for catalog in self._get_catalog_hrefs(): |
| for cat_item in self._get_catalogitems_hrefs(catalog): |
| res = self._get_catalogitem(cat_item) |
| res_ents = res.findall(fixxpath(res, 'Entity')) |
| images += [ |
| self._to_image(i) |
| for i in res_ents |
| if i.get('type') == |
| 'application/vnd.vmware.vcloud.vAppTemplate+xml' |
| ] |
| |
| def idfun(image): |
| return image.id |
| |
| return self._uniquer(images, idfun) |
| |
| def _uniquer(self, seq, idfun=None): |
| if idfun is None: |
| def idfun(x): |
| return x |
| 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(self, **kwargs): |
| """Creates and returns node. |
| |
| |
| @inherits: L{NodeDriver.create_node} |
| |
| @keyword ex_network: link to a "Network" e.g., |
| "https://services.vcloudexpress.terremark.com/api/v0.8/network/7" |
| @type ex_network: C{str} |
| |
| @keyword ex_vdc: Name of organisation's virtual data |
| center where vApp VMs will be deployed. |
| @type ex_vdc: C{str} |
| |
| @keyword ex_cpus: number of virtual cpus (limit depends on provider) |
| @type ex_cpus: C{int} |
| |
| @keyword ex_row: ???? |
| @type ex_row: C{str} |
| |
| @keyword ex_group: ???? |
| @type ex_group: C{str} |
| """ |
| name = kwargs['name'] |
| image = kwargs['image'] |
| size = kwargs['size'] |
| |
| # Some providers don't require a network link |
| try: |
| network = kwargs.get('ex_network', self.networks[0].get('href')) |
| except IndexError: |
| network = '' |
| |
| password = None |
| if 'auth' in kwargs: |
| auth = kwargs['auth'] |
| if isinstance(auth, NodeAuthPassword): |
| password = auth.password |
| else: |
| raise ValueError('auth must be of NodeAuthPassword type') |
| |
| instantiate_xml = InstantiateVAppXML( |
| name=name, |
| template=image.id, |
| net_href=network, |
| cpus=str(kwargs.get('ex_cpus', 1)), |
| memory=str(size.ram), |
| password=password, |
| row=kwargs.get('ex_row', None), |
| 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' % get_url_path(vdc.id), |
| data=instantiate_xml.tostring(), |
| method='POST', |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'} |
| ) |
| vapp_path = get_url_path(res.object.get('href')) |
| |
| # Deploy the VM from the identifier. |
| res = self.connection.request('%s/action/deploy' % vapp_path, |
| method='POST') |
| |
| self._wait_for_task_completion(res.object.get('href')) |
| |
| # Power on the VM. |
| res = self.connection.request('%s/power/action/powerOn' % vapp_path, |
| method='POST') |
| |
| res = self.connection.request(vapp_path) |
| node = self._to_node(res.object) |
| |
| return node |
| |
| features = {"create_node": ["password"]} |
| |
| |
| class HostingComConnection(VCloudConnection): |
| """ |
| vCloud connection subclass for Hosting.com |
| """ |
| |
| host = "vcloud.safesecureweb.com" |
| |
| def _get_auth_headers(self): |
| """hosting.com doesn't follow the standard vCloud authentication API""" |
| return { |
| 'Authentication': base64.b64encode(b('%s:%s' % (self.user_id, |
| self.key))), |
| 'Content-Length': '0' |
| } |
| |
| |
| class HostingComDriver(VCloudNodeDriver): |
| """ |
| vCloud node driver for Hosting.com |
| """ |
| connectionCls = HostingComConnection |
| |
| |
| class TerremarkConnection(VCloudConnection): |
| """ |
| vCloud connection subclass for Terremark |
| """ |
| |
| host = "services.vcloudexpress.terremark.com" |
| |
| |
| class TerremarkDriver(VCloudNodeDriver): |
| """ |
| vCloud node driver for Terremark |
| """ |
| |
| connectionCls = TerremarkConnection |
| |
| def list_locations(self): |
| return [NodeLocation(0, "Terremark Texas", 'US', self)] |
| |
| |
| 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('%s:%s' % (self.user_id, self.key))).decode('utf-8'), |
| 'Content-Length': '0', |
| 'Accept': 'application/*+xml;version=1.5' |
| } |
| |
| def _get_auth_token(self): |
| if not self.token: |
| # Log In |
| conn = self.conn_classes[self.secure](self.host, |
| self.port) |
| conn.request(method='POST', url='/api/sessions', |
| headers=self._get_auth_headers()) |
| |
| resp = conn.getresponse() |
| headers = dict(resp.getheaders()) |
| |
| # Set authorization token |
| try: |
| self.token = headers['x-vcloud-authorization'] |
| except KeyError: |
| raise InvalidCredsError() |
| |
| # Get the URL of the Organization |
| body = ET.XML(resp.read()) |
| 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') |
| ) |
| |
| conn.request(method='GET', url=org_list_url, |
| headers=self.add_default_headers({})) |
| 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') == self.org_name)).get('href') |
| ) |
| |
| def add_default_headers(self, headers): |
| headers['Accept'] = 'application/*+xml;version=1.5' |
| headers['x-vcloud-authorization'] = self.token |
| return headers |
| |
| |
| class Instantiate_1_5_VAppXML(object): |
| 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): |
| return ET.tostring(self.root) |
| |
| def _build_xmltree(self): |
| self.root = self._make_instantiation_root() |
| |
| if self.network is not None: |
| instantionation_params = ET.SubElement(self.root, |
| 'InstantiationParams') |
| network_config_section = ET.SubElement(instantionation_params, |
| 'NetworkConfigSection') |
| ET.SubElement( |
| network_config_section, |
| 'Info', |
| {'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1'} |
| ) |
| network_config = ET.SubElement(network_config_section, |
| 'NetworkConfig') |
| self._add_network_association(network_config) |
| |
| self._add_vapp_template(self.root) |
| |
| def _make_instantiation_root(self): |
| return ET.Element( |
| 'InstantiateVAppTemplateParams', |
| {'name': self.name, |
| 'deploy': 'false', |
| 'powerOn': 'false', |
| 'xml:lang': 'en', |
| 'xmlns': 'http://www.vmware.com/vcloud/v1.5', |
| 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'} |
| ) |
| |
| def _add_vapp_template(self, parent): |
| return ET.SubElement( |
| parent, |
| 'Source', |
| {'href': self.template} |
| ) |
| |
| def _add_network_association(self, parent): |
| 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')}) |
| |
| 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): |
| connectionCls = VCloud_1_5_Connection |
| |
| # Based on http://pubs.vmware.com/vcloud-api-1-5/api_prog/GUID-843BE3AD-5EF6-4442-B864-BCAE44A51867.html |
| NODE_STATE_MAP = {'-1': NodeState.UNKNOWN, |
| '0': NodeState.PENDING, |
| '1': NodeState.PENDING, |
| '2': NodeState.PENDING, |
| '3': NodeState.PENDING, |
| '4': NodeState.RUNNING, |
| '5': NodeState.RUNNING, |
| '6': NodeState.UNKNOWN, |
| '7': NodeState.UNKNOWN, |
| '8': NodeState.TERMINATED, |
| '9': NodeState.UNKNOWN, |
| '10': NodeState.UNKNOWN} |
| |
| def list_locations(self): |
| return [NodeLocation(id=self.connection.host, name=self.connection.host, country="N/A", driver=self)] |
| |
| def ex_find_node(self, node_name, vdcs=None): |
| """ |
| Searches for node across specified vDCs. This is more effective than querying all nodes to get a single |
| instance. |
| |
| @param node_name: The name of the node to search for |
| @type node_name: C{str} |
| |
| @param vdcs: None, vDC or a list of vDCs to search in. If None all vDCs will be searched. |
| @type vdcs: L{Vdc} |
| |
| @return: node instance or None if not found |
| @rtype: L{Node} or C{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)) |
| entity_elems = res.object.findall(fixxpath(res.object, "ResourceEntities/ResourceEntity")) |
| for entity_elem in entity_elems: |
| if entity_elem.get('type') == 'application/vnd.vmware.vcloud.vApp+xml' and entity_elem.get('name') == node_name: |
| res = self.connection.request(get_url_path(entity_elem.get('href')), |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.vApp+xml'}) |
| return self._to_node(res.object) |
| return None |
| |
| def destroy_node(self, node): |
| try: |
| self.ex_undeploy_node(node) |
| except Exception: |
| # Some vendors don't implement undeploy at all yet, |
| # so catch this and move on. |
| 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 |
| |
| def ex_deploy_node(self, node): |
| """ |
| Deploys existing node. Equal to vApp "start" operation. |
| |
| @param node: The node to be deployed |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| deploy_xml = ET.Element('DeployVAppParams', {'powerOn': 'true', |
| 'xmlns': 'http://www.vmware.com/vcloud/v1.5'}) |
| res = self.connection.request('%s/action/deploy' % get_url_path(node.id), |
| data=ET.tostring(deploy_xml), |
| method='POST', |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.deployVAppParams+xml' |
| }) |
| 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_undeploy_node(self, node): |
| """ |
| Undeploys existing node. Equal to vApp "stop" operation. |
| |
| @param node: The node to be deployed |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| undeploy_xml = ET.Element('UndeployVAppParams', {'xmlns': 'http://www.vmware.com/vcloud/v1.5'}) |
| undeploy_power_action_xml = ET.SubElement(undeploy_xml, 'UndeployPowerAction') |
| undeploy_power_action_xml.text = 'shutdown' |
| |
| try: |
| res = self.connection.request( |
| '%s/action/undeploy' % get_url_path(node.id), |
| data=ET.tostring(undeploy_xml), |
| method='POST', |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.undeployVAppParams+xml' |
| }) |
| self._wait_for_task_completion(res.object.get('href')) |
| except Exception: |
| undeploy_power_action_xml.text = 'powerOff' |
| res = self.connection.request( |
| '%s/action/undeploy' % get_url_path(node.id), |
| data=ET.tostring(undeploy_xml), |
| method='POST', |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.undeployVAppParams+xml' |
| }) |
| 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_power_off_node(self, node): |
| """ |
| Powers on all VMs under specified node. VMs need to be This operation |
| is allowed only when the vApp/VM is powered on. |
| |
| @param node: The node to be powered off |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| return self._perform_power_operation(node, 'powerOff') |
| |
| def ex_power_on_node(self, node): |
| """ |
| Powers on all VMs under specified node. This operation is allowed |
| only when the vApp/VM is powered off or suspended. |
| |
| @param node: The node to be powered on |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| return self._perform_power_operation(node, 'powerOn') |
| |
| def ex_shutdown_node(self, node): |
| """ |
| Shutdowns all VMs under specified node. This operation is allowed only |
| when the vApp/VM is powered on. |
| |
| @param node: The node to be shut down |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| return self._perform_power_operation(node, 'shutdown') |
| |
| def ex_suspend_node(self, node): |
| """ |
| Suspends all VMs under specified node. This operation is allowed only |
| when the vApp/VM is powered on. |
| |
| @param node: The node to be suspended |
| @type node: L{Node} |
| |
| @rtype: L{Node} |
| """ |
| return self._perform_power_operation(node, 'suspend') |
| |
| def _perform_power_operation(self, node, operation): |
| res = self.connection.request( |
| '%s/power/action/%s' % (get_url_path(node.id), operation), |
| method='POST') |
| 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): |
| """ |
| Returns the control access settings for specified node. |
| |
| @param node: node to get the control access for |
| @type node: L{Node} |
| |
| @rtype: L{ControlAccess} |
| """ |
| 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")).text |
| |
| # Parse all subjects |
| subjects = [] |
| for elem in res.object.findall( |
| fixxpath(res.object, "AccessSettings/AccessSetting")): |
| 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: |
| subj_type = 'user' |
| res = self.connection.request(get_url_path(subject_elem.get('href'))) |
| name = res.object.get('name') |
| subject = Subject(type=subj_type, |
| name=name, |
| access_level=access_level, |
| id=subject_elem.get('href')) |
| subjects.append(subject) |
| |
| return ControlAccess(node, everyone_access_level, subjects) |
| |
| def ex_set_control_access(self, node, control_access): |
| """ |
| Sets control access for the specified node. |
| |
| @param node: node |
| @type node: L{Node} |
| |
| @param control_access: control access settings |
| @type control_access: L{ControlAccess} |
| |
| @rtype: C{None} |
| """ |
| 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') |
| everyone_access_level.text = control_access.everyone_access_level |
| else: |
| 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 "%s %s" not found ' |
| % (subject.type, subject.name)) |
| href = res[0]['href'] |
| ET.SubElement(setting, 'Subject', {'href': href}) |
| ET.SubElement(setting, 'AccessLevel').text = subject.access_level |
| |
| self.connection.request( |
| '%s/action/controlAccess' % get_url_path(node.id), |
| data=ET.tostring(xml), |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.controlAccess+xml' |
| }, |
| method='POST') |
| |
| def ex_get_metadata(self, node): |
| """ |
| @param node: node |
| @type node: L{Node} |
| |
| @return: dictionary mapping metadata keys to metadata values |
| @rtype: dictionary mapping C{str} to C{str} |
| """ |
| res = self.connection.request('%s/metadata' % (get_url_path(node.id))) |
| metadata_entries = res.object.findall(fixxpath(res.object, 'MetadataEntry')) |
| res_dict = {} |
| |
| for entry in metadata_entries: |
| key = entry.findtext(fixxpath(res.object, 'Key')) |
| value = entry.findtext(fixxpath(res.object, 'Value')) |
| res_dict[key] = value |
| |
| return res_dict |
| |
| def ex_set_metadata_entry(self, node, key, value): |
| """ |
| @param node: node |
| @type node: L{Node} |
| |
| @param key: metadata key to be set |
| @type key: C{str} |
| |
| @param value: metadata value to be set |
| @type value: C{str} |
| |
| @rtype: C{None} |
| """ |
| metadata_elem = ET.Element( |
| 'Metadata', |
| {'xmlns': "http://www.vmware.com/vcloud/v1.5", |
| 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"} |
| ) |
| entry = ET.SubElement(metadata_elem, 'MetadataEntry') |
| key_elem = ET.SubElement(entry, 'Key') |
| key_elem.text = key |
| value_elem = ET.SubElement(entry, 'Value') |
| value_elem.text = value |
| |
| # send it back to the server |
| res = self.connection.request( |
| '%s/metadata' % get_url_path(node.id), |
| data=ET.tostring(metadata_elem), |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.metadata+xml' |
| }, |
| method='POST') |
| self._wait_for_task_completion(res.object.get('href')) |
| |
| def ex_query(self, type, filter=None, page=1, page_size=100, sort_asc=None, |
| sort_desc=None): |
| """ |
| Queries vCloud for specified type. See http://www.vmware.com/pdf/vcd_15_api_guide.pdf |
| for details. Each element of the returned list is a dictionary with all |
| attributes from the record. |
| |
| @param type: type to query (r.g. user, group, vApp etc.) |
| @type type: C{str} |
| |
| @param filter: filter expression (see documentation for syntax) |
| @type filter: C{str} |
| |
| @param page: page number |
| @type page: C{int} |
| |
| @param page_size: page size |
| @type page_size: C{int} |
| |
| @param sort_asc: sort in ascending order by specified field |
| @type sort_asc: C{str} |
| |
| @param sort_desc: sort in descending order by specified field |
| @type sort_desc: C{str} |
| |
| @rtype: C{list} of dict |
| """ |
| # This is a workaround for filter parameter encoding |
| # the urllib encodes (name==Developers%20Only) into |
| # %28name%3D%3DDevelopers%20Only%29) which is not accepted by vCloud |
| params = { |
| 'type': type, |
| '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 + ')' |
| url += '&filter=' + filter.replace(' ', '+') |
| |
| 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): |
| """Creates and returns node. If the source image is: |
| - vApp template - a new vApp is instantiated from template |
| - existing vApp - a new vApp is cloned from the source vApp. Can not clone more vApps is parallel otherwise |
| resource busy error is raised. |
| |
| |
| @inherits: L{NodeDriver.create_node} |
| |
| @keyword image: OS Image to boot on node. (required). Can be a NodeImage or existing Node that will be |
| cloned. |
| @type image: L{NodeImage} or L{Node} |
| |
| @keyword ex_network: Organisation's network name for attaching vApp VMs to. |
| @type ex_network: C{str} |
| |
| @keyword ex_vdc: Name of organisation's virtual data center where vApp VMs will be deployed. |
| @type ex_vdc: C{str} |
| |
| @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. |
| @type ex_vm_names: C{list} of C{str} |
| |
| @keyword ex_vm_cpu: number of virtual CPUs/cores to allocate for each vApp VM. |
| @type ex_vm_cpu: C{int} |
| |
| @keyword ex_vm_memory: amount of memory in MB to allocate for each vApp VM. |
| @type ex_vm_memory: C{int} |
| |
| @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{str} |
| |
| @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{str} |
| |
| @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{str} |
| |
| @keyword ex_vm_ipmode: IP address allocation mode for all vApp VM network connections. |
| @type ex_vm_ipmode: C{str} |
| |
| @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'] |
| 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 |
| if ex_network: |
| network_href = self._get_network_href(ex_network) |
| network_elem = self.connection.request( |
| get_url_path(network_href)).object |
| else: |
| network_elem = None |
| |
| 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, 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. |
| 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' % get_url_path(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(get_url_path(vapp_href)) |
| node = self._to_node(res.object) |
| return node |
| |
| 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, |
| vm_network=vm_network, |
| vm_fence=vm_fence |
| ) |
| |
| # Instantiate VM and get identifier. |
| res = self.connection.request( |
| '%s/action/instantiateVAppTemplate' % get_url_path(vdc.id), |
| data=instantiate_xml.tostring(), |
| method='POST', |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'} |
| ) |
| vapp_name = res.object.get('name') |
| vapp_href = res.object.get('href') |
| |
| task_href = res.object.find(fixxpath(res.object, "Tasks/Task")).get( |
| 'href') |
| self._wait_for_task_completion(task_href) |
| return vapp_name, vapp_href |
| |
| def _clone_node(self, name, sourceNode, vdc): |
| clone_xml = ET.Element( |
| "CloneVAppParams", |
| {'name': name, 'deploy': 'false', 'powerOn': 'false', |
| 'xmlns': "http://www.vmware.com/vcloud/v1.5", |
| 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"} |
| ) |
| ET.SubElement(clone_xml, |
| 'Description').text = 'Clone of ' + sourceNode.name |
| ET.SubElement(clone_xml, 'Source', {'href': sourceNode.id}) |
| |
| res = self.connection.request( |
| '%s/action/cloneVApp' % get_url_path(vdc.id), |
| data=ET.tostring(clone_xml), |
| method='POST', |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.cloneVAppParams+xml'} |
| ) |
| vapp_name = res.object.get('name') |
| vapp_href = res.object.get('href') |
| |
| task_href = res.object.find(fixxpath(res.object, "Tasks/Task")).get('href') |
| self._wait_for_task_completion(task_href) |
| |
| res = self.connection.request(get_url_path(vapp_href)) |
| |
| 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("NetworkConnectionSection", { |
| 'ovf:required': 'false', |
| 'xmlns': "http://www.vmware.com/vcloud/v1.5", |
| 'xmlns:ovf': 'http://schemas.dmtf.org/ovf/envelope/1'}) |
| ET.SubElement(network_xml, "ovf:Info").text = 'Specifies the available VM network connections' |
| res = self.connection.request( |
| '%s/networkConnectionSection' % get_url_path(vm.get('href')), |
| data=ET.tostring(network_xml), |
| method='PUT', |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.networkConnectionSection+xml'} |
| ) |
| self._wait_for_task_completion(res.object.get('href')) |
| |
| # Re-add network |
| network_xml = vm.find(fixxpath(vm, 'NetworkConnectionSection')) |
| network_conn_xml = network_xml.find( |
| fixxpath(network_xml, 'NetworkConnection')) |
| network_conn_xml.set('needsCustomization', 'true') |
| network_conn_xml.remove( |
| network_conn_xml.find(fixxpath(network_xml, 'IpAddress'))) |
| network_conn_xml.remove( |
| network_conn_xml.find(fixxpath(network_xml, 'MACAddress'))) |
| |
| res = self.connection.request( |
| '%s/networkConnectionSection' % get_url_path(vm.get('href')), |
| data=ET.tostring(network_xml), |
| method='PUT', |
| headers={ |
| 'Content-Type': 'application/vnd.vmware.vcloud.networkConnectionSection+xml'} |
| ) |
| self._wait_for_task_completion(res.object.get('href')) |
| |
| 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{str} |
| |
| @keyword vm_cpu: number of virtual CPUs/cores to allocate for specified VMs |
| @type vm_cpu: C{int} |
| |
| @rtype: C{None} |
| """ |
| 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{str} |
| |
| @keyword vm_memory: virtual memory in MB to allocate for the specified VM or VMs |
| @type vm_memory: C{int} |
| |
| @rtype: C{None} |
| """ |
| 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{str} |
| |
| @keyword vm_disk_size: the disk capacity in GB that will be added to the specified VM or VMs |
| @type vm_disk_size: C{int} |
| |
| @rtype: C{None} |
| """ |
| 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: |
| return |
| hname_re = re.compile('^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9]*)[\-])*([A-Za-z]|[A-Za-z][A-Za-z0-9]*[A-Za-z0-9])$') |
| 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): |
| raise ValueError('The VM name "' + name + '" can not be used. "' + name + '" is not a valid computer name for the VM.') |
| |
| @staticmethod |
| def _validate_vm_memory(vm_memory): |
| if vm_memory is None: |
| return |
| elif vm_memory not in VIRTUAL_MEMORY_VALS: |
| raise ValueError( |
| '%s is not a valid vApp VM memory value' % vm_memory) |
| |
| @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 |
| |
| # Get GuestCustomizationSection |
| res = self.connection.request( |
| '%s/guestCustomizationSection' % get_url_path(vm.get('href'))) |
| |
| # Update GuestCustomizationSection |
| res.object.find(fixxpath(res.object, 'ComputerName')).text = vm_names[i] |
| # Remove AdminPassword from customization section |
| admin_pass = res.object.find(fixxpath(res.object, 'AdminPassword')) |
| if admin_pass is not None: |
| res.object.remove(admin_pass) |
| 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')) |
| |
| # Update Vm name |
| req_xml = ET.Element("Vm", { |
| 'name': vm_names[i], |
| 'xmlns': "http://www.vmware.com/vcloud/v1.5"}) |
| res = self.connection.request( |
| get_url_path(vm.get('href')), |
| data=ET.tostring(req_xml), |
| method='PUT', |
| headers={'Content-Type': 'application/vnd.vmware.vcloud.vm+xml'} |
| ) |
| 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' % get_url_path(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' % get_url_path(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(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): |
| 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) |
| os_type_elem = vm_elem.find('{http://schemas.dmtf.org/ovf/envelope/1}OperatingSystemSection') |
| if os_type_elem: |
| os_type = os_type_elem.get('{http://www.vmware.com/schema/ovf}osType') |
| else: |
| os_type = None |
| 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, |
| 'os_type': os_type |
| } |
| 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']) |
| |
| # Find vDC |
| vdc_id = next(link.get('href') for link in node_elm.findall(fixxpath(node_elm, 'Link')) |
| if link.get('type') == 'application/vnd.vmware.vcloud.vdc+xml') |
| vdc = next(vdc for vdc in self.vdcs if vdc.id == vdc_id) |
| |
| 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={'vdc': vdc.name, 'vms': vms}) |
| return node |
| |
| def _to_vdc(self, vdc_elm): |
| |
| def get_capacity_values(capacity_elm): |
| if capacity_elm is None: |
| return None |
| 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'))) |
| memory = get_capacity_values(vdc_elm.find(fixxpath(vdc_elm, 'ComputeCapacity/Memory'))) |
| storage = get_capacity_values(vdc_elm.find(fixxpath(vdc_elm, 'StorageCapacity'))) |
| |
| return Vdc(id=vdc_elm.get('href'), |
| name=vdc_elm.get('name'), |
| driver=self, |
| allocation_model=vdc_elm.findtext(fixxpath(vdc_elm, 'AllocationModel')), |
| cpu=cpu, |
| memory=memory, |
| storage=storage) |
| |
| |
| class VCloud_5_1_NodeDriver(VCloud_1_5_NodeDriver): |
| |
| @staticmethod |
| def _validate_vm_memory(vm_memory): |
| if vm_memory is None: |
| return None |
| elif (vm_memory % 4) != 0: |
| #The vcd 5.1 virtual machine memory size must be a multiple of 4 MB |
| raise ValueError( |
| '%s is not a valid vApp VM memory value' % (vm_memory)) |