"""
Nova class
"""

import inspect
import logging
import time

import salt.utils.cloud
import salt.utils.files
from salt.exceptions import SaltCloudSystemExit
from salt.utils.versions import Version

HAS_NOVA = False
# pylint: disable=import-error
try:
    import novaclient
    import novaclient.auth_plugin
    import novaclient.base
    import novaclient.exceptions
    import novaclient.extension
    import novaclient.utils
    from novaclient import client
    from novaclient.shell import OpenStackComputeShell

    HAS_NOVA = True
except ImportError:
    pass

HAS_KEYSTONEAUTH = False
try:
    import keystoneauth1.loading
    import keystoneauth1.session

    HAS_KEYSTONEAUTH = True
except ImportError:
    pass
# pylint: enable=import-error


# Get logging started
log = logging.getLogger(__name__)

# Version added to novaclient.client.Client function
NOVACLIENT_MINVER = "2.6.1"
NOVACLIENT_MAXVER = "6.0.1"

# dict for block_device_mapping_v2
CLIENT_BDM2_KEYS = {
    "id": "uuid",
    "source": "source_type",
    "dest": "destination_type",
    "bus": "disk_bus",
    "device": "device_name",
    "size": "volume_size",
    "format": "guest_format",
    "bootindex": "boot_index",
    "type": "device_type",
    "shutdown": "delete_on_termination",
}


def check_nova():
    if HAS_NOVA:
        novaclient_ver = Version(novaclient.__version__)
        min_ver = Version(NOVACLIENT_MINVER)
        max_ver = Version(NOVACLIENT_MAXVER)
        if min_ver <= novaclient_ver <= max_ver:
            return HAS_NOVA
        elif novaclient_ver > max_ver:
            log.debug(
                "Older novaclient version required. Maximum: %s", NOVACLIENT_MAXVER
            )
            return False
        log.debug("Newer novaclient version required.  Minimum: %s", NOVACLIENT_MINVER)
    return False


if check_nova():
    try:
        import novaclient.auth_plugin
    except ImportError:
        log.debug(
            "Using novaclient version 7.0.0 or newer. Authentication "
            "plugin auth_plugin.py is not available anymore."
        )


# kwargs has to be an object instead of a dictionary for the __post_parse_arg__
class KwargsStruct:
    def __init__(self, **entries):
        self.__dict__.update(entries)


def _parse_block_device_mapping_v2(
    block_device=None, boot_volume=None, snapshot=None, ephemeral=None, swap=None
):
    bdm = []
    if block_device is None:
        block_device = []
    if ephemeral is None:
        ephemeral = []

    if boot_volume is not None:
        bdm_dict = {
            "uuid": boot_volume,
            "source_type": "volume",
            "destination_type": "volume",
            "boot_index": 0,
            "delete_on_termination": False,
        }
        bdm.append(bdm_dict)

    if snapshot is not None:
        bdm_dict = {
            "uuid": snapshot,
            "source_type": "snapshot",
            "destination_type": "volume",
            "boot_index": 0,
            "delete_on_termination": False,
        }
        bdm.append(bdm_dict)

    for device_spec in block_device:
        bdm_dict = {}

        for key, value in device_spec.items():
            bdm_dict[CLIENT_BDM2_KEYS[key]] = value

        # Convert the delete_on_termination to a boolean or set it to true by
        # default for local block devices when not specified.
        if "delete_on_termination" in bdm_dict:
            action = bdm_dict["delete_on_termination"]
            bdm_dict["delete_on_termination"] = action == "remove"
        elif bdm_dict.get("destination_type") == "local":
            bdm_dict["delete_on_termination"] = True

        bdm.append(bdm_dict)

    for ephemeral_spec in ephemeral:
        bdm_dict = {
            "source_type": "blank",
            "destination_type": "local",
            "boot_index": -1,
            "delete_on_termination": True,
        }
        if "size" in ephemeral_spec:
            bdm_dict["volume_size"] = ephemeral_spec["size"]
        if "format" in ephemeral_spec:
            bdm_dict["guest_format"] = ephemeral_spec["format"]

        bdm.append(bdm_dict)

    if swap is not None:
        bdm_dict = {
            "source_type": "blank",
            "destination_type": "local",
            "boot_index": -1,
            "delete_on_termination": True,
            "guest_format": "swap",
            "volume_size": swap,
        }
        bdm.append(bdm_dict)

    return bdm


class NovaServer:
    def __init__(self, name, server, password=None):
        """
        Make output look like libcloud output for consistency
        """
        self.name = name
        self.id = server["id"]
        self.image = server.get("image", {}).get("id", "Boot From Volume")
        self.size = server["flavor"]["id"]
        self.state = server["state"]
        self._uuid = None
        self.extra = {"metadata": server["metadata"], "access_ip": server["accessIPv4"]}

        self.addresses = server.get("addresses", {})
        self.public_ips, self.private_ips = [], []
        self.fixed_ips, self.floating_ips = [], []
        for network in self.addresses.values():
            for addr in network:
                if salt.utils.cloud.is_public_ip(addr["addr"]):
                    self.public_ips.append(addr["addr"])
                else:
                    self.private_ips.append(addr["addr"])
                if addr.get("OS-EXT-IPS:type") == "floating":
                    self.floating_ips.append(addr["addr"])
                else:
                    self.fixed_ips.append(addr["addr"])

        if password:
            self.extra["password"] = password

    def __str__(self):
        return str(self.__dict__)


