nova-powervm/nova_powervm/virt/powervm/volume/npiv.py

284 lines
12 KiB
Python

# Copyright 2015 IBM Corp.
#
# 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.
from oslo_config import cfg
from oslo_log import log as logging
from nova.compute import task_states
from nova.i18n import _LI
from pypowervm.tasks import wwpn as pvm_wwpn
from pypowervm.wrappers import virtual_io_server as pvm_vios
from nova_powervm.virt import powervm
from nova_powervm.virt.powervm.volume import driver as v_driver
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
WWPN_SYSTEM_METADATA_KEY = 'npiv_adpt_wwpns'
TASK_STATES_FOR_DISCONNECT = [task_states.DELETING, task_states.SPAWNING]
class NPIVVolumeAdapter(v_driver.PowerVMVolumeAdapter):
"""The NPIV implementation of the Volume Adapter.
NPIV stands for N_Port ID Virtualization. It is a means of providing
more efficient connections between virtual machines and Fibre Channel
backed SAN fabrics.
From a management level, the main difference is that the Virtual Machine
will have its own WWPNs and own Virtual FC adapter. The Virtual I/O
Server only passes through communication directly to the VM itself.
"""
def connect_volume(self, adapter, host_uuid, vm_uuid, instance,
connection_info):
"""Connects the volume.
:param adapter: The pypowervm adapter.
:param host_uuid: The pypowervm UUID of the host.
:param vios_uuid: The pypowervm UUID of the VIOS.
:param vm_uuid: The powervm UUID of the VM.
:param instance: The nova instance that the volume should connect to.
:param connection_info: Comes from the BDM. Example connection_info:
{
'driver_volume_type':'fibre_channel',
'serial':u'10d9934e-b031-48ff-9f02-2ac533e331c8',
'data':{
'initiator_target_map':{
'21000024FF649105':['500507680210E522'],
'21000024FF649104':['500507680210E522'],
'21000024FF649107':['500507680210E522'],
'21000024FF649106':['500507680210E522']
},
'target_discovered':False,
'qos_specs':None,
'volume_id':'10d9934e-b031-48ff-9f02-2ac533e331c8',
'target_lun':0,
'access_mode':'rw',
'target_wwn':'500507680210E522'
}
"""
# We need to gather each fabric's port mappings
npiv_port_mappings = []
for fabric in self._fabric_names():
npiv_port_mappings.extend(self._get_fabric_meta(instance, fabric))
# Now that we've collapsed all of the varying fabrics' port mappings
# into one list, we can call down into pypowervm to add them in one
# action.
#
# This method should no-op if the mappings are already attached to
# the instance...so it won't duplicate the settings every time an
# attach volume is called.
LOG.info(_LI("Adding NPIV mapping for instance %s") % instance.name)
pvm_wwpn.add_npiv_port_mappings(adapter, host_uuid, vm_uuid,
npiv_port_mappings)
def disconnect_volume(self, adapter, host_uuid, vm_uuid, instance,
connection_info):
"""Disconnect the volume.
:param adapter: The pypowervm adapter.
:param host_uuid: The pypowervm UUID of the host.
:param vm_uuid: The powervm UUID of the VM.
:param instance: The nova instance that the volume should disconnect
from.
:param connection_info: Comes from the BDM. Example connection_info:
{
'driver_volume_type':'fibre_channel',
'serial':u'10d9934e-b031-48ff-9f02-2ac533e331c8',
'data':{
'initiator_target_map':{
'21000024FF649105':['500507680210E522'],
'21000024FF649104':['500507680210E522'],
'21000024FF649107':['500507680210E522'],
'21000024FF649106':['500507680210E522']
},
'target_discovered':False,
'qos_specs':None,
'volume_id':'10d9934e-b031-48ff-9f02-2ac533e331c8',
'target_lun':0,
'access_mode':'rw',
'target_wwn':'500507680210E522'
}
"""
# We should only delete the NPIV mappings if we are running through a
# VM deletion. VM deletion occurs when the task state is deleting.
# However, it can also occur during a 'roll-back' of the spawn.
# Disconnect of the volumes will only be called during a roll back
# of the spawn.
if instance.task_state not in TASK_STATES_FOR_DISCONNECT:
# NPIV should only remove the VFC mapping upon a destroy of the VM
return
# We need to gather each fabric's port mappings
npiv_port_mappings = []
for fabric in self._fabric_names():
npiv_port_mappings.extend(self._get_fabric_meta(instance, fabric))
# Now that we've collapsed all of the varying fabrics' port mappings
# into one list, we can call down into pypowervm to remove them in one
# action.
LOG.info(_LI("Removing NPIV mapping for instance %s") % instance.name)
pvm_wwpn.remove_npiv_port_mappings(adapter, host_uuid,
npiv_port_mappings)
def wwpns(self, adapter, host_uuid, instance):
"""Builds the WWPNs of the adapters that will connect the ports.
:param adapter: The pypowervm API adapter.
:param host_uuid: The UUID of the host for the pypowervm adapter.
:param instance: The nova instance.
:returns: The list of WWPNs that need to be included in the zone set.
"""
# Check to see if all of the fabrics have had the metadata set.
# We check against all of the fabrics in case a fabric was later
# added to the system.
wwpn_keys = []
bad_fabric = False
for fabric in self._fabric_names():
metas = self._get_fabric_meta(instance, fabric)
if metas is None:
bad_fabric = True
break
# Must have been set. Extend the wwpn keys with each element.
wwpn_keys.extend([meta_val[1] for meta_val in metas])
# A 'bad_fabric' is when we had one fabric that was missing. So
# we just kick off a rebuild.
if not bad_fabric:
return wwpn_keys
# At this point, the WWPNs need to be logged into the fabric. But
# before that can be done, the mapping needs to be derived between
# the logical ports and the physical ports.
#
# This should be done on a per-fabric basis.
vios_resp = adapter.read(pvm_vios.VIOS.schema_type,
xag=[pvm_vios.VIOS.xags.FC_MAPPING,
pvm_vios.VIOS.xags.STORAGE])
vios_wraps = pvm_vios.VIOS.wrap(vios_resp)
resp_wwpns = []
for fabric in self._fabric_names():
v_port_wwpns = self._build_wwpns_for_fabric(adapter, host_uuid)
resp_wwpns.extend(v_port_wwpns)
# Derive the virtual to physical port mapping
port_map = pvm_wwpn.derive_npiv_map(vios_wraps,
self._fabric_ports(fabric),
v_port_wwpns)
self._set_fabric_meta(instance, fabric, port_map)
# Every loop through, we reverse the vios wrappers. This is done
# so that if Fabric A only has 1 port, it goes on the first VIOS.
# Then Fabric B would put its port on a different VIOS. As a form
# of multi pathing (so that your paths were not restricted to a
# single VIOS).
vios_wraps.reverse()
# TODO(IBM) Need to log the WWPNs in temporarily here.
# The return object needs to be a list for the volume connector.
return resp_wwpns
def host_name(self, adapter, host_uuid, instance):
"""Derives the host name that should be used for the storage device.
:param adapter: The pypowervm API adapter.
:param host_uuid: The UUID of the host for the pypowervm adapter.
:param instance: The nova instance.
:returns: The host name.
"""
return instance.name
def _set_fabric_meta(self, instance, fabric, port_map):
"""Sets the port map into the instance's system metadata.
The system metadata will store a per-fabric port map that links the
physical ports to the virtual ports. This is needed for the async
nature between the wwpns call (get_volume_connector) and the
connect_volume (spawn).
:param instance: The nova instance.
:param fabric: The name of the fabric.
:param port_map: The port map (as defined via the derive_npiv_map
pypowervm method).
"""
# We will store the metadata in a comma-separated string with a
# multiple of three tokens. Each set of three comprises the Physical
# Port WWPN followed by the two Virtual Port WWPNs:
# "p_wwpn1,v_wwpn1,v_wwpn2,p_wwpn2,v_wwpn3,v_wwpn4,..."
meta_elems = []
for p_wwpn, v_wwpns in port_map:
meta_elems.append(p_wwpn)
meta_elems.extend(v_wwpns.split())
meta_value = ",".join(meta_elems)
meta_key = self._sys_meta_fabric_key(fabric)
instance.system_metadata[meta_key] = meta_value
def _get_fabric_meta(self, instance, fabric):
"""Gets the port map from the instance's system metadata.
See _set_fabric_meta.
:param instance: The nova instance.
:param fabric: The name of the fabric.
:return: The port map (as defined via the derive_npiv_map pypowervm
method.
"""
meta_key = self._sys_meta_fabric_key(fabric)
if instance.system_metadata.get(meta_key) is None:
return None
meta_value = instance.system_metadata[meta_key]
wwpns = meta_value.split(",")
# Rebuild the WWPNs into the natural structure.
return [(p, ' '.join([v1, v2])) for p, v1, v2
in zip(wwpns[::3], wwpns[1::3], wwpns[2::3])]
def _sys_meta_fabric_key(self, fabric):
"""Returns the nova system metadata key for a given fabric."""
return WWPN_SYSTEM_METADATA_KEY + '_' + fabric
def _build_wwpns_for_fabric(self, adapter, host_uuid):
"""Builds all of the WWPNs that are needed for a given fabric."""
wwpns = []
i = 0
while i < self._ports_per_fabric():
wwpns.extend(pvm_wwpn.build_wwpn_pair(adapter, host_uuid))
i += 1
return wwpns
def _fabric_names(self):
"""Returns a list of the fabric names."""
return powervm.NPIV_FABRIC_WWPNS.keys()
def _fabric_ports(self, fabric_name):
"""Returns a list of WWPNs for the fabric's physical ports."""
return powervm.NPIV_FABRIC_WWPNS[fabric_name]
def _ports_per_fabric(self):
"""Returns the number of virtual ports that should be used per fabric.
"""
return CONF.npiv.ports_per_fabric