diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py index b271662b8b3..1c316bf9c63 100644 --- a/nova/api/metadata/base.py +++ b/nova/api/metadata/base.py @@ -24,11 +24,13 @@ import posixpath from nova.api.ec2 import ec2utils +from nova.api.metadata import password from nova import block_device from nova import context from nova import db from nova import network from nova.openstack.common import cfg +from nova.openstack.common import timeutils from nova.virt import netutils @@ -57,11 +59,17 @@ '2009-04-04', ] -OPENSTACK_VERSIONS = ["2012-08-10"] +FOLSOM = '2012-08-10' +GRIZZLY = '2013-04-04' +OPENSTACK_VERSIONS = [ + FOLSOM, + GRIZZLY, +] CONTENT_DIR = "content" MD_JSON_NAME = "meta_data.json" UD_NAME = "user_data" +PASS_NAME = "password" class InvalidMetadataVersion(Exception): @@ -128,6 +136,13 @@ def __init__(self, instance, address=None, content=[], extra_md=None): for item in instance.get('metadata', []): self.launch_metadata[item['key']] = item['value'] + self.password = '' + # get password if set + for item in instance.get('system_metadata', []): + if item['key'] == 'password': + self.password = item['value'] or '' + break + self.uuid = instance.get('uuid') self.content = {} @@ -257,6 +272,8 @@ def get_openstack_item(self, path_tokens): ret = [MD_JSON_NAME] if self.userdata_raw is not None: ret.append(UD_NAME) + if self._check_os_version(GRIZZLY, version): + ret.append(PASS_NAME) return ret if path == UD_NAME: @@ -264,6 +281,9 @@ def get_openstack_item(self, path_tokens): raise KeyError(path) return self.userdata_raw + if path == PASS_NAME and self._check_os_version(GRIZZLY, version): + return password.handle_password + if path != MD_JSON_NAME: raise KeyError(path) @@ -303,8 +323,11 @@ def get_openstack_item(self, path_tokens): return data[path] - def _check_version(self, required, requested): - return VERSIONS.index(requested) >= VERSIONS.index(required) + def _check_version(self, required, requested, versions=VERSIONS): + return versions.index(requested) >= versions.index(required) + + def _check_os_version(self, required, requested): + return self._check_version(required, requested, OPENSTACK_VERSIONS) def _get_hostname(self): return "%s%s%s" % (self.instance['hostname'], @@ -332,7 +355,10 @@ def lookup(self, path): # specifically handle the top level request if len(path_tokens) == 1: if path_tokens[0] == "openstack": - versions = OPENSTACK_VERSIONS + ["latest"] + # NOTE(vish): don't show versions that are in the future + today = timeutils.utcnow().strftime("%Y-%m-%d") + versions = [v for v in OPENSTACK_VERSIONS if v <= today] + versions += ["latest"] else: versions = VERSIONS + ["latest"] return versions diff --git a/nova/api/metadata/handler.py b/nova/api/metadata/handler.py index 06fdce30e6b..b164c5fea96 100644 --- a/nova/api/metadata/handler.py +++ b/nova/api/metadata/handler.py @@ -120,6 +120,9 @@ def __call__(self, req): except base.InvalidMetadataPath: raise webob.exc.HTTPNotFound() + if callable(data): + return data(req, meta_data) + return base.ec2_md_print(data) def _handle_remote_ip_request(self, req): diff --git a/nova/api/metadata/password.py b/nova/api/metadata/password.py new file mode 100644 index 00000000000..3cda67eee39 --- /dev/null +++ b/nova/api/metadata/password.py @@ -0,0 +1,44 @@ +# Copyright 2012 Nebula, Inc. +# 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 webob import exc + +from nova import context +from nova import db + + +MAX_SIZE = 256 + + +def handle_password(req, meta_data): + ctxt = context.get_admin_context() + password = meta_data.password + if req.method == 'GET': + return meta_data.password + elif req.method == 'POST': + # NOTE(vish): The conflict will only happen once the metadata cache + # updates, but it isn't a huge issue if it can be set for + # a short window. + if meta_data.password: + raise exc.HTTPConflict() + if (req.content_length > MAX_SIZE or len(req.body) > MAX_SIZE): + msg = _("Request is too large.") + raise exc.HTTPBadRequest(explanation=msg) + db.instance_system_metadata_update(ctxt, + meta_data.uuid, + {'password': req.body}, + False) + else: + raise exc.HTTPBadRequest() diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index d72bf57f536..39c84c1d8b9 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -27,6 +27,7 @@ from nova.api.metadata import base from nova.api.metadata import handler +from nova.api.metadata import password from nova import block_device from nova import db from nova.db.sqlalchemy import api @@ -319,6 +320,14 @@ def test_extra_md(self): for key, val in extra.iteritems(): self.assertEqual(mddict[key], val) + def test_password(self): + # make sure extra_md makes it through to metadata + inst = copy(self.instance) + mdinst = fake_InstanceMetadata(self.stubs, inst) + + result = mdinst.lookup("/openstack/latest/password") + self.assertEqual(result, password.handle_password) + def test_userdata(self): inst = copy(self.instance) mdinst = fake_InstanceMetadata(self.stubs, inst) @@ -351,6 +360,20 @@ def setUp(self): self.mdinst = fake_InstanceMetadata(self.stubs, self.instance, address=None, sgroups=None) + def test_callable(self): + + def verify(req, meta_data): + self.assertTrue(isinstance(meta_data, CallableMD)) + return "foo" + + class CallableMD(object): + def lookup(self, path_info): + return verify + + response = fake_request(self.stubs, CallableMD(), "/bar") + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, "foo") + def test_root(self): expected = "\n".join(base.VERSIONS) + "\nlatest" response = fake_request(self.stubs, self.mdinst, "/") @@ -469,3 +492,47 @@ def fake_get_metadata(instance_id, remote_address): '8387b96cbc5bd2474665192d2ec28' '8ffb67'}) self.assertEqual(response.status_int, 500) + + +class MetadataPasswordTestCase(test.TestCase): + def setUp(self): + super(MetadataPasswordTestCase, self).setUp() + fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs, + spectacular=True) + self.instance = copy(INSTANCES[0]) + self.mdinst = fake_InstanceMetadata(self.stubs, self.instance, + address=None, sgroups=None) + + def test_get_password(self): + request = webob.Request.blank('') + self.mdinst.password = 'foo' + result = password.handle_password(request, self.mdinst) + self.assertEqual(result, 'foo') + + def test_bad_method(self): + request = webob.Request.blank('') + request.method = 'PUT' + self.assertRaises(webob.exc.HTTPBadRequest, + password.handle_password, request, self.mdinst) + + def _try_set_password(self, val='bar'): + request = webob.Request.blank('') + request.method = 'POST' + request.body = val + self.stubs.Set(db, 'instance_system_metadata_update', + lambda *a, **kw: None) + password.handle_password(request, self.mdinst) + + def test_set_password(self): + self.mdinst.password = '' + self._try_set_password() + + def test_conflict(self): + self.mdinst.password = 'foo' + self.assertRaises(webob.exc.HTTPConflict, + self._try_set_password) + + def test_too_large(self): + self.mdinst.password = '' + self.assertRaises(webob.exc.HTTPBadRequest, + self._try_set_password, 'a' * 257)