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 <sfinucan@redhat.com>
This commit is contained in:
Michael Still 2016-12-29 10:12:55 +11:00 committed by Stephen Finucane
parent f9d7b383a7
commit 1f53bfcc79
6 changed files with 113 additions and 33 deletions

View File

@ -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,

View File

@ -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)

42
nova/conf/vendordata.py Normal file
View File

@ -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')
)
}

View File

@ -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)

View File

@ -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()

View File

@ -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.