Skip to content

Commit

Permalink
Allows an instance to post encrypted password
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vishvananda committed Dec 11, 2012
1 parent 255692f commit a2101c4
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 4 deletions.
34 changes: 30 additions & 4 deletions nova/api/metadata/base.py
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -257,13 +272,18 @@ 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:
if self.userdata_raw is None:
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)

Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions nova/api/metadata/handler.py
Expand Up @@ -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):
Expand Down
44 changes: 44 additions & 0 deletions 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()
67 changes: 67 additions & 0 deletions nova/tests/test_metadata.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, "/")
Expand Down Expand Up @@ -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)

0 comments on commit a2101c4

Please sign in to comment.