def get_entry(dict_, key, value, raise_error=True):
    for entry in dict_:
        if entry[key] == value:
            return entry
    if raise_error is True:
        raise SaltCloudSystemExit(f"Unable to find {key} in {dict_}.")
    return {}


def get_entry_multi(dict_, pairs, raise_error=True):
    for entry in dict_:
        if all([entry[key] == value for key, value in pairs]):
            return entry
    if raise_error is True:
        raise SaltCloudSystemExit(f"Unable to find {pairs} in {dict_}.")
    return {}


def get_endpoint_url_v3(catalog, service_type, region_name):
    for service_entry in catalog:
        if service_entry["type"] == service_type:
            for endpoint_entry in service_entry["endpoints"]:
                if (
                    endpoint_entry["region"] == region_name
                    and endpoint_entry["interface"] == "public"
                ):
                    return endpoint_entry["url"]
    return None


def sanatize_novaclient(kwargs):
    variables = (
        "username",
        "api_key",
        "project_id",
        "auth_url",
        "insecure",
        "timeout",
        "proxy_tenant_id",
        "proxy_token",
        "region_name",
        "endpoint_type",
        "extensions",
        "service_type",
        "service_name",
        "volume_service_name",
        "timings",
        "bypass_url",
        "os_cache",
        "no_cache",
        "http_log_debug",
        "auth_system",
        "auth_plugin",
        "auth_token",
        "cacert",
        "tenant_id",
    )
    ret = {}
    for var in kwargs:
        if var in variables:
            ret[var] = kwargs[var]

    return ret


