1114 lines
46 KiB
Python
1114 lines
46 KiB
Python
# 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.
|
|
|
|
"""Adapter for Dell EMC PowerStore Cinder driver."""
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import strutils
|
|
|
|
from cinder import coordination
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder.objects import fields
|
|
from cinder.objects.group_snapshot import GroupSnapshot
|
|
from cinder.objects.snapshot import Snapshot
|
|
from cinder.volume.drivers.dell_emc.powerstore import client
|
|
from cinder.volume.drivers.dell_emc.powerstore import utils
|
|
from cinder.volume import manager
|
|
from cinder.volume import volume_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
PROTOCOL_FC = "FC"
|
|
PROTOCOL_ISCSI = "iSCSI"
|
|
CHAP_MODE_SINGLE = "Single"
|
|
|
|
|
|
class CommonAdapter(object):
|
|
def __init__(self,
|
|
backend_id,
|
|
backend_name,
|
|
ports,
|
|
**client_config):
|
|
if isinstance(ports, str):
|
|
ports = ports.split(",")
|
|
self.allowed_ports = [port.strip().lower() for port in ports]
|
|
self.backend_id = backend_id
|
|
self.backend_name = backend_name
|
|
self.client = client.PowerStoreClient(**client_config)
|
|
self.storage_protocol = None
|
|
self.use_chap_auth = False
|
|
|
|
@staticmethod
|
|
def initiators(connector):
|
|
raise NotImplementedError
|
|
|
|
def _port_is_allowed(self, port):
|
|
"""Check if port is in allowed ports list.
|
|
|
|
If allowed ports are empty then all ports are allowed.
|
|
|
|
:param port: iSCSI IP/FC WWN to check
|
|
:return: is port allowed
|
|
"""
|
|
|
|
if not self.allowed_ports:
|
|
return True
|
|
return port.lower() in self.allowed_ports
|
|
|
|
def _get_connection_properties(self, volume_lun):
|
|
raise NotImplementedError
|
|
|
|
def check_for_setup_error(self):
|
|
self.client.check_for_setup_error()
|
|
if self.storage_protocol == PROTOCOL_ISCSI:
|
|
chap_config = self.client.get_chap_config()
|
|
if chap_config.get("mode") == CHAP_MODE_SINGLE:
|
|
self.use_chap_auth = True
|
|
LOG.debug("Successfully initialized PowerStore %(protocol)s adapter "
|
|
"for %(backend_id)s %(backend_name)s backend. "
|
|
"Allowed ports: %(allowed_ports)s. "
|
|
"Use CHAP authentication: %(use_chap_auth)s.",
|
|
{
|
|
"protocol": self.storage_protocol,
|
|
"backend_id": self.backend_id,
|
|
"backend_name": self.backend_name,
|
|
"allowed_ports": self.allowed_ports,
|
|
"use_chap_auth": self.use_chap_auth,
|
|
})
|
|
|
|
def create_volume(self, volume):
|
|
group_provider_id = None
|
|
if (
|
|
volume.group_id and
|
|
volume_utils.is_group_a_cg_snapshot_type(volume.group)
|
|
):
|
|
if volume.is_replicated():
|
|
msg = _("Volume with enabled replication can not be added to "
|
|
"PowerStore volume group.")
|
|
LOG.error(msg)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
group_provider_id = self.client.get_vg_id_by_name(
|
|
volume.group_id
|
|
)
|
|
if volume.is_replicated():
|
|
pp_name = utils.get_protection_policy_from_volume(volume)
|
|
pp_id = self.client.get_protection_policy_id_by_name(pp_name)
|
|
replication_status = fields.ReplicationStatus.ENABLED
|
|
else:
|
|
pp_name = None
|
|
pp_id = None
|
|
replication_status = fields.ReplicationStatus.DISABLED
|
|
LOG.debug("Create PowerStore volume %(volume_name)s of size "
|
|
"%(volume_size)s GiB with id %(volume_id)s. "
|
|
"Protection policy: %(pp_name)s. "
|
|
"Volume group id: %(group_id)s. ",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_size": volume.size,
|
|
"volume_id": volume.id,
|
|
"pp_name": pp_name,
|
|
"group_id": volume.group_id,
|
|
})
|
|
size_in_bytes = utils.gib_to_bytes(volume.size)
|
|
provider_id = self.client.create_volume(volume.name,
|
|
size_in_bytes,
|
|
pp_id,
|
|
group_provider_id)
|
|
LOG.debug("Successfully created PowerStore volume %(volume_name)s of "
|
|
"size %(volume_size)s GiB with id %(volume_id)s on "
|
|
"Protection policy: %(pp_name)s. "
|
|
"Volume group id: %(group_id)s. "
|
|
"PowerStore volume id: %(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_size": volume.size,
|
|
"volume_id": volume.id,
|
|
"pp_name": pp_name,
|
|
"group_id": volume.group_id,
|
|
"volume_provider_id": provider_id,
|
|
})
|
|
return {
|
|
"provider_id": provider_id,
|
|
"replication_status": replication_status,
|
|
}
|
|
|
|
def delete_volume(self, volume):
|
|
try:
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
except exception.VolumeBackendAPIException:
|
|
provider_id = None
|
|
if not provider_id:
|
|
LOG.warning("Volume %(volume_name)s with id %(volume_id)s "
|
|
"does not have provider_id thus does not "
|
|
"map to PowerStore volume.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
})
|
|
return
|
|
LOG.debug("Delete PowerStore volume %(volume_name)s with id "
|
|
"%(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_provider_id": provider_id,
|
|
})
|
|
self._detach_volume_from_hosts(volume)
|
|
self.client.delete_volume_or_snapshot(provider_id)
|
|
LOG.debug("Successfully deleted PowerStore volume %(volume_name)s "
|
|
"with id %(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_provider_id": provider_id,
|
|
})
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
LOG.debug("Extend PowerStore volume %(volume_name)s of size "
|
|
"%(volume_size)s GiB with id %(volume_id)s to "
|
|
"%(volume_new_size)s GiB. "
|
|
"PowerStore volume id: %(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_size": volume.size,
|
|
"volume_id": volume.id,
|
|
"volume_new_size": new_size,
|
|
"volume_provider_id": provider_id,
|
|
})
|
|
size_in_bytes = utils.gib_to_bytes(new_size)
|
|
self.client.extend_volume(provider_id, size_in_bytes)
|
|
LOG.debug("Successfully extended PowerStore volume %(volume_name)s "
|
|
"of size %(volume_size)s GiB with id "
|
|
"%(volume_id)s to %(volume_new_size)s GiB. "
|
|
"PowerStore volume id: %(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_size": volume.size,
|
|
"volume_id": volume.id,
|
|
"volume_new_size": new_size,
|
|
"volume_provider_id": provider_id,
|
|
})
|
|
|
|
def create_snapshot(self, snapshot):
|
|
volume_provider_id = self._get_volume_provider_id(snapshot.volume)
|
|
LOG.debug("Create PowerStore snapshot %(snapshot_name)s with id "
|
|
"%(snapshot_id)s of volume %(volume_name)s with id "
|
|
"%(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_name": snapshot.volume.name,
|
|
"volume_id": snapshot.volume.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
self.client.create_snapshot(volume_provider_id, snapshot.name)
|
|
LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s "
|
|
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
|
"id %(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_name": snapshot.volume.name,
|
|
"volume_id": snapshot.volume.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
try:
|
|
volume_provider_id = self._get_volume_provider_id(snapshot.volume)
|
|
except exception.VolumeBackendAPIException:
|
|
return
|
|
LOG.debug("Delete PowerStore snapshot %(snapshot_name)s with id "
|
|
"%(snapshot_id)s of volume %(volume_name)s with "
|
|
"id %(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_name": snapshot.volume.name,
|
|
"volume_id": snapshot.volume.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
try:
|
|
snapshot_provider_id = self.client.get_snapshot_id_by_name(
|
|
volume_provider_id,
|
|
snapshot.name
|
|
)
|
|
except exception.VolumeBackendAPIException:
|
|
return
|
|
self.client.delete_volume_or_snapshot(snapshot_provider_id,
|
|
entity="snapshot")
|
|
LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s "
|
|
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
|
"id %(volume_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_name": snapshot.volume.name,
|
|
"volume_id": snapshot.volume.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
|
|
def initialize_connection(self, volume, connector, **kwargs):
|
|
connection_properties = self._connect_volume(volume, connector)
|
|
LOG.debug("Connection properties for volume %(volume_name)s with id "
|
|
"%(volume_id)s: %(connection_properties)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"connection_properties": strutils.mask_password(
|
|
connection_properties
|
|
),
|
|
})
|
|
return connection_properties
|
|
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
self._disconnect_volume(volume, connector)
|
|
return {}
|
|
|
|
def update_volume_stats(self):
|
|
stats = {
|
|
"volume_backend_name": self.backend_name,
|
|
"storage_protocol": self.storage_protocol,
|
|
"thick_provisioning_support": False,
|
|
"thin_provisioning_support": True,
|
|
"compression_support": True,
|
|
"multiattach": True,
|
|
"consistent_group_snapshot_enabled": True,
|
|
}
|
|
backend_stats = self.client.get_metrics()
|
|
backend_total_capacity = utils.bytes_to_gib(
|
|
backend_stats["physical_total"]
|
|
)
|
|
backend_free_capacity = (
|
|
backend_total_capacity -
|
|
utils.bytes_to_gib(backend_stats["physical_used"])
|
|
)
|
|
stats["total_capacity_gb"] = backend_total_capacity
|
|
stats["free_capacity_gb"] = backend_free_capacity
|
|
LOG.debug("Free capacity for backend '%(backend)s': "
|
|
"%(free)s GiB, total capacity: %(total)s GiB.",
|
|
{
|
|
"backend": self.backend_name,
|
|
"free": backend_free_capacity,
|
|
"total": backend_total_capacity,
|
|
})
|
|
return stats
|
|
|
|
def create_volume_from_source(self, volume, source):
|
|
if isinstance(source, Snapshot):
|
|
entity = "snapshot"
|
|
source_size = source.volume_size
|
|
source_volume_provider_id = self._get_volume_provider_id(
|
|
source.volume
|
|
)
|
|
source_provider_id = self.client.get_snapshot_id_by_name(
|
|
source_volume_provider_id,
|
|
source.name
|
|
)
|
|
else:
|
|
entity = "volume"
|
|
source_size = source.size
|
|
source_provider_id = self._get_volume_provider_id(source)
|
|
if volume.is_replicated():
|
|
pp_name = utils.get_protection_policy_from_volume(volume)
|
|
pp_id = self.client.get_protection_policy_id_by_name(pp_name)
|
|
replication_status = fields.ReplicationStatus.ENABLED
|
|
else:
|
|
pp_name = None
|
|
pp_id = None
|
|
replication_status = fields.ReplicationStatus.DISABLED
|
|
LOG.debug("Create PowerStore volume %(volume_name)s of size "
|
|
"%(volume_size)s GiB with id %(volume_id)s from %(entity)s "
|
|
"%(entity_name)s with id %(entity_id)s. "
|
|
"Protection policy: %(pp_name)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_size": volume.size,
|
|
"entity": entity,
|
|
"entity_name": source.name,
|
|
"entity_id": source.id,
|
|
"pp_name": pp_name,
|
|
})
|
|
volume_provider_id = self.client.clone_volume_or_snapshot(
|
|
volume.name,
|
|
source_provider_id,
|
|
pp_id,
|
|
entity
|
|
)
|
|
if volume.size > source_size:
|
|
size_in_bytes = utils.gib_to_bytes(volume.size)
|
|
self.client.extend_volume(volume_provider_id, size_in_bytes)
|
|
LOG.debug("Successfully created PowerStore volume %(volume_name)s "
|
|
"of size %(volume_size)s GiB with id %(volume_id)s from "
|
|
"%(entity)s %(entity_name)s with id %(entity_id)s. "
|
|
"Protection policy %(pp_name)s. "
|
|
"PowerStore volume id: %(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_size": volume.size,
|
|
"entity": entity,
|
|
"entity_name": source.name,
|
|
"entity_id": source.id,
|
|
"pp_name": pp_name,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
return {
|
|
"provider_id": volume_provider_id,
|
|
"replication_status": replication_status,
|
|
}
|
|
|
|
def _filter_hosts_by_initiators(self, initiators):
|
|
"""Filter hosts by given list of initiators.
|
|
|
|
If initiators are added to different hosts the exception will be
|
|
raised. In this case one of the hosts should be deleted.
|
|
|
|
:param initiators: list of initiators
|
|
:return: PowerStore host object
|
|
"""
|
|
|
|
LOG.debug("Query PowerStore %(protocol)s hosts.",
|
|
{
|
|
"protocol": self.storage_protocol,
|
|
})
|
|
hosts = self.client.get_all_hosts(self.storage_protocol)
|
|
hosts_found = utils.filter_hosts_by_initiators(hosts, initiators)
|
|
if hosts_found:
|
|
if len(hosts_found) > 1:
|
|
hosts_names_found = [host["name"] for host in hosts_found]
|
|
msg = (_("Initiators are added to different PowerStore hosts: "
|
|
"%(hosts_names_found)s. Remove all of the hosts "
|
|
"except one to proceed. Initiators will be modified "
|
|
"during the next volume attach procedure.")
|
|
% {"hosts_names_found": hosts_names_found, })
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
else:
|
|
return hosts_found[0]
|
|
|
|
@coordination.synchronized("powerstore-create-host")
|
|
def _create_host_if_not_exist(self, connector):
|
|
"""Create PowerStore host if it does not exist.
|
|
|
|
:param connector: connection properties
|
|
:return: PowerStore host object, iSCSI CHAP credentials
|
|
"""
|
|
|
|
initiators = self.initiators(connector)
|
|
host = self._filter_hosts_by_initiators(initiators)
|
|
if self.use_chap_auth:
|
|
chap_credentials = utils.get_chap_credentials()
|
|
else:
|
|
chap_credentials = {}
|
|
if host:
|
|
self._modify_host_initiators(host, chap_credentials, initiators)
|
|
else:
|
|
host_name = utils.powerstore_host_name(
|
|
connector,
|
|
self.storage_protocol
|
|
)
|
|
LOG.debug("Create PowerStore host %(host_name)s. "
|
|
"Initiators: %(initiators)s.",
|
|
{
|
|
"host_name": host_name,
|
|
"initiators": initiators,
|
|
})
|
|
ports = [
|
|
{
|
|
"port_name": initiator,
|
|
"port_type": self.storage_protocol,
|
|
**chap_credentials,
|
|
} for initiator in initiators
|
|
]
|
|
host = self.client.create_host(host_name, ports)
|
|
host["name"] = host_name
|
|
LOG.debug("Successfully created PowerStore host %(host_name)s. "
|
|
"Initiators: %(initiators)s. PowerStore host id: "
|
|
"%(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators": initiators,
|
|
"host_provider_id": host["id"],
|
|
})
|
|
return host, chap_credentials
|
|
|
|
def _modify_host_initiators(self, host, chap_credentials, initiators):
|
|
"""Update PowerStore host initiators if needed.
|
|
|
|
:param host: PowerStore host object
|
|
:param chap_credentials: iSCSI CHAP credentials
|
|
:param initiators: list of initiators
|
|
:return: None
|
|
"""
|
|
|
|
initiators_added = [
|
|
initiator["port_name"] for initiator in host["host_initiators"]
|
|
]
|
|
initiators_to_add = []
|
|
initiators_to_modify = []
|
|
initiators_to_remove = [
|
|
initiator for initiator in initiators_added
|
|
if initiator not in initiators
|
|
]
|
|
for initiator in initiators:
|
|
initiator_add_modify = {
|
|
"port_name": initiator,
|
|
**chap_credentials,
|
|
}
|
|
if initiator not in initiators_added:
|
|
initiator_add_modify["port_type"] = self.storage_protocol
|
|
initiators_to_add.append(initiator_add_modify)
|
|
elif self.use_chap_auth:
|
|
initiators_to_modify.append(initiator_add_modify)
|
|
if initiators_to_remove:
|
|
LOG.debug("Remove initiators from PowerStore host %(host_name)s. "
|
|
"Initiators: %(initiators_to_remove)s. "
|
|
"PowerStore host id: %(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_remove": initiators_to_remove,
|
|
"host_provider_id": host["id"],
|
|
})
|
|
self.client.modify_host_initiators(
|
|
host["id"],
|
|
remove_initiators=initiators_to_remove
|
|
)
|
|
LOG.debug("Successfully removed initiators from PowerStore host "
|
|
"%(host_name)s. Initiators: %(initiators_to_remove)s. "
|
|
"PowerStore host id: %(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_remove": initiators_to_remove,
|
|
"host_provider_id": host["id"],
|
|
})
|
|
if initiators_to_add:
|
|
LOG.debug("Add initiators to PowerStore host %(host_name)s. "
|
|
"Initiators: %(initiators_to_add)s. PowerStore host id: "
|
|
"%(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_add": strutils.mask_password(
|
|
initiators_to_add
|
|
),
|
|
"host_provider_id": host["id"],
|
|
})
|
|
self.client.modify_host_initiators(
|
|
host["id"],
|
|
add_initiators=initiators_to_add
|
|
)
|
|
LOG.debug("Successfully added initiators to PowerStore host "
|
|
"%(host_name)s. Initiators: %(initiators_to_add)s. "
|
|
"PowerStore host id: %(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_add": strutils.mask_password(
|
|
initiators_to_add
|
|
),
|
|
"host_provider_id": host["id"],
|
|
})
|
|
if initiators_to_modify:
|
|
LOG.debug("Modify initiators of PowerStore host %(host_name)s. "
|
|
"Initiators: %(initiators_to_modify)s. "
|
|
"PowerStore host id: %(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_modify": strutils.mask_password(
|
|
initiators_to_modify
|
|
),
|
|
"host_provider_id": host["id"],
|
|
})
|
|
self.client.modify_host_initiators(
|
|
host["id"],
|
|
modify_initiators=initiators_to_modify
|
|
)
|
|
LOG.debug("Successfully modified initiators of PowerStore host "
|
|
"%(host_name)s. Initiators: %(initiators_to_modify)s. "
|
|
"PowerStore host id: %(host_provider_id)s.",
|
|
{
|
|
"host_name": host["name"],
|
|
"initiators_to_modify": strutils.mask_password(
|
|
initiators_to_modify
|
|
),
|
|
"host_provider_id": host["id"],
|
|
})
|
|
|
|
def _attach_volume_to_host(self, host, volume):
|
|
"""Attach PowerStore volume to host.
|
|
|
|
:param host: PowerStore host object
|
|
:param volume: OpenStack volume object
|
|
:return: attached volume logical number
|
|
"""
|
|
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
LOG.debug("Attach PowerStore volume %(volume_name)s with id "
|
|
"%(volume_id)s to host %(host_name)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s, host id: %(host_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"host_name": host["name"],
|
|
"volume_provider_id": provider_id,
|
|
"host_provider_id": host["id"],
|
|
})
|
|
self.client.attach_volume_to_host(host["id"], provider_id)
|
|
volume_lun = self.client.get_volume_lun(host["id"], provider_id)
|
|
LOG.debug("Successfully attached PowerStore volume %(volume_name)s "
|
|
"with id %(volume_id)s to host %(host_name)s. "
|
|
"PowerStore volume id: %(volume_provider_id)s, "
|
|
"host id: %(host_provider_id)s. Volume LUN: "
|
|
"%(volume_lun)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"host_name": host["name"],
|
|
"volume_provider_id": provider_id,
|
|
"host_provider_id": host["id"],
|
|
"volume_lun": volume_lun,
|
|
})
|
|
return volume_lun
|
|
|
|
def _create_host_and_attach(self, connector, volume):
|
|
"""Create PowerStore host and attach volume.
|
|
|
|
:param connector: connection properties
|
|
:param volume: OpenStack volume object
|
|
:return: iSCSI CHAP credentials, volume logical number
|
|
"""
|
|
|
|
host, chap_credentials = self._create_host_if_not_exist(connector)
|
|
return chap_credentials, self._attach_volume_to_host(host, volume)
|
|
|
|
def _connect_volume(self, volume, connector):
|
|
"""Attach PowerStore volume and return it's connection properties.
|
|
|
|
:param volume: OpenStack volume object
|
|
:param connector: connection properties
|
|
:return: volume connection properties
|
|
"""
|
|
|
|
chap_credentials, volume_lun = self._create_host_and_attach(
|
|
connector,
|
|
volume
|
|
)
|
|
connection_properties = self._get_connection_properties(volume_lun)
|
|
if self.use_chap_auth:
|
|
connection_properties["data"]["auth_method"] = "CHAP"
|
|
connection_properties["data"]["auth_username"] = (
|
|
chap_credentials.get("chap_single_username")
|
|
)
|
|
connection_properties["data"]["auth_password"] = (
|
|
chap_credentials.get("chap_single_password")
|
|
)
|
|
return connection_properties
|
|
|
|
def _detach_volume_from_hosts(self, volume, hosts_to_detach=None):
|
|
"""Detach volume from PowerStore hosts.
|
|
|
|
If hosts_to_detach is None, detach volume from all hosts.
|
|
|
|
:param volume: OpenStack volume object
|
|
:param hosts_to_detach: list of hosts to detach from
|
|
:return: None
|
|
"""
|
|
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
if hosts_to_detach is None:
|
|
# Force detach. Get all mapped hosts and detach.
|
|
hosts_to_detach = self.client.get_volume_mapped_hosts(provider_id)
|
|
if not hosts_to_detach:
|
|
# Volume is not attached to any host.
|
|
return
|
|
LOG.debug("Detach PowerStore volume %(volume_name)s with id "
|
|
"%(volume_id)s from hosts. PowerStore volume id: "
|
|
"%(volume_provider_id)s, hosts ids: %(hosts_provider_ids)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_provider_id": provider_id,
|
|
"hosts_provider_ids": hosts_to_detach,
|
|
})
|
|
for host_id in hosts_to_detach:
|
|
self.client.detach_volume_from_host(host_id, provider_id)
|
|
LOG.debug("Successfully detached PowerStore volume "
|
|
"%(volume_name)s with id %(volume_id)s from hosts. "
|
|
"PowerStore volume id: %(volume_provider_id)s, "
|
|
"hosts ids: %(hosts_provider_ids)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"volume_provider_id": provider_id,
|
|
"hosts_provider_ids": hosts_to_detach,
|
|
})
|
|
|
|
def _disconnect_volume(self, volume, connector):
|
|
"""Detach PowerStore volume.
|
|
|
|
:param volume: OpenStack volume object
|
|
:param connector: connection properties
|
|
:return: None
|
|
"""
|
|
|
|
if connector is None:
|
|
self._detach_volume_from_hosts(volume)
|
|
else:
|
|
is_multiattached = utils.is_multiattached_to_host(
|
|
volume.volume_attachment,
|
|
connector["host"]
|
|
)
|
|
if is_multiattached:
|
|
# Do not detach volume until it is attached to more than one
|
|
# instance on the same host.
|
|
return
|
|
initiators = self.initiators(connector)
|
|
host = self._filter_hosts_by_initiators(initiators)
|
|
if host:
|
|
self._detach_volume_from_hosts(volume, [host["id"]])
|
|
|
|
def revert_to_snapshot(self, volume, snapshot):
|
|
volume_provider_id = self._get_volume_provider_id(volume)
|
|
snapshot_volume_provider_id = self._get_volume_provider_id(
|
|
snapshot.volume
|
|
)
|
|
LOG.debug("Restore PowerStore volume %(volume_name)s with id "
|
|
"%(volume_id)s from snapshot %(snapshot_name)s with id "
|
|
"%(snapshot_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
snapshot_provider_id = self.client.get_snapshot_id_by_name(
|
|
snapshot_volume_provider_id,
|
|
snapshot.name
|
|
)
|
|
self.client.restore_from_snapshot(volume_provider_id,
|
|
snapshot_provider_id)
|
|
LOG.debug("Successfully restored PowerStore volume %(volume_name)s "
|
|
"with id %(volume_id)s from snapshot %(snapshot_name)s "
|
|
"with id %(snapshot_id)s. PowerStore volume id: "
|
|
"%(volume_provider_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
"snapshot_name": snapshot.name,
|
|
"snapshot_id": snapshot.id,
|
|
"volume_provider_id": volume_provider_id,
|
|
})
|
|
|
|
def _get_volume_provider_id(self, volume):
|
|
"""Get provider_id for volume.
|
|
|
|
If the secondary backend is used after failover operation try to get
|
|
volume provider_id from PowerStore API.
|
|
|
|
:param volume: OpenStack volume object
|
|
:return: volume provider_id
|
|
"""
|
|
|
|
if (
|
|
self.backend_id == manager.VolumeManager.FAILBACK_SENTINEL or
|
|
not volume.is_replicated()
|
|
):
|
|
return volume.provider_id
|
|
else:
|
|
return self.client.get_volume_id_by_name(volume.name)
|
|
|
|
def teardown_volume_replication(self, volume):
|
|
"""Teardown replication for volume so it can be deleted.
|
|
|
|
:param volume: OpenStack volume object
|
|
:return: None
|
|
"""
|
|
|
|
LOG.debug("Teardown replication for volume %(volume_name)s "
|
|
"with id %(volume_id)s.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
})
|
|
try:
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
rep_session_id = self.client.get_volume_replication_session_id(
|
|
provider_id
|
|
)
|
|
except exception.VolumeBackendAPIException:
|
|
LOG.warning("Replication session for volume %(volume_name)s with "
|
|
"id %(volume_id)s is not found. Replication for "
|
|
"volume was not configured or was modified from "
|
|
"storage side.",
|
|
{
|
|
"volume_name": volume.name,
|
|
"volume_id": volume.id,
|
|
})
|
|
return
|
|
self.client.unassign_volume_protection_policy(provider_id)
|
|
self.client.wait_for_replication_session_deletion(rep_session_id)
|
|
|
|
def failover_host(self, volumes, groups, is_failback):
|
|
volumes_updates = []
|
|
groups_updates = []
|
|
for volume in volumes:
|
|
updates = self.failover_volume(volume, is_failback)
|
|
if updates:
|
|
volumes_updates.append(updates)
|
|
return volumes_updates, groups_updates
|
|
|
|
def failover_volume(self, volume, is_failback):
|
|
error_status = (fields.ReplicationStatus.ERROR if is_failback else
|
|
fields.ReplicationStatus.FAILOVER_ERROR)
|
|
try:
|
|
provider_id = self._get_volume_provider_id(volume)
|
|
rep_session_id = self.client.get_volume_replication_session_id(
|
|
provider_id
|
|
)
|
|
failover_job_id = self.client.failover_volume_replication_session(
|
|
rep_session_id,
|
|
is_failback
|
|
)
|
|
failover_success = self.client.wait_for_failover_completion(
|
|
failover_job_id
|
|
)
|
|
if is_failback:
|
|
self.client.reprotect_volume_replication_session(
|
|
rep_session_id
|
|
)
|
|
except exception.VolumeBackendAPIException:
|
|
failover_success = False
|
|
if not failover_success:
|
|
return {
|
|
"volume_id": volume.id,
|
|
"updates": {
|
|
"replication_status": error_status,
|
|
},
|
|
}
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def create_group(self, group):
|
|
LOG.debug("Create PowerStore volume group %(group_name)s with id "
|
|
"%(group_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
})
|
|
self.client.create_vg(group.id)
|
|
LOG.debug("Successfully created PowerStore volume group "
|
|
"%(group_name)s with id %(group_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
})
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def delete_group(self, group):
|
|
LOG.debug("Delete PowerStore volume group %(group_name)s with id "
|
|
"%(group_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
})
|
|
try:
|
|
group_provider_id = self.client.get_vg_id_by_name(
|
|
group.id
|
|
)
|
|
except exception.VolumeBackendAPIException:
|
|
return None, None
|
|
self.client.delete_volume_or_snapshot(group_provider_id,
|
|
entity="volume group")
|
|
LOG.debug("Successfully deleted PowerStore volume group "
|
|
"%(group_name)s with id %(group_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
})
|
|
return None, None
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def update_group(self, group, add_volumes, remove_volumes):
|
|
volumes_to_add = []
|
|
for volume in add_volumes:
|
|
if volume.is_replicated():
|
|
msg = _("Volume with enabled replication can not be added to "
|
|
"PowerStore volume group.")
|
|
LOG.error(msg)
|
|
raise exception.InvalidVolume(reason=msg)
|
|
volumes_to_add.append(self._get_volume_provider_id(volume))
|
|
volumes_to_remove = [
|
|
self._get_volume_provider_id(volume) for volume in remove_volumes
|
|
]
|
|
LOG.debug("Update PowerStore volume group %(group_name)s with id "
|
|
"%(group_id)s. Add PowerStore volumes with ids: "
|
|
"%(volumes_to_add)s, remove PowerStore volumes with ids: "
|
|
"%(volumes_to_remove)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
"volumes_to_add": volumes_to_add,
|
|
"volumes_to_remove": volumes_to_remove,
|
|
})
|
|
group_provider_id = self.client.get_vg_id_by_name(group.id)
|
|
if volumes_to_add:
|
|
self.client.add_volumes_to_vg(group_provider_id,
|
|
volumes_to_add)
|
|
if volumes_to_remove:
|
|
self.client.remove_volumes_from_vg(group_provider_id,
|
|
volumes_to_remove)
|
|
LOG.debug("Successfully updated PowerStore volume group "
|
|
"%(group_name)s with id %(group_id)s. "
|
|
"Add PowerStore volumes with ids: %(volumes_to_add)s, "
|
|
"remove PowerStore volumes with ids: %(volumes_to_remove)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
"volumes_to_add": volumes_to_add,
|
|
"volumes_to_remove": volumes_to_remove,
|
|
})
|
|
return None, None, None
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def create_group_snapshot(self, group_snapshot):
|
|
LOG.debug("Create PowerStore snapshot %(snapshot_name)s with id "
|
|
"%(snapshot_id)s of volume group %(group_name)s with id "
|
|
"%(group_id)s.",
|
|
{
|
|
"snapshot_name": group_snapshot.name,
|
|
"snapshot_id": group_snapshot.id,
|
|
"group_name": group_snapshot.group.name,
|
|
"group_id": group_snapshot.group.id,
|
|
})
|
|
group_provider_id = self.client.get_vg_id_by_name(
|
|
group_snapshot.group.id
|
|
)
|
|
self.client.create_vg_snapshot(
|
|
group_provider_id,
|
|
group_snapshot.id
|
|
)
|
|
LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s "
|
|
"with id %(snapshot_id)s of volume group %(group_name)s "
|
|
"with id %(group_id)s.",
|
|
{
|
|
"snapshot_name": group_snapshot.name,
|
|
"snapshot_id": group_snapshot.id,
|
|
"group_name": group_snapshot.group.name,
|
|
"group_id": group_snapshot.group.id,
|
|
})
|
|
return None, None
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def delete_group_snapshot(self, group_snapshot):
|
|
LOG.debug("Delete PowerStore snapshot %(snapshot_name)s with id "
|
|
"%(snapshot_id)s of volume group %(group_name)s with "
|
|
"id %(group_id)s.",
|
|
{
|
|
"snapshot_name": group_snapshot.name,
|
|
"snapshot_id": group_snapshot.id,
|
|
"group_name": group_snapshot.group.name,
|
|
"group_id": group_snapshot.group.id,
|
|
})
|
|
try:
|
|
group_provider_id = self.client.get_vg_id_by_name(
|
|
group_snapshot.group.id
|
|
)
|
|
group_snapshot_provider_id = (
|
|
self.client.get_vg_snapshot_id_by_name(
|
|
group_provider_id,
|
|
group_snapshot.id
|
|
))
|
|
except exception.VolumeBackendAPIException:
|
|
return None, None
|
|
self.client.delete_volume_or_snapshot(group_snapshot_provider_id,
|
|
entity="volume group snapshot")
|
|
LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s "
|
|
"with id %(snapshot_id)s of volume group %(group_name)s "
|
|
"with id %(group_id)s.",
|
|
{
|
|
"snapshot_name": group_snapshot.name,
|
|
"snapshot_id": group_snapshot.id,
|
|
"group_name": group_snapshot.group.name,
|
|
"group_id": group_snapshot.group.id,
|
|
})
|
|
return None, None
|
|
|
|
@utils.is_group_a_cg_snapshot_type
|
|
def create_group_from_source(self,
|
|
group,
|
|
volumes,
|
|
source,
|
|
snapshots,
|
|
source_vols):
|
|
if isinstance(source, GroupSnapshot):
|
|
entity = "volume group snapshot"
|
|
group_provider_id = self.client.get_vg_id_by_name(
|
|
source.group.id
|
|
)
|
|
source_provider_id = self.client.get_vg_snapshot_id_by_name(
|
|
group_provider_id,
|
|
source.id
|
|
)
|
|
source_vols = [snapshot.volume for snapshot in snapshots]
|
|
base_clone_name = "%s.%s" % (group.id, source.id)
|
|
else:
|
|
entity = "volume group"
|
|
source_provider_id = self.client.get_vg_id_by_name(source.id)
|
|
base_clone_name = group.id
|
|
LOG.debug("Create PowerStore volume group %(group_name)s with id "
|
|
"%(group_id)s from %(entity)s %(entity_name)s with id "
|
|
"%(entity_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
"entity": entity,
|
|
"entity_name": source.name,
|
|
"entity_id": source.id,
|
|
})
|
|
self.client.clone_vg_or_vg_snapshot(
|
|
group.id,
|
|
source_provider_id,
|
|
entity
|
|
)
|
|
LOG.debug("Successfully created PowerStore volume group "
|
|
"%(group_name)s with id %(group_id)s from %(entity)s "
|
|
"%(entity_name)s with id %(entity_id)s.",
|
|
{
|
|
"group_name": group.name,
|
|
"group_id": group.id,
|
|
"entity": entity,
|
|
"entity_name": source.name,
|
|
"entity_id": source.id,
|
|
})
|
|
updates = []
|
|
for volume, source_vol in zip(volumes, source_vols):
|
|
volume_name = "%s.%s" % (base_clone_name, source_vol.name)
|
|
volume_provider_id = self.client.get_volume_id_by_name(volume_name)
|
|
self.client.rename_volume(volume_provider_id, volume.name)
|
|
volume_updates = {
|
|
"id": volume.id,
|
|
"provider_id": volume_provider_id,
|
|
"replication_status": group.replication_status,
|
|
}
|
|
updates.append(volume_updates)
|
|
return None, updates
|
|
|
|
|
|
class FibreChannelAdapter(CommonAdapter):
|
|
def __init__(self, **kwargs):
|
|
super(FibreChannelAdapter, self).__init__(**kwargs)
|
|
self.storage_protocol = PROTOCOL_FC
|
|
self.driver_volume_type = "fibre_channel"
|
|
|
|
@staticmethod
|
|
def initiators(connector):
|
|
return utils.extract_fc_wwpns(connector)
|
|
|
|
def _get_fc_targets(self):
|
|
"""Get available FC WWNs.
|
|
|
|
:return: list of FC WWNs
|
|
"""
|
|
|
|
wwns = []
|
|
fc_ports = self.client.get_fc_port()
|
|
for port in fc_ports:
|
|
if self._port_is_allowed(port["wwn"]):
|
|
wwns.append(utils.fc_wwn_to_string(port["wwn"]))
|
|
if not wwns:
|
|
msg = _("There are no accessible Fibre Channel targets on the "
|
|
"system.")
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
return wwns
|
|
|
|
def _get_connection_properties(self, volume_lun):
|
|
"""Fill connection properties dict with data to attach volume.
|
|
|
|
:param volume_lun: attached volume logical unit number
|
|
:return: connection properties
|
|
"""
|
|
|
|
target_wwns = self._get_fc_targets()
|
|
return {
|
|
"driver_volume_type": self.driver_volume_type,
|
|
"data": {
|
|
"target_discovered": False,
|
|
"target_lun": volume_lun,
|
|
"target_wwn": target_wwns,
|
|
}
|
|
}
|
|
|
|
|
|
class iSCSIAdapter(CommonAdapter):
|
|
def __init__(self, **kwargs):
|
|
super(iSCSIAdapter, self).__init__(**kwargs)
|
|
self.storage_protocol = PROTOCOL_ISCSI
|
|
self.driver_volume_type = "iscsi"
|
|
|
|
@staticmethod
|
|
def initiators(connector):
|
|
return [connector["initiator"]]
|
|
|
|
def _get_iscsi_targets(self):
|
|
"""Get available iSCSI portals and IQNs.
|
|
|
|
:return: iSCSI portals and IQNs
|
|
"""
|
|
|
|
iqns = []
|
|
portals = []
|
|
ip_pool_addresses = self.client.get_ip_pool_address()
|
|
for address in ip_pool_addresses:
|
|
if self._port_is_allowed(address["address"]):
|
|
portals.append(
|
|
utils.iscsi_portal_with_port(address["address"])
|
|
)
|
|
iqns.append(address["ip_port"]["target_iqn"])
|
|
if not portals:
|
|
msg = _("There are no accessible iSCSI targets on the "
|
|
"system.")
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
return iqns, portals
|
|
|
|
def _get_connection_properties(self, volume_lun):
|
|
"""Fill connection properties dict with data to attach volume.
|
|
|
|
:param volume_lun: attached volume logical unit number
|
|
:return: connection properties
|
|
"""
|
|
|
|
iqns, portals = self._get_iscsi_targets()
|
|
return {
|
|
"driver_volume_type": self.driver_volume_type,
|
|
"data": {
|
|
"target_discovered": False,
|
|
"target_portal": portals[0],
|
|
"target_iqn": iqns[0],
|
|
"target_lun": volume_lun,
|
|
"target_portals": portals,
|
|
"target_iqns": iqns,
|
|
"target_luns": [volume_lun] * len(portals),
|
|
},
|
|
}
|