| # 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. |
| # libcloud.org 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. |
| """ |
| from libcloud.providers import Provider |
| from libcloud.types import NodeState, InvalidCredsException |
| from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver |
| from libcloud.base import NodeSize, NodeImage, NodeAuthPassword, NodeLocation |
| |
| import base64 |
| import httplib |
| import time |
| from urlparse import urlparse |
| from xml.etree import ElementTree as ET |
| from xml.parsers.expat import ExpatError |
| |
| """ |
| 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 |
| |
| def get_url_path(url): |
| return urlparse(url.strip()).path |
| |
| 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 |
| |
| 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) |
| instantionation_params = ET.SubElement(self.root, |
| "InstantiationParams") |
| |
| product = self._make_product_section(instantionation_params) |
| virtual_hardware = self._make_virtual_hardware(instantionation_params) |
| network_config_section = ET.SubElement(instantionation_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(Response): |
| |
| def parse_body(self): |
| if not self.body: |
| return None |
| try: |
| return ET.XML(self.body) |
| except ExpatError, e: |
| raise Exception("%s: %s" % (e, self.parse_error())) |
| |
| def parse_error(self): |
| return self.error |
| |
| def success(self): |
| return self.status in (httplib.OK, httplib.CREATED, |
| httplib.NO_CONTENT, httplib.ACCEPTED) |
| |
| class VCloudConnection(ConnectionUserAndKey): |
| |
| 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('%s:%s' % (self.user_id, self.key)), |
| 'Content-Length': 0 |
| } |
| |
| def _get_auth_token(self): |
| if not self.token: |
| conn = self.conn_classes[self.secure](self.host, |
| self.port[self.secure]) |
| 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 InvalidCredsException() |
| |
| self.driver.org = get_url_path( |
| body.find(fixxpath(body, 'Org')).get('href') |
| ) |
| |
| def add_default_headers(self, headers): |
| headers['Cookie'] = self.token |
| return headers |
| |
| class VCloudNodeDriver(NodeDriver): |
| type = Provider.VCLOUD |
| name = "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} |
| |
| @property |
| def vdcs(self): |
| if not self._vdcs: |
| self.connection.check_org() # make sure the org is set. |
| res = self.connection.request(self.org) |
| self._vdcs = [ |
| get_url_path(i.get('href')) |
| for i |
| in res.object.findall(fixxpath(res.object, "Link")) |
| if i.get('type') == 'application/vnd.vmware.vcloud.vdc+xml' |
| ] |
| |
| return self._vdcs |
| |
| @property |
| def networks(self): |
| networks = [] |
| for vdc in self.vdcs: |
| res = self.connection.request(vdc).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, name, elm): |
| state = self.NODE_STATE_MAP[elm.get('status')] |
| public_ips = [] |
| private_ips = [] |
| |
| # Following code to find private IPs works for Terremark |
| connections = elm.findall('{http://schemas.dmtf.org/ovf/envelope/1}NetworkConnectionSection/{http://www.vmware.com/vcloud/v0.8}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_ip=public_ips, |
| private_ip=private_ips, |
| driver=self.connection.driver) |
| |
| return node |
| |
| def _get_catalog_hrefs(self): |
| res = self.connection.request(self.org) |
| catalogs = [ |
| get_url_path(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(task_href) |
| status = res.object.get('status') |
| while status != 'success': |
| if status == 'error': |
| raise Exception("Error status returned by task %s." |
| % task_href) |
| if status == 'canceled': |
| raise Exception("Canceled status returned by task %s." |
| % task_href) |
| if (time.time() - start_time >= timeout): |
| raise Exception("Timeout while waiting for task %s." |
| % task_href) |
| time.sleep(5) |
| res = self.connection.request(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, e: |
| 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, e: |
| # 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 == 202 |
| |
| def reboot_node(self, node): |
| res = self.connection.request('%s/power/action/reset' |
| % get_url_path(node.id), |
| method='POST') |
| return res.status == 202 or res.status == 204 |
| |
| def list_nodes(self): |
| nodes = [] |
| for vdc in self.vdcs: |
| res = self.connection.request(vdc) |
| elms = res.object.findall(fixxpath( |
| res.object, "ResourceEntities/ResourceEntity") |
| ) |
| vapps = [ |
| (i.get('name'), get_url_path(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: |
| res = self.connection.request( |
| vapp_href, |
| headers={ |
| 'Content-Type': |
| 'application/vnd.vmware.vcloud.vApp+xml' |
| } |
| ) |
| nodes.append(self._to_node(vapp_name, res.object)) |
| |
| 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( |
| 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( |
| 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(vdc).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' |
| ] |
| |
| return images |
| |
| def create_node(self, **kwargs): |
| """Creates and returns node. |
| |
| Non-standard optional keyword arguments: |
| network -- link to a "Network" e.g., |
| "https://services.vcloudexpress.terremark.com/api/v0.8/network/7" |
| vdc -- link to a "VDC" e.g., |
| "https://services.vcloudexpress.terremark.com/api/v0.8/vdc/1" |
| cpus -- number of virtual cpus (limit depends on provider) |
| password |
| row |
| group |
| """ |
| name = kwargs['name'] |
| image = kwargs['image'] |
| size = kwargs['size'] |
| |
| # Some providers don't require a network link |
| try: |
| network = kwargs.get('network', self.networks[0].get('href')) |
| except IndexError: |
| network = '' |
| |
| password = None |
| if kwargs.has_key('auth'): |
| 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('cpus', 1)), |
| memory=str(size.ram), |
| password=password, |
| row=kwargs.get('row', None), |
| group=kwargs.get('group', None) |
| ) |
| |
| # Instantiate VM and get identifier. |
| res = self.connection.request( |
| '%s/action/instantiateVAppTemplate' |
| % kwargs.get('vdc', self.vdcs[0]), |
| data=instantiate_xml.tostring(), |
| method='POST', |
| headers={ |
| 'Content-Type': |
| 'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml' |
| } |
| ) |
| vapp_name = res.object.get('name') |
| vapp_href = get_url_path(res.object.get('href')) |
| |
| # Deploy the VM from the identifier. |
| res = self.connection.request('%s/action/deploy' % vapp_href, |
| method='POST') |
| |
| self._wait_for_task_completion(res.object.get('href')) |
| |
| # Power on the VM. |
| res = self.connection.request('%s/power/action/powerOn' % vapp_href, |
| method='POST') |
| |
| res = self.connection.request(vapp_href) |
| node = self._to_node(vapp_name, res.object) |
| |
| return node |
| |
| features = {"create_node": ["password"]} |
| |
| class HostingComConnection(VCloudConnection): |
| host = "vcloud.safesecureweb.com" |
| |
| def _get_auth_headers(self): |
| """hosting.com doesn't follow the standard vCloud authentication API""" |
| return { |
| 'Authentication': |
| base64.b64encode('%s:%s' % (self.user_id, self.key)), |
| 'Content-Length': 0 |
| } |
| |
| class HostingComDriver(VCloudNodeDriver): |
| connectionCls = HostingComConnection |
| |
| class TerremarkConnection(VCloudConnection): |
| host = "services.vcloudexpress.terremark.com" |
| |
| class TerremarkDriver(VCloudNodeDriver): |
| connectionCls = TerremarkConnection |
| def list_locations(self): |
| return [NodeLocation(0, "Terremark Texas", 'US', self)] |