summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVishvananda Ishaya <vishvananda@gmail.com>2012-11-30 17:03:25 -0800
committerVishvananda Ishaya <vishvananda@gmail.com>2012-12-10 18:04:32 -0800
commita2101c4e7017715af0a29675b89e14ee2884bd89 (patch)
treea1e71cf5ba4af4eb175835ec98bf22be775f4324
parent255692feea3eee12bfc763f75fc8f3dabdbe9ba5 (diff)
downloadnova-a2101c4e7017715af0a29675b89e14ee2884bd89.tar.gz
nova-a2101c4e7017715af0a29675b89e14ee2884bd89.tar.xz
nova-a2101c4e7017715af0a29675b89e14ee2884bd89.zip
Allows an instance to post encrypted password
Exposes a new url in openstack metadata with two methods: GET 169.254.169.254/openstack/latest/password # get password POST 169.254.169.254/openstack/latest/password # post password The password can only be set once and will be stored in an instance_system_metadata value with the key 'password' Part of blueprint get-password Change-Id: I4bbee8326a09fe38d6393e9e70f009daae0c6ece
-rw-r--r--nova/api/metadata/base.py34
-rw-r--r--nova/api/metadata/handler.py3
-rw-r--r--nova/api/metadata/password.py44
-rw-r--r--nova/tests/test_metadata.py67
4 files changed, 144 insertions, 4 deletions
diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py
index b271662b8..1c316bf9c 100644
--- a/nova/api/metadata/base.py
+++ b/nova/api/metadata/base.py
@@ -24,11 +24,13 @@ import os
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 @@ VERSIONS = [
'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 @@ class InstanceMetadata():
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 @@ class InstanceMetadata():
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 @@ class InstanceMetadata():
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 @@ class InstanceMetadata():
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 @@ class InstanceMetadata():
# 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 06fdce30e..b164c5fea 100644
--- a/nova/api/metadata/handler.py
+++ b/nova/api/metadata/handler.py
@@ -120,6 +120,9 @@ class MetadataRequestHandler(wsgi.Application):
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 000000000..3cda67eee
--- /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 d72bf57f5..39c84c1d8 100644
--- a/nova/tests/test_metadata.py
+++ b/nova/tests/test_metadata.py
@@ -27,6 +27,7 @@ import webob
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 @@ class OpenStackMetadataTestCase(test.TestCase):
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 @@ class MetadataHandlerTestCase(test.TestCase):
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 @@ class MetadataHandlerTestCase(test.TestCase):
'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)