| # Copyright 2002-2009, Distributed Systems Architecture Group, Universidad |
| # Complutense de Madrid (dsa-research.org) |
| # |
| # 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. |
| |
| """ |
| OpenNebula.org driver. |
| """ |
| |
| __docformat__ = "epytext" |
| |
| import hashlib |
| from base64 import b64encode |
| |
| from libcloud.utils.py3 import ET, b, next, httplib |
| from libcloud.common.base import XmlResponse, ConnectionUserAndKey |
| from libcloud.common.types import InvalidCredsError |
| from libcloud.compute.base import ( |
| Node, |
| NodeSize, |
| NodeImage, |
| NodeState, |
| NodeDriver, |
| NodeLocation, |
| StorageVolume, |
| ) |
| from libcloud.compute.providers import Provider |
| |
| __all__ = [ |
| "ACTION", |
| "OpenNebulaResponse", |
| "OpenNebulaConnection", |
| "OpenNebulaNodeSize", |
| "OpenNebulaNetwork", |
| "OpenNebulaNodeDriver", |
| "OpenNebula_1_4_NodeDriver", |
| "OpenNebula_2_0_NodeDriver", |
| "OpenNebula_3_0_NodeDriver", |
| "OpenNebula_3_2_NodeDriver", |
| "OpenNebula_3_8_NodeDriver", |
| ] |
| |
| API_HOST = "" |
| API_PORT = (4567, 443) |
| API_SECURE = True |
| API_PLAIN_AUTH = False |
| DEFAULT_API_VERSION = "3.2" |
| |
| |
| class ACTION: |
| """ |
| All actions, except RESUME, only apply when the VM is in the "Running" |
| state. |
| """ |
| |
| STOP = "STOPPED" |
| """ |
| The VM is stopped, and its memory state stored to a checkpoint file. VM |
| state, and disk image, are transferred back to the front-end. Resuming |
| the VM requires the VM instance to be re-scheduled. |
| """ |
| |
| SUSPEND = "SUSPENDED" |
| """ |
| The VM is stopped, and its memory state stored to a checkpoint file. The VM |
| state, and disk image, are left on the host to be resumed later. Resuming |
| the VM does not require the VM to be re-scheduled. Rather, after |
| suspending, the VM resources are reserved for later resuming. |
| """ |
| |
| RESUME = "RESUME" |
| """ |
| The VM is resumed using the saved memory state from the checkpoint file, |
| and the VM's disk image. The VM is either started immediately, or |
| re-scheduled depending on how it was suspended. |
| """ |
| |
| CANCEL = "CANCEL" |
| """ |
| The VM is forcibly shutdown, its memory state is deleted. If a persistent |
| disk image was used, that disk image is transferred back to the front-end. |
| Any non-persistent disk images are deleted. |
| """ |
| |
| SHUTDOWN = "SHUTDOWN" |
| """ |
| The VM is gracefully shutdown by sending the ACPI signal. If the VM does |
| not shutdown, then it is considered to still be running. If successfully, |
| shutdown, its memory state is deleted. If a persistent disk image was used, |
| that disk image is transferred back to the front-end. Any non-persistent |
| disk images are deleted. |
| """ |
| |
| REBOOT = "REBOOT" |
| """ |
| Introduced in OpenNebula v3.2. |
| |
| The VM is gracefully restarted by sending the ACPI signal. |
| """ |
| |
| DONE = "DONE" |
| """ |
| The VM is forcibly shutdown, its memory state is deleted. If a persistent |
| disk image was used, that disk image is transferred back to the front-end. |
| Any non-persistent disk images are deleted. |
| """ |
| |
| |
| class OpenNebulaResponse(XmlResponse): |
| """ |
| XmlResponse class for the OpenNebula.org driver. |
| """ |
| |
| def success(self): |
| """ |
| Check if response has the appropriate HTTP response code to be a |
| success. |
| |
| :rtype: ``bool`` |
| :return: True is success, else False. |
| """ |
| i = int(self.status) |
| return 200 <= i <= 299 |
| |
| def parse_error(self): |
| """ |
| Check if response contains any errors. |
| |
| @raise: :class:`InvalidCredsError` |
| |
| :rtype: :class:`ElementTree` |
| :return: Contents of HTTP response body. |
| """ |
| if int(self.status) == httplib.UNAUTHORIZED: |
| raise InvalidCredsError(self.body) |
| return self.body |
| |
| |
| class OpenNebulaConnection(ConnectionUserAndKey): |
| """ |
| Connection class for the OpenNebula.org driver. |
| with plain_auth support |
| """ |
| |
| host = API_HOST |
| port = API_PORT |
| secure = API_SECURE |
| plain_auth = API_PLAIN_AUTH |
| responseCls = OpenNebulaResponse |
| |
| def __init__(self, *args, **kwargs): |
| if "plain_auth" in kwargs: |
| self.plain_auth = kwargs.pop("plain_auth") |
| super().__init__(*args, **kwargs) |
| |
| def add_default_headers(self, headers): |
| """ |
| Add headers required by the OpenNebula.org OCCI interface. |
| |
| Includes adding Basic HTTP Authorization headers for authenticating |
| against the OpenNebula.org OCCI interface. |
| |
| :type headers: ``dict`` |
| :param headers: Dictionary containing HTTP headers. |
| |
| :rtype: ``dict`` |
| :return: Dictionary containing updated headers. |
| """ |
| if self.plain_auth: |
| passwd = self.key |
| else: |
| passwd = hashlib.sha1(b(self.key)).hexdigest() |
| headers["Authorization"] = "Basic %s" % b64encode( |
| b("{}:{}".format(self.user_id, passwd)) |
| ).decode("utf-8") |
| return headers |
| |
| |
| class OpenNebulaNodeSize(NodeSize): |
| """ |
| NodeSize class for the OpenNebula.org driver. |
| """ |
| |
| def __init__(self, id, name, ram, disk, bandwidth, price, driver, cpu=None, vcpu=None): |
| super().__init__( |
| id=id, |
| name=name, |
| ram=ram, |
| disk=disk, |
| bandwidth=bandwidth, |
| price=price, |
| driver=driver, |
| ) |
| self.cpu = cpu |
| self.vcpu = vcpu |
| |
| def __repr__(self): |
| return ( |
| "<OpenNebulaNodeSize: id=%s, name=%s, ram=%s, disk=%s, " |
| "bandwidth=%s, price=%s, driver=%s, cpu=%s, vcpu=%s ...>" |
| ) % ( |
| self.id, |
| self.name, |
| self.ram, |
| self.disk, |
| self.bandwidth, |
| self.price, |
| self.driver.name, |
| self.cpu, |
| self.vcpu, |
| ) |
| |
| |
| class OpenNebulaNetwork: |
| """ |
| Provide a common interface for handling networks of all types. |
| |
| Network objects are analogous to physical switches connecting two or |
| more physical nodes together. The Network object provides the interface in |
| libcloud through which we can manipulate networks in different cloud |
| providers in the same way. Network objects don't actually do much directly |
| themselves, instead the network driver handles the connection to the |
| network. |
| |
| You don't normally create a network object yourself; instead you use |
| a driver and then have that create the network for you. |
| |
| >>> from libcloud.compute.drivers.dummy import DummyNodeDriver |
| >>> driver = DummyNodeDriver() |
| >>> network = driver.create_network() |
| >>> network = driver.list_networks()[0] |
| >>> network.name |
| 'dummy-1' |
| """ |
| |
| def __init__(self, id, name, address, size, driver, extra=None): |
| self.id = str(id) |
| self.name = name |
| self.address = address |
| self.size = size |
| self.driver = driver |
| self.uuid = self.get_uuid() |
| self.extra = extra or {} |
| |
| def get_uuid(self): |
| """ |
| Unique hash for this network. |
| |
| The hash is a function of an SHA1 hash of the network's ID and |
| its driver which means that it should be unique between all |
| networks. In some subclasses (e.g. GoGrid) there is no ID |
| available so the public IP address is used. This means that, |
| unlike a properly done system UUID, the same UUID may mean a |
| different system install at a different time |
| |
| >>> from libcloud.network.drivers.dummy import DummyNetworkDriver |
| >>> driver = DummyNetworkDriver() |
| >>> network = driver.create_network() |
| >>> network.get_uuid() |
| 'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f' |
| |
| Note, for example, that this example will always produce the |
| same UUID! |
| |
| :rtype: ``str`` |
| :return: Unique identifier for this instance. |
| """ |
| return hashlib.sha1(b("{}:{}".format(self.id, self.driver.type))).hexdigest() |
| |
| def __repr__(self): |
| return ( |
| "<OpenNebulaNetwork: uuid=%s, name=%s, address=%s, size=%s, " "provider=%s ...>" |
| ) % (self.uuid, self.name, self.address, self.size, self.driver.name) |
| |
| |
| class OpenNebulaNodeDriver(NodeDriver): |
| """ |
| OpenNebula.org node driver. |
| """ |
| |
| connectionCls = OpenNebulaConnection |
| name = "OpenNebula" |
| website = "http://opennebula.org/" |
| type = Provider.OPENNEBULA |
| |
| NODE_STATE_MAP = { |
| "INIT": NodeState.PENDING, |
| "PENDING": NodeState.PENDING, |
| "HOLD": NodeState.PENDING, |
| "ACTIVE": NodeState.RUNNING, |
| "STOPPED": NodeState.TERMINATED, |
| "SUSPENDED": NodeState.PENDING, |
| "DONE": NodeState.TERMINATED, |
| "FAILED": NodeState.TERMINATED, |
| } |
| |
| def __new__(cls, key, secret=None, api_version=DEFAULT_API_VERSION, **kwargs): |
| if cls is OpenNebulaNodeDriver: |
| if api_version in ["1.4"]: |
| cls = OpenNebula_1_4_NodeDriver |
| elif api_version in ["2.0", "2.2"]: |
| cls = OpenNebula_2_0_NodeDriver |
| elif api_version in ["3.0"]: |
| cls = OpenNebula_3_0_NodeDriver |
| elif api_version in ["3.2"]: |
| cls = OpenNebula_3_2_NodeDriver |
| elif api_version in ["3.6"]: |
| cls = OpenNebula_3_6_NodeDriver |
| elif api_version in ["3.8"]: |
| cls = OpenNebula_3_8_NodeDriver |
| if "plain_auth" not in kwargs: |
| kwargs["plain_auth"] = cls.plain_auth |
| else: |
| cls.plain_auth = kwargs["plain_auth"] |
| else: |
| raise NotImplementedError( |
| "No OpenNebulaNodeDriver found for API version %s" % (api_version) |
| ) |
| return super().__new__(cls) |
| |
| def create_node(self, name, size, image, networks=None): |
| """ |
| Create a new OpenNebula node. |
| |
| @inherits: :class:`NodeDriver.create_node` |
| |
| :keyword networks: List of virtual networks to which this node should |
| connect. (optional) |
| :type networks: :class:`OpenNebulaNetwork` or |
| ``list`` of :class:`OpenNebulaNetwork` |
| """ |
| compute = ET.Element("COMPUTE") |
| |
| name = ET.SubElement(compute, "NAME") |
| name.text = name |
| |
| instance_type = ET.SubElement(compute, "INSTANCE_TYPE") |
| instance_type.text = size.name |
| |
| storage = ET.SubElement(compute, "STORAGE") |
| ET.SubElement(storage, "DISK", {"image": "%s" % (str(image.id))}) |
| |
| if networks: |
| if not isinstance(networks, list): |
| networks = [networks] |
| |
| networkGroup = ET.SubElement(compute, "NETWORK") |
| for network in networks: |
| if network.address: |
| ET.SubElement( |
| networkGroup, |
| "NIC", |
| {"network": "%s" % (str(network.id)), "ip": network.address}, |
| ) |
| else: |
| ET.SubElement(networkGroup, "NIC", {"network": "%s" % (str(network.id))}) |
| |
| xml = ET.tostring(compute) |
| node = self.connection.request("/compute", method="POST", data=xml).object |
| |
| return self._to_node(node) |
| |
| def destroy_node(self, node): |
| url = "/compute/%s" % (str(node.id)) |
| resp = self.connection.request(url, method="DELETE") |
| |
| return resp.status == httplib.OK |
| |
| def list_nodes(self): |
| return self._to_nodes(self.connection.request("/compute").object) |
| |
| def list_images(self, location=None): |
| return self._to_images(self.connection.request("/storage").object) |
| |
| def list_sizes(self, location=None): |
| """ |
| Return list of sizes on a provider. |
| |
| @inherits: :class:`NodeDriver.list_sizes` |
| |
| :return: List of compute node sizes supported by the cloud provider. |
| :rtype: ``list`` of :class:`OpenNebulaNodeSize` |
| """ |
| return [ |
| NodeSize( |
| id=1, |
| name="small", |
| ram=None, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| NodeSize( |
| id=2, |
| name="medium", |
| ram=None, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| NodeSize( |
| id=3, |
| name="large", |
| ram=None, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| ] |
| |
| def list_locations(self): |
| return [NodeLocation(0, "", "", self)] |
| |
| def ex_list_networks(self, location=None): |
| """ |
| List virtual networks on a provider. |
| |
| :param location: Location from which to request a list of virtual |
| networks. (optional) |
| :type location: :class:`NodeLocation` |
| |
| :return: List of virtual networks available to be connected to a |
| compute node. |
| :rtype: ``list`` of :class:`OpenNebulaNetwork` |
| """ |
| return self._to_networks(self.connection.request("/network").object) |
| |
| def ex_node_action(self, node, action): |
| """ |
| Build action representation and instruct node to commit action. |
| |
| Build action representation from the compute node ID, and the |
| action which should be carried out on that compute node. Then |
| instruct the node to carry out that action. |
| |
| :param node: Compute node instance. |
| :type node: :class:`Node` |
| |
| :param action: Action to be carried out on the compute node. |
| :type action: ``str`` |
| |
| :return: False if an HTTP Bad Request is received, else, True is |
| returned. |
| :rtype: ``bool`` |
| """ |
| compute_node_id = str(node.id) |
| |
| compute = ET.Element("COMPUTE") |
| |
| compute_id = ET.SubElement(compute, "ID") |
| compute_id.text = compute_node_id |
| |
| state = ET.SubElement(compute, "STATE") |
| state.text = action |
| |
| xml = ET.tostring(compute) |
| |
| url = "/compute/%s" % compute_node_id |
| resp = self.connection.request(url, method="PUT", data=xml) |
| |
| if resp.status == httplib.BAD_REQUEST: |
| return False |
| else: |
| return True |
| |
| def _to_images(self, object): |
| """ |
| Request a list of images and convert that list to a list of NodeImage |
| objects. |
| |
| Request a list of images from the OpenNebula web interface, and |
| issue a request to convert each XML object representation of an image |
| to a NodeImage object. |
| |
| :rtype: ``list`` of :class:`NodeImage` |
| :return: List of images. |
| """ |
| images = [] |
| for element in object.findall("DISK"): |
| image_id = element.attrib["href"].partition("/storage/")[2] |
| image = self.connection.request("/storage/%s" % (image_id)).object |
| images.append(self._to_image(image)) |
| |
| return images |
| |
| def _to_image(self, image): |
| """ |
| Take XML object containing an image description and convert to |
| NodeImage object. |
| |
| :type image: :class:`ElementTree` |
| :param image: XML representation of an image. |
| |
| :rtype: :class:`NodeImage` |
| :return: The newly extracted :class:`NodeImage`. |
| """ |
| return NodeImage( |
| id=image.findtext("ID"), |
| name=image.findtext("NAME"), |
| driver=self.connection.driver, |
| extra={"size": image.findtext("SIZE"), "url": image.findtext("URL")}, |
| ) |
| |
| def _to_networks(self, object): |
| """ |
| Request a list of networks and convert that list to a list of |
| OpenNebulaNetwork objects. |
| |
| Request a list of networks from the OpenNebula web interface, and |
| issue a request to convert each XML object representation of a network |
| to an OpenNebulaNetwork object. |
| |
| :rtype: ``list`` of :class:`OpenNebulaNetwork` |
| :return: List of virtual networks. |
| """ |
| networks = [] |
| for element in object.findall("NETWORK"): |
| network_id = element.attrib["href"].partition("/network/")[2] |
| network_element = self.connection.request("/network/%s" % (network_id)).object |
| networks.append(self._to_network(network_element)) |
| |
| return networks |
| |
| def _to_network(self, element): |
| """ |
| Take XML object containing a network description and convert to |
| OpenNebulaNetwork object. |
| |
| Take XML representation containing a network description and |
| convert to OpenNebulaNetwork object. |
| |
| :rtype: :class:`OpenNebulaNetwork` |
| :return: The newly extracted :class:`OpenNebulaNetwork`. |
| """ |
| return OpenNebulaNetwork( |
| id=element.findtext("ID"), |
| name=element.findtext("NAME"), |
| address=element.findtext("ADDRESS"), |
| size=element.findtext("SIZE"), |
| driver=self.connection.driver, |
| ) |
| |
| def _to_nodes(self, object): |
| """ |
| Request a list of compute nodes and convert that list to a list of |
| Node objects. |
| |
| Request a list of compute nodes from the OpenNebula web interface, and |
| issue a request to convert each XML object representation of a node |
| to a Node object. |
| |
| :rtype: ``list`` of :class:`Node` |
| :return: A list of compute nodes. |
| """ |
| computes = [] |
| for element in object.findall("COMPUTE"): |
| compute_id = element.attrib["href"].partition("/compute/")[2] |
| compute = self.connection.request("/compute/%s" % (compute_id)).object |
| computes.append(self._to_node(compute)) |
| |
| return computes |
| |
| def _to_node(self, compute): |
| """ |
| Take XML object containing a compute node description and convert to |
| Node object. |
| |
| Take XML representation containing a compute node description and |
| convert to Node object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: :class:`Node` |
| :return: The newly extracted :class:`Node`. |
| """ |
| try: |
| state = self.NODE_STATE_MAP[compute.findtext("STATE").upper()] |
| except KeyError: |
| state = NodeState.UNKNOWN |
| |
| return Node( |
| id=compute.findtext("ID"), |
| name=compute.findtext("NAME"), |
| state=state, |
| public_ips=self._extract_networks(compute), |
| private_ips=[], |
| driver=self.connection.driver, |
| image=self._extract_images(compute), |
| ) |
| |
| def _extract_networks(self, compute): |
| """ |
| Extract networks from a compute node XML representation. |
| |
| Extract network descriptions from a compute node XML representation, |
| converting each network to an OpenNebulaNetwork object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: ``list`` of :class:`OpenNebulaNetwork`s. |
| :return: List of virtual networks attached to the compute node. |
| """ |
| networks = list() |
| |
| network_list = compute.find("NETWORK") |
| for element in network_list.findall("NIC"): |
| networks.append( |
| OpenNebulaNetwork( |
| id=element.attrib.get("network", None), |
| name=None, |
| address=element.attrib.get("ip", None), |
| size=1, |
| driver=self.connection.driver, |
| ) |
| ) |
| |
| return networks |
| |
| def _extract_images(self, compute): |
| """ |
| Extract image disks from a compute node XML representation. |
| |
| Extract image disk descriptions from a compute node XML representation, |
| converting the disks to an NodeImage object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: :class:`NodeImage`. |
| :return: First disk attached to a compute node. |
| """ |
| disks = list() |
| |
| disk_list = compute.find("STORAGE") |
| if disk_list is not None: |
| for element in disk_list.findall("DISK"): |
| disks.append( |
| NodeImage( |
| id=element.attrib.get("image", None), |
| name=None, |
| driver=self.connection.driver, |
| extra={"dev": element.attrib.get("dev", None)}, |
| ) |
| ) |
| |
| # @TODO: Return all disks when the Node type accepts multiple |
| # attached disks per node. |
| if len(disks) > 0: |
| return disks[0] |
| else: |
| return None |
| |
| |
| class OpenNebula_1_4_NodeDriver(OpenNebulaNodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v1.4. |
| """ |
| |
| name = "OpenNebula (v1.4)" |
| |
| |
| class OpenNebula_2_0_NodeDriver(OpenNebulaNodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v2.0 through OpenNebula.org |
| v2.2. |
| """ |
| |
| name = "OpenNebula (v2.0 - v2.2)" |
| |
| def create_node(self, name, size, image, networks=None, context=None): |
| """ |
| Create a new OpenNebula node. |
| |
| @inherits: :class:`NodeDriver.create_node` |
| |
| :keyword networks: List of virtual networks to which this node should |
| connect. (optional) |
| :type networks: :class:`OpenNebulaNetwork` or ``list`` |
| of :class:`OpenNebulaNetwork` |
| |
| :keyword context: Custom (key, value) pairs to be injected into |
| compute node XML description. (optional) |
| :type context: ``dict`` |
| |
| :return: Instance of a newly created node. |
| :rtype: :class:`Node` |
| """ |
| compute = ET.Element("COMPUTE") |
| |
| name = ET.SubElement(compute, "NAME") |
| name.text = name |
| |
| instance_type = ET.SubElement(compute, "INSTANCE_TYPE") |
| instance_type.text = size.name |
| |
| disk = ET.SubElement(compute, "DISK") |
| ET.SubElement(disk, "STORAGE", {"href": "/storage/%s" % (str(image.id))}) |
| |
| if networks: |
| if not isinstance(networks, list): |
| networks = [networks] |
| |
| for network in networks: |
| nic = ET.SubElement(compute, "NIC") |
| ET.SubElement(nic, "NETWORK", {"href": "/network/%s" % (str(network.id))}) |
| if network.address: |
| ip_line = ET.SubElement(nic, "IP") |
| ip_line.text = network.address |
| |
| if context and isinstance(context, dict): |
| contextGroup = ET.SubElement(compute, "CONTEXT") |
| for key, value in list(context.items()): |
| context = ET.SubElement(contextGroup, key.upper()) |
| context.text = value |
| |
| xml = ET.tostring(compute) |
| node = self.connection.request("/compute", method="POST", data=xml).object |
| |
| return self._to_node(node) |
| |
| def destroy_node(self, node): |
| url = "/compute/%s" % (str(node.id)) |
| resp = self.connection.request(url, method="DELETE") |
| |
| return resp.status == httplib.NO_CONTENT |
| |
| def list_sizes(self, location=None): |
| """ |
| Return list of sizes on a provider. |
| |
| @inherits: :class:`NodeDriver.list_sizes` |
| |
| :return: List of compute node sizes supported by the cloud provider. |
| :rtype: ``list`` of :class:`OpenNebulaNodeSize` |
| """ |
| return [ |
| OpenNebulaNodeSize( |
| id=1, |
| name="small", |
| ram=1024, |
| cpu=1, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| OpenNebulaNodeSize( |
| id=2, |
| name="medium", |
| ram=4096, |
| cpu=4, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| OpenNebulaNodeSize( |
| id=3, |
| name="large", |
| ram=8192, |
| cpu=8, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| OpenNebulaNodeSize( |
| id=4, |
| name="custom", |
| ram=0, |
| cpu=0, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self, |
| ), |
| ] |
| |
| def _to_images(self, object): |
| """ |
| Request a list of images and convert that list to a list of NodeImage |
| objects. |
| |
| Request a list of images from the OpenNebula web interface, and |
| issue a request to convert each XML object representation of an image |
| to a NodeImage object. |
| |
| :rtype: ``list`` of :class:`NodeImage` |
| :return: List of images. |
| """ |
| images = [] |
| for element in object.findall("STORAGE"): |
| image_id = element.attrib["href"].partition("/storage/")[2] |
| image = self.connection.request("/storage/%s" % (image_id)).object |
| images.append(self._to_image(image)) |
| |
| return images |
| |
| def _to_image(self, image): |
| """ |
| Take XML object containing an image description and convert to |
| NodeImage object. |
| |
| :type image: :class:`ElementTree` |
| :param image: XML representation of an image. |
| |
| :rtype: :class:`NodeImage` |
| :return: The newly extracted :class:`NodeImage`. |
| """ |
| return NodeImage( |
| id=image.findtext("ID"), |
| name=image.findtext("NAME"), |
| driver=self.connection.driver, |
| extra={ |
| "description": image.findtext("DESCRIPTION"), |
| "type": image.findtext("TYPE"), |
| "size": image.findtext("SIZE"), |
| "fstype": image.findtext("FSTYPE", None), |
| }, |
| ) |
| |
| def _to_node(self, compute): |
| """ |
| Take XML object containing a compute node description and convert to |
| Node object. |
| |
| Take XML representation containing a compute node description and |
| convert to Node object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: :class:`Node` |
| :return: The newly extracted :class:`Node`. |
| """ |
| try: |
| state = self.NODE_STATE_MAP[compute.findtext("STATE").upper()] |
| except KeyError: |
| state = NodeState.UNKNOWN |
| |
| return Node( |
| id=compute.findtext("ID"), |
| name=compute.findtext("NAME"), |
| state=state, |
| public_ips=self._extract_networks(compute), |
| private_ips=[], |
| driver=self.connection.driver, |
| image=self._extract_images(compute), |
| size=self._extract_size(compute), |
| extra={"context": self._extract_context(compute)}, |
| ) |
| |
| def _extract_networks(self, compute): |
| """ |
| Extract networks from a compute node XML representation. |
| |
| Extract network descriptions from a compute node XML representation, |
| converting each network to an OpenNebulaNetwork object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: ``list`` of :class:`OpenNebulaNetwork` |
| :return: List of virtual networks attached to the compute node. |
| """ |
| networks = [] |
| |
| for element in compute.findall("NIC"): |
| network = element.find("NETWORK") |
| network_id = network.attrib["href"].partition("/network/")[2] |
| |
| networks.append( |
| OpenNebulaNetwork( |
| id=network_id, |
| name=network.attrib.get("name", None), |
| address=element.findtext("IP"), |
| size=1, |
| driver=self.connection.driver, |
| extra={"mac": element.findtext("MAC")}, |
| ) |
| ) |
| |
| return networks |
| |
| def _extract_images(self, compute): |
| """ |
| Extract image disks from a compute node XML representation. |
| |
| Extract image disk descriptions from a compute node XML representation, |
| converting the disks to an NodeImage object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: ``list`` of :class:`NodeImage` |
| :return: Disks attached to a compute node. |
| """ |
| disks = list() |
| |
| for element in compute.findall("DISK"): |
| disk = element.find("STORAGE") |
| image_id = disk.attrib["href"].partition("/storage/")[2] |
| |
| if "id" in element.attrib: |
| disk_id = element.attrib["id"] |
| else: |
| disk_id = None |
| |
| disks.append( |
| NodeImage( |
| id=image_id, |
| name=disk.attrib.get("name", None), |
| driver=self.connection.driver, |
| extra={ |
| "type": element.findtext("TYPE"), |
| "disk_id": disk_id, |
| "target": element.findtext("TARGET"), |
| }, |
| ) |
| ) |
| |
| # Return all disks when the Node type accepts multiple attached disks |
| # per node. |
| if len(disks) > 1: |
| return disks |
| elif len(disks) == 1: |
| return disks[0] |
| else: |
| return None |
| |
| def _extract_size(self, compute): |
| """ |
| Extract size, or node type, from a compute node XML representation. |
| |
| Extract node size, or node type, description from a compute node XML |
| representation, converting the node size to a NodeSize object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: :class:`OpenNebulaNodeSize` |
| :return: Node type of compute node. |
| """ |
| instance_type = compute.find("INSTANCE_TYPE") |
| |
| try: |
| return next( |
| node_size for node_size in self.list_sizes() if node_size.name == instance_type.text |
| ) |
| except StopIteration: |
| return None |
| |
| def _extract_context(self, compute): |
| """ |
| Extract size, or node type, from a compute node XML representation. |
| |
| Extract node size, or node type, description from a compute node XML |
| representation, converting the node size to a NodeSize object. |
| |
| :type compute: :class:`ElementTree` |
| :param compute: XML representation of a compute node. |
| |
| :rtype: ``dict`` |
| :return: Dictionary containing (key, value) pairs related to |
| compute node context. |
| """ |
| contexts = dict() |
| context = compute.find("CONTEXT") |
| |
| if context is not None: |
| for context_element in list(context): |
| contexts[context_element.tag.lower()] = context_element.text |
| |
| return contexts |
| |
| |
| class OpenNebula_3_0_NodeDriver(OpenNebula_2_0_NodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v3.0. |
| """ |
| |
| name = "OpenNebula (v3.0)" |
| |
| def ex_node_set_save_name(self, node, name): |
| """ |
| Build action representation and instruct node to commit action. |
| |
| Build action representation from the compute node ID, the disk image |
| which will be saved, and the name under which the image will be saved |
| upon shutting down the compute node. |
| |
| :param node: Compute node instance. |
| :type node: :class:`Node` |
| |
| :param name: Name under which the image should be saved after shutting |
| down the compute node. |
| :type name: ``str`` |
| |
| :return: False if an HTTP Bad Request is received, else, True is |
| returned. |
| :rtype: ``bool`` |
| """ |
| compute_node_id = str(node.id) |
| |
| compute = ET.Element("COMPUTE") |
| |
| compute_id = ET.SubElement(compute, "ID") |
| compute_id.text = compute_node_id |
| |
| disk = ET.SubElement(compute, "DISK", {"id": str(node.image.id)}) |
| |
| ET.SubElement( |
| disk, |
| "STORAGE", |
| {"href": "/storage/%s" % (str(node.image.id)), "name": node.image.name}, |
| ) |
| |
| ET.SubElement(disk, "SAVE_AS", {"name": str(name)}) |
| |
| xml = ET.tostring(compute) |
| |
| url = "/compute/%s" % compute_node_id |
| resp = self.connection.request(url, method="PUT", data=xml) |
| |
| if resp.status == httplib.BAD_REQUEST: |
| return False |
| else: |
| return True |
| |
| def _to_network(self, element): |
| """ |
| Take XML object containing a network description and convert to |
| OpenNebulaNetwork object. |
| |
| Take XML representation containing a network description and |
| convert to OpenNebulaNetwork object. |
| |
| :return: The newly extracted :class:`OpenNebulaNetwork`. |
| :rtype: :class:`OpenNebulaNetwork` |
| """ |
| return OpenNebulaNetwork( |
| id=element.findtext("ID"), |
| name=element.findtext("NAME"), |
| address=element.findtext("ADDRESS"), |
| size=element.findtext("SIZE"), |
| driver=self.connection.driver, |
| extra={"public": element.findtext("PUBLIC")}, |
| ) |
| |
| |
| class OpenNebula_3_2_NodeDriver(OpenNebula_3_0_NodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v3.2. |
| """ |
| |
| name = "OpenNebula (v3.2)" |
| |
| def reboot_node(self, node): |
| return self.ex_node_action(node, ACTION.REBOOT) |
| |
| def list_sizes(self, location=None): |
| """ |
| Return list of sizes on a provider. |
| |
| @inherits: :class:`NodeDriver.list_sizes` |
| |
| :return: List of compute node sizes supported by the cloud provider. |
| :rtype: ``list`` of :class:`OpenNebulaNodeSize` |
| """ |
| return self._to_sizes(self.connection.request("/instance_type").object) |
| |
| def _to_sizes(self, object): |
| """ |
| Request a list of instance types and convert that list to a list of |
| OpenNebulaNodeSize objects. |
| |
| Request a list of instance types from the OpenNebula web interface, |
| and issue a request to convert each XML object representation of an |
| instance type to an OpenNebulaNodeSize object. |
| |
| :return: List of instance types. |
| :rtype: ``list`` of :class:`OpenNebulaNodeSize` |
| """ |
| sizes = [] |
| size_id = 1 |
| |
| attributes = [ |
| ("name", str, None), |
| ("ram", int, "MEMORY"), |
| ("cpu", float, None), |
| ("vcpu", float, None), |
| ("disk", str, None), |
| ("bandwidth", float, None), |
| ("price", float, None), |
| ] |
| |
| for element in object.findall("INSTANCE_TYPE"): |
| size_kwargs = {"id": size_id, "driver": self} |
| values = self._get_attributes_values(attributes=attributes, element=element) |
| size_kwargs.update(values) |
| |
| size = OpenNebulaNodeSize(**size_kwargs) |
| sizes.append(size) |
| size_id += 1 |
| |
| return sizes |
| |
| def _get_attributes_values(self, attributes, element): |
| values = {} |
| |
| for attribute_name, attribute_type, alias in attributes: |
| key = alias if alias else attribute_name.upper() |
| value = element.findtext(key) |
| |
| if value is not None: |
| value = attribute_type(value) |
| |
| values[attribute_name] = value |
| |
| return values |
| |
| |
| class OpenNebula_3_6_NodeDriver(OpenNebula_3_2_NodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v3.6. |
| """ |
| |
| name = "OpenNebula (v3.6)" |
| |
| def create_volume(self, size, name, location=None, snapshot=None): |
| storage = ET.Element("STORAGE") |
| |
| vol_name = ET.SubElement(storage, "NAME") |
| vol_name.text = name |
| |
| vol_type = ET.SubElement(storage, "TYPE") |
| vol_type.text = "DATABLOCK" |
| |
| description = ET.SubElement(storage, "DESCRIPTION") |
| description.text = "Attached storage" |
| |
| public = ET.SubElement(storage, "PUBLIC") |
| public.text = "NO" |
| |
| persistent = ET.SubElement(storage, "PERSISTENT") |
| persistent.text = "YES" |
| |
| fstype = ET.SubElement(storage, "FSTYPE") |
| fstype.text = "ext3" |
| |
| vol_size = ET.SubElement(storage, "SIZE") |
| vol_size.text = str(size) |
| |
| xml = ET.tostring(storage) |
| volume = self.connection.request("/storage", {"occixml": xml}, method="POST").object |
| |
| return self._to_volume(volume) |
| |
| def destroy_volume(self, volume): |
| url = "/storage/%s" % (str(volume.id)) |
| resp = self.connection.request(url, method="DELETE") |
| |
| return resp.status == httplib.NO_CONTENT |
| |
| def attach_volume(self, node, volume, device): |
| action = ET.Element("ACTION") |
| |
| perform = ET.SubElement(action, "PERFORM") |
| perform.text = "ATTACHDISK" |
| |
| params = ET.SubElement(action, "PARAMS") |
| |
| ET.SubElement(params, "STORAGE", {"href": "/storage/%s" % (str(volume.id))}) |
| |
| target = ET.SubElement(params, "TARGET") |
| target.text = device |
| |
| xml = ET.tostring(action) |
| |
| url = "/compute/%s/action" % node.id |
| |
| resp = self.connection.request(url, method="POST", data=xml) |
| return resp.status == httplib.ACCEPTED |
| |
| def _do_detach_volume(self, node_id, disk_id): |
| action = ET.Element("ACTION") |
| |
| perform = ET.SubElement(action, "PERFORM") |
| perform.text = "DETACHDISK" |
| |
| params = ET.SubElement(action, "PARAMS") |
| |
| ET.SubElement(params, "DISK", {"id": disk_id}) |
| |
| xml = ET.tostring(action) |
| |
| url = "/compute/%s/action" % node_id |
| |
| resp = self.connection.request(url, method="POST", data=xml) |
| return resp.status == httplib.ACCEPTED |
| |
| def detach_volume(self, volume): |
| # We need to find the node using this volume |
| for node in self.list_nodes(): |
| if type(node.image) is not list: |
| # This node has only one associated image. It is not the one we |
| # are after. |
| continue |
| |
| for disk in node.image: |
| if disk.id == volume.id: |
| # Node found. We can now detach the volume |
| disk_id = disk.extra["disk_id"] |
| return self._do_detach_volume(node.id, disk_id) |
| |
| return False |
| |
| def list_volumes(self): |
| return self._to_volumes(self.connection.request("/storage").object) |
| |
| def _to_volume(self, storage): |
| return StorageVolume( |
| id=storage.findtext("ID"), |
| name=storage.findtext("NAME"), |
| size=int(storage.findtext("SIZE")), |
| driver=self.connection.driver, |
| ) |
| |
| def _to_volumes(self, object): |
| volumes = [] |
| for storage in object.findall("STORAGE"): |
| storage_id = storage.attrib["href"].partition("/storage/")[2] |
| |
| volumes.append( |
| self._to_volume(self.connection.request("/storage/%s" % storage_id).object) |
| ) |
| |
| return volumes |
| |
| |
| class OpenNebula_3_8_NodeDriver(OpenNebula_3_6_NodeDriver): |
| """ |
| OpenNebula.org node driver for OpenNebula.org v3.8. |
| """ |
| |
| name = "OpenNebula (v3.8)" |
| plain_auth = API_PLAIN_AUTH |
| |
| def _to_sizes(self, object): |
| """ |
| Request a list of instance types and convert that list to a list of |
| OpenNebulaNodeSize objects. |
| |
| Request a list of instance types from the OpenNebula web interface, |
| and issue a request to convert each XML object representation of an |
| instance type to an OpenNebulaNodeSize object. |
| |
| :return: List of instance types. |
| :rtype: ``list`` of :class:`OpenNebulaNodeSize` |
| """ |
| sizes = [] |
| size_id = 1 |
| |
| attributes = [ |
| ("name", str, None), |
| ("ram", int, "MEMORY"), |
| ("cpu", float, None), |
| ("vcpu", float, None), |
| ("disk", str, None), |
| ("bandwidth", float, None), |
| ("price", float, None), |
| ] |
| |
| for element in object.findall("INSTANCE_TYPE"): |
| element = self.connection.request( |
| ("/instance_type/%s") % (element.attrib["name"]) |
| ).object |
| |
| size_kwargs = {"id": size_id, "driver": self} |
| values = self._get_attributes_values(attributes=attributes, element=element) |
| size_kwargs.update(values) |
| |
| size = OpenNebulaNodeSize(**size_kwargs) |
| sizes.append(size) |
| size_id += 1 |
| return sizes |
| |
| def _ex_connection_class_kwargs(self): |
| """ |
| Set plain_auth as an extra :class:`OpenNebulaConnection_3_8` argument |
| |
| :return: ``dict`` of :class:`OpenNebulaConnection_3_8` input arguments |
| """ |
| |
| return {"plain_auth": self.plain_auth} |