]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Implement AutoSupport for NetApp E-Series driver
authorRushil Chugh <rushil@netapp.com>
Mon, 4 May 2015 14:47:39 +0000 (10:47 -0400)
committerMichael Price <michael.price@netapp.com>
Thu, 27 Aug 2015 19:01:36 +0000 (14:01 -0500)
This patchset implements ASUP support for the NetApp
E-Series driver.

Implements blueprint netapp-e-series-asup
Change-Id: Idd371b99d519e280e9c82844172056c29814fd1b

cinder/tests/unit/test_netapp_eseries_iscsi.py
cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py
cinder/tests/unit/volume/drivers/netapp/eseries/test_library.py
cinder/tests/unit/volume/drivers/netapp/test_utils.py
cinder/volume/drivers/netapp/eseries/client.py
cinder/volume/drivers/netapp/eseries/library.py
cinder/volume/drivers/netapp/utils.py

index 269242af99fa652b089fb14395e58c5e1f54a232..ec849bfd8506a6ef1a96eaa95a9942ccc23f04e7 100644 (file)
@@ -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())
 
index 6285e41a66e7b4fa8f836ca35a8fe38abdb92c4d..1b1874cedab0fce8c21d68d3e1c431cd5516f411 100644 (file)
@@ -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
 
index d375e3346266744d347e61eb0d3688594139e72c..9083051bb227383d4c30873b4ada572db64c1062 100644 (file)
@@ -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)
index 20905082700832c4e413afd3e7f301205b7f794b..4b9e410fdc69a341f20ed89b99b957185e5643e1 100644 (file)
@@ -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.
index 68bd4e8a72ce54d9323d5ca7f73b800014e79939..d785d9ce77e6bcc2823eed1d79c90ded3a3ef873 100644 (file)
@@ -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')
index 561e0ca749ea92e01cea833dbc06891f592cb43b..1f720a5d5227b17f7687ee2be50336e204a24bd2 100644 (file)
@@ -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')
index 2405fa2f9d4bbddd548e53504e3b0d8d84f68e46..78fc727f91438c710e9a86d7b4ceb13ad3614542 100644 (file)
@@ -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.
index 89cb34f617e5e61401cbaa3687ec0b4c3d3d2de5..f64784c18f273c37508cf877fa0f96e24adf99ff 100644 (file)
@@ -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