Merge "New style vendordata support."
This commit is contained in:
commit
0b1a9cc7f8
1
doc/source/vendordata.rst
Normal file
1
doc/source/vendordata.rst
Normal file
@ -0,0 +1 @@
|
||||
TODO mikal
|
@ -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
|
||||
|
@ -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
|
||||
|
30
nova/api/metadata/vendordata.py
Normal file
30
nova/api/metadata/vendordata.py
Normal file
@ -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
|
124
nova/api/metadata/vendordata_dynamic.py
Normal file
124
nova/api/metadata/vendordata_dynamic.py
Normal file
@ -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
|
@ -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 = {}
|
||||
|
111
nova/conf/api.py
111
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 <name>@<url>.
|
||||
|
||||
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,
|
||||
|
176
nova/tests/functional/test_metadata.py
Normal file
176
nova/tests/functional/test_metadata.py
Normal file
@ -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'])
|
@ -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)
|
||||
|
@ -16543,7 +16543,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,
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user