# Copyright (c) 2020 Dell Inc. or its subsidiaries.
# All Rights Reserved.
#
#    Licensed 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.

import http.client as http_client
import json
import re
import urllib.parse

from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import units
import requests
from requests.exceptions import Timeout

from cinder import exception
from cinder.i18n import _
from cinder.utils import retry
from cinder.volume.drivers.dell_emc.powerflex import simplecache
from cinder.volume.drivers.dell_emc.powerflex import utils as flex_utils

LOG = logging.getLogger(__name__)


VOLUME_MIGRATION_IN_PROGRESS_ERROR = 717
VOLUME_MIGRATION_ALREADY_ON_DESTINATION_POOL_ERROR = 718
VOLUME_NOT_FOUND_ERROR = 79
OLD_VOLUME_NOT_FOUND_ERROR = 78
TOO_MANY_SNAPS_ERROR = 182
ILLEGAL_SYNTAX = 0

MAX_SNAPS_IN_VTREE = 126
TOKEN_REFRESH_SUCC_INTERVAL = 300
TOKEN_REFRESH_FAIL_INTERVAL = 60


class RestClient(object):
    def __init__(self, configuration, is_primary=True):
        self.configuration = configuration
        self.is_primary = is_primary
        self.spCache = simplecache.SimpleCache("Storage Pool", age_minutes=5)
        self.pdCache = simplecache.SimpleCache("Protection Domain",
                                               age_minutes=5)
        self.rcgCache = simplecache.SimpleCache("Replication CG",
                                                age_minutes=5)
        self.rest_ip = None
        self.rest_port = None
        self.rest_username = None
        self.rest_password = None
        self.rest_api_version = None
        self.verify_certificate = None
        self.certificate_path = None
        self.base_url = None
        self.is_configured = False
        self.rest_api_connect_timeout = 30
        self.rest_api_read_timeout = 30
        self.powerflex_gen_type = None
        self.base_ec_url = None

    @staticmethod
    def _get_headers():
        return {"content-type": "application/json"}

    def do_setup(self):
        if self.is_primary:
            get_config_value = self.configuration.safe_get
        else:
            replication_targets = self.configuration.safe_get(
                "replication_device"
            )
            if not replication_targets:
                return
            elif len(replication_targets) > 1:
                msg = _("PowerFlex does not support more than one "
                        "replication backend.")
                raise exception.InvalidInput(reason=msg)
            get_config_value = replication_targets[0].get
        self.verify_certificate = bool(
            get_config_value("sio_verify_server_certificate") or
            get_config_value("driver_ssl_cert_verify")
        )
        self.rest_ip = get_config_value("san_ip")
        self.rest_port = int(
            get_config_value("powerflex_rest_server_port") or
            get_config_value("sio_rest_server_port") or
            443
        )
        self.rest_username = get_config_value("san_login")
        self.rest_password = get_config_value("san_password")
        self.rest_api_connect_timeout = (
            get_config_value(flex_utils.POWERFLEX_REST_CONNECT_TIMEOUT)
            or self.rest_api_connect_timeout)
        self.rest_api_read_timeout = (
            get_config_value(flex_utils.POWERFLEX_REST_READ_TIMEOUT)
            or self.rest_api_read_timeout)
        if self.verify_certificate:
            self.certificate_path = (
                get_config_value("sio_server_certificate_path") or
                get_config_value("driver_ssl_cert_path")
            )
        if not all([self.rest_ip, self.rest_username, self.rest_password]):
            msg = _("REST server IP, username and password must be specified.")
            raise exception.InvalidInput(reason=msg)
        # validate certificate settings
        if self.verify_certificate and not self.certificate_path:
            msg = _("Path to REST server's certificate must be specified.")
            raise exception.InvalidInput(reason=msg)
        # log warning if not using certificates
        if not self.verify_certificate:
            LOG.warning("Verify certificate is not set, using default of "
                        "False.")
        self.base_url = ("https://%(server_ip)s:%(server_port)s/api" %
                         {
                             "server_ip": self.rest_ip,
                             "server_port": self.rest_port
                         })
        self.base_ec_url = ("https://%(server_ip)s:%(server_port)s/dtapi" %
                            {
                                "server_ip": self.rest_ip,
                                "server_port": self.rest_port
                            })
        LOG.info("REST server IP: %(ip)s, port: %(port)s, "
                 "username: %(user)s, rest connect timeout: "
                 "%(rest_api_connect_timeout)s, rest read timeout: "
                 "%(rest_api_read_timeout)s. Verify server's certificate: "
                 "%(verify_cert)s.",
                 {
                     "ip": self.rest_ip,
                     "port": self.rest_port,
                     "user": self.rest_username,
                     "verify_cert": self.verify_certificate,
                     "rest_api_connect_timeout": self.rest_api_connect_timeout,
                     "rest_api_read_timeout": self.rest_api_read_timeout
                 })
        self.is_configured = True
        # Set PowerFlex rest token
        self._set_rest_token()

    def _set_rest_token(self):
        self.rest_token = None

        # Initilaize PowerFlex rest token
        LOG.info("Initialize PowerFlex rest token.")
        self._refresh_token()

        # Start token refresh thread
        self._start_token_refresh_thread()

    def _start_token_refresh_thread(self):
        LOG.info("Start token refresh thread.")
        timer = loopingcall.DynamicLoopingCall(
            self._refresh_token_periodically)
        timer.start(initial_delay=0)

    def _refresh_token_periodically(self):
        if self._refresh_token():
            LOG.info("Token refresh succeeded. Sleeping for %d seconds.",
                     TOKEN_REFRESH_SUCC_INTERVAL)
            return TOKEN_REFRESH_SUCC_INTERVAL

        # Token refresh failed. Continue using current token.
        LOG.warning("Token refresh failed. Sleeping for %d seconds.",
                    TOKEN_REFRESH_FAIL_INTERVAL)
        return TOKEN_REFRESH_FAIL_INTERVAL

    def _refresh_token(self):
        login_url = "/login"
        login_request = self.base_url + login_url
        verify_cert = self._get_verify_cert()
        timeout = (self.rest_api_connect_timeout,
                   self.rest_api_read_timeout)

        try:
            r = requests.get(login_request,
                             auth=(self.rest_username, self.rest_password),
                             verify=verify_cert, timeout=timeout)
            token = r.json()
        except (requests.exceptions.RequestException, ValueError) as e:
            LOG.warning("Failed to refresh token: %s", str(e))
            return False

        self.rest_token = token
        return True

    def query_rest_api_version(self, fromcache=True):
        url = "/version"

        if self.rest_api_version is None or fromcache is False:
            r, unused = self.execute_powerflex_get_request(url)
            if r.status_code == http_client.OK:
                self.rest_api_version = r.text.replace('\"', "")
                LOG.info("REST API Version: %(api_version)s.",
                         {"api_version": self.rest_api_version})
            else:
                msg = (_("Failed to query REST API version. "
                         "Status code: %d.") % r.status_code)
                raise exception.VolumeBackendAPIException(data=msg)
            # make sure the response was valid
            pattern = re.compile(r"^\d+(\.\d+)*$")
            if not pattern.match(self.rest_api_version):
                msg = (_("Failed to query REST API version. Response: %s.") %
                       r.text)
                raise exception.VolumeBackendAPIException(data=msg)
        return self.rest_api_version

    def query_volume(self, vol_id):
        url = "/instances/Volume::%(vol_id)s"

        r, response = self.execute_powerflex_get_request(url, vol_id=vol_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query volume: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def create_volume(self,
                      protection_domain_name,
                      storage_pool_name,
                      volume_id,
                      volume_size,
                      provisioning,
                      compression):
        url = "/types/Volume/instances"

        domain_id = self._get_protection_domain_id(protection_domain_name)
        LOG.info("Protection Domain id: %s.", domain_id)
        pool_id = self.get_storage_pool_id(protection_domain_name,
                                           storage_pool_name)
        LOG.info("Storage Pool id: %s.", pool_id)
        volume_name = flex_utils.id_to_base64(volume_id)
        # units.Mi = 1024 ** 2
        volume_size_kb = volume_size * units.Mi
        params = {
            "protectionDomainId": domain_id,
            "storagePoolId": pool_id,
            "name": volume_name,
            "volumeType": provisioning,
            "volumeSizeInKb": str(volume_size_kb),
            "compressionMethod": compression,
        }
        if self.check_powerflex_ec_version():
            params.pop('compressionMethod')
        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to create volume: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response["id"]

    def snapshot_volume(self, volume_provider_id, snapshot_id,
                        from_source=False):
        url = "/instances/System/action/snapshotVolumes"
        if self.check_powerflex_ec_version():
            url = '/instances/System/action/createSnapshot'
            if from_source:
                url = '/instances/System/action/createThinClone'
        snap_name = flex_utils.id_to_base64(snapshot_id)
        params = {
            "snapshotDefs": [
                {
                    "volumeId": volume_provider_id,
                    "snapshotName": snap_name,
                },
            ],
        }
        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to create snapshot for volume %(vol_name)s: "
                     "%(response)s.") %
                   {"vol_name": volume_provider_id,
                    "response": self._get_response_message(response)})
            LOG.error(msg)
            # check if the volume reached snapshot limit
            if (response and "details" in response and
                    response["details"][0]["rc"] == TOO_MANY_SNAPS_ERROR):
                raise exception.SnapshotLimitReached(
                    set_limit=MAX_SNAPS_IN_VTREE)
            raise exception.VolumeBackendAPIException(data=msg)
        return response["volumeIdList"][0]

    def _get_replication_cg_id_by_name(self, rcg_name):
        url = ("/types/ReplicationConsistencyGroup/instances"
               "/action/queryIdByKey")

        if not rcg_name:
            msg = _("Unable to query Replication CG id with None name.")
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        cached_val = self.rcgCache.get_value(rcg_name)
        if cached_val is not None:
            return cached_val
        encoded_rcg_name = urllib.parse.quote(rcg_name, "")
        params = {"name": encoded_rcg_name}
        r, rcg_id = self.execute_powerflex_post_request(url, params)
        if not rcg_id:
            msg = (_("Replication CG with name %s wasn't found.") % rcg_id)
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        if r.status_code != http_client.OK:
            msg = (_("Failed to get Replication CG id with name "
                     "%(name)s: %(message)s.") %
                   {"name": rcg_name,
                    "message": self._get_response_message(rcg_id)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        LOG.info("Replication CG id: %s.", rcg_id)
        self.rcgCache.update(rcg_name, rcg_id)
        return rcg_id

    def _query_volumes_pair(self,
                            pair_id):
        url = "/instances/ReplicationPair::%(pair_id)s"

        r, response = self.execute_powerflex_get_request(url, pair_id=pair_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query volumes pair %(pair_id)s: %(err)s.") %
                   {"pair_id": pair_id,
                    "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def _query_replication_pairs(self):
        url = "/types/ReplicationPair/instances"

        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query replication pairs: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    @staticmethod
    def _filter_replication_pairs(replication_pairs,
                                  filter_key,
                                  filter_value):
        try:
            return next(filter(lambda pair: pair[filter_key] == filter_value,
                               replication_pairs))
        except StopIteration:
            msg = (_("Volume pair for volume with id %s is not found.")
                   % filter_value)
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def get_volumes_pair_attrs(self, filter_key, filter_value):
        replication_pairs = self._query_replication_pairs()
        founded = self._filter_replication_pairs(replication_pairs,
                                                 filter_key,
                                                 filter_value)
        pair_id = founded["id"]
        remote_pair_id = founded["remoteId"]
        vol_provider_id = founded["localVolumeId"]
        remote_vol_provider_id = founded["remoteVolumeId"]
        return pair_id, remote_pair_id, vol_provider_id, remote_vol_provider_id

    def create_volumes_pair(self,
                            rcg_name,
                            source_provider_id,
                            dest_provider_id):
        url = "/types/ReplicationPair/instances"

        rcg_id = self._get_replication_cg_id_by_name(rcg_name)
        params = {
            "name": source_provider_id,
            "replicationConsistencyGroupId": rcg_id,
            "copyType": "OnlineCopy",
            "sourceVolumeId": source_provider_id,
            "destinationVolumeId": dest_provider_id,
        }
        r, response = self.execute_powerflex_post_request(url, params, )
        if r.status_code != http_client.OK:
            msg = (_("Failed to create volumes pair: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        replication_pair = self._query_volumes_pair(response["id"])
        LOG.info("Created volumes pair %(vol_pair_id)s. "
                 "Remote pair %(remote_pair_id)s.",
                 {
                     "vol_pair_id": replication_pair["id"],
                     "remote_pair_id": replication_pair["remoteId"],
                 })
        return replication_pair["id"], replication_pair["remoteId"]

    def remove_volumes_pair(self, vol_pair_id):
        url = ("/instances/ReplicationPair::%(vol_pair_id)s/action"
               "/removeReplicationPair")

        r, response = self.execute_powerflex_post_request(
            url, vol_pair_id=vol_pair_id
        )
        if r.status_code != http_client.OK:
            msg = (_("Failed to delete volumes pair "
                     "%(vol_pair_id)s: %(err)s.") %
                   {"vol_pair_id": vol_pair_id,
                    "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def _get_protection_domain_id_by_name(self, domain_name):
        url = "/types/Domain/instances/getByName::%(encoded_domain_name)s"

        if not domain_name:
            msg = _("Unable to query Protection Domain id with None name.")
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        encoded_domain_name = urllib.parse.quote(domain_name, "")
        r, domain_id = self.execute_powerflex_get_request(
            url, encoded_domain_name=encoded_domain_name
        )
        if not domain_id:
            msg = (_("Prorection Domain with name %s wasn't found.")
                   % domain_name)
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        if r.status_code != http_client.OK:
            msg = (_("Failed to get Protection Domain id with name "
                     "%(name)s: %(err_msg)s.") %
                   {"name": domain_name,
                    "err_msg": self._get_response_message(domain_id)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        LOG.info("Protection Domain id: %s.", domain_id)
        return domain_id

    def _get_protection_domain_id(self, domain_name):
        response = self._get_protection_domain_properties(domain_name)
        if response is None:
            return None
        return response["id"]

    def _get_protection_domain_properties(self, domain_name):
        url = "/instances/ProtectionDomain::%(domain_id)s"

        cached_val = self.pdCache.get_value(domain_name)
        if cached_val is not None:
            return cached_val
        domain_id = self._get_protection_domain_id_by_name(domain_name)
        r, response = self.execute_powerflex_get_request(
            url, domain_id=domain_id
        )
        if r.status_code != http_client.OK:
            msg = (_("Failed to get domain properties from id %(domain_id)s: "
                     "%(err_msg)s.") %
                   {"domain_id": domain_id, "err_msg": response})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        self.pdCache.update(domain_name, response)
        return response

    def _get_storage_pool_id_by_name(self, domain_name, pool_name):
        url = ("/types/Pool/instances/getByName::"
               "%(domain_id)s,%(encoded_pool_name)s")

        if not domain_name or not pool_name:
            msg = (_("Unable to query storage pool id for "
                     "Pool %(pool_name)s and Domain %(domain_name)s.") %
                   {"pool_name": pool_name, "domain_name": domain_name})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        domain_id = self._get_protection_domain_id(domain_name)
        encoded_pool_name = urllib.parse.quote(pool_name, "")
        r, pool_id = self.execute_powerflex_get_request(
            url, domain_id=domain_id, encoded_pool_name=encoded_pool_name
        )
        if not pool_id:
            msg = (_("Pool with name %(pool_name)s wasn't found in "
                     "domain %(domain_id)s.") %
                   {"pool_name": pool_name, "domain_id": domain_id})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        if r.status_code != http_client.OK:
            msg = (_("Failed to get pool id from name %(pool_name)s: "
                     "%(err_msg)s.") %
                   {"pool_name": pool_name,
                    "err_msg": self._get_response_message(pool_id)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        LOG.info("Pool id: %s.", pool_id)
        return pool_id

    def get_storage_pool_properties(self, domain_name, pool_name):
        url = "/instances/StoragePool::%(pool_id)s"

        fullname = "{}:{}".format(domain_name, pool_name)
        cached_val = self.spCache.get_value(fullname)
        if cached_val is not None:
            return cached_val
        pool_id = self._get_storage_pool_id_by_name(domain_name, pool_name)
        r, response = self.execute_powerflex_get_request(url, pool_id=pool_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to get pool properties from id %(pool_id)s: "
                     "%(err_msg)s.") %
                   {"pool_id": pool_id, "err_msg": response})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        self.spCache.update(fullname, response)
        return response

    def get_storage_pool_id(self, domain_name, pool_name):
        response = self.get_storage_pool_properties(domain_name, pool_name)
        if response is None:
            return None
        return response["id"]

    def _get_verify_cert(self):
        verify_cert = False
        if self.verify_certificate:
            verify_cert = self.certificate_path
        return verify_cert

    def execute_powerflex_get_request(self, url, **url_params):
        r = requests.Response
        try:
            request = self.base_url + url % url_params
            timeout = (self.rest_api_connect_timeout,
                       self.rest_api_read_timeout)
            r = requests.get(request,
                             auth=(self.rest_username, self.rest_token),
                             verify=self._get_verify_cert(), timeout=timeout)
            r = self._check_response(r, request)
            response = r.json()
        except Timeout as e:
            r.status_code = http_client.INTERNAL_SERVER_ERROR
            msg = _("The request to URL %s failed with timeout "
                    "exception %s" % (url, str(e)))
            LOG.error(msg)
            response = {'errorCode': http_client.INTERNAL_SERVER_ERROR,
                        'message': msg}
        return r, response

    def execute_powerflex_post_request(self, url, params=None,
                                       ec_request=False, **url_params):
        r = requests.Response
        try:
            if not params:
                params = {}
            request = self.base_url + url % url_params
            if ec_request:
                request = self.base_ec_url + url % url_params
            timeout = (self.rest_api_connect_timeout,
                       self.rest_api_read_timeout)
            r = requests.post(request,
                              data=json.dumps(params),
                              headers=self._get_headers(),
                              auth=(self.rest_username, self.rest_token),
                              verify=self._get_verify_cert(), timeout=timeout)
            r = self._check_response(r, request, params)
            try:
                response = r.json()
            except ValueError:
                response = None
        except Timeout as e:
            r.status_code = http_client.INTERNAL_SERVER_ERROR
            msg = _("The request to URL %s failed with timeout "
                    "exception %s" % (url, str(e)))
            LOG.error(msg)
            response = {'errorCode': http_client.INTERNAL_SERVER_ERROR,
                        'message': msg}
        return r, response

    def _check_response(self,
                        response,
                        request,
                        params=None):

        level = logging.DEBUG
        # for anything other than an OK from the REST API, log an error
        if response.status_code != http_client.OK:
            level = logging.ERROR
        LOG.log(level,
                "REST Request: %s with params %s",
                request,
                json.dumps(params))
        LOG.log(level,
                "REST Response: %s with data %s",
                response.status_code,
                response.text)
        return response

    def _get_response_message(self, response, default_msg=None):
        """Safely extract message from response.

        Handle None responses and missing keys.

        :param response: Response dict from REST API call, may be None.
        :param default_msg: Default message if response is None or has no
            'message' key.
        :returns: The message string from the response.
        """
        if response is None:
            return default_msg or "Unknown error"
        return response.get("message", default_msg or "Unknown error")

    @retry(exception.VolumeBackendAPIException)
    def extend_volume(self, vol_id, new_size):
        url = "/instances/Volume::%(vol_id)s/action/setVolumeSize"

        round_volume_capacity = (
            self.configuration.powerflex_round_volume_capacity
        )
        if (not self.check_powerflex_ec_version()
                and not round_volume_capacity and not new_size % 8 == 0):
            LOG.warning("PowerFlex only supports volumes with a granularity "
                        "of 8 GBs. The new volume size is: %d.",
                        new_size)
        params = {"sizeInGB": str(new_size)}
        r, response = self.execute_powerflex_post_request(url,
                                                          params,
                                                          vol_id=vol_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to extend volume %(vol_id)s: %(err)s.") %
                   {"vol_id": vol_id,
                    "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def _unmap_volume_from_all_sdcs(self, vol_id):
        url = "/instances/Volume::%(vol_id)s/action/removeMappedSdc"

        volume_is_mapped = False
        try:
            volume = self.query_volume(vol_id)
            if volume.get("mappedSdcInfo") is not None:
                volume_is_mapped = True
        except exception.VolumeBackendAPIException:
            LOG.info("Volume %s is not found thus is not mapped to any SDC.",
                     vol_id)
        if volume_is_mapped:
            params = {"allSdcs": ""}
            LOG.info("Unmap volume from all sdcs.")
            r, response = self.execute_powerflex_post_request(url,
                                                              params,
                                                              vol_id=vol_id)
            if r.status_code != http_client.OK:
                msg = (_("Failed to unmap volume %(vol_id)s from all SDCs: "
                         "%(err)s.") % {
                             "vol_id": vol_id,
                             "err": self._get_response_message(response)})
                LOG.error(msg)
                raise exception.VolumeBackendAPIException(data=msg)

    @retry(exception.VolumeBackendAPIException)
    def remove_volume(self, vol_id):
        url = "/instances/Volume::%(vol_id)s/action/removeVolume"

        self._unmap_volume_from_all_sdcs(vol_id)
        params = {"removeMode": "ONLY_ME"}
        r, response = self.execute_powerflex_post_request(url,
                                                          params,
                                                          vol_id=vol_id)
        if r.status_code != http_client.OK:
            error_code = (response or {}).get("errorCode")
            if error_code == VOLUME_NOT_FOUND_ERROR:
                LOG.warning("Ignoring error in delete volume %s: "
                            "Volume not found.", vol_id)
            elif vol_id is None:
                LOG.warning("Volume does not have provider_id thus does not "
                            "map to PowerFlex volume. "
                            "Allowing deletion to proceed.")
            else:
                msg = (_("Failed to delete volume %(vol_id)s: %(err)s.") %
                       {"vol_id": vol_id,
                        "err": self._get_response_message(response)})
                LOG.error(msg)
                raise exception.VolumeBackendAPIException(data=msg)

    def is_volume_creation_safe(self, protection_domain, storage_pool):
        """Checks if volume creation is safe or not.

        Using volumes with zero padding disabled can lead to existing data
        being read off of a newly created volume.
        """

        # if we have been told to allow unsafe volumes
        if self.configuration.powerflex_allow_non_padded_volumes:
            # Enabled regardless of type, so safe to proceed
            return True
        try:
            properties = self.get_storage_pool_properties(
                protection_domain, storage_pool
            )
            padded = properties["zeroPaddingEnabled"]
        except Exception:
            msg = (_("Unable to retrieve properties for pool %s.") %
                   storage_pool)
            raise exception.InvalidInput(reason=msg)
        # zero padded storage pools are safe
        if padded:
            return True
        # if we got here, it's unsafe
        return False

    def rename_volume(self, volume, name):
        url = "/instances/Volume::%(id)s/action/setVolumeName"

        new_name = flex_utils.id_to_base64(name)
        vol_id = volume["provider_id"]
        params = {"newName": new_name}
        r, response = self.execute_powerflex_post_request(url,
                                                          params,
                                                          id=vol_id)
        if r.status_code != http_client.OK:
            error_code = (response or {}).get("errorCode")
            if ((error_code == VOLUME_NOT_FOUND_ERROR or
                 error_code == OLD_VOLUME_NOT_FOUND_ERROR or
                 error_code == ILLEGAL_SYNTAX)):
                LOG.info("Ignore renaming action because the volume "
                         "%(vol_id)s is not a PowerFlex volume.",
                         {"vol_id": vol_id})
            else:
                msg = (_("Failed to rename volume %(vol_id)s: %(err)s.") %
                       {"vol_id": vol_id,
                        "err": self._get_response_message(response)})
                LOG.error(msg)
                raise exception.VolumeBackendAPIException(data=msg)
        else:
            LOG.info("PowerFlex volume %(vol_id)s was renamed to "
                     "%(new_name)s.", {"vol_id": vol_id, "new_name": new_name})

    def failover_failback_replication_cg(self, rcg_name, is_failback):
        url = ("/instances/ReplicationConsistencyGroup::%(rcg_id)s"
               "/action/%(action)sReplicationConsistencyGroup")

        action = "restore" if is_failback else "failover"
        rcg_id = self._get_replication_cg_id_by_name(rcg_name)
        r, response = self.execute_powerflex_post_request(url,
                                                          rcg_id=rcg_id,
                                                          action=action)
        if r.status_code != http_client.OK:
            msg = (_("Failed to %(action)s rcg with id %(rcg_id)s: "
                     "%(err_msg)s.") % {
                         "action": action,
                         "rcg_id": rcg_id,
                         "err_msg": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def query_vtree(self, vtree_id, vol_id):
        url = "/instances/VTree::%(vtree_id)s"

        r, response = self.execute_powerflex_get_request(url,
                                                         vtree_id=vtree_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to check migration status of volume %s.")
                   % vol_id)
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(msg)
        return response

    def query_vtree_statistics(self, vtree_id):
        url = "/instances/VTree::%(vtree_id)s/relationships/Statistics"

        r, response = self.execute_powerflex_get_request(url,
                                                         vtree_id=vtree_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query vtree statistics: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def migrate_vtree(self, volume, params):
        url = "/instances/Volume::%(vol_id)s/action/migrateVTree"

        r, response = self.execute_powerflex_post_request(
            url,
            params=params,
            vol_id=volume.provider_id
        )
        if r.status_code != http_client.OK:
            error_code = (response or {}).get("errorCode")
            if error_code not in [
                VOLUME_MIGRATION_IN_PROGRESS_ERROR,
                VOLUME_MIGRATION_ALREADY_ON_DESTINATION_POOL_ERROR,
            ]:
                msg = (_("Failed to migrate volume %s.") % volume.id)
                LOG.error(msg)
                raise exception.VolumeBackendAPIException(msg)
        return response

    def overwrite_volume_content(self, volume, snapshot):
        url = "/instances/Volume::%(vol_id)s/action/overwriteVolumeContent"

        params = {"srcVolumeId": snapshot.provider_id}
        r, response = self.execute_powerflex_post_request(
            url,
            params=params,
            vol_id=volume.provider_id
        )
        if r.status_code != http_client.OK:
            msg = (_("Failed to revert volume %(vol_id)s to snapshot "
                     "%(snap_id)s: %(err)s.") % {
                         "vol_id": volume.id,
                         "snap_id": snapshot.id,
                         "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(msg)

    def query_sdc_id_by_guid(self, sdc_guid):
        url = "/types/Sdc/instances"
        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query SDC: %(err)s.") %
                   {"err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        for sdc in response:
            if (sdc["sdcGuid"] and
                    sdc["sdcGuid"].lower() == sdc_guid.lower()):
                return sdc["id"]
        msg = (_("Failed to query SDC by guid %(sdc)s: Not Found.") %
               {"sdc": sdc_guid})
        LOG.error(msg)
        raise exception.VolumeBackendAPIException(data=msg)

    def query_sdc_by_id(self, sdc_id):
        url = "/instances/Sdc::%(sdc_id)s"

        r, response = self.execute_powerflex_get_request(
            url,
            sdc_id = sdc_id)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query SDC id %(sdc_id)s: %(err)s.") % {
                "sdc_id": sdc_id,
                "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def map_volume(self, volume_id, sdc_id):
        params = {'sdcId': sdc_id, 'allowMultipleMappings': 'True'}
        url = "/instances/Volume::%(vol_id)s/action/addMappedSdc" % {
            'vol_id': volume_id
        }
        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to map volume %(vol_id)s to SDC "
                     "%(host_id)s: %(err)s.") % {
                         "vol_id": volume_id,
                         "host_id": sdc_id,
                         "err": self._get_response_message(response)})
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def unmap_volume(self, volume_id, sdc_id=None):
        if sdc_id:
            params = {'sdcId': sdc_id}
            url = "/instances/Volume::%(vol_id)s/action/removeMappedSdc" % {
                'vol_id': volume_id
            }
            r, response = self.execute_powerflex_post_request(url, params)
            if r.status_code != http_client.OK:
                msg = (_("Failed to unmap volume %(vol_id)s from SDC "
                         "%(sdc_id)s: %(err)s.") % {
                             "vol_id": volume_id,
                             "sdc_id": sdc_id,
                             "err": self._get_response_message(response)})
                LOG.error(msg)
                raise exception.VolumeBackendAPIException(data=msg)
        else:
            self._unmap_volume_from_all_sdcs(volume_id)

    def query_sdc_volumes(self, sdc_id):
        url = ("/instances/Sdc::%(sdc_id)s/relationships/Volume" %
               {'sdc_id': sdc_id})

        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query SDC volumes: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return [volume["id"] for volume in response]

    def set_sdc_limits(self, volume_id, sdc_id,
                       bandwidth_limit=None, iops_limit=None):
        params = {'sdcId': sdc_id}
        if bandwidth_limit is not None:
            params['bandwidthLimitInKbps'] = bandwidth_limit
        if iops_limit is not None:
            params['iopsLimit'] = iops_limit

        url = (
            "/instances/Volume::%(volume_id)s/action/setMappedSdcLimits" %
            {'volume_id': volume_id}
        )

        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to set SDC limits: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    """
       BEGIN - Methods for NVMe-TCP support
    """

    def query_system_id_nqn(self):
        url = "/types/System/instances"

        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query system nqn: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response[0]["id"], response[0]["systemNqn"]

    def query_SDTs(self):
        url = "/types/Sdt/instances"

        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query SDTs: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def query_hosts(self):
        url = "/types/Host/instances"

        r, response = self.execute_powerflex_get_request(url)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query hosts: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response

    def query_host_by_nqn(self, nqn):
        response = self.query_hosts()
        for host in response:
            if host["nqn"] and host["nqn"].lower() == nqn.lower():
                return host["id"]
        return None

    def create_nvme_host(self, name, nqn):
        url = "/types/Host/instances"

        params = {
            "nqn": nqn,
            "name": name,
        }
        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to create nvme host: %s.")
                   % self._get_response_message(response))
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)
        return response["id"]

    """
       END - Methods for NVMe-TCP support
    """

    def init_powerflex_gen_type(self, domain_name):
        """Initialize the PowerFlex generation type for the system.

        Queries the protection domain to determine whether the PowerFlex
        system is an EC (Erasure Coding) or Mirroring generation. The result
        is stored in self.powerflex_gen_type and used by
        check_powerflex_ec_version() to control behavior differences across
        volume creation, extension, statistics gathering, and snapshot APIs.

        :param domain_name: protection domain name
        :return: generation type string (e.g., 'Mirroring' or 'EC')
        """

        domain_id = self._get_protection_domain_id(domain_name)
        url = "/types/ProtectionDomain/instances/action/queryBySelectedIds"
        params = {"ids": [domain_id]}
        r, response = self.execute_powerflex_post_request(url, params)
        if r.status_code != http_client.OK:
            msg = (_("Failed to query gen type of powerflex with "
                     "protection domain id %s.") % domain_id)
            raise exception.VolumeBackendAPIException(data=msg)
        if not response:
            msg = (_("No protection domain data returned for "
                     "domain id %s.") % domain_id)
            raise exception.VolumeBackendAPIException(data=msg)
        self.powerflex_gen_type = response[0].get('genType', 'Mirroring')
        return self.powerflex_gen_type

    def check_powerflex_ec_version(self):
        return (self.powerflex_gen_type is not None and
                self.powerflex_gen_type.lower() == "ec")