# Function alias to not shadow built-ins
class SaltNova:
    """
    Class for all novaclient functions
    """

    extensions = []

    def __init__(
        self,
        username,
        project_id,
        auth_url,
        region_name=None,
        password=None,
        os_auth_plugin=None,
        use_keystoneauth=False,
        **kwargs,
    ):
        """
        Set up nova credentials
        """
        if all([use_keystoneauth, HAS_KEYSTONEAUTH]):
            self._new_init(
                username=username,
                project_id=project_id,
                auth_url=auth_url,
                region_name=region_name,
                password=password,
                os_auth_plugin=os_auth_plugin,
                **kwargs,
            )
        else:
            self._old_init(
                username=username,
                project_id=project_id,
                auth_url=auth_url,
                region_name=region_name,
                password=password,
                os_auth_plugin=os_auth_plugin,
                **kwargs,
            )

    def _new_init(
        self,
        username,
        project_id,
        auth_url,
        region_name,
        password,
        os_auth_plugin,
        auth=None,
        verify=True,
        **kwargs,
    ):
        if auth is None:
            auth = {}

        loader = keystoneauth1.loading.get_plugin_loader(os_auth_plugin or "password")

        self.client_kwargs = kwargs.copy()
        self.kwargs = auth.copy()
        if not self.extensions:
            if hasattr(OpenStackComputeShell, "_discover_extensions"):
                self.extensions = OpenStackComputeShell()._discover_extensions("2.0")
            else:
                self.extensions = client.discover_extensions("2.0")
            for extension in self.extensions:
                extension.run_hooks("__pre_parse_args__")
            self.client_kwargs["extensions"] = self.extensions

        self.kwargs["username"] = username
        self.kwargs["project_name"] = project_id
        self.kwargs["auth_url"] = auth_url
        self.kwargs["password"] = password
        if auth_url.endswith("3"):
            self.kwargs["user_domain_name"] = kwargs.get("user_domain_name", "default")
            self.kwargs["project_domain_name"] = kwargs.get(
                "project_domain_name", "default"
            )

        self.client_kwargs["region_name"] = region_name
        self.client_kwargs["service_type"] = "compute"

        if hasattr(self, "extensions"):
            # needs an object, not a dictionary
            self.kwargstruct = KwargsStruct(**self.client_kwargs)
            for extension in self.extensions:
                extension.run_hooks("__post_parse_args__", self.kwargstruct)
            self.client_kwargs = self.kwargstruct.__dict__

        # Requires novaclient version >= 2.6.1
        self.version = str(kwargs.get("version", 2))

        self.client_kwargs = sanatize_novaclient(self.client_kwargs)
        options = loader.load_from_options(**self.kwargs)
        self.session = keystoneauth1.session.Session(auth=options, verify=verify)
        conn = client.Client(
            version=self.version, session=self.session, **self.client_kwargs
        )
        self.kwargs["auth_token"] = conn.client.session.get_token()
        identity_service_type = kwargs.get("identity_service_type", "identity")
        self.catalog = (
            conn.client.session.get(
                "/auth/catalog", endpoint_filter={"service_type": identity_service_type}
            )
            .json()
            .get("catalog", [])
        )
        if conn.client.get_endpoint(service_type=identity_service_type).endswith("v3"):
            self._v3_setup(region_name)
        else:
            self._v2_setup(region_name)

    def _old_init(
        self,
        username,
        project_id,
        auth_url,
        region_name,
        password,
        os_auth_plugin,
        **kwargs,
    ):
        self.kwargs = kwargs.copy()
        if not self.extensions:
            if hasattr(OpenStackComputeShell, "_discover_extensions"):
                self.extensions = OpenStackComputeShell()._discover_extensions("2.0")
            else:
                self.extensions = client.discover_extensions("2.0")
            for extension in self.extensions:
                extension.run_hooks("__pre_parse_args__")
            self.kwargs["extensions"] = self.extensions

        self.kwargs["username"] = username
        self.kwargs["project_id"] = project_id
        self.kwargs["auth_url"] = auth_url
        self.kwargs["region_name"] = region_name
        self.kwargs["service_type"] = "compute"

        # used in novaclient extensions to see if they are rackspace or not, to know if it needs to load
        # the hooks for that extension or not.  This is cleaned up by sanatize_novaclient
        self.kwargs["os_auth_url"] = auth_url

        if os_auth_plugin is not None:
            novaclient.auth_plugin.discover_auth_systems()
            auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_plugin)
            self.kwargs["auth_plugin"] = auth_plugin
            self.kwargs["auth_system"] = os_auth_plugin

        if not self.kwargs.get("api_key", None):
            self.kwargs["api_key"] = password

        # This has to be run before sanatize_novaclient before extra variables are cleaned out.
        if hasattr(self, "extensions"):
            # needs an object, not a dictionary
            self.kwargstruct = KwargsStruct(**self.kwargs)
            for extension in self.extensions:
                extension.run_hooks("__post_parse_args__", self.kwargstruct)
            self.kwargs = self.kwargstruct.__dict__

        self.kwargs = sanatize_novaclient(self.kwargs)

        # Requires novaclient version >= 2.6.1
        self.kwargs["version"] = str(kwargs.get("version", 2))

        conn = client.Client(**self.kwargs)
        try:
            conn.client.authenticate()
        except novaclient.exceptions.AmbiguousEndpoints:
            raise SaltCloudSystemExit(
                "Nova provider requires a 'region_name' to be specified"
            )

        self.kwargs["auth_token"] = conn.client.auth_token
        self.catalog = conn.client.service_catalog.catalog["access"]["serviceCatalog"]

        self._v2_setup(region_name)

    def _v3_setup(self, region_name):
        if region_name is not None:
            self.client_kwargs["bypass_url"] = get_endpoint_url_v3(
                self.catalog, "compute", region_name
            )
            log.debug("Using Nova bypass_url: %s", self.client_kwargs["bypass_url"])

        self.compute_conn = client.Client(
            version=self.version, session=self.session, **self.client_kwargs
        )

        volume_endpoints = get_entry(
            self.catalog, "type", "volume", raise_error=False
        ).get("endpoints", {})
        if volume_endpoints:
            if region_name is not None:
                self.client_kwargs["bypass_url"] = get_endpoint_url_v3(
                    self.catalog, "volume", region_name
                )
                log.debug(
                    "Using Cinder bypass_url: %s", self.client_kwargs["bypass_url"]
                )

            self.volume_conn = client.Client(
                version=self.version, session=self.session, **self.client_kwargs
            )
            if hasattr(self, "extensions"):
                self.expand_extensions()
        else:
            self.volume_conn = None

    def _v2_setup(self, region_name):
        if region_name is not None:
            servers_endpoints = get_entry(self.catalog, "type", "compute")["endpoints"]
            self.kwargs["bypass_url"] = get_entry(
                servers_endpoints, "region", region_name
            )["publicURL"]

        self.compute_conn = client.Client(**self.kwargs)

        volume_endpoints = get_entry(
            self.catalog, "type", "volume", raise_error=False
        ).get("endpoints", {})
        if volume_endpoints:
            if region_name is not None:
                self.kwargs["bypass_url"] = get_entry(
                    volume_endpoints, "region", region_name
                )["publicURL"]

            self.volume_conn = client.Client(**self.kwargs)
            if hasattr(self, "extensions"):
                self.expand_extensions()
        else:
            self.volume_conn = None

    def expand_extensions(self):
        for connection in (self.compute_conn, self.volume_conn):
            if connection is None:
                continue
            for extension in self.extensions:
                for attr in extension.module.__dict__:
                    if not inspect.isclass(getattr(extension.module, attr)):
                        continue
                    for key, value in connection.__dict__.items():
                        if not isinstance(value, novaclient.base.Manager):
                            continue
                        if value.__class__.__name__ == attr:
                            setattr(
                                connection, key, extension.manager_class(connection)
                            )

    def get_catalog(self):
        """
        Return service catalog
        """
        return self.catalog

    def server_show_libcloud(self, uuid):
        """
        Make output look like libcloud output for consistency
        """
        server_info = self.server_show(uuid)
        server = next(iter(server_info.values()))
        server_name = next(iter(server_info.keys()))
        if not hasattr(self, "password"):
            self.password = None
        ret = NovaServer(server_name, server, self.password)

        return ret

    def boot(self, name, flavor_id=0, image_id=0, timeout=300, **kwargs):
        """
        Boot a cloud server.
        """
        nt_ks = self.compute_conn
        kwargs["name"] = name
        kwargs["flavor"] = flavor_id
        kwargs["image"] = image_id or None
        ephemeral = kwargs.pop("ephemeral", [])
        block_device = kwargs.pop("block_device", [])
        boot_volume = kwargs.pop("boot_volume", None)
        snapshot = kwargs.pop("snapshot", None)
        swap = kwargs.pop("swap", None)
        kwargs["block_device_mapping_v2"] = _parse_block_device_mapping_v2(
            block_device=block_device,
            boot_volume=boot_volume,
            snapshot=snapshot,
            ephemeral=ephemeral,
            swap=swap,
        )
        response = nt_ks.servers.create(**kwargs)
        self.uuid = response.id
        self.password = getattr(response, "adminPass", None)

        start = time.time()
        trycount = 0
        while True:
            trycount += 1
            try:
                return self.server_show_libcloud(self.uuid)
            except Exception as exc:  # pylint: disable=broad-except
                log.debug("Server information not yet available: %s", exc)
                time.sleep(1)
                if time.time() - start > timeout:
                    log.error(
                        "Timed out after %s seconds while waiting for data", timeout
                    )
                    return False

                log.debug("Retrying server_show() (try %s)", trycount)

    def show_instance(self, name):
        """
        Find a server by its name (libcloud)
        """
        return self.server_by_name(name)

    def root_password(self, server_id, password):
        """
        Change server(uuid's) root password
        """
        nt_ks = self.compute_conn
        nt_ks.servers.change_password(server_id, password)

    def server_by_name(self, name):
        """
        Find a server by its name
        """
        return self.server_show_libcloud(self.server_list().get(name, {}).get("id", ""))

    def _volume_get(self, volume_id):
        """
        Organize information about a volume from the volume_id
        """
        if self.volume_conn is None:
            raise SaltCloudSystemExit("No cinder endpoint available")
        nt_ks = self.volume_conn
        volume = nt_ks.volumes.get(volume_id)
        response = {
            "name": volume.display_name,
            "size": volume.size,
            "id": volume.id,
            "description": volume.display_description,
            "attachments": volume.attachments,
            "status": volume.status,
        }
        return response

    def volume_list(self, search_opts=None):
        """
        List all block volumes
        """
        if self.volume_conn is None:
            raise SaltCloudSystemExit("No cinder endpoint available")
        nt_ks = self.volume_conn
        volumes = nt_ks.volumes.list(search_opts=search_opts)
        response = {}
        for volume in volumes:
            response[volume.display_name] = {
                "name": volume.display_name,
                "size": volume.size,
                "id": volume.id,
                "description": volume.display_description,
                "attachments": volume.attachments,
                "status": volume.status,
            }
        return response

    def volume_show(self, name):
        """
        Show one volume
        """
        if self.volume_conn is None:
            raise SaltCloudSystemExit("No cinder endpoint available")

        volumes = self.volume_list(
            search_opts={"display_name": name},
        )
        volume = volumes[name]

        return volume

    def volume_create(
        self, name, size=100, snapshot=None, voltype=None, availability_zone=None
    ):
        """
        Create a block device
        """
        if self.volume_conn is None:
            raise SaltCloudSystemExit("No cinder endpoint available")
        nt_ks = self.volume_conn
        response = nt_ks.volumes.create(
            size=size,
            display_name=name,
            volume_type=voltype,
            snapshot_id=snapshot,
            availability_zone=availability_zone,
        )

        return self._volume_get(response.id)

    def volume_delete(self, name):
        """
        Delete a block device
        """
        if self.volume_conn is None:
            raise SaltCloudSystemExit("No cinder endpoint available")
        nt_ks = self.volume_conn
        try:
            volume = self.volume_show(name)
        except KeyError as exc:
            raise SaltCloudSystemExit(f"Unable to find {name} volume: {exc}")
        if volume["status"] == "deleted":
            return volume
        response = nt_ks.volumes.delete(volume["id"])
        return volume

    def volume_detach(self, name, timeout=300):
        """
        Detach a block device
        """
        try:
            volume = self.volume_show(name)
        except KeyError as exc:
            raise SaltCloudSystemExit(f"Unable to find {name} volume: {exc}")
        if not volume["attachments"]:
            return True
        response = self.compute_conn.volumes.delete_server_volume(
            volume["attachments"][0]["server_id"], volume["attachments"][0]["id"]
        )
        trycount = 0
        start = time.time()
        while True:
            trycount += 1
            try:
                response = self._volume_get(volume["id"])
                if response["status"] == "available":
                    return response
            except Exception as exc:  # pylint: disable=broad-except
                log.debug("Volume is detaching: %s", name)
                time.sleep(1)
                if time.time() - start > timeout:
                    log.error(
                        "Timed out after %d seconds while waiting for data", timeout
                    )
                    return False

                log.debug("Retrying volume_show() (try %d)", trycount)

    def volume_attach(self, name, server_name, device="/dev/xvdb", timeout=300):
        """
        Attach a block device
        """
        try:
            volume = self.volume_show(name)
        except KeyError as exc:
            raise SaltCloudSystemExit(f"Unable to find {name} volume: {exc}")
        server = self.server_by_name(server_name)
        response = self.compute_conn.volumes.create_server_volume(
            server.id, volume["id"], device=device
        )
        trycount = 0
        start = time.time()
        while True:
            trycount += 1
            try:
                response = self._volume_get(volume["id"])
                if response["status"] == "in-use":
                    return response
            except Exception as exc:  # pylint: disable=broad-except
                log.debug("Volume is attaching: %s", name)
                time.sleep(1)
                if time.time() - start > timeout:
                    log.error(
                        "Timed out after %s seconds while waiting for data", timeout
                    )
                    return False

                log.debug("Retrying volume_show() (try %s)", trycount)

    def suspend(self, instance_id):
        """
        Suspend a server
        """
        nt_ks = self.compute_conn
        response = nt_ks.servers.suspend(instance_id)
        return True

    def resume(self, instance_id):
        """
        Resume a server
        """
        nt_ks = self.compute_conn
        response = nt_ks.servers.resume(instance_id)
        return True

    def lock(self, instance_id):
        """
        Lock an instance
        """
        nt_ks = self.compute_conn
        response = nt_ks.servers.lock(instance_id)
        return True

    def delete(self, instance_id):
        """
        Delete a server
        """
        nt_ks = self.compute_conn
        response = nt_ks.servers.delete(instance_id)
        return True

    def flavor_list(self):
        """
        Return a list of available flavors (nova flavor-list)
        """
        nt_ks = self.compute_conn
        ret = {}
        for flavor in nt_ks.flavors.list():
            links = {}
            for link in flavor.links:
                links[link["rel"]] = link["href"]
            ret[flavor.name] = {
                "disk": flavor.disk,
                "id": flavor.id,
                "name": flavor.name,
                "ram": flavor.ram,
                "swap": flavor.swap,
                "vcpus": flavor.vcpus,
                "links": links,
            }
            if hasattr(flavor, "rxtx_factor"):
                ret[flavor.name]["rxtx_factor"] = flavor.rxtx_factor
        return ret

    list_sizes = flavor_list

    def flavor_create(
        self,
        name,  # pylint: disable=C0103
        flavor_id=0,  # pylint: disable=C0103
        ram=0,
        disk=0,
        vcpus=1,
    ):
        """
        Create a flavor
        """
        nt_ks = self.compute_conn
        nt_ks.flavors.create(
            name=name, flavorid=flavor_id, ram=ram, disk=disk, vcpus=vcpus
        )
        return {"name": name, "id": flavor_id, "ram": ram, "disk": disk, "vcpus": vcpus}

    def flavor_delete(self, flavor_id):  # pylint: disable=C0103
        """
        Delete a flavor
        """
        nt_ks = self.compute_conn
        nt_ks.flavors.delete(flavor_id)
        return f"Flavor deleted: {flavor_id}"

    def keypair_list(self):
        """
        List keypairs
        """
        nt_ks = self.compute_conn
        ret = {}
        for keypair in nt_ks.keypairs.list():
            ret[keypair.name] = {
                "name": keypair.name,
                "fingerprint": keypair.fingerprint,
                "public_key": keypair.public_key,
            }
        return ret

    def keypair_add(self, name, pubfile=None, pubkey=None):
        """
        Add a keypair
        """
        nt_ks = self.compute_conn
        if pubfile:
            with salt.utils.files.fopen(pubfile, "r") as fp_:
                pubkey = salt.utils.stringutils.to_unicode(fp_.read())
        if not pubkey:
            return False
        nt_ks.keypairs.create(name, public_key=pubkey)
        ret = {"name": name, "pubkey": pubkey}
        return ret

    def keypair_delete(self, name):
        """
        Delete a keypair
        """
        nt_ks = self.compute_conn
        nt_ks.keypairs.delete(name)
        return f"Keypair deleted: {name}"

    def image_show(self, image_id):
        """
        Show image details and metadata
        """
        nt_ks = self.compute_conn
        image = nt_ks.images.get(image_id)
        links = {}
        for link in image.links:
            links[link["rel"]] = link["href"]
        ret = {
            "name": image.name,
            "id": image.id,
            "status": image.status,
            "progress": image.progress,
            "created": image.created,
            "updated": image.updated,
            "metadata": image.metadata,
            "links": links,
        }
        if hasattr(image, "minDisk"):
            ret["minDisk"] = image.minDisk
        if hasattr(image, "minRam"):
            ret["minRam"] = image.minRam

        return ret

    def image_list(self, name=None):
        """
        List server images
        """
        nt_ks = self.compute_conn
        ret = {}
        for image in nt_ks.images.list():
            links = {}
            for link in image.links:
                links[link["rel"]] = link["href"]
            ret[image.name] = {
                "name": image.name,
                "id": image.id,
                "status": image.status,
                "progress": image.progress,
                "created": image.created,
                "updated": image.updated,
                "metadata": image.metadata,
                "links": links,
            }
            if hasattr(image, "minDisk"):
                ret[image.name]["minDisk"] = image.minDisk
            if hasattr(image, "minRam"):
                ret[image.name]["minRam"] = image.minRam
        if name:
            return {name: ret[name]}
        return ret

    list_images = image_list

    def image_meta_set(
        self, image_id=None, name=None, **kwargs
    ):  # pylint: disable=C0103
        """
        Set image metadata
        """
        nt_ks = self.compute_conn
        if name:
            for image in nt_ks.images.list():
                if image.name == name:
                    image_id = image.id  # pylint: disable=C0103
        if not image_id:
            return {"Error": "A valid image name or id was not specified"}
        nt_ks.images.set_meta(image_id, kwargs)
        return {image_id: kwargs}

    def image_meta_delete(
        self, image_id=None, name=None, keys=None  # pylint: disable=C0103
    ):
        """
        Delete image metadata
        """
        nt_ks = self.compute_conn
        if name:
            for image in nt_ks.images.list():
                if image.name == name:
                    image_id = image.id  # pylint: disable=C0103
        pairs = keys.split(",")
        if not image_id:
            return {"Error": "A valid image name or id was not specified"}
        nt_ks.images.delete_meta(image_id, pairs)
        return {image_id: f"Deleted: {pairs}"}

    def server_list(self):
        """
        List servers
        """
        nt_ks = self.compute_conn
        ret = {}
        for item in nt_ks.servers.list():
            try:
                ret[item.name] = {
                    "id": item.id,
                    "name": item.name,
                    "state": item.status,
                    "accessIPv4": item.accessIPv4,
                    "accessIPv6": item.accessIPv6,
                    "flavor": {"id": item.flavor["id"], "links": item.flavor["links"]},
                    "image": {
                        "id": item.image["id"] if item.image else "Boot From Volume",
                        "links": item.image["links"] if item.image else "",
                    },
                }
            except TypeError:
                pass
        return ret

    def server_list_min(self):
        """
        List minimal information about servers
        """
        nt_ks = self.compute_conn
        ret = {}
        for item in nt_ks.servers.list(detailed=False):
            try:
                ret[item.name] = {"id": item.id, "state": "Running"}
            except TypeError:
                pass
        return ret

    def server_list_detailed(self):
        """
        Detailed list of servers
        """
        nt_ks = self.compute_conn
        ret = {}
        for item in nt_ks.servers.list():
            try:
                ret[item.name] = {
                    "OS-EXT-SRV-ATTR": {},
                    "OS-EXT-STS": {},
                    "accessIPv4": item.accessIPv4,
                    "accessIPv6": item.accessIPv6,
                    "addresses": item.addresses,
                    "created": item.created,
                    "flavor": {"id": item.flavor["id"], "links": item.flavor["links"]},
                    "hostId": item.hostId,
                    "id": item.id,
                    "image": {
                        "id": item.image["id"] if item.image else "Boot From Volume",
                        "links": item.image["links"] if item.image else "",
                    },
                    "key_name": item.key_name,
                    "links": item.links,
                    "metadata": item.metadata,
                    "name": item.name,
                    "state": item.status,
                    "tenant_id": item.tenant_id,
                    "updated": item.updated,
                    "user_id": item.user_id,
                }
            except TypeError:
                continue

            ret[item.name]["progress"] = getattr(item, "progress", "0")

            if hasattr(item.__dict__, "OS-DCF:diskConfig"):
                ret[item.name]["OS-DCF"] = {
                    "diskConfig": item.__dict__["OS-DCF:diskConfig"]
                }
            if hasattr(item.__dict__, "OS-EXT-SRV-ATTR:host"):
                ret[item.name]["OS-EXT-SRV-ATTR"]["host"] = item.__dict__[
                    "OS-EXT-SRV-ATTR:host"
                ]
            if hasattr(item.__dict__, "OS-EXT-SRV-ATTR:hypervisor_hostname"):
                ret[item.name]["OS-EXT-SRV-ATTR"]["hypervisor_hostname"] = (
                    item.__dict__["OS-EXT-SRV-ATTR:hypervisor_hostname"]
                )
            if hasattr(item.__dict__, "OS-EXT-SRV-ATTR:instance_name"):
                ret[item.name]["OS-EXT-SRV-ATTR"]["instance_name"] = item.__dict__[
                    "OS-EXT-SRV-ATTR:instance_name"
                ]
            if hasattr(item.__dict__, "OS-EXT-STS:power_state"):
                ret[item.name]["OS-EXT-STS"]["power_state"] = item.__dict__[
                    "OS-EXT-STS:power_state"
                ]
            if hasattr(item.__dict__, "OS-EXT-STS:task_state"):
                ret[item.name]["OS-EXT-STS"]["task_state"] = item.__dict__[
                    "OS-EXT-STS:task_state"
                ]
            if hasattr(item.__dict__, "OS-EXT-STS:vm_state"):
                ret[item.name]["OS-EXT-STS"]["vm_state"] = item.__dict__[
                    "OS-EXT-STS:vm_state"
                ]
            if hasattr(item.__dict__, "security_groups"):
                ret[item.name]["security_groups"] = item.__dict__["security_groups"]
        return ret

    def server_show(self, server_id):
        """
        Show details of one server
        """
        ret = {}
        try:
            servers = self.server_list_detailed()
        except AttributeError:
            raise SaltCloudSystemExit(
                "Corrupt server in server_list_detailed. Remove corrupt servers."
            )
        for server_name, server in servers.items():
            if str(server["id"]) == server_id:
                ret[server_name] = server
        return ret

    def secgroup_create(self, name, description):
        """
        Create a security group
        """
        nt_ks = self.compute_conn
        nt_ks.security_groups.create(name, description)
        ret = {"name": name, "description": description}
        return ret

    def secgroup_delete(self, name):
        """
        Delete a security group
        """
        nt_ks = self.compute_conn
        for item in nt_ks.security_groups.list():
            if item.name == name:
                nt_ks.security_groups.delete(item.id)
                return {name: f"Deleted security group: {name}"}
        return f"Security group not found: {name}"

    def secgroup_list(self):
        """
        List security groups
        """
        nt_ks = self.compute_conn
        ret = {}
        for item in nt_ks.security_groups.list():
            ret[item.name] = {
                "name": item.name,
                "description": item.description,
                "id": item.id,
                "tenant_id": item.tenant_id,
                "rules": item.rules,
            }
        return ret

    def _item_list(self):
        """
        List items
        """
        nt_ks = self.compute_conn
        ret = []
        for item in nt_ks.items.list():
            ret.append(item.__dict__)
        return ret

    def _network_show(self, name, network_lst):
        """
        Parse the returned network list
        """
        for net in network_lst:
            if net.label == name:
                return net.__dict__
        return {}

    def network_show(self, name):
        """
        Show network information
        """
        nt_ks = self.compute_conn
        net_list = nt_ks.networks.list()
        return self._network_show(name, net_list)

    def network_list(self):
        """
        List extra private networks
        """
        nt_ks = self.compute_conn
        return [network.__dict__ for network in nt_ks.networks.list()]

    def _sanatize_network_params(self, kwargs):
        """
        Sanatize novaclient network parameters
        """
        params = [
            "label",
            "bridge",
            "bridge_interface",
            "cidr",
            "cidr_v6",
            "dns1",
            "dns2",
            "fixed_cidr",
            "gateway",
            "gateway_v6",
            "multi_host",
            "priority",
            "project_id",
            "vlan_start",
            "vpn_start",
        ]

        for variable in list(kwargs):  # iterate over a copy, we might delete some
            if variable not in params:
                del kwargs[variable]
        return kwargs

    def network_create(self, name, **kwargs):
        """
        Create extra private network
        """
        nt_ks = self.compute_conn
        kwargs["label"] = name
        kwargs = self._sanatize_network_params(kwargs)
        net = nt_ks.networks.create(**kwargs)
        return net.__dict__

    def _server_uuid_from_name(self, name):
        """
        Get server uuid from name
        """
        return self.server_list().get(name, {}).get("id", "")

    def virtual_interface_list(self, name):
        """
        Get virtual interfaces on slice
        """
        nt_ks = self.compute_conn
        nets = nt_ks.virtual_interfaces.list(self._server_uuid_from_name(name))
        return [network.__dict__ for network in nets]

    def virtual_interface_create(self, name, net_name):
        """
        Add an interfaces to a slice
        """
        nt_ks = self.compute_conn
        serverid = self._server_uuid_from_name(name)
        networkid = self.network_show(net_name).get("id", None)
        if networkid is None:
            return {net_name: False}
        nets = nt_ks.virtual_interfaces.create(networkid, serverid)
        return nets

    def floating_ip_pool_list(self):
        """
        List all floating IP pools

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        pools = nt_ks.floating_ip_pools.list()
        response = {}
        for pool in pools:
            response[pool.name] = {
                "name": pool.name,
            }
        return response

    def floating_ip_list(self):
        """
        List floating IPs

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        floating_ips = nt_ks.floating_ips.list()
        response = {}
        for floating_ip in floating_ips:
            response[floating_ip.ip] = {
                "ip": floating_ip.ip,
                "fixed_ip": floating_ip.fixed_ip,
                "id": floating_ip.id,
                "instance_id": floating_ip.instance_id,
                "pool": floating_ip.pool,
            }
        return response

    def floating_ip_show(self, ip):
        """
        Show info on specific floating IP

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        floating_ips = nt_ks.floating_ips.list()
        for floating_ip in floating_ips:
            if floating_ip.ip == ip:
                response = {
                    "ip": floating_ip.ip,
                    "fixed_ip": floating_ip.fixed_ip,
                    "id": floating_ip.id,
                    "instance_id": floating_ip.instance_id,
                    "pool": floating_ip.pool,
                }
                return response
        return {}

    def floating_ip_create(self, pool=None):
        """
        Allocate a floating IP

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        floating_ip = nt_ks.floating_ips.create(pool)
        response = {
            "ip": floating_ip.ip,
            "fixed_ip": floating_ip.fixed_ip,
            "id": floating_ip.id,
            "instance_id": floating_ip.instance_id,
            "pool": floating_ip.pool,
        }
        return response

    def floating_ip_delete(self, floating_ip):
        """
        De-allocate a floating IP

        .. versionadded:: 2016.3.0
        """
        ip = self.floating_ip_show(floating_ip)
        nt_ks = self.compute_conn
        return nt_ks.floating_ips.delete(ip)

    def floating_ip_associate(self, server_name, floating_ip):
        """
        Associate floating IP address to server

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        server_ = self.server_by_name(server_name)
        server = nt_ks.servers.get(server_.__dict__["id"])
        server.add_floating_ip(floating_ip)
        return self.floating_ip_list()[floating_ip]

    def floating_ip_disassociate(self, server_name, floating_ip):
        """
        Disassociate a floating IP from server

        .. versionadded:: 2016.3.0
        """
        nt_ks = self.compute_conn
        server_ = self.server_by_name(server_name)
        server = nt_ks.servers.get(server_.__dict__["id"])
        server.remove_floating_ip(floating_ip)
        return self.floating_ip_list()[floating_ip]


# The following is a list of functions that need to be incorporated in the
# nova module. This list should be updated as functions are added.
#
# absolute-limits     Print a list of absolute limits for a user
# actions             Retrieve server actions.
# add-fixed-ip        Add new IP address to network.
# aggregate-add-host  Add the host to the specified aggregate.
# aggregate-create    Create a new aggregate with the specified details.
# aggregate-delete    Delete the aggregate by its id.
# aggregate-details   Show details of the specified aggregate.
# aggregate-list      Print a list of all aggregates.
# aggregate-remove-host
#                     Remove the specified host from the specified aggregate.
# aggregate-set-metadata
#                     Update the metadata associated with the aggregate.
# aggregate-update    Update the aggregate's name and optionally
#                     availability zone.
# cloudpipe-create    Create a cloudpipe instance for the given project
# cloudpipe-list      Print a list of all cloudpipe instances.
# console-log         Get console log output of a server.
# credentials         Show user credentials returned from auth
# describe-resource   Show details about a resource
# diagnostics         Retrieve server diagnostics.
# dns-create          Create a DNS entry for domain, name and ip.
# dns-create-private-domain
#                     Create the specified DNS domain.
# dns-create-public-domain
#                     Create the specified DNS domain.
# dns-delete          Delete the specified DNS entry.
# dns-delete-domain   Delete the specified DNS domain.
# dns-domains         Print a list of available dns domains.
# dns-list            List current DNS entries for domain and ip or domain
#                     and name.
# endpoints           Discover endpoints that get returned from the
#                     authenticate services
# get-vnc-console     Get a vnc console to a server.
# host-action         Perform a power action on a host.
# host-update         Update host settings.
# image-create        Create a new image by taking a snapshot of a running
#                     server.
# image-delete        Delete an image.
# live-migration      Migrates a running instance to a new machine.
# meta                Set or Delete metadata on a server.
# migrate             Migrate a server.
# pause               Pause a server.
# rate-limits         Print a list of rate limits for a user
# reboot              Reboot a server.
# rebuild             Shutdown, re-image, and re-boot a server.
# remove-fixed-ip     Remove an IP address from a server.
# rename              Rename a server.
# rescue              Rescue a server.
# resize              Resize a server.
# resize-confirm      Confirm a previous resize.
# resize-revert       Revert a previous resize (and return to the previous
#                     VM).
# root-password       Change the root password for a server.
# secgroup-add-group-rule
#                     Add a source group rule to a security group.
# secgroup-add-rule   Add a rule to a security group.
# secgroup-delete-group-rule
#                     Delete a source group rule from a security group.
# secgroup-delete-rule
#                     Delete a rule from a security group.
# secgroup-list-rules
#                     List rules for a security group.
# ssh                 SSH into a server.
# unlock              Unlock a server.
# unpause             Unpause a server.
# unrescue            Unrescue a server.
# usage-list          List usage data for all tenants
# volume-list         List all the volumes.
# volume-snapshot-create
#                     Add a new snapshot.
# volume-snapshot-delete
#                     Remove a snapshot.
# volume-snapshot-list
#                     List all the snapshots.
# volume-snapshot-show
#                     Show details about a snapshot.
# volume-type-create  Create a new volume type.
# volume-type-delete  Delete a specific flavor
# volume-type-list    Print a list of available 'volume types'.
# x509-create-cert    Create x509 cert for a user in tenant
# x509-get-root-cert  Fetches the x509 root cert.
