diff --git a/manila/share/drivers/netapp/cluster_mode.py b/manila/share/drivers/netapp/cluster_mode.py index 3aadf504d7..a5e35b30bd 100644 --- a/manila/share/drivers/netapp/cluster_mode.py +++ b/manila/share/drivers/netapp/cluster_mode.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 NetApp, Inc. +# Copyright (c) 2015 Tom Barron. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -36,6 +37,7 @@ from manila.i18n import _LI from manila.openstack.common import log from manila.share import driver from manila.share.drivers.netapp import api as naapi +from manila.share.drivers.netapp import utils as na_utils from manila import utils @@ -135,6 +137,9 @@ class NetAppClusteredShareDriver(driver.ShareDriver): def __init__(self, db, *args, **kwargs): super(NetAppClusteredShareDriver, self).__init__(True, *args, **kwargs) + self._app_version = na_utils.OpenStackInfo().info() + LOG.info(_LI("OpenStack OS Version Info: %(info)s") % { + 'info': self._app_version}) self.db = db self._helpers = None self._licenses = [] @@ -198,6 +203,8 @@ class NetAppClusteredShareDriver(driver.ShareDriver): total_capacity_gb=(total / units.Gi), free_capacity_gb=(free / units.Gi)) super(NetAppClusteredShareDriver, self)._update_share_stats(data) + na_utils.provide_ems(self, self._client._client, self.backend_name, + self._app_version) def check_for_setup_error(self): """Raises error if prerequisites are not met.""" diff --git a/manila/share/drivers/netapp/utils.py b/manila/share/drivers/netapp/utils.py new file mode 100644 index 0000000000..34ce86a6b4 --- /dev/null +++ b/manila/share/drivers/netapp/utils.py @@ -0,0 +1,285 @@ +# Copyright (c) 2015 Bob Callaway. +# Copyright (c) 2015 Tom Barron. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Utilities for NetApp drivers. + +""" + +import copy +import platform +import socket + +from oslo.concurrency import processutils as putils +from oslo.utils import timeutils + +from manila.i18n import _LI, _LW +from manila.openstack.common import log as logging +from manila.share.drivers.netapp import api as na_api +from manila import version + + +LOG = logging.getLogger(__name__) + + +def provide_ems(requester, server, netapp_backend, app_version, + server_type="cluster"): + """Provide ems with volume stats for the requester. + + """ + # TODO(tbarron): rework provide_ems to not store timestamp in the caller. + # This requires upcoming Manila NetApp refactoring work. + + def _create_ems(netapp_backend, app_version, server_type): + """Create ems api request.""" + ems_log = na_api.NaElement('ems-autosupport-log') + host = socket.getfqdn() or 'Manila_node' + if server_type == "cluster": + dest = "cluster node" + else: + dest = "7 mode controller" + ems_log.add_new_child('computer-name', host) + ems_log.add_new_child('event-id', '0') + ems_log.add_new_child('event-source', + 'Manila driver %s' % netapp_backend) + ems_log.add_new_child('app-version', app_version) + ems_log.add_new_child('category', 'provisioning') + ems_log.add_new_child('event-description', + 'OpenStack Manila connected to %s' % dest) + ems_log.add_new_child('log-level', '6') + ems_log.add_new_child('auto-support', 'false') + return ems_log + + def _create_vs_get(): + """Create vs_get api request.""" + vs_get = na_api.NaElement('vserver-get-iter') + vs_get.add_new_child('max-records', '1') + query = na_api.NaElement('query') + query.add_node_with_children('vserver-info', + **{'vserver-type': 'node'}) + vs_get.add_child_elem(query) + desired = na_api.NaElement('desired-attributes') + desired.add_node_with_children( + 'vserver-info', **{'vserver-name': '', 'vserver-type': ''}) + vs_get.add_child_elem(desired) + return vs_get + + def _get_cluster_node(na_server): + """Get the cluster node for ems.""" + na_server.set_vserver(None) + vs_get = _create_vs_get() + res = na_server.invoke_successfully(vs_get) + if (res.get_child_content('num-records') and + int(res.get_child_content('num-records')) > 0): + attr_list = res.get_child_by_name('attributes-list') + vs_info = attr_list.get_child_by_name('vserver-info') + vs_name = vs_info.get_child_content('vserver-name') + return vs_name + return None + + do_ems = True + if hasattr(requester, 'last_ems'): + sec_limit = 3559 + if not (timeutils.is_older_than(requester.last_ems, sec_limit)): + do_ems = False + if do_ems: + na_server = copy.copy(server) + na_server.set_timeout(25) + ems = _create_ems(netapp_backend, app_version, server_type) + try: + if server_type == "cluster": + api_version = na_server.get_api_version() + if api_version: + major, minor = api_version + else: + raise na_api.NaApiError(code='Not found', + message='No api version found') + if major == 1 and minor > 15: + node = getattr(requester, 'vserver', None) + else: + node = _get_cluster_node(na_server) + if node is None: + raise na_api.NaApiError(code='Not found', + message='No vserver found') + na_server.set_vserver(node) + else: + na_server.set_vfiler(None) + na_server.invoke_successfully(ems, True) + LOG.debug("ems executed successfully.") + except na_api.NaApiError as e: + LOG.warn(_LW("Failed to invoke ems. Message : %s") % e) + finally: + requester.last_ems = timeutils.utcnow() + + +class OpenStackInfo(object): + """OS/distribution, release, and version. + + NetApp uses these fields as content for EMS log entry. + """ + + PACKAGE_NAME = 'python-manila' + + def __init__(self): + self._version = 'unknown version' + self._release = 'unknown release' + self._vendor = 'unknown vendor' + self._platform = 'unknown platform' + + @property + def version(self): + return self._version + + @version.setter + def version(self, value): + self._version = value + + @property + def release(self): + return self._release + + @release.setter + def release(self, value): + self._release = value + + @property + def vendor(self): + return self._vendor + + @vendor.setter + def vendor(self, value): + self._vendor = value + + @property + def platform(self): + return self._platform + + @platform.setter + def platform(self, value): + self._platform = value + + # Because of the variety of platforms and OpenStack distributions that we + # may run against, it is by no means a sure thing that these update methods + # will work. We collect what information we can and deliberately ignore + # exceptions of any kind in order to avoid fillling the manila share log + # with noise. + def _update_version_from_version_string(self): + try: + self.version = version.version_info.version_string() + except Exception: + pass + + def _update_release_from_release_string(self): + try: + self.release = version.version_info.release_string() + except Exception: + pass + + def _update_platform(self): + try: + self.platform = platform.platform() + except Exception: + pass + + @staticmethod + def _get_version_info_version(): + return version.version_info.version + + @staticmethod + def _get_version_info_release(): + return version.version_info.release + + def _update_info_from_version_info(self): + try: + ver = self._get_version_info_version() + if ver: + self.version = ver + except Exception: + pass + try: + rel = self._get_version_info_release() + if rel: + self.release = rel + except Exception: + pass + + # RDO, RHEL-OSP, Mirantis on Redhat, SUSE + def _update_info_from_rpm(self): + LOG.debug('Trying rpm command.') + try: + out, err = putils.execute("rpm", "-q", "--queryformat", + "'%{version}\t%{release}\t%{vendor}'", + self.PACKAGE_NAME) + if not out: + LOG.info(_LI('No rpm info found for %(pkg)s package.') % { + 'pkg': self.PACKAGE_NAME}) + return False + parts = out.split() + self.version = parts[0] + self.release = parts[1] + self.vendor = ' '.join(parts[2::]) + return True + except Exception as e: + LOG.info(_LI('Could not run rpm command: %(msg)s.') % { + 'msg': e}) + return False + + # ubuntu, mirantis on ubuntu + def _update_info_from_dpkg(self): + LOG.debug('Trying dpkg-query command.') + try: + _vendor = None + out, err = putils.execute("dpkg-query", "-W", "-f='${Version}'", + self.PACKAGE_NAME) + if not out: + LOG.info(_LI( + 'No dpkg-query info found for %(pkg)s package.') % { + 'pkg': self.PACKAGE_NAME}) + return False + # debian format: [epoch:]upstream_version[-debian_revision] + deb_version = out + # in case epoch or revision is missing, copy entire string + _release = deb_version + if ':' in deb_version: + deb_epoch, upstream_version = deb_version.split(':') + _release = upstream_version + if '-' in deb_version: + deb_revision = deb_version.split('-')[1] + _vendor = deb_revision + self.release = _release + if _vendor: + self.vendor = _vendor + return True + except Exception as e: + LOG.info(_LI('Could not run dpkg-query command: %(msg)s.') % { + 'msg': e}) + return False + + def _update_openstack_info(self): + self._update_version_from_version_string() + self._update_release_from_release_string() + self._update_platform() + # some distributions override with more meaningful information + self._update_info_from_version_info() + # see if we have still more targeted info from rpm or apt + found_package = self._update_info_from_rpm() + if not found_package: + self._update_info_from_dpkg() + + def info(self): + self._update_openstack_info() + return '%(version)s|%(release)s|%(vendor)s|%(platform)s' % { + 'version': self.version, 'release': self.release, + 'vendor': self.vendor, 'platform': self.platform} diff --git a/manila/tests/share/drivers/netapp/test_cluster_mode.py b/manila/tests/share/drivers/netapp/test_cluster_mode.py index d8e4987b72..53cb259d5b 100644 --- a/manila/tests/share/drivers/netapp/test_cluster_mode.py +++ b/manila/tests/share/drivers/netapp/test_cluster_mode.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 NetApp, Inc. +# Copyright (c) 2015 Tom Barron. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -23,6 +24,7 @@ from manila import exception from manila.share import configuration from manila.share.drivers.netapp import api as naapi from manila.share.drivers.netapp import cluster_mode as driver +from manila.share.drivers.netapp import utils as na_utils from manila import test from manila import utils @@ -30,6 +32,10 @@ from manila import utils class NetAppClusteredDrvTestCase(test.TestCase): """Test suite for NetApp Cluster Mode driver.""" + @mock.patch.object(na_utils.OpenStackInfo, '_update_info_from_rpm', + mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, '_update_info_from_dpkg', + mock.Mock()) def setUp(self): super(NetAppClusteredDrvTestCase, self).setUp() self._context = context.get_admin_context() @@ -114,6 +120,7 @@ class NetAppClusteredDrvTestCase(test.TestCase): ] ) + @mock.patch.object(na_utils, 'provide_ems', mock.Mock()) def test_update_share_stats(self): """Retrieve status info from share volume group.""" fake_aggr1_struct = { diff --git a/manila/tests/share/drivers/netapp/test_utils.py b/manila/tests/share/drivers/netapp/test_utils.py new file mode 100644 index 0000000000..a8d4a52407 --- /dev/null +++ b/manila/tests/share/drivers/netapp/test_utils.py @@ -0,0 +1,268 @@ +# Copyright 2015 Tom Barron. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import platform + +import mock +from oslo.concurrency import processutils as putils + +from manila.share.drivers.netapp import utils as na_utils +from manila import test +from manila import version + + +class OpenstackInfoTestCase(test.TestCase): + + UNKNOWN_VERSION = 'unknown version' + UNKNOWN_RELEASE = 'unknown release' + UNKNOWN_VENDOR = 'unknown vendor' + UNKNOWN_PLATFORM = 'unknown platform' + VERSION_STRING_RET_VAL = 'fake_version_1' + RELEASE_STRING_RET_VAL = 'fake_release_1' + PLATFORM_RET_VAL = 'fake_platform_1' + VERSION_INFO_VERSION = 'fake_version_2' + VERSION_INFO_RELEASE = 'fake_release_2' + RPM_INFO_VERSION = 'fake_version_3' + RPM_INFO_RELEASE = 'fake_release_3' + RPM_INFO_VENDOR = 'fake vendor 3' + PUTILS_RPM_RET_VAL = ('fake_version_3 fake_release_3 fake vendor 3', '') + NO_PKG_FOUND = ('', 'whatever') + PUTILS_DPKG_RET_VAL = ('epoch:upstream_version-debian_revision', '') + DEB_RLS = 'upstream_version-debian_revision' + DEB_VENDOR = 'debian_revision' + + def setUp(self): + super(OpenstackInfoTestCase, self).setUp() + + def test_openstack_info_init(self): + info = na_utils.OpenStackInfo() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'version_string', + mock.Mock(return_value=VERSION_STRING_RET_VAL)) + def test_update_version_from_version_string(self): + info = na_utils.OpenStackInfo() + info._update_version_from_version_string() + + self.assertEqual(self.VERSION_STRING_RET_VAL, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'version_string', + mock.Mock(side_effect=Exception)) + def test_exception_in_update_version_from_version_string(self): + info = na_utils.OpenStackInfo() + info._update_version_from_version_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'release_string', + mock.Mock(return_value=RELEASE_STRING_RET_VAL)) + def test_update_release_from_release_string(self): + info = na_utils.OpenStackInfo() + info._update_release_from_release_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.RELEASE_STRING_RET_VAL, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(version.version_info, 'release_string', + mock.Mock(side_effect=Exception)) + def test_exception_in_update_release_from_release_string(self): + info = na_utils.OpenStackInfo() + info._update_release_from_release_string() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(platform, 'platform', + mock.Mock(return_value=PLATFORM_RET_VAL)) + def test_update_platform(self): + info = na_utils.OpenStackInfo() + info._update_platform() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.PLATFORM_RET_VAL, info._platform) + + @mock.patch.object(platform, 'platform', + mock.Mock(side_effect=Exception)) + def test_exception_in_update_platform(self): + info = na_utils.OpenStackInfo() + info._update_platform() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value=VERSION_INFO_VERSION)) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(return_value=VERSION_INFO_RELEASE)) + def test_update_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.VERSION_INFO_VERSION, info._version) + self.assertEqual(self.VERSION_INFO_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value='')) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(return_value=None)) + def test_no_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_version', + mock.Mock(return_value=VERSION_INFO_VERSION)) + @mock.patch.object(na_utils.OpenStackInfo, '_get_version_info_release', + mock.Mock(side_effect=Exception)) + def test_exception_in_info_from_version_info(self): + info = na_utils.OpenStackInfo() + info._update_info_from_version_info() + + self.assertEqual(self.VERSION_INFO_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=PUTILS_RPM_RET_VAL)) + def test_update_info_from_rpm(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.RPM_INFO_VERSION, info._version) + self.assertEqual(self.RPM_INFO_RELEASE, info._release) + self.assertEqual(self.RPM_INFO_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertTrue(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=NO_PKG_FOUND)) + def test_update_info_from_rpm_no_pkg_found(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(side_effect=Exception)) + def test_exception_in_update_info_from_rpm(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_rpm() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=PUTILS_DPKG_RET_VAL)) + def test_update_info_from_dpkg(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.DEB_RLS, info._release) + self.assertEqual(self.DEB_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertTrue(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(return_value=NO_PKG_FOUND)) + def test_update_info_from_dpkg_no_pkg_found(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(putils, 'execute', + mock.Mock(side_effect=Exception)) + def test_exception_in_update_info_from_dpkg(self): + info = na_utils.OpenStackInfo() + found_package = info._update_info_from_dpkg() + + self.assertEqual(self.UNKNOWN_VERSION, info._version) + self.assertEqual(self.UNKNOWN_RELEASE, info._release) + self.assertEqual(self.UNKNOWN_VENDOR, info._vendor) + self.assertEqual(self.UNKNOWN_PLATFORM, info._platform) + self.assertFalse(found_package) + + @mock.patch.object(na_utils.OpenStackInfo, + '_update_version_from_version_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_release_from_release_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_platform', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_version_info', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_rpm', mock.Mock(return_value=True)) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_dpkg') + def test_update_openstack_info_rpm_pkg_found(self, mock_updt_from_dpkg): + info = na_utils.OpenStackInfo() + info._update_openstack_info() + + self.assertFalse(mock_updt_from_dpkg.called) + + @mock.patch.object(na_utils.OpenStackInfo, + '_update_version_from_version_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_release_from_release_string', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_platform', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_version_info', mock.Mock()) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_rpm', mock.Mock(return_value=False)) + @mock.patch.object(na_utils.OpenStackInfo, + '_update_info_from_dpkg') + def test_update_openstack_info_rpm_pkg_not_found(self, + mock_updt_from_dpkg): + info = na_utils.OpenStackInfo() + info._update_openstack_info() + + self.assertTrue(mock_updt_from_dpkg.called)