summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorAnthony Young <sleepsonthefloor@gmail.com>2011-08-24 11:45:12 -0700
committerAnthony Young <sleepsonthefloor@gmail.com>2011-08-24 11:45:12 -0700
commite2e32589c247bfa790f4459a51742cc335581d0c (patch)
treef96c056cab1658ce01f307981f90fce07cdf6db8 /nova
parent3d17d22926e2f589648fdac26302a58e8c4e9069 (diff)
parent4a99c3f134e20445eb1f6bdabf0f0c53ff9c39c5 (diff)
merge trunk, fix tests
Diffstat (limited to 'nova')
-rw-r--r--nova/api/auth.py1
-rw-r--r--nova/api/ec2/__init__.py21
-rw-r--r--nova/api/ec2/admin.py4
-rw-r--r--nova/api/openstack/auth.py52
-rw-r--r--nova/api/openstack/create_instance_helper.py6
-rw-r--r--nova/api/openstack/servers.py29
-rw-r--r--nova/api/openstack/views/addresses.py23
-rw-r--r--nova/api/openstack/views/servers.py5
-rw-r--r--nova/api/openstack/wsgi.py6
-rw-r--r--nova/auth/manager.py16
-rw-r--r--nova/cloudpipe/pipelib.py9
-rw-r--r--nova/compute/api.py31
-rw-r--r--nova/compute/manager.py6
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py38
-rw-r--r--nova/db/sqlalchemy/models.py2
-rw-r--r--nova/flags.py11
-rw-r--r--nova/ipv6/account_identifier.py6
-rw-r--r--nova/ipv6/rfc2462.py4
-rw-r--r--nova/network/manager.py2
-rw-r--r--nova/notifier/api.py27
-rw-r--r--nova/tests/api/openstack/test_server_actions.py147
-rw-r--r--nova/tests/api/openstack/test_servers.py299
-rw-r--r--nova/tests/integrated/integrated_helpers.py109
-rw-r--r--nova/tests/integrated/test_login.py39
-rw-r--r--nova/tests/integrated/test_servers.py2
-rw-r--r--nova/tests/monkey_patch_example/__init__.py33
-rw-r--r--nova/tests/monkey_patch_example/example_a.py29
-rw-r--r--nova/tests/monkey_patch_example/example_b.py30
-rw-r--r--nova/tests/test_auth.py1
-rw-r--r--nova/tests/test_compute.py20
-rw-r--r--nova/tests/test_ipv6.py38
-rw-r--r--nova/tests/test_notifier.py21
-rw-r--r--nova/tests/test_utils.py45
-rw-r--r--nova/tests/test_versions.py61
-rw-r--r--nova/utils.py41
-rw-r--r--nova/virt/disk.py32
-rw-r--r--nova/virt/libvirt.xml.template7
-rw-r--r--nova/virt/libvirt/connection.py72
-rw-r--r--nova/virt/xenapi/vm_utils.py15
-rw-r--r--nova/virt/xenapi/vmops.py5
-rw-r--r--nova/volume/driver.py2
41 files changed, 1095 insertions, 252 deletions
diff --git a/nova/api/auth.py b/nova/api/auth.py
index cd3e3e8a0..cd0d38b3f 100644
--- a/nova/api/auth.py
+++ b/nova/api/auth.py
@@ -62,6 +62,7 @@ class KeystoneContext(wsgi.Middleware):
req.headers.get('X_STORAGE_TOKEN'))
# Build a context, including the auth_token...
+ remote_address = getattr(req, 'remote_address', '127.0.0.1')
remote_address = req.remote_addr
if FLAGS.use_forwarded_for:
remote_address = req.headers.get('X-Forwarded-For', remote_address)
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 17969099d..5430f443d 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -183,6 +183,27 @@ class ToToken(wsgi.Middleware):
return self.application
+class NoAuth(wsgi.Middleware):
+ """Add user:project as 'nova.context' to WSGI environ."""
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ if 'AWSAccessKeyId' not in req.params:
+ raise webob.exc.HTTPBadRequest()
+ user_id, _sep, project_id = req.params['AWSAccessKeyId'].partition(':')
+ project_id = project_id or user_id
+ remote_address = getattr(req, 'remote_address', '127.0.0.1')
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
+ ctx = context.RequestContext(user_id,
+ project_id,
+ is_admin=True,
+ remote_address=remote_address)
+
+ req.environ['nova.context'] = ctx
+ return self.application
+
+
class Authenticate(wsgi.Middleware):
"""Authenticate an EC2 request and add 'nova.context' to WSGI environ."""
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index df7876b9d..dfbbc0a2b 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -283,8 +283,10 @@ class AdminController(object):
# NOTE(vish) import delayed because of __init__.py
from nova.cloudpipe import pipelib
pipe = pipelib.CloudPipe()
+ proj = manager.AuthManager().get_project(project)
+ user_id = proj.project_manager_id
try:
- pipe.launch_vpn_instance(project)
+ pipe.launch_vpn_instance(project, user_id)
except db.NoMoreNetworks:
raise exception.ApiError("Unable to claim IP for VPN instance"
", ensure it isn't running, and try "
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index b6ff1126b..6754fea27 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -33,6 +33,46 @@ from nova.api.openstack import faults
LOG = logging.getLogger('nova.api.openstack')
FLAGS = flags.FLAGS
+flags.DECLARE('use_forwarded_for', 'nova.api.auth')
+
+
+class NoAuthMiddleware(wsgi.Middleware):
+ """Return a fake token if one isn't specified."""
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ if 'X-Auth-Token' not in req.headers:
+ os_url = req.url
+ version = common.get_version_from_href(os_url)
+ user_id = req.headers.get('X-Auth-User', 'admin')
+ project_id = req.headers.get('X-Auth-Project-Id', 'admin')
+ if version == '1.1':
+ os_url += '/' + project_id
+ res = webob.Response()
+ # NOTE(vish): This is expecting and returning Auth(1.1), whereas
+ # keystone uses 2.0 auth. We should probably allow
+ # 2.0 auth here as well.
+ res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id)
+ res.headers['X-Server-Management-Url'] = os_url
+ res.headers['X-Storage-Url'] = ''
+ res.headers['X-CDN-Management-Url'] = ''
+ res.content_type = 'text/plain'
+ res.status = '204'
+ return res
+
+ token = req.headers['X-Auth-Token']
+ user_id, _sep, project_id = token.partition(':')
+ project_id = project_id or user_id
+ remote_address = getattr(req, 'remote_address', '127.0.0.1')
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
+ ctx = context.RequestContext(user_id,
+ project_id,
+ is_admin=True,
+ remote_address=remote_address)
+
+ req.environ['nova.context'] = ctx
+ return self.application
class AuthMiddleware(wsgi.Middleware):
@@ -85,9 +125,15 @@ class AuthMiddleware(wsgi.Middleware):
project_id = projects[0].id
is_admin = self.auth.is_admin(user_id)
- req.environ['nova.context'] = context.RequestContext(user_id,
- project_id,
- is_admin)
+ remote_address = getattr(req, 'remote_address', '127.0.0.1')
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
+ ctx = context.RequestContext(user_id,
+ project_id,
+ is_admin=is_admin,
+ remote_address=remote_address)
+ req.environ['nova.context'] = ctx
+
if not is_admin and not self.auth.is_project_member(user_id,
project_id):
msg = _("%(user_id)s must be an admin or a "
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 76b903599..4acee2211 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -1,4 +1,5 @@
# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -106,6 +107,7 @@ class CreateInstanceHelper(object):
raise exc.HTTPBadRequest(explanation=msg)
personality = server_dict.get('personality')
+ config_drive = server_dict.get('config_drive')
injected_files = []
if personality:
@@ -159,6 +161,7 @@ class CreateInstanceHelper(object):
extra_values = {
'instance_type': inst_type,
'image_ref': image_href,
+ 'config_drive': config_drive,
'password': password}
return (extra_values,
@@ -184,7 +187,8 @@ class CreateInstanceHelper(object):
requested_networks=requested_networks,
security_group=sg_names,
user_data=user_data,
- availability_zone=availability_zone))
+ availability_zone=availability_zone,
+ config_drive=config_drive,))
except quota.QuotaError as error:
self._handle_quota_error(error)
except exception.ImageNotFound as error:
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 8a358b532..3e76fa1a0 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -612,8 +612,10 @@ class ControllerV10(Controller):
LOG.debug(msg)
raise exc.HTTPBadRequest(explanation=msg)
+ password = utils.generate_password(16)
+
try:
- self.compute_api.rebuild(context, instance_id, image_id)
+ self.compute_api.rebuild(context, instance_id, image_id, password)
except exception.BuildInProgress:
msg = _("Instance %s is currently being rebuilt.") % instance_id
LOG.debug(msg)
@@ -749,15 +751,26 @@ class ControllerV11(Controller):
self._validate_metadata(metadata)
self._decode_personalities(personalities)
+ password = info["rebuild"].get("adminPass",
+ utils.generate_password(16))
+
try:
- self.compute_api.rebuild(context, instance_id, image_href, name,
- metadata, personalities)
+ self.compute_api.rebuild(context, instance_id, image_href,
+ password, name=name, metadata=metadata,
+ files_to_inject=personalities)
except exception.BuildInProgress:
msg = _("Instance %s is currently being rebuilt.") % instance_id
LOG.debug(msg)
raise exc.HTTPConflict(explanation=msg)
+ except exception.InstanceNotFound:
+ msg = _("Instance %s could not be found") % instance_id
+ raise exc.HTTPNotFound(explanation=msg)
- return webob.Response(status_int=202)
+ instance = self.compute_api.routing_get(context, instance_id)
+ view = self._build_view(request, instance, is_detail=True)
+ view['server']['adminPass'] = password
+
+ return view
@common.check_snapshots_enabled
def _action_create_image(self, input_dict, req, instance_id):
@@ -824,6 +837,9 @@ class HeadersSerializer(wsgi.ResponseHeadersSerializer):
def delete(self, response, data):
response.status_int = 204
+ def action(self, response, data):
+ response.status_int = 202
+
class ServerXMLSerializer(wsgi.XMLDictSerializer):
@@ -948,6 +964,11 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer):
node.setAttribute('adminPass', server_dict['server']['adminPass'])
return self.to_xml_string(node, True)
+ def action(self, server_dict):
+ #NOTE(bcwaldon): We need a way to serialize actions individually. This
+ # assumes all actions return a server entity
+ return self.create(server_dict)
+
def update(self, server_dict):
xml_doc = minidom.Document()
node = self._server_to_xml_detailed(xml_doc,
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index ddbf7a144..8f07a2289 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -17,9 +17,11 @@
from nova import flags
from nova import utils
+from nova import log as logging
from nova.api.openstack import common
FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.api.openstack.views.addresses')
class ViewBuilder(object):
@@ -48,7 +50,10 @@ class ViewBuilderV11(ViewBuilder):
def build(self, interfaces):
networks = {}
for interface in interfaces:
- network_label = interface['network']['label']
+ try:
+ network_label = self._extract_network_label(interface)
+ except TypeError:
+ continue
if network_label not in networks:
networks[network_label] = []
@@ -64,9 +69,14 @@ class ViewBuilderV11(ViewBuilder):
return networks
- def build_network(self, interfaces, network_label):
+ def build_network(self, interfaces, requested_network):
for interface in interfaces:
- if interface['network']['label'] == network_label:
+ try:
+ network_label = self._extract_network_label(interface)
+ except TypeError:
+ continue
+
+ if network_label == requested_network:
ips = list(self._extract_ipv4_addresses(interface))
ipv6 = self._extract_ipv6_address(interface)
if ipv6 is not None:
@@ -74,6 +84,13 @@ class ViewBuilderV11(ViewBuilder):
return {network_label: ips}
return None
+ def _extract_network_label(self, interface):
+ try:
+ return interface['network']['label']
+ except (TypeError, KeyError) as exc:
+ LOG.exception(exc)
+ raise TypeError
+
def _extract_ipv4_addresses(self, interface):
for fixed_ip in interface['fixed_ips']:
yield self._build_ip_entity(fixed_ip['address'], 4)
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 0bef58edc..fd7c040d4 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -190,6 +191,7 @@ class ViewBuilderV11(ViewBuilder):
def _build_extra(self, response, inst):
self._build_links(response, inst)
response['uuid'] = inst['uuid']
+ self._build_config_drive(response, inst)
def _build_links(self, response, inst):
href = self.generate_href(inst["id"])
@@ -208,6 +210,9 @@ class ViewBuilderV11(ViewBuilder):
response["links"] = links
+ def _build_config_drive(self, response, inst):
+ response['config_drive'] = inst.get('config_drive')
+
def generate_href(self, server_id):
"""Create an url that refers to a specific server id."""
return os.path.join(self.base_url, self.project_id,
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index dc0f1b93e..8641e960a 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -520,6 +520,6 @@ class Resource(wsgi.Application):
controller_method = getattr(self.controller, action)
try:
return controller_method(req=request, **action_args)
- except TypeError, exc:
- LOG.debug(str(exc))
- return webob.exc.HTTPBadRequest()
+ except TypeError as exc:
+ LOG.exception(exc)
+ return faults.Fault(webob.exc.HTTPBadRequest())
diff --git a/nova/auth/manager.py b/nova/auth/manager.py
index 6205cfb56..44e6e11ac 100644
--- a/nova/auth/manager.py
+++ b/nova/auth/manager.py
@@ -17,6 +17,9 @@
# under the License.
"""
+WARNING: This code is deprecated and will be removed.
+Keystone is the recommended solution for auth management.
+
Nova authentication management
"""
@@ -38,10 +41,13 @@ from nova.auth import signer
FLAGS = flags.FLAGS
+flags.DEFINE_bool('use_deprecated_auth',
+ False,
+ 'This flag must be set to use old style auth')
+
flags.DEFINE_list('allowed_roles',
['cloudadmin', 'itsec', 'sysadmin', 'netadmin', 'developer'],
'Allowed roles for project')
-
# NOTE(vish): a user with one of these roles will be a superuser and
# have access to all api commands
flags.DEFINE_list('superuser_roles', ['cloudadmin'],
@@ -811,7 +817,13 @@ class AuthManager(object):
s3_host = host
ec2_host = host
rc = open(FLAGS.credentials_template).read()
- rc = rc % {'access': user.access,
+ # NOTE(vish): Deprecated auth uses an access key, no auth uses a
+ # the user_id in place of it.
+ if FLAGS.use_deprecated_auth:
+ access = user.access
+ else:
+ access = user.id
+ rc = rc % {'access': access,
'project': pid,
'secret': user.secret,
'ec2': '%s://%s:%s%s' % (FLAGS.ec2_scheme,
diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py
index 2c4673f9e..3eb372844 100644
--- a/nova/cloudpipe/pipelib.py
+++ b/nova/cloudpipe/pipelib.py
@@ -34,7 +34,6 @@ from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
-from nova.auth import manager
# TODO(eday): Eventually changes these to something not ec2-specific
from nova.api.ec2 import cloud
@@ -57,7 +56,6 @@ LOG = logging.getLogger('nova.cloudpipe')
class CloudPipe(object):
def __init__(self):
self.controller = cloud.CloudController()
- self.manager = manager.AuthManager()
def get_encoded_zip(self, project_id):
# Make a payload.zip
@@ -93,11 +91,10 @@ class CloudPipe(object):
zippy.close()
return encoded
- def launch_vpn_instance(self, project_id):
+ def launch_vpn_instance(self, project_id, user_id):
LOG.debug(_("Launching VPN for %s") % (project_id))
- project = self.manager.get_project(project_id)
- ctxt = context.RequestContext(user=project.project_manager_id,
- project=project.id)
+ ctxt = context.RequestContext(user_id=user_id,
+ project_id=project_id)
key_name = self.setup_key_pair(ctxt)
group_name = self.setup_security_group(ctxt)
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 7de91584f..60a13631a 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -2,6 +2,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -54,15 +55,15 @@ def generate_default_hostname(instance):
"""Default function to generate a hostname given an instance reference."""
display_name = instance['display_name']
if display_name is None:
- return 'server_%d' % (instance['id'],)
+ return 'server-%d' % (instance['id'],)
table = ''
deletions = ''
for i in xrange(256):
c = chr(i)
if ('a' <= c <= 'z') or ('0' <= c <= '9') or (c == '-'):
table += c
- elif c == ' ':
- table += '_'
+ elif c in " _":
+ table += '-'
elif ('A' <= c <= 'Z'):
table += c.lower()
else:
@@ -164,7 +165,7 @@ class API(base.Base):
availability_zone=None, user_data=None, metadata=None,
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None, access_ip_v4=None, access_ip_v6=None,
- requested_networks=None):
+ requested_networks=None, config_drive=None,):
"""Verify all the input parameters regardless of the provisioning
strategy being performed."""
@@ -198,6 +199,11 @@ class API(base.Base):
(image_service, image_id) = nova.image.get_image_service(image_href)
image = image_service.show(context, image_id)
+ config_drive_id = None
+ if config_drive and config_drive is not True:
+ # config_drive is volume id
+ config_drive, config_drive_id = None, config_drive
+
os_type = None
if 'properties' in image and 'os_type' in image['properties']:
os_type = image['properties']['os_type']
@@ -225,6 +231,8 @@ class API(base.Base):
image_service.show(context, kernel_id)
if ramdisk_id:
image_service.show(context, ramdisk_id)
+ if config_drive_id:
+ image_service.show(context, config_drive_id)
self.ensure_default_security_group(context)
@@ -243,6 +251,8 @@ class API(base.Base):
'image_ref': image_href,
'kernel_id': kernel_id or '',
'ramdisk_id': ramdisk_id or '',
+ 'config_drive_id': config_drive_id or '',
+ 'config_drive': config_drive or '',
'state': 0,
'state_description': 'scheduling',
'user_id': context.user_id,
@@ -454,7 +464,7 @@ class API(base.Base):
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None, block_device_mapping=None,
access_ip_v4=None, access_ip_v6=None,
- requested_networks=None):
+ requested_networks=None, config_drive=None):
"""Provision the instances by passing the whole request to
the Scheduler for execution. Returns a Reservation ID
related to the creation of all of these instances."""
@@ -471,7 +481,7 @@ class API(base.Base):
availability_zone, user_data, metadata,
injected_files, admin_password, zone_blob,
reservation_id, access_ip_v4, access_ip_v6,
- requested_networks)
+ requested_networks, config_drive)
self._ask_scheduler_to_create_instance(context, base_options,
instance_type, zone_blob,
@@ -491,7 +501,7 @@ class API(base.Base):
injected_files=None, admin_password=None, zone_blob=None,
reservation_id=None, block_device_mapping=None,
access_ip_v4=None, access_ip_v6=None,
- requested_networks=None):
+ requested_networks=None, config_drive=None,):
"""
Provision the instances by sending off a series of single
instance requests to the Schedulers. This is fine for trival
@@ -516,7 +526,7 @@ class API(base.Base):
availability_zone, user_data, metadata,
injected_files, admin_password, zone_blob,
reservation_id, access_ip_v4, access_ip_v6,
- requested_networks)
+ requested_networks, config_drive)
block_device_mapping = block_device_mapping or []
instances = []
@@ -1013,8 +1023,8 @@ class API(base.Base):
self._cast_compute_message('reboot_instance', context, instance_id)
@scheduler_api.reroute_compute("rebuild")
- def rebuild(self, context, instance_id, image_href, name=None,
- metadata=None, files_to_inject=None):
+ def rebuild(self, context, instance_id, image_href, admin_password,
+ name=None, metadata=None, files_to_inject=None):
"""Rebuild the given instance with the provided metadata."""
instance = db.api.instance_get(context, instance_id)
@@ -1034,6 +1044,7 @@ class API(base.Base):
self.db.instance_update(context, instance_id, values)
rebuild_params = {
+ "new_pass": admin_password,
"image_ref": image_href,
"injected_files": files_to_inject,
}
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index c207eccbb..ade15e310 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -527,6 +527,7 @@ class ComputeManager(manager.SchedulerDependentManager):
:param context: `nova.RequestContext` object
:param instance_id: Instance identifier (integer)
:param image_ref: Image identifier (href or integer)
+ :param new_pass: password to set on rebuilt instance
"""
context = context.elevated()
@@ -544,6 +545,11 @@ class ComputeManager(manager.SchedulerDependentManager):
network_info = self.network_api.get_instance_nw_info(context,
instance_ref)
bd_mapping = self._setup_block_device_mapping(context, instance_id)
+
+ # pull in new password here since the original password isn't in the db
+ instance_ref.admin_pass = kwargs.get('new_pass',
+ utils.generate_password(FLAGS.password_length))
+
self.driver.spawn(context, instance_ref, network_info, bd_mapping)
self._update_image_ref(context, instance_id, image_ref)
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py b/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py
new file mode 100644
index 000000000..d3058f00d
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py
@@ -0,0 +1,38 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2011 Piston Cloud Computing, Inc.
+#
+# 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 sqlalchemy import Column, Integer, MetaData, String, Table
+
+from nova import utils
+
+
+meta = MetaData()
+
+instances = Table("instances", meta,
+ Column("id", Integer(), primary_key=True, nullable=False))
+
+# matches the size of an image_ref
+config_drive_column = Column("config_drive", String(255), nullable=True)
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances.create_column(config_drive_column)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances.drop_column(config_drive_column)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 19dc3302e..0680501e9 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -2,6 +2,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -230,6 +231,7 @@ class Instance(BASE, NovaBase):
uuid = Column(String(36))
root_device_name = Column(String(255))
+ config_drive = Column(String(255))
# User editable field meant to represent what ip should be used
# to connect to the instance
diff --git a/nova/flags.py b/nova/flags.py
index 48d5e8168..95000df1b 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -402,3 +402,14 @@ DEFINE_bool('resume_guests_state_on_host_boot', False,
DEFINE_string('root_helper', 'sudo',
'Command prefix to use for running commands as root')
+
+DEFINE_bool('use_ipv6', False, 'use ipv6')
+
+DEFINE_bool('monkey_patch', False,
+ 'Whether to log monkey patching')
+
+DEFINE_list('monkey_patch_modules',
+ ['nova.api.ec2.cloud:nova.notifier.api.notify_decorator',
+ 'nova.compute.api:nova.notifier.api.notify_decorator'],
+ 'Module list representing monkey '
+ 'patched module and decorator')
diff --git a/nova/ipv6/account_identifier.py b/nova/ipv6/account_identifier.py
index 258678f0a..27bb01988 100644
--- a/nova/ipv6/account_identifier.py
+++ b/nova/ipv6/account_identifier.py
@@ -34,8 +34,12 @@ def to_global(prefix, mac, project_id):
mac_addr = netaddr.IPAddress(int_addr)
maskIP = netaddr.IPNetwork(prefix).ip
return (project_hash ^ static_num ^ mac_addr | maskIP).format()
- except TypeError:
+ except netaddr.AddrFormatError:
raise TypeError(_('Bad mac for to_global_ipv6: %s') % mac)
+ except TypeError:
+ raise TypeError(_('Bad prefix for to_global_ipv6: %s') % prefix)
+ except NameError:
+ raise TypeError(_('Bad project_id for to_global_ipv6: %s') % project_id)
def to_mac(ipv6_address):
diff --git a/nova/ipv6/rfc2462.py b/nova/ipv6/rfc2462.py
index 0074efe98..acf42d201 100644
--- a/nova/ipv6/rfc2462.py
+++ b/nova/ipv6/rfc2462.py
@@ -30,8 +30,10 @@ def to_global(prefix, mac, project_id):
maskIP = netaddr.IPNetwork(prefix).ip
return (mac64_addr ^ netaddr.IPAddress('::0200:0:0:0') | maskIP).\
format()
- except TypeError:
+ except netaddr.AddrFormatError:
raise TypeError(_('Bad mac for to_global_ipv6: %s') % mac)
+ except TypeError:
+ raise TypeError(_('Bad prefix for to_global_ipv6: %s') % prefix)
def to_mac(ipv6_address):
diff --git a/nova/network/manager.py b/nova/network/manager.py
index aa2a3700c..404a3180e 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -106,8 +106,6 @@ flags.DEFINE_integer('create_unique_mac_address_attempts', 5,
'Number of attempts to create unique mac address')
flags.DEFINE_bool('auto_assign_floating_ip', False,
'Autoassigning floating ip to VM')
-flags.DEFINE_bool('use_ipv6', False,
- 'use the ipv6')
flags.DEFINE_string('network_host', socket.gethostname(),
'Network host to use for ip allocation in flat modes')
flags.DEFINE_bool('fake_call', False,
diff --git a/nova/notifier/api.py b/nova/notifier/api.py
index e18f3e280..6ef4a050e 100644
--- a/nova/notifier/api.py
+++ b/nova/notifier/api.py
@@ -25,6 +25,9 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('default_notification_level', 'INFO',
'Default notification level for outgoing notifications')
+flags.DEFINE_string('default_publisher_id', FLAGS.host,
+ 'Default publisher_id for outgoing notifications')
+
WARN = 'WARN'
INFO = 'INFO'
@@ -39,6 +42,30 @@ class BadPriorityException(Exception):
pass
+def notify_decorator(name, fn):
+ """ decorator for notify which is used from utils.monkey_patch()
+
+ :param name: name of the function
+ :param function: - object of the function
+ :returns: function -- decorated function
+
+ """
+ def wrapped_func(*args, **kwarg):
+ body = {}
+ body['args'] = []
+ body['kwarg'] = {}
+ for arg in args:
+ body['args'].append(arg)
+ for key in kwarg:
+ body['kwarg'][key] = kwarg[key]
+ notify(FLAGS.default_publisher_id,
+ name,
+ FLAGS.default_notification_level,
+ body)
+ return fn(*args, **kwarg)
+ return wrapped_func
+
+
def publisher_id(service, host=None):
if not host:
host = FLAGS.host
diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py
index 90fe2f0b3..3dfdeb79c 100644
--- a/nova/tests/api/openstack/test_server_actions.py
+++ b/nova/tests/api/openstack/test_server_actions.py
@@ -1,14 +1,13 @@
import base64
+import datetime
import json
-import unittest
-from xml.dom import minidom
import stubout
import webob
from nova import context
-from nova import db
from nova import utils
+from nova import exception
from nova import flags
from nova.api.openstack import create_instance_helper
from nova.compute import instance_types
@@ -23,61 +22,58 @@ FLAGS = flags.FLAGS
def return_server_by_id(context, id):
- return _get_instance()
+ return stub_instance(id)
def instance_update(context, instance_id, kwargs):
- return _get_instance()
+ return stub_instance(instance_id)
-def return_server_with_power_state(power_state):
+def return_server_with_attributes(**kwargs):
def _return_server(context, id):
- instance = _get_instance()
- instance['state'] = power_state
- return instance
+ return stub_instance(id, **kwargs)
return _return_server
+def return_server_with_power_state(power_state):
+ return return_server_with_attributes(power_state=power_state)
+
+
def return_server_with_uuid_and_power_state(power_state):
- def _return_server(context, id):
- return return_server_with_power_state(power_state)
- return _return_server
+ return return_server_with_power_state(power_state)
-class MockSetAdminPassword(object):
- def __init__(self):
- self.instance_id = None
- self.password = None
+def stub_instance(id, power_state=0, metadata=None,
+ image_ref="10", flavor_id="1", name=None):
- def __call__(self, context, instance_id, password):
- self.instance_id = instance_id
- self.password = password
+ if metadata is not None:
+ metadata_items = [{'key':k, 'value':v} for k, v in metadata.items()]
+ else:
+ metadata_items = [{'key':'seq', 'value':id}]
+ inst_type = instance_types.get_instance_type_by_flavor_id(int(flavor_id))
-def _get_instance():
instance = {
- "id": 1,
- "created_at": "2010-10-10 12:00:00",
- "updated_at": "2010-11-11 11:00:00",
+ "id": int(id),
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
"admin_pass": "",
- "user_id": "",
- "project_id": "",
- "image_ref": "5",
+ "user_id": "fake",
+ "project_id": "fake",
+ "image_ref": image_ref,
"kernel_id": "",
"ramdisk_id": "",
"launch_index": 0,
"key_name": "",
"key_data": "",
- "state": 0,
+ "state": power_state,
"state_description": "",
"memory_mb": 0,
"vcpus": 0,
"local_gb": 0,
"hostname": "",
"host": "",
- "instance_type": {
- "flavorid": 1,
- },
+ "instance_type": dict(inst_type),
"user_data": "",
"reservation_id": "",
"mac_address": "",
@@ -85,17 +81,34 @@ def _get_instance():
"launched_at": utils.utcnow(),
"terminated_at": utils.utcnow(),
"availability_zone": "",
- "display_name": "test_server",
+ "display_name": name or "server%s" % id,
"display_description": "",
"locked": False,
- "metadata": [],
- #"address": ,
- #"floating_ips": [{"address":ip} for ip in public_addresses]}
- "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"}
+ "metadata": metadata_items,
+ "access_ip_v4": "",
+ "access_ip_v6": "",
+ "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
+ "virtual_interfaces": [],
+ }
+
+ instance["fixed_ips"] = {
+ "address": '192.168.0.1',
+ "floating_ips": [],
+ }
return instance
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance_id, password):
+ self.instance_id = instance_id
+ self.password = password
+
+
class ServerActionsTest(test.TestCase):
def setUp(self):
@@ -103,8 +116,6 @@ class ServerActionsTest(test.TestCase):
super(ServerActionsTest, self).setUp()
self.flags(verbose=True)
self.stubs = stubout.StubOutForTesting()
- fakes.FakeAuthManager.reset_fake_data()
- fakes.FakeAuthDatabase.data = {}
fakes.stub_out_auth(self.stubs)
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
self.stubs.Set(nova.db.api, 'instance_update', instance_update)
@@ -468,8 +479,6 @@ class ServerActionsTestV11(test.TestCase):
self.maxDiff = None
super(ServerActionsTestV11, self).setUp()
self.stubs = stubout.StubOutForTesting()
- fakes.FakeAuthManager.reset_fake_data()
- fakes.FakeAuthDatabase.data = {}
fakes.stub_out_auth(self.stubs)
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
self.stubs.Set(nova.db.api, 'instance_update', instance_update)
@@ -606,6 +615,9 @@ class ServerActionsTestV11(test.TestCase):
self.assertEqual(res.status_int, 400)
def test_server_rebuild_accepted_minimum(self):
+ new_return_server = return_server_with_attributes(image_ref='2')
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
body = {
"rebuild": {
"imageRef": "http://localhost/images/2",
@@ -619,6 +631,9 @@ class ServerActionsTestV11(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(len(body['server']['adminPass']), 16)
def test_server_rebuild_rejected_when_building(self):
body = {
@@ -642,12 +657,15 @@ class ServerActionsTestV11(test.TestCase):
self.assertEqual(res.status_int, 409)
def test_server_rebuild_accepted_with_metadata(self):
+ metadata = {'new': 'metadata'}
+
+ new_return_server = return_server_with_attributes(metadata=metadata)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
body = {
"rebuild": {
"imageRef": "http://localhost/images/2",
- "metadata": {
- "new": "metadata",
- },
+ "metadata": metadata,
},
}
@@ -658,6 +676,8 @@ class ServerActionsTestV11(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['metadata'], metadata)
def test_server_rebuild_accepted_with_bad_metadata(self):
body = {
@@ -727,6 +747,49 @@ class ServerActionsTestV11(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 202)
+ body = json.loads(res.body)
+ self.assertTrue('personality' not in body['server'])
+
+ def test_server_rebuild_admin_pass(self):
+ new_return_server = return_server_with_attributes(image_ref='2')
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "adminPass": "asdf",
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/fake/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(body['server']['adminPass'], 'asdf')
+
+ def test_server_rebuild_server_not_found(self):
+ def server_not_found(self, instance_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ self.stubs.Set(nova.db.api, 'instance_get', server_not_found)
+
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/fake/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
def test_resize_server(self):
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 114fc4ea2..435644dcc 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -234,7 +235,6 @@ class MockSetAdminPassword(object):
class ServersTest(test.TestCase):
-
def setUp(self):
self.maxDiff = None
super(ServersTest, self).setUp()
@@ -266,6 +266,7 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api)
self.webreq = common.webob_factory('/v1.0/servers')
+ self.config_drive = None
def test_get_server_by_id(self):
req = webob.Request.blank('/v1.0/servers/1')
@@ -383,6 +384,7 @@ class ServersTest(test.TestCase):
"metadata": {
"seq": "1",
},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -555,6 +557,7 @@ class ServersTest(test.TestCase):
"metadata": {
"seq": "1",
},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -651,6 +654,7 @@ class ServersTest(test.TestCase):
"metadata": {
"seq": "1",
},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -780,6 +784,27 @@ class ServersTest(test.TestCase):
(ip,) = private_node.getElementsByTagName('ip')
self.assertEquals(ip.getAttribute('addr'), private)
+ # NOTE(bcwaldon): lp830817
+ def test_get_server_by_id_malformed_networks_v1_1(self):
+ ifaces = [
+ {
+ 'network': None,
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ ]
+ new_return_server = return_server_with_attributes(interfaces=ifaces)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
+ req = webob.Request.blank('/v1.1/fake/servers/1')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ self.assertEqual(res_dict['server']['id'], 1)
+ self.assertEqual(res_dict['server']['name'], 'server1')
+
def test_get_server_by_id_with_addresses_v1_1(self):
self.flags(use_ipv6=True)
interfaces = [
@@ -1434,6 +1459,7 @@ class ServersTest(test.TestCase):
'project_id': 'fake',
"created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
"updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "config_drive": self.config_drive,
}
def server_update(context, id, params):
@@ -1459,8 +1485,7 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.db.api, 'instance_create', instance_create)
self.stubs.Set(nova.rpc, 'cast', fake_method)
self.stubs.Set(nova.rpc, 'call', fake_method)
- self.stubs.Set(nova.db.api, 'instance_update',
- server_update)
+ self.stubs.Set(nova.db.api, 'instance_update', server_update)
self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for)
self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip',
fake_method)
@@ -1803,6 +1828,129 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
+ def test_create_instance_with_config_drive_v1_1(self):
+ self.config_drive = True
+ self._setup_for_create_instance()
+
+ image_href = 'http://localhost/v1.1/123/images/2'
+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'config_drive_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': {},
+ 'config_drive': True,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/123/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ print res
+ self.assertEqual(res.status_int, 202)
+ server = json.loads(res.body)['server']
+ self.assertEqual(1, server['id'])
+ self.assertTrue(server['config_drive'])
+
+ def test_create_instance_with_config_drive_as_id_v1_1(self):
+ self.config_drive = 2
+ self._setup_for_create_instance()
+
+ image_href = 'http://localhost/v1.1/123/images/2'
+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'config_drive_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': {},
+ 'config_drive': 2,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/123/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+
+ self.assertEqual(res.status_int, 202)
+ server = json.loads(res.body)['server']
+ self.assertEqual(1, server['id'])
+ self.assertTrue(server['config_drive'])
+ self.assertEqual(2, server['config_drive'])
+
+ def test_create_instance_with_bad_config_drive_v1_1(self):
+ self.config_drive = "asdf"
+ self._setup_for_create_instance()
+
+ image_href = 'http://localhost/v1.1/123/images/2'
+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'config_drive_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': {},
+ 'config_drive': 'asdf',
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/123/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_create_instance_without_config_drive_v1_1(self):
+ self._setup_for_create_instance()
+
+ image_href = 'http://localhost/v1.1/123/images/2'
+ flavor_ref = 'http://localhost/v1.1/123/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'config_drive_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': {},
+ 'config_drive': True,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/123/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ server = json.loads(res.body)['server']
+ self.assertEqual(1, server['id'])
+ self.assertFalse(server['config_drive'])
+
def test_create_instance_v1_1_bad_href(self):
self._setup_for_create_instance()
@@ -3515,6 +3663,7 @@ class ServersViewBuilderV11Test(test.TestCase):
"href": "http://localhost/servers/1",
},
],
+ "config_drive": None,
}
}
@@ -3527,6 +3676,7 @@ class ServersViewBuilderV11Test(test.TestCase):
"id": 1,
"uuid": self.instance['uuid'],
"name": "test_server",
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -3582,6 +3732,7 @@ class ServersViewBuilderV11Test(test.TestCase):
},
"addresses": {},
"metadata": {},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -3638,6 +3789,7 @@ class ServersViewBuilderV11Test(test.TestCase):
},
"addresses": {},
"metadata": {},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -3693,6 +3845,7 @@ class ServersViewBuilderV11Test(test.TestCase):
},
"addresses": {},
"metadata": {},
+ "config_drive": None,
"accessIPv4": "1.2.3.4",
"accessIPv6": "",
"links": [
@@ -3750,6 +3903,7 @@ class ServersViewBuilderV11Test(test.TestCase):
},
"addresses": {},
"metadata": {},
+ "config_drive": None,
"accessIPv4": "",
"accessIPv6": "fead::1234",
"links": [
@@ -3815,6 +3969,7 @@ class ServersViewBuilderV11Test(test.TestCase):
"Open": "Stack",
"Number": "1",
},
+ "config_drive": None,
"links": [
{
"rel": "self",
@@ -4503,3 +4658,141 @@ class ServerXMLSerializationTest(test.TestCase):
str(ip['version']))
self.assertEqual(str(ip_elem.get('addr')),
str(ip['addr']))
+
+ def test_action(self):
+ serializer = servers.ServerXMLSerializer()
+
+ fixture = {
+ "server": {
+ "id": 1,
+ "uuid": FAKE_UUID,
+ "user_id": "fake",
+ "tenant_id": "fake",
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ "progress": 0,
+ "name": "test_server",
+ "description": "fakedescription",
+ "status": "BUILD",
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "fead::1234",
+ "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
+ "adminPass": "test_password",
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.IMAGE_BOOKMARK,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.FLAVOR_BOOKMARK,
+ },
+ ],
+ },
+ "addresses": {
+ "network_one": [
+ {
+ "version": 4,
+ "addr": "67.23.10.138",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.138",
+ },
+ ],
+ "network_two": [
+ {
+ "version": 4,
+ "addr": "67.23.10.139",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.139",
+ },
+ ],
+ },
+ "metadata": {
+ "Open": "Stack",
+ "Number": "1",
+ },
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ }
+ }
+
+ output = serializer.serialize(fixture, 'action')
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'server')
+
+ expected_server_href = self.SERVER_HREF
+ expected_server_bookmark = self.SERVER_BOOKMARK
+ expected_image_bookmark = self.IMAGE_BOOKMARK
+ expected_flavor_bookmark = self.FLAVOR_BOOKMARK
+ expected_now = self.TIMESTAMP
+ expected_uuid = FAKE_UUID
+ server_dict = fixture['server']
+
+ for key in ['name', 'id', 'uuid', 'created', 'accessIPv4',
+ 'updated', 'progress', 'status', 'hostId',
+ 'accessIPv6', 'adminPass']:
+ self.assertEqual(root.get(key), str(server_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(server_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 2)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = server_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ image_root = root.find('{0}image'.format(NS))
+ self.assertEqual(image_root.get('id'), server_dict['image']['id'])
+ link_nodes = image_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 1)
+ for i, link in enumerate(server_dict['image']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ flavor_root = root.find('{0}flavor'.format(NS))
+ self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id'])
+ link_nodes = flavor_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 1)
+ for i, link in enumerate(server_dict['flavor']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ addresses_root = root.find('{0}addresses'.format(NS))
+ addresses_dict = server_dict['addresses']
+ network_elems = addresses_root.findall('{0}network'.format(NS))
+ self.assertEqual(len(network_elems), 2)
+ for i, network_elem in enumerate(network_elems):
+ network = addresses_dict.items()[i]
+ self.assertEqual(str(network_elem.get('id')), str(network[0]))
+ ip_elems = network_elem.findall('{0}ip'.format(NS))
+ for z, ip_elem in enumerate(ip_elems):
+ ip = network[1][z]
+ self.assertEqual(str(ip_elem.get('version')),
+ str(ip['version']))
+ self.assertEqual(str(ip_elem.get('addr')),
+ str(ip['addr']))
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index fb2f88502..343190427 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -22,10 +22,8 @@ Provides common functionality for integrated unit tests
import random
import string
-from nova import exception
from nova import service
from nova import test # For the flags
-from nova.auth import manager
import nova.image.glance
from nova.log import logging
from nova.tests.integrated.api import client
@@ -58,90 +56,6 @@ def generate_new_element(items, prefix, numeric=False):
LOG.debug("Random collision on %s" % candidate)
-class TestUser(object):
- def __init__(self, name, secret, auth_url):
- self.name = name
- self.secret = secret
- self.auth_url = auth_url
-
- if not auth_url:
- raise exception.Error("auth_url is required")
- self.openstack_api = client.TestOpenStackClient(self.name,
- self.secret,
- self.auth_url)
-
- def get_unused_server_name(self):
- servers = self.openstack_api.get_servers()
- server_names = [server['name'] for server in servers]
- return generate_new_element(server_names, 'server')
-
- def get_invalid_image(self):
- images = self.openstack_api.get_images()
- image_ids = [image['id'] for image in images]
- return generate_new_element(image_ids, '', numeric=True)
-
- def get_valid_image(self, create=False):
- images = self.openstack_api.get_images()
- if create and not images:
- # TODO(justinsb): No way currently to create an image through API
- #created_image = self.openstack_api.post_image(image)
- #images.append(created_image)
- raise exception.Error("No way to create an image through API")
-
- if images:
- return images[0]
- return None
-
-
-class IntegratedUnitTestContext(object):
- def __init__(self, auth_url):
- self.auth_manager = manager.AuthManager()
-
- self.auth_url = auth_url
- self.project_name = None
-
- self.test_user = None
-
- self.setup()
-
- def setup(self):
- self._create_test_user()
-
- def _create_test_user(self):
- self.test_user = self._create_unittest_user()
-
- # No way to currently pass this through the OpenStack API
- self.project_name = 'openstack'
- self._configure_project(self.project_name, self.test_user)
-
- def cleanup(self):
- self.test_user = None
-
- def _create_unittest_user(self):
- users = self.auth_manager.get_users()
- user_names = [user.name for user in users]
- auth_name = generate_new_element(user_names, 'unittest_user_')
- auth_key = generate_random_alphanumeric(16)
-
- # Right now there's a bug where auth_name and auth_key are reversed
- # bug732907
- auth_key = auth_name
-
- self.auth_manager.create_user(auth_name, auth_name, auth_key, False)
- return TestUser(auth_name, auth_key, self.auth_url)
-
- def _configure_project(self, project_name, user):
- projects = self.auth_manager.get_projects()
- project_names = [project.name for project in projects]
- if not project_name in project_names:
- project = self.auth_manager.create_project(project_name,
- user.name,
- description=None,
- member_users=None)
- else:
- self.auth_manager.add_to_project(user.name, project_name)
-
-
class _IntegratedTestBase(test.TestCase):
def setUp(self):
super(_IntegratedTestBase, self).setUp()
@@ -163,10 +77,7 @@ class _IntegratedTestBase(test.TestCase):
self._start_api_service()
- self.context = IntegratedUnitTestContext(self.auth_url)
-
- self.user = self.context.test_user
- self.api = self.user.openstack_api
+ self.api = client.TestOpenStackClient('fake', 'fake', self.auth_url)
def _start_api_service(self):
osapi = service.WSGIService("osapi")
@@ -174,10 +85,6 @@ class _IntegratedTestBase(test.TestCase):
self.auth_url = 'http://%s:%s/v1.1' % (osapi.host, osapi.port)
LOG.warn(self.auth_url)
- def tearDown(self):
- self.context.cleanup()
- super(_IntegratedTestBase, self).tearDown()
-
def _get_flags(self):
"""An opportunity to setup flags, before the services are started."""
f = {}
@@ -190,10 +97,20 @@ class _IntegratedTestBase(test.TestCase):
f['fake_network'] = True
return f
+ def get_unused_server_name(self):
+ servers = self.api.get_servers()
+ server_names = [server['name'] for server in servers]
+ return generate_new_element(server_names, 'server')
+
+ def get_invalid_image(self):
+ images = self.api.get_images()
+ image_ids = [image['id'] for image in images]
+ return generate_new_element(image_ids, '', numeric=True)
+
def _build_minimal_create_server_request(self):
server = {}
- image = self.user.get_valid_image(create=True)
+ image = self.api.get_images()[0]
LOG.debug("Image: %s" % image)
if 'imageRef' in image:
@@ -211,7 +128,7 @@ class _IntegratedTestBase(test.TestCase):
server['flavorRef'] = 'http://fake.server/%s' % flavor['id']
# Set a valid server name
- server_name = self.user.get_unused_server_name()
+ server_name = self.get_unused_server_name()
server['name'] = server_name
return server
diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py
index 9d1925bc0..3a863d0f9 100644
--- a/nova/tests/integrated/test_login.py
+++ b/nova/tests/integrated/test_login.py
@@ -15,11 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-import unittest
from nova.log import logging
from nova.tests.integrated import integrated_helpers
-from nova.tests.integrated.api import client
LOG = logging.getLogger('nova.tests.integrated')
@@ -31,40 +29,3 @@ class LoginTest(integrated_helpers._IntegratedTestBase):
flavors = self.api.get_flavors()
for flavor in flavors:
LOG.debug(_("flavor: %s") % flavor)
-
- def test_bad_login_password(self):
- """Test that I get a 401 with a bad username."""
- bad_credentials_api = client.TestOpenStackClient(self.user.name,
- "notso_password",
- self.user.auth_url)
-
- self.assertRaises(client.OpenStackApiAuthenticationException,
- bad_credentials_api.get_flavors)
-
- def test_bad_login_username(self):
- """Test that I get a 401 with a bad password."""
- bad_credentials_api = client.TestOpenStackClient("notso_username",
- self.user.secret,
- self.user.auth_url)
-
- self.assertRaises(client.OpenStackApiAuthenticationException,
- bad_credentials_api.get_flavors)
-
- def test_bad_login_both_bad(self):
- """Test that I get a 401 with both bad username and bad password."""
- bad_credentials_api = client.TestOpenStackClient("notso_username",
- "notso_password",
- self.user.auth_url)
-
- self.assertRaises(client.OpenStackApiAuthenticationException,
- bad_credentials_api.get_flavors)
-
- def test_good_login_bad_project(self):
- """Test that I get a 401 with valid user/pass but bad project"""
- self.api.project_id = 'openstackBAD'
-
- self.assertRaises(client.OpenStackApiAuthorizationException,
- self.api.get_flavors)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index 725f6d529..c2f800689 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -51,7 +51,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase):
self.api.post_server, post)
# With an invalid imageRef, this throws 500.
- server['imageRef'] = self.user.get_invalid_image()
+ server['imageRef'] = self.get_invalid_image()
# TODO(justinsb): Check whatever the spec says should be thrown here
self.assertRaises(client.OpenStackApiException,
self.api.post_server, post)
diff --git a/nova/tests/monkey_patch_example/__init__.py b/nova/tests/monkey_patch_example/__init__.py
new file mode 100644
index 000000000..25cf9ccfe
--- /dev/null
+++ b/nova/tests/monkey_patch_example/__init__.py
@@ -0,0 +1,33 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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.
+"""Example Module for testing utils.monkey_patch()."""
+
+
+CALLED_FUNCTION = []
+
+
+def example_decorator(name, function):
+ """ decorator for notify which is used from utils.monkey_patch()
+
+ :param name: name of the function
+ :param function: - object of the function
+ :returns: function -- decorated function
+ """
+ def wrapped_func(*args, **kwarg):
+ CALLED_FUNCTION.append(name)
+ return function(*args, **kwarg)
+ return wrapped_func
diff --git a/nova/tests/monkey_patch_example/example_a.py b/nova/tests/monkey_patch_example/example_a.py
new file mode 100644
index 000000000..21e79bcb0
--- /dev/null
+++ b/nova/tests/monkey_patch_example/example_a.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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.
+"""Example Module A for testing utils.monkey_patch()."""
+
+
+def example_function_a():
+ return 'Example function'
+
+
+class ExampleClassA():
+ def example_method(self):
+ return 'Example method'
+
+ def example_method_add(self, arg1, arg2):
+ return arg1 + arg2
diff --git a/nova/tests/monkey_patch_example/example_b.py b/nova/tests/monkey_patch_example/example_b.py
new file mode 100644
index 000000000..9d8f6d339
--- /dev/null
+++ b/nova/tests/monkey_patch_example/example_b.py
@@ -0,0 +1,30 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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.
+
+"""Example Module B for testing utils.monkey_patch()."""
+
+
+def example_function_b():
+ return 'Example function'
+
+
+class ExampleClassB():
+ def example_method(self):
+ return 'Example method'
+
+ def example_method_add(self, arg1, arg2):
+ return arg1 + arg2
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index 4561eb7f2..1b3166af7 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -147,6 +147,7 @@ class _AuthManagerBaseTestCase(test.TestCase):
'/services/Cloud'))
def test_can_get_credentials(self):
+ self.flags(use_deprecated_auth=True)
st = {'access': 'access', 'secret': 'secret'}
with user_and_project_generator(self.manager, user_state=st) as (u, p):
credentials = self.manager.get_environment_rc(u, p)
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 993a87f23..6659b81eb 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -2,6 +2,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Piston Cloud Computing, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -159,9 +160,24 @@ class ComputeTestCase(test.TestCase):
db.security_group_destroy(self.context, group['id'])
db.instance_destroy(self.context, ref[0]['id'])
+ def test_create_instance_associates_config_drive(self):
+ """Make sure create associates a config drive."""
+
+ instance_id = self._create_instance(params={'config_drive': True, })
+
+ try:
+ self.compute.run_instance(self.context, instance_id)
+ instances = db.instance_get_all(context.get_admin_context())
+ instance = instances[0]
+
+ self.assertTrue(instance.config_drive)
+ finally:
+ db.instance_destroy(self.context, instance_id)
+
def test_default_hostname_generator(self):
- cases = [(None, 'server_1'), ('Hello, Server!', 'hello_server'),
- ('<}\x1fh\x10e\x08l\x02l\x05o\x12!{>', 'hello')]
+ cases = [(None, 'server-1'), ('Hello, Server!', 'hello-server'),
+ ('<}\x1fh\x10e\x08l\x02l\x05o\x12!{>', 'hello'),
+ ('hello_server', 'hello-server')]
for display_name, hostname in cases:
ref = self.compute_api.create(self.context,
instance_types.get_default_instance_type(), None,
diff --git a/nova/tests/test_ipv6.py b/nova/tests/test_ipv6.py
index d123df6f1..04c1b5598 100644
--- a/nova/tests/test_ipv6.py
+++ b/nova/tests/test_ipv6.py
@@ -40,6 +40,25 @@ class IPv6RFC2462TestCase(test.TestCase):
mac = ipv6.to_mac('2001:db8::216:3eff:fe33:4455')
self.assertEquals(mac, '00:16:3e:33:44:55')
+ def test_to_global_with_bad_mac(self):
+ bad_mac = '02:16:3e:33:44:5Z'
+ self.assertRaises(TypeError, ipv6.to_global,
+ '2001:db8::', bad_mac, 'test')
+
+ def test_to_global_with_bad_prefix(self):
+ bad_prefix = '82'
+ self.assertRaises(TypeError, ipv6.to_global,
+ bad_prefix,
+ '2001:db8::216:3eff:fe33:4455',
+ 'test')
+
+ def test_to_global_with_bad_project(self):
+ bad_project = 'non-existent-project-name'
+ self.assertRaises(TypeError, ipv6.to_global,
+ '2001:db8::',
+ '2001:db8::a94a:8fe5:ff33:4455',
+ bad_project)
+
class IPv6AccountIdentiferTestCase(test.TestCase):
"""Unit tests for IPv6 account_identifier backend operations."""
@@ -55,3 +74,22 @@ class IPv6AccountIdentiferTestCase(test.TestCase):
def test_to_mac(self):
mac = ipv6.to_mac('2001:db8::a94a:8fe5:ff33:4455')
self.assertEquals(mac, '02:16:3e:33:44:55')
+
+ def test_to_global_with_bad_mac(self):
+ bad_mac = '02:16:3e:33:44:5X'
+ self.assertRaises(TypeError, ipv6.to_global,
+ '2001:db8::', bad_mac, 'test')
+
+ def test_to_global_with_bad_prefix(self):
+ bad_prefix = '78'
+ self.assertRaises(TypeError, ipv6.to_global,
+ bad_prefix,
+ '2001:db8::a94a:8fe5:ff33:4455',
+ 'test')
+
+ def test_to_global_with_bad_project(self):
+ bad_project = 'non-existent-project-name'
+ self.assertRaises(TypeError, ipv6.to_global,
+ '2001:db8::',
+ '2001:db8::a94a:8fe5:ff33:4455',
+ bad_project)
diff --git a/nova/tests/test_notifier.py b/nova/tests/test_notifier.py
index 64b799a2c..7de3a4a99 100644
--- a/nova/tests/test_notifier.py
+++ b/nova/tests/test_notifier.py
@@ -134,3 +134,24 @@ class NotifierTestCase(test.TestCase):
self.assertEqual(msg['event_type'], 'error_notification')
self.assertEqual(msg['priority'], 'ERROR')
self.assertEqual(msg['payload']['error'], 'foo')
+
+ def test_send_notification_by_decorator(self):
+ self.notify_called = False
+
+ def example_api(arg1, arg2):
+ return arg1 + arg2
+
+ example_api = nova.notifier.api.notify_decorator(
+ 'example_api',
+ example_api)
+
+ def mock_notify(cls, *args):
+ self.notify_called = True
+
+ self.stubs.Set(nova.notifier.no_op_notifier, 'notify',
+ mock_notify)
+
+ class Mock(object):
+ pass
+ self.assertEqual(3, example_api(1, 2))
+ self.assertEqual(self.notify_called, True)
diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py
index 28e366a8e..1ba794a1a 100644
--- a/nova/tests/test_utils.py
+++ b/nova/tests/test_utils.py
@@ -18,6 +18,7 @@ import datetime
import os
import tempfile
+import nova
from nova import exception
from nova import test
from nova import utils
@@ -394,3 +395,47 @@ class ToPrimitiveTestCase(test.TestCase):
self.assertTrue(ret[0].startswith(u"<module 'datetime' from "))
self.assertTrue(ret[1].startswith(u'<function foo at 0x'))
self.assertEquals(ret[2], u'<built-in function dir>')
+
+
+class MonkeyPatchTestCase(test.TestCase):
+ """Unit test for utils.monkey_patch()."""
+ def setUp(self):
+ super(MonkeyPatchTestCase, self).setUp()
+ self.example_package = 'nova.tests.monkey_patch_example.'
+ self.flags(
+ monkey_patch=True,
+ monkey_patch_modules=[self.example_package + 'example_a' + ':'
+ + self.example_package + 'example_decorator'])
+
+ def test_monkey_patch(self):
+ utils.monkey_patch()
+ nova.tests.monkey_patch_example.CALLED_FUNCTION = []
+ from nova.tests.monkey_patch_example import example_a, example_b
+
+ self.assertEqual('Example function', example_a.example_function_a())
+ exampleA = example_a.ExampleClassA()
+ exampleA.example_method()
+ ret_a = exampleA.example_method_add(3, 5)
+ self.assertEqual(ret_a, 8)
+
+ self.assertEqual('Example function', example_b.example_function_b())
+ exampleB = example_b.ExampleClassB()
+ exampleB.example_method()
+ ret_b = exampleB.example_method_add(3, 5)
+
+ self.assertEqual(ret_b, 8)
+ package_a = self.example_package + 'example_a.'
+ self.assertTrue(package_a + 'example_function_a'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
+
+ self.assertTrue(package_a + 'ExampleClassA.example_method'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
+ self.assertTrue(package_a + 'ExampleClassA.example_method_add'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
+ package_b = self.example_package + 'example_b.'
+ self.assertFalse(package_b + 'example_function_b'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
+ self.assertFalse(package_b + 'ExampleClassB.example_method'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
+ self.assertFalse(package_b + 'ExampleClassB.example_method_add'
+ in nova.tests.monkey_patch_example.CALLED_FUNCTION)
diff --git a/nova/tests/test_versions.py b/nova/tests/test_versions.py
new file mode 100644
index 000000000..4621b042b
--- /dev/null
+++ b/nova/tests/test_versions.py
@@ -0,0 +1,61 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Ken Pepple
+#
+# 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 nova import exception
+from nova import test
+from nova import utils
+from nova import version
+
+
+class VersionTestCase(test.TestCase):
+ """Test cases for Versions code"""
+ def setUp(self):
+ """setup test with unchanging values"""
+ super(VersionTestCase, self).setUp()
+ self.version = version
+ self.version.FINAL = False
+ self.version.NOVA_VERSION = ['2012', '10']
+ self.version.YEAR, self.version.COUNT = self.version.NOVA_VERSION
+ self.version.version_info = {'branch_nick': u'LOCALBRANCH',
+ 'revision_id': 'LOCALREVISION',
+ 'revno': 0}
+
+ def test_version_string_is_good(self):
+ """Ensure version string works"""
+ self.assertEqual("2012.10-dev", self.version.version_string())
+
+ def test_canonical_version_string_is_good(self):
+ """Ensure canonical version works"""
+ self.assertEqual("2012.10", self.version.canonical_version_string())
+
+ def test_final_version_strings_are_identical(self):
+ """Ensure final version strings match only at release"""
+ self.assertNotEqual(self.version.canonical_version_string(),
+ self.version.version_string())
+ self.version.FINAL = True
+ self.assertEqual(self.version.canonical_version_string(),
+ self.version.version_string())
+
+ def test_vcs_version_string_is_good(self):
+ """Ensure uninstalled code generates local """
+ self.assertEqual("LOCALBRANCH:LOCALREVISION",
+ self.version.vcs_version_string())
+
+ def test_version_string_with_vcs_is_good(self):
+ """Ensure uninstalled code get version string"""
+ self.assertEqual("2012.10-LOCALBRANCH:LOCALREVISION",
+ self.version.version_string_with_vcs())
diff --git a/nova/utils.py b/nova/utils.py
index fc4bbd53b..21e6221b2 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -35,6 +35,7 @@ import sys
import time
import types
import uuid
+import pyclbr
from xml.sax import saxutils
from eventlet import event
@@ -860,3 +861,43 @@ def is_valid_ipv4(address):
except ValueError:
return False
return True
+
+
+def monkey_patch():
+ """ If the Flags.monkey_patch set as True,
+ this functuion patches a decorator
+ for all functions in specified modules.
+ You can set decorators for each modules
+ using FLAGS.monkey_patch_modules.
+ The format is "Module path:Decorator function".
+ Example: 'nova.api.ec2.cloud:nova.notifier.api.notify_decorator'
+
+ Parameters of the decorator is as follows.
+ (See nova.notifier.api.notify_decorator)
+
+ name - name of the function
+ function - object of the function
+ """
+ # If FLAGS.monkey_patch is not True, this function do nothing.
+ if not FLAGS.monkey_patch:
+ return
+ # Get list of modules and decorators
+ for module_and_decorator in FLAGS.monkey_patch_modules:
+ module, decorator_name = module_and_decorator.split(':')
+ # import decorator function
+ decorator = import_class(decorator_name)
+ __import__(module)
+ # Retrieve module information using pyclbr
+ module_data = pyclbr.readmodule_ex(module)
+ for key in module_data.keys():
+ # set the decorator for the class methods
+ if isinstance(module_data[key], pyclbr.Class):
+ clz = import_class("%s.%s" % (module, key))
+ for method, func in inspect.getmembers(clz, inspect.ismethod):
+ setattr(clz, method,\
+ decorator("%s.%s.%s" % (module, key, method), func))
+ # set the decorator for the function
+ if isinstance(module_data[key], pyclbr.Function):
+ func = import_class("%s.%s" % (module, key))
+ setattr(sys.modules[module], key,\
+ decorator("%s.%s" % (module, key), func))
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
index 19f3ec185..52b2881e8 100644
--- a/nova/virt/disk.py
+++ b/nova/virt/disk.py
@@ -2,6 +2,9 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+#
+# Copyright 2011, Piston Cloud Computing, Inc.
+#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -22,6 +25,7 @@ Includes injection of SSH PGP keys into authorized_keys file.
"""
+import json
import os
import tempfile
import time
@@ -60,7 +64,8 @@ def extend(image, size):
utils.execute('resize2fs', image, check_exit_code=False)
-def inject_data(image, key=None, net=None, partition=None, nbd=False):
+def inject_data(image, key=None, net=None, metadata=None,
+ partition=None, nbd=False, tune2fs=True):
"""Injects a ssh key and optionally net data into a disk image.
it will mount the image as a fully partitioned disk and attempt to inject
@@ -89,10 +94,10 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
' only inject raw disk images): %s' %
mapped_device)
- # Configure ext2fs so that it doesn't auto-check every N boots
- out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
- mapped_device, run_as_root=True)
-
+ if tune2fs:
+ # Configure ext2fs so that it doesn't auto-check every N boots
+ out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
+ mapped_device, run_as_root=True)
tmpdir = tempfile.mkdtemp()
try:
# mount loopback to dir
@@ -103,7 +108,8 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
% err)
try:
- inject_data_into_fs(tmpdir, key, net, utils.execute)
+ inject_data_into_fs(tmpdir, key, net, metadata,
+ utils.execute)
finally:
# unmount device
utils.execute('umount', mapped_device, run_as_root=True)
@@ -155,6 +161,7 @@ def destroy_container(target, instance, nbd=False):
def _link_device(image, nbd):
"""Link image to device using loopback or nbd"""
+
if nbd:
device = _allocate_device()
utils.execute('qemu-nbd', '-c', device, image, run_as_root=True)
@@ -190,6 +197,7 @@ def _allocate_device():
# NOTE(vish): This assumes no other processes are allocating nbd devices.
# It may race cause a race condition if multiple
# workers are running on a given machine.
+
while True:
if not _DEVICES:
raise exception.Error(_('No free nbd devices'))
@@ -203,7 +211,7 @@ def _free_device(device):
_DEVICES.append(device)
-def inject_data_into_fs(fs, key, net, execute):
+def inject_data_into_fs(fs, key, net, metadata, execute):
"""Injects data into a filesystem already mounted by the caller.
Virt connections can call this directly if they mount their fs
in a different way to inject_data
@@ -212,6 +220,16 @@ def inject_data_into_fs(fs, key, net, execute):
_inject_key_into_fs(key, fs, execute=execute)
if net:
_inject_net_into_fs(net, fs, execute=execute)
+ if metadata:
+ _inject_metadata_into_fs(metadata, fs, execute=execute)
+
+
+def _inject_metadata_into_fs(metadata, fs, execute=None):
+ metadata_path = os.path.join(fs, "meta.js")
+ metadata = dict([(m.key, m.value) for m in metadata])
+
+ utils.execute('sudo', 'tee', metadata_path,
+ process_input=json.dumps(metadata))
def _inject_key_into_fs(key, fs, execute=None):
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index 210e2b0fb..6a02cfa24 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -106,6 +106,13 @@
</disk>
#end for
#end if
+ #if $getVar('config_drive', False)
+ <disk type='file'>
+ <driver type='raw' />
+ <source file='${basepath}/disk.config' />
+ <target dev='${disk_prefix}z' bus='${disk_bus}' />
+ </disk>
+ #end if
#end if
#for $nic in $nics
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index e8a657bac..4388291db 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -4,6 +4,7 @@
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
# Copyright (c) 2010 Citrix Systems, Inc.
+# Copyright (c) 2011 Piston Cloud Computing, Inc
#
# 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
@@ -130,6 +131,10 @@ flags.DEFINE_string('libvirt_vif_type', 'bridge',
flags.DEFINE_string('libvirt_vif_driver',
'nova.virt.libvirt.vif.LibvirtBridgeDriver',
'The libvirt VIF driver to configure the VIFs.')
+flags.DEFINE_string('default_local_format',
+ None,
+ 'The default format a local_volume will be formatted with '
+ 'on creation.')
def get_connection(read_only):
@@ -586,6 +591,7 @@ class LibvirtConnection(driver.ComputeDriver):
self.firewall_driver.prepare_instance_filter(instance, network_info)
self._create_image(context, instance, xml, network_info=network_info,
block_device_info=block_device_info)
+
domain = self._create_new_domain(xml)
LOG.debug(_("instance %s: is running"), instance['name'])
self.firewall_driver.apply_instance_filter(instance, network_info)
@@ -759,10 +765,15 @@ class LibvirtConnection(driver.ComputeDriver):
if size:
disk.extend(target, size)
- def _create_local(self, target, local_gb):
+ def _create_local(self, target, local_size, prefix='G', fs_format=None):
"""Create a blank image of specified size"""
- utils.execute('truncate', target, '-s', "%dG" % local_gb)
- # TODO(vish): should we format disk by default?
+
+ if not fs_format:
+ fs_format = FLAGS.default_local_format
+
+ utils.execute('truncate', target, '-s', "%d%c" % (local_size, prefix))
+ if fs_format:
+ utils.execute('mkfs', '-t', fs_format, target)
def _create_swap(self, target, swap_gb):
"""Create a swap file of specified size"""
@@ -849,14 +860,14 @@ class LibvirtConnection(driver.ComputeDriver):
target=basepath('disk.local'),
fname="local_%s" % local_gb,
cow=FLAGS.use_cow_images,
- local_gb=local_gb)
+ local_size=local_gb)
for eph in driver.block_device_info_get_ephemerals(block_device_info):
self._cache_image(fn=self._create_local,
target=basepath(_get_eph_disk(eph)),
fname="local_%s" % eph['size'],
cow=FLAGS.use_cow_images,
- local_gb=eph['size'])
+ local_size=eph['size'])
swap_gb = 0
@@ -882,9 +893,24 @@ class LibvirtConnection(driver.ComputeDriver):
if not inst['kernel_id']:
target_partition = "1"
- if FLAGS.libvirt_type == 'lxc':
+ config_drive_id = inst.get('config_drive_id')
+ config_drive = inst.get('config_drive')
+
+ if any((FLAGS.libvirt_type == 'lxc', config_drive, config_drive_id)):
target_partition = None
+ if config_drive_id:
+ fname = '%08x' % int(config_drive_id)
+ self._cache_image(fn=self._fetch_image,
+ target=basepath('disk.config'),
+ fname=fname,
+ image_id=config_drive_id,
+ user=user,
+ project=project)
+ elif config_drive:
+ self._create_local(basepath('disk.config'), 64, prefix="M",
+ fs_format='msdos') # 64MB
+
if inst['key_data']:
key = str(inst['key_data'])
else:
@@ -928,19 +954,29 @@ class LibvirtConnection(driver.ComputeDriver):
searchList=[{'interfaces': nets,
'use_ipv6': FLAGS.use_ipv6}]))
- if key or net:
+ metadata = inst.get('metadata')
+ if any((key, net, metadata)):
inst_name = inst['name']
- img_id = inst.image_ref
- if key:
- LOG.info(_('instance %(inst_name)s: injecting key into'
- ' image %(img_id)s') % locals())
- if net:
- LOG.info(_('instance %(inst_name)s: injecting net into'
- ' image %(img_id)s') % locals())
+
+ if config_drive: # Should be True or None by now.
+ injection_path = basepath('disk.config')
+ img_id = 'config-drive'
+ tune2fs = False
+ else:
+ injection_path = basepath('disk')
+ img_id = inst.image_ref
+ tune2fs = True
+
+ for injection in ('metadata', 'key', 'net'):
+ if locals()[injection]:
+ LOG.info(_('instance %(inst_name)s: injecting '
+ '%(injection)s into image %(img_id)s'
+ % locals()))
try:
- disk.inject_data(basepath('disk'), key, net,
+ disk.inject_data(injection_path, key, net, metadata,
partition=target_partition,
- nbd=FLAGS.use_cow_images)
+ nbd=FLAGS.use_cow_images,
+ tune2fs=tune2fs)
if FLAGS.libvirt_type == 'lxc':
disk.setup_container(basepath('disk'),
@@ -1070,6 +1106,10 @@ class LibvirtConnection(driver.ComputeDriver):
block_device_info)):
xml_info['swap_device'] = self.default_swap_device
+ config_drive = False
+ if instance.get('config_drive') or instance.get('config_drive_id'):
+ xml_info['config_drive'] = xml_info['basepath'] + "/disk.config"
+
if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'):
xml_info['vncserver_host'] = FLAGS.vncserver_host
xml_info['vnc_keymap'] = FLAGS.vnc_keymap
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 4a1f07bb1..efbea7076 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2010 Citrix Systems, Inc.
+# Copyright 2011 Piston Cloud Computing, Inc.
#
# 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
@@ -740,13 +741,14 @@ class VMHelper(HelperBase):
# if at all, so determine whether it's required first, and then do
# everything
mount_required = False
- key, net = _prepare_injectables(instance, network_info)
- mount_required = key or net
+ key, net, metadata = _prepare_injectables(instance, network_info)
+ mount_required = key or net or metadata
if not mount_required:
return
with_vdi_attached_here(session, vdi_ref, False,
- lambda dev: _mounted_processing(dev, key, net))
+ lambda dev: _mounted_processing(dev, key, net,
+ metadata))
@classmethod
def lookup_kernel_ramdisk(cls, session, vm):
@@ -1198,7 +1200,7 @@ def _find_guest_agent(base_dir, agent_rel_path):
return False
-def _mounted_processing(device, key, net):
+def _mounted_processing(device, key, net, metadata):
"""Callback which runs with the image VDI attached"""
dev_path = '/dev/' + device + '1' # NB: Partition 1 hardcoded
@@ -1212,7 +1214,7 @@ def _mounted_processing(device, key, net):
if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path):
LOG.info(_('Manipulating interface files '
'directly'))
- disk.inject_data_into_fs(tmpdir, key, net,
+ disk.inject_data_into_fs(tmpdir, key, net, metadata,
utils.execute)
finally:
utils.execute('umount', dev_path, run_as_root=True)
@@ -1235,6 +1237,7 @@ def _prepare_injectables(inst, networks_info):
template = t.Template
template_data = open(FLAGS.injected_network_template).read()
+ metadata = inst['metadata']
key = str(inst['key_data'])
net = None
if networks_info:
@@ -1272,4 +1275,4 @@ def _prepare_injectables(inst, networks_info):
net = str(template(template_data,
searchList=[{'interfaces': interfaces_info,
'use_ipv6': FLAGS.use_ipv6}]))
- return key, net
+ return key, net, metadata
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 9a6215f88..64c106f47 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -239,8 +239,9 @@ class VMOps(object):
self._attach_disks(instance, disk_image_type, vm_ref, first_vdi_ref,
vdis)
- # Alter the image before VM start for, e.g. network injection
- if FLAGS.flat_injected:
+ # Alter the image before VM start for, e.g. network injection also
+ # alter the image if there's metadata.
+ if FLAGS.flat_injected or instance['metadata']:
VMHelper.preconfigure_instance(self._session, instance,
first_vdi_ref, network_info)
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index c99534c07..7d2fb45d4 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -495,7 +495,7 @@ class ISCSIDriver(VolumeDriver):
(out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
iscsi_properties['target_iqn'],
'-p', iscsi_properties['target_portal'],
- iscsi_command, run_as_root=True)
+ *iscsi_command, run_as_root=True)
LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
(iscsi_command, out, err))
return (out, err)