# 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()  # nosec
        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 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()  # nosec

    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}
