self.mock_object(requests, 'Session', FakeEseriesHTTPSession)
self.mock_object(self.library,
'_check_mode_get_or_register_storage_system')
+ self.mock_object(self.driver.library, '_check_storage_system')
self.driver.do_setup(context='context')
self.driver.library._client._endpoint = fakes.FAKE_ENDPOINT_HTTP
return configuration
def test_embedded_mode(self):
+ self.mock_object(self.driver.library,
+ '_check_mode_get_or_register_storage_system')
+ self.mock_object(client.RestClient, '_init_features')
configuration = self._set_config(create_configuration())
configuration.netapp_controller_ips = '127.0.0.1,127.0.0.3'
+
driver = common.NetAppDriver(configuration=configuration)
self.mock_object(client.RestClient, 'list_storage_systems', mock.Mock(
return_value=[fakes.STORAGE_SYSTEM]))
driver.do_setup(context='context')
+
self.assertEqual('1fa6efb5-f07b-4de4-9f0e-52e5f7ff5d1b',
driver.library._client.get_system_id())
import copy
+import json
import mock
]
}
+FAKE_RESOURCE_URL = '/devmgr/v2/devmgr/utils/about'
+FAKE_APP_VERSION = '2015.2|2015.2.dev59|vendor|Linux-3.13.0-24-generic'
+FAKE_BACKEND = 'eseriesiSCSI'
+FAKE_CINDER_HOST = 'ubuntu-1404'
+FAKE_SERIAL_NUMBERS = ['021436000943', '021436001321']
+FAKE_SERIAL_NUMBER = ['021436001321']
+FAKE_DEFAULT_SERIAL_NUMBER = ['unknown', 'unknown']
+FAKE_DEFAULT_MODEL = 'unknown'
+FAKE_ABOUT_RESPONSE = {
+ 'runningAsProxy': True,
+ 'version': '01.53.9010.0005',
+ 'systemId': 'a89355ab-692c-4d4a-9383-e249095c3c0',
+}
+
+FAKE_CONTROLLERS = [
+ {'serialNumber': FAKE_SERIAL_NUMBERS[0], 'modelName': '2752'},
+ {'serialNumber': FAKE_SERIAL_NUMBERS[1], 'modelName': '2752'}]
+
+FAKE_SINGLE_CONTROLLER = [{'serialNumber': FAKE_SERIAL_NUMBERS[1]}]
+
+FAKE_KEY = ('openstack-%s-%s-%s' % (FAKE_CINDER_HOST, FAKE_SERIAL_NUMBERS[0],
+ FAKE_SERIAL_NUMBERS[1]))
+
+FAKE_ASUP_DATA = {
+ 'category': 'provisioning',
+ 'app-version': FAKE_APP_VERSION,
+ 'event-source': 'Cinder driver NetApp_iSCSI_ESeries',
+ 'event-description': 'OpenStack Cinder connected to E-Series proxy',
+ 'system-version': '08.10.15.00',
+ 'computer-name': FAKE_CINDER_HOST,
+ 'model': FAKE_CONTROLLERS[0]['modelName'],
+ 'controller2-serial': FAKE_CONTROLLERS[1]['serialNumber'],
+ 'controller1-serial': FAKE_CONTROLLERS[0]['serialNumber'],
+ 'operating-mode': 'proxy',
+}
+FAKE_POST_INVOKE_DATA = ('POST', '/key-values/%s' % FAKE_KEY,
+ json.dumps(FAKE_ASUP_DATA))
VOLUME_COPY_JOB = {
"status": "complete",
def list_hardware_inventory(self):
return HARDWARE_INVENTORY
+ def get_eseries_api_info(self, verify=False):
+ return 'Proxy', '1.53.9010.0005'
+
+ def set_counter(self, key):
+ pass
+
+ def add_autosupport_data(self, *args):
+ pass
+
+ def get_serial_numbers(self):
+ pass
+
+ def get_model_name(self):
+ pass
+
+ def api_operating_mode(self):
+ pass
+
+ def get_firmware_version(self):
+ return FAKE_POST_INVOKE_DATA["system-version"]
+
def create_volume_copy_job(self, *args, **kwargs):
return VOLUME_COPY_JOB
import copy
+import ddt
import mock
from cinder import test
from cinder.volume.drivers.netapp.eseries import client
+@ddt.ddt
class NetAppEseriesClientDriverTestCase(test.TestCase):
"""Test case for NetApp e-series client."""
self.assertIn(status, final_msg)
self.assertIn(headers_string, final_msg)
self.assertIn(body_string, final_msg)
+
+ def test_add_autosupport_data(self):
+ self.mock_object(
+ client.RestClient, 'get_eseries_api_info',
+ mock.Mock(return_value=(
+ eseries_fake.FAKE_ASUP_DATA['operating-mode'],
+ eseries_fake.FAKE_ABOUT_RESPONSE['version'])))
+ self.mock_object(
+ self.my_client, 'get_firmware_version',
+ mock.Mock(
+ return_value=eseries_fake.FAKE_ABOUT_RESPONSE['version']))
+ self.mock_object(
+ self.my_client, 'get_serial_numbers',
+ mock.Mock(return_value=eseries_fake.FAKE_SERIAL_NUMBERS))
+ self.mock_object(
+ self.my_client, 'get_model_name',
+ mock.Mock(
+ return_value=eseries_fake.FAKE_CONTROLLERS[0]['modelName']))
+ self.mock_object(
+ self.my_client, 'set_counter',
+ mock.Mock(return_value={'value': 1}))
+ mock_invoke = self.mock_object(
+ self.my_client, '_invoke',
+ mock.Mock(return_value=eseries_fake.FAKE_ASUP_DATA))
+
+ client.RestClient.add_autosupport_data(
+ self.my_client,
+ eseries_fake.FAKE_KEY,
+ eseries_fake.FAKE_ASUP_DATA
+ )
+
+ mock_invoke.assert_called_with(*eseries_fake.FAKE_POST_INVOKE_DATA)
+
+ @ddt.data((eseries_fake.FAKE_SERIAL_NUMBERS,
+ eseries_fake.FAKE_CONTROLLERS),
+ (eseries_fake.FAKE_DEFAULT_SERIAL_NUMBER, []),
+ (eseries_fake.FAKE_SERIAL_NUMBER,
+ eseries_fake.FAKE_SINGLE_CONTROLLER))
+ @ddt.unpack
+ def test_get_serial_numbers(self, expected_serial_numbers, controllers):
+ self.mock_object(
+ client.RestClient, '_get_controllers',
+ mock.Mock(return_value=controllers))
+
+ serial_numbers = client.RestClient.get_serial_numbers(self.my_client)
+
+ self.assertEqual(expected_serial_numbers, serial_numbers)
+
+ def test_get_model_name(self):
+ self.mock_object(
+ client.RestClient, '_get_controllers',
+ mock.Mock(return_value=eseries_fake.FAKE_CONTROLLERS))
+
+ model = client.RestClient.get_model_name(self.my_client)
+
+ self.assertEqual(eseries_fake.FAKE_CONTROLLERS[0]['modelName'],
+ model)
+
+ def test_get_model_name_empty_controllers_list(self):
+ self.mock_object(
+ client.RestClient, '_get_controllers',
+ mock.Mock(return_value=[]))
+
+ model = client.RestClient.get_model_name(self.my_client)
+
+ self.assertEqual(eseries_fake.FAKE_DEFAULT_MODEL, model)
+
+ def test_get_eseries_api_info(self):
+ fake_invoke_service = mock.Mock()
+ fake_invoke_service.json = mock.Mock(
+ return_value=eseries_fake.FAKE_ABOUT_RESPONSE)
+ self.mock_object(
+ client.RestClient, '_get_resource_url',
+ mock.Mock(return_value=eseries_fake.FAKE_RESOURCE_URL))
+ self.mock_object(
+ self.my_client.client, 'invoke_service',
+ mock.Mock(return_value=fake_invoke_service))
+
+ eseries_info = client.RestClient.get_eseries_api_info(
+ self.my_client, verify=False)
+
+ self.assertEqual((eseries_fake.FAKE_ASUP_DATA['operating-mode'],
+ eseries_fake.FAKE_ABOUT_RESPONSE['version']),
+ eseries_info)
+
+ @ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3',
+ '01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4')
+ def test_api_version_not_support_asup(self, api_version):
+
+ self.mock_object(client.RestClient,
+ 'get_eseries_api_info',
+ mock.Mock(return_value=('proxy', api_version)))
+
+ client.RestClient._init_features(self.my_client)
+
+ self.assertFalse(self.my_client.features.AUTOSUPPORT)
+
+ @ddt.data('01.52.9000.3', '01.52.9000.4', '01.52.8999.2',
+ '01.52.8999.3', '01.53.8999.3', '01.53.9000.2',
+ '02.51.9000.3', '02.52.8999.3', '02.51.8999.2')
+ def test_api_version_supports_asup(self, api_version):
+
+ self.mock_object(client.RestClient,
+ 'get_eseries_api_info',
+ mock.Mock(return_value=('proxy', api_version)))
+
+ client.RestClient._init_features(self.my_client)
+
+ self.assertTrue(self.my_client.features.AUTOSUPPORT)
self.assertEqual({'test_vg1': {'netapp_raid_type': 'unknown'}},
ssc_stats)
+ def test_create_asup(self):
+ self.library._client = mock.Mock()
+ self.library._client.features.AUTOSUPPORT = True
+ self.library._client.api_operating_mode = (
+ eseries_fake.FAKE_ASUP_DATA['operating-mode'])
+ self.library._app_version = eseries_fake.FAKE_APP_VERSION
+ self.mock_object(
+ self.library._client, 'get_firmware_version',
+ mock.Mock(return_value=(
+ eseries_fake.FAKE_ASUP_DATA['system-version'])))
+ self.mock_object(
+ self.library._client, 'get_serial_numbers',
+ mock.Mock(return_value=eseries_fake.FAKE_SERIAL_NUMBERS))
+ self.mock_object(
+ self.library._client, 'get_model_name',
+ mock.Mock(
+ return_value=eseries_fake.FAKE_CONTROLLERS[0]['modelName']))
+ self.mock_object(
+ self.library._client, 'set_counter',
+ mock.Mock(return_value={'value': 1}))
+ mock_invoke = self.mock_object(
+ self.library._client, 'add_autosupport_data')
+
+ self.library._create_asup(eseries_fake.FAKE_CINDER_HOST)
+
+ mock_invoke.assert_called_with(eseries_fake.FAKE_KEY,
+ eseries_fake.FAKE_ASUP_DATA)
+
+ def test_create_asup_not_supported(self):
+ self.library._client = mock.Mock()
+ self.library._client.features.AUTOSUPPORT = False
+ mock_invoke = self.mock_object(
+ self.library._client, 'add_autosupport_data')
+
+ self.library._create_asup(eseries_fake.FAKE_CINDER_HOST)
+
+ mock_invoke.assert_not_called()
+
class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
"""Test driver when netapp_enable_multiattach is enabled.
"""
import copy
+import ddt
import platform
import mock
info._update_openstack_info()
self.assertTrue(mock_updt_from_dpkg.called)
+
+
+@ddt.ddt
+class FeaturesTestCase(test.TestCase):
+
+ def setUp(self):
+ super(FeaturesTestCase, self).setUp()
+ self.features = na_utils.Features()
+
+ def test_init(self):
+ self.assertSetEqual(set(), self.features.defined_features)
+
+ def test_add_feature_default(self):
+ self.features.add_feature('FEATURE_1')
+
+ self.assertTrue(self.features.FEATURE_1)
+ self.assertIn('FEATURE_1', self.features.defined_features)
+
+ @ddt.data(True, False)
+ def test_add_feature(self, value):
+ self.features.add_feature('FEATURE_2', value)
+
+ self.assertEqual(value, self.features.FEATURE_2)
+ self.assertIn('FEATURE_2', self.features.defined_features)
+
+ @ddt.data('True', 'False', 0, 1, 1.0, None, [], {}, (True,))
+ def test_add_feature_type_error(self, value):
+ self.assertRaises(TypeError,
+ self.features.add_feature,
+ 'FEATURE_3',
+ value)
+ self.assertNotIn('FEATURE_3', self.features.defined_features)
+
+ def test_get_attr_missing(self):
+ self.assertRaises(AttributeError, getattr, self.features, 'FEATURE_4')
from cinder.i18n import _
import cinder.utils as cinder_utils
from cinder.volume.drivers.netapp.eseries import utils
+from cinder.volume.drivers.netapp import utils as na_utils
netapp_lib = importutils.try_import('netapp_lib')
if netapp_lib:
class RestClient(object):
"""REST client specific to e-series storage service."""
+ ASUP_VALID_VERSION = (1, 52, 9000, 3)
+
def __init__(self, scheme, host, port, service_path, username,
password, **kwargs):
self._system_id = kwargs.get('system_id')
self._content_type = kwargs.get('content_type') or 'json'
+ def _init_features(self):
+ """Sets up and initializes E-Series feature support map."""
+ self.features = na_utils.Features()
+ self.api_operating_mode, self.api_version = self.get_eseries_api_info(
+ verify=False)
+
+ api_version_tuple = tuple(int(version)
+ for version in self.api_version.split('.'))
+ api_valid_version = self._validate_version(self.ASUP_VALID_VERSION,
+ api_version_tuple)
+
+ self.features.add_feature('AUTOSUPPORT', supported=api_valid_version)
+
+ def _validate_version(self, version, actual_version):
+ """Determine if version is newer than, or equal to the actual version
+
+ The proxy version number is formatted as AA.BB.CCCC.DDDD
+ A: Major version part 1
+ B: Major version part 2
+ C: Release version: 9000->Release, 9010->Pre-release, 9090->Integration
+ D: Minor version
+
+ Examples:
+ 02.53.9000.0010
+ 02.52.9010.0001
+
+ Note: The build version is actually 'newer' the lower the release
+ (CCCC) number is.
+
+ :param version: The version to validate
+ :param actual_version: The running version of the Webservice
+ :return: True if the actual_version is equal or newer than the
+ current running version, otherwise False
+ """
+ major_1, major_2, release, minor = version
+ actual_major_1, actual_major_2, actual_release, actual_minor = (
+ actual_version)
+
+ # We need to invert the release number for it to work with this
+ # comparison
+ return (actual_major_1, actual_major_2, 10000 - actual_release,
+ actual_minor) >= (major_1, major_2, 10000 - release, minor)
+
def set_system_id(self, system_id):
"""Set the storage system id."""
self._system_id = system_id
"""Delete volume copy job."""
path = "/storage-systems/{system-id}/volume-copy-jobs/{object-id}"
return self._invoke('DELETE', path, **{'object-id': object_id})
+
+ def add_autosupport_data(self, key, data):
+ """Register driver statistics via autosupport log."""
+ path = ('/key-values/%s' % key)
+ self._invoke('POST', path, json.dumps(data))
+
+ def get_firmware_version(self):
+ """Get firmware version information from the array."""
+ return self.list_storage_system()['fwVersion']
+
+ def set_counter(self, key, value):
+ path = ('/counters/%s/setCounter?value=%d' % (key, value))
+ self._invoke('POST', path)
+
+ def _get_controllers(self):
+ """Get controller information from the array."""
+ return self.list_hardware_inventory()['controllers']
+
+ def get_eseries_api_info(self, verify=False):
+ """Get E-Series API information from the array."""
+ api_operating_mode = 'embedded'
+ path = 'devmgr/utils/about'
+ headers = {'Content-Type': 'application/json',
+ 'Accept': 'application/json'}
+ url = self._get_resource_url(path, True).replace(
+ '/devmgr/v2', '', 1)
+ result = self.client.invoke_service(method='GET', url=url,
+ headers=headers,
+ verify=verify)
+ about_response_dict = result.json()
+ mode_is_proxy = about_response_dict['runningAsProxy']
+ if mode_is_proxy:
+ api_operating_mode = 'proxy'
+ return api_operating_mode, about_response_dict['version']
+
+ def get_serial_numbers(self):
+ """Get the list of Serial Numbers from the array."""
+ controllers = self._get_controllers()
+ if not controllers:
+ return ['unknown', 'unknown']
+ serial_numbers = [value['serialNumber'].rstrip()
+ for _, value in enumerate(controllers)]
+ serial_numbers.sort()
+ for index, value in enumerate(serial_numbers):
+ if not value:
+ serial_numbers[index] = 'unknown'
+ return serial_numbers
+
+ def get_model_name(self):
+ """Get Model Name from the array."""
+ controllers = self._get_controllers()
+ if not controllers:
+ return 'unknown'
+ return controllers[0].get('modelName', 'unknown')
class NetAppESeriesLibrary(object):
"""Executes commands relating to Volumes."""
+ DRIVER_NAME = 'NetApp_iSCSI_ESeries'
+ AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly
VERSION = "1.0.0"
REQUIRED_FLAGS = ['netapp_server_hostname', 'netapp_controller_ips',
'netapp_login', 'netapp_password',
def __init__(self, driver_name, driver_protocol="iSCSI",
configuration=None, **kwargs):
self.configuration = configuration
+ self._app_version = kwargs.pop("app_version", "unknown")
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
self.configuration.append_config_values(
na_opts.netapp_connection_opts)
self._update_ssc_info)
ssc_periodic_task.start(interval=self.SSC_UPDATE_INTERVAL)
+ # Start the task that logs autosupport (ASUP) data to the controller
+ asup_periodic_task = loopingcall.FixedIntervalLoopingCall(
+ self._create_asup, CONF.host)
+ asup_periodic_task.start(interval=self.AUTOSUPPORT_INTERVAL_SECONDS,
+ initial_delay=0)
+
def check_for_setup_error(self):
self._check_host_type()
self._check_multipath()
system = self._client.register_storage_system(
ips, password=self.configuration.netapp_sa_password)
self._client.set_system_id(system.get('id'))
+ self._client._init_features()
def _check_storage_system(self):
"""Checks whether system is registered and has good status."""
self._stats = data
self._garbage_collect_tmp_vols()
+ def _create_asup(self, cinder_host):
+ if not self._client.features.AUTOSUPPORT:
+ msg = _LI("E-series proxy API version %s does not support "
+ "autosupport logging.")
+ LOG.info(msg % self._client.api_version)
+ return
+
+ firmware_version = self._client.get_firmware_version()
+ event_source = ("Cinder driver %s" % self.DRIVER_NAME)
+ category = "provisioning"
+ event_description = "OpenStack Cinder connected to E-Series proxy"
+ model = self._client.get_model_name()
+ serial_numbers = self._client.get_serial_numbers()
+
+ key = ("openstack-%s-%s-%s"
+ % (cinder_host, serial_numbers[0], serial_numbers[1]))
+
+ # The counter is being set here to a key-value combination
+ # comprised of serial numbers and cinder host with a default
+ # heartbeat of 1. The counter is set to inform the user that the
+ # key does not have a stale value.
+ self._client.set_counter("%s-heartbeat" % key, value=1)
+ data = {
+ 'computer-name': cinder_host,
+ 'event-source': event_source,
+ 'app-version': self._app_version,
+ 'category': category,
+ 'event-description': event_description,
+ 'controller1-serial': serial_numbers[0],
+ 'controller2-serial': serial_numbers[1],
+ 'model': model,
+ 'system-version': firmware_version,
+ 'operating-mode': self._client.api_operating_mode
+ }
+ self._client.add_autosupport_data(key, data)
+
@cinder_utils.synchronized("netapp_update_ssc_info", external=False)
def _update_ssc_info(self):
"""Periodically runs to update ssc information from the backend.
return '%(version)s|%(release)s|%(vendor)s|%(platform)s' % {
'version': self._version, 'release': self._release,
'vendor': self._vendor, 'platform': self._platform}
+
+
+class Features(object):
+
+ def __init__(self):
+ self.defined_features = set()
+
+ def add_feature(self, name, supported=True):
+ if not isinstance(supported, bool):
+ raise TypeError("Feature value must be a bool type.")
+ self.defined_features.add(name)
+ setattr(self, name, supported)
+
+ def __getattr__(self, name):
+ # NOTE(cknight): Needed to keep pylint happy.
+ raise AttributeError