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:
parent
f9d7b383a7
commit
1f53bfcc79
@ -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,
|
||||
|
@ -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
42
nova/conf/vendordata.py
Normal 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')
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user