From 1f53bfcc7998f63f130a2cedaf15b41a4506c568 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Thu, 29 Dec 2016 10:12:55 +1100 Subject: [PATCH] Use a service account to make vendordata requests. We should use a service account to make requests to external vendordata services. This something which we got wrong in the newton cycle, and discussed how to resolve at the ocata summit. It is intended that this fix be backported to newton as well. There is a sample external vendordata server which has been tested with this implementat at: https://github.com/mikalstill/vendordata Change-Id: I7d29ecc00f99724731d120ff94b4bf3210f3a64e Co-Authored-By: Stephen Finucane --- nova/api/metadata/vendordata_dynamic.py | 52 +++++++++++++------ nova/conf/__init__.py | 2 + nova/conf/vendordata.py | 42 +++++++++++++++ nova/tests/functional/test_metadata.py | 14 ++--- nova/tests/unit/test_metadata.py | 22 ++++---- ...rdata-service-tokens-876505167395a56d.yaml | 14 +++++ 6 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 nova/conf/vendordata.py create mode 100644 releasenotes/notes/vendordata-service-tokens-876505167395a56d.yaml diff --git a/nova/api/metadata/vendordata_dynamic.py b/nova/api/metadata/vendordata_dynamic.py index ffa97d8a79d0..5f979e4e0f1c 100644 --- a/nova/api/metadata/vendordata_dynamic.py +++ b/nova/api/metadata/vendordata_dynamic.py @@ -17,6 +17,8 @@ import requests +from keystoneauth1 import exceptions as ks_exceptions +from keystoneauth1 import loading as ks_loading from oslo_log import log as logging from oslo_serialization import jsonutils @@ -27,15 +29,34 @@ from nova.i18n import _LW CONF = nova.conf.CONF LOG = logging.getLogger(__name__) +_SESSION = None +_ADMIN_AUTH = None -def generate_identity_headers(context, status='Confirmed'): - return { - 'X-Auth-Token': getattr(context, 'auth_token', None), - 'X-User-Id': getattr(context, 'user', None), - 'X-Project-Id': getattr(context, 'tenant', None), - 'X-Roles': ','.join(getattr(context, 'roles', [])), - 'X-Identity-Status': status, - } + +def _load_ks_session(conf): + """Load session. + + This is either an authenticated session or a requests session, depending on + what's configured. + """ + global _ADMIN_AUTH + global _SESSION + + if not _ADMIN_AUTH: + _ADMIN_AUTH = ks_loading.load_auth_from_conf_options( + conf, nova.conf.vendordata.vendordata_group.name) + + if not _ADMIN_AUTH: + LOG.warning(_LW('Passing insecure dynamic vendordata requests ' + 'because of missing or incorrect service account ' + 'configuration.')) + + if not _SESSION: + _SESSION = ks_loading.load_session_from_conf_options( + conf, nova.conf.vendordata.vendordata_group.name, + auth=_ADMIN_AUTH) + + return _SESSION class DynamicVendorData(vendordata.VendorDataDriver): @@ -46,6 +67,7 @@ class DynamicVendorData(vendordata.VendorDataDriver): # JSON plugin. self.context = context self.instance = instance + self.session = _load_ks_session(CONF) def _do_request(self, service_name, url): try: @@ -59,9 +81,6 @@ class DynamicVendorData(vendordata.VendorDataDriver): 'Accept': 'application/json', 'User-Agent': 'openstack-nova-vendordata'} - if self.context: - headers.update(generate_identity_headers(self.context)) - # SSL verification verify = url.startswith('https://') @@ -71,9 +90,9 @@ class DynamicVendorData(vendordata.VendorDataDriver): timeout = (CONF.api.vendordata_dynamic_connect_timeout, CONF.api.vendordata_dynamic_read_timeout) - res = requests.request('POST', url, data=jsonutils.dumps(body), - headers=headers, verify=verify, - timeout=timeout) + res = self.session.request(url, 'POST', data=jsonutils.dumps(body), + verify=verify, headers=headers, + timeout=timeout) if res.status_code in (requests.codes.OK, requests.codes.CREATED, requests.codes.ACCEPTED): @@ -83,8 +102,9 @@ class DynamicVendorData(vendordata.VendorDataDriver): return {} - except (TypeError, ValueError, requests.exceptions.RequestException, - requests.exceptions.SSLError) as e: + except (TypeError, ValueError, + ks_exceptions.connection.ConnectionError, + ks_exceptions.http.HttpError) as e: LOG.warning(_LW('Error from dynamic vendordata service ' '%(service_name)s at %(url)s: %(error)s'), {'service_name': service_name, diff --git a/nova/conf/__init__.py b/nova/conf/__init__.py index c102ddd1108a..b3306af1ec22 100644 --- a/nova/conf/__init__.py +++ b/nova/conf/__init__.py @@ -65,6 +65,7 @@ from nova.conf import servicegroup from nova.conf import spice from nova.conf import ssl from nova.conf import upgrade_levels +from nova.conf import vendordata from nova.conf import vmware from nova.conf import vnc from nova.conf import workarounds @@ -119,6 +120,7 @@ servicegroup.register_opts(CONF) spice.register_opts(CONF) ssl.register_opts(CONF) upgrade_levels.register_opts(CONF) +vendordata.register_opts(CONF) vmware.register_opts(CONF) vnc.register_opts(CONF) workarounds.register_opts(CONF) diff --git a/nova/conf/vendordata.py b/nova/conf/vendordata.py new file mode 100644 index 000000000000..a669481326ed --- /dev/null +++ b/nova/conf/vendordata.py @@ -0,0 +1,42 @@ +# Copyright 2015 OpenStack Foundation +# 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. + +from keystoneauth1 import loading as ks_loading +from oslo_config import cfg + +vendordata_group = cfg.OptGroup('vendordata_dynamic_auth', + title='Vendordata dynamic fetch auth options', + help=""" +Options within this group control the authentication of the vendordata +subsystem of the metadata API server (and config drive) with external systems. +""") + + +def register_opts(conf): + conf.register_group(vendordata_group) + ks_loading.register_session_conf_options(conf, vendordata_group.name) + ks_loading.register_auth_conf_options(conf, vendordata_group.name) + + +def list_opts(): + return { + vendordata_group: ( + ks_loading.get_session_conf_options() + + ks_loading.get_auth_common_conf_options() + + ks_loading.get_auth_plugin_conf_options('password') + + ks_loading.get_auth_plugin_conf_options('v2password') + + ks_loading.get_auth_plugin_conf_options('v3password') + ) + } diff --git a/nova/tests/functional/test_metadata.py b/nova/tests/functional/test_metadata.py index 1e7ff1e92988..8a75012a44e1 100644 --- a/nova/tests/functional/test_metadata.py +++ b/nova/tests/functional/test_metadata.py @@ -38,7 +38,7 @@ class fake_result(object): real_request = requests.request -def fake_request(method, url, **kwargs): +def fake_request(obj, url, method, **kwargs): if url.startswith('http://127.0.0.1:123'): return fake_result({'a': 1, 'b': 'foo'}) if url.startswith('http://127.0.0.1:124'): @@ -114,8 +114,8 @@ class MetadataTest(test.TestCase): group='api' ) - self.useFixture(fixtures.MonkeyPatch('requests.request', - fake_request)) + self.useFixture(fixtures.MonkeyPatch( + 'keystoneauth1.session.Session.request', fake_request)) url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url res = requests.request('GET', url, timeout=5) @@ -138,8 +138,8 @@ class MetadataTest(test.TestCase): group='api' ) - self.useFixture(fixtures.MonkeyPatch('requests.request', - fake_request)) + self.useFixture(fixtures.MonkeyPatch( + 'keystoneauth1.session.Session.request', fake_request)) url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url res = requests.request('GET', url, timeout=5) @@ -165,8 +165,8 @@ class MetadataTest(test.TestCase): group='api' ) - self.useFixture(fixtures.MonkeyPatch('requests.request', - fake_request)) + self.useFixture(fixtures.MonkeyPatch( + 'keystoneauth1.session.Session.request', fake_request)) url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url res = requests.request('GET', url, timeout=5) diff --git a/nova/tests/unit/test_metadata.py b/nova/tests/unit/test_metadata.py index 21be2e0f66d1..c25593ee6f50 100644 --- a/nova/tests/unit/test_metadata.py +++ b/nova/tests/unit/test_metadata.py @@ -28,6 +28,8 @@ try: except ImportError: import pickle +from keystoneauth1 import exceptions as ks_exceptions +from keystoneauth1 import session import mock from oslo_config import cfg from oslo_serialization import base64 @@ -866,22 +868,22 @@ class OpenStackMetadataTestCase(test.TestCase): else: self.assertEqual({}, vd['web']) - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_ok(self, request_mock): self._test_vendordata2_response_inner(request_mock, requests.codes.OK) - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_created(self, request_mock): self._test_vendordata2_response_inner(request_mock, requests.codes.CREATED) - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_accepted(self, request_mock): self._test_vendordata2_response_inner(request_mock, requests.codes.ACCEPTED) - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_no_content(self, request_mock): self._test_vendordata2_response_inner(request_mock, requests.codes.NO_CONTENT, @@ -918,34 +920,34 @@ class OpenStackMetadataTestCase(test.TestCase): self.assertTrue(log_mock.called) @mock.patch.object(vendordata_dynamic.LOG, 'warning') - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_type_error(self, request_mock, log_mock): self._test_vendordata2_response_inner_exceptional( request_mock, log_mock, TypeError) @mock.patch.object(vendordata_dynamic.LOG, 'warning') - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_value_error(self, request_mock, log_mock): self._test_vendordata2_response_inner_exceptional( request_mock, log_mock, ValueError) @mock.patch.object(vendordata_dynamic.LOG, 'warning') - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_request_error(self, request_mock, log_mock): self._test_vendordata2_response_inner_exceptional( - request_mock, log_mock, requests.exceptions.RequestException) + request_mock, log_mock, ks_exceptions.BadRequest) @mock.patch.object(vendordata_dynamic.LOG, 'warning') - @mock.patch.object(requests, 'request') + @mock.patch.object(session.Session, 'request') def test_vendor_data_response_vendordata2_ssl_error(self, request_mock, log_mock): self._test_vendordata2_response_inner_exceptional( - request_mock, log_mock, requests.exceptions.SSLError) + request_mock, log_mock, ks_exceptions.SSLError) def test_network_data_presence(self): inst = self.instance.obj_clone() diff --git a/releasenotes/notes/vendordata-service-tokens-876505167395a56d.yaml b/releasenotes/notes/vendordata-service-tokens-876505167395a56d.yaml new file mode 100644 index 000000000000..ad13652569e8 --- /dev/null +++ b/releasenotes/notes/vendordata-service-tokens-876505167395a56d.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + The nova metadata service will now pass a nove service token to the + external vendordata server. These options can be configured using various + Keystone-related options available in the ``vendordata_dynamic_auth`` + group. A new service token has been created for this purpose. Previously, + the requesting user's keystone token was passed through to the external + vendordata server if available, otherwise no token is passed. This resolves + issues with scenarios such as cloud-init's use of the metadata server on + first boot to determine configuration information. Refer to the blueprints + at + http://specs.openstack.org/openstack/nova-specs/specs/ocata/approved/vendordata-reboot-ocata.html + for more information.