diff options
| author | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-04-06 14:28:05 +0100 |
|---|---|---|
| committer | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-04-06 14:28:05 +0100 |
| commit | e68174e419e105e174ce9f8b221e0ef201fa1990 (patch) | |
| tree | f2ec46a474d7db41ea03ef144c5e387f403f8b74 | |
| parent | 574980cf15f63c08d4d7d6ac4a406149cfb8bcaf (diff) | |
| parent | c0a8904508edb0687b588d40a7bd181d0393884f (diff) | |
| download | nova-e68174e419e105e174ce9f8b221e0ef201fa1990.tar.gz nova-e68174e419e105e174ce9f8b221e0ef201fa1990.tar.xz nova-e68174e419e105e174ce9f8b221e0ef201fa1990.zip | |
Improved unit tests
Fixed docstring formatting
Merged with trunk
33 files changed, 712 insertions, 622 deletions
@@ -32,6 +32,7 @@ Jesse Andrews <anotherjesse@gmail.com> Joe Heck <heckj@mac.com> Joel Moore <joelbm24@gmail.com> John Dewey <john@dewey.ws> +John Tran <jtran@attinteractive.com> Jonathan Bryce <jbryce@jbryce.com> Jordan Rinke <jordan@openstack.org> Josh Durgin <joshd@hq.newdream.net> diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy index 0342c620a..d88f59e40 100755 --- a/bin/nova-ajax-console-proxy +++ b/bin/nova-ajax-console-proxy @@ -108,17 +108,17 @@ class AjaxConsoleProxy(object): return "Server Error" def register_listeners(self): - class Callback: - def __call__(self, data, message): - if data['method'] == 'authorize_ajax_console': - AjaxConsoleProxy.tokens[data['args']['token']] = \ - {'args': data['args'], 'last_activity': time.time()} + class TopicProxy(): + @staticmethod + def authorize_ajax_console(context, **kwargs): + AjaxConsoleProxy.tokens[kwargs['token']] = \ + {'args': kwargs, 'last_activity': time.time()} conn = rpc.Connection.instance(new=True) consumer = rpc.TopicAdapterConsumer( - connection=conn, - topic=FLAGS.ajax_console_proxy_topic) - consumer.register_callback(Callback()) + connection=conn, + proxy=TopicProxy, + topic=FLAGS.ajax_console_proxy_topic) def delete_expired_tokens(): now = time.time() @@ -130,8 +130,7 @@ class AjaxConsoleProxy(object): for k in to_delete: del AjaxConsoleProxy.tokens[k] - utils.LoopingCall(consumer.fetch, auto_ack=True, - enable_callbacks=True).start(0.1) + utils.LoopingCall(consumer.fetch, enable_callbacks=True).start(0.1) utils.LoopingCall(delete_expired_tokens).start(1) if __name__ == '__main__': diff --git a/bin/nova-manage b/bin/nova-manage index 25695482f..6789efba8 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -902,7 +902,7 @@ class ImageCommands(object): 'disk_format': disk_format, 'container_format': container_format, 'properties': {'image_state': 'available', - 'owner': owner, + 'owner_id': owner, 'type': image_type, 'architecture': architecture, 'image_location': 'local', @@ -980,7 +980,7 @@ class ImageCommands(object): 'is_public': True, 'name': old['imageId'], 'properties': {'image_state': old['imageState'], - 'owner': old['imageOwnerId'], + 'owner_id': old['imageOwnerId'], 'architecture': old['architecture'], 'type': old['type'], 'image_location': old['imageLocation'], diff --git a/nova/adminclient.py b/nova/adminclient.py deleted file mode 100644 index f570e12c2..000000000 --- a/nova/adminclient.py +++ /dev/null @@ -1,473 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Nova User API client library. -""" - -import base64 -import boto -import boto.exception -import httplib -import re -import string - -from boto.ec2.regioninfo import RegionInfo - - -DEFAULT_CLC_URL = 'http://127.0.0.1:8773' -DEFAULT_REGION = 'nova' - - -class UserInfo(object): - """ - Information about a Nova user, as parsed through SAX. - - **Fields Include** - - * username - * accesskey - * secretkey - * file (optional) containing zip of X509 cert & rc file - - """ - - def __init__(self, connection=None, username=None, endpoint=None): - self.connection = connection - self.username = username - self.endpoint = endpoint - - def __repr__(self): - return 'UserInfo:%s' % self.username - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'username': - self.username = str(value) - elif name == 'file': - self.file = base64.b64decode(str(value)) - elif name == 'accesskey': - self.accesskey = str(value) - elif name == 'secretkey': - self.secretkey = str(value) - - -class UserRole(object): - """ - Information about a Nova user's role, as parsed through SAX. - - **Fields include** - - * role - - """ - - def __init__(self, connection=None): - self.connection = connection - self.role = None - - def __repr__(self): - return 'UserRole:%s' % self.role - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'role': - self.role = value - else: - setattr(self, name, str(value)) - - -class ProjectInfo(object): - """ - Information about a Nova project, as parsed through SAX. - - **Fields include** - - * projectname - * description - * projectManagerId - * memberIds - - """ - - def __init__(self, connection=None): - self.connection = connection - self.projectname = None - self.description = None - self.projectManagerId = None - self.memberIds = [] - - def __repr__(self): - return 'ProjectInfo:%s' % self.projectname - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'projectname': - self.projectname = value - elif name == 'description': - self.description = value - elif name == 'projectManagerId': - self.projectManagerId = value - elif name == 'memberId': - self.memberIds.append(value) - else: - setattr(self, name, str(value)) - - -class ProjectMember(object): - """ - Information about a Nova project member, as parsed through SAX. - - **Fields include** - - * memberId - - """ - - def __init__(self, connection=None): - self.connection = connection - self.memberId = None - - def __repr__(self): - return 'ProjectMember:%s' % self.memberId - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'member': - self.memberId = value - else: - setattr(self, name, str(value)) - - -class HostInfo(object): - """ - Information about a Nova Host, as parsed through SAX. - - **Fields Include** - - * Hostname - * Compute service status - * Volume service status - * Instance count - * Volume count - """ - - def __init__(self, connection=None): - self.connection = connection - self.hostname = None - self.compute = None - self.volume = None - self.instance_count = 0 - self.volume_count = 0 - - def __repr__(self): - return 'Host:%s' % self.hostname - - # this is needed by the sax parser, so ignore the ugly name - def startElement(self, name, attrs, connection): - return None - - # this is needed by the sax parser, so ignore the ugly name - def endElement(self, name, value, connection): - fixed_name = string.lower(re.sub(r'([A-Z])', r'_\1', name)) - setattr(self, fixed_name, value) - - -class Vpn(object): - """ - Information about a Vpn, as parsed through SAX - - **Fields Include** - - * instance_id - * project_id - * public_ip - * public_port - * created_at - * internal_ip - * state - """ - - def __init__(self, connection=None): - self.connection = connection - self.instance_id = None - self.project_id = None - - def __repr__(self): - return 'Vpn:%s:%s' % (self.project_id, self.instance_id) - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - fixed_name = string.lower(re.sub(r'([A-Z])', r'_\1', name)) - setattr(self, fixed_name, value) - - -class InstanceType(object): - """ - Information about a Nova instance type, as parsed through SAX. - - **Fields include** - - * name - * vcpus - * disk_gb - * memory_mb - * flavor_id - - """ - - def __init__(self, connection=None): - self.connection = connection - self.name = None - self.vcpus = None - self.disk_gb = None - self.memory_mb = None - self.flavor_id = None - - def __repr__(self): - return 'InstanceType:%s' % self.name - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == "memoryMb": - self.memory_mb = str(value) - elif name == "flavorId": - self.flavor_id = str(value) - elif name == "diskGb": - self.disk_gb = str(value) - else: - setattr(self, name, str(value)) - - -class NovaAdminClient(object): - - def __init__( - self, - clc_url=DEFAULT_CLC_URL, - region=DEFAULT_REGION, - access_key=None, - secret_key=None, - **kwargs): - parts = self.split_clc_url(clc_url) - - self.clc_url = clc_url - self.region = region - self.access = access_key - self.secret = secret_key - self.apiconn = boto.connect_ec2(aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - is_secure=parts['is_secure'], - region=RegionInfo(None, - region, - parts['ip']), - port=parts['port'], - path='/services/Admin', - **kwargs) - self.apiconn.APIVersion = 'nova' - - def connection_for(self, username, project, clc_url=None, region=None, - **kwargs): - """Returns a boto ec2 connection for the given username.""" - if not clc_url: - clc_url = self.clc_url - if not region: - region = self.region - parts = self.split_clc_url(clc_url) - user = self.get_user(username) - access_key = '%s:%s' % (user.accesskey, project) - return boto.connect_ec2(aws_access_key_id=access_key, - aws_secret_access_key=user.secretkey, - is_secure=parts['is_secure'], - region=RegionInfo(None, - self.region, - parts['ip']), - port=parts['port'], - path='/services/Cloud', - **kwargs) - - def split_clc_url(self, clc_url): - """Splits a cloud controller endpoint url.""" - parts = httplib.urlsplit(clc_url) - is_secure = parts.scheme == 'https' - ip, port = parts.netloc.split(':') - return {'ip': ip, 'port': int(port), 'is_secure': is_secure} - - def get_users(self): - """Grabs the list of all users.""" - return self.apiconn.get_list('DescribeUsers', {}, [('item', UserInfo)]) - - def get_user(self, name): - """Grab a single user by name.""" - user = self.apiconn.get_object('DescribeUser', - {'Name': name}, - UserInfo) - if user.username != None: - return user - - def has_user(self, username): - """Determine if user exists.""" - return self.get_user(username) != None - - def create_user(self, username): - """Creates a new user, returning the userinfo object with - access/secret.""" - return self.apiconn.get_object('RegisterUser', {'Name': username}, - UserInfo) - - def delete_user(self, username): - """Deletes a user.""" - return self.apiconn.get_object('DeregisterUser', {'Name': username}, - UserInfo) - - def get_roles(self, project_roles=True): - """Returns a list of available roles.""" - return self.apiconn.get_list('DescribeRoles', - {'ProjectRoles': project_roles}, - [('item', UserRole)]) - - def get_user_roles(self, user, project=None): - """Returns a list of roles for the given user. - - Omitting project will return any global roles that the user has. - Specifying project will return only project specific roles. - - """ - params = {'User': user} - if project: - params['Project'] = project - return self.apiconn.get_list('DescribeUserRoles', - params, - [('item', UserRole)]) - - def add_user_role(self, user, role, project=None): - """Add a role to a user either globally or for a specific project.""" - return self.modify_user_role(user, role, project=project, - operation='add') - - def remove_user_role(self, user, role, project=None): - """Remove a role from a user either globally or for a specific - project.""" - return self.modify_user_role(user, role, project=project, - operation='remove') - - def modify_user_role(self, user, role, project=None, operation='add', - **kwargs): - """Add or remove a role for a user and project.""" - params = {'User': user, - 'Role': role, - 'Project': project, - 'Operation': operation} - return self.apiconn.get_status('ModifyUserRole', params) - - def get_projects(self, user=None): - """Returns a list of all projects.""" - if user: - params = {'User': user} - else: - params = {} - return self.apiconn.get_list('DescribeProjects', - params, - [('item', ProjectInfo)]) - - def get_project(self, name): - """Returns a single project with the specified name.""" - project = self.apiconn.get_object('DescribeProject', - {'Name': name}, - ProjectInfo) - - if project.projectname != None: - return project - - def create_project(self, projectname, manager_user, description=None, - member_users=None): - """Creates a new project.""" - params = {'Name': projectname, - 'ManagerUser': manager_user, - 'Description': description, - 'MemberUsers': member_users} - return self.apiconn.get_object('RegisterProject', params, ProjectInfo) - - def modify_project(self, projectname, manager_user=None, description=None): - """Modifies an existing project.""" - params = {'Name': projectname, - 'ManagerUser': manager_user, - 'Description': description} - return self.apiconn.get_status('ModifyProject', params) - - def delete_project(self, projectname): - """Permanently deletes the specified project.""" - return self.apiconn.get_object('DeregisterProject', - {'Name': projectname}, - ProjectInfo) - - def get_project_members(self, name): - """Returns a list of members of a project.""" - return self.apiconn.get_list('DescribeProjectMembers', - {'Name': name}, - [('item', ProjectMember)]) - - def add_project_member(self, user, project): - """Adds a user to a project.""" - return self.modify_project_member(user, project, operation='add') - - def remove_project_member(self, user, project): - """Removes a user from a project.""" - return self.modify_project_member(user, project, operation='remove') - - def modify_project_member(self, user, project, operation='add'): - """Adds or removes a user from a project.""" - params = {'User': user, - 'Project': project, - 'Operation': operation} - return self.apiconn.get_status('ModifyProjectMember', params) - - def get_zip(self, user, project): - """Returns the content of a zip file containing novarc and access - credentials.""" - params = {'Name': user, 'Project': project} - zip = self.apiconn.get_object('GenerateX509ForUser', params, UserInfo) - return zip.file - - def start_vpn(self, project): - """ - Starts the vpn for a user - """ - return self.apiconn.get_object('StartVpn', {'Project': project}, Vpn) - - def get_vpns(self): - """Return a list of vpn with project name""" - return self.apiconn.get_list('DescribeVpns', {}, [('item', Vpn)]) - - def get_hosts(self): - return self.apiconn.get_list('DescribeHosts', {}, [('item', HostInfo)]) - - def get_instance_types(self): - """Grabs the list of all users.""" - return self.apiconn.get_list('DescribeInstanceTypes', {}, - [('item', InstanceType)]) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7ba8dfbea..425784e8a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -757,6 +757,8 @@ class CloudController(object): iterator = db.floating_ip_get_all_by_project(context, context.project_id) for floating_ip_ref in iterator: + if floating_ip_ref['project_id'] is None: + continue address = floating_ip_ref['address'] ec2_id = None if (floating_ip_ref['fixed_ip'] diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 0e9c4b26f..940bd8771 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -60,6 +60,7 @@ class Fault(webob.exc.HTTPException): serializer = wsgi.Serializer(metadata) content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) + self.wrapped_exc.content_type = content_type return self.wrapped_exc diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 6bd173bb8..6704a68ae 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -150,6 +150,15 @@ class Controller(wsgi.Controller): injected_files = self._get_injected_files(personality) flavor_id = self._flavor_id_from_req_data(env) + + if not 'name' in env['server']: + msg = _("Server name is not defined") + return exc.HTTPBadRequest(msg) + + name = env['server']['name'] + self._validate_server_name(name) + name = name.strip() + try: (inst,) = self.compute_api.create( context, @@ -157,8 +166,8 @@ class Controller(wsgi.Controller): image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, - display_name=env['server']['name'], - display_description=env['server']['name'], + display_name=name, + display_description=name, key_name=key_name, key_data=key_data, metadata=metadata, @@ -171,8 +180,7 @@ class Controller(wsgi.Controller): builder = self._get_view_builder(req) server = builder.build(inst, is_detail=True) - password = "%s%s" % (server['server']['name'][:4], - utils.generate_password(12)) + password = utils.generate_password(16) server['server']['adminPass'] = password self.compute_api.set_admin_password(context, server['server']['id'], password) @@ -246,31 +254,45 @@ class Controller(wsgi.Controller): ctxt = req.environ['nova.context'] update_dict = {} - if 'adminPass' in inst_dict['server']: - update_dict['admin_pass'] = inst_dict['server']['adminPass'] - try: - self.compute_api.set_admin_password(ctxt, id) - except exception.TimeoutException: - return exc.HTTPRequestTimeout() + if 'name' in inst_dict['server']: - update_dict['display_name'] = inst_dict['server']['name'] + name = inst_dict['server']['name'] + self._validate_server_name(name) + update_dict['display_name'] = name.strip() + + self._parse_update(ctxt, id, inst_dict, update_dict) + try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNoContent() + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(msg) + + def _parse_update(self, context, id, inst_dict, update_dict): + pass + @scheduler_api.redirect_handler def action(self, req, id): """Multi-purpose method used to reboot, rebuild, or resize a server""" actions = { - 'reboot': self._action_reboot, - 'resize': self._action_resize, + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'resize': self._action_resize, 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - 'rebuild': self._action_rebuild, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, } input_dict = self._deserialize(req.body, req.get_content_type()) @@ -279,6 +301,9 @@ class Controller(wsgi.Controller): return actions[key](input_dict, req, id) return faults.Fault(exc.HTTPNotImplemented()) + def _action_change_password(self, input_dict, req, id): + return exc.HTTPNotImplemented() + def _action_confirm_resize(self, input_dict, req, id): try: self.compute_api.confirm_resize(req.environ['nova.context'], id) @@ -576,6 +601,14 @@ class ControllerV10(Controller): def _limit_items(self, items, req): return common.limited(items, req) + def _parse_update(self, context, server_id, inst_dict, update_dict): + if 'adminPass' in inst_dict['server']: + update_dict['admin_pass'] = inst_dict['server']['adminPass'] + try: + self.compute_api.set_admin_password(context, server_id) + except exception.TimeoutException: + return exc.HTTPRequestTimeout() + class ControllerV11(Controller): def _image_id_from_req_data(self, data): @@ -599,6 +632,19 @@ class ControllerV11(Controller): def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV11(req) + def _action_change_password(self, input_dict, req, id): + context = req.environ['nova.context'] + if (not 'changePassword' in input_dict + or not 'adminPass' in input_dict['changePassword']): + msg = _("No adminPass was specified") + return exc.HTTPBadRequest(msg) + password = input_dict['changePassword']['adminPass'] + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + return exc.HTTPBadRequest(msg) + self.compute_api.set_admin_password(context, id, password) + return exc.HTTPAccepted() + def _limit_items(self, items, req): return common.limited_by_marker(items, req) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 33f1dd628..3f9d91934 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import webob import webob.dec -import webob.exc from nova import wsgi import nova.api.openstack.views.versions @@ -51,4 +51,10 @@ class Versions(wsgi.Application): } content_type = req.best_match_content_type() - return wsgi.Serializer(metadata).serialize(response, content_type) + body = wsgi.Serializer(metadata).serialize(response, content_type) + + response = webob.Response() + response.content_type = content_type + response.body = body + + return response diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 3807fa95f..16195b050 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -60,8 +60,8 @@ class ViewBuilder(object): self._format_status(image_obj) image = { - "id": image_obj["id"], - "name": image_obj["name"], + "id": image_obj.get("id"), + "name": image_obj.get("name"), } if "instance_id" in properties: @@ -72,9 +72,9 @@ class ViewBuilder(object): if detail: image.update({ - "created": image_obj["created_at"], - "updated": image_obj["updated_at"], - "status": image_obj["status"], + "created": image_obj.get("created_at"), + "updated": image_obj.get("updated_at"), + "status": image_obj.get("status"), }) if image["status"] == "SAVING": diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 4e7f62eb3..d24c025be 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -57,16 +57,16 @@ class ViewBuilder(object): def _build_detail(self, inst): """Returns a detailed model of a server.""" power_mapping = { - None: 'build', - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'paused', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error', - power_state.FAILED: 'error'} + None: 'BUILD', + power_state.NOSTATE: 'BUILD', + power_state.RUNNING: 'ACTIVE', + power_state.BLOCKED: 'ACTIVE', + power_state.SUSPENDED: 'SUSPENDED', + power_state.PAUSED: 'PAUSED', + power_state.SHUTDOWN: 'ACTIVE', + power_state.SHUTOFF: 'ACTIVE', + power_state.CRASHED: 'ERROR', + power_state.FAILED: 'ERROR'} inst_dict = { 'id': int(inst['id']), @@ -77,7 +77,7 @@ class ViewBuilder(object): ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() if compute_api.has_finished_migration(ctxt, inst['id']): - inst_dict['status'] = 'resize-confirm' + inst_dict['status'] = 'RESIZE-CONFIRM' # Return the metadata as a dictionary metadata = {} diff --git a/nova/compute/api.py b/nova/compute/api.py index 1dbd73f8f..996955fe3 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -37,10 +37,14 @@ from nova.compute import instance_types from nova.scheduler import api as scheduler_api from nova.db import base -FLAGS = flags.FLAGS + LOG = logging.getLogger('nova.compute.api') +FLAGS = flags.FLAGS +flags.DECLARE('vncproxy_topic', 'nova.vnc') + + def generate_default_hostname(instance_id): """Default function to generate a hostname given an instance reference.""" return str(instance_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b2a13a01b..6da8dac10 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -660,7 +660,9 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): filter(models.FixedIp.instance_id != None).\ filter_by(allocated=0).\ update({'instance_id': None, - 'leased': 0}) + 'leased': 0, + 'updated_at': datetime.datetime.utcnow()}, + synchronize_session='fetch') return result diff --git a/nova/image/s3.py b/nova/image/s3.py index 85a2c651c..ddec5f3aa 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -31,6 +31,7 @@ from xml.etree import ElementTree import boto.s3.connection +from nova import crypto from nova import exception from nova import flags from nova import utils @@ -210,7 +211,7 @@ class S3ImageService(service.BaseImageService): # FIXME(vish): grab key from common service so this can run on # any host. - cloud_pk = os.path.join(FLAGS.ca_path, "private/cakey.pem") + cloud_pk = crypto.key_path(context.project_id) decrypted_filename = os.path.join(image_path, 'image.tar.gz') self._decrypt_image(encrypted_filename, encrypted_key, diff --git a/nova/network/api.py b/nova/network/api.py index 4ee1148cb..c56e3062b 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -66,6 +66,21 @@ class API(base.Base): if isinstance(fixed_ip, str) or isinstance(fixed_ip, unicode): fixed_ip = self.db.fixed_ip_get_by_address(context, fixed_ip) floating_ip = self.db.floating_ip_get_by_address(context, floating_ip) + # Check if the floating ip address is allocated + if floating_ip['project_id'] is None: + raise exception.ApiError(_("Address (%s) is not allocated") % + floating_ip['address']) + # Check if the floating ip address is allocated to the same project + if floating_ip['project_id'] != context.project_id: + LOG.warn(_("Address (%(address)s) is not allocated to your " + "project (%(project)s)"), + {'address': floating_ip['address'], + 'project': context.project_id}) + raise exception.ApiError(_("Address (%(address)s) is not " + "allocated to your project" + "(%(project)s)") % + {'address': floating_ip['address'], + 'project': context.project_id}) # NOTE(vish): Perhaps we should just pass this on to compute and # let compute communicate with network. host = fixed_ip['network']['host'] diff --git a/nova/rpc.py b/nova/rpc.py index 388f78d69..b610cdf9b 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -74,7 +74,12 @@ class Connection(carrot_connection.BrokerConnection): """Recreates the connection instance This is necessary to recover from some network errors/disconnects""" - del cls._instance + try: + del cls._instance + except AttributeError, e: + # The _instance stuff is for testing purposes. Usually we don't use + # it. So don't freak out if it doesn't exist. + pass return cls.instance() @@ -125,9 +130,9 @@ class Consumer(messaging.Consumer): # NOTE(vish): This is catching all errors because we really don't # want exceptions to be logged 10 times a second if some # persistent failure occurs. - except Exception: # pylint: disable=W0703 + except Exception, e: # pylint: disable=W0703 if not self.failed_connection: - LOG.exception(_("Failed to fetch message from queue")) + LOG.exception(_("Failed to fetch message from queue: %s" % e)) self.failed_connection = True def attach_to_eventlet(self): diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index 9deaa2777..f4461cee2 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -34,5 +34,7 @@ class ChanceScheduler(driver.Scheduler): hosts = self.hosts_up(context, topic) if not hosts: - raise driver.NoValidHost(_("No hosts found")) + raise driver.NoValidHost(_("Scheduler was unable to locate a host" + " for this request. Is the appropriate" + " service running?")) return hosts[int(random.random() * len(hosts))] diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 0191ceb3d..dd568d2c6 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -72,7 +72,9 @@ class SimpleScheduler(chance.ChanceScheduler): {'host': service['host'], 'scheduled_at': now}) return service['host'] - raise driver.NoValidHost(_("No hosts found")) + raise driver.NoValidHost(_("Scheduler was unable to locate a host" + " for this request. Is the appropriate" + " service running?")) def schedule_create_volume(self, context, volume_id, *_args, **_kwargs): """Picks a host that is up and has the fewest volumes.""" @@ -107,7 +109,9 @@ class SimpleScheduler(chance.ChanceScheduler): {'host': service['host'], 'scheduled_at': now}) return service['host'] - raise driver.NoValidHost(_("No hosts found")) + raise driver.NoValidHost(_("Scheduler was unable to locate a host" + " for this request. Is the appropriate" + " service running?")) def schedule_set_network_host(self, context, *_args, **_kwargs): """Picks a host that is up and has the fewest networks.""" @@ -119,4 +123,6 @@ class SimpleScheduler(chance.ChanceScheduler): raise driver.NoValidHost(_("All hosts have too many networks")) if self.service_is_up(service): return service['host'] - raise driver.NoValidHost(_("No hosts found")) + raise driver.NoValidHost(_("Scheduler was unable to locate a host" + " for this request. Is the appropriate" + " service running?")) diff --git a/nova/scheduler/zone.py b/nova/scheduler/zone.py index 49786cd32..44d5a166f 100644 --- a/nova/scheduler/zone.py +++ b/nova/scheduler/zone.py @@ -52,5 +52,8 @@ class ZoneScheduler(driver.Scheduler): zone = _kwargs.get('availability_zone') hosts = self.hosts_up_with_zone(context, topic, zone) if not hosts: - raise driver.NoValidHost(_("No hosts found")) + raise driver.NoValidHost(_("Scheduler was unable to locate a host" + " for this request. Is the appropriate" + " service running?")) + return hosts[int(random.random() * len(hosts))] diff --git a/nova/tests/api/openstack/test_faults.py b/nova/tests/api/openstack/test_faults.py index 7667753f4..9746e8168 100644 --- a/nova/tests/api/openstack/test_faults.py +++ b/nova/tests/api/openstack/test_faults.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + import webob import webob.dec import webob.exc @@ -24,35 +26,115 @@ from nova.api.openstack import faults class TestFaults(test.TestCase): + """Tests covering `nova.api.openstack.faults:Fault` class.""" - def test_fault_parts(self): - req = webob.Request.blank('/.xml') - f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) - resp = req.get_response(f) + def _prepare_xml(self, xml_string): + """Remove characters from string which hinder XML equality testing.""" + xml_string = xml_string.replace(" ", "") + xml_string = xml_string.replace("\n", "") + xml_string = xml_string.replace("\t", "") + return xml_string - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['<badRequest', 'code="400">']) - body_without_spaces = ''.join(resp.body.split()) - self.assertTrue('<message>scram</message>' in body_without_spaces) + def test_400_fault_xml(self): + """Test fault serialized to XML via file-extension and/or header.""" + requests = [ + webob.Request.blank('/.xml'), + webob.Request.blank('/', headers={"Accept": "application/xml"}), + ] - def test_retry_header(self): - req = webob.Request.blank('/.xml') - exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', - headers={'Retry-After': 4}) - f = faults.Fault(exc) - resp = req.get_response(f) - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['<overLimit', 'code="413">']) - body_sans_spaces = ''.join(resp.body.split()) - self.assertTrue('<message>sorry</message>' in body_sans_spaces) - self.assertTrue('<retryAfter>4</retryAfter>' in body_sans_spaces) - self.assertEqual(resp.headers['Retry-After'], 4) + for request in requests: + fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + expected = self._prepare_xml(""" + <badRequest code="400"> + <message>scram</message> + </badRequest> + """) + actual = self._prepare_xml(response.body) + + self.assertEqual(response.content_type, "application/xml") + self.assertEqual(expected, actual) + + def test_400_fault_json(self): + """Test fault serialized to JSON via file-extension and/or header.""" + requests = [ + webob.Request.blank('/.json'), + webob.Request.blank('/', headers={"Accept": "application/json"}), + ] + + for request in requests: + fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + expected = { + "badRequest": { + "message": "scram", + "code": 400, + }, + } + actual = json.loads(response.body) + + self.assertEqual(response.content_type, "application/json") + self.assertEqual(expected, actual) + + def test_413_fault_xml(self): + requests = [ + webob.Request.blank('/.xml'), + webob.Request.blank('/', headers={"Accept": "application/xml"}), + ] + + for request in requests: + exc = webob.exc.HTTPRequestEntityTooLarge + fault = faults.Fault(exc(explanation='sorry', + headers={'Retry-After': 4})) + response = request.get_response(fault) + + expected = self._prepare_xml(""" + <overLimit code="413"> + <message>sorry</message> + <retryAfter>4</retryAfter> + </overLimit> + """) + actual = self._prepare_xml(response.body) + + self.assertEqual(expected, actual) + self.assertEqual(response.content_type, "application/xml") + self.assertEqual(response.headers['Retry-After'], 4) + + def test_413_fault_json(self): + """Test fault serialized to JSON via file-extension and/or header.""" + requests = [ + webob.Request.blank('/.json'), + webob.Request.blank('/', headers={"Accept": "application/json"}), + ] + + for request in requests: + exc = webob.exc.HTTPRequestEntityTooLarge + fault = faults.Fault(exc(explanation='sorry', + headers={'Retry-After': 4})) + response = request.get_response(fault) + + expected = { + "overLimit": { + "message": "sorry", + "code": 413, + "retryAfter": 4, + }, + } + actual = json.loads(response.body) + + self.assertEqual(response.content_type, "application/json") + self.assertEqual(expected, actual) def test_raise(self): + """Ensure the ability to raise `Fault`s in WSGI-ified methods.""" @webob.dec.wsgify def raiser(req): raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + req = webob.Request.blank('/.xml') resp = req.get_response(raiser) + self.assertEqual(resp.content_type, "application/xml") self.assertEqual(resp.status_int, 404) self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 57e447dce..69cc3116d 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -263,7 +263,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): {'id': 124, 'name': 'queued backup'}, {'id': 125, 'name': 'saving backup'}, {'id': 126, 'name': 'active backup'}, - {'id': 127, 'name': 'killed backup'}] + {'id': 127, 'name': 'killed backup'}, + {'id': 129, 'name': None}] self.assertDictListMatch(response_list, expected) @@ -339,6 +340,24 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image.toxml(), actual_image.toxml()) + def test_get_image_xml_no_name(self): + request = webob.Request.blank('/v1.0/images/129') + request.accept = "application/xml" + response = request.get_response(fakes.wsgi_app()) + + actual_image = minidom.parseString(response.body.replace(" ", "")) + + expected_now = self.NOW_API_FORMAT + expected_image = minidom.parseString(""" + <image id="129" + name="None" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE" /> + """ % (locals())) + + self.assertEqual(expected_image.toxml(), actual_image.toxml()) + def test_get_image_v1_1_xml(self): request = webob.Request.blank('/v1.1/images/123') request.accept = "application/xml" @@ -516,6 +535,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + }, + { + 'id': 129, + 'name': None, + 'updated': self.NOW_API_FORMAT, + 'created': self.NOW_API_FORMAT, + 'status': 'ACTIVE', }] self.assertDictListMatch(expected, response_list) @@ -635,7 +661,29 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "type": "application/xml", "href": "http://localhost/v1.1/images/127", }], - }] + }, + { + 'id': 129, + 'name': None, + 'updated': self.NOW_API_FORMAT, + 'created': self.NOW_API_FORMAT, + 'status': 'ACTIVE', + "links": [{ + "rel": "self", + "href": "http://localhost/v1.1/images/129", + }, + { + "rel": "bookmark", + "type": "application/json", + "href": "http://localhost/v1.1/images/129", + }, + { + "rel": "bookmark", + "type": "application/xml", + "href": "http://localhost/v1.1/images/129", + }], + }, + ] self.assertDictListMatch(expected, response_list) @@ -694,4 +742,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): status='active', properties=other_backup_properties) image_id += 1 + # Image without a name + add_fixture(id=image_id, is_public=True, status='active', + properties={}) + image_id += 1 + return fixtures diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 989385a8c..313676e72 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -377,7 +377,6 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) server = json.loads(res.body)['server'] - self.assertEqual('serv', server['adminPass'][:4]) self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) self.assertEqual(1, server['id']) @@ -392,6 +391,74 @@ class ServersTest(test.TestCase): fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False) self._test_create_instance_helper() + def test_create_instance_no_name(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'imageId': 3, + 'flavorId': 1, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + }, + } + + req = webob.Request.blank('/v1.0/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_nonstring_name(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'name': 12, + 'imageId': 3, + 'flavorId': 1, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + }, + } + + req = webob.Request.blank('/v1.0/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_whitespace_name(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'name': ' ', + 'imageId': 3, + 'flavorId': 1, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + }, + } + + req = webob.Request.blank('/v1.0/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_v11(self): self._setup_for_create_instance() @@ -418,7 +485,6 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) server = json.loads(res.body)['server'] - self.assertEqual('serv', server['adminPass'][:4]) self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) self.assertEqual(1, server['id']) @@ -448,39 +514,82 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 422) - def test_update_bad_params(self): + def test_update_nonstring_name(self): + """ Confirm that update is filtering params """ + inst_dict = dict(name=12, adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.content_type = "application/json" + req.body = self.body + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_whitespace_name(self): """ Confirm that update is filtering params """ - inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') + inst_dict = dict(name=' ', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.content_type = "application/json" + req.body = self.body + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_null_name(self): + """ Confirm that update is filtering params """ + inst_dict = dict(name='', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.content_type = "application/json" + req.body = self.body + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_server_v10(self): + inst_dict = dict(name='server_test', adminPass='bacon') self.body = json.dumps(dict(server=inst_dict)) def server_update(context, id, params): - self.update_called = True - filtered_dict = dict(name='server_test', admin_pass='bacon') + filtered_dict = dict( + display_name='server_test', + admin_pass='bacon', + ) self.assertEqual(params, filtered_dict) + return filtered_dict self.stubs.Set(nova.db.api, 'instance_update', server_update) req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' + req.content_type = "application/json" req.body = self.body - req.get_response(fakes.wsgi_app()) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) - def test_update_server(self): + def test_update_server_adminPass_ignored_v11(self): inst_dict = dict(name='server_test', adminPass='bacon') self.body = json.dumps(dict(server=inst_dict)) def server_update(context, id, params): - filtered_dict = dict(name='server_test', admin_pass='bacon') + filtered_dict = dict(display_name='server_test') self.assertEqual(params, filtered_dict) + return filtered_dict self.stubs.Set(nova.db.api, 'instance_update', server_update) - req = webob.Request.blank('/v1.0/servers/1') + req = webob.Request.blank('/v1.1/servers/1') req.method = 'PUT' + req.content_type = "application/json" req.body = self.body - req.get_response(fakes.wsgi_app()) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedule') @@ -520,6 +629,7 @@ class ServersTest(test.TestCase): self.assertEqual(s['name'], 'server%d' % i) self.assertEqual(s['imageId'], '10') self.assertEqual(s['flavorId'], '1') + self.assertEqual(s['status'], 'BUILD') self.assertEqual(s['metadata']['seq'], i) def test_get_all_server_details_v1_1(self): @@ -533,6 +643,7 @@ class ServersTest(test.TestCase): self.assertEqual(s['name'], 'server%d' % i) self.assertEqual(s['imageRef'], 'http://localhost/v1.1/images/10') self.assertEqual(s['flavorRef'], 'http://localhost/v1.1/flavors/1') + self.assertEqual(s['status'], 'BUILD') self.assertEqual(s['metadata']['seq'], i) def test_get_all_server_details_with_host(self): @@ -653,6 +764,74 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) + def test_server_change_password(self): + body = {'changePassword': {'adminPass': '1234pass'}} + req = webob.Request.blank('/v1.0/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, 501) + + def test_server_change_password_v1_1(self): + + 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 + + mock_method = MockSetAdminPassword() + self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': '1234pass'}} + req = webob.Request.blank('/v1.1/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) + self.assertEqual(mock_method.instance_id, '1') + self.assertEqual(mock_method.password, '1234pass') + + def test_server_change_password_bad_request_v1_1(self): + body = {'changePassword': {'pass': '12345'}} + req = webob.Request.blank('/v1.1/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, 400) + + def test_server_change_password_empty_string_v1_1(self): + body = {'changePassword': {'adminPass': ''}} + req = webob.Request.blank('/v1.1/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, 400) + + def test_server_change_password_none_v1_1(self): + body = {'changePassword': {'adminPass': None}} + req = webob.Request.blank('/v1.1/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, 400) + + def test_server_change_password_not_a_string_v1_1(self): + body = {'changePassword': {'adminPass': 1234}} + req = webob.Request.blank('/v1.1/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, 400) + def test_server_reboot(self): body = dict(server=dict( name='server_test', imageId=2, flavorId=2, metadata={}, @@ -738,7 +917,7 @@ class ServersTest(test.TestCase): fake_migration_get) res = req.get_response(fakes.wsgi_app()) body = json.loads(res.body) - self.assertEqual(body['server']['status'], 'resize-confirm') + self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM') def test_confirm_resize_server(self): req = self.webreq('/1/action', 'POST', dict(confirmResize=None)) @@ -1315,7 +1494,7 @@ class TestServerInstanceCreation(test.TestCase): self.assertEquals(response.status_int, 200) response = json.loads(response.body) self.assertTrue('adminPass' in response['server']) - self.assertTrue(response['server']['adminPass'].startswith('fake')) + self.assertEqual(16, len(response['server']['adminPass'])) def test_create_instance_admin_pass_xml(self): request, response, dummy = \ @@ -1324,7 +1503,7 @@ class TestServerInstanceCreation(test.TestCase): dom = minidom.parseString(response.body) server = dom.childNodes[0] self.assertEquals(server.nodeName, 'server') - self.assertTrue(server.getAttribute('adminPass').startswith('fake')) + self.assertEqual(16, len(server.getAttribute('adminPass'))) class TestGetKernelRamdiskFromImage(test.TestCase): diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index ebb59a9a6..2640a4ddb 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -34,8 +34,10 @@ class VersionsTest(test.TestCase): def test_get_version_list(self): req = webob.Request.blank('/') + req.accept = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") versions = json.loads(res.body)["versions"] expected = [ { @@ -61,6 +63,30 @@ class VersionsTest(test.TestCase): ] self.assertEqual(versions, expected) + def test_get_version_list_xml(self): + req = webob.Request.blank('/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + + expected = """<versions> + <version id="v1.1" status="CURRENT"> + <links> + <link href="http://localhost/v1.1" rel="self"/> + </links> + </version> + <version id="v1.0" status="DEPRECATED"> + <links> + <link href="http://localhost/v1.0" rel="self"/> + </links> + </version> + </versions>""".replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + + self.assertEqual(expected, actual) + def test_view_builder(self): base_url = "http://example.org/" diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 00803d0ad..5cb969979 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -41,6 +41,7 @@ from nova.compute import power_state from nova.api.ec2 import cloud from nova.api.ec2 import ec2utils from nova.image import local +from nova.exception import NotFound FLAGS = flags.FLAGS @@ -71,7 +72,8 @@ class CloudTestCase(test.TestCase): host = self.network.get_network_host(self.context.elevated()) def fake_show(meh, context, id): - return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} + return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + 'type': 'machine'}} self.stubs.Set(local.LocalImageService, 'show', fake_show) self.stubs.Set(local.LocalImageService, 'show_by_name', fake_show) @@ -216,6 +218,35 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def test_describe_images(self): + describe_images = self.cloud.describe_images + + def fake_detail(meh, context): + return [{'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + 'type': 'machine'}}] + + def fake_show_none(meh, context, id): + raise NotFound + + self.stubs.Set(local.LocalImageService, 'detail', fake_detail) + # list all + result1 = describe_images(self.context) + result1 = result1['imagesSet'][0] + self.assertEqual(result1['imageId'], 'ami-00000001') + # provided a valid image_id + result2 = describe_images(self.context, ['ami-00000001']) + self.assertEqual(1, len(result2['imagesSet'])) + # provide more than 1 valid image_id + result3 = describe_images(self.context, ['ami-00000001', + 'ami-00000002']) + self.assertEqual(2, len(result3['imagesSet'])) + # provide an non-existing image_id + self.stubs.UnsetAll() + self.stubs.Set(local.LocalImageService, 'show', fake_show_none) + self.stubs.Set(local.LocalImageService, 'show_by_name', fake_show_none) + self.assertRaises(NotFound, describe_images, + self.context, ['ami-fake']) + def test_console_output(self): instance_type = FLAGS.default_instance_type max_count = 1 diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 01d46b21d..3cdbd681a 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -188,7 +188,7 @@ class XenAPIVMTestCase(test.TestCase): stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) stubs.stubout_get_this_vm_uuid(self.stubs) stubs.stubout_stream_disk(self.stubs) - stubs.stubout_is_vdi_pv(self.stubs) + stubs.stubout_determine_is_pv_objectstore(self.stubs) self.stubs.Set(VMOps, 'reset_network', reset_network) stubs.stub_out_vm_methods(self.stubs) glance_stubs.stubout_glance_client(self.stubs, @@ -327,18 +327,17 @@ class XenAPIVMTestCase(test.TestCase): self.assertEquals(self.vm['HVM_boot_params'], {}) self.assertEquals(self.vm['HVM_boot_policy'], '') - def _check_no_unbound_vdi(self): + def _list_vdis(self): url = FLAGS.xenapi_connection_url username = FLAGS.xenapi_connection_username password = FLAGS.xenapi_connection_password session = xenapi_conn.XenAPISession(url, username, password) - vdi_refs = session.call_xenapi('VDI.get_all') - for vdi_ref in vdi_refs: - vdi_rec = session.call_xenapi('VDI.get_record', vdi_ref) - if 'VBDs' in vdi_rec: - self.assertEquals(vdi_rec['VBDs'], {}) - else: - self.fail('Found unexpected unbound VDI:%s' % vdi_rec['uuid']) + return session.call_xenapi('VDI.get_all') + + def _check_vdis(self, start_list, end_list): + for vdi_ref in end_list: + if not vdi_ref in start_list: + self.fail('Found unexpected VDI:%s' % vdi_ref) def _test_spawn(self, image_id, kernel_id, ramdisk_id, instance_type="m1.large", os_type="linux", @@ -365,28 +364,32 @@ class XenAPIVMTestCase(test.TestCase): 1, 2, 3, "m1.xlarge") def test_spawn_fail_cleanup_1(self): + """Simulates an error while downloading an image. + Verifies that VDIs created are properly cleaned up. + """ - Simulates an error while downloading image - Verifies VDI create are properly cleaned up. - """ + vdi_recs_start = self._list_vdis() FLAGS.xenapi_image_service = 'glance' stubs.stubout_fetch_image_glance_disk(self.stubs) self.assertRaises(xenapi_fake.Failure, self._test_spawn, 1, 2, 3) - # ensure there is no VDI without a VBD - self._check_no_unbound_vdi() + # no new VDI should be found + vdi_recs_end = self._list_vdis() + self._check_vdis(vdi_recs_start, vdi_recs_end) def test_spawn_fail_cleanup_2(self): + """Simulates an error while creating VM record. It + verifies that VDIs created are properly cleaned up. + """ - Simulates an error while creating VM record. It - verifies that VDI created are properly cleaned up. - """ + vdi_recs_start = self._list_vdis() FLAGS.xenapi_image_service = 'glance' stubs.stubout_create_vm(self.stubs) self.assertRaises(xenapi_fake.Failure, self._test_spawn, 1, 2, 3) - # ensure there is no VDI without a VBD - self._check_no_unbound_vdi() + # no new VDI should be found + vdi_recs_end = self._list_vdis() + self._check_vdis(vdi_recs_start, vdi_recs_end) def test_spawn_raw_objectstore(self): FLAGS.xenapi_image_service = 'objectstore' diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 62457ea16..8b34f7cd4 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -132,10 +132,13 @@ def stubout_stream_disk(stubs): stubs.Set(vm_utils, '_stream_disk', f) -def stubout_is_vdi_pv(stubs): - def f(_1): +def stubout_determine_is_pv_objectstore(stubs): + """Assumes VMs never have PV kernels""" + + @classmethod + def f(cls, *args): return False - stubs.Set(vm_utils, '_is_vdi_pv', f) + stubs.Set(vm_utils.VMHelper, '_determine_is_pv_objectstore', f) def stubout_lookup_image(stubs): @@ -176,7 +179,10 @@ class FakeSessionForVMTests(fake.SessionBase): def __init__(self, uri): super(FakeSessionForVMTests, self).__init__(uri) - def host_call_plugin(self, _1, _2, _3, _4, _5): + def host_call_plugin(self, _1, _2, plugin, fn, args): + # copy_kernel_vdi returns nothing + if fn == 'copy_kernel_vdi': + return sr_ref = fake.get_all('SR')[0] vdi_ref = fake.create_vdi('', False, sr_ref, False) vdi_rec = fake.get_record('VDI', vdi_ref) diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index a1ed5ebbf..13f403a66 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -485,3 +485,7 @@ class HyperVConnection(driver.ComputeDriver): def poll_rescued_instances(self, timeout): pass + + def update_available_resource(self, ctxt, host): + """This method is supported only by libvirt.""" + return diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index b28584cb6..babbc610d 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -828,7 +828,10 @@ class LibvirtConnection(driver.ComputeDriver): if FLAGS.libvirt_type == 'lxc': target_partition = None - key = str(inst['key_data']) + if inst['key_data']: + key = str(inst['key_data']) + else: + key = None net = None nets = [] @@ -839,7 +842,7 @@ class LibvirtConnection(driver.ComputeDriver): for (network_ref, mapping) in network_info: ifc_num += 1 - if not 'injected' in network_ref: + if not network_ref['injected']: continue have_injected_networks = True diff --git a/nova/virt/vmwareapi/vim.py b/nova/virt/vmwareapi/vim.py index ba14f1512..159e16a80 100644 --- a/nova/virt/vmwareapi/vim.py +++ b/nova/virt/vmwareapi/vim.py @@ -21,10 +21,10 @@ Classes for making VMware VI SOAP calls. import httplib
-from suds import WebFault
-from suds.client import Client
-from suds.plugin import MessagePlugin
-from suds.sudsobject import Property
+try:
+ import suds
+except ImportError:
+ suds = None
from nova import flags
from nova.virt.vmwareapi import error_util
@@ -42,24 +42,25 @@ flags.DEFINE_string('vmwareapi_wsdl_loc', 'Refer readme-vmware to setup')
-class VIMMessagePlugin(MessagePlugin):
+if suds:
+ class VIMMessagePlugin(suds.plugin.MessagePlugin):
- def addAttributeForValue(self, node):
- # suds does not handle AnyType properly.
- # VI SDK requires type attribute to be set when AnyType is used
- if node.name == 'value':
- node.set('xsi:type', 'xsd:string')
+ def addAttributeForValue(self, node):
+ # suds does not handle AnyType properly.
+ # VI SDK requires type attribute to be set when AnyType is used
+ if node.name == 'value':
+ node.set('xsi:type', 'xsd:string')
- def marshalled(self, context):
- """suds will send the specified soap envelope.
- Provides the plugin with the opportunity to prune empty
- nodes and fixup nodes before sending it to the server.
- """
- # suds builds the entire request object based on the wsdl schema.
- # VI SDK throws server errors if optional SOAP nodes are sent without
- # values, e.g. <test/> as opposed to <test>test</test>
- context.envelope.prune()
- context.envelope.walk(self.addAttributeForValue)
+ def marshalled(self, context):
+ """suds will send the specified soap envelope.
+ Provides the plugin with the opportunity to prune empty
+ nodes and fixup nodes before sending it to the server.
+ """
+ # suds builds the entire request object based on the wsdl schema.
+ # VI SDK throws server errors if optional SOAP nodes are sent
+ # without values, e.g. <test/> as opposed to <test>test</test>
+ context.envelope.prune()
+ context.envelope.walk(self.addAttributeForValue)
class Vim:
@@ -75,6 +76,9 @@ class Vim: protocol: http or https
host : ESX IPAddress[:port] or ESX Hostname[:port]
"""
+ if not suds:
+ raise Exception(_("Unable to import suds."))
+
self._protocol = protocol
self._host_name = host
wsdl_url = FLAGS.vmwareapi_wsdl_loc
@@ -84,7 +88,7 @@ class Vim: #wsdl_url = '%s://%s/sdk/vimService.wsdl' % (self._protocol,
# self._host_name)
url = '%s://%s/sdk' % (self._protocol, self._host_name)
- self.client = Client(wsdl_url, location=url,
+ self.client = suds.client.Client(wsdl_url, location=url,
plugins=[VIMMessagePlugin()])
self._service_content = \
self.RetrieveServiceContent("ServiceInstance")
@@ -127,7 +131,7 @@ class Vim: # check of the SOAP response
except error_util.VimFaultException, excep:
raise
- except WebFault, excep:
+ except suds.WebFault, excep:
doc = excep.document
detail = doc.childAtPath("/Envelope/Body/Fault/detail")
fault_list = []
@@ -163,7 +167,7 @@ class Vim: """Builds the request managed object."""
# Request Managed Object Builder
if type(managed_object) == type(""):
- mo = Property(managed_object)
+ mo = suds.sudsobject.Property(managed_object)
mo._type = managed_object
else:
mo = managed_object
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 87c3fa299..20c1b2b45 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -47,6 +47,7 @@ from nova.virt.vmwareapi import vim from nova.virt.vmwareapi import vim_util
from nova.virt.vmwareapi.vmops import VMWareVMOps
+
LOG = logging.getLogger("nova.virt.vmwareapi_conn")
FLAGS = flags.FLAGS
@@ -109,7 +110,7 @@ class VMWareESXConnection(object): def __init__(self, host_ip, host_username, host_password,
api_retry_count, scheme="https"):
session = VMWareAPISession(host_ip, host_username, host_password,
- api_retry_count, scheme=scheme)
+ api_retry_count, scheme=scheme)
self._vmops = VMWareVMOps(session)
def init_host(self, host):
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 94eaabe73..5496a6bd5 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -22,6 +22,8 @@ # XenAPI plugin for reading/writing information to xenstore # +import base64 +import commands try: import json except ImportError: @@ -66,7 +68,7 @@ def key_init(self, arg_dict): try: resp = _wait_for_agent(self, request_id, arg_dict) except TimeoutError, e: - raise PluginError("%s" % e) + raise PluginError(e) return resp @@ -87,7 +89,7 @@ def password(self, arg_dict): try: resp = _wait_for_agent(self, request_id, arg_dict) except TimeoutError, e: - raise PluginError("%s" % e) + raise PluginError(e) return resp @@ -102,6 +104,75 @@ def resetnetwork(self, arg_dict): xenstore.write_record(self, arg_dict) +@jsonify +def inject_file(self, arg_dict): + """Expects a file path and the contents of the file to be written. Both + should be base64-encoded in order to eliminate errors as they are passed + through the stack. Writes that information to xenstore for the agent, + which will decode the file and intended path, and create it on the + instance. The original agent munged both of these into a single entry; + the new agent keeps them separate. We will need to test for the new agent, + and write the xenstore records to match the agent version. We will also + need to test to determine if the file injection method on the agent has + been disabled, and raise a NotImplemented error if that is the case. + """ + b64_path = arg_dict["b64_path"] + b64_file = arg_dict["b64_file"] + request_id = arg_dict["id"] + if self._agent_has_method("file_inject"): + # New version of the agent. Agent should receive a 'value' + # key whose value is a dictionary containing 'b64_path' and + # 'b64_file'. See old version below. + arg_dict["value"] = json.dumps({"name": "file_inject", + "value": {"b64_path": b64_path, "b64_file": b64_file}}) + elif self._agent_has_method("injectfile"): + # Old agent requires file path and file contents to be + # combined into one base64 value. + raw_path = base64.b64decode(b64_path) + raw_file = base64.b64decode(b64_file) + new_b64 = base64.b64encode("%s,%s") % (raw_path, raw_file) + arg_dict["value"] = json.dumps({"name": "injectfile", + "value": new_b64}) + else: + # Either the methods don't exist in the agent, or they + # have been disabled. + raise NotImplementedError(_("NOT IMPLEMENTED: Agent does not" + " support file injection.")) + arg_dict["path"] = "data/host/%s" % request_id + xenstore.write_record(self, arg_dict) + try: + resp = _wait_for_agent(self, request_id, arg_dict) + except TimeoutError, e: + raise PluginError(e) + return resp + + +def _agent_has_method(self, method): + """Check that the agent has a particular method by checking its + features. Cache the features so we don't have to query the agent + every time we need to check. + """ + try: + self._agent_methods + except AttributeError: + self._agent_methods = [] + if not self._agent_methods: + # Haven't been defined + tmp_id = commands.getoutput("uuidgen") + dct = {} + dct["value"] = json.dumps({"name": "features", "value": ""}) + dct["path"] = "data/host/%s" % tmp_id + xenstore.write_record(self, dct) + try: + resp = _wait_for_agent(self, tmp_id, dct) + except TimeoutError, e: + raise PluginError(e) + response = json.loads(resp) + # The agent returns a comma-separated list of methods. + self._agent_methods = response.split(",") + return method in self._agent_methods + + def _wait_for_agent(self, request_id, arg_dict): """Periodically checks xenstore for a response from the agent. The request is always written to 'data/host/{id}', and @@ -119,9 +190,8 @@ def _wait_for_agent(self, request_id, arg_dict): # First, delete the request record arg_dict["path"] = "data/host/%s" % request_id xenstore.delete_record(self, arg_dict) - raise TimeoutError( - "TIMEOUT: No response from agent within %s seconds." % - AGENT_TIMEOUT) + raise TimeoutError(_("TIMEOUT: No response from agent within" + " %s seconds.") % AGENT_TIMEOUT) ret = xenstore.read_record(self, arg_dict) # Note: the response for None with be a string that includes # double quotes. @@ -136,4 +206,5 @@ if __name__ == "__main__": XenAPIPlugin.dispatch( {"key_init": key_init, "password": password, - "resetnetwork": resetnetwork}) + "resetnetwork": resetnetwork, + "inject_file": inject_file}) diff --git a/smoketests/test_admin.py b/smoketests/test_admin.py index 46e5b2233..1b7a8d673 100644 --- a/smoketests/test_admin.py +++ b/smoketests/test_admin.py @@ -30,7 +30,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -from nova import adminclient from smoketests import flags from smoketests import base @@ -47,6 +46,7 @@ TEST_PROJECTNAME = '%sproject' % TEST_PREFIX class AdminSmokeTestCase(base.SmokeTestCase): def setUp(self): + import nova_adminclient as adminclient self.admin = adminclient.NovaAdminClient( access_key=os.getenv('EC2_ACCESS_KEY'), secret_key=os.getenv('EC2_SECRET_KEY'), diff --git a/tools/euca-get-ajax-console b/tools/euca-get-ajax-console index 3df3dcb53..a67c79d90 100755 --- a/tools/euca-get-ajax-console +++ b/tools/euca-get-ajax-console @@ -35,6 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): import boto import nova from boto.ec2.connection import EC2Connection +import euca2ools from euca2ools import Euca2ool, InstanceValidationError, Util usage_string = """ @@ -93,8 +94,13 @@ def override_connect_ec2(aws_access_key_id=None, aws_secret_access_key, **kwargs) # override boto's connect_ec2 method, so that we can use NovaEC2Connection +# (This is for Euca2ools 1.2) boto.connect_ec2 = override_connect_ec2 +# Override Euca2ools' EC2Connection class (which it gets from boto) +# (This is for Euca2ools 1.3) +euca2ools.EC2Connection = NovaEC2Connection + def usage(status=1): print usage_string diff --git a/tools/pip-requires b/tools/pip-requires index 4ab9644d8..6ea446493 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -30,4 +30,5 @@ sqlalchemy-migrate netaddr sphinx glance +nova-adminclient suds==0.4 |
