From: Bob Callaway Date: Fri, 20 Jun 2014 11:31:29 +0000 (-0400) Subject: Fix NetApp AutoSupport Shortcomings. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=b3be30b14ce3f31af8a5f23542246ca8d0c4da8c;p=openstack-build%2Fcinder-build.git Fix NetApp AutoSupport Shortcomings. This patch addresses several problems with the current implementation. 1. Appending a record to EMS should not in itself trigger ASUP delivery. These should be separately scheduled and Openstack cinder should have no role in ASUP scheduling or delivery, only a role in logging via EMS. 2. Log frequency should be adjusted from weekly to hourly. 3. The log message should be useful for support. It should include release (Havana, Icehouse, Juno, etc.) version (2014.1.1), and distribution information (RHEL-OSP, etc.) rather than simply noting that the message came from "Openstack." Closes-Bug: 1367676 Change-Id: I2f81fed18342fe384e3c61184948f1e4052765d5 --- diff --git a/cinder/tests/test_netapp_utils.py b/cinder/tests/test_netapp_utils.py new file mode 100644 index 000000000..0c9ac42a4 --- /dev/null +++ b/cinder/tests/test_netapp_utils.py @@ -0,0 +1,268 @@ +# Copyright 2014 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 cinder.openstack.common import processutils as putils +from cinder import test +from cinder import version +from cinder.volume.drivers.netapp import utils as na_utils + + +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_xcption_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_xcption_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_xcption_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_xcption_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_xcption_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_xcption_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) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 938a218c0..ff233a070 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -25,6 +25,7 @@ from cinder.openstack.common import importutils from cinder.openstack.common import log as logging from cinder.volume import driver from cinder.volume.drivers.netapp.options import netapp_proxy_opts +from cinder.volume.drivers.netapp import utils LOG = logging.getLogger(__name__) @@ -75,12 +76,16 @@ class NetAppDriver(object): def __init__(self, *args, **kwargs): super(NetAppDriver, self).__init__() + app_version = utils.OpenStackInfo().info() + LOG.info(_('OpenStack OS Version Info: %(info)s') % { + 'info': app_version}) self.configuration = kwargs.get('configuration', None) if self.configuration: self.configuration.append_config_values(netapp_proxy_opts) else: raise exception.InvalidInput( reason=_("Required configuration not found")) + kwargs['app_version'] = app_version self.driver = NetAppDriverFactory.create_driver( self.configuration.netapp_storage_family, self.configuration.netapp_storage_protocol, diff --git a/cinder/volume/drivers/netapp/iscsi.py b/cinder/volume/drivers/netapp/iscsi.py index e08c0dc10..6897932d2 100644 --- a/cinder/volume/drivers/netapp/iscsi.py +++ b/cinder/volume/drivers/netapp/iscsi.py @@ -84,6 +84,7 @@ class NetAppLun(object): class NetAppDirectISCSIDriver(driver.ISCSIDriver): """NetApp Direct iSCSI volume driver.""" + # do not increment this as it may be used in volume type definitions VERSION = "1.0.0" IGROUP_PREFIX = 'openstack-' @@ -92,6 +93,7 @@ class NetAppDirectISCSIDriver(driver.ISCSIDriver): 'netapp_server_port'] def __init__(self, *args, **kwargs): + self._app_version = kwargs.pop("app_version", "unknown") super(NetAppDirectISCSIDriver, self).__init__(*args, **kwargs) validate_instantiation(**kwargs) self.configuration.append_config_values(netapp_connection_opts) @@ -1081,7 +1083,8 @@ class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver): data['storage_protocol'] = 'iSCSI' data['pools'] = self._get_pool_stats() - na_utils.provide_ems(self, self.client, data, netapp_backend) + na_utils.provide_ems(self, self.client, netapp_backend, + self._app_version) self._stats = data def _get_pool_stats(self): @@ -1494,8 +1497,8 @@ class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver): data['storage_protocol'] = 'iSCSI' data['pools'] = self._get_pool_stats() - na_utils.provide_ems(self, self.client, data, netapp_backend, - server_type='7mode') + na_utils.provide_ems(self, self.client, netapp_backend, + self._app_version, server_type='7mode') self._stats = data def _get_pool_stats(self): diff --git a/cinder/volume/drivers/netapp/nfs.py b/cinder/volume/drivers/netapp/nfs.py index c74f8551e..8a7062e3a 100644 --- a/cinder/volume/drivers/netapp/nfs.py +++ b/cinder/volume/drivers/netapp/nfs.py @@ -59,6 +59,7 @@ class NetAppNFSDriver(nfs.NfsDriver): Executes commands relating to Volumes. """ + # do not increment this as it may be used in volume type definitions VERSION = "1.0.0" def __init__(self, *args, **kwargs): @@ -66,6 +67,7 @@ class NetAppNFSDriver(nfs.NfsDriver): validate_instantiation(**kwargs) self._execute = None self._context = None + self._app_version = kwargs.pop("app_version", "unknown") super(NetAppNFSDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(netapp_connection_opts) self.configuration.append_config_values(netapp_basicauth_opts) @@ -961,7 +963,8 @@ class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver): data['pools'] = self._get_pool_stats() self._spawn_clean_cache_job() - na_utils.provide_ems(self, self._client, data, netapp_backend) + na_utils.provide_ems(self, self._client, netapp_backend, + self._app_version) self._stats = data def _get_pool_stats(self): @@ -1510,8 +1513,8 @@ class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver): data['pools'] = self._get_pool_stats() self._spawn_clean_cache_job() - na_utils.provide_ems(self, self._client, data, netapp_backend, - server_type="7mode") + na_utils.provide_ems(self, self._client, netapp_backend, + self._app_version, server_type="7mode") self._stats = data def _get_pool_stats(self): diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index 6a6bfc0e1..85d24ca00 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -24,6 +24,7 @@ import base64 import binascii import copy import decimal +import platform import socket import uuid @@ -33,8 +34,10 @@ from cinder import context from cinder import exception from cinder.i18n import _ from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils as putils from cinder.openstack.common import timeutils from cinder import utils +from cinder import version from cinder.volume.drivers.netapp.api import NaApiError from cinder.volume.drivers.netapp.api import NaElement from cinder.volume.drivers.netapp.api import NaErrors @@ -53,29 +56,31 @@ DEPRECATED_SSC_SPECS = {'netapp_unmirrored': 'netapp_mirrored', 'netapp_thick_provisioned': 'netapp_thin_provisioned'} -def provide_ems(requester, server, stats, netapp_backend, +def provide_ems(requester, server, netapp_backend, app_version, server_type="cluster"): """Provide ems with volume stats for the requester. :param server_type: cluster or 7mode. """ - def _create_ems(stats, netapp_backend, server_type): + + def _create_ems(netapp_backend, app_version, server_type): """Create ems api request.""" ems_log = NaElement('ems-autosupport-log') host = socket.getfqdn() or 'Cinder_node' - dest = "cluster node" if server_type == "cluster"\ - else "7 mode controller" + 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', 'Cinder driver %s' % netapp_backend) - ems_log.add_new_child('app-version', stats.get('driver_version', - 'Undefined')) + ems_log.add_new_child('app-version', app_version) ems_log.add_new_child('category', 'provisioning') ems_log.add_new_child('event-description', - 'OpenStack volume created on %s' % dest) + 'OpenStack Cinder connected to %s' % dest) ems_log.add_new_child('log-level', '6') - ems_log.add_new_child('auto-support', 'true') + ems_log.add_new_child('auto-support', 'false') return ems_log def _create_vs_get(): @@ -107,14 +112,13 @@ def provide_ems(requester, server, stats, netapp_backend, do_ems = True if hasattr(requester, 'last_ems'): - sec_limit = 604800 - if not (timeutils.is_older_than(requester.last_ems, sec_limit) or - timeutils.is_older_than(requester.last_ems, sec_limit - 59)): + 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(stats, netapp_backend, server_type) + ems = _create_ems(netapp_backend, app_version, server_type) try: if server_type == "cluster": api_version = na_server.get_api_version() @@ -385,3 +389,126 @@ def log_extra_spec_warnings(extra_specs): msg = _('Extra spec %(old)s is deprecated. Use %(new)s instead.') args = {'old': spec, 'new': DEPRECATED_SSC_SPECS[spec]} LOG.warn(msg % args) + + +class OpenStackInfo(object): + """OS/distribution, release, and version. + + NetApp uses these fields as content for EMS log entry. + """ + + PACKAGE_NAME = 'python-cinder' + + def __init__(self): + self._version = 'unknown version' + self._release = 'unknown release' + self._vendor = 'unknown vendor' + self._platform = 'unknown platform' + + 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", "-qa", "--queryformat", + "'%{version}\t%{release}\t%{vendor}'", + self.PACKAGE_NAME) + if not out: + LOG.info(_('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(_('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(_('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(_('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}