From: Rushil Chugh Date: Mon, 4 May 2015 14:47:39 +0000 (-0400) Subject: Implement AutoSupport for NetApp E-Series driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=5af10e9f37ea9308e7d6185650612aae4ff0a02a;p=openstack-build%2Fcinder-build.git Implement AutoSupport for NetApp E-Series driver This patchset implements ASUP support for the NetApp E-Series driver. Implements blueprint netapp-e-series-asup Change-Id: Idd371b99d519e280e9c82844172056c29814fd1b --- diff --git a/cinder/tests/unit/test_netapp_eseries_iscsi.py b/cinder/tests/unit/test_netapp_eseries_iscsi.py index 269242af9..ec849bfd8 100644 --- a/cinder/tests/unit/test_netapp_eseries_iscsi.py +++ b/cinder/tests/unit/test_netapp_eseries_iscsi.py @@ -673,6 +673,7 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): 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 @@ -692,12 +693,17 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): 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()) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py index 6285e41a6..1b1874ced 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py @@ -16,6 +16,7 @@ import copy +import json import mock @@ -557,6 +558,43 @@ HARDWARE_INVENTORY = { ] } +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", @@ -762,6 +800,27 @@ class FakeEseriesClient(object): 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 diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py index d375e3346..9083051bb 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py @@ -16,6 +16,7 @@ import copy +import ddt import mock from cinder import test @@ -24,6 +25,7 @@ from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \ from cinder.volume.drivers.netapp.eseries import client +@ddt.ddt class NetAppEseriesClientDriverTestCase(test.TestCase): """Test case for NetApp e-series client.""" @@ -295,3 +297,112 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): 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) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py index 209050827..4b9e410fd 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py @@ -670,6 +670,44 @@ class NetAppEseriesLibraryTestCase(test.TestCase): 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. diff --git a/cinder/tests/unit/volume/drivers/netapp/test_utils.py b/cinder/tests/unit/volume/drivers/netapp/test_utils.py index 68bd4e8a7..d785d9ce7 100644 --- a/cinder/tests/unit/volume/drivers/netapp/test_utils.py +++ b/cinder/tests/unit/volume/drivers/netapp/test_utils.py @@ -18,6 +18,7 @@ Mock unit tests for the NetApp driver utility module """ import copy +import ddt import platform import mock @@ -751,3 +752,38 @@ class OpenStackInfoTestCase(test.TestCase): 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') diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 561e0ca74..1f720a5d5 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -33,6 +33,7 @@ from cinder import exception 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: @@ -45,6 +46,8 @@ LOG = logging.getLogger(__name__) 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): @@ -56,6 +59,49 @@ class RestClient(object): 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 @@ -439,3 +485,57 @@ class RestClient(object): """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') diff --git a/cinder/volume/drivers/netapp/eseries/library.py b/cinder/volume/drivers/netapp/eseries/library.py index 2405fa2f9..78fc727f9 100644 --- a/cinder/volume/drivers/netapp/eseries/library.py +++ b/cinder/volume/drivers/netapp/eseries/library.py @@ -56,6 +56,8 @@ CONF.register_opts(na_opts.netapp_san_opts) 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', @@ -107,6 +109,7 @@ class NetAppESeriesLibrary(object): 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) @@ -153,6 +156,12 @@ class NetAppESeriesLibrary(object): 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() @@ -215,6 +224,7 @@ class NetAppESeriesLibrary(object): 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.""" @@ -879,6 +889,42 @@ class NetAppESeriesLibrary(object): 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. diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index 89cb34f61..f64784c18 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -437,3 +437,19 @@ class OpenStackInfo(object): 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