diff --git a/doc/source/vendordata.rst b/doc/source/vendordata.rst new file mode 100644 index 000000000000..86bcb21a17f1 --- /dev/null +++ b/doc/source/vendordata.rst @@ -0,0 +1 @@ +TODO mikal diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py index c29a14b1ce2c..4b9bba86fb0f 100644 --- a/nova/api/metadata/base.py +++ b/nova/api/metadata/base.py @@ -28,12 +28,16 @@ import six from nova.api.ec2 import ec2utils from nova.api.metadata import password +from nova.api.metadata import vendordata +from nova.api.metadata import vendordata_dynamic +from nova.api.metadata import vendordata_json from nova import availability_zones as az from nova import block_device from nova.cells import opts as cells_opts from nova.cells import rpcapi as cells_rpcapi import nova.conf from nova import context +from nova.i18n import _LI, _LW from nova import network from nova.network.security_group import openstack_driver from nova import objects @@ -56,18 +60,26 @@ VERSIONS = [ '2009-04-04', ] +# NOTE(mikal): think of these strings as version numbers. They traditionally +# correlate with OpenStack release dates, with all the changes for a given +# release bundled into a single version. Note that versions in the future are +# hidden from the listing, but can still be requested explicitly, which is +# required for testing purposes. We know this isn't great, but its inherited +# from EC2, which this needs to be compatible with. FOLSOM = '2012-08-10' GRIZZLY = '2013-04-04' HAVANA = '2013-10-17' LIBERTY = '2015-10-15' -NEWTON = '2016-06-30' +NEWTON_ONE = '2016-06-30' +NEWTON_TWO = '2016-10-06' OPENSTACK_VERSIONS = [ FOLSOM, GRIZZLY, HAVANA, LIBERTY, - NEWTON, + NEWTON_ONE, + NEWTON_TWO, ] VERSION = "version" @@ -75,6 +87,7 @@ CONTENT = "content" CONTENT_DIR = "content" MD_JSON_NAME = "meta_data.json" VD_JSON_NAME = "vendor_data.json" +VD2_JSON_NAME = "vendor_data2.json" NW_JSON_NAME = "network_data.json" UD_NAME = "user_data" PASS_NAME = "password" @@ -96,7 +109,8 @@ class InstanceMetadata(object): """Instance metadata.""" def __init__(self, instance, address=None, content=None, extra_md=None, - network_info=None, vd_driver=None, network_metadata=None): + network_info=None, vd_driver=None, network_metadata=None, + request_context=None): """Creation of this object should basically cover all time consuming collection. Methods after that should not cause time delays due to network operations or lengthy cpu operations. @@ -182,6 +196,19 @@ class InstanceMetadata(object): self.route_configuration = None + # NOTE(mikal): the decision to not pass extra_md here like we + # do to the StaticJSON driver is deliberate. extra_md will + # contain the admin password for the instance, and we shouldn't + # pass that to external services. + self.vendordata_providers = { + 'StaticJSON': vendordata_json.JsonFileVendorData( + instance=instance, address=address, + extra_md=extra_md, network_info=network_info), + 'DynamicJSON': vendordata_dynamic.DynamicVendorData( + instance=instance, address=address, + network_info=network_info, context=request_context) + } + def _route_configuration(self): if self.route_configuration: return self.route_configuration @@ -189,6 +216,7 @@ class InstanceMetadata(object): path_handlers = {UD_NAME: self._user_data, PASS_NAME: self._password, VD_JSON_NAME: self._vendor_data, + VD2_JSON_NAME: self._vendor_data2, MD_JSON_NAME: self._metadata_as_json, NW_JSON_NAME: self._network_data, VERSION: self._handle_version, @@ -342,7 +370,7 @@ class InstanceMetadata(object): if self._check_os_version(LIBERTY, version): metadata['project_id'] = self.instance.project_id - if self._check_os_version(NEWTON, version): + if self._check_os_version(NEWTON_ONE, version): metadata['devices'] = self._get_device_metadata() self.set_mimetype(MIME_TYPE_APPLICATION_JSON) @@ -425,6 +453,8 @@ class InstanceMetadata(object): ret.append(VD_JSON_NAME) if self._check_os_version(LIBERTY, version): ret.append(NW_JSON_NAME) + if self._check_os_version(NEWTON_TWO, version): + ret.append(VD2_JSON_NAME) return ret @@ -446,7 +476,43 @@ class InstanceMetadata(object): def _vendor_data(self, version, path): if self._check_os_version(HAVANA, version): self.set_mimetype(MIME_TYPE_APPLICATION_JSON) - return jsonutils.dump_as_bytes(self.vddriver.get()) + + # NOTE(mikal): backwards compatability... If the deployer has + # specified providers, and one of those providers is StaticJSON, + # then do that thing here. Otherwise, if the deployer has + # specified an old style driver here, then use that. This second + # bit can be removed once old style vendordata is fully deprecated + # and removed. + if (CONF.vendordata_providers and + 'StaticJSON' in CONF.vendordata_providers): + return jsonutils.dump_as_bytes( + self.vendordata_providers['StaticJSON'].get()) + else: + # TODO(mikal): when we removed the old style vendordata + # drivers, we need to remove self.vddriver as well. + return jsonutils.dump_as_bytes(self.vddriver.get()) + + raise KeyError(path) + + def _vendor_data2(self, version, path): + if self._check_os_version(NEWTON_TWO, version): + self.set_mimetype(MIME_TYPE_APPLICATION_JSON) + + j = {} + for provider in CONF.vendordata_providers: + if provider == 'StaticJSON': + j['static'] = self.vendordata_providers['StaticJSON'].get() + else: + values = self.vendordata_providers[provider].get() + for key in list(values): + if key in j: + LOG.warning(_LW('Removing duplicate metadata key: ' + '%s'), key, instance=self.instance) + del values[key] + j.update(values) + + return jsonutils.dump_as_bytes(j) + raise KeyError(path) def _check_version(self, required, requested, versions=VERSIONS): @@ -490,7 +556,7 @@ class InstanceMetadata(object): if OPENSTACK_VERSIONS != versions: LOG.debug("future versions %s hidden in version list", [v for v in OPENSTACK_VERSIONS - if v not in versions]) + if v not in versions], instance=self.instance) versions += ["latest"] else: versions = VERSIONS + ["latest"] @@ -544,6 +610,11 @@ class InstanceMetadata(object): path = 'openstack/%s/%s' % (version, NW_JSON_NAME) yield (path, self.lookup(path)) + if self._check_version(NEWTON_TWO, version, + ALL_OPENSTACK_VERSIONS): + path = 'openstack/%s/%s' % (version, VD2_JSON_NAME) + yield (path, self.lookup(path)) + for (cid, content) in six.iteritems(self.content): yield ('%s/%s/%s' % ("openstack", CONTENT_DIR, cid), content) @@ -578,24 +649,11 @@ class RouteConfiguration(object): return path_handler(version, path) -class VendorDataDriver(object): - """The base VendorData Drivers should inherit from.""" - - def __init__(self, *args, **kwargs): - """Init method should do all expensive operations.""" - self._data = {} - - def get(self): - """Return a dictionary of primitives to be rendered in metadata - - :return: A dictionary or primitives. - """ - return self._data - - def get_metadata_by_address(address): ctxt = context.get_admin_context() fixed_ip = network.API().get_fixed_ip_by_address(ctxt, address) + LOG.info(_LI('Fixed IP %(ip)s translates to instance UUID %(uuid)s'), + {'ip': address, 'uuid': fixed_ip['instance_uuid']}) return get_metadata_by_instance_id(fixed_ip['instance_uuid'], address, @@ -653,3 +711,8 @@ def find_path_in_tree(data, path_tokens): raise KeyError("/".join(path_tokens[0:i])) data = data[path_tokens[i]] return data + + +# NOTE(mikal): this alias is to stop old style vendordata plugins from breaking +# post refactor. It should be removed when we finish deprecating those plugins. +VendorDataDriver = vendordata.VendorDataDriver diff --git a/nova/api/metadata/handler.py b/nova/api/metadata/handler.py index 41e5741325a4..edf962849fc1 100644 --- a/nova/api/metadata/handler.py +++ b/nova/api/metadata/handler.py @@ -133,7 +133,7 @@ class MetadataRequestHandler(wsgi.Application): try: meta_data = self.get_metadata_by_remote_address(remote_address) except Exception: - LOG.exception(_LE('Failed to get metadata for IP: %s'), + LOG.exception(_LE('Failed to get metadata for IP %s'), remote_address) msg = _('An unknown error has occurred. ' 'Please try your request again.') @@ -141,7 +141,7 @@ class MetadataRequestHandler(wsgi.Application): explanation=six.text_type(msg)) if meta_data is None: - LOG.error(_LE('Failed to get metadata for IP: %s'), + LOG.error(_LE('Failed to get metadata for IP %s: no metadata'), remote_address) return meta_data diff --git a/nova/api/metadata/vendordata.py b/nova/api/metadata/vendordata.py new file mode 100644 index 000000000000..e58b07c9c8cf --- /dev/null +++ b/nova/api/metadata/vendordata.py @@ -0,0 +1,30 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + + +class VendorDataDriver(object): + """The base VendorData Drivers should inherit from.""" + + def __init__(self, *args, **kwargs): + """Init method should do all expensive operations.""" + self._data = {} + + def get(self): + """Return a dictionary of primitives to be rendered in metadata + + :return: A dictionary of primitives. + """ + return self._data diff --git a/nova/api/metadata/vendordata_dynamic.py b/nova/api/metadata/vendordata_dynamic.py new file mode 100644 index 000000000000..2736936770cb --- /dev/null +++ b/nova/api/metadata/vendordata_dynamic.py @@ -0,0 +1,124 @@ +# Copyright 2016 Rackspace Australia +# 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. + +"""Render vendordata as stored fetched from REST microservices.""" + +import requests + +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import excutils + +from nova.api.metadata import vendordata +import nova.conf +from nova.i18n import _LW + +CONF = nova.conf.CONF +LOG = logging.getLogger(__name__) + + +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, + } + + +class DynamicVendorData(vendordata.VendorDataDriver): + def __init__(self, context=None, instance=None, address=None, + network_info=None): + # NOTE(mikal): address and network_info are unused, but can't be + # removed / renamed as this interface is shared with the static + # JSON plugin. + self.context = context + self.instance = instance + + def _do_request(self, service_name, url): + try: + body = {'project-id': self.instance.project_id, + 'instance-id': self.instance.uuid, + 'image-id': self.instance.image_ref, + 'user-data': self.instance.user_data, + 'hostname': self.instance.hostname, + 'metadata': self.instance.metadata} + headers = {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'openstack-nova-vendordata'} + + if self.context: + headers.update(generate_identity_headers(self.context)) + + # SSL verification + verify = url.startswith('https://') + + if verify and CONF.vendordata_dynamic_ssl_certfile: + verify = CONF.vendordata_dynamic_ssl_certfile + + timeout = (CONF.vendordata_dynamic_connect_timeout, + CONF.vendordata_dynamic_read_timeout) + + res = requests.request('POST', url, data=jsonutils.dumps(body), + headers=headers, verify=verify, + timeout=timeout) + if res.status_code in (requests.codes.OK, + requests.codes.CREATED, + requests.codes.ACCEPTED, + requests.codes.NO_CONTENT): + # TODO(mikal): Use the Cache-Control response header to do some + # sensible form of caching here. + return jsonutils.loads(res.text) + + return {} + + except (TypeError, ValueError, requests.exceptions.RequestException, + requests.exceptions.SSLError) as e: + with excutils.save_and_reraise_exception(): + LOG.warning(_LW('Error from dynamic vendordata service ' + '%(service_name)s at %(url)s: %(error)s'), + {'service_name': service_name, + 'url': url, + 'error': e}, + instance=self.instance) + + def get(self): + j = {} + + for target in CONF.vendordata_dynamic_targets: + # NOTE(mikal): a target is composed of the following: + # name@url + # where name is the name to use in the metadata handed to + # instances, and url is the URL to fetch it from + if target.find('@') == -1: + LOG.warning(_LW('Vendordata target %(target)s lacks a name. ' + 'Skipping'), + {'target': target}, instance=self.instance) + continue + + tokens = target.split('@') + name = tokens[0] + url = '@'.join(tokens[1:]) + + if name in j: + LOG.warning(_LW('Vendordata already contains an entry named ' + '%(target)s. Skipping'), + {'target': target}, instance=self.instance) + continue + + j[name] = self._do_request(name, url) + + return j diff --git a/nova/api/metadata/vendordata_json.py b/nova/api/metadata/vendordata_json.py index 240c21aec9fe..2960d1502f2d 100644 --- a/nova/api/metadata/vendordata_json.py +++ b/nova/api/metadata/vendordata_json.py @@ -20,7 +20,7 @@ import errno from oslo_log import log as logging from oslo_serialization import jsonutils -from nova.api.metadata import base +from nova.api.metadata import vendordata import nova.conf from nova.i18n import _LW @@ -28,7 +28,7 @@ CONF = nova.conf.CONF LOG = logging.getLogger(__name__) -class JsonFileVendorData(base.VendorDataDriver): +class JsonFileVendorData(vendordata.VendorDataDriver): def __init__(self, *args, **kwargs): super(JsonFileVendorData, self).__init__(*args, **kwargs) data = {} diff --git a/nova/conf/api.py b/nova/conf/api.py index 420d65f23146..d36cf117325c 100644 --- a/nova/conf/api.py +++ b/nova/conf/api.py @@ -123,6 +123,117 @@ request. The value should be the full dot-separated path to the class to use. * Related options: None +"""), + cfg.ListOpt('vendordata_providers', + default=[], + help=""" +A list of vendordata providers. + +vendordata providers are how deployers can provide metadata via configdrive and +metadata that is specific to their deployment. There are currently two +supported providers: StaticJSON and DynamicJSON. + +StaticJSON reads a JSON file configured by the flag vendordata_jsonfile_path +and places the JSON from that file into vendor_data.json and vendor_data2.json. + +DynamicJSON is configured via the vendordata_dynamic_targets flag, which is +documented separately. For each of the endpoints specified in that flag, a +section is added to the vendor_data2.json. + +For more information on the requirements for implementing a vendordata +dynamic endpoint, please see the vendordata.rst file in the nova developer +reference. + +* Possible values: + + A list of vendordata providers, with StaticJSON and DynamicJSON being + current options. + +* Services that use this: + + ``nova-api`` + +* Related options: + + vendordata_dynamic_targets + vendordata_dynamic_ssl_certfile + vendordata_dynamic_connect_timeout + vendordata_dynamic_read_timeout +"""), + cfg.ListOpt('vendordata_dynamic_targets', + default=[], + help=""" +A list of targets for the dynamic vendordata provider. These targets are of +the form @. + +The dynamic vendordata provider collects metadata by contacting external REST +services and querying them for information about the instance. This behaviour +is documented in the vendordata.rst file in the nova developer reference. +"""), + cfg.StrOpt('vendordata_dynamic_ssl_certfile', + default='', + help=""" +Path to an optional certificate file or CA bundle to verify dynamic vendordata +REST services ssl certificates against. + +* Possible values: + + An empty string, or a path to a valid certificate file + +* Services that use this: + + ``nova-api`` + +* Related options: + + vendordata_providers + vendordata_dynamic_targets + vendordata_dynamic_connect_timeout + vendordata_dynamic_read_timeout +"""), + cfg.IntOpt('vendordata_dynamic_connect_timeout', + default=5, + min=3, + help=""" +Maximum wait time for an external REST service to connect. + +* Possible values: + + Any integer with a value greater than three (the TCP packet retransmission + timeout). Note that instance start may be blocked during this wait time, + so this value should be kept small. + +* Services that use this: + + ``nova-api`` + +* Related options: + + vendordata_providers + vendordata_dynamic_targets + vendordata_dynamic_ssl_certfile + vendordata_dynamic_read_timeout +"""), + cfg.IntOpt('vendordata_dynamic_read_timeout', + default=5, + help=""" +Maximum wait time for an external REST service to return data once connected. + +* Possible values: + + Any integer. Note that instance start is blocked during this wait time, + so this value should be kept small. + +* Services that use this: + + ``nova-api`` + +* Related options: + + vendordata_providers + vendordata_dynamic_targets + vendordata_dynamic_ssl_certfile + vendordata_dynamic_connect_timeout """), cfg.IntOpt("metadata_cache_expiration", default=15, diff --git a/nova/tests/functional/test_metadata.py b/nova/tests/functional/test_metadata.py new file mode 100644 index 000000000000..a3d6392ba1a6 --- /dev/null +++ b/nova/tests/functional/test_metadata.py @@ -0,0 +1,176 @@ +# Copyright 2016 Rackspace Australia +# 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 fixtures +import requests + +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import uuidutils + +from nova import context +from nova import objects +from nova import test +from nova.tests import fixtures as nova_fixtures + + +LOG = logging.getLogger(__name__) + + +class fake_result(object): + def __init__(self, result): + self.status_code = 200 + self.text = jsonutils.dumps(result) + + +real_request = requests.request + + +def fake_request(method, url, **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'): + return fake_result({'c': 3}) + if url.startswith('http://127.0.0.1:125'): + return fake_result(jsonutils.loads(kwargs.get('data', '{}'))) + return real_request(method, url, **kwargs) + + +class MetadataTest(test.TestCase): + def setUp(self): + super(MetadataTest, self).setUp() + self.api_fixture = self.useFixture(nova_fixtures.OSMetadataServer()) + self.md_url = self.api_fixture.md_url + + ctxt = context.RequestContext('fake', 'fake') + flavor = objects.Flavor( + id=1, name='flavor1', memory_mb=256, vcpus=1, root_gb=1, + ephemeral_gb=1, flavorid='1', swap=0, rxtx_factor=1.0, + vcpu_weight=1, disabled=False, is_public=True, extra_specs={}, + projects=[]) + instance = objects.Instance(ctxt, flavor=flavor, vcpus=1, + memory_mb=256, root_gb=0, ephemeral_gb=0, + project_id='fake') + instance.create() + + # NOTE(mikal): We could create a network and a fixed IP here, but it + # turns out to be heaps of fiddly boiler plate code, so let's just + # fake it and hope mriedem doesn't notice. + def fake_get_fixed_ip_by_address(self, ctxt, address): + return {'instance_uuid': instance.uuid} + + self.useFixture( + fixtures.MonkeyPatch( + 'nova.network.api.API.get_fixed_ip_by_address', + fake_get_fixed_ip_by_address)) + + def fake_get_ip_info_for_instance_from_nw_info(nw_info): + return {'fixed_ips': ['127.0.0.2'], + 'fixed_ip6s': [], + 'floating_ips': []} + + self.useFixture( + fixtures.MonkeyPatch( + 'nova.api.ec2.ec2utils.get_ip_info_for_instance_from_nw_info', + fake_get_ip_info_for_instance_from_nw_info)) + + def test_lookup_metadata_root_url(self): + res = requests.request('GET', self.md_url, timeout=5) + self.assertEqual(200, res.status_code) + + def test_lookup_metadata_openstack_url(self): + url = '%sopenstack' % self.md_url + res = requests.request('GET', url, timeout=5, + headers={'X-Forwarded-For': '127.0.0.2'}) + self.assertEqual(200, res.status_code) + + def test_lookup_metadata_data_url(self): + url = '%sopenstack/latest/meta_data.json' % self.md_url + res = requests.request('GET', url, timeout=5) + self.assertEqual(200, res.status_code) + + def test_lookup_external_service(self): + self.flags( + vendordata_providers=['StaticJSON', 'DynamicJSON'], + vendordata_dynamic_targets=[ + 'testing@http://127.0.0.1:123', + 'hamster@http://127.0.0.1:123' + ] + ) + + self.useFixture(fixtures.MonkeyPatch('requests.request', + fake_request)) + + url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url + res = requests.request('GET', url, timeout=5) + self.assertEqual(200, res.status_code) + + j = jsonutils.loads(res.text) + self.assertEqual({}, j['static']) + self.assertEqual(1, j['testing']['a']) + self.assertEqual('foo', j['testing']['b']) + self.assertEqual(1, j['hamster']['a']) + self.assertEqual('foo', j['hamster']['b']) + + def test_lookup_external_service_no_overwrite(self): + self.flags( + vendordata_providers=['DynamicJSON'], + vendordata_dynamic_targets=[ + 'testing@http://127.0.0.1:123', + 'testing@http://127.0.0.1:124' + ] + ) + + self.useFixture(fixtures.MonkeyPatch('requests.request', + fake_request)) + + url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url + res = requests.request('GET', url, timeout=5) + self.assertEqual(200, res.status_code) + + j = jsonutils.loads(res.text) + self.assertNotIn('static', j) + self.assertEqual(1, j['testing']['a']) + self.assertEqual('foo', j['testing']['b']) + self.assertNotIn('c', j['testing']) + + def test_lookup_external_service_passes_data(self): + # Much of the data we pass to the REST service is missing because of + # the way we've created the fake instance, but we should at least try + # and ensure we're passing _some_ data through to the external REST + # service. + + self.flags( + vendordata_providers=['DynamicJSON'], + vendordata_dynamic_targets=[ + 'testing@http://127.0.0.1:125' + ] + ) + + self.useFixture(fixtures.MonkeyPatch('requests.request', + fake_request)) + + url = '%sopenstack/2016-10-06/vendor_data2.json' % self.md_url + res = requests.request('GET', url, timeout=5) + self.assertEqual(200, res.status_code) + + j = jsonutils.loads(res.text) + self.assertIn('instance-id', j['testing']) + self.assertTrue(uuidutils.is_uuid_like(j['testing']['instance-id'])) + self.assertIn('hostname', j['testing']) + self.assertEqual('fake', j['testing']['project-id']) + self.assertIn('metadata', j['testing']) + self.assertIn('image-id', j['testing']) + self.assertIn('user-data', j['testing']) diff --git a/nova/tests/unit/test_metadata.py b/nova/tests/unit/test_metadata.py index ecd54fd16a8a..8070e4f55e34 100644 --- a/nova/tests/unit/test_metadata.py +++ b/nova/tests/unit/test_metadata.py @@ -20,7 +20,9 @@ import base64 import copy import hashlib import hmac +import os import re +import requests try: import cPickle as pickle @@ -36,6 +38,7 @@ import webob from nova.api.metadata import base from nova.api.metadata import handler from nova.api.metadata import password +from nova.api.metadata import vendordata from nova import block_device from nova.compute import flavors from nova.conductor import api as conductor_api @@ -52,6 +55,7 @@ from nova.tests.unit.api.openstack import fakes from nova.tests.unit import fake_block_device from nova.tests.unit import fake_network from nova.tests import uuidsentinel as uuids +from nova import utils from nova.virt import netutils CONF = cfg.CONF @@ -441,10 +445,16 @@ class MetadataTestCase(test.TestCase): 'openstack/2016-06-30/user_data', 'openstack/2016-06-30/vendor_data.json', 'openstack/2016-06-30/network_data.json', + 'openstack/2016-10-06/meta_data.json', + 'openstack/2016-10-06/user_data', + 'openstack/2016-10-06/vendor_data.json', + 'openstack/2016-10-06/network_data.json', + 'openstack/2016-10-06/vendor_data2.json', 'openstack/latest/meta_data.json', 'openstack/latest/user_data', 'openstack/latest/vendor_data.json', 'openstack/latest/network_data.json', + 'openstack/latest/vendor_data2.json', ] actual_paths = [] for (path, value) in inst_md.metadata_for_config_drive(): @@ -525,7 +535,7 @@ class MetadataTestCase(test.TestCase): expected_metadata['random_seed'] = FAKE_SEED if md._check_os_version(base.LIBERTY, os_version): expected_metadata['project_id'] = instance.project_id - if md._check_os_version(base.NEWTON, os_version): + if md._check_os_version(base.NEWTON_ONE, os_version): expected_metadata['devices'] = fake_metadata_dicts() mock_cells_keypair.return_value = keypair @@ -790,12 +800,16 @@ class OpenStackMetadataTestCase(test.TestCase): result = mdinst.lookup("/openstack/2013-04-04") self.assertNotIn('vendor_data.json', result) + # verify that 2016-10-06 has the vendor_data2.json file + result = mdinst.lookup("/openstack/2016-10-06") + self.assertIn('vendor_data2.json', result) + def test_vendor_data_response(self): inst = self.instance.obj_clone() mydata = {'mykey1': 'value1', 'mykey2': 'value2'} - class myVdriver(base.VendorDataDriver): + class myVdriver(vendordata.VendorDataDriver): def __init__(self, *args, **kwargs): super(myVdriver, self).__init__(*args, **kwargs) data = mydata.copy() @@ -820,6 +834,45 @@ class OpenStackMetadataTestCase(test.TestCase): for k, v in mydata.items(): self.assertEqual(vd[k], v) + @mock.patch.object(requests, 'request') + def test_vendor_data_response_vendordata2(self, request_mock): + request_mock.return_value.status_code = requests.codes.OK + request_mock.return_value.text = '{"color": "blue"}' + + with utils.tempdir() as tmpdir: + jsonfile = os.path.join(tmpdir, 'test.json') + with open(jsonfile, 'w') as f: + f.write(jsonutils.dumps({'ldap': '10.0.0.1', + 'ad': '10.0.0.2'})) + + self.flags(vendordata_providers=['StaticJSON', 'DynamicJSON'], + vendordata_jsonfile_path=jsonfile, + vendordata_dynamic_targets=[ + 'web@http://fake.com/foobar'] + ) + + inst = self.instance.obj_clone() + mdinst = fake_InstanceMetadata(self, inst) + + # verify that 2013-10-17 has the vendor_data.json file + vdpath = "/openstack/2013-10-17/vendor_data.json" + vd = jsonutils.loads(mdinst.lookup(vdpath)) + self.assertEqual('10.0.0.1', vd.get('ldap')) + self.assertEqual('10.0.0.2', vd.get('ad')) + + # verify that 2016-10-06 works as well + vdpath = "/openstack/2016-10-06/vendor_data.json" + vd = jsonutils.loads(mdinst.lookup(vdpath)) + self.assertEqual('10.0.0.1', vd.get('ldap')) + self.assertEqual('10.0.0.2', vd.get('ad')) + + # verify the new format as well + vdpath = "/openstack/2016-10-06/vendor_data2.json" + vd = jsonutils.loads(mdinst.lookup(vdpath)) + self.assertEqual('10.0.0.1', vd['static'].get('ldap')) + self.assertEqual('10.0.0.2', vd['static'].get('ad')) + self.assertEqual('blue', vd['web'].get('color')) + def test_network_data_presence(self): inst = self.instance.obj_clone() mdinst = fake_InstanceMetadata(self, inst) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index a48ed6870ed8..a314384fd357 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -16544,7 +16544,8 @@ class LibvirtDriverTestCase(test.NoDBTestCase): instance_metadata.InstanceMetadata.__init__(mox.IgnoreArg(), content=mox.IgnoreArg(), extra_md=mox.IgnoreArg(), - network_info=mox.IgnoreArg()) + network_info=mox.IgnoreArg(), + request_context=mox.IgnoreArg()) image_meta = objects.ImageMeta.from_dict( {'id': uuids.image_id, 'name': 'fake'}) self.drvr._get_guest_xml(mox.IgnoreArg(), instance, diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 457eac0c5891..0692d57d17c8 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -3158,7 +3158,7 @@ class LibvirtDriver(driver.ComputeDriver): inst_md = instance_metadata.InstanceMetadata( instance, content=files, extra_md=extra_md, - network_info=network_info) + network_info=network_info, request_context=context) cdb = configdrive.ConfigDriveBuilder(instance_md=inst_md) with cdb: