cinder/cinder/volume/drivers/dell/dell_storagecenter_api.py

1180 lines
45 KiB
Python

# Copyright 2015 Dell Inc.
#
# 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.
'''Interface for interacting with the Dell Storage Center array.'''
import json
import os.path
from oslo_log import log as logging
import requests
import six
from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
from cinder import utils
LOG = logging.getLogger(__name__)
class PayloadFilter(object):
'''PayloadFilter
Simple class for creating filters for interacting with the Dell
Storage API.
Note that this defaults to "AND" filter types.
'''
def __init__(self):
self.payload = {}
self.payload['filterType'] = 'AND'
self.payload['filters'] = []
def append(self, name, val, filtertype='Equals'):
if val is not None:
apifilter = {}
apifilter['attributeName'] = name
apifilter['attributeValue'] = val
apifilter['filterType'] = filtertype
self.payload['filters'].append(apifilter)
class HttpClient(object):
'''HttpClient
Helper for making the REST calls.
'''
def __init__(self, host, port, user, password):
self.baseUrl = 'https://%s:%s/api/rest/' % (host, port)
self.session = requests.Session()
self.session.auth = (user, password)
self.header = {}
self.header['Content-Type'] = 'application/json; charset=utf-8'
self.header['x-dell-api-version'] = '2.0'
self.verify = False
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.session.close()
def __formatUrl(self, url):
return '%s%s' % (self.baseUrl, url if url[0] != '/' else url[1:])
@utils.retry(exceptions=(requests.ConnectionError, ))
def get(self, url):
return self.session.get(
self.__formatUrl(url),
headers=self.header,
verify=self.verify)
@utils.retry(exceptions=(requests.ConnectionError, ))
def post(self, url, payload):
return self.session.post(
self.__formatUrl(url),
data=json.dumps(payload,
ensure_ascii=False).encode('utf-8'),
headers=self.header,
verify=self.verify)
@utils.retry(exceptions=(requests.ConnectionError, ))
def put(self, url, payload):
return self.session.put(
self.__formatUrl(url),
data=json.dumps(payload,
ensure_ascii=False).encode('utf-8'),
headers=self.header,
verify=self.verify)
@utils.retry(exceptions=(requests.ConnectionError, ))
def delete(self, url):
return self.session.delete(
self.__formatUrl(url),
headers=self.header,
verify=self.verify)
class StorageCenterApiHelper(object):
'''StorageCenterApiHelper
Helper class for API access. Handles opening and closing the
connection to the Storage Center.
'''
def __init__(self, config):
self.config = config
def open_connection(self):
'''Open connection to Enterprise Manager.'''
connection = StorageCenterApi(self.config.san_ip,
self.config.dell_sc_api_port,
self.config.san_login,
self.config.san_password)
connection.open_connection()
return connection
class StorageCenterApi(object):
'''StorageCenterApi
Handles calls to EnterpriseManager via the REST API interface.
'''
APIVERSION = '1.0.1'
def __init__(self, host, port, user, password):
self.notes = 'Created by Dell Cinder Driver'
self.client = HttpClient(host,
port,
user,
password)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close_connection()
def _path_to_array(self, path):
array = []
while True:
(path, tail) = os.path.split(path)
if tail == '':
array.reverse()
return array
array.append(tail)
def _first_result(self, blob):
return self._get_result(blob, None, None)
def _get_result(self, blob, attribute, value):
rsp = None
content = self._get_json(blob)
if content is not None:
# We can get a list or a dict or nothing
if isinstance(content, list):
for r in content:
if attribute is None or r.get(attribute) == value:
rsp = r
break
elif isinstance(content, dict):
if attribute is None or content.get(attribute) == value:
rsp = content
elif attribute is None:
rsp = content
if rsp is None:
LOG.debug('Unable to find result where %(attr)s is %(val)s',
{'attr': attribute,
'val': value})
LOG.debug('Blob was %(blob)s', {'blob': blob.text})
return rsp
def _get_json(self, blob):
try:
return blob.json()
except AttributeError:
LOG.error(_LE('Error invalid json: %s'),
blob)
return None
def _get_id(self, blob):
try:
if isinstance(blob, dict):
return blob.get('instanceId')
except AttributeError:
LOG.error(_LE('Invalid API object: %s'),
blob)
return None
def open_connection(self):
# Authenticate against EM
payload = {}
payload['Application'] = 'Cinder REST Driver'
payload['ApplicationVersion'] = self.APIVERSION
r = self.client.post('ApiConnection/Login',
payload)
if r.status_code != 200:
LOG.error(_LE('Login error: %(c)d %(r)s'),
{'c': r.status_code,
'r': r.reason})
raise exception.VolumeBackendAPIException(
_('Failed to connect to Enterprise Manager'))
def close_connection(self):
r = self.client.post('ApiConnection/Logout',
{})
if r.status_code != 204:
LOG.warning(_LW('Logout error: %(c)d %(r)s'),
{'c': r.status_code,
'r': r.reason})
self.client = None
def find_sc(self, ssn):
'''This is really just a check that the sc is there and being managed by
EM.
'''
r = self.client.get('StorageCenter/StorageCenter')
result = self._get_result(r,
'scSerialNumber',
ssn)
if result is None:
LOG.error(_LE('Failed to find %(s)s. Result %(r)s'),
{'s': ssn,
'r': r})
raise exception.VolumeBackendAPIException(
_('Failed to find Storage Center'))
return self._get_id(result)
# Volume functions
def _create_folder(self, url, ssn, parent, folder):
'''This is generic to server and volume folders.
'''
f = None
payload = {}
payload['Name'] = folder
payload['StorageCenter'] = ssn
if parent != '':
payload['Parent'] = parent
payload['Notes'] = self.notes
r = self.client.post(url,
payload)
if r.status_code != 201:
LOG.debug('%(u)s error: %(c)d %(r)s',
{'u': url,
'c': r.status_code,
'r': r.reason})
else:
f = self._first_result(r)
return f
def _create_folder_path(self, url, ssn, foldername):
'''This is generic to server and volume folders.
'''
path = self._path_to_array(foldername)
folderpath = ''
instanceId = ''
# Technically the first folder is the root so that is already created.
found = True
f = None
for folder in path:
folderpath = folderpath + folder
# If the last was found see if this part of the path exists too
if found:
listurl = url + '/GetList'
f = self._find_folder(listurl,
ssn,
folderpath)
if f is None:
found = False
# We didn't find it so create it
if found is False:
f = self._create_folder(url,
ssn,
instanceId,
folder)
# If we haven't found a folder or created it then leave
if f is None:
LOG.error(_LE('Unable to create folder path %s'),
folderpath)
break
# Next part of the path will need this
instanceId = self._get_id(f)
folderpath = folderpath + '/'
return f
def _find_folder(self, url, ssn, foldername):
'''Most of the time the folder will already have been created so
we look for the end folder and check that the rest of the path is
right.
This is generic to server and volume folders.
'''
pf = PayloadFilter()
pf.append('scSerialNumber', ssn)
basename = os.path.basename(foldername)
pf.append('Name', basename)
# If we have any kind of path we add '/' to match the storage
# center's convention and throw it into the filters.
folderpath = os.path.dirname(foldername)
if folderpath != '':
folderpath += '/'
pf.append('folderPath', folderpath)
folder = None
r = self.client.post(url,
pf.payload)
if r.status_code == 200:
folder = self._get_result(r,
'folderPath',
folderpath)
else:
LOG.debug('%(u)s error: %(c)d %(r)s',
{'u': url,
'c': r.status_code,
'r': r.reason})
return folder
def _create_volume_folder_path(self, ssn, foldername):
return self._create_folder_path('StorageCenter/ScVolumeFolder',
ssn,
foldername)
def _find_volume_folder(self, ssn, foldername):
return self._find_folder('StorageCenter/ScVolumeFolder/GetList',
ssn,
foldername)
def _init_volume(self, scvolume):
'''Maps the volume to a random server and immediately unmaps
it. This initializes the volume.
Don't wig out if this fails.
'''
pf = PayloadFilter()
pf.append('scSerialNumber', scvolume.get('scSerialNumber'), 'Equals')
r = self.client.post('StorageCenter/ScServer/GetList', pf.payload)
if r.status_code == 200:
scservers = self._get_json(r)
# Sort through the servers looking for one with connectivity.
for scserver in scservers:
# TODO(tom_swanson): Add check for server type.
# This needs to be either a physical or virtual server.
# Outside of tempest tests this should not matter as we only
# "init" a volume to allow snapshotting of an empty volume.
if scserver.get('status', '').lower() != 'down':
# Map to actually create the volume
self.map_volume(scvolume,
scserver)
self.unmap_volume(scvolume,
scserver)
break
def create_volume(self, name, size, ssn, volfolder):
'''This creates a new volume on the storage center. It
will create it in volfolder. If volfolder does not
exist it will create it. If it cannot create volfolder
the volume will be created in the root.
'''
scvolume = None
# Find our folder
LOG.debug('Create Volume %(name)s %(ssn)s %(folder)s',
{'name': name,
'ssn': ssn,
'folder': volfolder})
folder = self._find_volume_folder(ssn,
volfolder)
# Doesn't exist? make it
if folder is None:
folder = self._create_volume_folder_path(ssn,
volfolder)
# If we actually have a place to put our volume create it
if folder is None:
LOG.error(_LE('Unable to create folder %s'),
volfolder)
# Create the volume
payload = {}
payload['Name'] = name
payload['Notes'] = self.notes
payload['Size'] = '%d GB' % size
payload['StorageCenter'] = ssn
if folder is not None:
payload['VolumeFolder'] = self._get_id(folder)
r = self.client.post('StorageCenter/ScVolume',
payload)
if r.status_code == 201:
scvolume = self._get_json(r)
else:
LOG.error(_LE('ScVolume create error %(name)s: %(c)d %(r)s'),
{'name': name,
'c': r.status_code,
'r': r.reason})
if scvolume:
LOG.info(_LI('Created volume %(instanceId)s: %(name)s'),
{'instanceId': scvolume['instanceId'],
'name': scvolume['name']})
else:
LOG.error(_LE('ScVolume returned success with empty payload.'
' Attempting to locate volume'))
# In theory it is there since success was returned.
# Try one last time to find it before returning.
scvolume = self.find_volume(ssn, name, None)
return scvolume
def find_volume(self, ssn, name=None, instanceid=None):
'''search ssn for volume of name and/or instance id
'''
LOG.debug('finding volume %(sn)s : %(name)s : %(id)s',
{'sn': ssn,
'name': name,
'id': instanceid})
pf = PayloadFilter()
pf.append('scSerialNumber', ssn)
# We need at least a name and or an instance id. If we have
# that we can find a volume.
if instanceid is not None:
pf.append('instanceId', instanceid)
elif name is not None:
pf.append('Name', name)
else:
return None
r = self.client.post('StorageCenter/ScVolume/GetList',
pf.payload)
if r.status_code != 200:
LOG.debug('ScVolume GetList error %(i)s: %(c)d %(r)s',
{'i': instanceid,
'c': r.status_code,
'r': r.reason})
return self._first_result(r)
def delete_volume(self, ssn, name):
# find our volume
vol = self.find_volume(ssn, name, None)
if vol is not None:
r = self.client.delete('StorageCenter/ScVolume/%s'
% self._get_id(vol))
if r.status_code != 200:
raise exception.VolumeBackendAPIException(
_('Error deleting volume %(ssn)s: %(sn)s: %(c)d %(r)s') %
{'ssn': ssn,
'sn': name,
'c': r.status_code,
'r': r.reason})
# json return should be true or false
return self._get_json(r)
LOG.warning(_LW('delete_volume: unable to find volume %s'),
name)
# If we can't find the volume then it is effectively gone.
return True
def _create_server_folder_path(self, ssn, foldername):
return self._create_folder_path('StorageCenter/ScServerFolder',
ssn,
foldername)
def _find_server_folder(self, ssn, foldername):
return self._find_folder('StorageCenter/ScServerFolder/GetList',
ssn,
foldername)
def _add_hba(self, scserver, wwnoriscsiname, isfc=False):
'''Adds an HBA to the scserver. The HBA will be added
even if it has not been seen by the storage center.
'''
payload = {}
if isfc is True:
payload['HbaPortType'] = 'FibreChannel'
else:
payload['HbaPortType'] = 'Iscsi'
payload['WwnOrIscsiName'] = wwnoriscsiname
payload['AllowManual'] = True
r = self.client.post('StorageCenter/ScPhysicalServer/%s/AddHba'
% self._get_id(scserver),
payload)
if r.status_code != 200:
LOG.error(_LE('AddHba error: %(i)s to %(s)s : %(c)d %(r)s'),
{'i': wwnoriscsiname,
's': scserver['name'],
'c': r.status_code,
'r': r.reason})
return False
return True
# We do not know that we are red hat linux 6.x but that works
# best for red hat and ubuntu. So, there.
def _find_serveros(self, ssn, osname='Red Hat Linux 6.x'):
'''Returns the serveros instance id of the specified osname.
Required to create a server.
'''
pf = PayloadFilter()
pf.append('scSerialNumber', ssn)
r = self.client.post('StorageCenter/ScServerOperatingSystem/GetList',
pf.payload)
if r.status_code == 200:
oslist = self._get_json(r)
for srvos in oslist:
name = srvos.get('name', 'nope')
if name.lower() == osname.lower():
# Found it return the id
return self._get_id(srvos)
LOG.warning(_LW('ScServerOperatingSystem GetList return: %(c)d %(r)s'),
{'c': r.status_code,
'r': r.reason})
return None
def create_server_multiple_hbas(self, ssn, foldername, wwns):
'''Same as create_server except it can take a list of hbas. hbas
can be wwns or iqns.
'''
# Add hbas
scserver = None
# Our instance names
for wwn in wwns:
if scserver is None:
# Use the fist wwn to create the server.
scserver = self.create_server(ssn,
foldername,
wwn,
True)
else:
# Add the wwn to our server
self._add_hba(scserver,
wwn,
True)
return scserver
def create_server(self, ssn, foldername, wwnoriscsiname, isfc=False):
'''creates a server on the the storage center ssn. Adds the first
HBA to it.
'''
scserver = None
payload = {}
payload['Name'] = 'Server_' + wwnoriscsiname
payload['StorageCenter'] = ssn
payload['Notes'] = self.notes
# We pick Red Hat Linux 6.x because it supports multipath and
# will attach luns to paths as they are found.
scserveros = self._find_serveros(ssn, 'Red Hat Linux 6.x')
if scserveros is not None:
payload['OperatingSystem'] = scserveros
# Find our folder or make it
folder = self._find_server_folder(ssn,
foldername)
if folder is None:
folder = self._create_server_folder_path(ssn,
foldername)
# At this point it doesn't matter if the folder was created or not.
# We just attempt to create the server. Let it be in the root if
# the folder creation fails.
if folder is not None:
payload['ServerFolder'] = self._get_id(folder)
# create our server
r = self.client.post('StorageCenter/ScPhysicalServer',
payload)
if r.status_code != 201:
LOG.error(_LE('ScPhysicalServer create error: %(i)s: %(c)d %(r)s'),
{'i': wwnoriscsiname,
'c': r.status_code,
'r': r.reason})
else:
# Server was created
scserver = self._first_result(r)
# Add hba to our server
if scserver is not None:
if not self._add_hba(scserver,
wwnoriscsiname,
isfc):
LOG.error(_LE('Error adding HBA to server'))
# Can't have a server without an HBA
self._delete_server(scserver)
scserver = None
# Success or failure is determined by the caller
return scserver
def find_server(self, ssn, instance_name):
'''Hunts for a server by looking for an HBA with the server's IQN
or wwn.
If found, the server the HBA is attached to, if any, is returned.
'''
scserver = None
# We search for our server by first finding our HBA
hba = self._find_serverhba(ssn, instance_name)
# Once created hbas stay in the system. So it isn't enough
# that we found one it actually has to be attached to a
# server.
if hba is not None and hba.get('server') is not None:
pf = PayloadFilter()
pf.append('scSerialNumber', ssn)
pf.append('instanceId', self._get_id(hba['server']))
r = self.client.post('StorageCenter/ScServer/GetList',
pf.payload)
if r.status_code != 200:
LOG.error(_LE('ScServer error: %(c)d %(r)s'),
{'c': r.status_code,
'r': r.reason})
else:
scserver = self._first_result(r)
if scserver is None:
LOG.debug('Server (%s) not found.',
instance_name)
return scserver
def _find_serverhba(self, ssn, instance_name):
'''Hunts for a sc server HBA by looking for an HBA with the
server's IQN or wwn.
If found, the sc server HBA is returned.
'''
scserverhba = None
# We search for our server by first finding our HBA
pf = PayloadFilter()
pf.append('scSerialNumber', ssn)
pf.append('instanceName', instance_name)
r = self.client.post('StorageCenter/ScServerHba/GetList',
pf.payload)
if r.status_code != 200:
LOG.debug('ScServerHba error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
else:
scserverhba = self._first_result(r)
return scserverhba
def _find_domains(self, cportid):
r = self.client.get('StorageCenter/ScControllerPort/%s/FaultDomainList'
% cportid)
if r.status_code == 200:
domains = self._get_json(r)
return domains
else:
LOG.debug('FaultDomainList error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Error getting FaultDomainList'))
return None
def _find_domain(self, cportid, domainip):
'''Returns the fault domain which a given controller port can
be seen by the server
'''
domains = self._find_domains(cportid)
if domains:
# Wiffle through the domains looking for our
# configured ip.
for domain in domains:
# If this is us we return the port.
if domain.get('targetIpv4Address',
domain.get('wellKnownIpAddress')) == domainip:
return domain
return None
def _find_fc_initiators(self, scserver):
'''_find_fc_initiators
returns the server's fc HBA's wwns
'''
initiators = []
r = self.client.get('StorageCenter/ScServer/%s/HbaList'
% self._get_id(scserver))
if r.status_code == 200:
hbas = self._get_json(r)
for hba in hbas:
wwn = hba.get('instanceName')
if (hba.get('portType') == 'FibreChannel' and
wwn is not None):
initiators.append(wwn)
else:
LOG.debug('HbaList error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Unable to find FC intitiators'))
return initiators
def get_volume_count(self, scserver):
r = self.client.get('StorageCenter/ScServer/%s/MappingList'
% self._get_id(scserver))
if r.status_code == 200:
mappings = self._get_json(r)
return len(mappings)
# Panic mildly but do not return 0.
return -1
def _find_mappings(self, scvolume):
'''find mappings
returns the volume's mappings
'''
mappings = []
if scvolume.get('active', False):
r = self.client.get('StorageCenter/ScVolume/%s/MappingList'
% self._get_id(scvolume))
if r.status_code == 200:
mappings = self._get_json(r)
else:
LOG.debug('MappingList error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Unable to find volume mappings: %s'),
scvolume.get('name'))
else:
LOG.error(_LE('_find_mappings: volume is not active'))
return mappings
def _find_controller_port(self, cportid):
'''_find_controller_port
returns the controller port dict
'''
controllerport = None
r = self.client.get('StorageCenter/ScControllerPort/%s'
% cportid)
if r.status_code == 200:
controllerport = self._first_result(r)
else:
LOG.debug('ScControllerPort error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Unable to find controller port: %s'),
cportid)
return controllerport
def find_wwns(self, scvolume, scserver):
'''returns the lun and wwns of the mapped volume'''
# Our returnables
lun = None # our lun. We return the first lun.
wwns = [] # list of targets
itmap = {} # dict of initiators and the associated targets
# Make sure we know our server's initiators. Only return
# mappings that contain HBA for this server.
initiators = self._find_fc_initiators(scserver)
# Get our volume mappings
mappings = self._find_mappings(scvolume)
if len(mappings) > 0:
# We check each of our mappings. We want to return
# the mapping we have been configured to use.
for mapping in mappings:
# Find the controller port for this mapping
cport = mapping.get('controllerPort')
controllerport = self._find_controller_port(
self._get_id(cport))
if controllerport is not None:
# This changed case at one point or another.
# Look for both keys.
wwn = controllerport.get('wwn',
controllerport.get('WWN'))
if wwn is None:
LOG.error(_LE('Find_wwns: Unable to find port wwn'))
serverhba = mapping.get('serverHba')
if wwn is not None and serverhba is not None:
hbaname = serverhba.get('instanceName')
if hbaname in initiators:
if itmap.get(hbaname) is None:
itmap[hbaname] = []
itmap[hbaname].append(wwn)
wwns.append(wwn)
mappinglun = mapping.get('lun')
if lun is None:
lun = mappinglun
elif lun != mappinglun:
LOG.warning(_LW('Inconsistent Luns.'))
else:
LOG.error(_LE('Find_wwns: Volume appears unmapped'))
LOG.debug(lun)
LOG.debug(wwns)
LOG.debug(itmap)
# TODO(tom_swanson): if we have nothing to return raise an exception
# here. We can't do anything with an unmapped volume. We shouldn't
# pretend we succeeded.
return lun, wwns, itmap
def _find_active_controller(self, scvolume):
# Find the controller on which scvolume is active.
actvctrl = None
r = self.client.get('StorageCenter/ScVolume/%s/VolumeConfiguration'
% self._get_id(scvolume))
if r.status_code == 200:
volconfig = self._first_result(r)
controller = volconfig.get('controller')
actvctrl = self._get_id(controller)
else:
LOG.debug('VolumeConfiguration error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Unable to retrieve VolumeConfiguration: %s'),
self._get_id(scvolume))
LOG.debug('activecontroller %s', actvctrl)
return actvctrl
def _get_controller_id(self, mapping):
# The mapping lists the associated controller.
return self._get_id(mapping.get('controller'))
def _get_domains(self, mapping):
# Return a list of domains associated with this controller port.
return self._find_domains(self._get_id(mapping.get('controllerPort')))
def _get_iqn(self, mapping):
# Get our iqn from our controller port.
iqn = None
cportid = self._get_id(mapping.get('controllerPort'))
controllerport = self._find_controller_port(cportid)
LOG.debug('controllerport: %s', controllerport)
if controllerport:
iqn = controllerport.get('iscsiName')
return iqn
def find_iscsi_properties(self, scvolume, ip=None, port=None):
LOG.debug('enter find_iscsi_properties')
LOG.debug('scvolume: %s', scvolume)
active = -1
up = -1
access_mode = 'rw'
portals = []
luns = []
iqns = []
mappings = self._find_mappings(scvolume)
if len(mappings) > 0:
# In multipath (per Liberty) we will return all paths. But
# if multipath is not set (ip and port are None) then we need
# to return a mapping from the controller on which the volume
# is active. So find that controller.
actvctrl = self._find_active_controller(scvolume)
for mapping in mappings:
# The lun, ro mode and status are in the mapping.
LOG.debug('mapping: %s', mapping)
lun = mapping.get('lun')
ro = mapping.get('readOnly', False)
status = mapping.get('status')
# Dig a bit to get our domains,IQN and controller id.
domains = self._get_domains(mapping)
iqn = self._get_iqn(mapping)
ctrlid = self._get_controller_id(mapping)
if domains and iqn is not None:
for d in domains:
LOG.debug('domain: %s', d)
ipaddress = d.get('targetIpv4Address',
d.get('wellKnownIpAddress'))
portnumber = d.get('portNumber')
# We've all the information. Do we want to return
# it?
if ((ip is None or ip == ipaddress) and
(port is None or port == portnumber)):
portals.append(ipaddress + ':' +
six.text_type(portnumber))
iqns.append(iqn)
luns.append(lun)
# We need to point to the best link.
# So state active and status up is preferred
# but we don't actually need the state to be
# up at this point.
if up == -1:
access_mode = 'rw' if ro is False else 'ro'
if actvctrl == ctrlid:
active = len(iqns) - 1
if status == 'Up':
up = active
# Make sure we point to the best portal we can. This means it is
# on the active controller and, preferably, up. If it isn't return
# what we have.
if up != -1:
# We found a connection that is already up. Return that.
active = up
elif active == -1:
# This shouldn't be able to happen. Maybe a controller went
# down in the middle of this so just return the first one and
# hope the ports are up by the time the connection is attempted.
LOG.debug('Volume is not yet active on any controller.')
active = 0
data = {'target_discovered': False,
'target_iqns': iqns,
'target_portals': portals,
'target_luns': luns,
'access_mode': access_mode
}
LOG.debug('find_iscsi_properties return: %(a)d %(d)s',
{'a': active,
'd': data})
return active, data
def map_volume(self, scvolume, scserver):
'''map_volume
The check for server existence is elsewhere; does not create the
server.
'''
# Make sure we have what we think we have
serverid = self._get_id(scserver)
volumeid = self._get_id(scvolume)
if serverid is not None and volumeid is not None:
payload = {}
payload['server'] = serverid
advanced = {}
advanced['MapToDownServerHbas'] = True
payload['Advanced'] = advanced
r = self.client.post('StorageCenter/ScVolume/%s/MapToServer'
% volumeid,
payload)
if r.status_code == 200:
# We just return our mapping
return self._first_result(r)
# Should not be here.
LOG.debug('MapToServer error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
# Error out
LOG.error(_LE('Unable to map %(vol)s to %(srv)s'),
{'vol': scvolume['name'],
'srv': scserver['name']})
return None
def unmap_volume(self, scvolume, scserver):
'''unmap_volume
deletes all mappings to a server, not just the ones on the path
defined in cinder.conf.
'''
rtn = True
serverid = self._get_id(scserver)
volumeid = self._get_id(scvolume)
if serverid is not None and volumeid is not None:
r = self.client.get('StorageCenter/ScVolume/%s/MappingProfileList'
% volumeid)
if r.status_code == 200:
profiles = self._get_json(r)
for profile in profiles:
prosrv = profile.get('server')
if prosrv is not None and self._get_id(prosrv) == serverid:
r = self.client.delete(
'StorageCenter/ScMappingProfile/%s'
% self._get_id(profile))
if (r.status_code != 200 or r.ok is False):
LOG.debug('ScMappingProfile error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Unable to unmap Volume %s'),
volumeid)
# 1 failed unmap is as good as 100.
# Fail it and leave
rtn = False
break
LOG.debug('Volume %(v)s unmapped from %(s)s',
{'v': volumeid,
's': serverid})
else:
LOG.debug('MappingProfileList error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
rtn = False
return rtn
def get_storage_usage(self, ssn):
'''get_storage_usage'''
storageusage = None
if ssn is not None:
r = self.client.get('StorageCenter/StorageCenter/%s/StorageUsage'
% ssn)
if r.status_code == 200:
storageusage = self._get_json(r)
else:
LOG.debug('StorageUsage error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
return storageusage
def create_replay(self, scvolume, replayid, expire):
'''create_replay
expire is in minutes.
one could snap a volume before it has been activated, so activate
by mapping and unmapping to a random server and let them. This
should be a fail but the Tempest tests require it.
'''
replay = None
if scvolume is not None:
if (scvolume.get('active') is not True or
scvolume.get('replayAllowed') is not True):
self._init_volume(scvolume)
payload = {}
payload['description'] = replayid
payload['expireTime'] = expire
r = self.client.post('StorageCenter/ScVolume/%s/CreateReplay'
% self._get_id(scvolume),
payload)
if r.status_code != 200:
LOG.debug('CreateReplay error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
LOG.error(_LE('Error creating replay.'))
else:
replay = self._first_result(r)
return replay
def find_replay(self, scvolume, replayid):
'''find_replay
searches for the replay by replayid which we store in the
replay's description attribute
'''
replay = None
r = self.client.get('StorageCenter/ScVolume/%s/ReplayList'
% self._get_id(scvolume))
try:
content = self._get_json(r)
# This will be a list. If it isn't bail
if isinstance(content, list):
for r in content:
# The only place to save our information with the public
# api is the description field which isn't quite long
# enough. So we check that our description is pretty much
# the max length and we compare that to the start of
# the snapshot id.
description = r.get('description')
if (len(description) >= 30 and
replayid.startswith(description) is True and
r.get('markedForExpiration') is not True):
replay = r
break
except Exception:
LOG.error(_LE('Invalid ReplayList return: %s'),
r)
if replay is None:
LOG.debug('Unable to find snapshot %s',
replayid)
return replay
def delete_replay(self, scvolume, replayid):
'''delete_replay
hunts down a replay by replayid string and expires it.
once marked for expiration we do not return the replay as
a snapshot.
'''
LOG.debug('Expiring replay %s', replayid)
replay = self.find_replay(scvolume,
replayid)
if replay is not None:
r = self.client.post('StorageCenter/ScReplay/%s/Expire'
% self._get_id(replay),
{})
if r.status_code != 204:
LOG.debug('ScReplay Expire error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
return False
# We either couldn't find it or expired it.
return True
def create_view_volume(self, volname, volfolder, screplay):
'''create_view_volume
creates a new volume named volname in the folder
volfolder from the screplay.
'''
# find our ssn and get our folder
ssn = screplay.get('scSerialNumber')
folder = self._find_volume_folder(ssn,
volfolder)
# Doesn't exist? make it
if folder is None:
folder = self._create_volume_folder_path(ssn,
volfolder)
# payload is just the volume name and folder if we have one.
payload = {}
payload['Name'] = volname
payload['Notes'] = self.notes
if folder is not None:
payload['VolumeFolder'] = self._get_id(folder)
r = self.client.post('StorageCenter/ScReplay/%s/CreateView'
% self._get_id(screplay),
payload)
volume = None
if r.status_code == 200:
volume = self._first_result(r)
else:
LOG.debug('ScReplay CreateView error: %(c)d %(r)s',
{'c': r.status_code,
'r': r.reason})
if volume is None:
LOG.error(_LE('Unable to create volume %s from replay'),
volname)
return volume
def create_cloned_volume(self, volumename, volumefolder, scvolume):
'''create_cloned_volume
creates a temporary replay and then creates a
view volume from that.
'''
clone = None
replay = self.create_replay(scvolume,
'Cinder Clone Replay',
60)
if replay is not None:
clone = self.create_view_volume(volumename,
volumefolder,
replay)
else:
LOG.error(_LE('Error: unable to snap replay'))
return clone
def expand_volume(self, scvolume, newsize):
'''expand_volume'''
payload = {}
payload['NewSize'] = '%d GB' % newsize
r = self.client.post('StorageCenter/ScVolume/%s/ExpandToSize'
% self._get_id(scvolume),
payload)
vol = None
if r.status_code == 200:
vol = self._get_json(r)
else:
LOG.error(_LE('Error expanding volume %(n)s: %(c)d %(r)s'),
{'n': scvolume['name'],
'c': r.status_code,
'r': r.reason})
if vol is not None:
LOG.debug('Volume expanded: %(i)s %(s)s',
{'i': vol['instanceId'],
's': vol['configuredSize']})
return vol
def _delete_server(self, scserver):
'''_delete_server
Just give it a shot. If it fails it doesn't matter to cinder.
'''
if scserver.get('deleteAllowed') is True:
r = self.client.delete('StorageCenter/ScServer/%s'
% self._get_id(scserver))
LOG.debug('ScServer %(i)s delete return: %(c)d %(r)s',
{'i': self._get_id(scserver),
'c': r.status_code,
'r': r.reason})
else:
LOG.debug('_delete_server: deleteAllowed is False.')