summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Hooker <matt@cloudscaling.com>2011-08-09 18:46:57 -0400
committerMatthew Hooker <matt@cloudscaling.com>2011-08-09 18:46:57 -0400
commite33a54ec84d7dde281f7a125695aabc171989964 (patch)
treefb5e124c814ef5d4b5652d178c9cc15aacd6d996
parent1fc7164ecaa42c33ff510ec8ef3ca92f183c96e3 (diff)
parent4b3165429797d40da17f5c59aaeadb00673b71b2 (diff)
merge from trunk
-rw-r--r--.mailmap2
-rw-r--r--Authors2
-rw-r--r--HACKING119
-rw-r--r--MANIFEST.in1
-rwxr-xr-xbin/nova-ajax-console-proxy10
-rwxr-xr-xbin/nova-manage69
-rw-r--r--nova/api/direct.py4
-rw-r--r--nova/api/ec2/__init__.py8
-rw-r--r--nova/api/ec2/apirequest.py2
-rw-r--r--nova/api/ec2/cloud.py243
-rw-r--r--nova/api/ec2/ec2utils.py29
-rw-r--r--nova/api/openstack/__init__.py22
-rw-r--r--nova/api/openstack/common.py101
-rw-r--r--nova/api/openstack/contrib/admin_only.py30
-rw-r--r--nova/api/openstack/contrib/floating_ips.py12
-rw-r--r--nova/api/openstack/contrib/hosts.py32
-rw-r--r--nova/api/openstack/create_instance_helper.py125
-rw-r--r--nova/api/openstack/extensions.py31
-rw-r--r--nova/api/openstack/image_metadata.py16
-rw-r--r--nova/api/openstack/images.py13
-rw-r--r--nova/api/openstack/schemas/atom-link.rng141
-rw-r--r--nova/api/openstack/schemas/atom.rng597
-rw-r--r--nova/api/openstack/schemas/v1.1/extension.rng11
-rw-r--r--nova/api/openstack/schemas/v1.1/extensions.rng6
-rw-r--r--nova/api/openstack/server_metadata.py138
-rw-r--r--nova/api/openstack/servers.py182
-rw-r--r--nova/api/openstack/versions.py270
-rw-r--r--nova/api/openstack/views/images.py4
-rw-r--r--nova/api/openstack/views/servers.py16
-rw-r--r--nova/api/openstack/views/versions.py56
-rw-r--r--nova/api/openstack/wsgi.py3
-rw-r--r--nova/api/openstack/xmlutil.py37
-rw-r--r--nova/api/openstack/zones.py2
-rw-r--r--nova/auth/novarc.template1
-rw-r--r--nova/block_device.py71
-rw-r--r--nova/cloudpipe/pipelib.py17
-rw-r--r--nova/compute/api.py201
-rw-r--r--nova/compute/manager.py104
-rw-r--r--nova/db/api.py32
-rw-r--r--nova/db/sqlalchemy/api.py253
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py72
-rw-r--r--nova/db/sqlalchemy/models.py7
-rw-r--r--nova/exception.py19
-rw-r--r--nova/flags.py7
-rw-r--r--nova/image/__init__.py3
-rw-r--r--nova/image/fake.py41
-rw-r--r--nova/image/glance.py136
-rw-r--r--nova/network/manager.py64
-rw-r--r--nova/scheduler/api.py13
-rw-r--r--nova/scheduler/zone_aware_scheduler.py12
-rw-r--r--nova/scheduler/zone_manager.py7
-rw-r--r--nova/test.py43
-rw-r--r--nova/tests/api/openstack/__init__.py3
-rw-r--r--nova/tests/api/openstack/fakes.py11
-rw-r--r--nova/tests/api/openstack/test_accounts.py7
-rw-r--r--nova/tests/api/openstack/test_adminapi.py5
-rw-r--r--nova/tests/api/openstack/test_common.py60
-rw-r--r--nova/tests/api/openstack/test_extensions.py25
-rw-r--r--nova/tests/api/openstack/test_flavors_extra_specs.py3
-rw-r--r--nova/tests/api/openstack/test_images.py40
-rw-r--r--nova/tests/api/openstack/test_server_actions.py1075
-rw-r--r--nova/tests/api/openstack/test_server_metadata.py300
-rw-r--r--nova/tests/api/openstack/test_servers.py1070
-rw-r--r--nova/tests/api/openstack/test_users.py8
-rw-r--r--nova/tests/api/openstack/test_versions.py862
-rw-r--r--nova/tests/api/openstack/test_zones.py3
-rw-r--r--nova/tests/glance/stubs.py5
-rw-r--r--nova/tests/hyperv_unittest.py5
-rw-r--r--nova/tests/image/test_glance.py39
-rw-r--r--nova/tests/image/test_s3.py10
-rw-r--r--nova/tests/integrated/integrated_helpers.py4
-rw-r--r--nova/tests/integrated/test_extensions.py5
-rw-r--r--nova/tests/integrated/test_login.py4
-rw-r--r--nova/tests/integrated/test_servers.py5
-rw-r--r--nova/tests/integrated/test_volumes.py5
-rw-r--r--nova/tests/integrated/test_xml.py5
-rw-r--r--nova/tests/scheduler/test_host_filter.py12
-rw-r--r--nova/tests/scheduler/test_least_cost_scheduler.py23
-rw-r--r--nova/tests/scheduler/test_scheduler.py19
-rw-r--r--nova/tests/scheduler/test_zone_aware_scheduler.py27
-rw-r--r--nova/tests/test_api.py36
-rw-r--r--nova/tests/test_auth.py4
-rw-r--r--nova/tests/test_block_device.py87
-rw-r--r--nova/tests/test_cloud.py161
-rw-r--r--nova/tests/test_compute.py497
-rw-r--r--nova/tests/test_db_api.py4
-rw-r--r--nova/tests/test_host_filter.py12
-rw-r--r--nova/tests/test_hosts.py18
-rw-r--r--nova/tests/test_image.py134
-rw-r--r--nova/tests/test_ipv6.py3
-rw-r--r--nova/tests/test_libvirt.py66
-rw-r--r--nova/tests/test_metadata.py7
-rw-r--r--nova/tests/test_network.py2
-rw-r--r--nova/tests/test_nova_manage.py82
-rw-r--r--nova/tests/test_quota.py42
-rw-r--r--nova/tests/test_rpc.py2
-rw-r--r--nova/tests/test_rpc_amqp.py24
-rw-r--r--nova/tests/test_service.py1
-rw-r--r--nova/tests/test_skip_examples.py47
-rw-r--r--nova/tests/test_virt.py83
-rw-r--r--nova/tests/test_vmwareapi.py9
-rw-r--r--nova/tests/test_xenapi.py111
-rw-r--r--nova/tests/test_zones.py1
-rw-r--r--nova/tests/xenapi/stubs.py6
-rw-r--r--nova/utils.py20
-rw-r--r--nova/virt/driver.py72
-rw-r--r--nova/virt/fake.py11
-rw-r--r--nova/virt/hyperv.py7
-rw-r--r--nova/virt/images.py6
-rw-r--r--nova/virt/libvirt.xml.template33
-rw-r--r--nova/virt/libvirt/connection.py241
-rw-r--r--nova/virt/libvirt/vif.py3
-rw-r--r--nova/virt/vmwareapi/vmops.py12
-rw-r--r--nova/virt/vmwareapi_conn.py13
-rw-r--r--nova/virt/xenapi/vm_utils.py23
-rw-r--r--nova/virt/xenapi/vmops.py178
-rw-r--r--nova/virt/xenapi_conn.py45
-rw-r--r--nova/vnc/proxy.py4
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/glance42
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost109
-rw-r--r--smoketests/test_netadmin.py10
-rw-r--r--tools/pip-requires3
122 files changed, 7319 insertions, 2029 deletions
diff --git a/.mailmap b/.mailmap
index 76e7bc669..5c8df80e0 100644
--- a/.mailmap
+++ b/.mailmap
@@ -18,6 +18,8 @@
<devin.carlen@gmail.com> <devcamcar@illian.local>
<ewan.mellor@citrix.com> <emellor@silver>
<itoumsn@nttdata.co.jp> <itoumsn@shayol>
+<jake@ansolabs.com> <jake@markupisart.com>
+<jake@ansolabs.com> <admin@jakedahn.com>
<jaypipes@gmail.com> <jpipes@serialcoder>
<jmckenty@gmail.com> <jmckenty@joshua-mckentys-macbook-pro.local>
<jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com>
diff --git a/Authors b/Authors
index 0e5931462..e639cbf76 100644
--- a/Authors
+++ b/Authors
@@ -37,6 +37,7 @@ Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
Hisaki Ohara <hisaki.ohara@intel.com>
Ilya Alekseyev <ilyaalekseyev@acm.org>
Isaku Yamahata <yamahata@valinux.co.jp>
+Jake Dahn <jake@ansolabs.com>
Jason Cannavale <jason.cannavale@rackspace.com>
Jason Koelker <jason@koelker.net>
Jay Pipes <jaypipes@gmail.com>
@@ -64,6 +65,7 @@ Kirill Shileev <kshileev@gmail.com>
Koji Iida <iida.koji@lab.ntt.co.jp>
Lorin Hochstein <lorin@isi.edu>
Lvov Maxim <usrleon@gmail.com>
+Mandell Degerness <mdegerne@gmail.com>
Mark Washenberger <mark.washenberger@rackspace.com>
Masanori Itoh <itoumsn@nttdata.co.jp>
Matt Dietz <matt.dietz@rackspace.com>
diff --git a/HACKING b/HACKING
index 2f364c894..232a353fd 100644
--- a/HACKING
+++ b/HACKING
@@ -5,12 +5,23 @@ Step 1: Read http://www.python.org/dev/peps/pep-0008/
Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
Step 3: Read on
+
+General
+-------
+- Put two newlines between top-level code (funcs, classes, etc)
+- Put one newline between methods in classes and anywhere else
+- Do not write "except:", use "except Exception:" at the very least
+- Include your name with TODOs as in "#TODO(termie)"
+- Do not name anything the same name as a built-in or reserved word
+
+
Imports
-------
-- thou shalt not import objects, only modules
-- thou shalt not import more than one module per line
-- thou shalt not make relative imports
-- thou shalt organize your imports according to the following template
+- Do not import objects, only modules
+- Do not import more than one module per line
+- Do not make relative imports
+- Order your imports by the full module path
+- Organize your imports according to the following template
::
# vim: tabstop=4 shiftwidth=4 softtabstop=4
@@ -22,16 +33,6 @@ Imports
{{begin your code}}
-General
--------
-- thou shalt put two newlines twixt toplevel code (funcs, classes, etc)
-- thou shalt put one newline twixt methods in classes and anywhere else
-- thou shalt not write "except:", use "except Exception:" at the very least
-- thou shalt include your name with TODOs as in "TODO(termie)"
-- thou shalt not name anything the same name as a builtin or reserved word
-- thou shalt not violate causality in our time cone, or else
-
-
Human Alphabetical Order Examples
---------------------------------
::
@@ -42,11 +43,13 @@ Human Alphabetical Order Examples
import time
import unittest
- from nova import flags
- from nova import test
+ import nova.api.ec2
+ from nova.api import openstack
from nova.auth import users
- from nova.endpoint import api
+ import nova.flags
from nova.endpoint import cloud
+ from nova import test
+
Docstrings
----------
@@ -70,6 +73,88 @@ Docstrings
:param foo: the foo parameter
:param bar: the bar parameter
+ :returns: return_type -- description of the return value
:returns: description of the return value
+ :raises: AttributeError, KeyError
"""
+
+
+Dictionaries/Lists
+------------------
+ If a dictionary (dict) or list object is longer than 80 characters, its
+ items should be split with newlines. Embedded iterables should have their
+ items indented. Additionally, the last item in the dictionary should have
+ a trailing comma. This increases readability and simplifies future diffs.
+
+ Example:
+
+ my_dictionary = {
+ "image": {
+ "name": "Just a Snapshot",
+ "size": 2749573,
+ "properties": {
+ "user_id": 12,
+ "arch": "x86_64",
+ },
+ "things": [
+ "thing_one",
+ "thing_two",
+ ],
+ "status": "ACTIVE",
+ },
+ }
+
+
+Calling Methods
+---------------
+ Calls to methods 80 characters or longer should format each argument with
+ newlines. This is not a requirement, but a guideline.
+
+ unnecessarily_long_function_name('string one',
+ 'string two',
+ kwarg1=constants.ACTIVE,
+ kwarg2=['a', 'b', 'c'])
+
+
+ Rather than constructing parameters inline, it is better to break things up:
+
+ list_of_strings = [
+ 'what_a_long_string',
+ 'not as long',
+ ]
+
+ dict_of_numbers = {
+ 'one': 1,
+ 'two': 2,
+ 'twenty four': 24,
+ }
+
+ object_one.call_a_method('string three',
+ 'string four',
+ kwarg1=list_of_strings,
+ kwarg2=dict_of_numbers)
+
+
+Internationalization (i18n) Strings
+-----------------------------------
+ In order to support multiple languages, we have a mechanism to support
+ automatic translations of exception and log strings.
+
+ Example:
+ msg = _("An error occurred")
+ raise HTTPBadRequest(explanation=msg)
+
+ If you have a variable to place within the string, first internationalize
+ the template string then do the replacement.
+
+ Example:
+ msg = _("Missing parameter: %s") % ("flavor",)
+ LOG.error(msg)
+
+ If you have multiple variables to place in the string, use keyword
+ parameters. This helps our translators reorder parameters when needed.
+
+ Example:
+ msg = _("The server with id %(s_id)s has no key %(m_key)s")
+ LOG.error(msg % {"s_id": "1234", "m_key": "imageId"})
diff --git a/MANIFEST.in b/MANIFEST.in
index 421cd806a..883aba8a1 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -10,6 +10,7 @@ graft bzrplugins
graft contrib
graft po
graft plugins
+graft nova/api/openstack/schemas
include nova/api/openstack/notes.txt
include nova/auth/*.schema
include nova/auth/novarc.template
diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy
index 21cf68007..2329581a2 100755
--- a/bin/nova-ajax-console-proxy
+++ b/bin/nova-ajax-console-proxy
@@ -114,11 +114,11 @@ class AjaxConsoleProxy(object):
AjaxConsoleProxy.tokens[kwargs['token']] = \
{'args': kwargs, 'last_activity': time.time()}
- conn = rpc.Connection.instance(new=True)
- consumer = rpc.TopicAdapterConsumer(
- connection=conn,
- proxy=TopicProxy,
- topic=FLAGS.ajax_console_proxy_topic)
+ conn = rpc.create_connection(new=True)
+ consumer = rpc.create_consumer(
+ conn,
+ FLAGS.ajax_console_proxy_topic,
+ TopicProxy)
def delete_expired_tokens():
now = time.time()
diff --git a/bin/nova-manage b/bin/nova-manage
index 75d74903c..077a89d6f 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -56,6 +56,7 @@
import gettext
import glob
import json
+import math
import netaddr
import os
import sys
@@ -591,6 +592,31 @@ class FixedIpCommands(object):
fixed_ip['address'],
mac_address, hostname, host)
+ @args('--address', dest="address", metavar='<ip address>',
+ help='IP address')
+ def reserve(self, address):
+ """Mark fixed ip as reserved
+ arguments: address"""
+ self._set_reserved(address, True)
+
+ @args('--address', dest="address", metavar='<ip address>',
+ help='IP address')
+ def unreserve(self, address):
+ """Mark fixed ip as free to use
+ arguments: address"""
+ self._set_reserved(address, False)
+
+ def _set_reserved(self, address, reserved):
+ ctxt = context.get_admin_context()
+
+ try:
+ fixed_ip = db.fixed_ip_get_by_address(ctxt, address)
+ db.fixed_ip_update(ctxt, fixed_ip['address'],
+ {'reserved': reserved})
+ except exception.NotFound as ex:
+ print "error: %s" % ex
+ sys.exit(2)
+
class FloatingIpCommands(object):
"""Class for managing floating ip."""
@@ -662,8 +688,9 @@ class NetworkCommands(object):
# check for certain required inputs
if not label:
raise exception.NetworkNotCreated(req='--label')
- if not fixed_range_v4:
- raise exception.NetworkNotCreated(req='--fixed_range_v4')
+ if not (fixed_range_v4 or fixed_range_v6):
+ req = '--fixed_range_v4 or --fixed_range_v6'
+ raise exception.NetworkNotCreated(req=req)
bridge = bridge or FLAGS.flat_network_bridge
if not bridge:
@@ -689,21 +716,20 @@ class NetworkCommands(object):
if FLAGS.network_manager in interface_required:
raise exception.NetworkNotCreated(req='--bridge_interface')
- if FLAGS.use_ipv6:
- fixed_range_v6 = fixed_range_v6 or FLAGS.fixed_range_v6
- if not fixed_range_v6:
- raise exception.NetworkNotCreated(req='with use_ipv6, '
- '--fixed_range_v6')
- gateway_v6 = gateway_v6 or FLAGS.gateway_v6
- if not gateway_v6:
- raise exception.NetworkNotCreated(req='with use_ipv6, '
- '--gateway_v6')
-
# sanitize other input using FLAGS if necessary
if not num_networks:
num_networks = FLAGS.num_networks
if not network_size:
- network_size = FLAGS.network_size
+ fixnet = netaddr.IPNetwork(fixed_range_v4)
+ each_subnet_size = fixnet.size / int(num_networks)
+ if each_subnet_size > FLAGS.network_size:
+ network_size = FLAGS.network_size
+ subnet = 32 - int(math.log(network_size, 2))
+ oversize_msg = _('Subnet(s) too large, defaulting to /%s.'
+ ' To override, specify network_size flag.') % subnet
+ print oversize_msg
+ else:
+ network_size = fixnet.size
if not multi_host:
multi_host = FLAGS.multi_host
else:
@@ -735,8 +761,8 @@ class NetworkCommands(object):
def list(self):
"""List all created networks"""
print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
- _('network'),
- _('netmask'),
+ _('IPv4'),
+ _('IPv6'),
_('start address'),
_('DNS1'),
_('DNS2'),
@@ -745,7 +771,7 @@ class NetworkCommands(object):
for network in db.network_get_all(context.get_admin_context()):
print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
network.cidr,
- network.netmask,
+ network.cidr_v6,
network.dhcp_start,
network.dns1,
network.dns2,
@@ -1093,10 +1119,12 @@ class InstanceTypeCommands(object):
@args('--name', dest='name', metavar='<name>',
help='Name of instance type/flavor')
- def delete(self, name, purge=None):
+ @args('--purge', action="store_true", dest='purge', default=False,
+ help='purge record from database')
+ def delete(self, name, purge):
"""Marks instance types / flavors as deleted"""
try:
- if purge == "--purge":
+ if purge:
instance_types.purge(name)
verb = "purged"
else:
@@ -1233,11 +1261,12 @@ class ImageCommands(object):
is_public, architecture)
def _lookup(self, old_image_id):
+ elevated = context.get_admin_context()
try:
internal_id = ec2utils.ec2_id_to_id(old_image_id)
- image = self.image_service.show(context, internal_id)
+ image = self.image_service.show(elevated, internal_id)
except (exception.InvalidEc2Id, exception.ImageNotFound):
- image = self.image_service.show_by_name(context, old_image_id)
+ image = self.image_service.show_by_name(elevated, old_image_id)
return image['id']
def _old_to_new(self, old):
diff --git a/nova/api/direct.py b/nova/api/direct.py
index 993815fc7..fdd2943d2 100644
--- a/nova/api/direct.py
+++ b/nova/api/direct.py
@@ -296,8 +296,8 @@ class ServiceWrapper(object):
'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
}[content_type]
return serializer.serialize(result)
- except:
- raise exception.Error("returned non-serializable type: %s"
+ except Exception, e:
+ raise exception.Error(_("Returned non-serializable type: %s")
% result)
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index af232edda..8b6e47cfb 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -147,7 +147,7 @@ class Authenticate(wsgi.Middleware):
try:
signature = req.params['Signature']
access = req.params['AWSAccessKeyId']
- except:
+ except KeyError, e:
raise webob.exc.HTTPBadRequest()
# Make a copy of args for authentication and signature verification.
@@ -211,7 +211,7 @@ class Requestify(wsgi.Middleware):
for non_arg in non_args:
# Remove, but raise KeyError if omitted
args.pop(non_arg)
- except:
+ except KeyError, e:
raise webob.exc.HTTPBadRequest()
LOG.debug(_('action: %s'), action)
@@ -354,6 +354,10 @@ class Executor(wsgi.Application):
LOG.debug(_('KeyPairExists raised: %s'), unicode(ex),
context=context)
return self._error(req, context, type(ex).__name__, unicode(ex))
+ except exception.InvalidParameterValue as ex:
+ LOG.debug(_('InvalidParameterValue raised: %s'), unicode(ex),
+ context=context)
+ return self._error(req, context, type(ex).__name__, unicode(ex))
except Exception as ex:
extra = {'environment': req.environ}
LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py
index 7d78c5cfa..9a3e55925 100644
--- a/nova/api/ec2/apirequest.py
+++ b/nova/api/ec2/apirequest.py
@@ -104,7 +104,7 @@ class APIRequest(object):
for key in data.keys():
val = data[key]
el.appendChild(self._render_data(xml, key, val))
- except:
+ except Exception:
LOG.debug(data)
raise
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 0294c09c5..87bba58c3 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -25,11 +25,13 @@ datastore.
import base64
import netaddr
import os
-import urllib
+import re
+import shutil
import tempfile
import time
-import shutil
+import urllib
+from nova import block_device
from nova import compute
from nova import context
@@ -78,6 +80,10 @@ def _gen_key(context, user_id, key_name):
# TODO(yamahata): hypervisor dependent default device name
_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1'
+_DEFAULT_MAPPINGS = {'ami': 'sda1',
+ 'ephemeral0': 'sda2',
+ 'root': _DEFAULT_ROOT_DEVICE_NAME,
+ 'swap': 'sda3'}
def _parse_block_device_mapping(bdm):
@@ -105,7 +111,7 @@ def _parse_block_device_mapping(bdm):
def _properties_get_mappings(properties):
- return ec2utils.mappings_prepend_dev(properties.get('mappings', []))
+ return block_device.mappings_prepend_dev(properties.get('mappings', []))
def _format_block_device_mapping(bdm):
@@ -144,8 +150,7 @@ def _format_mappings(properties, result):
"""Format multiple BlockDeviceMappingItemType"""
mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']}
for m in _properties_get_mappings(properties)
- if (m['virtual'] == 'swap' or
- m['virtual'].startswith('ephemeral'))]
+ if block_device.is_swap_or_ephemeral(m['virtual'])]
block_device_mapping = [_format_block_device_mapping(bdm) for bdm in
properties.get('block_device_mapping', [])]
@@ -208,8 +213,9 @@ class CloudController(object):
def _get_mpi_data(self, context, project_id):
result = {}
+ search_opts = {'project_id': project_id}
for instance in self.compute_api.get_all(context,
- project_id=project_id):
+ search_opts=search_opts):
if instance['fixed_ips']:
line = '%s slots=%d' % (instance['fixed_ips'][0]['address'],
instance['vcpus'])
@@ -233,10 +239,39 @@ class CloudController(object):
state = 'available'
return image['properties'].get('image_state', state)
+ def _format_instance_mapping(self, ctxt, instance_ref):
+ root_device_name = instance_ref['root_device_name']
+ if root_device_name is None:
+ return _DEFAULT_MAPPINGS
+
+ mappings = {}
+ mappings['ami'] = block_device.strip_dev(root_device_name)
+ mappings['root'] = root_device_name
+
+ # 'ephemeralN' and 'swap'
+ for bdm in db.block_device_mapping_get_all_by_instance(
+ ctxt, instance_ref['id']):
+ if (bdm['volume_id'] or bdm['snapshot_id'] or bdm['no_device']):
+ continue
+
+ virtual_name = bdm['virtual_name']
+ if not virtual_name:
+ continue
+
+ if block_device.is_swap_or_ephemeral(virtual_name):
+ mappings[virtual_name] = bdm['device_name']
+
+ return mappings
+
def get_metadata(self, address):
ctxt = context.get_admin_context()
- instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)
- if instance_ref is None:
+ search_opts = {'fixed_ip': address}
+ try:
+ instance_ref = self.compute_api.get_all(ctxt,
+ search_opts=search_opts)
+ except exception.NotFound:
+ instance_ref = None
+ if not instance_ref:
return None
# This ensures that all attributes of the instance
@@ -259,18 +294,14 @@ class CloudController(object):
security_groups = db.security_group_get_by_instance(ctxt,
instance_ref['id'])
security_groups = [x['name'] for x in security_groups]
+ mappings = self._format_instance_mapping(ctxt, instance_ref)
data = {
- 'user-data': base64.b64decode(instance_ref['user_data']),
+ 'user-data': self._format_user_data(instance_ref),
'meta-data': {
'ami-id': image_ec2_id,
'ami-launch-index': instance_ref['launch_index'],
'ami-manifest-path': 'FIXME',
- 'block-device-mapping': {
- # TODO(vish): replace with real data
- 'ami': 'sda1',
- 'ephemeral0': 'sda2',
- 'root': _DEFAULT_ROOT_DEVICE_NAME,
- 'swap': 'sda3'},
+ 'block-device-mapping': mappings,
'hostname': hostname,
'instance-action': 'none',
'instance-id': ec2_id,
@@ -765,6 +796,22 @@ class CloudController(object):
return source_project_id
def create_security_group(self, context, group_name, group_description):
+ if not re.match('^[a-zA-Z0-9_\- ]+$', str(group_name)):
+ # Some validation to ensure that values match API spec.
+ # - Alphanumeric characters, spaces, dashes, and underscores.
+ # TODO(Daviey): LP: #813685 extend beyond group_name checking, and
+ # probably create a param validator that can be used elsewhere.
+ err = _("Value (%s) for parameter GroupName is invalid."
+ " Content limited to Alphanumeric characters, "
+ "spaces, dashes, and underscores.") % group_name
+ # err not that of master ec2 implementation, as they fail to raise.
+ raise exception.InvalidParameterValue(err=err)
+
+ if len(str(group_name)) > 255:
+ err = _("Value (%s) for parameter GroupName is invalid."
+ " Length exceeds maximum of 255.") % group_name
+ raise exception.InvalidParameterValue(err=err)
+
LOG.audit(_("Create Security Group %s"), group_name, context=context)
self.compute_api.ensure_default_security_group(context)
if db.security_group_exists(context, context.project_id, group_name):
@@ -948,19 +995,113 @@ class CloudController(object):
'status': volume['attach_status'],
'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
- def _convert_to_set(self, lst, label):
+ @staticmethod
+ def _convert_to_set(lst, label):
if lst is None or lst == []:
return None
if not isinstance(lst, list):
lst = [lst]
return [{label: x} for x in lst]
+ def _format_kernel_id(self, instance_ref, result, key):
+ kernel_id = instance_ref['kernel_id']
+ if kernel_id is None:
+ return
+ result[key] = self.image_ec2_id(instance_ref['kernel_id'], 'aki')
+
+ def _format_ramdisk_id(self, instance_ref, result, key):
+ ramdisk_id = instance_ref['ramdisk_id']
+ if ramdisk_id is None:
+ return
+ result[key] = self.image_ec2_id(instance_ref['ramdisk_id'], 'ari')
+
+ @staticmethod
+ def _format_user_data(instance_ref):
+ return base64.b64decode(instance_ref['user_data'])
+
+ def describe_instance_attribute(self, context, instance_id, attribute,
+ **kwargs):
+ def _unsupported_attribute(instance, result):
+ raise exception.ApiError(_('attribute not supported: %s') %
+ attribute)
+
+ def _format_attr_block_device_mapping(instance, result):
+ tmp = {}
+ self._format_instance_root_device_name(instance, tmp)
+ self._format_instance_bdm(context, instance_id,
+ tmp['rootDeviceName'], result)
+
+ def _format_attr_disable_api_termination(instance, result):
+ _unsupported_attribute(instance, result)
+
+ def _format_attr_group_set(instance, result):
+ CloudController._format_group_set(instance, result)
+
+ def _format_attr_instance_initiated_shutdown_behavior(instance,
+ result):
+ state_description = instance['state_description']
+ state_to_value = {'stopping': 'stop',
+ 'stopped': 'stop',
+ 'terminating': 'terminate'}
+ value = state_to_value.get(state_description)
+ if value:
+ result['instanceInitiatedShutdownBehavior'] = value
+
+ def _format_attr_instance_type(instance, result):
+ self._format_instance_type(instance, result)
+
+ def _format_attr_kernel(instance, result):
+ self._format_kernel_id(instance, result, 'kernel')
+
+ def _format_attr_ramdisk(instance, result):
+ self._format_ramdisk_id(instance, result, 'ramdisk')
+
+ def _format_attr_root_device_name(instance, result):
+ self._format_instance_root_device_name(instance, result)
+
+ def _format_attr_source_dest_check(instance, result):
+ _unsupported_attribute(instance, result)
+
+ def _format_attr_user_data(instance, result):
+ result['userData'] = self._format_user_data(instance)
+
+ attribute_formatter = {
+ 'blockDeviceMapping': _format_attr_block_device_mapping,
+ 'disableApiTermination': _format_attr_disable_api_termination,
+ 'groupSet': _format_attr_group_set,
+ 'instanceInitiatedShutdownBehavior':
+ _format_attr_instance_initiated_shutdown_behavior,
+ 'instanceType': _format_attr_instance_type,
+ 'kernel': _format_attr_kernel,
+ 'ramdisk': _format_attr_ramdisk,
+ 'rootDeviceName': _format_attr_root_device_name,
+ 'sourceDestCheck': _format_attr_source_dest_check,
+ 'userData': _format_attr_user_data,
+ }
+
+ fn = attribute_formatter.get(attribute)
+ if fn is None:
+ raise exception.ApiError(
+ _('attribute not supported: %s') % attribute)
+
+ ec2_instance_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
+ instance = self.compute_api.get(context, instance_id)
+ result = {'instance_id': ec2_instance_id}
+ fn(instance, result)
+ return result
+
def describe_instances(self, context, **kwargs):
- return self._format_describe_instances(context, **kwargs)
+ # Optional DescribeInstances argument
+ instance_id = kwargs.get('instance_id', None)
+ return self._format_describe_instances(context,
+ instance_id=instance_id)
def describe_instances_v6(self, context, **kwargs):
- kwargs['use_v6'] = True
- return self._format_describe_instances(context, **kwargs)
+ # Optional DescribeInstancesV6 argument
+ instance_id = kwargs.get('instance_id', None)
+ return self._format_describe_instances(context,
+ instance_id=instance_id, use_v6=True)
def _format_describe_instances(self, context, **kwargs):
return {'reservationSet': self._format_instances(context, **kwargs)}
@@ -1001,7 +1142,29 @@ class CloudController(object):
result['blockDeviceMapping'] = mapping
result['rootDeviceType'] = root_device_type
- def _format_instances(self, context, instance_id=None, **kwargs):
+ @staticmethod
+ def _format_instance_root_device_name(instance, result):
+ result['rootDeviceName'] = (instance.get('root_device_name') or
+ _DEFAULT_ROOT_DEVICE_NAME)
+
+ @staticmethod
+ def _format_instance_type(instance, result):
+ if instance['instance_type']:
+ result['instanceType'] = instance['instance_type'].get('name')
+ else:
+ result['instanceType'] = None
+
+ @staticmethod
+ def _format_group_set(instance, result):
+ security_group_names = []
+ if instance.get('security_groups'):
+ for security_group in instance['security_groups']:
+ security_group_names.append(security_group['name'])
+ result['groupSet'] = CloudController._convert_to_set(
+ security_group_names, 'groupId')
+
+ def _format_instances(self, context, instance_id=None, use_v6=False,
+ **search_opts):
# TODO(termie): this method is poorly named as its name does not imply
# that it will be making a variety of database calls
# rather than simply formatting a bunch of instances that
@@ -1012,11 +1175,17 @@ class CloudController(object):
instances = []
for ec2_id in instance_id:
internal_id = ec2utils.ec2_id_to_id(ec2_id)
- instance = self.compute_api.get(context,
- instance_id=internal_id)
+ try:
+ instance = self.compute_api.get(context, internal_id)
+ except exception.NotFound:
+ continue
instances.append(instance)
else:
- instances = self.compute_api.get_all(context, **kwargs)
+ try:
+ instances = self.compute_api.get_all(context,
+ search_opts=search_opts)
+ except exception.NotFound:
+ instances = []
for instance in instances:
if not context.is_admin:
if instance['image_ref'] == str(FLAGS.vpn_image_id):
@@ -1026,6 +1195,8 @@ class CloudController(object):
ec2_id = ec2utils.id_to_ec2_id(instance_id)
i['instanceId'] = ec2_id
i['imageId'] = self.image_ec2_id(instance['image_ref'])
+ self._format_kernel_id(instance, i, 'kernelId')
+ self._format_ramdisk_id(instance, i, 'ramdiskId')
i['instanceState'] = {
'code': instance['state'],
'name': instance['state_description']}
@@ -1036,7 +1207,7 @@ class CloudController(object):
fixed_addr = fixed['address']
if fixed['floating_ips']:
floating_addr = fixed['floating_ips'][0]['address']
- if fixed['network'] and 'use_v6' in kwargs:
+ if fixed['network'] and use_v6:
i['dnsNameV6'] = ipv6.to_global(
fixed['network']['cidr_v6'],
fixed['virtual_interface']['address'],
@@ -1054,16 +1225,12 @@ class CloudController(object):
instance['project_id'],
instance['host'])
i['productCodesSet'] = self._convert_to_set([], 'product_codes')
- if instance['instance_type']:
- i['instanceType'] = instance['instance_type'].get('name')
- else:
- i['instanceType'] = None
+ self._format_instance_type(instance, i)
i['launchTime'] = instance['created_at']
i['amiLaunchIndex'] = instance['launch_index']
i['displayName'] = instance['display_name']
i['displayDescription'] = instance['display_description']
- i['rootDeviceName'] = (instance.get('root_device_name') or
- _DEFAULT_ROOT_DEVICE_NAME)
+ self._format_instance_root_device_name(instance, i)
self._format_instance_bdm(context, instance_id,
i['rootDeviceName'], i)
host = instance['host']
@@ -1073,12 +1240,7 @@ class CloudController(object):
r = {}
r['reservationId'] = instance['reservation_id']
r['ownerId'] = instance['project_id']
- security_group_names = []
- if instance.get('security_groups'):
- for security_group in instance['security_groups']:
- security_group_names.append(security_group['name'])
- r['groupSet'] = self._convert_to_set(security_group_names,
- 'groupId')
+ self._format_group_set(instance, r)
r['instancesSet'] = []
reservations[instance['reservation_id']] = r
reservations[instance['reservation_id']]['instancesSet'].append(i)
@@ -1182,7 +1344,7 @@ class CloudController(object):
'AvailabilityZone'),
block_device_mapping=kwargs.get('block_device_mapping', {}))
return self._format_run_instances(context,
- instances[0]['reservation_id'])
+ reservation_id=instances[0]['reservation_id'])
def _do_instance(self, action, context, ec2_id):
instance_id = ec2utils.ec2_id_to_id(ec2_id)
@@ -1314,7 +1476,7 @@ class CloudController(object):
i['architecture'] = image['properties'].get('architecture')
properties = image['properties']
- root_device_name = ec2utils.properties_root_device_name(properties)
+ root_device_name = block_device.properties_root_device_name(properties)
root_device_type = 'instance-store'
for bdm in properties.get('block_device_mapping', []):
if (bdm.get('device_name') == root_device_name and
@@ -1387,7 +1549,7 @@ class CloudController(object):
def _root_device_name_attribute(image, result):
result['rootDeviceName'] = \
- ec2utils.properties_root_device_name(image['properties'])
+ block_device.properties_root_device_name(image['properties'])
if result['rootDeviceName'] is None:
result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME
@@ -1520,8 +1682,7 @@ class CloudController(object):
if virtual_name in ('ami', 'root'):
continue
- assert (virtual_name == 'swap' or
- virtual_name.startswith('ephemeral'))
+ assert block_device.is_swap_or_ephemeral(virtual_name)
device_name = m['device']
if device_name in [b['device_name'] for b in mapping
if not b.get('no_device', False)]:
diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py
index bae1e0ee5..bcdf2ba78 100644
--- a/nova/api/ec2/ec2utils.py
+++ b/nova/api/ec2/ec2utils.py
@@ -135,32 +135,3 @@ def dict_from_dotted_str(items):
args[key] = value
return args
-
-
-def properties_root_device_name(properties):
- """get root device name from image meta data.
- If it isn't specified, return None.
- """
- root_device_name = None
-
- # NOTE(yamahata): see image_service.s3.s3create()
- for bdm in properties.get('mappings', []):
- if bdm['virtual'] == 'root':
- root_device_name = bdm['device']
-
- # NOTE(yamahata): register_image's command line can override
- # <machine>.manifest.xml
- if 'root_device_name' in properties:
- root_device_name = properties['root_device_name']
-
- return root_device_name
-
-
-def mappings_prepend_dev(mappings):
- """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type"""
- for m in mappings:
- virtual = m['virtual']
- if ((virtual == 'swap' or virtual.startswith('ephemeral')) and
- (not m['device'].startswith('/'))):
- m['device'] = '/dev/' + m['device']
- return mappings
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 0af39c2df..e0c1e9d04 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -40,6 +40,7 @@ from nova.api.openstack import servers
from nova.api.openstack import server_metadata
from nova.api.openstack import shared_ip_groups
from nova.api.openstack import users
+from nova.api.openstack import versions
from nova.api.openstack import wsgi
from nova.api.openstack import zones
@@ -49,6 +50,9 @@ FLAGS = flags.FLAGS
flags.DEFINE_bool('allow_admin_api',
False,
'When True, this API service will accept admin operations.')
+flags.DEFINE_bool('allow_instance_snapshots',
+ True,
+ 'When True, this API service will permit instance snapshot operations.')
class FaultWrapper(base_wsgi.Middleware):
@@ -99,6 +103,7 @@ class APIRouter(base_wsgi.Router):
server_members['suspend'] = 'POST'
server_members['resume'] = 'POST'
server_members['rescue'] = 'POST'
+ server_members['migrate'] = 'POST'
server_members['unrescue'] = 'POST'
server_members['reset_network'] = 'POST'
server_members['inject_network_info'] = 'POST'
@@ -118,6 +123,10 @@ class APIRouter(base_wsgi.Router):
'select': 'POST',
'boot': 'POST'})
+ mapper.connect("versions", "/",
+ controller=versions.create_resource(version),
+ action='show')
+
mapper.resource("console", "consoles",
controller=consoles.create_resource(),
parent_resource=dict(member_name='server',
@@ -167,7 +176,9 @@ class APIRouterV11(APIRouter):
def _setup_routes(self, mapper):
self._setup_base_routes(mapper, '1.1')
+
image_metadata_controller = image_metadata.create_resource()
+
mapper.resource("image_meta", "metadata",
controller=image_metadata_controller,
parent_resource=dict(member_name='image',
@@ -178,7 +189,14 @@ class APIRouterV11(APIRouter):
action='update_all',
conditions={"method": ['PUT']})
- mapper.resource("server_meta", "meta",
- controller=server_metadata.create_resource(),
+ server_metadata_controller = server_metadata.create_resource()
+
+ mapper.resource("server_meta", "metadata",
+ controller=server_metadata_controller,
parent_resource=dict(member_name='server',
collection_name='servers'))
+
+ mapper.connect("metadata", "/servers/{server_id}/metadata",
+ controller=server_metadata_controller,
+ action='update_all',
+ conditions={"method": ['PUT']})
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index efa4ab385..dfdd62201 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -15,8 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import functools
import re
-from urlparse import urlparse
+import urlparse
from xml.dom import minidom
import webob
@@ -24,7 +25,9 @@ import webob
from nova import exception
from nova import flags
from nova import log as logging
+from nova import quota
from nova.api.openstack import wsgi
+from nova.compute import power_state as compute_power_state
LOG = logging.getLogger('nova.api.openstack.common')
@@ -35,6 +38,38 @@ XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
+_STATUS_MAP = {
+ None: 'BUILD',
+ compute_power_state.NOSTATE: 'BUILD',
+ compute_power_state.RUNNING: 'ACTIVE',
+ compute_power_state.BLOCKED: 'ACTIVE',
+ compute_power_state.SUSPENDED: 'SUSPENDED',
+ compute_power_state.PAUSED: 'PAUSED',
+ compute_power_state.SHUTDOWN: 'SHUTDOWN',
+ compute_power_state.SHUTOFF: 'SHUTOFF',
+ compute_power_state.CRASHED: 'ERROR',
+ compute_power_state.FAILED: 'ERROR',
+ compute_power_state.BUILDING: 'BUILD',
+}
+
+
+def status_from_power_state(power_state):
+ """Map the power state to the server status string"""
+ return _STATUS_MAP[power_state]
+
+
+def power_states_from_status(status):
+ """Map the server status string to a list of power states"""
+ power_states = []
+ for power_state, status_map in _STATUS_MAP.iteritems():
+ # Skip the 'None' state
+ if power_state is None:
+ continue
+ if status.lower() == status_map.lower():
+ power_states.append(power_state)
+ return power_states
+
+
def get_pagination_params(request):
"""Return marker, limit tuple from request.
@@ -137,8 +172,8 @@ def get_id_from_href(href):
if re.match(r'\d+$', str(href)):
return int(href)
try:
- return int(urlparse(href).path.split('/')[-1])
- except:
+ return int(urlparse.urlsplit(href).path.split('/')[-1])
+ except ValueError, e:
LOG.debug(_("Error extracting id from href: %s") % href)
raise ValueError(_('could not parse id from href'))
@@ -153,22 +188,18 @@ def remove_version_from_href(href):
Returns: 'http://www.nova.com'
"""
- try:
- #removes the first instance that matches /v#.#/
- new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1)
+ parsed_url = urlparse.urlsplit(href)
+ new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path,
+ count=1)
- #if no version was found, try finding /v#.# at the end of the string
- if new_href == href:
- new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1)
- except:
- LOG.debug(_("Error removing version from href: %s") % href)
- msg = _('could not parse version from href')
+ if new_path == parsed_url.path:
+ msg = _('href %s does not contain version') % href
+ LOG.debug(msg)
raise ValueError(msg)
- if new_href == href:
- msg = _('href does not contain version')
- raise ValueError(msg)
- return new_href
+ parsed_url = list(parsed_url)
+ parsed_url[2] = new_path
+ return urlparse.urlunsplit(parsed_url)
def get_version_from_href(href):
@@ -196,7 +227,27 @@ def get_version_from_href(href):
return version
-class MetadataXMLDeserializer(wsgi.MetadataXMLDeserializer):
+def check_img_metadata_quota_limit(context, metadata):
+ if metadata is None:
+ return
+ num_metadata = len(metadata)
+ quota_metadata = quota.allowed_metadata_items(context, num_metadata)
+ if quota_metadata < num_metadata:
+ expl = _("Image metadata limit exceeded")
+ raise webob.exc.HTTPBadRequest(explanation=expl)
+
+
+class MetadataXMLDeserializer(wsgi.XMLDeserializer):
+
+ def extract_metadata(self, metadata_node):
+ """Marshal the metadata attribute of a parsed request"""
+ if metadata_node is None:
+ return {}
+ metadata = {}
+ for meta_node in self.find_children_named(metadata_node, "meta"):
+ key = meta_node.getAttribute("key")
+ metadata[key] = self.extract_text(meta_node)
+ return metadata
def _extract_metadata_container(self, datastring):
dom = minidom.parseString(datastring)
@@ -247,7 +298,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
container_node = self.meta_list_to_xml(xml_doc, items)
xml_doc.appendChild(container_node)
self._add_xmlns(container_node)
- return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
+ return xml_doc.toxml('UTF-8')
def index(self, metadata_dict):
return self._meta_list_to_xml_string(metadata_dict)
@@ -264,7 +315,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
item_node = self._meta_item_to_xml(xml_doc, item_key, item_value)
xml_doc.appendChild(item_node)
self._add_xmlns(item_node)
- return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
+ return xml_doc.toxml('UTF-8')
def show(self, meta_item_dict):
return self._meta_item_to_xml_string(meta_item_dict['meta'])
@@ -274,3 +325,15 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer):
def default(self, *args, **kwargs):
return ''
+
+
+def check_snapshots_enabled(f):
+ @functools.wraps(f)
+ def inner(*args, **kwargs):
+ if not FLAGS.allow_instance_snapshots:
+ LOG.warn(_('Rejecting snapshot request, snapshots currently'
+ ' disabled'))
+ msg = _("Instance snapshots are not permitted at this time.")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return f(*args, **kwargs)
+ return inner
diff --git a/nova/api/openstack/contrib/admin_only.py b/nova/api/openstack/contrib/admin_only.py
new file mode 100644
index 000000000..e821c9e1f
--- /dev/null
+++ b/nova/api/openstack/contrib/admin_only.py
@@ -0,0 +1,30 @@
+# Copyright (c) 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.
+
+"""Decorator for limiting extensions that should be admin-only."""
+
+from functools import wraps
+from nova import flags
+FLAGS = flags.FLAGS
+
+
+def admin_only(fnc):
+ @wraps(fnc)
+ def _wrapped(self, *args, **kwargs):
+ if FLAGS.allow_admin_api:
+ return fnc(self, *args, **kwargs)
+ return []
+ _wrapped.func_name = fnc.func_name
+ return _wrapped
diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py
index 3d8049324..52c9c6cf9 100644
--- a/nova/api/openstack/contrib/floating_ips.py
+++ b/nova/api/openstack/contrib/floating_ips.py
@@ -18,12 +18,16 @@
from webob import exc
from nova import exception
+from nova import log as logging
from nova import network
from nova import rpc
from nova.api.openstack import faults
from nova.api.openstack import extensions
+LOG = logging.getLogger('nova.api.openstack.contrib.floating_ips')
+
+
def _translate_floating_ip_view(floating_ip):
result = {'id': floating_ip['id'],
'ip': floating_ip['address']}
@@ -97,8 +101,14 @@ class FloatingIPController(object):
def delete(self, req, id):
context = req.environ['nova.context']
-
ip = self.network_api.get_floating_ip(context, id)
+
+ if 'fixed_ip' in ip:
+ try:
+ self.disassociate(req, id, '')
+ except Exception as e:
+ LOG.exception(_("Error disassociating fixed_ip %s"), e)
+
self.network_api.release_floating_ip(context, address=ip)
return {'released': {
diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py
index 55e57e1a4..ecaa365b7 100644
--- a/nova/api/openstack/contrib/hosts.py
+++ b/nova/api/openstack/contrib/hosts.py
@@ -24,6 +24,7 @@ from nova import log as logging
from nova.api.openstack import common
from nova.api.openstack import extensions
from nova.api.openstack import faults
+from nova.api.openstack.contrib import admin_only
from nova.scheduler import api as scheduler_api
@@ -70,7 +71,7 @@ class HostController(object):
key = raw_key.lower().strip()
val = raw_val.lower().strip()
# NOTE: (dabo) Right now only 'status' can be set, but other
- # actions may follow.
+ # settings may follow.
if key == "status":
if val[:6] in ("enable", "disabl"):
return self._set_enabled_status(req, id,
@@ -89,8 +90,30 @@ class HostController(object):
LOG.audit(_("Setting host %(host)s to %(state)s.") % locals())
result = self.compute_api.set_host_enabled(context, host=host,
enabled=enabled)
+ if result not in ("enabled", "disabled"):
+ # An error message was returned
+ raise webob.exc.HTTPBadRequest(explanation=result)
return {"host": host, "status": result}
+ def _host_power_action(self, req, host, action):
+ """Reboots, shuts down or powers up the host."""
+ context = req.environ['nova.context']
+ try:
+ result = self.compute_api.host_power_action(context, host=host,
+ action=action)
+ except NotImplementedError as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.msg)
+ return {"host": host, "power_action": result}
+
+ def startup(self, req, id):
+ return self._host_power_action(req, host=id, action="startup")
+
+ def shutdown(self, req, id):
+ return self._host_power_action(req, host=id, action="shutdown")
+
+ def reboot(self, req, id):
+ return self._host_power_action(req, host=id, action="reboot")
+
class Hosts(extensions.ExtensionDescriptor):
def get_name(self):
@@ -108,7 +131,10 @@ class Hosts(extensions.ExtensionDescriptor):
def get_updated(self):
return "2011-06-29T00:00:00+00:00"
+ @admin_only.admin_only
def get_resources(self):
- resources = [extensions.ResourceExtension('os-hosts', HostController(),
- collection_actions={'update': 'PUT'}, member_actions={})]
+ resources = [extensions.ResourceExtension('os-hosts',
+ HostController(), collection_actions={'update': 'PUT'},
+ member_actions={"startup": "GET", "shutdown": "GET",
+ "reboot": "GET"})]
return resources
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index e4b897cd6..1425521a9 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -27,6 +27,7 @@ from nova import quota
from nova import utils
from nova.compute import instance_types
+from nova.api.openstack import common
from nova.api.openstack import wsgi
@@ -88,6 +89,11 @@ class CreateInstanceHelper(object):
key_data = key_pair['public_key']
image_href = self.controller._image_ref_from_req_data(body)
+ # If the image href was generated by nova api, strip image_href
+ # down to an id and use the default glance connection params
+
+ if str(image_href).startswith(req.application_url):
+ image_href = image_href.split('/').pop()
try:
image_service, image_id = nova.image.get_image_service(image_href)
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
@@ -286,7 +292,7 @@ class CreateInstanceHelper(object):
return password
-class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
+class ServerXMLDeserializer(wsgi.XMLDeserializer):
"""
Deserializer to handle xml-formatted server create requests.
@@ -294,6 +300,56 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
and personality attributes
"""
+ metadata_deserializer = common.MetadataXMLDeserializer()
+
+ def create(self, string):
+ """Deserialize an xml-formatted server create request"""
+ dom = minidom.parseString(string)
+ server = self._extract_server(dom)
+ return {'body': {'server': server}}
+
+ def _extract_server(self, node):
+ """Marshal the server attribute of a parsed request"""
+ server = {}
+ server_node = self.find_first_child_named(node, 'server')
+
+ attributes = ["name", "imageId", "flavorId", "adminPass"]
+ for attr in attributes:
+ if server_node.getAttribute(attr):
+ server[attr] = server_node.getAttribute(attr)
+
+ metadata_node = self.find_first_child_named(server_node, "metadata")
+ server["metadata"] = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+
+ server["personality"] = self._extract_personality(server_node)
+
+ return server
+
+ def _extract_personality(self, server_node):
+ """Marshal the personality attribute of a parsed request"""
+ node = self.find_first_child_named(server_node, "personality")
+ personality = []
+ if node is not None:
+ for file_node in self.find_children_named(node, "file"):
+ item = {}
+ if file_node.hasAttribute("path"):
+ item["path"] = file_node.getAttribute("path")
+ item["contents"] = self.extract_text(file_node)
+ personality.append(item)
+ return personality
+
+
+class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
+ """
+ Deserializer to handle xml-formatted server create requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ metadata_deserializer = common.MetadataXMLDeserializer()
+
def action(self, string):
dom = minidom.parseString(string)
action_node = dom.childNodes[0]
@@ -302,6 +358,12 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
action_deserializer = {
'createImage': self._action_create_image,
'createBackup': self._action_create_backup,
+ 'changePassword': self._action_change_password,
+ 'reboot': self._action_reboot,
+ 'rebuild': self._action_rebuild,
+ 'resize': self._action_resize,
+ 'confirmResize': self._action_confirm_resize,
+ 'revertResize': self._action_revert_resize,
}.get(action_name, self.default)
action_data = action_deserializer(action_node)
@@ -315,6 +377,46 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
attributes = ('name', 'backup_type', 'rotation')
return self._deserialize_image_action(node, attributes)
+ def _action_change_password(self, node):
+ if not node.hasAttribute("adminPass"):
+ raise AttributeError("No adminPass was specified in request")
+ return {"adminPass": node.getAttribute("adminPass")}
+
+ def _action_reboot(self, node):
+ if not node.hasAttribute("type"):
+ raise AttributeError("No reboot type was specified in request")
+ return {"type": node.getAttribute("type")}
+
+ def _action_rebuild(self, node):
+ rebuild = {}
+ if node.hasAttribute("name"):
+ rebuild['name'] = node.getAttribute("name")
+
+ metadata_node = self.find_first_child_named(node, "metadata")
+ if metadata_node is not None:
+ rebuild["metadata"] = self.extract_metadata(metadata_node)
+
+ personality = self._extract_personality(node)
+ if personality is not None:
+ rebuild["personality"] = personality
+
+ if not node.hasAttribute("imageRef"):
+ raise AttributeError("No imageRef was specified in request")
+ rebuild["imageRef"] = node.getAttribute("imageRef")
+
+ return rebuild
+
+ def _action_resize(self, node):
+ if not node.hasAttribute("flavorRef"):
+ raise AttributeError("No flavorRef was specified in request")
+ return {"flavorRef": node.getAttribute("flavorRef")}
+
+ def _action_confirm_resize(self, node):
+ return None
+
+ def _action_revert_resize(self, node):
+ return None
+
def _deserialize_image_action(self, node, allowed_attributes):
data = {}
for attribute in allowed_attributes:
@@ -322,7 +424,10 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
if value:
data[attribute] = value
metadata_node = self.find_first_child_named(node, 'metadata')
- data['metadata'] = self.extract_metadata(metadata_node)
+ if metadata_node is not None:
+ metadata = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+ data['metadata'] = metadata
return data
def create(self, string):
@@ -336,28 +441,32 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
server = {}
server_node = self.find_first_child_named(node, 'server')
- attributes = ["name", "imageId", "flavorId", "imageRef",
- "flavorRef", "adminPass"]
+ attributes = ["name", "imageRef", "flavorRef", "adminPass"]
for attr in attributes:
if server_node.getAttribute(attr):
server[attr] = server_node.getAttribute(attr)
metadata_node = self.find_first_child_named(server_node, "metadata")
- server["metadata"] = self.extract_metadata(metadata_node)
+ if metadata_node is not None:
+ server["metadata"] = self.extract_metadata(metadata_node)
- server["personality"] = self._extract_personality(server_node)
+ personality = self._extract_personality(server_node)
+ if personality is not None:
+ server["personality"] = personality
return server
def _extract_personality(self, server_node):
"""Marshal the personality attribute of a parsed request"""
node = self.find_first_child_named(server_node, "personality")
- personality = []
if node is not None:
+ personality = []
for file_node in self.find_children_named(node, "file"):
item = {}
if file_node.hasAttribute("path"):
item["path"] = file_node.getAttribute("path")
item["contents"] = self.extract_text(file_node)
personality.append(item)
- return personality
+ return personality
+ else:
+ return None
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index b070dd61a..b1538950f 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -23,7 +23,7 @@ import sys
import routes
import webob.dec
import webob.exc
-from xml.etree import ElementTree
+from lxml import etree
from nova import exception
from nova import flags
@@ -32,6 +32,7 @@ from nova import wsgi as base_wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
LOG = logging.getLogger('extensions')
@@ -474,36 +475,38 @@ class ResourceExtension(object):
class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
+ NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
def show(self, ext_dict):
- ext = self._create_ext_elem(ext_dict['extension'])
+ ext = etree.Element('extension', nsmap=self.NSMAP)
+ self._populate_ext(ext, ext_dict['extension'])
return self._to_xml(ext)
def index(self, exts_dict):
- exts = ElementTree.Element('extensions')
+ exts = etree.Element('extensions', nsmap=self.NSMAP)
for ext_dict in exts_dict['extensions']:
- exts.append(self._create_ext_elem(ext_dict))
+ ext = etree.SubElement(exts, 'extension')
+ self._populate_ext(ext, ext_dict)
return self._to_xml(exts)
- def _create_ext_elem(self, ext_dict):
- """Create an extension xml element from a dict."""
- ext_elem = ElementTree.Element('extension')
+ def _populate_ext(self, ext_elem, ext_dict):
+ """Populate an extension xml element from a dict."""
+
ext_elem.set('name', ext_dict['name'])
ext_elem.set('namespace', ext_dict['namespace'])
ext_elem.set('alias', ext_dict['alias'])
ext_elem.set('updated', ext_dict['updated'])
- desc = ElementTree.Element('description')
+ desc = etree.Element('description')
desc.text = ext_dict['description']
ext_elem.append(desc)
for link in ext_dict.get('links', []):
- elem = ElementTree.Element('atom:link')
+ elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM)
elem.set('rel', link['rel'])
elem.set('href', link['href'])
elem.set('type', link['type'])
- ext_elem.append(elem)
return ext_elem
def _to_xml(self, root):
- """Convert the xml tree object to an xml string."""
- root.set('xmlns', wsgi.XMLNS_V11)
- root.set('xmlns:atom', wsgi.XMLNS_ATOM)
- return ElementTree.tostring(root, encoding='UTF-8')
+ """Convert the xml object to an xml string."""
+
+ return etree.tostring(root, encoding='UTF-8')
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index aaf64a123..4d615ea96 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -19,7 +19,6 @@ from webob import exc
from nova import flags
from nova import image
-from nova import quota
from nova import utils
from nova.api.openstack import common
from nova.api.openstack import wsgi
@@ -40,15 +39,6 @@ class Controller(object):
metadata = image.get('properties', {})
return metadata
- def _check_quota_limit(self, context, metadata):
- if metadata is None:
- return
- num_metadata = len(metadata)
- quota_metadata = quota.allowed_metadata_items(context, num_metadata)
- if quota_metadata < num_metadata:
- expl = _("Image metadata limit exceeded")
- raise exc.HTTPBadRequest(explanation=expl)
-
def index(self, req, image_id):
"""Returns the list of metadata for a given instance"""
context = req.environ['nova.context']
@@ -70,7 +60,7 @@ class Controller(object):
if 'metadata' in body:
for key, value in body['metadata'].iteritems():
metadata[key] = value
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
@@ -93,7 +83,7 @@ class Controller(object):
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
metadata[id] = meta[id]
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(meta=meta)
@@ -102,7 +92,7 @@ class Controller(object):
context = req.environ['nova.context']
img = self.image_service.show(context, image_id)
metadata = body.get('metadata', {})
- self._check_quota_limit(context, metadata)
+ common.check_img_metadata_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 0834adfa5..0aabb9e56 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -106,6 +106,7 @@ class Controller(object):
class ControllerV10(Controller):
"""Version 1.0 specific controller logic."""
+ @common.check_snapshots_enabled
def create(self, req, body):
"""Snapshot a server instance and save the image."""
try:
@@ -116,13 +117,17 @@ class ControllerV10(Controller):
try:
image_name = image["name"]
- server_id = image["serverId"]
+ instance_id = image["serverId"]
except KeyError as missing_key:
msg = _("Image entity requires %s") % missing_key
raise webob.exc.HTTPBadRequest(explanation=msg)
context = req.environ["nova.context"]
- image = self._compute_service.snapshot(context, server_id, image_name)
+ props = {'instance_id': instance_id}
+ image = self._compute_service.snapshot(context,
+ instance_id,
+ image_name,
+ extra_properties=props)
return dict(image=self.get_builder(req).build(image, detail=True))
@@ -139,7 +144,7 @@ class ControllerV10(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- images = self._image_service.index(context, filters)
+ images = self._image_service.index(context, filters=filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images])
@@ -152,7 +157,7 @@ class ControllerV10(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- images = self._image_service.detail(context, filters)
+ images = self._image_service.detail(context, filters=filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
diff --git a/nova/api/openstack/schemas/atom-link.rng b/nova/api/openstack/schemas/atom-link.rng
new file mode 100644
index 000000000..edba5eee6
--- /dev/null
+++ b/nova/api/openstack/schemas/atom-link.rng
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ -*- rnc -*-
+ RELAX NG Compact Syntax Grammar for the
+ Atom Format Specification Version 11
+-->
+<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+ <start>
+ <choice>
+ <ref name="atomLink"/>
+ </choice>
+ </start>
+ <!-- Common attributes -->
+ <define name="atomCommonAttributes">
+ <optional>
+ <attribute name="xml:base">
+ <ref name="atomUri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="xml:lang">
+ <ref name="atomLanguageTag"/>
+ </attribute>
+ </optional>
+ <zeroOrMore>
+ <ref name="undefinedAttribute"/>
+ </zeroOrMore>
+ </define>
+ <!-- atom:link -->
+ <define name="atomLink">
+ <element name="atom:link">
+ <ref name="atomCommonAttributes"/>
+ <attribute name="href">
+ <ref name="atomUri"/>
+ </attribute>
+ <optional>
+ <attribute name="rel">
+ <choice>
+ <ref name="atomNCName"/>
+ <ref name="atomUri"/>
+ </choice>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="type">
+ <ref name="atomMediaType"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="hreflang">
+ <ref name="atomLanguageTag"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="title"/>
+ </optional>
+ <optional>
+ <attribute name="length"/>
+ </optional>
+ <ref name="undefinedContent"/>
+ </element>
+ </define>
+ <!-- Low-level simple types -->
+ <define name="atomNCName">
+ <data type="string">
+ <param name="minLength">1</param>
+ <param name="pattern">[^:]*</param>
+ </data>
+ </define>
+ <!-- Whatever a media type is, it contains at least one slash -->
+ <define name="atomMediaType">
+ <data type="string">
+ <param name="pattern">.+/.+</param>
+ </data>
+ </define>
+ <!-- As defined in RFC 3066 -->
+ <define name="atomLanguageTag">
+ <data type="string">
+ <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param>
+ </data>
+ </define>
+ <!--
+ Unconstrained; it's not entirely clear how IRI fit into
+ xsd:anyURI so let's not try to constrain it here
+ -->
+ <define name="atomUri">
+ <text/>
+ </define>
+ <!-- Other Extensibility -->
+ <define name="undefinedAttribute">
+ <attribute>
+ <anyName>
+ <except>
+ <name>xml:base</name>
+ <name>xml:lang</name>
+ <nsName ns=""/>
+ </except>
+ </anyName>
+ </attribute>
+ </define>
+ <define name="undefinedContent">
+ <zeroOrMore>
+ <choice>
+ <text/>
+ <ref name="anyForeignElement"/>
+ </choice>
+ </zeroOrMore>
+ </define>
+ <define name="anyElement">
+ <element>
+ <anyName/>
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="anyForeignElement">
+ <element>
+ <anyName>
+ <except>
+ <nsName ns="http://www.w3.org/2005/Atom"/>
+ </except>
+ </anyName>
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+</grammar>
diff --git a/nova/api/openstack/schemas/atom.rng b/nova/api/openstack/schemas/atom.rng
new file mode 100644
index 000000000..c2df4e410
--- /dev/null
+++ b/nova/api/openstack/schemas/atom.rng
@@ -0,0 +1,597 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ -*- rnc -*-
+ RELAX NG Compact Syntax Grammar for the
+ Atom Format Specification Version 11
+-->
+<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+ <start>
+ <choice>
+ <ref name="atomFeed"/>
+ <ref name="atomEntry"/>
+ </choice>
+ </start>
+ <!-- Common attributes -->
+ <define name="atomCommonAttributes">
+ <optional>
+ <attribute name="xml:base">
+ <ref name="atomUri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="xml:lang">
+ <ref name="atomLanguageTag"/>
+ </attribute>
+ </optional>
+ <zeroOrMore>
+ <ref name="undefinedAttribute"/>
+ </zeroOrMore>
+ </define>
+ <!-- Text Constructs -->
+ <define name="atomPlainTextConstruct">
+ <ref name="atomCommonAttributes"/>
+ <optional>
+ <attribute name="type">
+ <choice>
+ <value>text</value>
+ <value>html</value>
+ </choice>
+ </attribute>
+ </optional>
+ <text/>
+ </define>
+ <define name="atomXHTMLTextConstruct">
+ <ref name="atomCommonAttributes"/>
+ <attribute name="type">
+ <value>xhtml</value>
+ </attribute>
+ <ref name="xhtmlDiv"/>
+ </define>
+ <define name="atomTextConstruct">
+ <choice>
+ <ref name="atomPlainTextConstruct"/>
+ <ref name="atomXHTMLTextConstruct"/>
+ </choice>
+ </define>
+ <!-- Person Construct -->
+ <define name="atomPersonConstruct">
+ <ref name="atomCommonAttributes"/>
+ <interleave>
+ <element name="atom:name">
+ <text/>
+ </element>
+ <optional>
+ <element name="atom:uri">
+ <ref name="atomUri"/>
+ </element>
+ </optional>
+ <optional>
+ <element name="atom:email">
+ <ref name="atomEmailAddress"/>
+ </element>
+ </optional>
+ <zeroOrMore>
+ <ref name="extensionElement"/>
+ </zeroOrMore>
+ </interleave>
+ </define>
+ <!-- Date Construct -->
+ <define name="atomDateConstruct">
+ <ref name="atomCommonAttributes"/>
+ <data type="dateTime"/>
+ </define>
+ <!-- atom:feed -->
+ <define name="atomFeed">
+ <element name="atom:feed">
+ <s:rule context="atom:feed">
+ <s:assert test="atom:author or not(atom:entry[not(atom:author)])">An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author.</s:assert>
+ </s:rule>
+ <ref name="atomCommonAttributes"/>
+ <interleave>
+ <zeroOrMore>
+ <ref name="atomAuthor"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="atomCategory"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="atomContributor"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomGenerator"/>
+ </optional>
+ <optional>
+ <ref name="atomIcon"/>
+ </optional>
+ <ref name="atomId"/>
+ <zeroOrMore>
+ <ref name="atomLink"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomLogo"/>
+ </optional>
+ <optional>
+ <ref name="atomRights"/>
+ </optional>
+ <optional>
+ <ref name="atomSubtitle"/>
+ </optional>
+ <ref name="atomTitle"/>
+ <ref name="atomUpdated"/>
+ <zeroOrMore>
+ <ref name="extensionElement"/>
+ </zeroOrMore>
+ </interleave>
+ <zeroOrMore>
+ <ref name="atomEntry"/>
+ </zeroOrMore>
+ </element>
+ </define>
+ <!-- atom:entry -->
+ <define name="atomEntry">
+ <element name="atom:entry">
+ <s:rule context="atom:entry">
+ <s:assert test="atom:link[@rel='alternate'] or atom:link[not(@rel)] or atom:content">An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content.</s:assert>
+ </s:rule>
+ <s:rule context="atom:entry">
+ <s:assert test="atom:author or ../atom:author or atom:source/atom:author">An atom:entry must have an atom:author if its feed does not.</s:assert>
+ </s:rule>
+ <ref name="atomCommonAttributes"/>
+ <interleave>
+ <zeroOrMore>
+ <ref name="atomAuthor"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="atomCategory"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomContent"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="atomContributor"/>
+ </zeroOrMore>
+ <ref name="atomId"/>
+ <zeroOrMore>
+ <ref name="atomLink"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomPublished"/>
+ </optional>
+ <optional>
+ <ref name="atomRights"/>
+ </optional>
+ <optional>
+ <ref name="atomSource"/>
+ </optional>
+ <optional>
+ <ref name="atomSummary"/>
+ </optional>
+ <ref name="atomTitle"/>
+ <ref name="atomUpdated"/>
+ <zeroOrMore>
+ <ref name="extensionElement"/>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+ <!-- atom:content -->
+ <define name="atomInlineTextContent">
+ <element name="atom:content">
+ <ref name="atomCommonAttributes"/>
+ <optional>
+ <attribute name="type">
+ <choice>
+ <value>text</value>
+ <value>html</value>
+ </choice>
+ </attribute>
+ </optional>
+ <zeroOrMore>
+ <text/>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="atomInlineXHTMLContent">
+ <element name="atom:content">
+ <ref name="atomCommonAttributes"/>
+ <attribute name="type">
+ <value>xhtml</value>
+ </attribute>
+ <ref name="xhtmlDiv"/>
+ </element>
+ </define>
+ <define name="atomInlineOtherContent">
+ <element name="atom:content">
+ <ref name="atomCommonAttributes"/>
+ <optional>
+ <attribute name="type">
+ <ref name="atomMediaType"/>
+ </attribute>
+ </optional>
+ <zeroOrMore>
+ <choice>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="atomOutOfLineContent">
+ <element name="atom:content">
+ <ref name="atomCommonAttributes"/>
+ <optional>
+ <attribute name="type">
+ <ref name="atomMediaType"/>
+ </attribute>
+ </optional>
+ <attribute name="src">
+ <ref name="atomUri"/>
+ </attribute>
+ <empty/>
+ </element>
+ </define>
+ <define name="atomContent">
+ <choice>
+ <ref name="atomInlineTextContent"/>
+ <ref name="atomInlineXHTMLContent"/>
+ <ref name="atomInlineOtherContent"/>
+ <ref name="atomOutOfLineContent"/>
+ </choice>
+ </define>
+ <!-- atom:author -->
+ <define name="atomAuthor">
+ <element name="atom:author">
+ <ref name="atomPersonConstruct"/>
+ </element>
+ </define>
+ <!-- atom:category -->
+ <define name="atomCategory">
+ <element name="atom:category">
+ <ref name="atomCommonAttributes"/>
+ <attribute name="term"/>
+ <optional>
+ <attribute name="scheme">
+ <ref name="atomUri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="label"/>
+ </optional>
+ <ref name="undefinedContent"/>
+ </element>
+ </define>
+ <!-- atom:contributor -->
+ <define name="atomContributor">
+ <element name="atom:contributor">
+ <ref name="atomPersonConstruct"/>
+ </element>
+ </define>
+ <!-- atom:generator -->
+ <define name="atomGenerator">
+ <element name="atom:generator">
+ <ref name="atomCommonAttributes"/>
+ <optional>
+ <attribute name="uri">
+ <ref name="atomUri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="version"/>
+ </optional>
+ <text/>
+ </element>
+ </define>
+ <!-- atom:icon -->
+ <define name="atomIcon">
+ <element name="atom:icon">
+ <ref name="atomCommonAttributes"/>
+ <ref name="atomUri"/>
+ </element>
+ </define>
+ <!-- atom:id -->
+ <define name="atomId">
+ <element name="atom:id">
+ <ref name="atomCommonAttributes"/>
+ <ref name="atomUri"/>
+ </element>
+ </define>
+ <!-- atom:logo -->
+ <define name="atomLogo">
+ <element name="atom:logo">
+ <ref name="atomCommonAttributes"/>
+ <ref name="atomUri"/>
+ </element>
+ </define>
+ <!-- atom:link -->
+ <define name="atomLink">
+ <element name="atom:link">
+ <ref name="atomCommonAttributes"/>
+ <attribute name="href">
+ <ref name="atomUri"/>
+ </attribute>
+ <optional>
+ <attribute name="rel">
+ <choice>
+ <ref name="atomNCName"/>
+ <ref name="atomUri"/>
+ </choice>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="type">
+ <ref name="atomMediaType"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="hreflang">
+ <ref name="atomLanguageTag"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="title"/>
+ </optional>
+ <optional>
+ <attribute name="length"/>
+ </optional>
+ <ref name="undefinedContent"/>
+ </element>
+ </define>
+ <!-- atom:published -->
+ <define name="atomPublished">
+ <element name="atom:published">
+ <ref name="atomDateConstruct"/>
+ </element>
+ </define>
+ <!-- atom:rights -->
+ <define name="atomRights">
+ <element name="atom:rights">
+ <ref name="atomTextConstruct"/>
+ </element>
+ </define>
+ <!-- atom:source -->
+ <define name="atomSource">
+ <element name="atom:source">
+ <ref name="atomCommonAttributes"/>
+ <interleave>
+ <zeroOrMore>
+ <ref name="atomAuthor"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="atomCategory"/>
+ </zeroOrMore>
+ <zeroOrMore>
+ <ref name="atomContributor"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomGenerator"/>
+ </optional>
+ <optional>
+ <ref name="atomIcon"/>
+ </optional>
+ <optional>
+ <ref name="atomId"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="atomLink"/>
+ </zeroOrMore>
+ <optional>
+ <ref name="atomLogo"/>
+ </optional>
+ <optional>
+ <ref name="atomRights"/>
+ </optional>
+ <optional>
+ <ref name="atomSubtitle"/>
+ </optional>
+ <optional>
+ <ref name="atomTitle"/>
+ </optional>
+ <optional>
+ <ref name="atomUpdated"/>
+ </optional>
+ <zeroOrMore>
+ <ref name="extensionElement"/>
+ </zeroOrMore>
+ </interleave>
+ </element>
+ </define>
+ <!-- atom:subtitle -->
+ <define name="atomSubtitle">
+ <element name="atom:subtitle">
+ <ref name="atomTextConstruct"/>
+ </element>
+ </define>
+ <!-- atom:summary -->
+ <define name="atomSummary">
+ <element name="atom:summary">
+ <ref name="atomTextConstruct"/>
+ </element>
+ </define>
+ <!-- atom:title -->
+ <define name="atomTitle">
+ <element name="atom:title">
+ <ref name="atomTextConstruct"/>
+ </element>
+ </define>
+ <!-- atom:updated -->
+ <define name="atomUpdated">
+ <element name="atom:updated">
+ <ref name="atomDateConstruct"/>
+ </element>
+ </define>
+ <!-- Low-level simple types -->
+ <define name="atomNCName">
+ <data type="string">
+ <param name="minLength">1</param>
+ <param name="pattern">[^:]*</param>
+ </data>
+ </define>
+ <!-- Whatever a media type is, it contains at least one slash -->
+ <define name="atomMediaType">
+ <data type="string">
+ <param name="pattern">.+/.+</param>
+ </data>
+ </define>
+ <!-- As defined in RFC 3066 -->
+ <define name="atomLanguageTag">
+ <data type="string">
+ <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param>
+ </data>
+ </define>
+ <!--
+ Unconstrained; it's not entirely clear how IRI fit into
+ xsd:anyURI so let's not try to constrain it here
+ -->
+ <define name="atomUri">
+ <text/>
+ </define>
+ <!-- Whatever an email address is, it contains at least one @ -->
+ <define name="atomEmailAddress">
+ <data type="string">
+ <param name="pattern">.+@.+</param>
+ </data>
+ </define>
+ <!-- Simple Extension -->
+ <define name="simpleExtensionElement">
+ <element>
+ <anyName>
+ <except>
+ <nsName ns="http://www.w3.org/2005/Atom"/>
+ </except>
+ </anyName>
+ <text/>
+ </element>
+ </define>
+ <!-- Structured Extension -->
+ <define name="structuredExtensionElement">
+ <element>
+ <anyName>
+ <except>
+ <nsName ns="http://www.w3.org/2005/Atom"/>
+ </except>
+ </anyName>
+ <choice>
+ <group>
+ <oneOrMore>
+ <attribute>
+ <anyName/>
+ </attribute>
+ </oneOrMore>
+ <zeroOrMore>
+ <choice>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </group>
+ <group>
+ <zeroOrMore>
+ <attribute>
+ <anyName/>
+ </attribute>
+ </zeroOrMore>
+ <group>
+ <optional>
+ <text/>
+ </optional>
+ <oneOrMore>
+ <ref name="anyElement"/>
+ </oneOrMore>
+ <zeroOrMore>
+ <choice>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </group>
+ </group>
+ </choice>
+ </element>
+ </define>
+ <!-- Other Extensibility -->
+ <define name="extensionElement">
+ <choice>
+ <ref name="simpleExtensionElement"/>
+ <ref name="structuredExtensionElement"/>
+ </choice>
+ </define>
+ <define name="undefinedAttribute">
+ <attribute>
+ <anyName>
+ <except>
+ <name>xml:base</name>
+ <name>xml:lang</name>
+ <nsName ns=""/>
+ </except>
+ </anyName>
+ </attribute>
+ </define>
+ <define name="undefinedContent">
+ <zeroOrMore>
+ <choice>
+ <text/>
+ <ref name="anyForeignElement"/>
+ </choice>
+ </zeroOrMore>
+ </define>
+ <define name="anyElement">
+ <element>
+ <anyName/>
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="anyForeignElement">
+ <element>
+ <anyName>
+ <except>
+ <nsName ns="http://www.w3.org/2005/Atom"/>
+ </except>
+ </anyName>
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyElement"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <!-- XHTML -->
+ <define name="anyXHTML">
+ <element>
+ <nsName ns="http://www.w3.org/1999/xhtml"/>
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyXHTML"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+ <define name="xhtmlDiv">
+ <element name="xhtml:div">
+ <zeroOrMore>
+ <choice>
+ <attribute>
+ <anyName/>
+ </attribute>
+ <text/>
+ <ref name="anyXHTML"/>
+ </choice>
+ </zeroOrMore>
+ </element>
+ </define>
+</grammar>
diff --git a/nova/api/openstack/schemas/v1.1/extension.rng b/nova/api/openstack/schemas/v1.1/extension.rng
new file mode 100644
index 000000000..336659755
--- /dev/null
+++ b/nova/api/openstack/schemas/v1.1/extension.rng
@@ -0,0 +1,11 @@
+<element name="extension" ns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns="http://relaxng.org/ns/structure/1.0">
+ <attribute name="alias"> <text/> </attribute>
+ <attribute name="name"> <text/> </attribute>
+ <attribute name="namespace"> <text/> </attribute>
+ <attribute name="updated"> <text/> </attribute>
+ <element name="description"> <text/> </element>
+ <zeroOrMore>
+ <externalRef href="../atom-link.rng"/>
+ </zeroOrMore>
+</element>
diff --git a/nova/api/openstack/schemas/v1.1/extensions.rng b/nova/api/openstack/schemas/v1.1/extensions.rng
new file mode 100644
index 000000000..4d8bff646
--- /dev/null
+++ b/nova/api/openstack/schemas/v1.1/extensions.rng
@@ -0,0 +1,6 @@
+<element name="extensions" xmlns="http://relaxng.org/ns/structure/1.0"
+ ns="http://docs.openstack.org/compute/api/v1.1">
+ <zeroOrMore>
+ <externalRef href="extension.rng"/>
+ </zeroOrMore>
+</element>
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py
index d4f42bbf5..2b235f79a 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -18,6 +18,7 @@
from webob import exc
from nova import compute
+from nova.api.openstack import common
from nova.api.openstack import wsgi
from nova import exception
from nova import quota
@@ -31,89 +32,122 @@ class Controller(object):
super(Controller, self).__init__()
def _get_metadata(self, context, server_id):
- metadata = self.compute_api.get_instance_metadata(context, server_id)
+ try:
+ meta = self.compute_api.get_instance_metadata(context, server_id)
+ except exception.InstanceNotFound:
+ msg = _('Server does not exist')
+ raise exc.HTTPNotFound(explanation=msg)
+
meta_dict = {}
- for key, value in metadata.iteritems():
+ for key, value in meta.iteritems():
meta_dict[key] = value
- return dict(metadata=meta_dict)
-
- def _check_body(self, body):
- if body == None or body == "":
- expl = _('No Request Body')
- raise exc.HTTPBadRequest(explanation=expl)
+ return meta_dict
def index(self, req, server_id):
""" Returns the list of metadata for a given instance """
context = req.environ['nova.context']
- try:
- return self._get_metadata(context, server_id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ return {'metadata': self._get_metadata(context, server_id)}
def create(self, req, server_id, body):
- self._check_body(body)
- context = req.environ['nova.context']
- metadata = body.get('metadata')
try:
- self.compute_api.update_or_create_instance_metadata(context,
- server_id,
- metadata)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ metadata = body['metadata']
+ except (KeyError, TypeError):
+ msg = _("Malformed request body")
+ raise exc.HTTPBadRequest(explanation=msg)
- except quota.QuotaError as error:
- self._handle_quota_error(error)
+ context = req.environ['nova.context']
- return body
+ new_metadata = self._update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete=False)
+
+ return {'metadata': new_metadata}
def update(self, req, server_id, id, body):
- self._check_body(body)
- context = req.environ['nova.context']
- if not id in body:
+ try:
+ meta_item = body['meta']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ try:
+ meta_value = meta_item[id]
+ except (AttributeError, KeyError):
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
- if len(body) > 1:
+
+ if len(meta_item) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['nova.context']
+ self._update_instance_metadata(context,
+ server_id,
+ meta_item,
+ delete=False)
+
+ return {'meta': meta_item}
+
+ def update_all(self, req, server_id, body):
+ try:
+ metadata = body['metadata']
+ except (TypeError, KeyError):
+ expl = _('Malformed request body')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ context = req.environ['nova.context']
+ new_metadata = self._update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete=True)
+
+ return {'metadata': new_metadata}
+
+ def _update_instance_metadata(self, context, server_id, metadata,
+ delete=False):
try:
- self.compute_api.update_or_create_instance_metadata(context,
- server_id,
- body)
+ return self.compute_api.update_instance_metadata(context,
+ server_id,
+ metadata,
+ delete)
+
except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
+ msg = _('Server does not exist')
raise exc.HTTPNotFound(explanation=msg)
+ except (ValueError, AttributeError):
+ msg = _("Malformed request body")
+ raise exc.HTTPBadRequest(explanation=msg)
+
except quota.QuotaError as error:
self._handle_quota_error(error)
- return body
-
def show(self, req, server_id, id):
""" Return a single metadata item """
context = req.environ['nova.context']
- try:
- data = self._get_metadata(context, server_id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
- raise exc.HTTPNotFound(explanation=msg)
+ data = self._get_metadata(context, server_id)
try:
- return {id: data['metadata'][id]}
+ return {'meta': {id: data[id]}}
except KeyError:
- msg = _("metadata item %s was not found" % (id))
+ msg = _("Metadata item was not found")
raise exc.HTTPNotFound(explanation=msg)
def delete(self, req, server_id, id):
""" Deletes an existing metadata """
context = req.environ['nova.context']
+
+ metadata = self._get_metadata(context, server_id)
+
try:
- self.compute_api.delete_instance_metadata(context, server_id, id)
- except exception.InstanceNotFound:
- msg = _('Server %(server_id)s does not exist') % locals()
+ meta_value = metadata[id]
+ except KeyError:
+ msg = _("Metadata item was not found")
raise exc.HTTPNotFound(explanation=msg)
+ self.compute_api.delete_instance_metadata(context, server_id, id)
+
def _handle_quota_error(self, error):
"""Reraise quota errors as api-specific http exceptions."""
if error.code == "MetadataLimitExceeded":
@@ -122,10 +156,16 @@ class Controller(object):
def create_resource():
- body_serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
+ headers_serializer = common.MetadataHeadersSerializer()
+
+ body_deserializers = {
+ 'application/xml': common.MetadataXMLDeserializer(),
}
- serializer = wsgi.ResponseSerializer(body_serializers)
+ body_serializers = {
+ 'application/xml': common.MetadataXMLSerializer(),
+ }
+ serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
- return wsgi.Resource(Controller(), serializer=serializer)
+ return wsgi.Resource(Controller(), deserializer, serializer)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 7b757143d..736fdf6ce 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -44,7 +44,7 @@ FLAGS = flags.FLAGS
class Controller(object):
- """ The Server API controller for the OpenStack API """
+ """ The Server API base controller class for the OpenStack API """
def __init__(self):
self.compute_api = compute.API()
@@ -53,17 +53,21 @@ class Controller(object):
def index(self, req):
""" Returns a list of server names and ids for a given user """
try:
- servers = self._items(req, is_detail=False)
+ servers = self._get_servers(req, is_detail=False)
except exception.Invalid as err:
return exc.HTTPBadRequest(explanation=str(err))
+ except exception.NotFound:
+ return exc.HTTPNotFound()
return servers
def detail(self, req):
""" Returns a list of server details for a given user """
try:
- servers = self._items(req, is_detail=True)
+ servers = self._get_servers(req, is_detail=True)
except exception.Invalid as err:
return exc.HTTPBadRequest(explanation=str(err))
+ except exception.NotFound as err:
+ return exc.HTTPNotFound()
return servers
def _build_view(self, req, instance, is_detail=False):
@@ -75,22 +79,55 @@ class Controller(object):
def _action_rebuild(self, info, request, instance_id):
raise NotImplementedError()
- def _items(self, req, is_detail):
- """Returns a list of servers for a given user.
-
- builder - the response model builder
+ def _get_servers(self, req, is_detail):
+ """Returns a list of servers, taking into account any search
+ options specified.
"""
- query_str = req.str_GET
- reservation_id = query_str.get('reservation_id')
- project_id = query_str.get('project_id')
- fixed_ip = query_str.get('fixed_ip')
- recurse_zones = utils.bool_from_str(query_str.get('recurse_zones'))
+
+ search_opts = {}
+ search_opts.update(req.str_GET)
+
+ context = req.environ['nova.context']
+ remove_invalid_options(context, search_opts,
+ self._get_server_search_options())
+
+ # Convert recurse_zones into a boolean
+ search_opts['recurse_zones'] = utils.bool_from_str(
+ search_opts.get('recurse_zones', False))
+
+ # If search by 'status', we need to convert it to 'state'
+ # If the status is unknown, bail.
+ # Leave 'state' in search_opts so compute can pass it on to
+ # child zones..
+ if 'status' in search_opts:
+ status = search_opts['status']
+ search_opts['state'] = common.power_states_from_status(status)
+ if len(search_opts['state']) == 0:
+ reason = _('Invalid server status: %(status)s') % locals()
+ LOG.error(reason)
+ raise exception.InvalidInput(reason=reason)
+
+ # By default, compute's get_all() will return deleted instances.
+ # If an admin hasn't specified a 'deleted' search option, we need
+ # to filter out deleted instances by setting the filter ourselves.
+ # ... Unless 'changes-since' is specified, because 'changes-since'
+ # should return recently deleted images according to the API spec.
+
+ if 'deleted' not in search_opts:
+ # Admin hasn't specified deleted filter
+ if 'changes-since' not in search_opts:
+ # No 'changes-since', so we need to find non-deleted servers
+ search_opts['deleted'] = False
+ else:
+ # This is the default, but just in case..
+ search_opts['deleted'] = True
+
instance_list = self.compute_api.get_all(
- req.environ['nova.context'],
- reservation_id=reservation_id,
- project_id=project_id,
- fixed_ip=fixed_ip,
- recurse_zones=recurse_zones)
+ context, search_opts=search_opts)
+
+ # FIXME(comstud): 'changes-since' is not fully implemented. Where
+ # should this be filtered?
+
limited_list = self._limit_items(instance_list, req)
servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
@@ -164,7 +201,6 @@ class Controller(object):
'confirmResize': self._action_confirm_resize,
'revertResize': self._action_revert_resize,
'rebuild': self._action_rebuild,
- 'migrate': self._action_migrate,
'createImage': self._action_create_image,
}
@@ -219,13 +255,14 @@ class Controller(object):
props = {'instance_ref': server_ref}
metadata = entity.get('metadata', {})
+ context = req.environ["nova.context"]
+ common.check_img_metadata_quota_limit(context, metadata)
try:
props.update(metadata)
except ValueError:
msg = _("Invalid metadata")
raise webob.exc.HTTPBadRequest(explanation=msg)
- context = req.environ["nova.context"]
image = self.compute_api.backup(context,
instance_id,
image_name,
@@ -241,6 +278,7 @@ class Controller(object):
resp.headers['Location'] = image_ref
return resp
+ @common.check_snapshots_enabled
def _action_create_image(self, input_dict, req, id):
return exc.HTTPNotImplemented()
@@ -268,10 +306,16 @@ class Controller(object):
def _action_reboot(self, input_dict, req, id):
if 'reboot' in input_dict and 'type' in input_dict['reboot']:
- reboot_type = input_dict['reboot']['type']
+ valid_reboot_types = ['HARD', 'SOFT']
+ reboot_type = input_dict['reboot']['type'].upper()
+ if not valid_reboot_types.count(reboot_type):
+ msg = _("Argument 'type' for reboot is not HARD or SOFT")
+ LOG.exception(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
else:
- LOG.exception(_("Missing argument 'type' for reboot"))
- raise exc.HTTPUnprocessableEntity()
+ msg = _("Missing argument 'type' for reboot")
+ LOG.exception(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
try:
# TODO(gundlach): pass reboot_type, support soft reboot in
# virt driver
@@ -281,14 +325,6 @@ class Controller(object):
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
- def _action_migrate(self, input_dict, req, id):
- try:
- self.compute_api.resize(req.environ['nova.context'], id)
- except Exception, e:
- LOG.exception(_("Error in migrate %s"), e)
- raise exc.HTTPBadRequest()
- return webob.Response(status_int=202)
-
@scheduler_api.redirect_handler
def lock(self, req, id):
"""
@@ -299,7 +335,7 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.lock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::lock %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -315,7 +351,7 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.unlock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unlock %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -330,14 +366,14 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.get_lock(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::get_lock %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def reset_network(self, req, id, body):
+ def reset_network(self, req, id):
"""
Reset networking on an instance (admin only).
@@ -345,14 +381,14 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.reset_network(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::reset_network %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def inject_network_info(self, req, id, body):
+ def inject_network_info(self, req, id):
"""
Inject network info for an instance (admin only).
@@ -360,67 +396,76 @@ class Controller(object):
context = req.environ['nova.context']
try:
self.compute_api.inject_network_info(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::inject_network_info %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def pause(self, req, id, body):
+ def pause(self, req, id):
""" Permit Admins to Pause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.pause(ctxt, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::pause %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def unpause(self, req, id, body):
+ def unpause(self, req, id):
""" Permit Admins to Unpause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.unpause(ctxt, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unpause %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def suspend(self, req, id, body):
+ def suspend(self, req, id):
"""permit admins to suspend the server"""
context = req.environ['nova.context']
try:
self.compute_api.suspend(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::suspend %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
- def resume(self, req, id, body):
+ def resume(self, req, id):
"""permit admins to resume the server from suspend"""
context = req.environ['nova.context']
try:
self.compute_api.resume(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::resume %s"), readable)
raise exc.HTTPUnprocessableEntity()
return webob.Response(status_int=202)
@scheduler_api.redirect_handler
+ def migrate(self, req, id):
+ try:
+ self.compute_api.resize(req.environ['nova.context'], id)
+ except Exception, e:
+ LOG.exception(_("Error in migrate %s"), e)
+ raise exc.HTTPBadRequest()
+ return webob.Response(status_int=202)
+
+ @scheduler_api.redirect_handler
def rescue(self, req, id):
"""Permit users to rescue the server."""
context = req.environ["nova.context"]
try:
self.compute_api.rescue(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::rescue %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -432,7 +477,7 @@ class Controller(object):
context = req.environ["nova.context"]
try:
self.compute_api.unrescue(context, id)
- except:
+ except Exception:
readable = traceback.format_exc()
LOG.exception(_("compute.api::unrescue %s"), readable)
raise exc.HTTPUnprocessableEntity()
@@ -498,6 +543,7 @@ class Controller(object):
class ControllerV10(Controller):
+ """v1.0 OpenStack API controller"""
@scheduler_api.redirect_handler
def delete(self, req, id):
@@ -560,8 +606,13 @@ class ControllerV10(Controller):
""" Determine the admin password for a server on creation """
return self.helper._get_server_admin_password_old_style(server)
+ def _get_server_search_options(self):
+ """Return server search options allowed by non-admin"""
+ return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones'
+
class ControllerV11(Controller):
+ """v1.1 OpenStack API controller"""
@scheduler_api.redirect_handler
def delete(self, req, id):
@@ -646,6 +697,9 @@ class ControllerV11(Controller):
""" Resizes a given instance to the flavor size requested """
try:
flavor_ref = input_dict["resize"]["flavorRef"]
+ if not flavor_ref:
+ msg = _("Resize request has invalid 'flavorRef' attribute.")
+ raise exc.HTTPBadRequest(explanation=msg)
except (KeyError, TypeError):
msg = _("Resize requests require 'flavorRef' attribute.")
raise exc.HTTPBadRequest(explanation=msg)
@@ -680,6 +734,7 @@ class ControllerV11(Controller):
return webob.Response(status_int=202)
+ @common.check_snapshots_enabled
def _action_create_image(self, input_dict, req, instance_id):
"""Snapshot a server instance."""
entity = input_dict.get("createImage", {})
@@ -702,13 +757,14 @@ class ControllerV11(Controller):
props = {'instance_ref': server_ref}
metadata = entity.get('metadata', {})
+ context = req.environ['nova.context']
+ common.check_img_metadata_quota_limit(context, metadata)
try:
props.update(metadata)
except ValueError:
msg = _("Invalid metadata")
raise webob.exc.HTTPBadRequest(explanation=msg)
- context = req.environ['nova.context']
image = self.compute_api.snapshot(context,
instance_id,
image_name,
@@ -729,9 +785,17 @@ class ControllerV11(Controller):
""" Determine the admin password for a server on creation """
return self.helper._get_server_admin_password_new_style(server)
+ def _get_server_search_options(self):
+ """Return server search options allowed by non-admin"""
+ return ('reservation_id', 'name', 'recurse_zones',
+ 'status', 'image', 'flavor', 'changes-since')
+
class HeadersSerializer(wsgi.ResponseHeadersSerializer):
+ def create(self, response, data):
+ response.status_int = 202
+
def delete(self, response, data):
response.status_int = 204
@@ -891,11 +955,31 @@ def create_resource(version='1.0'):
'application/xml': xml_serializer,
}
+ xml_deserializer = {
+ '1.0': helper.ServerXMLDeserializer(),
+ '1.1': helper.ServerXMLDeserializerV11(),
+ }[version]
+
body_deserializers = {
- 'application/xml': helper.ServerXMLDeserializer(),
+ 'application/xml': xml_deserializer,
}
serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
deserializer = wsgi.RequestDeserializer(body_deserializers)
return wsgi.Resource(controller, deserializer, serializer)
+
+
+def remove_invalid_options(context, search_options, allowed_search_options):
+ """Remove search options that are not valid for non-admin API/context"""
+ if FLAGS.allow_admin_api and context.is_admin:
+ # Allow all options
+ return
+ # Otherwise, strip out all unknown options
+ unknown_options = [opt for opt in search_options
+ if opt not in allowed_search_options]
+ unk_opt_str = ", ".join(unknown_options)
+ log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals()
+ LOG.debug(log_msg)
+ for opt in unknown_options:
+ search_options.pop(opt, None)
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
index df7a94b7e..e2f892fb6 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -24,7 +24,66 @@ import nova.api.openstack.views.versions
from nova.api.openstack import wsgi
-ATOM_XMLNS = "http://www.w3.org/2005/Atom"
+VERSIONS = {
+ "v1.0": {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.0+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.0+json",
+ }
+ ],
+ },
+ "v1.1": {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.1+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.1+json",
+ }
+ ],
+ },
+}
class Versions(wsgi.Resource):
@@ -36,16 +95,20 @@ class Versions(wsgi.Resource):
}
}
+ headers_serializer = VersionsHeadersSerializer()
+
body_serializers = {
'application/atom+xml': VersionsAtomSerializer(metadata=metadata),
'application/xml': VersionsXMLSerializer(metadata=metadata),
}
- serializer = wsgi.ResponseSerializer(body_serializers)
+ serializer = wsgi.ResponseSerializer(
+ body_serializers=body_serializers,
+ headers_serializer=headers_serializer)
supported_content_types = ('application/json',
'application/xml',
'application/atom+xml')
- deserializer = wsgi.RequestDeserializer(
+ deserializer = VersionsRequestDeserializer(
supported_content_types=supported_content_types)
wsgi.Resource.__init__(self, None, serializer=serializer,
@@ -53,60 +116,131 @@ class Versions(wsgi.Resource):
def dispatch(self, request, *args):
"""Respond to a request for all OpenStack API versions."""
- version_objs = [
- {
- "id": "v1.1",
- "status": "CURRENT",
- #TODO(wwolf) get correct value for these
- "updated": "2011-07-18T11:30:00Z",
- },
- {
- "id": "v1.0",
- "status": "DEPRECATED",
- #TODO(wwolf) get correct value for these
- "updated": "2010-10-09T11:30:00Z",
- },
- ]
-
builder = nova.api.openstack.views.versions.get_view_builder(request)
- versions = [builder.build(version) for version in version_objs]
- return dict(versions=versions)
+ if request.path == '/':
+ # List Versions
+ return builder.build_versions(VERSIONS)
+ else:
+ # Versions Multiple Choice
+ return builder.build_choices(VERSIONS, request)
+
+
+class VersionV10(object):
+ def show(self, req):
+ builder = nova.api.openstack.views.versions.get_view_builder(req)
+ return builder.build_version(VERSIONS['v1.0'])
+
+
+class VersionV11(object):
+ def show(self, req):
+ builder = nova.api.openstack.views.versions.get_view_builder(req)
+ return builder.build_version(VERSIONS['v1.1'])
+
+
+class VersionsRequestDeserializer(wsgi.RequestDeserializer):
+ def get_expected_content_type(self, request):
+ supported_content_types = list(self.supported_content_types)
+ if request.path != '/':
+ # Remove atom+xml accept type for 300 responses
+ if 'application/atom+xml' in supported_content_types:
+ supported_content_types.remove('application/atom+xml')
+
+ return request.best_match_content_type(supported_content_types)
+
+ def get_action_args(self, request_environment):
+ """Parse dictionary created by routes library."""
+ args = {}
+ if request_environment['PATH_INFO'] == '/':
+ args['action'] = 'index'
+ else:
+ args['action'] = 'multi'
+
+ return args
class VersionsXMLSerializer(wsgi.XMLDictSerializer):
- def _versions_to_xml(self, versions):
- root = self._xml_doc.createElement('versions')
+ #TODO(wwolf): this is temporary until we get rid of toprettyxml
+ # in the base class (XMLDictSerializer), which I plan to do in
+ # another branch
+ def to_xml_string(self, node, has_atom=False):
+ self._add_xmlns(node, has_atom)
+ return node.toxml(encoding='UTF-8')
+
+ def _versions_to_xml(self, versions, name="versions", xmlns=None):
+ root = self._xml_doc.createElement(name)
+ root.setAttribute("xmlns", wsgi.XMLNS_V11)
+ root.setAttribute("xmlns:atom", wsgi.XMLNS_ATOM)
for version in versions:
root.appendChild(self._create_version_node(version))
return root
- def _create_version_node(self, version):
+ def _create_media_types(self, media_types):
+ base = self._xml_doc.createElement('media-types')
+ for type in media_types:
+ node = self._xml_doc.createElement('media-type')
+ node.setAttribute('base', type['base'])
+ node.setAttribute('type', type['type'])
+ base.appendChild(node)
+
+ return base
+
+ def _create_version_node(self, version, create_ns=False):
version_node = self._xml_doc.createElement('version')
+ if create_ns:
+ xmlns = wsgi.XMLNS_V11
+ xmlns_atom = wsgi.XMLNS_ATOM
+ version_node.setAttribute('xmlns', xmlns)
+ version_node.setAttribute('xmlns:atom', xmlns_atom)
+
version_node.setAttribute('id', version['id'])
version_node.setAttribute('status', version['status'])
- version_node.setAttribute('updated', version['updated'])
+ if 'updated' in version:
+ version_node.setAttribute('updated', version['updated'])
+
+ if 'media-types' in version:
+ media_types = self._create_media_types(version['media-types'])
+ version_node.appendChild(media_types)
- for link in version['links']:
- link_node = self._xml_doc.createElement('atom:link')
- link_node.setAttribute('rel', link['rel'])
- link_node.setAttribute('href', link['href'])
- version_node.appendChild(link_node)
+ link_nodes = self._create_link_nodes(self._xml_doc, version['links'])
+ for link in link_nodes:
+ version_node.appendChild(link)
return version_node
- def default(self, data):
+ def index(self, data):
self._xml_doc = minidom.Document()
node = self._versions_to_xml(data['versions'])
return self.to_xml_string(node)
+ def show(self, data):
+ self._xml_doc = minidom.Document()
+ node = self._create_version_node(data['version'], True)
+
+ return self.to_xml_string(node)
+
+ def multi(self, data):
+ self._xml_doc = minidom.Document()
+ node = self._versions_to_xml(data['choices'], 'choices',
+ xmlns=wsgi.XMLNS_V11)
+
+ return self.to_xml_string(node)
+
class VersionsAtomSerializer(wsgi.XMLDictSerializer):
+ #TODO(wwolf): this is temporary until we get rid of toprettyxml
+ # in the base class (XMLDictSerializer), which I plan to do in
+ # another branch
+ def to_xml_string(self, node, has_atom=False):
+ self._add_xmlns(node, has_atom)
+ return node.toxml(encoding='UTF-8')
+
def __init__(self, metadata=None, xmlns=None):
+ self.metadata = metadata or {}
if not xmlns:
- self.xmlns = ATOM_XMLNS
+ self.xmlns = wsgi.XMLNS_ATOM
else:
self.xmlns = xmlns
@@ -135,8 +269,33 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer):
link_href = link_href.rstrip('/')
return link_href.rsplit('/', 1)[0] + '/'
- def _create_meta(self, root, versions):
- title = self._create_text_elem('title', 'Available API Versions',
+ def _create_detail_meta(self, root, version):
+ title = self._create_text_elem('title', "About This Version",
+ type='text')
+
+ updated = self._create_text_elem('updated', version['updated'])
+
+ uri = version['links'][0]['href']
+ id = self._create_text_elem('id', uri)
+
+ link = self._xml_doc.createElement('link')
+ link.setAttribute('rel', 'self')
+ link.setAttribute('href', uri)
+
+ author = self._xml_doc.createElement('author')
+ author_name = self._create_text_elem('name', 'Rackspace')
+ author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/')
+ author.appendChild(author_name)
+ author.appendChild(author_uri)
+
+ root.appendChild(title)
+ root.appendChild(updated)
+ root.appendChild(id)
+ root.appendChild(author)
+ root.appendChild(link)
+
+ def _create_list_meta(self, root, versions):
+ title = self._create_text_elem('title', "Available API Versions",
type='text')
# Set this updated to the most recently updated version
recent = self._get_most_recent_update(versions)
@@ -144,6 +303,7 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer):
base_url = self._get_base_url(versions[0]['links'][0]['href'])
id = self._create_text_elem('id', base_url)
+
link = self._xml_doc.createElement('link')
link.setAttribute('rel', 'self')
link.setAttribute('href', base_url)
@@ -178,7 +338,10 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer):
link_node = self._xml_doc.createElement('link')
link_node.setAttribute('rel', link['rel'])
link_node.setAttribute('href', link['href'])
- entry.appendChild(link_node)
+ if 'type' in link:
+ link_node.setAttribute('type', link['type'])
+
+ entry.appendChild(link_node)
content = self._create_text_elem('content',
'Version %s %s (%s)' %
@@ -190,10 +353,45 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer):
entry.appendChild(content)
root.appendChild(entry)
- def default(self, data):
+ def index(self, data):
self._xml_doc = minidom.Document()
node = self._xml_doc.createElementNS(self.xmlns, 'feed')
- self._create_meta(node, data['versions'])
+ self._create_list_meta(node, data['versions'])
self._create_version_entries(node, data['versions'])
return self.to_xml_string(node)
+
+ def show(self, data):
+ self._xml_doc = minidom.Document()
+ node = self._xml_doc.createElementNS(self.xmlns, 'feed')
+ self._create_detail_meta(node, data['version'])
+ self._create_version_entries(node, [data['version']])
+
+ return self.to_xml_string(node)
+
+
+class VersionsHeadersSerializer(wsgi.ResponseHeadersSerializer):
+ def multi(self, response, data):
+ response.status_int = 300
+
+
+def create_resource(version='1.0'):
+ controller = {
+ '1.0': VersionV10,
+ '1.1': VersionV11,
+ }[version]()
+
+ body_serializers = {
+ 'application/xml': VersionsXMLSerializer(),
+ 'application/atom+xml': VersionsAtomSerializer(),
+ }
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ supported_content_types = ('application/json',
+ 'application/xml',
+ 'application/atom+xml')
+ deserializer = wsgi.RequestDeserializer(
+ supported_content_types=supported_content_types)
+
+ return wsgi.Resource(controller, serializer=serializer,
+ deserializer=deserializer)
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index 873ce212a..912303d14 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -77,7 +77,9 @@ class ViewBuilder(object):
"status": image_obj.get("status"),
})
- if image["status"] == "SAVING":
+ if image["status"].upper() == "ACTIVE":
+ image["progress"] = 100
+ else:
image["progress"] = 0
return image
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 2873a8e0f..8222f6766 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -20,7 +20,6 @@ import hashlib
import os
from nova import exception
-from nova.compute import power_state
import nova.compute
import nova.context
from nova.api.openstack import common
@@ -61,24 +60,11 @@ 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: 'SHUTDOWN',
- power_state.SHUTOFF: 'SHUTOFF',
- power_state.CRASHED: 'ERROR',
- power_state.FAILED: 'ERROR',
- power_state.BUILDING: 'BUILD',
- }
inst_dict = {
'id': inst['id'],
'name': inst['display_name'],
- 'status': power_mapping[inst.get('state')]}
+ 'status': common.status_from_power_state(inst.get('state'))}
ctxt = nova.context.get_admin_context()
compute_api = nova.compute.API()
diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py
index 9fa8f49dc..03da80818 100644
--- a/nova/api/openstack/views/versions.py
+++ b/nova/api/openstack/views/versions.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
import os
@@ -31,16 +32,44 @@ class ViewBuilder(object):
"""
self.base_url = base_url
- def build(self, version_data):
- """Generic method used to generate a version entity."""
- version = {
- "id": version_data["id"],
- "status": version_data["status"],
- "updated": version_data["updated"],
- "links": self._build_links(version_data),
- }
+ def build_choices(self, VERSIONS, req):
+ version_objs = []
+ for version in VERSIONS:
+ version = VERSIONS[version]
+ version_objs.append({
+ "id": version['id'],
+ "status": version['status'],
+ "links": [
+ {
+ "rel": "self",
+ "href": self.generate_href(version['id'], req.path),
+ },
+ ],
+ "media-types": version['media-types'],
+ })
- return version
+ return dict(choices=version_objs)
+
+ def build_versions(self, versions):
+ version_objs = []
+ for version in versions:
+ version = versions[version]
+ version_objs.append({
+ "id": version['id'],
+ "status": version['status'],
+ "updated": version['updated'],
+ "links": self._build_links(version),
+ })
+
+ return dict(versions=version_objs)
+
+ def build_version(self, version):
+ reval = copy.deepcopy(version)
+ reval['links'].insert(0, {
+ "rel": "self",
+ "href": self.base_url.rstrip('/') + '/',
+ })
+ return dict(version=reval)
def _build_links(self, version_data):
"""Generate a container of links that refer to the provided version."""
@@ -55,6 +84,11 @@ class ViewBuilder(object):
return links
- def generate_href(self, version_number):
+ def generate_href(self, version_number, path=None):
"""Create an url that refers to a specific version_number."""
- return os.path.join(self.base_url, version_number) + '/'
+ version_number = version_number.strip('/')
+ if path:
+ path = path.strip('/')
+ return os.path.join(self.base_url, version_number, path)
+ else:
+ return os.path.join(self.base_url, version_number) + '/'
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 53dab22e8..0eb47044e 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -13,6 +13,7 @@ from nova import wsgi
XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
+
XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
LOG = logging.getLogger('nova.api.openstack.wsgi')
@@ -386,6 +387,8 @@ class XMLDictSerializer(DictSerializer):
link_node = xml_doc.createElement('atom:link')
link_node.setAttribute('rel', link['rel'])
link_node.setAttribute('href', link['href'])
+ if 'type' in link:
+ link_node.setAttribute('type', link['type'])
link_nodes.append(link_node)
return link_nodes
diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py
new file mode 100644
index 000000000..97ad90ada
--- /dev/null
+++ b/nova/api/openstack/xmlutil.py
@@ -0,0 +1,37 @@
+# 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.
+
+import os.path
+
+from lxml import etree
+
+from nova import utils
+
+
+XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
+XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
+XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
+
+
+def validate_schema(xml, schema_name):
+ if type(xml) is str:
+ xml = etree.fromstring(xml)
+ schema_path = os.path.join(utils.novadir(),
+ 'nova/api/openstack/schemas/v1.1/%s.rng' % schema_name)
+ schema_doc = etree.parse(schema_path)
+ relaxng = etree.RelaxNG(schema_doc)
+ relaxng.assertValid(xml)
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index f7fd87bcd..a2bf267ed 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -166,7 +166,7 @@ class Controller(object):
return self.helper._get_server_admin_password_old_style(server)
-class ControllerV11(object):
+class ControllerV11(Controller):
"""Controller for 1.1 Zone resources."""
def _get_server_admin_password(self, server):
diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template
index d05c099d7..978ffb210 100644
--- a/nova/auth/novarc.template
+++ b/nova/auth/novarc.template
@@ -16,3 +16,4 @@ export NOVA_API_KEY="%(access)s"
export NOVA_USERNAME="%(user)s"
export NOVA_PROJECT_ID="%(project)s"
export NOVA_URL="%(os)s"
+export NOVA_VERSION="1.1"
diff --git a/nova/block_device.py b/nova/block_device.py
new file mode 100644
index 000000000..8d95e0029
--- /dev/null
+++ b/nova/block_device.py
@@ -0,0 +1,71 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata <yamahata@valinux co jp>
+# 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.
+
+import re
+
+
+def properties_root_device_name(properties):
+ """get root device name from image meta data.
+ If it isn't specified, return None.
+ """
+ root_device_name = None
+
+ # NOTE(yamahata): see image_service.s3.s3create()
+ for bdm in properties.get('mappings', []):
+ if bdm['virtual'] == 'root':
+ root_device_name = bdm['device']
+
+ # NOTE(yamahata): register_image's command line can override
+ # <machine>.manifest.xml
+ if 'root_device_name' in properties:
+ root_device_name = properties['root_device_name']
+
+ return root_device_name
+
+
+_ephemeral = re.compile('^ephemeral(\d|[1-9]\d+)$')
+
+
+def is_ephemeral(device_name):
+ return _ephemeral.match(device_name)
+
+
+def ephemeral_num(ephemeral_name):
+ assert is_ephemeral(ephemeral_name)
+ return int(_ephemeral.sub('\\1', ephemeral_name))
+
+
+def is_swap_or_ephemeral(device_name):
+ return device_name == 'swap' or is_ephemeral(device_name)
+
+
+def mappings_prepend_dev(mappings):
+ """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type"""
+ for m in mappings:
+ virtual = m['virtual']
+ if (is_swap_or_ephemeral(virtual) and
+ (not m['device'].startswith('/'))):
+ m['device'] = '/dev/' + m['device']
+ return mappings
+
+
+_dev = re.compile('^/dev/')
+
+
+def strip_dev(device_name):
+ """remove leading '/dev/'"""
+ return _dev.sub('', device_name)
diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py
index 521525205..2c4673f9e 100644
--- a/nova/cloudpipe/pipelib.py
+++ b/nova/cloudpipe/pipelib.py
@@ -141,15 +141,12 @@ class CloudPipe(object):
try:
result = cloud._gen_key(context, context.user_id, key_name)
private_key = result['private_key']
- try:
- key_dir = os.path.join(FLAGS.keys_path, context.user_id)
- if not os.path.exists(key_dir):
- os.makedirs(key_dir)
- key_path = os.path.join(key_dir, '%s.pem' % key_name)
- with open(key_path, 'w') as f:
- f.write(private_key)
- except:
- pass
- except exception.Duplicate:
+ key_dir = os.path.join(FLAGS.keys_path, context.user_id)
+ if not os.path.exists(key_dir):
+ os.makedirs(key_dir)
+ key_path = os.path.join(key_dir, '%s.pem' % key_name)
+ with open(key_path, 'w') as f:
+ f.write(private_key)
+ except (exception.Duplicate, os.error, IOError):
pass
return key_name
diff --git a/nova/compute/api.py b/nova/compute/api.py
index fe62b50ce..91a0c93b2 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -19,9 +19,11 @@
"""Handles all requests relating to instances (guest vms)."""
import eventlet
+import novaclient
import re
import time
+from nova import block_device
from nova import db
from nova import exception
from nova import flags
@@ -32,7 +34,6 @@ from nova import quota
from nova import rpc
from nova import utils
from nova import volume
-from nova.api.ec2 import ec2utils
from nova.compute import instance_types
from nova.compute import power_state
from nova.compute.utils import terminate_volumes
@@ -222,7 +223,7 @@ class API(base.Base):
if reservation_id is None:
reservation_id = utils.generate_uid('r')
- root_device_name = ec2utils.properties_root_device_name(
+ root_device_name = block_device.properties_root_device_name(
image['properties'])
base_options = {
@@ -254,34 +255,64 @@ class API(base.Base):
return (num_instances, base_options, image)
- def _update_image_block_device_mapping(self, elevated_context, instance_id,
+ @staticmethod
+ def _ephemeral_size(instance_type, ephemeral_name):
+ num = block_device.ephemeral_num(ephemeral_name)
+
+ # TODO(yamahata): ephemeralN where N > 0
+ # Only ephemeral0 is allowed for now because InstanceTypes
+ # table only allows single local disk, local_gb.
+ # In order to enhance it, we need to add a new columns to
+ # instance_types table.
+ if num > 0:
+ return 0
+
+ return instance_type.get('local_gb')
+
+ def _update_image_block_device_mapping(self, elevated_context,
+ instance_type, instance_id,
mappings):
"""tell vm driver to create ephemeral/swap device at boot time by
updating BlockDeviceMapping
"""
- for bdm in ec2utils.mappings_prepend_dev(mappings):
+ instance_type = (instance_type or
+ instance_types.get_default_instance_type())
+
+ for bdm in block_device.mappings_prepend_dev(mappings):
LOG.debug(_("bdm %s"), bdm)
virtual_name = bdm['virtual']
if virtual_name == 'ami' or virtual_name == 'root':
continue
- assert (virtual_name == 'swap' or
- virtual_name.startswith('ephemeral'))
+ if not block_device.is_swap_or_ephemeral(virtual_name):
+ continue
+
+ size = 0
+ if virtual_name == 'swap':
+ size = instance_type.get('swap', 0)
+ elif block_device.is_ephemeral(virtual_name):
+ size = self._ephemeral_size(instance_type, virtual_name)
+
+ if size == 0:
+ continue
+
values = {
'instance_id': instance_id,
'device_name': bdm['device'],
- 'virtual_name': virtual_name, }
+ 'virtual_name': virtual_name,
+ 'volume_size': size}
self.db.block_device_mapping_update_or_create(elevated_context,
values)
- def _update_block_device_mapping(self, elevated_context, instance_id,
+ def _update_block_device_mapping(self, elevated_context,
+ instance_type, instance_id,
block_device_mapping):
"""tell vm driver to attach volume at boot time by updating
BlockDeviceMapping
"""
+ LOG.debug(_("block_device_mapping %s"), block_device_mapping)
for bdm in block_device_mapping:
- LOG.debug(_('bdm %s'), bdm)
assert 'device_name' in bdm
values = {'instance_id': instance_id}
@@ -290,10 +321,18 @@ class API(base.Base):
'no_device'):
values[key] = bdm.get(key)
+ virtual_name = bdm.get('virtual_name')
+ if (virtual_name is not None and
+ block_device.is_ephemeral(virtual_name)):
+ size = self._ephemeral_size(instance_type, virtual_name)
+ if size == 0:
+ continue
+ values['volume_size'] = size
+
# NOTE(yamahata): NoDevice eliminates devices defined in image
# files by command line option.
# (--block-device-mapping)
- if bdm.get('virtual_name') == 'NoDevice':
+ if virtual_name == 'NoDevice':
values['no_device'] = True
for k in ('delete_on_termination', 'volume_id',
'snapshot_id', 'volume_id', 'volume_size',
@@ -303,8 +342,8 @@ class API(base.Base):
self.db.block_device_mapping_update_or_create(elevated_context,
values)
- def create_db_entry_for_new_instance(self, context, image, base_options,
- security_group, block_device_mapping, num=1):
+ def create_db_entry_for_new_instance(self, context, instance_type, image,
+ base_options, security_group, block_device_mapping, num=1):
"""Create an entry in the DB for this new instance,
including any related table updates (such as security group,
etc).
@@ -337,12 +376,12 @@ class API(base.Base):
security_group_id)
# BlockDeviceMapping table
- self._update_image_block_device_mapping(elevated, instance_id,
- image['properties'].get('mappings', []))
- self._update_block_device_mapping(elevated, instance_id,
+ self._update_image_block_device_mapping(elevated, instance_type,
+ instance_id, image['properties'].get('mappings', []))
+ self._update_block_device_mapping(elevated, instance_type, instance_id,
image['properties'].get('block_device_mapping', []))
# override via command line option
- self._update_block_device_mapping(elevated, instance_id,
+ self._update_block_device_mapping(elevated, instance_type, instance_id,
block_device_mapping)
# Set sane defaults if not specified
@@ -364,6 +403,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
instance_id=None, num_instances=1):
"""Send the run_instance request to the schedulers for processing."""
pid = context.project_id
@@ -377,6 +417,7 @@ class API(base.Base):
filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter'
request_spec = {
+ 'image': image,
'instance_properties': base_options,
'instance_type': instance_type,
'filter': filter_class,
@@ -423,6 +464,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
num_instances=num_instances)
return base_options['reservation_id']
@@ -464,7 +506,8 @@ class API(base.Base):
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
for num in range(num_instances):
- instance = self.create_db_entry_for_new_instance(context, image,
+ instance = self.create_db_entry_for_new_instance(context,
+ instance_type, image,
base_options, security_group,
block_device_mapping, num=num)
instances.append(instance)
@@ -474,6 +517,7 @@ class API(base.Base):
instance_type, zone_blob,
availability_zone, injected_files,
admin_password,
+ image,
instance_id=instance_id)
return [dict(x.iteritems()) for x in instances]
@@ -680,59 +724,84 @@ class API(base.Base):
"""
return self.get(context, instance_id)
- def get_all(self, context, project_id=None, reservation_id=None,
- fixed_ip=None, recurse_zones=False):
+ def get_all(self, context, search_opts=None):
"""Get all instances filtered by one of the given parameters.
If there is no filter and the context is an admin, it will retreive
all instances in the system.
"""
- if reservation_id is not None:
- recurse_zones = True
- instances = self.db.instance_get_all_by_reservation(
- context, reservation_id)
- elif fixed_ip is not None:
+ if search_opts is None:
+ search_opts = {}
+
+ LOG.debug(_("Searching by: %s") % str(search_opts))
+
+ # Fixups for the DB call
+ filters = {}
+
+ def _remap_flavor_filter(flavor_id):
+ instance_type = self.db.instance_type_get_by_flavor_id(
+ context, flavor_id)
+ filters['instance_type_id'] = instance_type['id']
+
+ def _remap_fixed_ip_filter(fixed_ip):
+ # Turn fixed_ip into a regexp match. Since '.' matches
+ # any character, we need to use regexp escaping for it.
+ filters['ip'] = '^%s$' % fixed_ip.replace('.', '\\.')
+
+ # search_option to filter_name mapping.
+ filter_mapping = {
+ 'image': 'image_ref',
+ 'name': 'display_name',
+ 'instance_name': 'name',
+ 'recurse_zones': None,
+ 'flavor': _remap_flavor_filter,
+ 'fixed_ip': _remap_fixed_ip_filter}
+
+ # copy from search_opts, doing various remappings as necessary
+ for opt, value in search_opts.iteritems():
+ # Do remappings.
+ # Values not in the filter_mapping table are copied as-is.
+ # If remapping is None, option is not copied
+ # If the remapping is a string, it is the filter_name to use
try:
- instances = self.db.fixed_ip_get_instance(context, fixed_ip)
- except exception.FloatingIpNotFound, e:
- if not recurse_zones:
- raise
- instances = None
- elif project_id or not context.is_admin:
- if not context.project_id:
- instances = self.db.instance_get_all_by_user(
- context, context.user_id)
+ remap_object = filter_mapping[opt]
+ except KeyError:
+ filters[opt] = value
else:
- if project_id is None:
- project_id = context.project_id
- instances = self.db.instance_get_all_by_project(
- context, project_id)
- else:
- instances = self.db.instance_get_all(context)
+ if remap_object:
+ if isinstance(remap_object, basestring):
+ filters[remap_object] = value
+ else:
+ remap_object(value)
+
+ recurse_zones = search_opts.get('recurse_zones', False)
+ if 'reservation_id' in filters:
+ recurse_zones = True
- if instances is None:
- instances = []
- elif not isinstance(instances, list):
- instances = [instances]
+ instances = self.db.instance_get_all_by_filters(context, filters)
if not recurse_zones:
return instances
+ # Recurse zones. Need admin context for this. Send along
+ # the un-modified search options we received..
admin_context = context.elevated()
children = scheduler_api.call_zone_method(admin_context,
"list",
+ errors_to_ignore=[novaclient.exceptions.NotFound],
novaclient_collection_name="servers",
- reservation_id=reservation_id,
- project_id=project_id,
- fixed_ip=fixed_ip,
- recurse_zones=True)
+ search_opts=search_opts)
for zone, servers in children:
+ # 'servers' can be None if a 404 was returned by a zone
+ if servers is None:
+ continue
for server in servers:
# Results are ready to send to user. No need to scrub.
server._info['_is_precooked'] = True
instances.append(server._info)
+
return instances
def _cast_compute_message(self, method, context, instance_id, host=None,
@@ -899,7 +968,7 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('revert_resize', context,
instance_ref['uuid'],
- migration_ref['source_compute'],
+ migration_ref['dest_compute'],
params=params)
self.db.migration_update(context, migration_ref['id'],
@@ -919,7 +988,7 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('confirm_resize', context,
instance_ref['uuid'],
- migration_ref['dest_compute'],
+ migration_ref['source_compute'],
params=params)
self.db.migration_update(context, migration_ref['id'],
@@ -966,7 +1035,7 @@ class API(base.Base):
{"method": "prep_resize",
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_ref['uuid'],
- "flavor_id": new_instance_type['id']}})
+ "instance_type_id": new_instance_type['id']}})
@scheduler_api.reroute_compute("add_fixed_ip")
def add_fixed_ip(self, context, instance_id, network_id):
@@ -1004,7 +1073,12 @@ class API(base.Base):
def set_host_enabled(self, context, host, enabled):
"""Sets the specified host's ability to accept new instances."""
return self._call_compute_message("set_host_enabled", context,
- instance_id=None, host=host, params={"enabled": enabled})
+ host=host, params={"enabled": enabled})
+
+ def host_power_action(self, context, host, action):
+ """Reboots, shuts down or powers up the host."""
+ return self._call_compute_message("host_power_action", context,
+ host=host, params={"action": action})
@scheduler_api.reroute_compute("diagnostics")
def get_diagnostics(self, context, instance_id):
@@ -1177,11 +1251,20 @@ class API(base.Base):
"""Delete the given metadata item from an instance."""
self.db.instance_metadata_delete(context, instance_id, key)
- def update_or_create_instance_metadata(self, context, instance_id,
- metadata):
- """Updates or creates instance metadata."""
- combined_metadata = self.get_instance_metadata(context, instance_id)
- combined_metadata.update(metadata)
- self._check_metadata_properties_quota(context, combined_metadata)
- self.db.instance_metadata_update_or_create(context, instance_id,
- metadata)
+ def update_instance_metadata(self, context, instance_id,
+ metadata, delete=False):
+ """Updates or creates instance metadata.
+
+ If delete is True, metadata items that are not specified in the
+ `metadata` argument will be deleted.
+
+ """
+ if delete:
+ _metadata = metadata
+ else:
+ _metadata = self.get_instance_metadata(context, instance_id)
+ _metadata.update(metadata)
+
+ self._check_metadata_properties_quota(context, _metadata)
+ self.db.instance_metadata_update(context, instance_id, _metadata, True)
+ return _metadata
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index a2d84cd76..d38213083 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -44,6 +44,8 @@ import functools
from eventlet import greenthread
+import nova.context
+from nova import block_device
from nova import exception
from nova import flags
import nova.image
@@ -147,6 +149,31 @@ class ComputeManager(manager.SchedulerDependentManager):
def init_host(self):
"""Initialization for a standalone compute service."""
self.driver.init_host(host=self.host)
+ context = nova.context.get_admin_context()
+ instances = self.db.instance_get_all_by_host(context, self.host)
+ for instance in instances:
+ inst_name = instance['name']
+ db_state = instance['state']
+ drv_state = self._update_state(context, instance['id'])
+
+ expect_running = db_state == power_state.RUNNING \
+ and drv_state != db_state
+
+ LOG.debug(_('Current state of %(inst_name)s is %(drv_state)s, '
+ 'state in DB is %(db_state)s.'), locals())
+
+ if (expect_running and FLAGS.resume_guests_state_on_host_boot)\
+ or FLAGS.start_guests_on_host_boot:
+ LOG.info(_('Rebooting instance %(inst_name)s after '
+ 'nova-compute restart.'), locals())
+ self.reboot_instance(context, instance['id'])
+ elif drv_state == power_state.RUNNING:
+ # Hyper-V and VMWareAPI drivers will raise and exception
+ try:
+ self.driver.ensure_filtering_rules_for_instance(instance)
+ except NotImplementedError:
+ LOG.warning(_('Hypervisor driver does not '
+ 'support firewall rules'))
def _update_state(self, context, instance_id, state=None):
"""Update the state of an instance from the driver info."""
@@ -154,6 +181,7 @@ class ComputeManager(manager.SchedulerDependentManager):
if state is None:
try:
+ LOG.debug(_('Checking state of %s'), instance_ref['name'])
info = self.driver.get_info(instance_ref['name'])
except exception.NotFound:
info = None
@@ -164,6 +192,7 @@ class ComputeManager(manager.SchedulerDependentManager):
state = power_state.FAILED
self.db.instance_set_state(context, instance_id, state)
+ return state
def _update_launched_at(self, context, instance_id, launched_at=None):
"""Update the launched_at parameter of the given instance."""
@@ -232,6 +261,8 @@ class ComputeManager(manager.SchedulerDependentManager):
volume_api = volume.API()
block_device_mapping = []
+ swap = None
+ ephemerals = []
for bdm in self.db.block_device_mapping_get_all_by_instance(
context, instance_id):
LOG.debug(_("setting up bdm %s"), bdm)
@@ -239,11 +270,18 @@ class ComputeManager(manager.SchedulerDependentManager):
if bdm['no_device']:
continue
if bdm['virtual_name']:
- # TODO(yamahata):
- # block devices for swap and ephemeralN will be
- # created by virt driver locally in compute node.
- assert (bdm['virtual_name'] == 'swap' or
- bdm['virtual_name'].startswith('ephemeral'))
+ virtual_name = bdm['virtual_name']
+ device_name = bdm['device_name']
+ assert block_device.is_swap_or_ephemeral(virtual_name)
+ if virtual_name == 'swap':
+ swap = {'device_name': device_name,
+ 'swap_size': bdm['volume_size']}
+ elif block_device.is_ephemeral(virtual_name):
+ eph = {'num': block_device.ephemeral_num(virtual_name),
+ 'virtual_name': virtual_name,
+ 'device_name': device_name,
+ 'size': bdm['volume_size']}
+ ephemerals.append(eph)
continue
if ((bdm['snapshot_id'] is not None) and
@@ -279,7 +317,7 @@ class ComputeManager(manager.SchedulerDependentManager):
'mount_device':
bdm['device_name']})
- return block_device_mapping
+ return (swap, ephemerals, block_device_mapping)
def _run_instance(self, context, instance_id, **kwargs):
"""Launch a new instance with specified options."""
@@ -320,13 +358,21 @@ class ComputeManager(manager.SchedulerDependentManager):
# all vif creation and network injection, maybe this is correct
network_info = []
- bd_mapping = self._setup_block_device_mapping(context, instance_id)
+ (swap, ephemerals,
+ block_device_mapping) = self._setup_block_device_mapping(
+ context, instance_id)
+ block_device_info = {
+ 'root_device_name': instance['root_device_name'],
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
# TODO(vish) check to make sure the availability zone matches
self._update_state(context, instance_id, power_state.BUILDING)
try:
- self.driver.spawn(instance, network_info, bd_mapping)
+ self.driver.spawn(context, instance,
+ network_info, block_device_info)
except Exception as ex: # pylint: disable=W0702
msg = _("Instance '%(instance_id)s' failed to spawn. Is "
"virtualization enabled in the BIOS? Details: "
@@ -433,7 +479,7 @@ 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)
- self.driver.spawn(instance_ref, network_info, bd_mapping)
+ self.driver.spawn(context, instance_ref, network_info, bd_mapping)
self._update_image_ref(context, instance_id, image_ref)
self._update_launched_at(context, instance_id)
@@ -501,7 +547,7 @@ class ComputeManager(manager.SchedulerDependentManager):
'instance: %(instance_id)s (state: %(state)s '
'expected: %(running)s)') % locals())
- self.driver.snapshot(instance_ref, image_id)
+ self.driver.snapshot(context, instance_ref, image_id)
if image_type == 'snapshot':
if rotation:
@@ -660,7 +706,7 @@ class ComputeManager(manager.SchedulerDependentManager):
_update_state = lambda result: self._update_state_callback(
self, context, instance_id, result)
network_info = self._get_instance_nw_info(context, instance_ref)
- self.driver.rescue(instance_ref, _update_state, network_info)
+ self.driver.rescue(context, instance_ref, _update_state, network_info)
self._update_state(context, instance_id)
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@@ -720,7 +766,8 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref['host'])
rpc.cast(context, topic,
{'method': 'finish_revert_resize',
- 'args': {'migration_id': migration_ref['id']},
+ 'args': {'instance_id': instance_ref['uuid'],
+ 'migration_id': migration_ref['id']},
})
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@@ -736,8 +783,8 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref = self.db.instance_get_by_uuid(context,
migration_ref.instance_uuid)
- instance_type = self.db.instance_type_get_by_flavor_id(context,
- migration_ref['old_flavor_id'])
+ instance_type = self.db.instance_type_get(context,
+ migration_ref['old_instance_type_id'])
# Just roll back the record. There's no need to resize down since
# the 'old' VM already has the preferred attributes
@@ -758,7 +805,7 @@ class ComputeManager(manager.SchedulerDependentManager):
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@checks_instance_lock
- def prep_resize(self, context, instance_id, flavor_id):
+ def prep_resize(self, context, instance_id, instance_type_id):
"""Initiates the process of moving a running instance to another host.
Possibly changes the RAM and disk size in the process.
@@ -777,16 +824,16 @@ class ComputeManager(manager.SchedulerDependentManager):
old_instance_type = self.db.instance_type_get(context,
instance_ref['instance_type_id'])
- new_instance_type = self.db.instance_type_get_by_flavor_id(context,
- flavor_id)
+ new_instance_type = self.db.instance_type_get(context,
+ instance_type_id)
migration_ref = self.db.migration_create(context,
{'instance_uuid': instance_ref['uuid'],
'source_compute': instance_ref['host'],
'dest_compute': FLAGS.host,
'dest_host': self.driver.get_host_ip_addr(),
- 'old_flavor_id': old_instance_type['flavorid'],
- 'new_flavor_id': flavor_id,
+ 'old_instance_type_id': old_instance_type['id'],
+ 'new_instance_type_id': instance_type_id,
'status': 'pre-migrating'})
LOG.audit(_('instance %s: migrating'), instance_ref['uuid'],
@@ -849,9 +896,10 @@ class ComputeManager(manager.SchedulerDependentManager):
resize_instance = False
instance_ref = self.db.instance_get_by_uuid(context,
migration_ref.instance_uuid)
- if migration_ref['old_flavor_id'] != migration_ref['new_flavor_id']:
- instance_type = self.db.instance_type_get_by_flavor_id(context,
- migration_ref['new_flavor_id'])
+ if migration_ref['old_instance_type_id'] != \
+ migration_ref['new_instance_type_id']:
+ instance_type = self.db.instance_type_get(context,
+ migration_ref['new_instance_type_id'])
self.db.instance_update(context, instance_ref.uuid,
dict(instance_type_id=instance_type['id'],
memory_mb=instance_type['memory_mb'],
@@ -863,8 +911,8 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_ref.uuid)
network_info = self._get_instance_nw_info(context, instance_ref)
- self.driver.finish_migration(instance_ref, disk_info, network_info,
- resize_instance)
+ self.driver.finish_migration(context, instance_ref, disk_info,
+ network_info, resize_instance)
self.db.migration_update(context, migration_id,
{'status': 'finished', })
@@ -928,8 +976,12 @@ class ComputeManager(manager.SchedulerDependentManager):
result))
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
- def set_host_enabled(self, context, instance_id=None, host=None,
- enabled=None):
+ def host_power_action(self, context, host=None, action=None):
+ """Reboots, shuts down or powers up the host."""
+ return self.driver.host_power_action(host, action)
+
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
+ def set_host_enabled(self, context, host=None, enabled=None):
"""Sets the specified host's ability to accept new instances."""
return self.driver.set_host_enabled(host, enabled)
diff --git a/nova/db/api.py b/nova/db/api.py
index 47308bdba..3d0727f8b 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -387,15 +387,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id):
return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id)
-def fixed_ip_get_instance(context, address):
- """Get an instance for a fixed ip by address."""
- return IMPL.fixed_ip_get_instance(context, address)
-
-
-def fixed_ip_get_instance_v6(context, address):
- return IMPL.fixed_ip_get_instance_v6(context, address)
-
-
def fixed_ip_get_network(context, address):
"""Get a network for a fixed ip by address."""
return IMPL.fixed_ip_get_network(context, address)
@@ -500,6 +491,11 @@ def instance_get_all(context):
return IMPL.instance_get_all(context)
+def instance_get_all_by_filters(context, filters):
+ """Get all instances that match all filters."""
+ return IMPL.instance_get_all_by_filters(context, filters)
+
+
def instance_get_active_by_window(context, begin, end=None):
"""Get instances active during a certain time window."""
return IMPL.instance_get_active_by_window(context, begin, end)
@@ -521,10 +517,20 @@ def instance_get_all_by_host(context, host):
def instance_get_all_by_reservation(context, reservation_id):
- """Get all instance belonging to a reservation."""
+ """Get all instances belonging to a reservation."""
return IMPL.instance_get_all_by_reservation(context, reservation_id)
+def instance_get_by_fixed_ip(context, address):
+ """Get an instance for a fixed ip by address."""
+ return IMPL.instance_get_by_fixed_ip(context, address)
+
+
+def instance_get_by_fixed_ipv6(context, address):
+ """Get an instance for a fixed ip by IPv6 address."""
+ return IMPL.instance_get_by_fixed_ipv6(context, address)
+
+
def instance_get_fixed_addresses(context, instance_id):
"""Get the fixed ip address of an instance."""
return IMPL.instance_get_fixed_addresses(context, instance_id)
@@ -1381,9 +1387,9 @@ def instance_metadata_delete(context, instance_id, key):
IMPL.instance_metadata_delete(context, instance_id, key)
-def instance_metadata_update_or_create(context, instance_id, metadata):
- """Create or update instance metadata."""
- IMPL.instance_metadata_update_or_create(context, instance_id, metadata)
+def instance_metadata_update(context, instance_id, metadata, delete):
+ """Update metadata if it exists, otherwise create it."""
+ IMPL.instance_metadata_update(context, instance_id, metadata, delete)
####################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 60220cd68..1da54b3da 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -18,8 +18,10 @@
"""
Implementation of SQLAlchemy backend.
"""
+import re
import warnings
+from nova import block_device
from nova import db
from nova import exception
from nova import flags
@@ -828,28 +830,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id):
return rv
-@require_context
-def fixed_ip_get_instance(context, address):
- fixed_ip_ref = fixed_ip_get_by_address(context, address)
- return fixed_ip_ref.instance
-
-
-@require_context
-def fixed_ip_get_instance_v6(context, address):
- session = get_session()
-
- # convert IPv6 address to mac
- mac = ipv6.to_mac(address)
-
- # get virtual interface
- vif_ref = virtual_interface_get_by_address(context, mac)
-
- # look up instance based on instance_id from vif row
- result = session.query(models.Instance).\
- filter_by(id=vif_ref['instance_id'])
- return result
-
-
@require_admin_context
def fixed_ip_get_network(context, address):
fixed_ip_ref = fixed_ip_get_by_address(context, address)
@@ -1168,6 +1148,114 @@ def instance_get_all(context):
all()
+@require_context
+def instance_get_all_by_filters(context, filters):
+ """Return instances that match all filters. Deleted instances
+ will be returned by default, unless there's a filter that says
+ otherwise"""
+
+ def _regexp_filter_by_ipv6(instance, filter_re):
+ for interface in instance['virtual_interfaces']:
+ fixed_ipv6 = interface.get('fixed_ipv6')
+ if fixed_ipv6 and filter_re.match(fixed_ipv6):
+ return True
+ return False
+
+ def _regexp_filter_by_ip(instance, filter_re):
+ for interface in instance['virtual_interfaces']:
+ for fixed_ip in interface['fixed_ips']:
+ if not fixed_ip or not fixed_ip['address']:
+ continue
+ if filter_re.match(fixed_ip['address']):
+ return True
+ for floating_ip in fixed_ip.get('floating_ips', []):
+ if not floating_ip or not floating_ip['address']:
+ continue
+ if filter_re.match(floating_ip['address']):
+ return True
+ return False
+
+ def _regexp_filter_by_column(instance, filter_name, filter_re):
+ try:
+ v = getattr(instance, filter_name)
+ except AttributeError:
+ return True
+ if v and filter_re.match(str(v)):
+ return True
+ return False
+
+ def _exact_match_filter(query, column, value):
+ """Do exact match against a column. value to match can be a list
+ so you can match any value in the list.
+ """
+ if isinstance(value, list):
+ column_attr = getattr(models.Instance, column)
+ return query.filter(column_attr.in_(value))
+ else:
+ filter_dict = {}
+ filter_dict[column] = value
+ return query.filter_by(**filter_dict)
+
+ session = get_session()
+ query_prefix = session.query(models.Instance).\
+ options(joinedload_all('fixed_ips.floating_ips')).\
+ options(joinedload_all('virtual_interfaces.network')).\
+ options(joinedload_all(
+ 'virtual_interfaces.fixed_ips.floating_ips')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
+ options(joinedload('instance_type'))
+
+ # Make a copy of the filters dictionary to use going forward, as we'll
+ # be modifying it and we shouldn't affect the caller's use of it.
+ filters = filters.copy()
+
+ if not context.is_admin:
+ # If we're not admin context, add appropriate filter..
+ if context.project_id:
+ filters['project_id'] = context.project_id
+ else:
+ filters['user_id'] = context.user_id
+
+ # Filters for exact matches that we can do along with the SQL query...
+ # For other filters that don't match this, we will do regexp matching
+ exact_match_filter_names = ['project_id', 'user_id', 'image_ref',
+ 'state', 'instance_type_id', 'deleted']
+
+ query_filters = [key for key in filters.iterkeys()
+ if key in exact_match_filter_names]
+
+ for filter_name in query_filters:
+ # Do the matching and remove the filter from the dictionary
+ # so we don't try it again below..
+ query_prefix = _exact_match_filter(query_prefix, filter_name,
+ filters.pop(filter_name))
+
+ instances = query_prefix.all()
+
+ if not instances:
+ return []
+
+ # Now filter on everything else for regexp matching..
+ # For filters not in the list, we'll attempt to use the filter_name
+ # as a column name in Instance..
+ regexp_filter_funcs = {'ip6': _regexp_filter_by_ipv6,
+ 'ip': _regexp_filter_by_ip}
+
+ for filter_name in filters.iterkeys():
+ filter_func = regexp_filter_funcs.get(filter_name, None)
+ filter_re = re.compile(str(filters[filter_name]))
+ if filter_func:
+ filter_l = lambda instance: filter_func(instance, filter_re)
+ else:
+ filter_l = lambda instance: _regexp_filter_by_column(instance,
+ filter_name, filter_re)
+ instances = filter(filter_l, instances)
+
+ return instances
+
+
@require_admin_context
def instance_get_active_by_window(context, begin, end=None):
"""Return instances that were continuously active over the given window"""
@@ -1236,30 +1324,48 @@ def instance_get_all_by_project(context, project_id):
@require_context
def instance_get_all_by_reservation(context, reservation_id):
session = get_session()
+ query = session.query(models.Instance).\
+ filter_by(reservation_id=reservation_id).\
+ options(joinedload_all('fixed_ips.floating_ips')).\
+ options(joinedload('virtual_interfaces')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
+ options(joinedload('instance_type'))
if is_admin_context(context):
- return session.query(models.Instance).\
- options(joinedload_all('fixed_ips.floating_ips')).\
- options(joinedload('virtual_interfaces')).\
- options(joinedload('security_groups')).\
- options(joinedload_all('fixed_ips.network')).\
- options(joinedload('metadata')).\
- options(joinedload('instance_type')).\
- filter_by(reservation_id=reservation_id).\
- filter_by(deleted=can_read_deleted(context)).\
- all()
+ return query.\
+ filter_by(deleted=can_read_deleted(context)).\
+ all()
elif is_user_context(context):
- return session.query(models.Instance).\
- options(joinedload_all('fixed_ips.floating_ips')).\
- options(joinedload('virtual_interfaces')).\
- options(joinedload('security_groups')).\
- options(joinedload_all('fixed_ips.network')).\
- options(joinedload('metadata')).\
- options(joinedload('instance_type')).\
- filter_by(project_id=context.project_id).\
- filter_by(reservation_id=reservation_id).\
- filter_by(deleted=False).\
- all()
+ return query.\
+ filter_by(project_id=context.project_id).\
+ filter_by(deleted=False).\
+ all()
+
+
+@require_context
+def instance_get_by_fixed_ip(context, address):
+ """Return instance ref by exact match of FixedIP"""
+ fixed_ip_ref = fixed_ip_get_by_address(context, address)
+ return fixed_ip_ref.instance
+
+
+@require_context
+def instance_get_by_fixed_ipv6(context, address):
+ """Return instance ref by exact match of IPv6"""
+ session = get_session()
+
+ # convert IPv6 address to mac
+ mac = ipv6.to_mac(address)
+
+ # get virtual interface
+ vif_ref = virtual_interface_get_by_address(context, mac)
+
+ # look up instance based on instance_id from vif row
+ result = session.query(models.Instance).\
+ filter_by(id=vif_ref['instance_id'])
+ return result
@require_admin_context
@@ -1301,7 +1407,7 @@ def instance_get_fixed_addresses_v6(context, instance_id):
network_refs = network_get_all_by_instance(context, instance_id)
# compile a list of cidr_v6 prefixes sorted by network id
prefixes = [ref.cidr_v6 for ref in
- sorted(network_refs, key=lambda ref: ref.id)]
+ sorted(network_refs, key=lambda ref: ref.id)]
# get vifs associated with instance
vif_refs = virtual_interface_get_by_instance(context, instance_ref.id)
# compile list of the mac_addresses for vifs sorted by network id
@@ -1345,9 +1451,10 @@ def instance_update(context, instance_id, values):
session = get_session()
metadata = values.get('metadata')
if metadata is not None:
- instance_metadata_delete_all(context, instance_id)
- instance_metadata_update_or_create(context, instance_id,
- values.pop('metadata'))
+ instance_metadata_update(context,
+ instance_id,
+ values.pop('metadata'),
+ delete=True)
with session.begin():
if utils.is_uuid_like(instance_id):
instance_ref = instance_get_by_uuid(context, instance_id,
@@ -1426,9 +1533,14 @@ def instance_action_create(context, values):
def instance_get_actions(context, instance_id):
"""Return the actions associated to the given instance id"""
session = get_session()
+
+ if utils.is_uuid_like(instance_id):
+ instance = instance_get_by_uuid(context, instance_id, session)
+ instance_id = instance.id
+
return session.query(models.InstanceActions).\
filter_by(instance_id=instance_id).\
- all()
+ all()
###################
@@ -1680,7 +1792,10 @@ def network_get_by_bridge(context, bridge):
def network_get_by_cidr(context, cidr):
session = get_session()
result = session.query(models.Network).\
- filter_by(cidr=cidr).first()
+ filter(or_(models.Network.cidr == cidr,
+ models.Network.cidr_v6 == cidr)).\
+ filter_by(deleted=False).\
+ first()
if not result:
raise exception.NetworkNotFoundForCidr(cidr=cidr)
@@ -2264,6 +2379,20 @@ def block_device_mapping_update_or_create(context, values):
else:
result.update(values)
+ # NOTE(yamahata): same virtual device name can be specified multiple
+ # times. So delete the existing ones.
+ virtual_name = values['virtual_name']
+ if (virtual_name is not None and
+ block_device.is_swap_or_ephemeral(virtual_name)):
+ session.query(models.BlockDeviceMapping).\
+ filter_by(instance_id=values['instance_id']).\
+ filter_by(virtual_name=virtual_name).\
+ filter(models.BlockDeviceMapping.device_name !=
+ values['device_name']).\
+ update({'deleted': True,
+ 'deleted_at': utils.utcnow(),
+ 'updated_at': literal_column('updated_at')})
+
@require_context
def block_device_mapping_get_all_by_instance(context, instance_id):
@@ -3195,21 +3324,35 @@ def instance_metadata_get_item(context, instance_id, key, session=None):
@require_context
@require_instance_exists
-def instance_metadata_update_or_create(context, instance_id, metadata):
+def instance_metadata_update(context, instance_id, metadata, delete):
session = get_session()
- original_metadata = instance_metadata_get(context, instance_id)
+ # Set existing metadata to deleted if delete argument is True
+ if delete:
+ original_metadata = instance_metadata_get(context, instance_id)
+ for meta_key, meta_value in original_metadata.iteritems():
+ if meta_key not in metadata:
+ meta_ref = instance_metadata_get_item(context, instance_id,
+ meta_key, session)
+ meta_ref.update({'deleted': True})
+ meta_ref.save(session=session)
meta_ref = None
- for key, value in metadata.iteritems():
+
+ # Now update all existing items with new values, or create new meta objects
+ for meta_key, meta_value in metadata.iteritems():
+
+ # update the value whether it exists or not
+ item = {"value": meta_value}
+
try:
meta_ref = instance_metadata_get_item(
context, instance_id, key, session)
except exception.InstanceMetadataNotFound, e:
meta_ref = models.InstanceMetadata()
- meta_ref.update({"key": key, "value": value,
- "instance_id": instance_id,
- "deleted": False})
+ item.update({"key": meta_key, "instance_id": instance_id})
+
+ meta_ref.update(item)
meta_ref.save(session=session)
return metadata
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py b/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py
new file mode 100644
index 000000000..f3244033b
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/036_change_flavor_id_in_migrations.py
@@ -0,0 +1,72 @@
+# 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.from sqlalchemy import *
+
+from sqlalchemy import Column, Integer, MetaData, Table
+
+
+meta = MetaData()
+
+
+#
+# Tables to alter
+#
+#
+
+old_flavor_id = Column('old_flavor_id', Integer())
+new_flavor_id = Column('new_flavor_id', Integer())
+old_instance_type_id = Column('old_instance_type_id', Integer())
+new_instance_type_id = Column('new_instance_type_id', Integer())
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instance_types = Table('instance_types', meta, autoload=True)
+ migrations = Table('migrations', meta, autoload=True)
+ migrations.create_column(old_instance_type_id)
+ migrations.create_column(new_instance_type_id)
+
+ # Convert flavor_id to instance_type_id
+ for instance_type in migrate_engine.execute(instance_types.select()):
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.old_flavor_id == instance_type.flavorid)\
+ .values(old_instance_type_id=instance_type.id))
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.new_flavor_id == instance_type.flavorid)\
+ .values(new_instance_type_id=instance_type.id))
+
+ migrations.c.old_flavor_id.drop()
+ migrations.c.new_flavor_id.drop()
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instance_types = Table('instance_types', meta, autoload=True)
+ migrations = Table('migrations', meta, autoload=True)
+ migrations.create_column(old_flavor_id)
+ migrations.create_column(new_flavor_id)
+
+ # Convert instance_type_id to flavor_id
+ for instance_type in migrate_engine.execute(instance_types.select()):
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.old_instance_type_id == instance_type.id)\
+ .values(old_flavor_id=instance_type.flavorid))
+ migrate_engine.execute(migrations.update()\
+ .where(migrations.c.new_instance_type_id == instance_type.id)\
+ .values(new_flavor_id=instance_type.flavorid))
+
+ migrations.c.old_instance_type_id.drop()
+ migrations.c.new_instance_type_id.drop()
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 056259539..939fde199 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -180,6 +180,7 @@ class Instance(BASE, NovaBase):
image_ref = Column(String(255))
kernel_id = Column(String(255))
ramdisk_id = Column(String(255))
+ server_name = Column(String(255))
# image_ref = Column(Integer, ForeignKey('images.id'), nullable=True)
# kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True)
@@ -202,7 +203,7 @@ class Instance(BASE, NovaBase):
hostname = Column(String(255))
host = Column(String(255)) # , ForeignKey('hosts.id'))
- # aka flavor_id
+ # *not* flavor_id
instance_type_id = Column(Integer)
user_data = Column(Text)
@@ -511,8 +512,8 @@ class Migration(BASE, NovaBase):
source_compute = Column(String(255))
dest_compute = Column(String(255))
dest_host = Column(String(255))
- old_flavor_id = Column(Integer())
- new_flavor_id = Column(Integer())
+ old_instance_type_id = Column(Integer())
+ new_instance_type_id = Column(Integer())
instance_uuid = Column(String(255), ForeignKey('instances.uuid'),
nullable=True)
#TODO(_cerberus_): enum
diff --git a/nova/exception.py b/nova/exception.py
index 68e6ac937..0d60cb0bf 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -25,6 +25,7 @@ SHOULD include dedicated exception logging.
"""
from functools import wraps
+import sys
from nova import log as logging
@@ -96,6 +97,10 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None,
try:
return f(*args, **kw)
except Exception, e:
+ # Save exception since it can be clobbered during processing
+ # below before we can re-raise
+ exc_info = sys.exc_info()
+
if notifier:
payload = dict(args=args, exception=e)
payload.update(kw)
@@ -122,7 +127,9 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None,
LOG.exception(_('Uncaught exception'))
#logging.error(traceback.extract_stack(exc_traceback))
raise Error(str(e))
- raise
+
+ # re-raise original exception since it may have been clobbered
+ raise exc_info[0], exc_info[1], exc_info[2]
return wraps(f)(wrapped)
return inner
@@ -150,6 +157,10 @@ class NovaException(Exception):
return self._error_string
+class ImagePaginationFailed(NovaException):
+ message = _("Failed to paginate through images from image service")
+
+
class VirtualInterfaceCreateException(NovaException):
message = _("Virtual Interface creation failed")
@@ -198,6 +209,12 @@ class InvalidContentType(Invalid):
message = _("Invalid content type %(content_type)s.")
+# Cannot be templated as the error syntax varies.
+# msg needs to be constructed when raised.
+class InvalidParameterValue(Invalid):
+ message = _("%(err)s")
+
+
class InstanceNotRunning(Invalid):
message = _("Instance %(instance_id)s is not running.")
diff --git a/nova/flags.py b/nova/flags.py
index fa6d8860a..eb6366ed9 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -317,7 +317,7 @@ DEFINE_string('osapi_extensions_path', '/var/lib/nova/extensions',
DEFINE_string('osapi_host', '$my_ip', 'ip of api server')
DEFINE_string('osapi_scheme', 'http', 'prefix for openstack')
DEFINE_integer('osapi_port', 8774, 'OpenStack API port')
-DEFINE_string('osapi_path', '/v1.0/', 'suffix for openstack')
+DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack')
DEFINE_integer('osapi_max_limit', 1000,
'max number of items returned in a collection response')
@@ -387,3 +387,8 @@ DEFINE_list('zone_capabilities',
'Key/Multi-value list representng capabilities of this zone')
DEFINE_string('build_plan_encryption_key', None,
'128bit (hex) encryption key for scheduler build plans.')
+
+DEFINE_bool('start_guests_on_host_boot', False,
+ 'Whether to restart guests when the host reboots')
+DEFINE_bool('resume_guests_state_on_host_boot', False,
+ 'Whether to start guests, that was running before the host reboot')
diff --git a/nova/image/__init__.py b/nova/image/__init__.py
index a27d649d4..5447c8a3a 100644
--- a/nova/image/__init__.py
+++ b/nova/image/__init__.py
@@ -35,6 +35,7 @@ def _parse_image_ref(image_href):
:param image_href: href of an image
:returns: a tuple of the form (image_id, host, port)
+ :raises ValueError
"""
o = urlparse(image_href)
@@ -72,7 +73,7 @@ def get_glance_client(image_href):
try:
(image_id, host, port) = _parse_image_ref(image_href)
- except:
+ except ValueError:
raise exception.InvalidImageRef(image_href=image_href)
glance_client = GlanceClient(host, port)
return (glance_client, image_id)
diff --git a/nova/image/fake.py b/nova/image/fake.py
index 28e912534..97af81711 100644
--- a/nova/image/fake.py
+++ b/nova/image/fake.py
@@ -45,9 +45,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': False,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel,
'architecture': 'x86_64'}}
@@ -56,9 +59,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -66,9 +72,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -76,9 +85,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -86,9 +98,12 @@ class _FakeImageService(service.BaseImageService):
'name': 'fakeimage123456',
'created_at': timestamp,
'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
'status': 'active',
- 'container_format': 'ami',
- 'disk_format': 'raw',
+ 'is_public': True,
+# 'container_format': 'ami',
+# 'disk_format': 'raw',
'properties': {'kernel_id': FLAGS.null_kernel,
'ramdisk_id': FLAGS.null_kernel}}
@@ -101,7 +116,11 @@ class _FakeImageService(service.BaseImageService):
def index(self, context, filters=None, marker=None, limit=None):
"""Returns list of images."""
- return copy.deepcopy(self.images.values())
+ retval = []
+ for img in self.images.values():
+ retval += [dict([(k, v) for k, v in img.iteritems()
+ if k in ['id', 'name']])]
+ return retval
def detail(self, context, filters=None, marker=None, limit=None):
"""Return list of detailed image information."""
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 5c2dc957b..9060f6a91 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -19,7 +19,9 @@
from __future__ import absolute_import
+import copy
import datetime
+import json
import random
from glance.common import exception as glance_exception
@@ -83,43 +85,79 @@ class GlanceImageService(service.BaseImageService):
client = property(_get_client, _set_client)
- def index(self, context, filters=None, marker=None, limit=None):
+ def _set_client_context(self, context):
+ """Sets the client's auth token."""
+ self.client.set_auth_token(context.auth_token)
+
+ def index(self, context, **kwargs):
"""Calls out to Glance for a list of images available."""
- # NOTE(sirp): We need to use `get_images_detailed` and not
- # `get_images` here because we need `is_public` and `properties`
- # included so we can filter by user
- filtered = []
- filters = filters or {}
- if 'is_public' not in filters:
- # NOTE(vish): don't filter out private images
- filters['is_public'] = 'none'
- image_metas = self.client.get_images_detailed(filters=filters,
- marker=marker,
- limit=limit)
+ params = self._extract_query_params(kwargs)
+ image_metas = self._get_images(context, **params)
+
+ images = []
for image_meta in image_metas:
+ # NOTE(sirp): We need to use `get_images_detailed` and not
+ # `get_images` here because we need `is_public` and `properties`
+ # included so we can filter by user
if self._is_image_available(context, image_meta):
meta_subset = utils.subset_dict(image_meta, ('id', 'name'))
- filtered.append(meta_subset)
- return filtered
+ images.append(meta_subset)
+ return images
- def detail(self, context, filters=None, marker=None, limit=None):
+ def detail(self, context, **kwargs):
"""Calls out to Glance for a list of detailed image information."""
- filtered = []
- filters = filters or {}
- if 'is_public' not in filters:
- # NOTE(vish): don't filter out private images
- filters['is_public'] = 'none'
- image_metas = self.client.get_images_detailed(filters=filters,
- marker=marker,
- limit=limit)
+ params = self._extract_query_params(kwargs)
+ image_metas = self._get_images(context, **params)
+
+ images = []
for image_meta in image_metas:
if self._is_image_available(context, image_meta):
base_image_meta = self._translate_to_base(image_meta)
- filtered.append(base_image_meta)
- return filtered
+ images.append(base_image_meta)
+ return images
+
+ def _extract_query_params(self, params):
+ _params = {}
+ accepted_params = ('filters', 'marker', 'limit',
+ 'sort_key', 'sort_dir')
+ for param in accepted_params:
+ if param in params:
+ _params[param] = params.get(param)
+
+ return _params
+
+ def _get_images(self, context, **kwargs):
+ """Get image entitites from images service"""
+ self._set_client_context(context)
+
+ # ensure filters is a dict
+ kwargs['filters'] = kwargs.get('filters') or {}
+ # NOTE(vish): don't filter out private images
+ kwargs['filters'].setdefault('is_public', 'none')
+
+ return self._fetch_images(self.client.get_images_detailed, **kwargs)
+
+ def _fetch_images(self, fetch_func, **kwargs):
+ """Paginate through results from glance server"""
+ images = fetch_func(**kwargs)
+
+ for image in images:
+ yield image
+ else:
+ # break out of recursive loop to end pagination
+ return
+
+ try:
+ # attempt to advance the marker in order to fetch next page
+ kwargs['marker'] = images[-1]['id']
+ except KeyError:
+ raise exception.ImagePaginationFailed()
+
+ self._fetch_images(fetch_func, **kwargs)
def show(self, context, image_id):
"""Returns a dict with image data for the given opaque image id."""
+ self._set_client_context(context)
try:
image_meta = self.client.get_image_meta(image_id)
except glance_exception.NotFound:
@@ -143,6 +181,7 @@ class GlanceImageService(service.BaseImageService):
def get(self, context, image_id, data):
"""Calls out to Glance for metadata and data and writes data."""
+ self._set_client_context(context)
try:
image_meta, image_chunks = self.client.get_image(image_id)
except glance_exception.NotFound:
@@ -160,6 +199,7 @@ class GlanceImageService(service.BaseImageService):
:raises: AlreadyExists if the image already exist.
"""
+ self._set_client_context(context)
# Translate Base -> Service
LOG.debug(_('Creating image in Glance. Metadata passed in %s'),
image_meta)
@@ -182,8 +222,10 @@ class GlanceImageService(service.BaseImageService):
:raises: ImageNotFound if the image does not exist.
"""
+ self._set_client_context(context)
# NOTE(vish): show is to check if image is available
self.show(context, image_id)
+ image_meta = _convert_to_string(image_meta)
try:
image_meta = self.client.update_image(image_id, image_meta, data)
except glance_exception.NotFound:
@@ -198,6 +240,7 @@ class GlanceImageService(service.BaseImageService):
:raises: ImageNotFound if the image does not exist.
"""
+ self._set_client_context(context)
# NOTE(vish): show is to check if image is available
self.show(context, image_id)
try:
@@ -211,11 +254,19 @@ class GlanceImageService(service.BaseImageService):
pass
@classmethod
+ def _translate_to_service(cls, image_meta):
+ image_meta = super(GlanceImageService,
+ cls)._translate_to_service(image_meta)
+ image_meta = _convert_to_string(image_meta)
+ return image_meta
+
+ @classmethod
def _translate_to_base(cls, image_meta):
"""Override translation to handle conversion to datetime objects."""
image_meta = service.BaseImageService._propertify_metadata(
image_meta, cls.SERVICE_IMAGE_ATTRS)
image_meta = _convert_timestamps_to_datetimes(image_meta)
+ image_meta = _convert_from_string(image_meta)
return image_meta
@@ -241,3 +292,38 @@ def _parse_glance_iso8601_timestamp(timestamp):
raise ValueError(_('%(timestamp)s does not follow any of the '
'signatures: %(ISO_FORMATS)s') % locals())
+
+
+# TODO(yamahata): use block-device-mapping extension to glance
+def _json_loads(properties, attr):
+ prop = properties[attr]
+ if isinstance(prop, basestring):
+ properties[attr] = json.loads(prop)
+
+
+def _json_dumps(properties, attr):
+ prop = properties[attr]
+ if not isinstance(prop, basestring):
+ properties[attr] = json.dumps(prop)
+
+
+_CONVERT_PROPS = ('block_device_mapping', 'mappings')
+
+
+def _convert(method, metadata):
+ metadata = copy.deepcopy(metadata) # don't touch original metadata
+ properties = metadata.get('properties')
+ if properties:
+ for attr in _CONVERT_PROPS:
+ if attr in properties:
+ method(properties, attr)
+
+ return metadata
+
+
+def _convert_from_string(metadata):
+ return _convert(_json_loads, metadata)
+
+
+def _convert_to_string(metadata):
+ return _convert(_json_dumps, metadata)
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 4a3791d8a..8fc6a295f 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -410,8 +410,11 @@ class NetworkManager(manager.SchedulerDependentManager):
kwargs can contain fixed_ips to circumvent another db lookup
"""
instance_id = kwargs.pop('instance_id')
- fixed_ips = kwargs.get('fixed_ips') or \
+ try:
+ fixed_ips = kwargs.get('fixed_ips') or \
self.db.fixed_ip_get_by_instance(context, instance_id)
+ except exceptions.FixedIpNotFoundForInstance:
+ fixed_ips = []
LOG.debug(_("network deallocation for instance |%s|"), instance_id,
context=context)
# deallocate fixed ips
@@ -551,15 +554,17 @@ class NetworkManager(manager.SchedulerDependentManager):
# with a network, or a cluster of computes with a network
# and use that network here with a method like
# network_get_by_compute_host
- address = self.db.fixed_ip_associate_pool(context.elevated(),
- network['id'],
- instance_id)
- vif = self.db.virtual_interface_get_by_instance_and_network(context,
- instance_id,
- network['id'])
- values = {'allocated': True,
- 'virtual_interface_id': vif['id']}
- self.db.fixed_ip_update(context, address, values)
+ address = None
+ if network['cidr']:
+ address = self.db.fixed_ip_associate_pool(context.elevated(),
+ network['id'],
+ instance_id)
+ get_vif = self.db.virtual_interface_get_by_instance_and_network
+ vif = get_vif(context, instance_id, network['id'])
+ values = {'allocated': True,
+ 'virtual_interface_id': vif['id']}
+ self.db.fixed_ip_update(context, address, values)
+
self._setup_network(context, network)
return address
@@ -613,34 +618,39 @@ class NetworkManager(manager.SchedulerDependentManager):
network_size, cidr_v6, gateway_v6, bridge,
bridge_interface, dns1=None, dns2=None, **kwargs):
"""Create networks based on parameters."""
- fixed_net = netaddr.IPNetwork(cidr)
- if FLAGS.use_ipv6:
+ if cidr_v6:
fixed_net_v6 = netaddr.IPNetwork(cidr_v6)
significant_bits_v6 = 64
network_size_v6 = 1 << 64
- for index in range(num_networks):
- start = index * network_size
+ if cidr:
+ fixed_net = netaddr.IPNetwork(cidr)
significant_bits = 32 - int(math.log(network_size, 2))
- cidr = '%s/%s' % (fixed_net[start], significant_bits)
- project_net = netaddr.IPNetwork(cidr)
+
+ for index in range(num_networks):
net = {}
net['bridge'] = bridge
net['bridge_interface'] = bridge_interface
net['dns1'] = dns1
net['dns2'] = dns2
- net['cidr'] = cidr
- net['multi_host'] = multi_host
- net['netmask'] = str(project_net.netmask)
- net['gateway'] = str(project_net[1])
- net['broadcast'] = str(project_net.broadcast)
- net['dhcp_start'] = str(project_net[2])
+
+ if cidr:
+ start = index * network_size
+ project_net = netaddr.IPNetwork('%s/%s' % (fixed_net[start],
+ significant_bits))
+ net['cidr'] = str(project_net)
+ net['multi_host'] = multi_host
+ net['netmask'] = str(project_net.netmask)
+ net['gateway'] = str(project_net[1])
+ net['broadcast'] = str(project_net.broadcast)
+ net['dhcp_start'] = str(project_net[2])
+
if num_networks > 1:
net['label'] = '%s_%d' % (label, index)
else:
net['label'] = label
- if FLAGS.use_ipv6:
+ if cidr_v6:
start_v6 = index * network_size_v6
cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6],
significant_bits_v6)
@@ -673,11 +683,11 @@ class NetworkManager(manager.SchedulerDependentManager):
# None if network with cidr or cidr_v6 already exists
network = self.db.network_create_safe(context, net)
- if network:
+ if not network:
+ raise ValueError(_('Network already exists!'))
+
+ if network and cidr:
self._create_fixed_ips(context, network['id'])
- else:
- raise ValueError(_('Network with cidr %s already exists') %
- cidr)
@property
def _bottom_reserved_ips(self): # pylint: disable=R0201
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 137b671c0..55cea5f8f 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -17,7 +17,8 @@
Handles all requests relating to schedulers.
"""
-import novaclient
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
from nova import db
from nova import exception
@@ -112,7 +113,7 @@ def _wrap_method(function, self):
def _process(func, zone):
"""Worker stub for green thread pool. Give the worker
an authenticated nova client and zone info."""
- nova = novaclient.OpenStack(zone.username, zone.password, None,
+ nova = novaclient.Client(zone.username, zone.password, None,
zone.api_url)
nova.authenticate()
return func(nova, zone)
@@ -132,10 +133,10 @@ def call_zone_method(context, method_name, errors_to_ignore=None,
zones = db.zone_get_all(context)
for zone in zones:
try:
- nova = novaclient.OpenStack(zone.username, zone.password, None,
+ nova = novaclient.Client(zone.username, zone.password, None,
zone.api_url)
nova.authenticate()
- except novaclient.exceptions.BadRequest, e:
+ except novaclient_exceptions.BadRequest, e:
url = zone.api_url
LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s")
% locals())
@@ -188,7 +189,7 @@ def _issue_novaclient_command(nova, zone, collection,
if method_name in ['find', 'findall']:
try:
return getattr(manager, method_name)(**kwargs)
- except novaclient.NotFound:
+ except novaclient_exceptions.NotFound:
url = zone.api_url
LOG.debug(_("%(collection)s.%(method_name)s didn't find "
"anything matching '%(kwargs)s' on '%(url)s'" %
@@ -200,7 +201,7 @@ def _issue_novaclient_command(nova, zone, collection,
item = args.pop(0)
try:
result = manager.get(item)
- except novaclient.NotFound:
+ except novaclient_exceptions.NotFound:
url = zone.api_url
LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" %
locals()))
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
index 9ac0f94ed..d1924c9f9 100644
--- a/nova/scheduler/zone_aware_scheduler.py
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -24,7 +24,9 @@ import operator
import json
import M2Crypto
-import novaclient
+
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
from nova import crypto
from nova import db
@@ -58,12 +60,13 @@ class ZoneAwareScheduler(driver.Scheduler):
"""Create the requested resource in this Zone."""
host = build_plan_item['hostname']
base_options = request_spec['instance_properties']
+ image = request_spec['image']
# TODO(sandy): I guess someone needs to add block_device_mapping
# support at some point? Also, OS API has no concept of security
# groups.
instance = compute_api.API().create_db_entry_for_new_instance(context,
- base_options, None, [])
+ image, base_options, None, [])
instance_id = instance['id']
kwargs['instance_id'] = instance_id
@@ -117,10 +120,9 @@ class ZoneAwareScheduler(driver.Scheduler):
% locals())
nova = None
try:
- nova = novaclient.OpenStack(zone.username, zone.password, None,
- url)
+ nova = novaclient.Client(zone.username, zone.password, None, url)
nova.authenticate()
- except novaclient.exceptions.BadRequest, e:
+ except novaclient_exceptions.BadRequest, e:
raise exception.NotAuthorized(_("Bad credentials attempting "
"to talk to zone at %(url)s.") % locals())
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index efdac06e1..97bdf3d44 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -18,10 +18,11 @@ ZoneManager oversees all communications with child Zones.
"""
import datetime
-import novaclient
import thread
import traceback
+from novaclient import v1_1 as novaclient
+
from eventlet import greenpool
from nova import db
@@ -89,8 +90,8 @@ class ZoneState(object):
def _call_novaclient(zone):
"""Call novaclient. Broken out for testing purposes."""
- client = novaclient.OpenStack(zone.username, zone.password, None,
- zone.api_url)
+ client = novaclient.Client(zone.username, zone.password, None,
+ zone.api_url)
return client.zones.info()._info
diff --git a/nova/test.py b/nova/test.py
index 549aa6fcf..88f1489e8 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -60,11 +60,42 @@ class skip_test(object):
self.message = msg
def __call__(self, func):
+ @functools.wraps(func)
def _skipper(*args, **kw):
"""Wrapped skipper function."""
raise nose.SkipTest(self.message)
- _skipper.__name__ = func.__name__
- _skipper.__doc__ = func.__doc__
+ return _skipper
+
+
+class skip_if(object):
+ """Decorator that skips a test if contition is true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ @functools.wraps(func)
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
+ return _skipper
+
+
+class skip_unless(object):
+ """Decorator that skips a test if condition is not true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ @functools.wraps(func)
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if not self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
return _skipper
@@ -141,11 +172,9 @@ class TestCase(unittest.TestCase):
def flags(self, **kw):
"""Override flag variables for a test."""
for k, v in kw.iteritems():
- if k in self.flag_overrides:
- self.reset_flags()
- raise Exception(
- 'trying to override already overriden flag: %s' % k)
- self.flag_overrides[k] = getattr(FLAGS, k)
+ # Store original flag value if it's not been overriden yet
+ if k not in self.flag_overrides:
+ self.flag_overrides[k] = getattr(FLAGS, k)
setattr(FLAGS, k, v)
def reset_flags(self):
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py
index bfb424afe..458434a81 100644
--- a/nova/tests/api/openstack/__init__.py
+++ b/nova/tests/api/openstack/__init__.py
@@ -22,14 +22,11 @@ import webob.dec
from nova import test
from nova import context
-from nova import flags
from nova.api.openstack.limits import RateLimitingMiddleware
from nova.api.openstack.common import limited
from nova.tests.api.openstack import fakes
from webob import Request
-FLAGS = flags.FLAGS
-
@webob.dec.wsgify
def simple_wsgi(req):
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 28969d5f8..d11fbf788 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -71,14 +71,18 @@ def fake_wsgi(self, req):
return self.application
-def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True):
+def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True,
+ fake_auth_context=None):
if not inner_app10:
inner_app10 = openstack.APIRouterV10()
if not inner_app11:
inner_app11 = openstack.APIRouterV11()
if fake_auth:
- ctxt = context.RequestContext('fake', 'fake')
+ if fake_auth_context is not None:
+ ctxt = fake_auth_context
+ else:
+ ctxt = context.RequestContext('fake', 'fake')
api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
limits.RateLimitingMiddleware(inner_app10)))
api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
@@ -113,8 +117,7 @@ def stub_out_key_pair_funcs(stubs, have_key_pair=True):
def stub_out_image_service(stubs):
def fake_get_image_service(image_href):
- image_id = int(str(image_href).split('/')[-1])
- return (nova.image.fake.FakeImageService(), image_id)
+ return (nova.image.fake.FakeImageService(), image_href)
stubs.Set(nova.image, 'get_image_service', fake_get_image_service)
stubs.Set(nova.image, 'get_default_image_service',
lambda: nova.image.fake.FakeImageService())
diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py
index 89dbf5213..707a2599f 100644
--- a/nova/tests/api/openstack/test_accounts.py
+++ b/nova/tests/api/openstack/test_accounts.py
@@ -18,17 +18,12 @@ import json
import webob
-from nova import flags
from nova import test
from nova.api.openstack import accounts
from nova.auth.manager import User
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
def fake_init(self):
self.manager = fakes.FakeAuthManager()
@@ -40,7 +35,7 @@ def fake_admin_check(self, req):
class AccountsTest(test.TestCase):
def setUp(self):
super(AccountsTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
self.stubs.Set(accounts.Controller, '__init__',
fake_init)
self.stubs.Set(accounts.Controller, '_check_admin',
diff --git a/nova/tests/api/openstack/test_adminapi.py b/nova/tests/api/openstack/test_adminapi.py
index b83de40cf..c9e66dc4c 100644
--- a/nova/tests/api/openstack/test_adminapi.py
+++ b/nova/tests/api/openstack/test_adminapi.py
@@ -18,12 +18,9 @@
import webob
-from nova import flags
from nova import test
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-
class AdminAPITest(test.TestCase):
@@ -31,7 +28,7 @@ class AdminAPITest(test.TestCase):
super(AdminAPITest, self).setUp()
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
- self.allow_admin = FLAGS.allow_admin_api
+ self.flags(verbose=True)
def test_admin_enabled(self):
self.flags(allow_admin_api=True)
diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py
index 0b76841f0..5a6e43579 100644
--- a/nova/tests/api/openstack/test_common.py
+++ b/nova/tests/api/openstack/test_common.py
@@ -323,14 +323,10 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="three">
- four
- </meta>
- <meta key="one">
- two
- </meta>
+ <meta key="three">four</meta>
+ <meta key="one">two</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -346,11 +342,9 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="None">
- None
- </meta>
+ <meta key="None">None</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -366,11 +360,9 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString(u"""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="three">
- Jos\xe9
- </meta>
+ <meta key="three">Jos\xe9</meta>
</metadata>
- """.encode("UTF-8").replace(" ", ""))
+ """.encode("UTF-8").replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -385,10 +377,9 @@ class MetadataXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected = minidom.parseString("""
- <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
- two
- </meta>
- """.replace(" ", ""))
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="one">two</meta>
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -405,14 +396,10 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="key6">
- value6
- </meta>
- <meta key="key4">
- value4
- </meta>
+ <meta key="key6">value6</meta>
+ <meta key="key4">value4</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -427,10 +414,9 @@ class MetadataXMLSerializationTest(test.TestCase):
actual = minidom.parseString(output.replace(" ", ""))
expected = minidom.parseString("""
- <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one">
- two
- </meta>
- """.replace(" ", ""))
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="one">two</meta>
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
@@ -448,17 +434,11 @@ class MetadataXMLSerializationTest(test.TestCase):
expected = minidom.parseString("""
<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
- <meta key="key2">
- value2
- </meta>
- <meta key="key9">
- value9
- </meta>
- <meta key="key1">
- value1
- </meta>
+ <meta key="key2">value2</meta>
+ <meta key="key9">value9</meta>
+ <meta key="key1">value1</meta>
</metadata>
- """.replace(" ", ""))
+ """.replace(" ", "").replace("\n", ""))
self.assertEqual(expected.toxml(), actual.toxml())
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 47c37225c..49f0ea8b3 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -18,18 +18,17 @@
import json
import os.path
import webob
-from xml.etree import ElementTree
+from lxml import etree
from nova import context
-from nova import flags
from nova import test
from nova.api import openstack
from nova.api.openstack import extensions
from nova.api.openstack import flavors
from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
NS = "{http://docs.openstack.org/compute/api/v1.1}"
ATOMNS = "{http://www.w3.org/2005/Atom}"
response_body = "Try to say this Mr. Knox, sir..."
@@ -140,7 +139,7 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(200, response.status_int)
print response.body
- root = ElementTree.XML(response.body)
+ root = etree.XML(response.body)
self.assertEqual(root.tag.split('extensions')[0], NS)
# Make sure we have all the extensions.
@@ -156,6 +155,8 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(fox_ext.findtext('{0}description'.format(NS)),
'The Fox In Socks Extension')
+ xmlutil.validate_schema(root, 'extensions')
+
def test_get_extension_xml(self):
app = openstack.APIRouterV11()
ext_midware = extensions.ExtensionMiddleware(app)
@@ -163,9 +164,10 @@ class ExtensionControllerTest(test.TestCase):
request.accept = "application/xml"
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
- print response.body
+ xml = response.body
+ print xml
- root = ElementTree.XML(response.body)
+ root = etree.XML(xml)
self.assertEqual(root.tag.split('extension')[0], NS)
self.assertEqual(root.get('alias'), 'FOXNSOX')
self.assertEqual(root.get('name'), 'Fox In Socks')
@@ -175,6 +177,8 @@ class ExtensionControllerTest(test.TestCase):
self.assertEqual(root.findtext('{0}description'.format(NS)),
'The Fox In Socks Extension')
+ xmlutil.validate_schema(root, 'extension')
+
class ResourceExtensionTest(test.TestCase):
@@ -354,7 +358,8 @@ class ExtensionsXMLSerializerTest(test.TestCase):
}
xml = serializer.serialize(data, 'show')
- root = ElementTree.XML(xml)
+ print xml
+ root = etree.XML(xml)
ext_dict = data['extension']
self.assertEqual(root.findtext('{0}description'.format(NS)),
ext_dict['description'])
@@ -368,6 +373,8 @@ class ExtensionsXMLSerializerTest(test.TestCase):
for key, value in link.items():
self.assertEqual(link_nodes[i].get(key), value)
+ xmlutil.validate_schema(root, 'extension')
+
def test_serialize_extensions(self):
serializer = extensions.ExtensionsXMLSerializer()
data = {
@@ -415,7 +422,7 @@ class ExtensionsXMLSerializerTest(test.TestCase):
xml = serializer.serialize(data, 'index')
print xml
- root = ElementTree.XML(xml)
+ root = etree.XML(xml)
ext_elems = root.findall('{0}extension'.format(NS))
self.assertEqual(len(ext_elems), 2)
for i, ext_elem in enumerate(ext_elems):
@@ -431,3 +438,5 @@ class ExtensionsXMLSerializerTest(test.TestCase):
for i, link in enumerate(ext_dict['links']):
for key, value in link.items():
self.assertEqual(link_nodes[i].get(key), value)
+
+ xmlutil.validate_schema(root, 'extensions')
diff --git a/nova/tests/api/openstack/test_flavors_extra_specs.py b/nova/tests/api/openstack/test_flavors_extra_specs.py
index d386958db..ccd1b0d9f 100644
--- a/nova/tests/api/openstack/test_flavors_extra_specs.py
+++ b/nova/tests/api/openstack/test_flavors_extra_specs.py
@@ -21,15 +21,12 @@ import webob
import os.path
-from nova import flags
from nova import test
from nova.api import openstack
from nova.api.openstack import extensions
from nova.tests.api.openstack import fakes
import nova.wsgi
-FLAGS = flags.FLAGS
-
def return_create_flavor_extra_specs(context, flavor_id, extra_specs):
return stub_flavor_extra_specs()
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 942c0b333..383ed2e03 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -34,7 +34,6 @@ import webob
from glance import client as glance_client
from nova import context
from nova import exception
-from nova import flags
from nova import test
from nova import utils
import nova.api.openstack
@@ -42,9 +41,6 @@ from nova.api.openstack import images
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-
-
class _BaseImageServiceTests(test.TestCase):
"""Tasks to test for all image services"""
@@ -328,8 +324,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def setUp(self):
"""Run before each test."""
super(ImageControllerWithGlanceServiceTest, self).setUp()
- self.orig_image_service = FLAGS.image_service
- FLAGS.image_service = 'nova.image.glance.GlanceImageService'
+ self.flags(image_service='nova.image.glance.GlanceImageService')
self.stubs = stubout.StubOutForTesting()
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
@@ -342,7 +337,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
def tearDown(self):
"""Run after each test."""
self.stubs.UnsetAll()
- FLAGS.image_service = self.orig_image_service
super(ImageControllerWithGlanceServiceTest, self).tearDown()
def _applicable_fixture(self, fixture, user_id):
@@ -385,6 +379,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "ACTIVE",
+ "progress": 100,
},
}
@@ -408,6 +403,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "QUEUED",
+ "progress": 0,
'server': {
'id': 42,
"links": [{
@@ -450,6 +446,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
updated="%(expected_now)s"
created="%(expected_now)s"
status="ACTIVE"
+ progress="100"
xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" />
""" % (locals()))
@@ -469,6 +466,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
updated="%(expected_now)s"
created="%(expected_now)s"
status="ACTIVE"
+ progress="100"
xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" />
""" % (locals()))
@@ -593,6 +591,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
},
{
'id': 124,
@@ -600,6 +599,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'progress': 0,
},
{
'id': 125,
@@ -614,7 +614,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'name': 'active snapshot',
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
- 'status': 'ACTIVE'
+ 'status': 'ACTIVE',
+ 'progress': 100,
},
{
'id': 127,
@@ -622,6 +623,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'progress': 0,
},
{
'id': 129,
@@ -629,6 +631,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
}]
self.assertDictListMatch(expected, response_list)
@@ -649,6 +652,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/123",
@@ -668,6 +672,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'progress': 0,
'server': {
'id': 42,
"links": [{
@@ -729,6 +734,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
'server': {
'id': 42,
"links": [{
@@ -759,6 +765,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'progress': 0,
'server': {
'id': 42,
"links": [{
@@ -786,6 +793,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
"links": [{
"rel": "self",
"href": "http://localhost/v1.1/images/129",
@@ -1007,7 +1015,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
image_meta = json.loads(res.body)['image']
expected = {'id': 123, 'name': 'public image',
'updated': self.NOW_API_FORMAT,
- 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE'}
+ 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE',
+ 'progress': 100}
self.assertDictMatch(image_meta, expected)
def test_get_image_non_existent(self):
@@ -1031,6 +1040,9 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
req.headers["content-type"] = "application/json"
response = req.get_response(fakes.wsgi_app())
self.assertEqual(200, response.status_int)
+ image_meta = json.loads(response.body)['image']
+ self.assertEqual(123, image_meta['serverId'])
+ self.assertEqual('Snapshot 1', image_meta['name'])
def test_create_snapshot_no_name(self):
"""Name is required for snapshots"""
@@ -1052,6 +1064,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
response = req.get_response(fakes.wsgi_app())
self.assertEqual(400, response.status_int)
+ def test_create_image_snapshots_disabled(self):
+ self.flags(allow_instance_snapshots=False)
+ body = dict(image=dict(serverId='123', name='Snapshot 1'))
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
@classmethod
def _make_image_fixtures(cls):
image_id = 123
diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py
new file mode 100644
index 000000000..717e11c00
--- /dev/null
+++ b/nova/tests/api/openstack/test_server_actions.py
@@ -0,0 +1,1075 @@
+import base64
+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 flags
+from nova.api.openstack import create_instance_helper
+from nova.compute import instance_types
+from nova.compute import power_state
+import nova.db.api
+from nova import test
+from nova.tests.api.openstack import common
+from nova.tests.api.openstack import fakes
+
+
+FLAGS = flags.FLAGS
+
+
+def return_server_by_id(context, id):
+ return _get_instance()
+
+
+def instance_update(context, instance_id, kwargs):
+ return _get_instance()
+
+
+def return_server_with_power_state(power_state):
+ def _return_server(context, id):
+ instance = _get_instance()
+ instance['state'] = power_state
+ return instance
+ return _return_server
+
+
+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
+
+
+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
+
+
+def _get_instance():
+ instance = {
+ "id": 1,
+ "created_at": "2010-10-10 12:00:00",
+ "updated_at": "2010-11-11 11:00:00",
+ "admin_pass": "",
+ "user_id": "",
+ "project_id": "",
+ "image_ref": "5",
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": "",
+ "key_data": "",
+ "state": 0,
+ "state_description": "",
+ "memory_mb": 0,
+ "vcpus": 0,
+ "local_gb": 0,
+ "hostname": "",
+ "host": "",
+ "instance_type": {
+ "flavorid": 1,
+ },
+ "user_data": "",
+ "reservation_id": "",
+ "mac_address": "",
+ "scheduled_at": utils.utcnow(),
+ "launched_at": utils.utcnow(),
+ "terminated_at": utils.utcnow(),
+ "availability_zone": "",
+ "display_name": "test_server",
+ "display_description": "",
+ "locked": False,
+ "metadata": [],
+ #"address": ,
+ #"floating_ips": [{"address":ip} for ip in public_addresses]}
+ "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"}
+
+ return instance
+
+
+class ServerActionsTest(test.TestCase):
+
+ def setUp(self):
+ self.maxDiff = None
+ 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)
+
+ self.webreq = common.webob_factory('/v1.0/servers')
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+
+ 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_xml(self):
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.content_type = 'application/xml'
+ req.body = '<changePassword adminPass="1234pass">'
+# res = req.get_response(fakes.wsgi_app())
+# self.assertEqual(res.status_int, 501)
+
+ def test_server_reboot(self):
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ 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())
+
+ def test_server_rebuild_accepted(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ 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, 202)
+ self.assertEqual(res.body, "")
+
+ def test_server_rebuild_rejected_when_building(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ state = power_state.BUILDING
+ new_return_server = return_server_with_power_state(state)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_with_uuid_and_power_state(state))
+
+ 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, 409)
+
+ def test_server_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ },
+ }
+
+ 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, 400)
+
+ def test_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_bad_flavor_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(derp=3)))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+ self.assertEqual(self.resize_called, False)
+
+ def test_resize_raises_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
+
+ def resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 500)
+
+ def test_resized_server_has_correct_status(self):
+ req = self.webreq('/1', 'GET')
+
+ def fake_migration_get(*args):
+ return {}
+
+ self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
+ fake_migration_get)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM')
+
+ def test_confirm_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
+
+ self.resize_called = False
+
+ def confirm_resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize',
+ confirm_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(self.resize_called, True)
+
+ def test_confirm_resize_server_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
+
+ def confirm_resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize',
+ confirm_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_revert_resize_server(self):
+ req = self.webreq('/1/action', 'POST', dict(revertResize=None))
+
+ self.resize_called = False
+
+ def revert_resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize',
+ revert_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_revert_resize_server_fails(self):
+ req = self.webreq('/1/action', 'POST', dict(revertResize=None))
+
+ def revert_resize_mock(*args):
+ raise Exception('hurr durr')
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize',
+ revert_resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_migrate_server(self):
+ """This is basically the same as resize, only we provide the `migrate`
+ attribute in the body's dict.
+ """
+ req = self.webreq('/1/migrate', 'POST')
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_create_backup(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_admin_api_off(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=False)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(501, response.status_int)
+
+ def test_create_backup_with_metadata(self):
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': {'123': 'asdf'},
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+ def test_create_backup_with_too_much_metadata(self):
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': {'123': 'asdf'},
+ },
+ }
+ for num in range(FLAGS.quota_metadata_items + 1):
+ body['createBackup']['metadata']['foo%i' % num] = "bar"
+ req = webob.Request.blank('/v1.0/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_name(self):
+ """Name is required for backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_rotation(self):
+ """Rotation is required for backup requests"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ },
+ }
+
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_no_backup_type(self):
+ """Backup Type (daily or weekly) is required for backup requests"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'rotation': 1,
+ },
+ }
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup_bad_entity(self):
+ self.flags(allow_admin_api=True)
+
+ body = {'createBackup': 'go'}
+ req = webob.Request.blank('/v1.0/images')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+
+class ServerActionsTestV11(test.TestCase):
+
+ def setUp(self):
+ 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)
+
+ fakes.stub_out_glance(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
+ service_class = 'nova.image.glance.GlanceImageService'
+ self.service = utils.import_object(service_class)
+ self.context = context.RequestContext(1, None)
+ self.service.delete_all()
+ self.sent_to_glance = {}
+ fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance)
+ self.flags(allow_instance_snapshots=True)
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+
+ def test_server_change_password(self):
+ 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_xml(self):
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.content_type = "application/xml"
+ req.body = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass="1234pass"/>"""
+ 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_not_a_string(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_change_password_bad_request(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(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(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_reboot_hard(self):
+ body = dict(reboot=dict(type="HARD"))
+ 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)
+
+ def test_server_reboot_soft(self):
+ body = dict(reboot=dict(type="SOFT"))
+ 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)
+
+ def test_server_reboot_incorrect_type(self):
+ body = dict(reboot=dict(type="NOT_A_TYPE"))
+ 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_missing_type(self):
+ body = dict(reboot=dict())
+ 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_rebuild_accepted_minimum(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ },
+ }
+
+ 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)
+
+ def test_server_rebuild_rejected_when_building(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ },
+ }
+
+ state = power_state.BUILDING
+ new_return_server = return_server_with_power_state(state)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_with_uuid_and_power_state(state))
+
+ 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, 409)
+
+ def test_server_rebuild_accepted_with_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "metadata": {
+ "new": "metadata",
+ },
+ },
+ }
+
+ 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)
+
+ def test_server_rebuild_accepted_with_bad_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "metadata": "stack",
+ },
+ }
+
+ 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_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ "imageId": 2,
+ },
+ }
+
+ 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_rebuild_bad_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": "INVALID b64",
+ }]
+ },
+ }
+
+ 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_rebuild_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/2",
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": base64.b64encode("Test String"),
+ }]
+ },
+ }
+
+ 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)
+
+ def test_resize_server(self):
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict(flavorRef="http://localhost/3"))
+ req.body = json.dumps(body_dict)
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_server_no_flavor(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict())
+ req.body = json.dumps(body_dict)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_resize_server_no_flavor_ref(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(resize=dict(flavorRef=None))
+ req.body = json.dumps(body_dict)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_confirm_resize_server(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(confirmResize=None)
+ req.body = json.dumps(body_dict)
+
+ self.confirm_resize_called = False
+
+ def cr_mock(*args):
+ self.confirm_resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'confirm_resize', cr_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(self.confirm_resize_called, True)
+
+ def test_revert_resize_server(self):
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.content_type = 'application/json'
+ req.method = 'POST'
+ body_dict = dict(revertResize=None)
+ req.body = json.dumps(body_dict)
+
+ self.revert_resize_called = False
+
+ def revert_mock(*args):
+ self.revert_resize_called = True
+
+ self.stubs.Set(nova.compute.api.API, 'revert_resize', revert_mock)
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+ self.assertEqual(self.revert_resize_called, True)
+
+ def test_create_image(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v1.1/images/123', location)
+
+ def test_create_image_snapshots_disabled(self):
+ """Don't permit a snapshot if the allow_instance_snapshots flag is
+ False
+ """
+ self.flags(allow_instance_snapshots=False)
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_with_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {'key': 'asdf'},
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v1.1/images/123', location)
+
+ def test_create_image_with_too_much_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {},
+ },
+ }
+ for num in range(FLAGS.quota_metadata_items + 1):
+ body['createImage']['metadata']['foo%i' % num] = "bar"
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_no_name(self):
+ body = {
+ 'createImage': {},
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_image_bad_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'geoff',
+ 'metadata': 'henry',
+ },
+ }
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, response.status_int)
+
+ def test_create_backup(self):
+ """The happy path for creating backups"""
+ self.flags(allow_admin_api=True)
+
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers/1/action')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ response = req.get_response(fakes.wsgi_app())
+ self.assertEqual(202, response.status_int)
+ self.assertTrue(response.headers['Location'])
+
+
+class TestServerActionXMLDeserializerV11(test.TestCase):
+
+ def setUp(self):
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
+
+ def tearDown(self):
+ pass
+
+ def test_create_image(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_create_image_with_metadata(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="key1">value1</meta>
+ </metadata>
+</createImage>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ "metadata": {"key1": "value1"},
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_change_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass="1234pass"/> """
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "changePassword": {
+ "adminPass": "1234pass",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_change_pass_no_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/> """
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_reboot(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ type="HARD"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "reboot": {
+ "type": "HARD",
+ },
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_reboot_no_type(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ flavorRef="http://localhost/flavors/3"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "resize": {"flavorRef": "http://localhost/flavors/3"},
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_resize_no_flavor_ref(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_confirm_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <confirmResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "confirmResize": None,
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_revert_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <revertResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "revertResize": None,
+ }
+ self.assertEquals(request['body'], expected)
+
+ def test_rebuild(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"
+ imageRef="http://localhost/images/1">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "name": "new-server-test",
+ "imageRef": "http://localhost/images/1",
+ "metadata": {
+ "My Server Name": "Apache1",
+ },
+ "personality": [
+ {"path": "/etc/banner.txt", "contents": "Mg=="},
+ ],
+ },
+ }
+ self.assertDictMatch(request['body'], expected)
+
+ def test_rebuild_minimum(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ imageRef="http://localhost/images/1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/1",
+ },
+ }
+ self.assertDictMatch(request['body'], expected)
+
+ def test_rebuild_no_imageRef(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py
index f90485067..ec446f0f0 100644
--- a/nova/tests/api/openstack/test_server_metadata.py
+++ b/nova/tests/api/openstack/test_server_metadata.py
@@ -17,7 +17,7 @@
import json
import webob
-
+from xml.dom import minidom
from nova import exception
from nova import flags
@@ -29,11 +29,11 @@ import nova.wsgi
FLAGS = flags.FLAGS
-def return_create_instance_metadata_max(context, server_id, metadata):
+def return_create_instance_metadata_max(context, server_id, metadata, delete):
return stub_max_server_metadata()
-def return_create_instance_metadata(context, server_id, metadata):
+def return_create_instance_metadata(context, server_id, metadata, delete):
return stub_server_metadata()
@@ -51,11 +51,10 @@ def delete_server_metadata(context, server_id, key):
def stub_server_metadata():
metadata = {
- "key1": "value1",
- "key2": "value2",
- "key3": "value3",
- "key4": "value4",
- "key5": "value5"}
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ }
return metadata
@@ -84,94 +83,182 @@ class ServerMetaDataTest(test.TestCase):
def test_index(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value1', res_dict['metadata']['key1'])
+ expected = {
+ 'metadata': {
+ 'key1': 'value1',
+ 'key2': 'value2',
+ 'key3': 'value3',
+ },
+ }
+ self.assertEqual(expected, res_dict)
+
+ def test_index_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ request = webob.Request.blank("/v1.1/servers/1/metadata")
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("application/xml", response.content_type)
+
+ actual_metadata = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key3">value3</meta>
+ <meta key="key2">value2</meta>
+ <meta key="key1">value1</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_index_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_index_no_data(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
res = req.get_response(fakes.wsgi_app())
- res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual(0, len(res_dict['metadata']))
+ res_dict = json.loads(res.body)
+ expected = {'metadata': {}}
+ self.assertEqual(expected, res_dict)
def test_show(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value5', res_dict['key5'])
+ expected = {'meta': {'key2': 'value2'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ request = webob.Request.blank("/v1.1/servers/1/metadata/key2")
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("application/xml", response.content_type)
+
+ actual_metadata = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_metadata = minidom.parseString("""
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="key2">value2</meta>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_show_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_show_meta_not_found(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key6')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_delete(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
self.stubs.Set(nova.db.api, 'instance_metadata_delete',
delete_server_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(200, res.status_int)
+ self.assertEqual(204, res.status_int)
+ self.assertEqual('', res.body)
def test_delete_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/1/meta/key5')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
+ req.method = 'DELETE'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)
+
+ def test_delete_meta_not_found(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_empty_server_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_create(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
- req.body = '{"metadata": {"key1": "value1"}}'
- req.headers["content-type"] = "application/json"
+ req.content_type = "application/json"
+ input = {"metadata": {"key9": "value9"}}
+ req.body = json.dumps(input)
res = req.get_response(fakes.wsgi_app())
+
self.assertEqual(200, res.status_int)
res_dict = json.loads(res.body)
- self.assertEqual('application/json', res.headers['Content-Type'])
- self.assertEqual('value1', res_dict['metadata']['key1'])
+ input['metadata'].update({
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ })
+ self.assertEqual(input, res_dict)
+
+ def test_create_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(nova.db.api, "instance_metadata_update",
+ return_create_instance_metadata)
+ req = webob.Request.blank("/v1.1/servers/1/metadata")
+ req.method = "POST"
+ req.content_type = "application/xml"
+ req.accept = "application/xml"
+
+ request_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key5">value5</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ req.body = str(request_metadata.toxml())
+ response = req.get_response(fakes.wsgi_app())
+
+ expected_metadata = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key3">value3</meta>
+ <meta key="key2">value2</meta>
+ <meta key="key1">value1</meta>
+ <meta key="key5">value5</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(200, response.status_int)
+ actual_metadata = minidom.parseString(response.body)
+
+ self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml())
def test_create_empty_body(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
@@ -179,79 +266,153 @@ class ServerMetaDataTest(test.TestCase):
def test_create_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/100/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/100/metadata')
req.method = 'POST'
req.body = '{"metadata": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
+ def test_update_all(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {
+ 'metadata': {
+ 'key10': 'value10',
+ 'key99': 'value99',
+ },
+ }
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_empty_container(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': {}}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_malformed_container(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'meta': {}}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_all_malformed_data(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': ['asdf']}
+ req.body = json.dumps(expected)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_all_nonexistant_server(self):
+ self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
+ req = webob.Request.blank('/v1.1/servers/100/metadata')
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ req.body = json.dumps({'metadata': {'key10': 'value10'}})
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)
+
def test_update_item(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
self.assertEqual('application/json', res.headers['Content-Type'])
res_dict = json.loads(res.body)
- self.assertEqual('value1', res_dict['key1'])
+ expected = {'meta': {'key1': 'value1'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_update_item_xml(self):
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key9')
+ req.method = 'PUT'
+ req.accept = "application/json"
+ req.content_type = "application/xml"
+ req.body = """
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="key9">value9</meta>
+ """
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ self.assertEqual('application/json', res.headers['Content-Type'])
+ res_dict = json.loads(res.body)
+ expected = {'meta': {'key9': 'value9'}}
+ self.assertEqual(expected, res_dict)
def test_update_item_nonexistant_server(self):
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
- req = webob.Request.blank('/v1.1/servers/asdf/100/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta":{"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)
def test_update_item_empty_body(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_too_many_keys(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"key1": "value1", "key2": "value2"}'
+ req.body = '{"meta": {"key1": "value1", "key2": "value2"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_body_uri_mismatch(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
- req = webob.Request.blank('/v1.1/servers/1/meta/bad')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/bad')
req.method = 'PUT'
- req.body = '{"key1": "value1"}'
+ req.body = '{"meta": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_too_many_metadata_items_on_create(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata)
data = {"metadata": {}}
for num in range(FLAGS.quota_metadata_items + 1):
data['metadata']['key%i' % num] = "blah"
json_string = str(data).replace("\'", "\"")
- req = webob.Request.blank('/v1.1/servers/1/meta')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata')
req.method = 'POST'
req.body = json_string
req.headers["content-type"] = "application/json"
@@ -259,12 +420,11 @@ class ServerMetaDataTest(test.TestCase):
self.assertEqual(400, res.status_int)
def test_to_many_metadata_items_on_update_item(self):
- self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
+ self.stubs.Set(nova.db.api, 'instance_metadata_update',
return_create_instance_metadata_max)
- req = webob.Request.blank('/v1.1/servers/1/meta/key1')
- req.environ['api.version'] = '1.1'
+ req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
req.method = 'PUT'
- req.body = '{"a new key": "a new value"}'
+ req.body = '{"meta": {"a new key": "a new value"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 1871cac96..b6342ae2f 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -26,7 +26,6 @@ import webob
from nova import context
from nova import db
from nova import exception
-from nova import flags
from nova import test
from nova import utils
import nova.api.openstack
@@ -46,10 +45,6 @@ from nova.tests.api.openstack import common
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
@@ -235,12 +230,14 @@ class ServersTest(test.TestCase):
def setUp(self):
self.maxDiff = None
super(ServersTest, self).setUp()
+ self.flags(verbose=True)
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_key_pair_funcs(self.stubs)
fakes.stub_out_image_service(self.stubs)
self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid)
- self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
+ return_servers)
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
self.stubs.Set(nova.db, 'instance_get_by_uuid',
return_server_by_uuid)
@@ -260,17 +257,6 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api)
self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api)
- fakes.stub_out_glance(self.stubs)
- fakes.stub_out_compute_api_snapshot(self.stubs)
- service_class = 'nova.image.glance.GlanceImageService'
- self.service = utils.import_object(service_class)
- self.context = context.RequestContext(1, None)
- self.service.delete_all()
- self.sent_to_glance = {}
- fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance)
-
- self.allow_admin = FLAGS.allow_admin_api
-
self.webreq = common.webob_factory('/v1.0/servers')
def test_get_server_by_id(self):
@@ -767,7 +753,7 @@ class ServersTest(test.TestCase):
self.assertEquals(ip.getAttribute('addr'), private)
def test_get_server_by_id_with_addresses_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -811,7 +797,7 @@ class ServersTest(test.TestCase):
self.assertEqual(addresses, expected)
def test_get_server_by_id_with_addresses_v1_1_ipv6_disabled(self):
- FLAGS.use_ipv6 = False
+ self.flags(use_ipv6=False)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -854,7 +840,7 @@ class ServersTest(test.TestCase):
self.assertEqual(addresses, expected)
def test_get_server_addresses_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -905,7 +891,7 @@ class ServersTest(test.TestCase):
self.assertEqual(res_dict, expected)
def test_get_server_addresses_single_network_v1_1(self):
- FLAGS.use_ipv6 = True
+ self.flags(use_ipv6=True)
interfaces = [
{
'network': {'label': 'network_1'},
@@ -1113,6 +1099,277 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 400)
self.assertTrue(res.body.find('marker param') > -1)
+ def test_get_servers_with_bad_option_v1_0(self):
+ # 1.0 API ignores unknown options
+ def fake_get_all(compute_self, context, search_opts=None):
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.0/servers?unknownoption=whee')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_with_bad_option_v1_1(self):
+ # 1.1 API also ignores unknown options
+ def fake_get_all(compute_self, context, search_opts=None):
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?unknownoption=whee')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_image_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('image' in search_opts)
+ self.assertEqual(search_opts['image'], '12345')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?image=12345')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_flavor_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('flavor' in search_opts)
+ # flavor is an integer ID
+ self.assertEqual(search_opts['flavor'], '12345')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?flavor=12345')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_allows_status_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('state' in search_opts)
+ self.assertEqual(set(search_opts['state']),
+ set([power_state.RUNNING, power_state.BLOCKED]))
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?status=active')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_invalid_status_v1_1(self):
+ """Test getting servers by invalid status"""
+
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?status=running')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(res.body.find('Invalid server status') > -1)
+
+ def test_get_servers_allows_name_v1_1(self):
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('name' in search_opts)
+ self.assertEqual(search_opts['name'], 'whee.*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+ self.flags(allow_admin_api=False)
+
+ req = webob.Request.blank('/v1.1/servers?name=whee.*')
+ res = req.get_response(fakes.wsgi_app())
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options1(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is off. Make sure the admin and
+ unknown options are stripped before they get to
+ compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=False)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertFalse('ip' in search_opts)
+ self.assertFalse('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options2(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is on, but context is a user.
+ Make sure the admin and unknown options are stripped before
+ they get to compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertFalse('ip' in search_opts)
+ self.assertFalse('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=False)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_unknown_or_admin_options3(self):
+ """Test getting servers by admin-only or unknown options.
+ This tests when admin_api is on and context is admin.
+ All options should be passed through to compute_api.get_all()
+ """
+
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ # Allowed by user
+ self.assertTrue('name' in search_opts)
+ self.assertTrue('status' in search_opts)
+ # Allowed only by admins with admin API on
+ self.assertTrue('ip' in search_opts)
+ self.assertTrue('unknown_option' in search_opts)
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = webob.Request.blank('/v1.1/servers?%s' % query_str)
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_admin_allows_ip_v1_1(self):
+ """Test getting servers by ip with admin_api enabled and
+ admin context
+ """
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('ip' in search_opts)
+ self.assertEqual(search_opts['ip'], '10\..*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?ip=10\..*')
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
+ def test_get_servers_admin_allows_ip6_v1_1(self):
+ """Test getting servers by ip6 with admin_api enabled and
+ admin context
+ """
+ self.flags(allow_admin_api=True)
+
+ def fake_get_all(compute_self, context, search_opts=None):
+ self.assertNotEqual(search_opts, None)
+ self.assertTrue('ip6' in search_opts)
+ self.assertEqual(search_opts['ip6'], 'ffff.*')
+ return [stub_instance(100)]
+
+ self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
+
+ req = webob.Request.blank('/v1.1/servers?ip6=ffff.*')
+ # Request admin context
+ context = nova.context.RequestContext('testuser', 'testproject',
+ is_admin=True)
+ res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
+ # The following assert will fail if either of the asserts in
+ # fake_get_all() fail
+ self.assertEqual(res.status_int, 200)
+ servers = json.loads(res.body)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], 100)
+
def _setup_for_create_instance(self):
"""Shared implementation for tests below that create instance"""
def instance_create(context, inst):
@@ -1174,7 +1431,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual('server_test', server['name'])
@@ -1325,7 +1582,8 @@ class ServersTest(test.TestCase):
def test_create_instance_v1_1(self):
self._setup_for_create_instance()
- image_href = 'http://localhost/images/2'
+ # proper local hrefs must start with 'http://localhost/v1.1/'
+ image_href = 'http://localhost/v1.1/images/2'
flavor_ref = 'http://localhost/flavors/3'
expected_flavor = {
"id": "3",
@@ -1370,7 +1628,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual(1, server['id'])
@@ -1465,7 +1723,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(expected_flavor, server['flavor'])
self.assertEqual(expected_image, server['image'])
@@ -1510,7 +1768,7 @@ class ServersTest(test.TestCase):
req.body = json.dumps(body)
req.headers['content-type'] = "application/json"
res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.status_int, 202)
server = json.loads(res.body)['server']
self.assertEqual(server['adminPass'], body['server']['adminPass'])
@@ -1679,6 +1937,7 @@ class ServersTest(test.TestCase):
def test_get_all_server_details_v1_0(self):
req = webob.Request.blank('/v1.0/servers/detail')
res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
res_dict = json.loads(res.body)
for i, s in enumerate(res_dict['servers']):
@@ -1734,7 +1993,7 @@ class ServersTest(test.TestCase):
return [stub_instance(i, 'fake', 'fake', None, None, i % 2)
for i in xrange(5)]
- self.stubs.Set(nova.db.api, 'instance_get_all_by_project',
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
return_servers_with_host)
req = webob.Request.blank('/v1.0/servers/detail')
@@ -1755,339 +2014,67 @@ class ServersTest(test.TestCase):
def test_server_pause(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/pause')
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)
def test_server_unpause(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/unpause')
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)
def test_server_suspend(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/suspend')
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)
def test_server_resume(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/resume')
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)
def test_server_reset_network(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank('/v1.0/servers/1/reset_network')
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)
def test_server_inject_network_info(self):
self.flags(allow_admin_api=True)
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
req = webob.Request.blank(
'/v1.0/servers/1/inject_network_info')
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)
def test_server_diagnostics(self):
+ self.flags(allow_admin_api=False)
req = webob.Request.blank("/v1.0/servers/1/diagnostics")
req.method = "GET"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 404)
def test_server_actions(self):
+ self.flags(allow_admin_api=False)
req = webob.Request.blank("/v1.0/servers/1/actions")
req.method = "GET"
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_xml(self):
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/xml'
- req.body = '<changePassword adminPass="1234pass">'
-# res = req.get_response(fakes.wsgi_app())
-# self.assertEqual(res.status_int, 501)
-
- def test_server_change_password_v1_1(self):
- 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={},
- personality={}))
- 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())
-
- def test_server_rebuild_accepted(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- 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, 202)
- self.assertEqual(res.body, "")
-
- def test_server_rebuild_rejected_when_building(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- state = power_state.BUILDING
- new_return_server = return_server_with_power_state(state)
- self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
- self.stubs.Set(nova.db, 'instance_get_by_uuid',
- return_server_with_uuid_and_power_state(state))
-
- 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, 409)
-
- def test_server_rebuild_bad_entity(self):
- body = {
- "rebuild": {
- },
- }
-
- 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, 400)
-
- def test_server_rebuild_accepted_minimum_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- },
- }
-
- 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)
-
- def test_server_rebuild_rejected_when_building_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- },
- }
-
- state = power_state.BUILDING
- new_return_server = return_server_with_power_state(state)
- self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
- self.stubs.Set(nova.db, 'instance_get_by_uuid',
- return_server_with_uuid_and_power_state(state))
-
- 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, 409)
-
- def test_server_rebuild_accepted_with_metadata_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "metadata": {
- "new": "metadata",
- },
- },
- }
-
- 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)
-
- def test_server_rebuild_accepted_with_bad_metadata_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "metadata": "stack",
- },
- }
-
- 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_rebuild_bad_entity_v1_1(self):
- body = {
- "rebuild": {
- "imageId": 2,
- },
- }
-
- 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_rebuild_bad_personality_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "personality": [{
- "path": "/path/to/file",
- "contents": "INVALID b64",
- }]
- },
- }
-
- 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_rebuild_personality_v1_1(self):
- body = {
- "rebuild": {
- "imageRef": "http://localhost/images/2",
- "personality": [{
- "path": "/path/to/file",
- "contents": base64.b64encode("Test String"),
- }]
- },
- }
-
- 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)
-
def test_delete_server_instance(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'DELETE'
@@ -2155,190 +2142,6 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 204)
self.assertEqual(self.server_delete_called, True)
- def test_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_resize_server_v11(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- body_dict = {
- "resize": {
- "flavorRef": 3,
- },
- }
- req.body = json.dumps(body_dict)
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_resize_bad_flavor_data(self):
- req = self.webreq('/1/action', 'POST', {"resize": "bad_data"})
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
- self.assertEqual(self.resize_called, False)
-
- def test_resize_invalid_flavorid(self):
- req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": 300}})
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_nonint_flavorid(self):
- req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": "a"}})
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_invalid_flavorid_v1_1(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- resize_body = {
- "resize": {
- "image": {
- "id": 300,
- },
- },
- }
- req.body = json.dumps(resize_body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_nonint_flavorid_v1_1(self):
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.content_type = 'application/json'
- req.method = 'POST'
- resize_body = {
- "resize": {
- "image": {
- "id": "a",
- },
- },
- }
- req.body = json.dumps(resize_body)
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_resize_raises_fails(self):
- req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3)))
-
- def resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 500)
-
- def test_resized_server_has_correct_status(self):
- req = self.webreq('/1', 'GET')
-
- def fake_migration_get(*args):
- return {}
-
- self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
- fake_migration_get)
- res = req.get_response(fakes.wsgi_app())
- body = json.loads(res.body)
- self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM')
-
- def test_confirm_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
-
- self.resize_called = False
-
- def confirm_resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'confirm_resize',
- confirm_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 204)
- self.assertEqual(self.resize_called, True)
-
- def test_confirm_resize_server_fails(self):
- req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
-
- def confirm_resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'confirm_resize',
- confirm_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_revert_resize_server(self):
- req = self.webreq('/1/action', 'POST', dict(revertResize=None))
-
- self.resize_called = False
-
- def revert_resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'revert_resize',
- revert_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
- def test_revert_resize_server_fails(self):
- req = self.webreq('/1/action', 'POST', dict(revertResize=None))
-
- def revert_resize_mock(*args):
- raise Exception("An error occurred.")
-
- self.stubs.Set(nova.compute.api.API, 'revert_resize',
- revert_resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 400)
-
- def test_migrate_server(self):
- """This is basically the same as resize, only we provide the `migrate`
- attribute in the body's dict.
- """
- req = self.webreq('/1/action', 'POST', dict(migrate=None))
-
- self.resize_called = False
-
- def resize_mock(*args):
- self.resize_called = True
-
- self.stubs.Set(nova.compute.api.API, 'resize', resize_mock)
-
- res = req.get_response(fakes.wsgi_app())
- self.assertEqual(res.status_int, 202)
- self.assertEqual(self.resize_called, True)
-
def test_shutdown_status(self):
new_server = return_server_with_power_state(power_state.SHUTDOWN)
self.stubs.Set(nova.db.api, 'instance_get', new_server)
@@ -2357,268 +2160,6 @@ class ServersTest(test.TestCase):
res_dict = json.loads(res.body)
self.assertEqual(res_dict['server']['status'], 'SHUTOFF')
- def test_create_image_v1_1(self):
- body = {
- 'createImage': {
- 'name': 'Snapshot 1',
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- location = response.headers['Location']
- self.assertEqual('http://localhost/v1.1/images/123', location)
-
- def test_create_image_v1_1_with_metadata(self):
- body = {
- 'createImage': {
- 'name': 'Snapshot 1',
- 'metadata': {'key': 'asdf'},
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- location = response.headers['Location']
- self.assertEqual('http://localhost/v1.1/images/123', location)
-
- def test_create_image_v1_1_no_name(self):
- body = {
- 'createImage': {},
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_image_v1_1_bad_metadata(self):
- body = {
- 'createImage': {
- 'name': 'geoff',
- 'metadata': 'henry',
- },
- }
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup(self):
- """The happy path for creating backups"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_v1_1(self):
- """The happy path for creating backups through v1.1 api"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.1/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_admin_api_off(self):
- """The happy path for creating backups"""
- FLAGS.allow_admin_api = False
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(501, response.status_int)
-
- def test_create_backup_with_metadata(self):
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- 'rotation': 1,
- 'metadata': {'123': 'asdf'},
- },
- }
-
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(202, response.status_int)
- self.assertTrue(response.headers['Location'])
-
- def test_create_backup_no_name(self):
- """Name is required for backups"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'backup_type': 'daily',
- 'rotation': 1,
- },
- }
-
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_no_rotation(self):
- """Rotation is required for backup requests"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'backup_type': 'daily',
- },
- }
-
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_no_backup_type(self):
- """Backup Type (daily or weekly) is required for backup requests"""
- FLAGS.allow_admin_api = True
-
- body = {
- 'createBackup': {
- 'name': 'Backup 1',
- 'rotation': 1,
- },
- }
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
- def test_create_backup_bad_entity(self):
- FLAGS.allow_admin_api = True
-
- body = {'createBackup': 'go'}
- req = webob.Request.blank('/v1.0/images')
- req.method = 'POST'
- req.body = json.dumps(body)
- req.headers["content-type"] = "application/json"
-
- response = req.get_response(fakes.wsgi_app())
- self.assertEqual(400, response.status_int)
-
-
-class TestServerActionXMLDeserializer(test.TestCase):
-
- def setUp(self):
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
-
- def tearDown(self):
- pass
-
- def test_create_image(self):
- serial_request = """
-<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test"/>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createImage": {
- "name": "new-server-test",
- "metadata": {},
- },
- }
- self.assertEquals(request['body'], expected)
-
- def test_create_image_with_metadata(self):
- serial_request = """
-<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test">
- <metadata>
- <meta key="key1">value1</meta>
- </metadata>
-</createImage>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createImage": {
- "name": "new-server-test",
- "metadata": {"key1": "value1"},
- },
- }
- self.assertEquals(request['body'], expected)
-
- def test_create_backup_with_metadata(self):
- serial_request = """
-<createBackup xmlns="http://docs.openstack.org/compute/api/v1.1"
- name="new-server-test"
- rotation="12"
- backup_type="daily">
- <metadata>
- <meta key="key1">value1</meta>
- </metadata>
-</createBackup>"""
- request = self.deserializer.deserialize(serial_request, 'action')
- expected = {
- "createBackup": {
- "name": "new-server-test",
- "rotation": "12",
- "backup_type": "daily",
- "metadata": {"key1": "value1"},
- },
- }
- self.assertEquals(request['body'], expected)
-
class TestServerCreateRequestXMLDeserializerV10(unittest.TestCase):
@@ -2905,10 +2446,11 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""",
self.assertEqual(request['body'], expected)
-class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
+class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
def setUp(self):
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
+ super(TestServerCreateRequestXMLDeserializerV11, self).setUp()
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
def test_minimal_request(self):
serial_request = """
@@ -2922,8 +2464,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2942,8 +2482,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"imageRef": "1",
"flavorRef": "2",
"adminPass": "1234",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2960,8 +2498,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "http://localhost:8774/v1.1/images/2",
"flavorRef": "3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2978,8 +2514,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "http://localhost:8774/v1.1/flavors/3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -3023,7 +2557,6 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"imageRef": "1",
"flavorRef": "2",
"metadata": {"one": "two", "open": "snack"},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -3045,14 +2578,13 @@ class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
"personality": [
{"path": "/etc/banner.txt", "contents": "MQ=="},
{"path": "/etc/hosts", "contents": "Mg=="},
],
},
}
- self.assertEquals(request['body'], expected)
+ self.assertDictMatch(request['body'], expected)
def test_spec_request(self):
image_bookmark_link = "http://servers.api.openstack.org/1234/" + \
@@ -3254,13 +2786,13 @@ class TestServerInstanceCreation(test.TestCase):
def test_create_instance_with_no_personality(self):
request, response, injected_files = \
self._create_instance_with_personality_json(personality=None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [])
def test_create_instance_with_no_personality_xml(self):
request, response, injected_files = \
self._create_instance_with_personality_xml(personality=None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [])
def test_create_instance_with_personality(self):
@@ -3270,7 +2802,7 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, b64contents)]
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_with_personality_xml(self):
@@ -3280,7 +2812,7 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, b64contents)]
request, response, injected_files = \
self._create_instance_with_personality_xml(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_with_personality_no_path(self):
@@ -3343,7 +2875,7 @@ class TestServerInstanceCreation(test.TestCase):
request = self._get_create_request_json(body_dict)
compute_api, response = \
self._run_create_instance_with_mock_compute_api(request)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
def test_create_instance_with_three_personalities(self):
files = [
@@ -3356,7 +2888,7 @@ class TestServerInstanceCreation(test.TestCase):
personality.append((path, base64.b64encode(content)))
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, files)
def test_create_instance_personality_empty_content(self):
@@ -3365,13 +2897,13 @@ class TestServerInstanceCreation(test.TestCase):
personality = [(path, contents)]
request, response, injected_files = \
self._create_instance_with_personality_json(personality)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
self.assertEquals(injected_files, [(path, contents)])
def test_create_instance_admin_pass_json(self):
request, response, dummy = \
self._create_instance_with_personality_json(None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
response = json.loads(response.body)
self.assertTrue('adminPass' in response['server'])
self.assertEqual(16, len(response['server']['adminPass']))
@@ -3379,7 +2911,7 @@ class TestServerInstanceCreation(test.TestCase):
def test_create_instance_admin_pass_xml(self):
request, response, dummy = \
self._create_instance_with_personality_xml(None)
- self.assertEquals(response.status_int, 200)
+ self.assertEquals(response.status_int, 202)
dom = minidom.parseString(response.body)
server = dom.childNodes[0]
self.assertEquals(server.nodeName, 'server')
diff --git a/nova/tests/api/openstack/test_users.py b/nova/tests/api/openstack/test_users.py
index 705c02f6b..1d133f9ab 100644
--- a/nova/tests/api/openstack/test_users.py
+++ b/nova/tests/api/openstack/test_users.py
@@ -17,7 +17,6 @@ import json
import webob
-from nova import flags
from nova import test
from nova import utils
from nova.api.openstack import users
@@ -25,10 +24,6 @@ from nova.auth.manager import User, Project
from nova.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
def fake_init(self):
self.manager = fakes.FakeAuthManager()
@@ -40,7 +35,7 @@ def fake_admin_check(self, req):
class UsersTest(test.TestCase):
def setUp(self):
super(UsersTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
self.stubs.Set(users.Controller, '__init__',
fake_init)
self.stubs.Set(users.Controller, '_check_admin',
@@ -56,7 +51,6 @@ class UsersTest(test.TestCase):
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_auth(self.stubs)
- self.allow_admin = FLAGS.allow_admin_api
fakemgr = fakes.FakeAuthManager()
fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False))
fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True))
diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py
index da964ee1f..1269f13c9 100644
--- a/nova/tests/api/openstack/test_versions.py
+++ b/nova/tests/api/openstack/test_versions.py
@@ -16,21 +16,92 @@
# under the License.
import json
+import stubout
import webob
+import xml.etree.ElementTree
+
from nova import context
from nova import test
from nova.tests.api.openstack import fakes
from nova.api.openstack import versions
from nova.api.openstack import views
+from nova.api.openstack import wsgi
+
+VERSIONS = {
+ "v1.0": {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.0+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.0+json",
+ },
+ ],
+ },
+ "v1.1": {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.1+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.1+json",
+ },
+ ],
+ },
+}
class VersionsTest(test.TestCase):
def setUp(self):
super(VersionsTest, self).setUp()
self.context = context.get_admin_context()
+ self.stubs = stubout.StubOutForTesting()
+ fakes.stub_out_auth(self.stubs)
+ #Stub out VERSIONS
+ self.old_versions = versions.VERSIONS
+ versions.VERSIONS = VERSIONS
def tearDown(self):
+ versions.VERSIONS = self.old_versions
super(VersionsTest, self).tearDown()
def test_get_version_list(self):
@@ -44,7 +115,7 @@ class VersionsTest(test.TestCase):
{
"id": "v1.1",
"status": "CURRENT",
- "updated": "2011-07-18T11:30:00Z",
+ "updated": "2011-01-21T11:33:21Z",
"links": [
{
"rel": "self",
@@ -54,7 +125,7 @@ class VersionsTest(test.TestCase):
{
"id": "v1.0",
"status": "DEPRECATED",
- "updated": "2010-10-09T11:30:00Z",
+ "updated": "2011-01-21T11:33:21Z",
"links": [
{
"rel": "self",
@@ -64,6 +135,183 @@ class VersionsTest(test.TestCase):
]
self.assertEqual(versions, expected)
+ def test_get_version_1_0_detail(self):
+ req = webob.Request.blank('/v1.0/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = json.loads(res.body)
+ expected = {
+ "version": {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v1.0/",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/"
+ "vnd.openstack.compute-v1.0+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/"
+ "vnd.openstack.compute-v1.0+json",
+ },
+ ],
+ },
+ }
+ self.assertEqual(expected, version)
+
+ def test_get_version_1_1_detail(self):
+ req = webob.Request.blank('/v1.1/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = json.loads(res.body)
+ expected = {
+ "version": {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v1.1/",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/"
+ "vnd.openstack.compute-v1.1+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/"
+ "vnd.openstack.compute-v1.1+json",
+ },
+ ],
+ },
+ }
+ self.assertEqual(expected, version)
+
+ def test_get_version_1_0_detail_xml(self):
+ req = webob.Request.blank('/v1.0/')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/xml")
+ root = xml.etree.ElementTree.XML(res.body)
+ self.assertEqual(root.tag.split('}')[1], "version")
+ self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11)
+
+ children = list(root)
+ media_types = children[0]
+ media_type_nodes = list(media_types)
+ links = (children[1], children[2], children[3])
+
+ self.assertEqual(media_types.tag.split('}')[1], 'media-types')
+ for media_node in media_type_nodes:
+ self.assertEqual(media_node.tag.split('}')[1], 'media-type')
+
+ expected = """
+ <version id="v1.0" status="DEPRECATED"
+ updated="2011-01-21T11:33:21Z"
+ xmlns="%s"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+
+ <media-types>
+ <media-type base="application/xml"
+ type="application/vnd.openstack.compute-v1.0+xml"/>
+ <media-type base="application/json"
+ type="application/vnd.openstack.compute-v1.0+json"/>
+ </media-types>
+
+ <atom:link href="http://localhost/v1.0/"
+ rel="self"/>
+
+ <atom:link href="http://docs.rackspacecloud.com/servers/
+ api/v1.0/cs-devguide-20110125.pdf"
+ rel="describedby"
+ type="application/pdf"/>
+
+ <atom:link href="http://docs.rackspacecloud.com/servers/
+ api/v1.0/application.wadl"
+ rel="describedby"
+ type="application/vnd.sun.wadl+xml"/>
+ </version>""".replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11
+
+ actual = res.body.replace(" ", "").replace("\n", "")
+ self.assertEqual(expected, actual)
+
+ def test_get_version_1_1_detail_xml(self):
+ req = webob.Request.blank('/v1.1/')
+ 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 = """
+ <version id="v1.1" status="CURRENT"
+ updated="2011-01-21T11:33:21Z"
+ xmlns="%s"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+
+ <media-types>
+ <media-type base="application/xml"
+ type="application/vnd.openstack.compute-v1.1+xml"/>
+ <media-type base="application/json"
+ type="application/vnd.openstack.compute-v1.1+json"/>
+ </media-types>
+
+ <atom:link href="http://localhost/v1.1/"
+ rel="self"/>
+
+ <atom:link href="http://docs.rackspacecloud.com/servers/
+ api/v1.1/cs-devguide-20110125.pdf"
+ rel="describedby"
+ type="application/pdf"/>
+
+ <atom:link href="http://docs.rackspacecloud.com/servers/
+ api/v1.1/application.wadl"
+ rel="describedby"
+ type="application/vnd.sun.wadl+xml"/>
+ </version>""".replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11
+
+ actual = res.body.replace(" ", "").replace("\n", "")
+ self.assertEqual(expected, actual)
+
def test_get_version_list_xml(self):
req = webob.Request.blank('/')
req.accept = "application/xml"
@@ -71,18 +319,94 @@ class VersionsTest(test.TestCase):
self.assertEqual(res.status_int, 200)
self.assertEqual(res.content_type, "application/xml")
- expected = """<versions>
- <version id="v1.1" status="CURRENT" updated="2011-07-18T11:30:00Z">
+ expected = """
+ <versions xmlns="%s" xmlns:atom="%s">
+ <version id="v1.1" status="CURRENT" updated="2011-01-21T11:33:21Z">
<atom:link href="http://localhost/v1.1/" rel="self"/>
</version>
<version id="v1.0" status="DEPRECATED"
- updated="2010-10-09T11:30:00Z">
+ updated="2011-01-21T11:33:21Z">
<atom:link href="http://localhost/v1.0/" rel="self"/>
</version>
- </versions>""".replace(" ", "").replace("\n", "")
+ </versions>""".replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11,
+ wsgi.XMLNS_ATOM)
+
+ actual = res.body.replace(" ", "").replace("\n", "")
+
+ self.assertEqual(expected, actual)
+
+ def test_get_version_1_0_detail_atom(self):
+ req = webob.Request.blank('/v1.0/')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual("application/atom+xml", res.content_type)
+ expected = """
+ <feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="text">About This Version</title>
+ <updated>2011-01-21T11:33:21Z</updated>
+ <id>http://localhost/v1.0/</id>
+ <author>
+ <name>Rackspace</name>
+ <uri>http://www.rackspace.com/</uri>
+ </author>
+ <link href="http://localhost/v1.0/" rel="self"/>
+ <entry>
+ <id>http://localhost/v1.0/</id>
+ <title type="text">Version v1.0</title>
+ <updated>2011-01-21T11:33:21Z</updated>
+ <link href="http://localhost/v1.0/"
+ rel="self"/>
+ <link href="http://docs.rackspacecloud.com/servers/
+ api/v1.0/cs-devguide-20110125.pdf"
+ rel="describedby" type="application/pdf"/>
+ <link href="http://docs.rackspacecloud.com/servers/
+ api/v1.0/application.wadl"
+ rel="describedby" type="application/vnd.sun.wadl+xml"/>
+ <content type="text">
+ Version v1.0 DEPRECATED (2011-01-21T11:33:21Z)
+ </content>
+ </entry>
+ </feed>""".replace(" ", "").replace("\n", "")
actual = res.body.replace(" ", "").replace("\n", "")
+ self.assertEqual(expected, actual)
+
+ def test_get_version_1_1_detail_atom(self):
+ req = webob.Request.blank('/v1.1/')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual("application/atom+xml", res.content_type)
+ expected = """
+ <feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="text">About This Version</title>
+ <updated>2011-01-21T11:33:21Z</updated>
+ <id>http://localhost/v1.1/</id>
+ <author>
+ <name>Rackspace</name>
+ <uri>http://www.rackspace.com/</uri>
+ </author>
+ <link href="http://localhost/v1.1/" rel="self"/>
+ <entry>
+ <id>http://localhost/v1.1/</id>
+ <title type="text">Version v1.1</title>
+ <updated>2011-01-21T11:33:21Z</updated>
+ <link href="http://localhost/v1.1/"
+ rel="self"/>
+ <link href="http://docs.rackspacecloud.com/servers/
+ api/v1.1/cs-devguide-20110125.pdf"
+ rel="describedby" type="application/pdf"/>
+ <link href="http://docs.rackspacecloud.com/servers/
+ api/v1.1/application.wadl"
+ rel="describedby" type="application/vnd.sun.wadl+xml"/>
+ <content type="text">
+ Version v1.1 CURRENT (2011-01-21T11:33:21Z)
+ </content>
+ </entry>
+ </feed>""".replace(" ", "").replace("\n", "")
+ actual = res.body.replace(" ", "").replace("\n", "")
self.assertEqual(expected, actual)
def test_get_version_list_atom(self):
@@ -95,7 +419,7 @@ class VersionsTest(test.TestCase):
expected = """
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Available API Versions</title>
- <updated>2011-07-18T11:30:00Z</updated>
+ <updated>2011-01-21T11:33:21Z</updated>
<id>http://localhost/</id>
<author>
<name>Rackspace</name>
@@ -105,19 +429,19 @@ class VersionsTest(test.TestCase):
<entry>
<id>http://localhost/v1.1/</id>
<title type="text">Version v1.1</title>
- <updated>2011-07-18T11:30:00Z</updated>
+ <updated>2011-01-21T11:33:21Z</updated>
<link href="http://localhost/v1.1/" rel="self"/>
<content type="text">
- Version v1.1 CURRENT (2011-07-18T11:30:00Z)
+ Version v1.1 CURRENT (2011-01-21T11:33:21Z)
</content>
</entry>
<entry>
<id>http://localhost/v1.0/</id>
<title type="text">Version v1.0</title>
- <updated>2010-10-09T11:30:00Z</updated>
+ <updated>2011-01-21T11:33:21Z</updated>
<link href="http://localhost/v1.0/" rel="self"/>
<content type="text">
- Version v1.0 DEPRECATED (2010-10-09T11:30:00Z)
+ Version v1.0 DEPRECATED (2011-01-21T11:33:21Z)
</content>
</entry>
</feed>
@@ -127,28 +451,184 @@ class VersionsTest(test.TestCase):
self.assertEqual(expected, actual)
+ def test_multi_choice_image(self):
+ req = webob.Request.blank('/images/1')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ expected = {
+ "choices": [
+ {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "links": [
+ {
+ "href": "http://localhost/v1.1/images/1",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.1+xml"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.1+json"
+ },
+ ],
+ },
+ {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ "links": [
+ {
+ "href": "http://localhost/v1.0/images/1",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.0+xml"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.0+json"
+ },
+ ],
+ },
+ ], }
+
+ self.assertDictMatch(expected, json.loads(res.body))
+
+ def test_multi_choice_image_xml(self):
+ req = webob.Request.blank('/images/1')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/xml")
+
+ expected = """
+ <choices xmlns="%s" xmlns:atom="%s">
+ <version id="v1.1" status="CURRENT">
+ <media-types>
+ <media-type base="application/xml"
+ type="application/vnd.openstack.compute-v1.1+xml"/>
+ <media-type base="application/json"
+ type="application/vnd.openstack.compute-v1.1+json"/>
+ </media-types>
+ <atom:link href="http://localhost/v1.1/images/1" rel="self"/>
+ </version>
+ <version id="v1.0" status="DEPRECATED">
+ <media-types>
+ <media-type base="application/xml"
+ type="application/vnd.openstack.compute-v1.0+xml"/>
+ <media-type base="application/json"
+ type="application/vnd.openstack.compute-v1.0+json"/>
+ </media-types>
+ <atom:link href="http://localhost/v1.0/images/1" rel="self"/>
+ </version>
+ </choices>""".replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11,
+ wsgi.XMLNS_ATOM)
+
+ def test_multi_choice_server_atom(self):
+ """
+ Make sure multi choice responses do not have content-type
+ application/atom+xml (should use default of json)
+ """
+ req = webob.Request.blank('/servers/2')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ def test_multi_choice_server(self):
+ req = webob.Request.blank('/servers/2')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ expected = {
+ "choices": [
+ {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "links": [
+ {
+ "href": "http://localhost/v1.1/servers/2",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.1+xml"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.1+json"
+ },
+ ],
+ },
+ {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ "links": [
+ {
+ "href": "http://localhost/v1.0/servers/2",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.0+xml"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.0+json"
+ },
+ ],
+ },
+ ], }
+
+ self.assertDictMatch(expected, json.loads(res.body))
+
+
+class VersionsViewBuilderTests(test.TestCase):
def test_view_builder(self):
base_url = "http://example.org/"
version_data = {
- "id": "3.2.1",
- "status": "CURRENT",
- "updated": "2011-07-18T11:30:00Z"}
+ "v3.2.1": {
+ "id": "3.2.1",
+ "status": "CURRENT",
+ "updated": "2011-07-18T11:30:00Z",
+ }
+ }
expected = {
- "id": "3.2.1",
- "status": "CURRENT",
- "updated": "2011-07-18T11:30:00Z",
- "links": [
+ "versions": [
{
- "rel": "self",
- "href": "http://example.org/3.2.1/",
- },
- ],
+ "id": "3.2.1",
+ "status": "CURRENT",
+ "updated": "2011-07-18T11:30:00Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://example.org/3.2.1/",
+ },
+ ],
+ }
+ ]
}
builder = views.versions.ViewBuilder(base_url)
- output = builder.build(version_data)
+ output = builder.build_versions(version_data)
self.assertEqual(output, expected)
@@ -163,7 +643,9 @@ class VersionsTest(test.TestCase):
self.assertEqual(actual, expected)
- def test_xml_serializer(self):
+
+class VersionsSerializerTests(test.TestCase):
+ def test_versions_list_xml_serializer(self):
versions_data = {
'versions': [
{
@@ -180,20 +662,137 @@ class VersionsTest(test.TestCase):
]
}
- expected = """
- <versions>
- <version id="2.7.1" status="DEPRECATED"
- updated="2011-07-18T11:30:00Z">
- <atom:link href="http://test/2.7.1" rel="self"/>
- </version>
- </versions>""".replace(" ", "").replace("\n", "")
+ serializer = versions.VersionsXMLSerializer()
+ response = serializer.index(versions_data)
+
+ root = xml.etree.ElementTree.XML(response)
+ self.assertEqual(root.tag.split('}')[1], "versions")
+ self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11)
+ version = list(root)[0]
+ self.assertEqual(version.tag.split('}')[1], "version")
+ self.assertEqual(version.get('id'),
+ versions_data['versions'][0]['id'])
+ self.assertEqual(version.get('status'),
+ versions_data['versions'][0]['status'])
+
+ link = list(version)[0]
+
+ self.assertEqual(link.tag.split('}')[1], "link")
+ self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM)
+ for key, val in versions_data['versions'][0]['links'][0].items():
+ self.assertEqual(link.get(key), val)
+
+ def test_versions_multi_xml_serializer(self):
+ versions_data = {
+ 'choices': [
+ {
+ "id": "2.7.1",
+ "updated": "2011-07-18T11:30:00Z",
+ "status": "DEPRECATED",
+ "media-types": VERSIONS['v1.1']['media-types'],
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://test/2.7.1/images",
+ },
+ ],
+ },
+ ]
+ }
+
+ serializer = versions.VersionsXMLSerializer()
+ response = serializer.multi(versions_data)
+
+ root = xml.etree.ElementTree.XML(response)
+ self.assertEqual(root.tag.split('}')[1], "choices")
+ self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11)
+ version = list(root)[0]
+ self.assertEqual(version.tag.split('}')[1], "version")
+ self.assertEqual(version.get('id'), versions_data['choices'][0]['id'])
+ self.assertEqual(version.get('status'),
+ versions_data['choices'][0]['status'])
+
+ media_types = list(version)[0]
+ media_type_nodes = list(media_types)
+ self.assertEqual(media_types.tag.split('}')[1], "media-types")
+
+ set_types = versions_data['choices'][0]['media-types']
+ for i, type in enumerate(set_types):
+ node = media_type_nodes[i]
+ self.assertEqual(node.tag.split('}')[1], "media-type")
+ for key, val in set_types[i].items():
+ self.assertEqual(node.get(key), val)
+
+ link = list(version)[1]
+
+ self.assertEqual(link.tag.split('}')[1], "link")
+ self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM)
+ for key, val in versions_data['choices'][0]['links'][0].items():
+ self.assertEqual(link.get(key), val)
+
+ def test_version_detail_xml_serializer(self):
+ version_data = {
+ "version": {
+ "id": "v1.0",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v1.0/",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.0/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.0+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.0+json",
+ },
+ ],
+ },
+ }
serializer = versions.VersionsXMLSerializer()
- response = serializer.default(versions_data)
- response = response.replace(" ", "").replace("\n", "")
- self.assertEqual(expected, response)
+ response = serializer.show(version_data)
+
+ root = xml.etree.ElementTree.XML(response)
+ self.assertEqual(root.tag.split('}')[1], "version")
+ self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11)
- def test_atom_serializer(self):
+ children = list(root)
+ media_types = children[0]
+ media_type_nodes = list(media_types)
+ links = (children[1], children[2], children[3])
+
+ self.assertEqual(media_types.tag.split('}')[1], 'media-types')
+ for i, media_node in enumerate(media_type_nodes):
+ self.assertEqual(media_node.tag.split('}')[1], 'media-type')
+ for key, val in version_data['version']['media-types'][i].items():
+ self.assertEqual(val, media_node.get(key))
+
+ for i, link in enumerate(links):
+ self.assertEqual(link.tag.split('}')[0].strip('{'),
+ 'http://www.w3.org/2005/Atom')
+ self.assertEqual(link.tag.split('}')[1], 'link')
+ for key, val in version_data['version']['links'][i].items():
+ self.assertEqual(val, link.get(key))
+
+ def test_versions_list_atom_serializer(self):
versions_data = {
'versions': [
{
@@ -210,45 +809,158 @@ class VersionsTest(test.TestCase):
]
}
- expected = """
- <feed xmlns="http://www.w3.org/2005/Atom">
- <title type="text">
- Available API Versions
- </title>
- <updated>
- 2011-07-20T11:40:00Z
- </updated>
- <id>
- http://test/
- </id>
- <author>
- <name>
- Rackspace
- </name>
- <uri>
- http://www.rackspace.com/
- </uri>
- </author>
- <link href="http://test/" rel="self"/>
- <entry>
- <id>
- http://test/2.9.8
- </id>
- <title type="text">
- Version 2.9.8
- </title>
- <updated>
- 2011-07-20T11:40:00Z
- </updated>
- <link href="http://test/2.9.8" rel="self"/>
- <content type="text">
- Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)
- </content>
- </entry>
- </feed>""".replace(" ", "").replace("\n", "")
+ serializer = versions.VersionsAtomSerializer()
+ response = serializer.index(versions_data)
+
+ root = xml.etree.ElementTree.XML(response)
+ self.assertEqual(root.tag.split('}')[1], "feed")
+ self.assertEqual(root.tag.split('}')[0].strip('{'),
+ "http://www.w3.org/2005/Atom")
+
+ children = list(root)
+ title = children[0]
+ updated = children[1]
+ id = children[2]
+ author = children[3]
+ link = children[4]
+ entry = children[5]
+
+ self.assertEqual(title.tag.split('}')[1], 'title')
+ self.assertEqual(title.text, 'Available API Versions')
+ self.assertEqual(updated.tag.split('}')[1], 'updated')
+ self.assertEqual(updated.text, '2011-07-20T11:40:00Z')
+ self.assertEqual(id.tag.split('}')[1], 'id')
+ self.assertEqual(id.text, 'http://test/')
+
+ self.assertEqual(author.tag.split('}')[1], 'author')
+ author_name = list(author)[0]
+ author_uri = list(author)[1]
+ self.assertEqual(author_name.tag.split('}')[1], 'name')
+ self.assertEqual(author_name.text, 'Rackspace')
+ self.assertEqual(author_uri.tag.split('}')[1], 'uri')
+ self.assertEqual(author_uri.text, 'http://www.rackspace.com/')
+
+ self.assertEqual(link.get('href'), 'http://test/')
+ self.assertEqual(link.get('rel'), 'self')
+
+ self.assertEqual(entry.tag.split('}')[1], 'entry')
+ entry_children = list(entry)
+ entry_id = entry_children[0]
+ entry_title = entry_children[1]
+ entry_updated = entry_children[2]
+ entry_link = entry_children[3]
+ entry_content = entry_children[4]
+ self.assertEqual(entry_id.tag.split('}')[1], "id")
+ self.assertEqual(entry_id.text, "http://test/2.9.8")
+ self.assertEqual(entry_title.tag.split('}')[1], "title")
+ self.assertEqual(entry_title.get('type'), "text")
+ self.assertEqual(entry_title.text, "Version 2.9.8")
+ self.assertEqual(entry_updated.tag.split('}')[1], "updated")
+ self.assertEqual(entry_updated.text, "2011-07-20T11:40:00Z")
+ self.assertEqual(entry_link.tag.split('}')[1], "link")
+ self.assertEqual(entry_link.get('href'), "http://test/2.9.8")
+ self.assertEqual(entry_link.get('rel'), "self")
+ self.assertEqual(entry_content.tag.split('}')[1], "content")
+ self.assertEqual(entry_content.get('type'), "text")
+ self.assertEqual(entry_content.text,
+ "Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)")
+
+ def test_version_detail_atom_serializer(self):
+ versions_data = {
+ "version": {
+ "id": "v1.1",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v1.1/",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/cs-devguide-20110125.pdf",
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/"
+ "servers/api/v1.1/application.wadl",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute-v1.1+xml",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute-v1.1+json",
+ }
+ ],
+ },
+ }
serializer = versions.VersionsAtomSerializer()
- response = serializer.default(versions_data)
- print response
- response = response.replace(" ", "").replace("\n", "")
- self.assertEqual(expected, response)
+ response = serializer.show(versions_data)
+
+ root = xml.etree.ElementTree.XML(response)
+ self.assertEqual(root.tag.split('}')[1], "feed")
+ self.assertEqual(root.tag.split('}')[0].strip('{'),
+ "http://www.w3.org/2005/Atom")
+
+ children = list(root)
+ title = children[0]
+ updated = children[1]
+ id = children[2]
+ author = children[3]
+ link = children[4]
+ entry = children[5]
+
+ self.assertEqual(root.tag.split('}')[1], 'feed')
+ self.assertEqual(title.tag.split('}')[1], 'title')
+ self.assertEqual(title.text, 'About This Version')
+ self.assertEqual(updated.tag.split('}')[1], 'updated')
+ self.assertEqual(updated.text, '2011-01-21T11:33:21Z')
+ self.assertEqual(id.tag.split('}')[1], 'id')
+ self.assertEqual(id.text, 'http://localhost/v1.1/')
+
+ self.assertEqual(author.tag.split('}')[1], 'author')
+ author_name = list(author)[0]
+ author_uri = list(author)[1]
+ self.assertEqual(author_name.tag.split('}')[1], 'name')
+ self.assertEqual(author_name.text, 'Rackspace')
+ self.assertEqual(author_uri.tag.split('}')[1], 'uri')
+ self.assertEqual(author_uri.text, 'http://www.rackspace.com/')
+
+ self.assertEqual(link.get('href'),
+ 'http://localhost/v1.1/')
+ self.assertEqual(link.get('rel'), 'self')
+
+ self.assertEqual(entry.tag.split('}')[1], 'entry')
+ entry_children = list(entry)
+ entry_id = entry_children[0]
+ entry_title = entry_children[1]
+ entry_updated = entry_children[2]
+ entry_links = (entry_children[3], entry_children[4], entry_children[5])
+ entry_content = entry_children[6]
+
+ self.assertEqual(entry_id.tag.split('}')[1], "id")
+ self.assertEqual(entry_id.text,
+ "http://localhost/v1.1/")
+ self.assertEqual(entry_title.tag.split('}')[1], "title")
+ self.assertEqual(entry_title.get('type'), "text")
+ self.assertEqual(entry_title.text, "Version v1.1")
+ self.assertEqual(entry_updated.tag.split('}')[1], "updated")
+ self.assertEqual(entry_updated.text, "2011-01-21T11:33:21Z")
+
+ for i, link in enumerate(versions_data["version"]["links"]):
+ self.assertEqual(entry_links[i].tag.split('}')[1], "link")
+ for key, val in versions_data["version"]["links"][i].items():
+ self.assertEqual(entry_links[i].get(key), val)
+
+ self.assertEqual(entry_content.tag.split('}')[1], "content")
+ self.assertEqual(entry_content.get('type'), "text")
+ self.assertEqual(entry_content.text,
+ "Version v1.1 CURRENT (2011-01-21T11:33:21Z)")
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 3deb844aa..4a46a5764 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -29,7 +29,6 @@ from nova.scheduler import api
FLAGS = flags.FLAGS
-FLAGS.verbose = True
def zone_get(context, zone_id):
@@ -95,7 +94,7 @@ def zone_select(context, specs):
class ZonesTest(test.TestCase):
def setUp(self):
super(ZonesTest, self).setUp()
- self.flags(allow_admin_api=True)
+ self.flags(verbose=True, allow_admin_api=True)
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py
index aac3ff330..d51b19ccd 100644
--- a/nova/tests/glance/stubs.py
+++ b/nova/tests/glance/stubs.py
@@ -60,7 +60,10 @@ class FakeGlance(object):
'container_format': 'ovf'},
'image_data': StringIO.StringIO('')}}
- def __init__(self, host, port=None, use_ssl=False):
+ def __init__(self, host, port=None, use_ssl=False, auth_tok=None):
+ pass
+
+ def set_auth_token(self, auth_tok):
pass
def get_image_meta(self, image_id):
diff --git a/nova/tests/hyperv_unittest.py b/nova/tests/hyperv_unittest.py
index 0ea196950..d346d0a70 100644
--- a/nova/tests/hyperv_unittest.py
+++ b/nova/tests/hyperv_unittest.py
@@ -21,13 +21,9 @@ import random
from nova import context
from nova import db
-from nova import flags
from nova import test
from nova.virt import hyperv
-FLAGS = flags.FLAGS
-FLAGS.connection_type = 'hyperv'
-
class HyperVTestCase(test.TestCase):
"""Test cases for the Hyper-V driver"""
@@ -36,6 +32,7 @@ class HyperVTestCase(test.TestCase):
self.user_id = 'fake'
self.project_id = 'fake'
self.context = context.RequestContext(self.user_id, self.project_id)
+ self.flags(connection_type='hyperv')
def test_create_destroy(self):
"""Create a VM and destroy it"""
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
index 223e7ae57..0ff508ffa 100644
--- a/nova/tests/image/test_glance.py
+++ b/nova/tests/image/test_glance.py
@@ -31,6 +31,9 @@ class StubGlanceClient(object):
self.add_response = add_response
self.update_response = update_response
+ def set_auth_token(self, auth_tok):
+ pass
+
def get_image_meta(self, image_id):
return self.images[image_id]
@@ -232,3 +235,39 @@ class TestMutatorDateTimeTests(BaseGlanceTest):
'updated_at': None,
'deleted_at': None}
return fixture
+
+
+class TestGlanceSerializer(unittest.TestCase):
+ def test_serialize(self):
+ metadata = {'name': 'image1',
+ 'is_public': True,
+ 'foo': 'bar',
+ 'properties': {
+ 'prop1': 'propvalue1',
+ 'mappings': [
+ {'virtual': 'aaa',
+ 'device': 'bbb'},
+ {'virtual': 'xxx',
+ 'device': 'yyy'}],
+ 'block_device_mapping': [
+ {'virtual_device': 'fake',
+ 'device_name': '/dev/fake'},
+ {'virtual_device': 'ephemeral0',
+ 'device_name': '/dev/fake0'}]}}
+
+ converted_expected = {
+ 'name': 'image1',
+ 'is_public': True,
+ 'foo': 'bar',
+ 'properties': {
+ 'prop1': 'propvalue1',
+ 'mappings':
+ '[{"device": "bbb", "virtual": "aaa"}, '
+ '{"device": "yyy", "virtual": "xxx"}]',
+ 'block_device_mapping':
+ '[{"virtual_device": "fake", "device_name": "/dev/fake"}, '
+ '{"virtual_device": "ephemeral0", '
+ '"device_name": "/dev/fake0"}]'}}
+ converted = glance._convert_to_string(metadata)
+ self.assertEqual(converted, converted_expected)
+ self.assertEqual(glance._convert_from_string(converted), metadata)
diff --git a/nova/tests/image/test_s3.py b/nova/tests/image/test_s3.py
index 231e109f8..f1ceeb7fe 100644
--- a/nova/tests/image/test_s3.py
+++ b/nova/tests/image/test_s3.py
@@ -16,12 +16,9 @@
# under the License.
from nova import context
-from nova import flags
from nova import test
from nova.image import s3
-FLAGS = flags.FLAGS
-
ami_manifest_xml = """<?xml version="1.0" ?>
<manifest>
@@ -59,15 +56,10 @@ ami_manifest_xml = """<?xml version="1.0" ?>
class TestS3ImageService(test.TestCase):
def setUp(self):
super(TestS3ImageService, self).setUp()
- self.orig_image_service = FLAGS.image_service
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
self.image_service = s3.S3ImageService()
self.context = context.RequestContext(None, None)
- def tearDown(self):
- super(TestS3ImageService, self).tearDown()
- FLAGS.image_service = self.orig_image_service
-
def _assertEqualList(self, list0, list1, keys):
self.assertEqual(len(list0), len(list1))
key = keys[0]
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index 47bd8c1e4..fb2f88502 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -23,7 +23,6 @@ import random
import string
from nova import exception
-from nova import flags
from nova import service
from nova import test # For the flags
from nova.auth import manager
@@ -32,8 +31,6 @@ from nova.log import logging
from nova.tests.integrated.api import client
-FLAGS = flags.FLAGS
-
LOG = logging.getLogger('nova.tests.integrated')
@@ -151,6 +148,7 @@ class _IntegratedTestBase(test.TestCase):
f = self._get_flags()
self.flags(**f)
+ self.flags(verbose=True)
def fake_get_image_service(image_href):
image_id = int(str(image_href).split('/')[-1])
diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py
index 0d4ee8cab..c22cf0be0 100644
--- a/nova/tests/integrated/test_extensions.py
+++ b/nova/tests/integrated/test_extensions.py
@@ -17,7 +17,6 @@
import os
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
@@ -25,10 +24,6 @@ from nova.tests.integrated import integrated_helpers
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class ExtensionsTest(integrated_helpers._IntegratedTestBase):
def _get_flags(self):
f = super(ExtensionsTest, self)._get_flags()
diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py
index a5180b6bc..06359a52f 100644
--- a/nova/tests/integrated/test_login.py
+++ b/nova/tests/integrated/test_login.py
@@ -17,7 +17,6 @@
import unittest
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -25,9 +24,6 @@ from nova.tests.integrated.api import client
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
class LoginTest(integrated_helpers._IntegratedTestBase):
def test_login(self):
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index 67b3c485a..150279a95 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -18,7 +18,6 @@
import time
import unittest
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -27,10 +26,6 @@ from nova.tests.integrated.api import client
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class ServersTest(integrated_helpers._IntegratedTestBase):
def test_get_servers(self):
"""Simple check that listing servers works."""
diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py
index e9fb3c4d1..d3e936462 100644
--- a/nova/tests/integrated/test_volumes.py
+++ b/nova/tests/integrated/test_volumes.py
@@ -18,7 +18,6 @@
import unittest
import time
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.tests.integrated.api import client
@@ -28,10 +27,6 @@ from nova.volume import driver
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class VolumesTest(integrated_helpers._IntegratedTestBase):
def setUp(self):
super(VolumesTest, self).setUp()
diff --git a/nova/tests/integrated/test_xml.py b/nova/tests/integrated/test_xml.py
index fde32f797..74baaacc2 100644
--- a/nova/tests/integrated/test_xml.py
+++ b/nova/tests/integrated/test_xml.py
@@ -15,7 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova import flags
from nova.log import logging
from nova.tests.integrated import integrated_helpers
from nova.api.openstack import common
@@ -24,10 +23,6 @@ from nova.api.openstack import common
LOG = logging.getLogger('nova.tests.integrated')
-FLAGS = flags.FLAGS
-FLAGS.verbose = True
-
-
class XmlTests(integrated_helpers._IntegratedTestBase):
""""Some basic XML sanity checks."""
diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py
index b1892dab4..7e664d3f9 100644
--- a/nova/tests/scheduler/test_host_filter.py
+++ b/nova/tests/scheduler/test_host_filter.py
@@ -19,12 +19,9 @@ Tests For Scheduler Host Filters.
import json
from nova import exception
-from nova import flags
from nova import test
from nova.scheduler import host_filter
-FLAGS = flags.FLAGS
-
class FakeZoneManager:
pass
@@ -57,9 +54,9 @@ class HostFilterTestCase(test.TestCase):
'host_name-label': 'xs-%s' % multiplier}
def setUp(self):
- self.old_flag = FLAGS.default_host_filter
- FLAGS.default_host_filter = \
- 'nova.scheduler.host_filter.AllHostsFilter'
+ super(HostFilterTestCase, self).setUp()
+ default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter'
+ self.flags(default_host_filter=default_host_filter)
self.instance_type = dict(name='tiny',
memory_mb=50,
vcpus=10,
@@ -98,9 +95,6 @@ class HostFilterTestCase(test.TestCase):
host09['xpu_arch'] = 'fermi'
host09['xpu_info'] = 'Tesla 2150'
- def tearDown(self):
- FLAGS.default_host_filter = self.old_flag
-
def test_choose_filter(self):
# Test default filter ...
hf = host_filter.choose_host_filter()
diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py
index 49791053e..fbe6b2f77 100644
--- a/nova/tests/scheduler/test_least_cost_scheduler.py
+++ b/nova/tests/scheduler/test_least_cost_scheduler.py
@@ -16,13 +16,11 @@
Tests For Least Cost Scheduler
"""
-from nova import flags
from nova import test
from nova.scheduler import least_cost
from nova.tests.scheduler import test_zone_aware_scheduler
MB = 1024 * 1024
-FLAGS = flags.FLAGS
class FakeHost(object):
@@ -95,10 +93,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_noop_cost_fn(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.noop_cost_fn',
- ]
- FLAGS.noop_cost_fn_weight = 1
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.noop_cost_fn'],
+ noop_cost_fn_weight=1)
num = 1
request_spec = {}
@@ -109,10 +106,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_cost_fn_weights(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.noop_cost_fn',
- ]
- FLAGS.noop_cost_fn_weight = 2
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.noop_cost_fn'],
+ noop_cost_fn_weight=2)
num = 1
request_spec = {}
@@ -123,10 +119,9 @@ class LeastCostSchedulerTestCase(test.TestCase):
self.assertWeights(expected, num, request_spec, hosts)
def test_compute_fill_first_cost_fn(self):
- FLAGS.least_cost_scheduler_cost_functions = [
- 'nova.scheduler.least_cost.compute_fill_first_cost_fn',
- ]
- FLAGS.compute_fill_first_cost_fn_weight = 1
+ self.flags(least_cost_scheduler_cost_functions=[
+ 'nova.scheduler.least_cost.compute_fill_first_cost_fn'],
+ compute_fill_first_cost_fn_weight=1)
num = 1
instance_type = {'memory_mb': 1024}
diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py
index 0d7634d0d..7a26fd1bb 100644
--- a/nova/tests/scheduler/test_scheduler.py
+++ b/nova/tests/scheduler/test_scheduler.py
@@ -21,9 +21,11 @@ Tests For Scheduler
import datetime
import mox
-import novaclient.exceptions
import stubout
+from novaclient import v1_1 as novaclient
+from novaclient import exceptions as novaclient_exceptions
+
from mox import IgnoreArg
from nova import context
from nova import db
@@ -962,13 +964,10 @@ class ZoneRedirectTest(test.TestCase):
self.stubs.Set(db, 'zone_get_all', zone_get_all)
self.stubs.Set(db, 'instance_get_by_uuid',
fake_instance_get_by_uuid)
-
- self.enable_zone_routing = FLAGS.enable_zone_routing
- FLAGS.enable_zone_routing = True
+ self.flags(enable_zone_routing=True)
def tearDown(self):
self.stubs.UnsetAll()
- FLAGS.enable_zone_routing = self.enable_zone_routing
super(ZoneRedirectTest, self).tearDown()
def test_trap_found_locally(self):
@@ -998,7 +997,7 @@ class ZoneRedirectTest(test.TestCase):
self.assertEquals(e.results['magic'], 'found me')
def test_routing_flags(self):
- FLAGS.enable_zone_routing = False
+ self.flags(enable_zone_routing=False)
decorator = FakeRerouteCompute("foo")
self.assertRaises(exception.InstanceNotFound, decorator(go_boom),
None, None, 1)
@@ -1039,10 +1038,10 @@ class FakeServerCollection(object):
class FakeEmptyServerCollection(object):
def get(self, f):
- raise novaclient.NotFound(1)
+ raise novaclient_exceptions.NotFound(1)
def find(self, name):
- raise novaclient.NotFound(2)
+ raise novaclient_exceptions.NotFound(2)
class FakeNovaClient(object):
@@ -1088,7 +1087,7 @@ class FakeZonesProxy(object):
raise Exception('testing')
-class FakeNovaClientOpenStack(object):
+class FakeNovaClientZones(object):
def __init__(self, *args, **kwargs):
self.zones = FakeZonesProxy()
@@ -1101,7 +1100,7 @@ class CallZoneMethodTest(test.TestCase):
super(CallZoneMethodTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.stubs.Set(db, 'zone_get_all', zone_get_all)
- self.stubs.Set(novaclient, 'OpenStack', FakeNovaClientOpenStack)
+ self.stubs.Set(novaclient, 'Client', FakeNovaClientZones)
def tearDown(self):
self.stubs.UnsetAll()
diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py
index 7833028c3..788efca52 100644
--- a/nova/tests/scheduler/test_zone_aware_scheduler.py
+++ b/nova/tests/scheduler/test_zone_aware_scheduler.py
@@ -21,7 +21,9 @@ import json
import nova.db
from nova import exception
+from nova import rpc
from nova import test
+from nova.compute import api as compute_api
from nova.scheduler import driver
from nova.scheduler import zone_aware_scheduler
from nova.scheduler import zone_manager
@@ -114,7 +116,7 @@ def fake_provision_resource_from_blob(context, item, instance_id,
def fake_decrypt_blob_returns_local_info(blob):
- return {'foo': True} # values aren't important.
+ return {'hostname': 'foooooo'} # values aren't important.
def fake_decrypt_blob_returns_child_info(blob):
@@ -283,14 +285,29 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
global was_called
sched = FakeZoneAwareScheduler()
was_called = False
+
+ def fake_create_db_entry_for_new_instance(self, context,
+ image, base_options, security_group,
+ block_device_mapping, num=1):
+ global was_called
+ was_called = True
+ # return fake instances
+ return {'id': 1, 'uuid': 'f874093c-7b17-49c0-89c3-22a5348497f9'}
+
+ def fake_rpc_cast(*args, **kwargs):
+ pass
+
self.stubs.Set(sched, '_decrypt_blob',
fake_decrypt_blob_returns_local_info)
- self.stubs.Set(sched, '_provision_resource_locally',
- fake_provision_resource_locally)
+ self.stubs.Set(compute_api.API,
+ 'create_db_entry_for_new_instance',
+ fake_create_db_entry_for_new_instance)
+ self.stubs.Set(rpc, 'cast', fake_rpc_cast)
- request_spec = {'blob': "Non-None blob data"}
+ build_plan_item = {'blob': "Non-None blob data"}
+ request_spec = {'image': {}, 'instance_properties': {}}
- sched._provision_resource_from_blob(None, request_spec, 1,
+ sched._provision_resource_from_blob(None, build_plan_item, 1,
request_spec, {})
self.assertTrue(was_called)
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index d9b1d39c9..2011ae756 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -27,6 +27,7 @@ import random
import StringIO
import webob
+from nova import block_device
from nova import context
from nova import exception
from nova import test
@@ -147,10 +148,12 @@ class Ec2utilsTestCase(test.TestCase):
properties0 = {'mappings': mappings}
properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings}
- root_device_name = ec2utils.properties_root_device_name(properties0)
+ root_device_name = block_device.properties_root_device_name(
+ properties0)
self.assertEqual(root_device_name, '/dev/sda1')
- root_device_name = ec2utils.properties_root_device_name(properties1)
+ root_device_name = block_device.properties_root_device_name(
+ properties1)
self.assertEqual(root_device_name, '/dev/sdb')
def test_mapping_prepend_dev(self):
@@ -184,7 +187,7 @@ class Ec2utilsTestCase(test.TestCase):
'device': '/dev/sdc1'},
{'virtual': 'ephemeral1',
'device': '/dev/sdc1'}]
- self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings),
+ self.assertDictListMatch(block_device.mappings_prepend_dev(mappings),
expected_result)
@@ -336,6 +339,33 @@ class ApiEc2TestCase(test.TestCase):
self.ec2.delete_security_group(security_group_name)
+ def test_group_name_valid_chars_security_group(self):
+ """ Test that we sanely handle invalid security group names.
+ API Spec states we should only accept alphanumeric characters,
+ spaces, dashes, and underscores. """
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ # Test block group_name of non alphanumeric characters, spaces,
+ # dashes, and underscores.
+ security_group_name = "aa #^% -=99"
+
+ self.assertRaises(EC2ResponseError, self.ec2.create_security_group,
+ security_group_name, 'test group')
+
+ def test_group_name_valid_length_security_group(self):
+ """Test that we sanely handle invalid security group names.
+ API Spec states that the length should not exceed 255 chars """
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ # Test block group_name > 255 chars
+ security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc")
+ for x in range(random.randint(256, 266)))
+
+ self.assertRaises(EC2ResponseError, self.ec2.create_security_group,
+ security_group_name, 'test group')
+
def test_authorize_revoke_security_group_cidr(self):
"""
Test that we can add and remove CIDR based rules
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index 0fb2fdcc6..4561eb7f2 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -88,9 +88,9 @@ class user_and_project_generator(object):
class _AuthManagerBaseTestCase(test.TestCase):
def setUp(self):
- FLAGS.auth_driver = self.auth_driver
super(_AuthManagerBaseTestCase, self).setUp()
- self.flags(connection_type='fake')
+ self.flags(auth_driver=self.auth_driver,
+ connection_type='fake')
self.manager = manager.AuthManager(new=True)
self.manager.mc.cache = {}
diff --git a/nova/tests/test_block_device.py b/nova/tests/test_block_device.py
new file mode 100644
index 000000000..b8e9b35e2
--- /dev/null
+++ b/nova/tests/test_block_device.py
@@ -0,0 +1,87 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata
+# 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.
+
+"""
+Tests for Block Device utility functions.
+"""
+
+from nova import block_device
+from nova import test
+
+
+class BlockDeviceTestCase(test.TestCase):
+ def test_properties(self):
+ root_device0 = '/dev/sda'
+ root_device1 = '/dev/sdb'
+ mappings = [{'virtual': 'root',
+ 'device': root_device0}]
+
+ properties0 = {'mappings': mappings}
+ properties1 = {'mappings': mappings,
+ 'root_device_name': root_device1}
+
+ self.assertEqual(block_device.properties_root_device_name({}), None)
+ self.assertEqual(
+ block_device.properties_root_device_name(properties0),
+ root_device0)
+ self.assertEqual(
+ block_device.properties_root_device_name(properties1),
+ root_device1)
+
+ def test_ephemeral(self):
+ self.assertFalse(block_device.is_ephemeral('ephemeral'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral0'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral1'))
+ self.assertTrue(block_device.is_ephemeral('ephemeral11'))
+ self.assertFalse(block_device.is_ephemeral('root'))
+ self.assertFalse(block_device.is_ephemeral('swap'))
+ self.assertFalse(block_device.is_ephemeral('/dev/sda1'))
+
+ self.assertEqual(block_device.ephemeral_num('ephemeral0'), 0)
+ self.assertEqual(block_device.ephemeral_num('ephemeral1'), 1)
+ self.assertEqual(block_device.ephemeral_num('ephemeral11'), 11)
+
+ self.assertFalse(block_device.is_swap_or_ephemeral('ephemeral'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral0'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral1'))
+ self.assertTrue(block_device.is_swap_or_ephemeral('swap'))
+ self.assertFalse(block_device.is_swap_or_ephemeral('root'))
+ self.assertFalse(block_device.is_swap_or_ephemeral('/dev/sda1'))
+
+ def test_mappings_prepend_dev(self):
+ mapping = [
+ {'virtual': 'ami', 'device': '/dev/sda'},
+ {'virtual': 'root', 'device': 'sda'},
+ {'virtual': 'ephemeral0', 'device': 'sdb'},
+ {'virtual': 'swap', 'device': 'sdc'},
+ {'virtual': 'ephemeral1', 'device': 'sdd'},
+ {'virtual': 'ephemeral2', 'device': 'sde'}]
+
+ expected = [
+ {'virtual': 'ami', 'device': '/dev/sda'},
+ {'virtual': 'root', 'device': 'sda'},
+ {'virtual': 'ephemeral0', 'device': '/dev/sdb'},
+ {'virtual': 'swap', 'device': '/dev/sdc'},
+ {'virtual': 'ephemeral1', 'device': '/dev/sdd'},
+ {'virtual': 'ephemeral2', 'device': '/dev/sde'}]
+
+ prepended = block_device.mappings_prepend_dev(mapping)
+ self.assertEqual(prepended.sort(), expected.sort())
+
+ def test_strip_dev(self):
+ self.assertEqual(block_device.strip_dev('/dev/sda'), 'sda')
+ self.assertEqual(block_device.strip_dev('sda'), 'sda')
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index 8c1a74c70..b2afc53c9 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -17,6 +17,8 @@
# under the License.
import mox
+import functools
+
from base64 import b64decode
from M2Crypto import BIO
from M2Crypto import RSA
@@ -99,11 +101,9 @@ class CloudTestCase(test.TestCase):
"""Makes sure describe regions runs without raising an exception"""
result = self.cloud.describe_regions(self.context)
self.assertEqual(len(result['regionInfo']), 1)
- regions = FLAGS.region_list
- FLAGS.region_list = ["one=test_host1", "two=test_host2"]
+ self.flags(region_list=["one=test_host1", "two=test_host2"])
result = self.cloud.describe_regions(self.context)
self.assertEqual(len(result['regionInfo']), 2)
- FLAGS.region_list = regions
def test_describe_addresses(self):
"""Makes sure describe addresses runs without raising an exception"""
@@ -894,13 +894,16 @@ class CloudTestCase(test.TestCase):
def test_modify_image_attribute(self):
modify_image_attribute = self.cloud.modify_image_attribute
+ fake_metadata = {'id': 1, 'container_format': 'ami',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
+ 'type': 'machine'}, 'is_public': False}
+
def fake_show(meh, context, id):
- return {'id': 1, 'container_format': 'ami',
- 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
- 'type': 'machine'}, 'is_public': False}
+ return fake_metadata
def fake_update(meh, context, image_id, metadata, data=None):
- return metadata
+ fake_metadata.update(metadata)
+ return fake_metadata
self.stubs.Set(fake._FakeImageService, 'show', fake_show)
self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
@@ -1466,3 +1469,147 @@ class CloudTestCase(test.TestCase):
# TODO(yamahata): clean up snapshot created by CreateImage.
self._restart_compute_service()
+
+ @staticmethod
+ def _fake_bdm_get(ctxt, id):
+ return [{'volume_id': 87654321,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': None,
+ 'delete_on_termination': True,
+ 'device_name': '/dev/sdh'},
+ {'volume_id': None,
+ 'snapshot_id': 98765432,
+ 'no_device': None,
+ 'virtual_name': None,
+ 'delete_on_termination': True,
+ 'device_name': '/dev/sdi'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': True,
+ 'virtual_name': None,
+ 'delete_on_termination': None,
+ 'device_name': None},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral0',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdb'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'swap',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdc'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral1',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sdd'},
+ {'volume_id': None,
+ 'snapshot_id': None,
+ 'no_device': None,
+ 'virtual_name': 'ephemeral2',
+ 'delete_on_termination': None,
+ 'device_name': '/dev/sd3'},
+ ]
+
+ def test_get_instance_mapping(self):
+ """Make sure that _get_instance_mapping works"""
+ ctxt = None
+ instance_ref0 = {'id': 0,
+ 'root_device_name': None}
+ instance_ref1 = {'id': 0,
+ 'root_device_name': '/dev/sda1'}
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ self._fake_bdm_get)
+
+ expected = {'ami': 'sda1',
+ 'root': '/dev/sda1',
+ 'ephemeral0': '/dev/sdb',
+ 'swap': '/dev/sdc',
+ 'ephemeral1': '/dev/sdd',
+ 'ephemeral2': '/dev/sd3'}
+
+ self.assertEqual(self.cloud._format_instance_mapping(ctxt,
+ instance_ref0),
+ cloud._DEFAULT_MAPPINGS)
+ self.assertEqual(self.cloud._format_instance_mapping(ctxt,
+ instance_ref1),
+ expected)
+
+ def test_describe_instance_attribute(self):
+ """Make sure that describe_instance_attribute works"""
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ self._fake_bdm_get)
+
+ def fake_get(ctxt, instance_id):
+ return {
+ 'id': 0,
+ 'root_device_name': '/dev/sdh',
+ 'security_groups': [{'name': 'fake0'}, {'name': 'fake1'}],
+ 'state_description': 'stopping',
+ 'instance_type': {'name': 'fake_type'},
+ 'kernel_id': 1,
+ 'ramdisk_id': 2,
+ 'user_data': 'fake-user data',
+ }
+ self.stubs.Set(self.cloud.compute_api, 'get', fake_get)
+
+ def fake_volume_get(ctxt, volume_id, session=None):
+ if volume_id == 87654321:
+ return {'id': volume_id,
+ 'attach_time': '13:56:24',
+ 'status': 'in-use'}
+ raise exception.VolumeNotFound(volume_id=volume_id)
+ self.stubs.Set(db.api, 'volume_get', fake_volume_get)
+
+ get_attribute = functools.partial(
+ self.cloud.describe_instance_attribute,
+ self.context, 'i-12345678')
+
+ bdm = get_attribute('blockDeviceMapping')
+ bdm['blockDeviceMapping'].sort()
+
+ expected_bdm = {'instance_id': 'i-12345678',
+ 'rootDeviceType': 'ebs',
+ 'blockDeviceMapping': [
+ {'deviceName': '/dev/sdh',
+ 'ebs': {'status': 'in-use',
+ 'deleteOnTermination': True,
+ 'volumeId': 87654321,
+ 'attachTime': '13:56:24'}}]}
+ expected_bdm['blockDeviceMapping'].sort()
+ self.assertEqual(bdm, expected_bdm)
+ # NOTE(yamahata): this isn't supported
+ # get_attribute('disableApiTermination')
+ groupSet = get_attribute('groupSet')
+ groupSet['groupSet'].sort()
+ expected_groupSet = {'instance_id': 'i-12345678',
+ 'groupSet': [{'groupId': 'fake0'},
+ {'groupId': 'fake1'}]}
+ expected_groupSet['groupSet'].sort()
+ self.assertEqual(groupSet, expected_groupSet)
+ self.assertEqual(get_attribute('instanceInitiatedShutdownBehavior'),
+ {'instance_id': 'i-12345678',
+ 'instanceInitiatedShutdownBehavior': 'stop'})
+ self.assertEqual(get_attribute('instanceType'),
+ {'instance_id': 'i-12345678',
+ 'instanceType': 'fake_type'})
+ self.assertEqual(get_attribute('kernel'),
+ {'instance_id': 'i-12345678',
+ 'kernel': 'aki-00000001'})
+ self.assertEqual(get_attribute('ramdisk'),
+ {'instance_id': 'i-12345678',
+ 'ramdisk': 'ari-00000002'})
+ self.assertEqual(get_attribute('rootDeviceName'),
+ {'instance_id': 'i-12345678',
+ 'rootDeviceName': '/dev/sdh'})
+ # NOTE(yamahata): this isn't supported
+ # get_attribute('sourceDestCheck')
+ self.assertEqual(get_attribute('userData'),
+ {'instance_id': 'i-12345678',
+ 'userData': '}\xa9\x1e\xba\xc7\xabu\xabZ'})
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 9be349d14..73c9bd78d 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -26,6 +26,7 @@ from nova.compute import power_state
from nova import context
from nova import db
from nova.db.sqlalchemy import models
+from nova.db.sqlalchemy import api as sqlalchemy_api
from nova import exception
from nova import flags
import nova.image.fake
@@ -541,7 +542,9 @@ class ComputeTestCase(test.TestCase):
db.instance_update(self.context, instance_id, {'host': 'foo'})
- self.compute.prep_resize(context, inst_ref['uuid'], 3)
+ new_instance_type_ref = db.instance_type_get_by_flavor_id(context, 3)
+ self.compute.prep_resize(context, inst_ref['uuid'],
+ new_instance_type_ref['id'])
migration_ref = db.migration_get_by_instance_and_status(context,
inst_ref['uuid'], 'pre-migrating')
@@ -868,6 +871,458 @@ class ComputeTestCase(test.TestCase):
self.assertEqual(len(instances), 1)
self.assertEqual(power_state.SHUTOFF, instances[0]['state'])
+ def test_get_all_by_name_regexp(self):
+ """Test searching instances by name (display_name)"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'woo.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'woot.*'})
+ instance_ids = [instance.id for instance in instances]
+ self.assertEqual(len(instances), 1)
+ self.assertTrue(instance_id1 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': '.*oot.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'n.*'})
+ self.assertEqual(len(instances), 1)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'name': 'noth.*'})
+ self.assertEqual(len(instances), 0)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_instance_name_regexp(self):
+ """Test searching instances by name"""
+ self.flags(instance_name_template='instance-%d')
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance()
+ instance_id2 = self._create_instance({'id': 2})
+ instance_id3 = self._create_instance({'id': 10})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': 'instance.*'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': '.*\-\d$'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'instance_name': 'i.*2'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_by_fixed_ip(self):
+ """Test getting 1 instance by Fixed IP"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance()
+ instance_id2 = self._create_instance({'id': 20})
+ instance_id3 = self._create_instance({'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+
+ # regex not allowed
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '.*'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.3.1'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.1.1'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'fixed_ip': '1.1.2.1'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+
+ def test_get_all_by_ip_regexp(self):
+ """Test searching by Floating and Fixed IP"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+ fix_addr = db.fixed_ip_create(c,
+ {'address': '1.1.3.1',
+ 'instance_id': instance_id3,
+ 'virtual_interface_id': vif_ref3['id']})
+ fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
+ flo_ref = db.floating_ip_create(c,
+ {'address': '10.0.0.2',
+ 'fixed_ip_id': fix_ref['id']})
+
+ # ends up matching 2nd octet here.. so all 3 match
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '1.*'})
+ self.assertEqual(len(instances), 3)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1.\d+$'})
+ self.assertEqual(len(instances), 1)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.2.+'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '10.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.floating_ip_destroy(c, '10.0.0.2')
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_ipv6_regexp(self):
+ """Test searching by IPv6 address"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ # This will create IPv6 addresses of:
+ # 1: fd00::1034:56ff:fe78:9012
+ # 20: fd00::9212:34ff:fe56:7890
+ # 30: fd00::3656:78ff:fe90:1234
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '.*1034.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '^fd00.*'})
+ self.assertEqual(len(instances), 3)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id1 in instance_ids)
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip6': '^.*12.*34.*'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_multiple_options_at_once(self):
+ """Test searching by multiple options at once"""
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'display_name': 'woot'})
+ instance_id2 = self._create_instance({
+ 'display_name': 'woo',
+ 'id': 20})
+ instance_id3 = self._create_instance({
+ 'display_name': 'not-woot',
+ 'id': 30})
+
+ vif_ref1 = db.virtual_interface_create(c,
+ {'address': '12:34:56:78:90:12',
+ 'instance_id': instance_id1,
+ 'network_id': 1})
+ vif_ref2 = db.virtual_interface_create(c,
+ {'address': '90:12:34:56:78:90',
+ 'instance_id': instance_id2,
+ 'network_id': 1})
+ vif_ref3 = db.virtual_interface_create(c,
+ {'address': '34:56:78:90:12:34',
+ 'instance_id': instance_id3,
+ 'network_id': 1})
+
+ db.fixed_ip_create(c,
+ {'address': '1.1.1.1',
+ 'instance_id': instance_id1,
+ 'virtual_interface_id': vif_ref1['id']})
+ db.fixed_ip_create(c,
+ {'address': '1.1.2.1',
+ 'instance_id': instance_id2,
+ 'virtual_interface_id': vif_ref2['id']})
+ fix_addr = db.fixed_ip_create(c,
+ {'address': '1.1.3.1',
+ 'instance_id': instance_id3,
+ 'virtual_interface_id': vif_ref3['id']})
+ fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
+ flo_ref = db.floating_ip_create(c,
+ {'address': '10.0.0.2',
+ 'fixed_ip_id': fix_ref['id']})
+
+ # ip ends up matching 2nd octet here.. so all 3 match ip
+ # but 'name' only matches one
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1', 'name': 'not.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ # ip ends up matching any ip with a '2' in it.. so instance
+ # 2 and 3.. but name should only match #2
+ # but 'name' only matches one
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*2', 'name': '^woo.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id2)
+
+ # same as above but no match on name (name matches instance_id1
+ # but the ip query doesn't
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*2.*', 'name': '^woot.*'})
+ self.assertEqual(len(instances), 0)
+
+ # ip matches all 3... ipv6 matches #2+#3...name matches #3
+ instances = self.compute_api.get_all(c,
+ search_opts={'ip': '.*\.1',
+ 'name': 'not.*',
+ 'ip6': '^.*12.*34.*'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id3)
+
+ db.virtual_interface_delete(c, vif_ref1['id'])
+ db.virtual_interface_delete(c, vif_ref2['id'])
+ db.virtual_interface_delete(c, vif_ref3['id'])
+ db.floating_ip_destroy(c, '10.0.0.2')
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_image(self):
+ """Test searching instances by image"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'image_ref': '1234'})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'image_ref': '4567'})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'image_ref': '4567'})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '123'})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '1234'})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': '4567'})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ # Test passing a list as search arg
+ instances = self.compute_api.get_all(c,
+ search_opts={'image': ['1234', '4567']})
+ self.assertEqual(len(instances), 3)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_flavor(self):
+ """Test searching instances by image"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'instance_type_id': 1})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'instance_type_id': 2})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'instance_type_id': 2})
+
+ # NOTE(comstud): Migrations set up the instance_types table
+ # for us. Therefore, we assume the following is true for
+ # these tests:
+ # instance_type_id 1 == flavor 3
+ # instance_type_id 2 == flavor 1
+ # instance_type_id 3 == flavor 4
+ # instance_type_id 4 == flavor 5
+ # instance_type_id 5 == flavor 2
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 5})
+ self.assertEqual(len(instances), 0)
+
+ self.assertRaises(exception.FlavorNotFound,
+ self.compute_api.get_all,
+ c, search_opts={'flavor': 99})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 3})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'flavor': 1})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
+ def test_get_all_by_state(self):
+ """Test searching instances by state"""
+
+ c = context.get_admin_context()
+ instance_id1 = self._create_instance({'state': power_state.SHUTDOWN})
+ instance_id2 = self._create_instance({
+ 'id': 2,
+ 'state': power_state.RUNNING})
+ instance_id3 = self._create_instance({
+ 'id': 10,
+ 'state': power_state.RUNNING})
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.SUSPENDED})
+ self.assertEqual(len(instances), 0)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.SHUTDOWN})
+ self.assertEqual(len(instances), 1)
+ self.assertEqual(instances[0].id, instance_id1)
+
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': power_state.RUNNING})
+ self.assertEqual(len(instances), 2)
+ instance_ids = [instance.id for instance in instances]
+ self.assertTrue(instance_id2 in instance_ids)
+ self.assertTrue(instance_id3 in instance_ids)
+
+ # Test passing a list as search arg
+ instances = self.compute_api.get_all(c,
+ search_opts={'state': [power_state.SHUTDOWN,
+ power_state.RUNNING]})
+ self.assertEqual(len(instances), 3)
+
+ db.instance_destroy(c, instance_id1)
+ db.instance_destroy(c, instance_id2)
+ db.instance_destroy(c, instance_id3)
+
@staticmethod
def _parse_db_block_device_mapping(bdm_ref):
attr_list = ('delete_on_termination', 'device_name', 'no_device',
@@ -881,15 +1336,17 @@ class ComputeTestCase(test.TestCase):
return bdm
def test_update_block_device_mapping(self):
+ swap_size = 1
+ instance_type = {'swap': swap_size}
instance_id = self._create_instance()
mappings = [
{'virtual': 'ami', 'device': 'sda1'},
{'virtual': 'root', 'device': '/dev/sda1'},
- {'virtual': 'swap', 'device': 'sdb1'},
- {'virtual': 'swap', 'device': 'sdb2'},
- {'virtual': 'swap', 'device': 'sdb3'},
{'virtual': 'swap', 'device': 'sdb4'},
+ {'virtual': 'swap', 'device': 'sdb3'},
+ {'virtual': 'swap', 'device': 'sdb2'},
+ {'virtual': 'swap', 'device': 'sdb1'},
{'virtual': 'ephemeral0', 'device': 'sdc1'},
{'virtual': 'ephemeral1', 'device': 'sdc2'},
@@ -931,32 +1388,36 @@ class ComputeTestCase(test.TestCase):
'no_device': True}]
self.compute_api._update_image_block_device_mapping(
- self.context, instance_id, mappings)
+ self.context, instance_type, instance_id, mappings)
bdms = [self._parse_db_block_device_mapping(bdm_ref)
for bdm_ref in db.block_device_mapping_get_all_by_instance(
self.context, instance_id)]
expected_result = [
- {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb2'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb3'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb4'},
+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1',
+ 'volume_size': swap_size},
{'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'},
- {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'},
- {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}]
+
+ # NOTE(yamahata): ATM only ephemeral0 is supported.
+ # they're ignored for now
+ #{'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'},
+ #{'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}
+ ]
bdms.sort()
expected_result.sort()
self.assertDictListMatch(bdms, expected_result)
self.compute_api._update_block_device_mapping(
- self.context, instance_id, block_device_mapping)
+ self.context, instance_types.get_default_instance_type(),
+ instance_id, block_device_mapping)
bdms = [self._parse_db_block_device_mapping(bdm_ref)
for bdm_ref in db.block_device_mapping_get_all_by_instance(
self.context, instance_id)]
expected_result = [
{'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'},
- {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1',
+ 'volume_size': swap_size},
{'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'},
{'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'},
{'no_device': True, 'device_name': '/dev/sdb4'},
@@ -978,3 +1439,13 @@ class ComputeTestCase(test.TestCase):
self.context, instance_id):
db.block_device_mapping_destroy(self.context, bdm['id'])
self.compute.terminate_instance(self.context, instance_id)
+
+ def test_ephemeral_size(self):
+ local_size = 2
+ inst_type = {'local_gb': local_size}
+ self.assertEqual(self.compute_api._ephemeral_size(inst_type,
+ 'ephemeral0'),
+ local_size)
+ self.assertEqual(self.compute_api._ephemeral_size(inst_type,
+ 'ephemeral1'),
+ 0)
diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py
index 54448f9d6..0c07cbb7c 100644
--- a/nova/tests/test_db_api.py
+++ b/nova/tests/test_db_api.py
@@ -57,7 +57,7 @@ class DbApiTestCase(test.TestCase):
def test_instance_get_project_vpn(self):
values = {'instance_type_id': FLAGS.default_instance_type,
'image_ref': FLAGS.vpn_image_id,
- 'project_id': self.project_id
+ 'project_id': self.project_id,
}
instance = db.instance_create(self.context, values)
result = db.instance_get_project_vpn(self.context.elevated(),
@@ -67,7 +67,7 @@ class DbApiTestCase(test.TestCase):
def test_instance_get_project_vpn_joins(self):
values = {'instance_type_id': FLAGS.default_instance_type,
'image_ref': FLAGS.vpn_image_id,
- 'project_id': self.project_id
+ 'project_id': self.project_id,
}
instance = db.instance_create(self.context, values)
_setup_networking(instance['id'])
diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py
index 438f3e522..3a1389a49 100644
--- a/nova/tests/test_host_filter.py
+++ b/nova/tests/test_host_filter.py
@@ -19,12 +19,9 @@ Tests For Scheduler Host Filters.
import json
from nova import exception
-from nova import flags
from nova import test
from nova.scheduler import host_filter
-FLAGS = flags.FLAGS
-
class FakeZoneManager:
pass
@@ -57,9 +54,9 @@ class HostFilterTestCase(test.TestCase):
'host_name-label': 'xs-%s' % multiplier}
def setUp(self):
- self.old_flag = FLAGS.default_host_filter
- FLAGS.default_host_filter = \
- 'nova.scheduler.host_filter.AllHostsFilter'
+ super(HostFilterTestCase, self).setUp()
+ default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter'
+ self.flags(default_host_filter=default_host_filter)
self.instance_type = dict(name='tiny',
memory_mb=50,
vcpus=10,
@@ -76,9 +73,6 @@ class HostFilterTestCase(test.TestCase):
states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}
self.zone_manager.service_states = states
- def tearDown(self):
- FLAGS.default_host_filter = self.old_flag
-
def test_choose_filter(self):
# Test default filter ...
hf = host_filter.choose_host_filter()
diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py
index 548f81f8b..a724db9da 100644
--- a/nova/tests/test_hosts.py
+++ b/nova/tests/test_hosts.py
@@ -48,6 +48,10 @@ def stub_set_host_enabled(context, host, enabled):
return status
+def stub_host_power_action(context, host, action):
+ return action
+
+
class FakeRequest(object):
environ = {"nova.context": context.get_admin_context()}
@@ -62,6 +66,8 @@ class HostTestCase(test.TestCase):
self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list)
self.stubs.Set(self.controller.compute_api, 'set_host_enabled',
stub_set_host_enabled)
+ self.stubs.Set(self.controller.compute_api, 'host_power_action',
+ stub_host_power_action)
def test_list_hosts(self):
"""Verify that the compute hosts are returned."""
@@ -87,6 +93,18 @@ class HostTestCase(test.TestCase):
result_c2 = self.controller.update(self.req, "host_c2", body=en_body)
self.assertEqual(result_c2["status"], "disabled")
+ def test_host_startup(self):
+ result = self.controller.startup(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "startup")
+
+ def test_host_shutdown(self):
+ result = self.controller.shutdown(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "shutdown")
+
+ def test_host_reboot(self):
+ result = self.controller.reboot(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "reboot")
+
def test_bad_status_value(self):
bad_body = {"status": "bad"}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py
new file mode 100644
index 000000000..9680d6f2b
--- /dev/null
+++ b/nova/tests/test_image.py
@@ -0,0 +1,134 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+# Author: Soren Hansen
+#
+# 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.
+
+import datetime
+
+from nova import context
+from nova import exception
+from nova import test
+import nova.image
+
+
+class _ImageTestCase(test.TestCase):
+ def setUp(self):
+ super(_ImageTestCase, self).setUp()
+ self.context = context.get_admin_context()
+
+ def test_index(self):
+ res = self.image_service.index(self.context)
+ for image in res:
+ self.assertEquals(set(image.keys()), set(['id', 'name']))
+
+ def test_detail(self):
+ res = self.image_service.detail(self.context)
+ for image in res:
+ keys = set(image.keys())
+ self.assertEquals(keys, set(['id', 'name', 'created_at',
+ 'updated_at', 'deleted_at', 'deleted',
+ 'status', 'is_public', 'properties']))
+ self.assertTrue(isinstance(image['created_at'], datetime.datetime))
+ self.assertTrue(isinstance(image['updated_at'], datetime.datetime))
+
+ if not (isinstance(image['deleted_at'], datetime.datetime) or
+ image['deleted_at'] is None):
+ self.fail('image\'s "deleted_at" attribute was neither a '
+ 'datetime object nor None')
+
+ def check_is_bool(image, key):
+ val = image.get('deleted')
+ if not isinstance(val, bool):
+ self.fail('image\'s "%s" attribute wasn\'t '
+ 'a bool: %r' % (key, val))
+
+ check_is_bool(image, 'deleted')
+ check_is_bool(image, 'is_public')
+
+ def test_index_and_detail_have_same_results(self):
+ index = self.image_service.index(self.context)
+ detail = self.image_service.detail(self.context)
+ index_set = set([(i['id'], i['name']) for i in index])
+ detail_set = set([(i['id'], i['name']) for i in detail])
+ self.assertEqual(index_set, detail_set)
+
+ def test_show_raises_imagenotfound_for_invalid_id(self):
+ self.assertRaises(exception.ImageNotFound,
+ self.image_service.show,
+ self.context,
+ 'this image does not exist')
+
+ def test_show_by_name(self):
+ self.assertRaises(exception.ImageNotFound,
+ self.image_service.show_by_name,
+ self.context,
+ 'this image does not exist')
+
+ def test_create_adds_id(self):
+ index = self.image_service.index(self.context)
+ image_count = len(index)
+
+ self.image_service.create(self.context, {})
+
+ index = self.image_service.index(self.context)
+ self.assertEquals(len(index), image_count + 1)
+
+ self.assertTrue(index[0]['id'])
+
+ def test_create_keeps_id(self):
+ self.image_service.create(self.context, {'id': '34'})
+ self.image_service.show(self.context, '34')
+
+ def test_create_rejects_duplicate_ids(self):
+ self.image_service.create(self.context, {'id': '34'})
+ self.assertRaises(exception.Duplicate,
+ self.image_service.create,
+ self.context,
+ {'id': '34'})
+
+ # Make sure there's still one left
+ self.image_service.show(self.context, '34')
+
+ def test_update(self):
+ self.image_service.create(self.context,
+ {'id': '34', 'foo': 'bar'})
+
+ self.image_service.update(self.context, '34',
+ {'id': '34', 'foo': 'baz'})
+
+ img = self.image_service.show(self.context, '34')
+ self.assertEquals(img['foo'], 'baz')
+
+ def test_delete(self):
+ self.image_service.create(self.context, {'id': '34', 'foo': 'bar'})
+ self.image_service.delete(self.context, '34')
+ self.assertRaises(exception.NotFound,
+ self.image_service.show,
+ self.context,
+ '34')
+
+ def test_delete_all(self):
+ self.image_service.create(self.context, {'id': '32', 'foo': 'bar'})
+ self.image_service.create(self.context, {'id': '33', 'foo': 'bar'})
+ self.image_service.create(self.context, {'id': '34', 'foo': 'bar'})
+ self.image_service.delete_all()
+ index = self.image_service.index(self.context)
+ self.assertEquals(len(index), 0)
+
+
+class FakeImageTestCase(_ImageTestCase):
+ def setUp(self):
+ super(FakeImageTestCase, self).setUp()
+ self.image_service = nova.image.fake.FakeImageService()
diff --git a/nova/tests/test_ipv6.py b/nova/tests/test_ipv6.py
index 11dc2ec98..d123df6f1 100644
--- a/nova/tests/test_ipv6.py
+++ b/nova/tests/test_ipv6.py
@@ -16,15 +16,12 @@
"""Test suite for IPv6."""
-from nova import flags
from nova import ipv6
from nova import log as logging
from nova import test
LOG = logging.getLogger('nova.tests.test_ipv6')
-FLAGS = flags.FLAGS
-
import sys
diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py
index aa77258cb..17f1be967 100644
--- a/nova/tests/test_libvirt.py
+++ b/nova/tests/test_libvirt.py
@@ -38,7 +38,6 @@ from nova.virt.libvirt import firewall
libvirt = None
FLAGS = flags.FLAGS
-flags.DECLARE('instances_path', 'nova.compute.manager')
def _concurrency(wait, done, target):
@@ -93,6 +92,7 @@ def _setup_networking(instance_id, ip='1.2.3.4'):
class CacheConcurrencyTestCase(test.TestCase):
def setUp(self):
super(CacheConcurrencyTestCase, self).setUp()
+ self.flags(instances_path='nova.compute.manager')
def fake_exists(fname):
basedir = os.path.join(FLAGS.instances_path, '_base')
@@ -158,7 +158,7 @@ class LibvirtConnTestCase(test.TestCase):
self.context = context.RequestContext(self.user_id, self.project_id)
self.network = utils.import_object(FLAGS.network_manager)
self.context = context.get_admin_context()
- FLAGS.instances_path = ''
+ self.flags(instances_path='')
self.call_libvirt_dependant_setup = False
self.test_ip = '10.11.12.13'
@@ -169,6 +169,7 @@ class LibvirtConnTestCase(test.TestCase):
'project_id': 'fake',
'bridge': 'br101',
'image_ref': '123456',
+ 'local_gb': 20,
'instance_type_id': '5'} # m1.small
def lazy_load_library_exists(self):
@@ -322,7 +323,7 @@ class LibvirtConnTestCase(test.TestCase):
if not self.lazy_load_library_exists():
return
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
# Start test
image_service = utils.import_object(FLAGS.image_service)
@@ -346,7 +347,7 @@ class LibvirtConnTestCase(test.TestCase):
self.mox.ReplayAll()
conn = connection.LibvirtConnection(False)
- conn.snapshot(instance_ref, recv_meta['id'])
+ conn.snapshot(self.context, instance_ref, recv_meta['id'])
snapshot = image_service.show(context, recv_meta['id'])
self.assertEquals(snapshot['properties']['image_state'], 'available')
@@ -357,7 +358,7 @@ class LibvirtConnTestCase(test.TestCase):
if not self.lazy_load_library_exists():
return
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
# Start test
image_service = utils.import_object(FLAGS.image_service)
@@ -386,7 +387,7 @@ class LibvirtConnTestCase(test.TestCase):
self.mox.ReplayAll()
conn = connection.LibvirtConnection(False)
- conn.snapshot(instance_ref, recv_meta['id'])
+ conn.snapshot(self.context, instance_ref, recv_meta['id'])
snapshot = image_service.show(context, recv_meta['id'])
self.assertEquals(snapshot['properties']['image_state'], 'available')
@@ -521,7 +522,7 @@ class LibvirtConnTestCase(test.TestCase):
'disk.local')]
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
- FLAGS.libvirt_type = libvirt_type
+ self.flags(libvirt_type=libvirt_type)
conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
@@ -546,9 +547,9 @@ class LibvirtConnTestCase(test.TestCase):
# checking against that later on. This way we make sure the
# implementation doesn't fiddle around with the FLAGS.
testuri = 'something completely different'
- FLAGS.libvirt_uri = testuri
+ self.flags(libvirt_uri=testuri)
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
- FLAGS.libvirt_type = libvirt_type
+ self.flags(libvirt_type=libvirt_type)
conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
self.assertEquals(uri, testuri)
@@ -556,8 +557,7 @@ class LibvirtConnTestCase(test.TestCase):
def test_update_available_resource_works_correctly(self):
"""Confirm compute_node table is updated successfully."""
- org_path = FLAGS.instances_path = ''
- FLAGS.instances_path = '.'
+ self.flags(instances_path='.')
# Prepare mocks
def getVersion():
@@ -604,12 +604,10 @@ class LibvirtConnTestCase(test.TestCase):
self.assertTrue(compute_node['hypervisor_version'] > 0)
db.service_destroy(self.context, service_ref['id'])
- FLAGS.instances_path = org_path
def test_update_resource_info_no_compute_record_found(self):
"""Raise exception if no recorde found on services table."""
- org_path = FLAGS.instances_path = ''
- FLAGS.instances_path = '.'
+ self.flags(instances_path='.')
self.create_fake_libvirt_mock()
self.mox.ReplayAll()
@@ -618,8 +616,6 @@ class LibvirtConnTestCase(test.TestCase):
conn.update_available_resource,
self.context, 'dummy')
- FLAGS.instances_path = org_path
-
def test_ensure_filtering_rules_for_instance_timeout(self):
"""ensure_filtering_fules_for_instance() finishes with timeout."""
# Skip if non-libvirt environment
@@ -736,7 +732,7 @@ class LibvirtConnTestCase(test.TestCase):
network_info = _create_network_info()
try:
- conn.spawn(instance, network_info)
+ conn.spawn(self.context, instance, network_info)
except Exception, e:
count = (0 <= str(e.message).find('Unexpected method call'))
@@ -749,6 +745,42 @@ class LibvirtConnTestCase(test.TestCase):
ip = conn.get_host_ip_addr()
self.assertEquals(ip, FLAGS.my_ip)
+ def test_volume_in_mapping(self):
+ conn = connection.LibvirtConnection(False)
+ swap = {'device_name': '/dev/sdb',
+ 'swap_size': 1}
+ ephemerals = [{'num': 0,
+ 'virtual_name': 'ephemeral0',
+ 'device_name': '/dev/sdc1',
+ 'size': 1},
+ {'num': 2,
+ 'virtual_name': 'ephemeral2',
+ 'device_name': '/dev/sdd',
+ 'size': 1}]
+ block_device_mapping = [{'mount_device': '/dev/sde',
+ 'device_path': 'fake_device'},
+ {'mount_device': '/dev/sdf',
+ 'device_path': 'fake_device'}]
+ block_device_info = {
+ 'root_device_name': '/dev/sda',
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
+
+ def _assert_volume_in_mapping(device_name, true_or_false):
+ self.assertEquals(conn._volume_in_mapping(device_name,
+ block_device_info),
+ true_or_false)
+
+ _assert_volume_in_mapping('sda', False)
+ _assert_volume_in_mapping('sdb', True)
+ _assert_volume_in_mapping('sdc1', True)
+ _assert_volume_in_mapping('sdd', True)
+ _assert_volume_in_mapping('sde', True)
+ _assert_volume_in_mapping('sdf', True)
+ _assert_volume_in_mapping('sdg', False)
+ _assert_volume_in_mapping('sdh1', False)
+
class NWFilterFakes:
def __init__(self):
diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py
index c862726ab..ad678714e 100644
--- a/nova/tests/test_metadata.py
+++ b/nova/tests/test_metadata.py
@@ -43,16 +43,21 @@ class MetadataTestCase(test.TestCase):
'reservation_id': 'r-xxxxxxxx',
'user_data': '',
'image_ref': 7,
+ 'fixed_ips': [],
+ 'root_device_name': '/dev/sda1',
'hostname': 'test'})
def instance_get(*args, **kwargs):
return self.instance
+ def instance_get_list(*args, **kwargs):
+ return [self.instance]
+
def floating_get(*args, **kwargs):
return '99.99.99.99'
self.stubs.Set(api, 'instance_get', instance_get)
- self.stubs.Set(api, 'fixed_ip_get_instance', instance_get)
+ self.stubs.Set(api, 'instance_get_all_by_filters', instance_get_list)
self.stubs.Set(api, 'instance_get_floating_address', floating_get)
self.app = metadatarequesthandler.MetadataRequestHandler()
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 28f50d328..2ca8b64f4 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -17,7 +17,6 @@
from nova import db
from nova import exception
-from nova import flags
from nova import log as logging
from nova import test
from nova.network import manager as network_manager
@@ -26,7 +25,6 @@ from nova.network import manager as network_manager
import mox
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.network')
diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py
new file mode 100644
index 000000000..9c6563f14
--- /dev/null
+++ b/nova/tests/test_nova_manage.py
@@ -0,0 +1,82 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC
+# Copyright 2011 Ilya Alekseyev
+#
+# 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.
+
+import os
+import sys
+
+TOPDIR = os.path.normpath(os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ os.pardir,
+ os.pardir))
+NOVA_MANAGE_PATH = os.path.join(TOPDIR, 'bin', 'nova-manage')
+
+sys.dont_write_bytecode = True
+import imp
+nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH)
+sys.dont_write_bytecode = False
+
+import netaddr
+from nova import context
+from nova import db
+from nova import flags
+from nova import test
+
+FLAGS = flags.FLAGS
+
+
+class FixedIpCommandsTestCase(test.TestCase):
+ def setUp(self):
+ super(FixedIpCommandsTestCase, self).setUp()
+ cidr = '10.0.0.0/24'
+ net = netaddr.IPNetwork(cidr)
+ net_info = {'bridge': 'fakebr',
+ 'bridge_interface': 'fakeeth',
+ 'dns': FLAGS.flat_network_dns,
+ 'cidr': cidr,
+ 'netmask': str(net.netmask),
+ 'gateway': str(net[1]),
+ 'broadcast': str(net.broadcast),
+ 'dhcp_start': str(net[2])}
+ self.network = db.network_create_safe(context.get_admin_context(),
+ net_info)
+ num_ips = len(net)
+ for index in range(num_ips):
+ address = str(net[index])
+ reserved = (index == 1 or index == 2)
+ db.fixed_ip_create(context.get_admin_context(),
+ {'network_id': self.network['id'],
+ 'address': address,
+ 'reserved': reserved})
+ self.commands = nova_manage.FixedIpCommands()
+
+ def tearDown(self):
+ db.network_delete_safe(context.get_admin_context(), self.network['id'])
+ super(FixedIpCommandsTestCase, self).tearDown()
+
+ def test_reserve(self):
+ self.commands.reserve('10.0.0.100')
+ address = db.fixed_ip_get_by_address(context.get_admin_context(),
+ '10.0.0.100')
+ self.assertEqual(address['reserved'], True)
+
+ def test_unreserve(self):
+ db.fixed_ip_update(context.get_admin_context(), '10.0.0.100',
+ {'reserved': True})
+ self.commands.unreserve('10.0.0.100')
+ address = db.fixed_ip_get_by_address(context.get_admin_context(),
+ '10.0.0.100')
+ self.assertEqual(address['reserved'], False)
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 92393b536..f4b481ebe 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -114,9 +114,7 @@ class QuotaTestCase(test.TestCase):
db.quota_destroy_all_by_project(self.context, self.project_id)
def test_unlimited_instances(self):
- FLAGS.quota_instances = 2
- FLAGS.quota_ram = -1
- FLAGS.quota_cores = -1
+ self.flags(quota_instances=2, quota_ram=-1, quota_cores=-1)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -130,9 +128,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_ram(self):
- FLAGS.quota_instances = -1
- FLAGS.quota_ram = 2 * 2048
- FLAGS.quota_cores = -1
+ self.flags(quota_instances=-1, quota_ram=2 * 2048, quota_cores=-1)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -146,9 +142,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_cores(self):
- FLAGS.quota_instances = -1
- FLAGS.quota_ram = -1
- FLAGS.quota_cores = 2
+ self.flags(quota_instances=-1, quota_ram=-1, quota_cores=2)
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -162,8 +156,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(num_instances, 101)
def test_unlimited_volumes(self):
- FLAGS.quota_volumes = 10
- FLAGS.quota_gigabytes = -1
+ self.flags(quota_volumes=10, quota_gigabytes=-1)
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
db.quota_create(self.context, self.project_id, 'volumes', None)
@@ -173,8 +166,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(volumes, 101)
def test_unlimited_gigabytes(self):
- FLAGS.quota_volumes = -1
- FLAGS.quota_gigabytes = 10
+ self.flags(quota_volumes=-1, quota_gigabytes=10)
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
db.quota_create(self.context, self.project_id, 'gigabytes', None)
@@ -184,7 +176,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(volumes, 101)
def test_unlimited_floating_ips(self):
- FLAGS.quota_floating_ips = 10
+ self.flags(quota_floating_ips=10)
floating_ips = quota.allowed_floating_ips(self.context, 100)
self.assertEqual(floating_ips, 10)
db.quota_create(self.context, self.project_id, 'floating_ips', None)
@@ -194,7 +186,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(floating_ips, 101)
def test_unlimited_metadata_items(self):
- FLAGS.quota_metadata_items = 10
+ self.flags(quota_metadata_items=10)
items = quota.allowed_metadata_items(self.context, 100)
self.assertEqual(items, 10)
db.quota_create(self.context, self.project_id, 'metadata_items', None)
@@ -286,49 +278,49 @@ class QuotaTestCase(test.TestCase):
metadata=metadata)
def test_default_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 55
+ self.flags(quota_max_injected_files=55)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 55)
def test_overridden_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 5
+ self.flags(quota_max_injected_files=5)
db.quota_create(self.context, self.project_id, 'injected_files', 77)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 77)
def test_unlimited_default_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = -1
+ self.flags(quota_max_injected_files=-1)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
def test_unlimited_db_allowed_injected_files(self):
- FLAGS.quota_max_injected_files = 5
+ self.flags(quota_max_injected_files=5)
db.quota_create(self.context, self.project_id, 'injected_files', None)
self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
def test_default_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 12345)
def test_overridden_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
db.quota_create(self.context, self.project_id,
'injected_file_content_bytes', 5678)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 5678)
def test_unlimited_default_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = -1
+ self.flags(quota_max_injected_file_content_bytes=-1)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 23456)
def test_unlimited_db_allowed_injected_file_content_bytes(self):
- FLAGS.quota_max_injected_file_content_bytes = 12345
+ self.flags(quota_max_injected_file_content_bytes=12345)
db.quota_create(self.context, self.project_id,
'injected_file_content_bytes', None)
limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
self.assertEqual(limit, 23456)
def _create_with_injected_files(self, files):
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
api = compute.API(image_service=self.StubImageService())
inst_type = instance_types.get_instance_type_by_name('m1.small')
api.create(self.context, min_count=1, max_count=1,
@@ -336,7 +328,7 @@ class QuotaTestCase(test.TestCase):
injected_files=files)
def test_no_injected_files(self):
- FLAGS.image_service = 'nova.image.fake.FakeImageService'
+ self.flags(image_service='nova.image.fake.FakeImageService')
api = compute.API(image_service=self.StubImageService())
inst_type = instance_types.get_instance_type_by_name('m1.small')
api.create(self.context, instance_type=inst_type, image_href='3')
diff --git a/nova/tests/test_rpc.py b/nova/tests/test_rpc.py
index 2d2436175..ba9c0a859 100644
--- a/nova/tests/test_rpc.py
+++ b/nova/tests/test_rpc.py
@@ -20,13 +20,11 @@ Unit Tests for remote procedure calls using queue
"""
from nova import context
-from nova import flags
from nova import log as logging
from nova import rpc
from nova import test
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.rpc')
diff --git a/nova/tests/test_rpc_amqp.py b/nova/tests/test_rpc_amqp.py
index d29f7ae32..2215a908b 100644
--- a/nova/tests/test_rpc_amqp.py
+++ b/nova/tests/test_rpc_amqp.py
@@ -1,12 +1,32 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Openstack, LLC.
+# 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.
+
+"""
+Tests For RPC AMQP.
+"""
+
from nova import context
-from nova import flags
from nova import log as logging
from nova import rpc
from nova.rpc import amqp
from nova import test
-FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.rpc')
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index bbf47b50f..8f92406ff 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -33,7 +33,6 @@ from nova import manager
from nova import wsgi
from nova.compute import manager as compute_manager
-FLAGS = flags.FLAGS
flags.DEFINE_string("fake_manager", "nova.tests.test_service.FakeManager",
"Manager for testing")
diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py
new file mode 100644
index 000000000..8ca203442
--- /dev/null
+++ b/nova/tests/test_skip_examples.py
@@ -0,0 +1,47 @@
+# 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.
+
+from nova import test
+
+
+class ExampleSkipTestCase(test.TestCase):
+ test_counter = 0
+
+ @test.skip_test("Example usage of @test.skip_test()")
+ def test_skip_test_example(self):
+ self.fail("skip_test failed to work properly.")
+
+ @test.skip_if(True, "Example usage of @test.skip_if()")
+ def test_skip_if_example(self):
+ self.fail("skip_if failed to work properly.")
+
+ @test.skip_unless(False, "Example usage of @test.skip_unless()")
+ def test_skip_unless_example(self):
+ self.fail("skip_unless failed to work properly.")
+
+ @test.skip_if(False, "This test case should never be skipped.")
+ def test_001_increase_test_counter(self):
+ ExampleSkipTestCase.test_counter += 1
+
+ @test.skip_unless(True, "This test case should never be skipped.")
+ def test_002_increase_test_counter(self):
+ ExampleSkipTestCase.test_counter += 1
+
+ def test_003_verify_test_counter(self):
+ self.assertEquals(ExampleSkipTestCase.test_counter, 2,
+ "Tests were not skipped appropriately")
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
new file mode 100644
index 000000000..388f075af
--- /dev/null
+++ b/nova/tests/test_virt.py
@@ -0,0 +1,83 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Isaku Yamahata
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import flags
+from nova import test
+from nova.virt import driver
+
+FLAGS = flags.FLAGS
+
+
+class TestVirtDriver(test.TestCase):
+ def test_block_device(self):
+ swap = {'device_name': '/dev/sdb',
+ 'swap_size': 1}
+ ephemerals = [{'num': 0,
+ 'virtual_name': 'ephemeral0',
+ 'device_name': '/dev/sdc1',
+ 'size': 1}]
+ block_device_mapping = [{'mount_device': '/dev/sde',
+ 'device_path': 'fake_device'}]
+ block_device_info = {
+ 'root_device_name': '/dev/sda',
+ 'swap': swap,
+ 'ephemerals': ephemerals,
+ 'block_device_mapping': block_device_mapping}
+
+ empty_block_device_info = {}
+
+ self.assertEqual(
+ driver.block_device_info_get_root(block_device_info), '/dev/sda')
+ self.assertEqual(
+ driver.block_device_info_get_root(empty_block_device_info), None)
+ self.assertEqual(
+ driver.block_device_info_get_root(None), None)
+
+ self.assertEqual(
+ driver.block_device_info_get_swap(block_device_info), swap)
+ self.assertEqual(driver.block_device_info_get_swap(
+ empty_block_device_info)['device_name'], None)
+ self.assertEqual(driver.block_device_info_get_swap(
+ empty_block_device_info)['swap_size'], 0)
+ self.assertEqual(
+ driver.block_device_info_get_swap({'swap': None})['device_name'],
+ None)
+ self.assertEqual(
+ driver.block_device_info_get_swap({'swap': None})['swap_size'],
+ 0)
+ self.assertEqual(
+ driver.block_device_info_get_swap(None)['device_name'], None)
+ self.assertEqual(
+ driver.block_device_info_get_swap(None)['swap_size'], 0)
+
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(block_device_info),
+ ephemerals)
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(empty_block_device_info),
+ [])
+ self.assertEqual(
+ driver.block_device_info_get_ephemerals(None),
+ [])
+
+ def test_swap_is_usable(self):
+ self.assertFalse(driver.swap_is_usable(None))
+ self.assertFalse(driver.swap_is_usable({'device_name': None}))
+ self.assertFalse(driver.swap_is_usable({'device_name': '/dev/sdb',
+ 'swap_size': 0}))
+ self.assertTrue(driver.swap_is_usable({'device_name': '/dev/sdb',
+ 'swap_size': 1}))
diff --git a/nova/tests/test_vmwareapi.py b/nova/tests/test_vmwareapi.py
index 3d87d67ad..06daf46e8 100644
--- a/nova/tests/test_vmwareapi.py
+++ b/nova/tests/test_vmwareapi.py
@@ -40,6 +40,7 @@ class VMWareAPIVMTestCase(test.TestCase):
def setUp(self):
super(VMWareAPIVMTestCase, self).setUp()
+ self.context = context.RequestContext('fake', 'fake', False)
self.flags(vmwareapi_host_ip='test_url',
vmwareapi_host_username='test_username',
vmwareapi_host_password='test_pass')
@@ -94,7 +95,7 @@ class VMWareAPIVMTestCase(test.TestCase):
"""Create and spawn the VM."""
self._create_instance_in_the_db()
self.type_data = db.instance_type_get_by_name(None, 'm1.large')
- self.conn.spawn(self.instance, self.network_info)
+ self.conn.spawn(self.context, self.instance, self.network_info)
self._check_vm_record()
def _check_vm_record(self):
@@ -156,14 +157,14 @@ class VMWareAPIVMTestCase(test.TestCase):
self._create_vm()
info = self.conn.get_info(1)
self._check_vm_info(info, power_state.RUNNING)
- self.conn.snapshot(self.instance, "Test-Snapshot")
+ self.conn.snapshot(self.context, self.instance, "Test-Snapshot")
info = self.conn.get_info(1)
self._check_vm_info(info, power_state.RUNNING)
def test_snapshot_non_existent(self):
self._create_instance_in_the_db()
- self.assertRaises(Exception, self.conn.snapshot, self.instance,
- "Test-Snapshot")
+ self.assertRaises(Exception, self.conn.snapshot, self.context,
+ self.instance, "Test-Snapshot")
def test_reboot(self):
self._create_vm()
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index 34aba140b..952277e3f 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -71,9 +71,9 @@ class XenAPIVolumeTestCase(test.TestCase):
self.user_id = 'fake'
self.project_id = 'fake'
self.context = context.RequestContext(self.user_id, self.project_id)
- FLAGS.target_host = '127.0.0.1'
- FLAGS.xenapi_connection_url = 'test_url'
- FLAGS.xenapi_connection_password = 'test_pass'
+ self.flags(target_host='127.0.0.1',
+ xenapi_connection_url='test_url',
+ xenapi_connection_password='test_pass')
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
@@ -170,6 +170,10 @@ def reset_network(*args):
pass
+def _find_rescue_vbd_ref(*args):
+ pass
+
+
class XenAPIVMTestCase(test.TestCase):
"""Unit tests for VM operations."""
def setUp(self):
@@ -189,6 +193,8 @@ class XenAPIVMTestCase(test.TestCase):
stubs.stubout_stream_disk(self.stubs)
stubs.stubout_is_vdi_pv(self.stubs)
self.stubs.Set(vmops.VMOps, 'reset_network', reset_network)
+ self.stubs.Set(vmops.VMOps, '_find_rescue_vbd_ref',
+ _find_rescue_vbd_ref)
stubs.stub_out_vm_methods(self.stubs)
glance_stubs.stubout_glance_client(self.stubs)
fake_utils.stub_out_utils_execute(self.stubs)
@@ -226,7 +232,7 @@ class XenAPIVMTestCase(test.TestCase):
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
instance = db.instance_create(self.context, values)
- self.conn.spawn(instance, network_info)
+ self.conn.spawn(self.context, instance, network_info)
gt1 = eventlet.spawn(_do_build, 1, self.project_id, self.user_id)
gt2 = eventlet.spawn(_do_build, 2, self.project_id, self.user_id)
@@ -256,14 +262,15 @@ class XenAPIVMTestCase(test.TestCase):
instance = self._create_instance()
name = "MySnapshot"
- self.assertRaises(exception.Error, self.conn.snapshot, instance, name)
+ self.assertRaises(exception.Error, self.conn.snapshot,
+ self.context, instance, name)
def test_instance_snapshot(self):
stubs.stubout_instance_snapshot(self.stubs)
instance = self._create_instance()
name = "MySnapshot"
- template_vm_ref = self.conn.snapshot(instance, name)
+ template_vm_ref = self.conn.snapshot(self.context, instance, name)
def ensure_vm_was_torn_down():
vm_labels = []
@@ -396,7 +403,7 @@ class XenAPIVMTestCase(test.TestCase):
instance_type_id="3", os_type="linux",
architecture="x86-64", instance_id=1,
check_injection=False,
- create_record=True):
+ create_record=True, empty_dns=False):
stubs.stubout_loopingcall_start(self.stubs)
if create_record:
values = {'id': instance_id,
@@ -425,12 +432,22 @@ class XenAPIVMTestCase(test.TestCase):
'label': 'fake',
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
- self.conn.spawn(instance, network_info)
+ if empty_dns:
+ network_info[0][1]['dns'] = []
+
+ self.conn.spawn(self.context, instance, network_info)
self.create_vm_record(self.conn, os_type, instance_id)
self.check_vm_record(self.conn, check_injection)
self.assertTrue(instance.os_type)
self.assertTrue(instance.architecture)
+ def test_spawn_empty_dns(self):
+ """"Test spawning with an empty dns list"""
+ self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
+ os_type="linux", architecture="x86-64",
+ empty_dns=True)
+ self.check_vm_params_for_linux()
+
def test_spawn_not_enough_memory(self):
self.assertRaises(Exception,
self._test_spawn,
@@ -629,7 +646,7 @@ class XenAPIVMTestCase(test.TestCase):
self.flags(flat_injected=False)
instance = self._create_instance()
conn = xenapi_conn.get_connection(False)
- conn.rescue(instance, None, [])
+ conn.rescue(self.context, instance, None, [])
def test_unrescue(self):
instance = self._create_instance()
@@ -684,7 +701,7 @@ class XenAPIVMTestCase(test.TestCase):
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
if spawn:
- self.conn.spawn(instance, network_info)
+ self.conn.spawn(self.context, instance, network_info)
return instance
@@ -736,9 +753,9 @@ class XenAPIMigrateInstance(test.TestCase):
def setUp(self):
super(XenAPIMigrateInstance, self).setUp()
self.stubs = stubout.StubOutForTesting()
- FLAGS.target_host = '127.0.0.1'
- FLAGS.xenapi_connection_url = 'test_url'
- FLAGS.xenapi_connection_password = 'test_pass'
+ self.flags(target_host='127.0.0.1',
+ xenapi_connection_url='test_url',
+ xenapi_connection_password='test_pass')
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
@@ -768,15 +785,67 @@ class XenAPIMigrateInstance(test.TestCase):
conn = xenapi_conn.get_connection(False)
conn.migrate_disk_and_power_off(instance, '127.0.0.1')
+ def test_revert_migrate(self):
+ instance = db.instance_create(self.context, self.values)
+ self.called = False
+ self.fake_vm_start_called = False
+ self.fake_revert_migration_called = False
+
+ def fake_vm_start(*args, **kwargs):
+ self.fake_vm_start_called = True
+
+ def fake_vdi_resize(*args, **kwargs):
+ self.called = True
+
+ def fake_revert_migration(*args, **kwargs):
+ self.fake_revert_migration_called = True
+
+ self.stubs.Set(stubs.FakeSessionForMigrationTests,
+ "VDI_resize_online", fake_vdi_resize)
+ self.stubs.Set(vmops.VMOps, '_start', fake_vm_start)
+ self.stubs.Set(vmops.VMOps, 'revert_migration', fake_revert_migration)
+
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
+ stubs.stubout_loopingcall_start(self.stubs)
+ conn = xenapi_conn.get_connection(False)
+ network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False},
+ {'broadcast': '192.168.0.255',
+ 'dns': ['192.168.0.1'],
+ 'gateway': '192.168.0.1',
+ 'gateway6': 'dead:beef::1',
+ 'ip6s': [{'enabled': '1',
+ 'ip': 'dead:beef::dcad:beff:feef:0',
+ 'netmask': '64'}],
+ 'ips': [{'enabled': '1',
+ 'ip': '192.168.0.100',
+ 'netmask': '255.255.255.0'}],
+ 'label': 'fake',
+ 'mac': 'DE:AD:BE:EF:00:00',
+ 'rxtx_cap': 3})]
+ conn.finish_migration(self.context, instance,
+ dict(base_copy='hurr', cow='durr'),
+ network_info, resize_instance=True)
+ self.assertEqual(self.called, True)
+ self.assertEqual(self.fake_vm_start_called, True)
+
+ conn.revert_migration(instance)
+ self.assertEqual(self.fake_revert_migration_called, True)
+
def test_finish_migrate(self):
instance = db.instance_create(self.context, self.values)
self.called = False
+ self.fake_vm_start_called = False
+
+ def fake_vm_start(*args, **kwargs):
+ self.fake_vm_start_called = True
def fake_vdi_resize(*args, **kwargs):
self.called = True
self.stubs.Set(stubs.FakeSessionForMigrationTests,
"VDI_resize_online", fake_vdi_resize)
+ self.stubs.Set(vmops.VMOps, '_start', fake_vm_start)
+
stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
stubs.stubout_loopingcall_start(self.stubs)
conn = xenapi_conn.get_connection(False)
@@ -794,9 +863,11 @@ class XenAPIMigrateInstance(test.TestCase):
'label': 'fake',
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
- conn.finish_migration(instance, dict(base_copy='hurr', cow='durr'),
- network_info, resize_instance=True)
+ conn.finish_migration(self.context, instance,
+ dict(base_copy='hurr', cow='durr'),
+ network_info, resize_instance=True)
self.assertEqual(self.called, True)
+ self.assertEqual(self.fake_vm_start_called, True)
def test_finish_migrate_no_local_storage(self):
tiny_type_id = \
@@ -826,8 +897,9 @@ class XenAPIMigrateInstance(test.TestCase):
'label': 'fake',
'mac': 'DE:AD:BE:EF:00:00',
'rxtx_cap': 3})]
- conn.finish_migration(instance, dict(base_copy='hurr', cow='durr'),
- network_info, resize_instance=True)
+ conn.finish_migration(self.context, instance,
+ dict(base_copy='hurr', cow='durr'),
+ network_info, resize_instance=True)
def test_finish_migrate_no_resize_vdi(self):
instance = db.instance_create(self.context, self.values)
@@ -856,8 +928,9 @@ class XenAPIMigrateInstance(test.TestCase):
'rxtx_cap': 3})]
# Resize instance would be determined by the compute call
- conn.finish_migration(instance, dict(base_copy='hurr', cow='durr'),
- network_info, resize_instance=False)
+ conn.finish_migration(self.context, instance,
+ dict(base_copy='hurr', cow='durr'),
+ network_info, resize_instance=False)
class XenAPIImageTypeTestCase(test.TestCase):
diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py
index a943fee27..9efa23015 100644
--- a/nova/tests/test_zones.py
+++ b/nova/tests/test_zones.py
@@ -18,7 +18,6 @@ Tests For ZoneManager
import datetime
import mox
-import novaclient
from nova import context
from nova import db
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index 66c79d465..0d0f84e32 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -28,8 +28,8 @@ from nova import utils
def stubout_instance_snapshot(stubs):
@classmethod
- def fake_fetch_image(cls, session, instance_id, image, user, project,
- type):
+ def fake_fetch_image(cls, context, session, instance_id, image, user,
+ project, type):
from nova.virt.xenapi.fake import create_vdi
name_label = "instance-%s" % instance_id
#TODO: create fake SR record
@@ -227,7 +227,7 @@ def stub_out_vm_methods(stubs):
def fake_release_bootlock(self, vm):
pass
- def fake_spawn_rescue(self, inst):
+ def fake_spawn_rescue(self, context, inst, network_info):
inst._rescue = False
stubs.Set(vmops.VMOps, "_shutdown", fake_shutdown)
diff --git a/nova/utils.py b/nova/utils.py
index 4ea623cc1..372358b42 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -126,6 +126,22 @@ def fetchfile(url, target):
def execute(*cmd, **kwargs):
+ """
+ Helper method to execute command with optional retry.
+
+ :cmd Passed to subprocess.Popen.
+ :process_input Send to opened process.
+ :addl_env Added to the processes env.
+ :check_exit_code Defaults to 0. Raise exception.ProcessExecutionError
+ unless program exits with this code.
+ :delay_on_retry True | False. Defaults to True. If set to True, wait a
+ short amount of time before retrying.
+ :attempts How many times to retry cmd.
+
+ :raises exception.Error on receiving unknown arguments
+ :raises exception.ProcessExecutionError
+ """
+
process_input = kwargs.pop('process_input', None)
addl_env = kwargs.pop('addl_env', None)
check_exit_code = kwargs.pop('check_exit_code', 0)
@@ -223,7 +239,7 @@ def abspath(s):
def novadir():
import nova
- return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
+ return os.path.abspath(nova.__file__).split('nova/__init__.py')[0]
def default_flagfile(filename='nova.conf', args=None):
@@ -790,7 +806,7 @@ def parse_server_string(server_str):
(address, port) = server_str.split(':')
return (address, port)
- except:
+ except Exception:
LOG.debug(_('Invalid server_string: %s' % server_str))
return ('', '')
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index b219fb2cb..df4a66ac2 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -32,6 +32,33 @@ class InstanceInfo(object):
self.state = state
+def block_device_info_get_root(block_device_info):
+ block_device_info = block_device_info or {}
+ return block_device_info.get('root_device_name')
+
+
+def block_device_info_get_swap(block_device_info):
+ block_device_info = block_device_info or {}
+ return block_device_info.get('swap') or {'device_name': None,
+ 'swap_size': 0}
+
+
+def swap_is_usable(swap):
+ return swap and swap['device_name'] and swap['swap_size'] > 0
+
+
+def block_device_info_get_ephemerals(block_device_info):
+ block_device_info = block_device_info or {}
+ ephemerals = block_device_info.get('ephemerals') or []
+ return ephemerals
+
+
+def block_device_info_get_mapping(block_device_info):
+ block_device_info = block_device_info or {}
+ block_device_mapping = block_device_info.get('block_device_mapping') or []
+ return block_device_mapping
+
+
class ComputeDriver(object):
"""Base class for compute drivers.
@@ -40,6 +67,7 @@ class ComputeDriver(object):
def init_host(self, host):
"""Adopt existing VM's running here"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def get_info(self, instance_name):
@@ -52,16 +80,20 @@ class ComputeDriver(object):
:num_cpu: (int) the number of virtual CPUs for the domain
:cpu_time: (int) the CPU time used in nanoseconds
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def list_instances(self):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def list_instances_detail(self):
"""Return a list of InstanceInfo for all registered VMs"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""Launch a VM for the specified instance"""
raise NotImplementedError()
@@ -79,29 +111,36 @@ class ComputeDriver(object):
warning in that case.
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def reboot(self, instance, network_info):
"""Reboot specified VM"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def snapshot_instance(self, context, instance_id, image_id):
raise NotImplementedError()
def get_console_pool_info(self, console_type):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def get_console_output(self, instance):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def get_ajax_console(self, instance):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def get_diagnostics(self, instance):
"""Return data about VM diagnostics"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def get_host_ip_addr(self):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def attach_volume(self, context, instance_id, volume_id, mountpoint):
@@ -116,43 +155,50 @@ class ComputeDriver(object):
def migrate_disk_and_power_off(self, instance, dest):
"""Transfers the VHD of a running instance to another host, then shuts
off the instance copies over the COW disk"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
- def snapshot(self, instance, image_id):
+ def snapshot(self, context, instance, image_id):
"""Create snapshot from a running VM instance."""
raise NotImplementedError()
- def finish_migration(self, instance, disk_info, network_info,
+ def finish_migration(self, context, instance, disk_info, network_info,
resize_instance):
"""Completes a resize, turning on the migrated instance"""
raise NotImplementedError()
def revert_migration(self, instance):
"""Reverts a resize, powering back on the instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def pause(self, instance, callback):
"""Pause VM instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def unpause(self, instance, callback):
"""Unpause paused VM instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def suspend(self, instance, callback):
"""suspend the specified instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def resume(self, instance, callback):
"""resume the specified instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
- def rescue(self, instance, callback, network_info):
+ def rescue(self, context, instance, callback, network_info):
"""Rescue the specified instance"""
raise NotImplementedError()
def unrescue(self, instance, callback, network_info):
"""Unrescue the specified instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def update_available_resource(self, ctxt, host):
@@ -165,6 +211,7 @@ class ComputeDriver(object):
:param host: hostname that compute manager is currently running
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def live_migration(self, ctxt, instance_ref, dest,
@@ -184,20 +231,25 @@ class ComputeDriver(object):
expected nova.compute.manager.recover_live_migration.
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def refresh_security_group_rules(self, security_group_id):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def refresh_security_group_members(self, security_group_id):
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def refresh_provider_fw_rules(self, security_group_id):
"""See: nova/virt/fake.py for docs."""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def reset_network(self, instance):
"""reset networking for specified instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
pass
def ensure_filtering_rules_for_instance(self, instance_ref):
@@ -223,10 +275,12 @@ class ComputeDriver(object):
:params instance_ref: nova.db.sqlalchemy.models.Instance object
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def unfilter_instance(self, instance, network_info):
"""Stop filtering instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def set_admin_password(self, context, instance_id, new_pass=None):
@@ -237,24 +291,34 @@ class ComputeDriver(object):
"""Create a file on the VM instance. The file path and contents
should be base64-encoded.
"""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def agent_update(self, instance, url, md5hash):
"""Update agent on the VM instance."""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def inject_network_info(self, instance, nw_info):
"""inject network info for specified instance"""
+ # TODO(Vek): Need to pass context in for access to auth_token
pass
def poll_rescued_instances(self, timeout):
"""Poll for rescued instances"""
+ # TODO(Vek): Need to pass context in for access to auth_token
+ raise NotImplementedError()
+
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
raise NotImplementedError()
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
def plug_vifs(self, instance, network_info):
"""Plugs in VIFs to networks."""
+ # TODO(Vek): Need to pass context in for access to auth_token
raise NotImplementedError()
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index b7396d61c..880702af1 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -129,7 +129,8 @@ class FakeConnection(driver.ComputeDriver):
info_list.append(self._map_to_instance_info(instance))
return info_list
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""
Create a new instance/VM/domain on the virtualization platform.
@@ -153,7 +154,7 @@ class FakeConnection(driver.ComputeDriver):
fake_instance = FakeInstance(name, state)
self.instances[name] = fake_instance
- def snapshot(self, instance, name):
+ def snapshot(self, context, instance, name):
"""
Snapshots the specified instance.
@@ -240,7 +241,7 @@ class FakeConnection(driver.ComputeDriver):
"""
pass
- def rescue(self, instance, callback, network_info):
+ def rescue(self, context, instance, callback, network_info):
"""
Rescue the specified instance.
"""
@@ -511,6 +512,10 @@ class FakeConnection(driver.ComputeDriver):
"""Return fake Host Status of ram, disk, network."""
return self.host_status
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py
index 1e4cce10c..03a78db1f 100644
--- a/nova/virt/hyperv.py
+++ b/nova/virt/hyperv.py
@@ -138,7 +138,8 @@ class HyperVConnection(driver.ComputeDriver):
return instance_infos
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
""" Create a new VM and start it."""
vm = self._lookup(instance.name)
if vm is not None:
@@ -498,6 +499,10 @@ class HyperVConnection(driver.ComputeDriver):
"""See xenapi_conn.py implementation."""
pass
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/images.py b/nova/virt/images.py
index 2e9fca3d6..54c691a40 100644
--- a/nova/virt/images.py
+++ b/nova/virt/images.py
@@ -21,7 +21,6 @@
Handling of VM disk images.
"""
-from nova import context
from nova import flags
from nova.image import glance as glance_image_service
import nova.image
@@ -33,13 +32,12 @@ FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.virt.images')
-def fetch(image_href, path, _user_id, _project_id):
+def fetch(context, image_href, path, _user_id, _project_id):
# TODO(vish): Improve context handling and add owner and auth data
# when it is added to glance. Right now there is no
# auth checking in glance, so we assume that access was
# checked before we got here.
(image_service, image_id) = nova.image.get_image_service(image_href)
with open(path, "wb") as image_file:
- elevated = context.get_admin_context()
- metadata = image_service.get(elevated, image_id, image_file)
+ metadata = image_service.get(context, image_id, image_file)
return metadata
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index a75636390..210e2b0fb 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -3,24 +3,22 @@
<memory>${memory_kb}</memory>
<os>
#if $type == 'lxc'
- #set $disk_prefix = ''
#set $disk_bus = ''
<type>exe</type>
<init>/sbin/init</init>
#else if $type == 'uml'
- #set $disk_prefix = 'ubd'
#set $disk_bus = 'uml'
<type>uml</type>
<kernel>/usr/bin/linux</kernel>
- <root>/dev/ubda</root>
+ #set $root_device_name = $getVar('root_device_name', '/dev/ubda')
+ <root>${root_device_name}</root>
#else
#if $type == 'xen'
- #set $disk_prefix = 'sd'
#set $disk_bus = 'scsi'
<type>linux</type>
- <root>/dev/xvda</root>
+ #set $root_device_name = $getVar('root_device_name', '/dev/xvda')
+ <root>${root_device_name}</root>
#else
- #set $disk_prefix = 'vd'
#set $disk_bus = 'virtio'
<type>hvm</type>
#end if
@@ -33,7 +31,8 @@
#if $type == 'xen'
<cmdline>ro</cmdline>
#else
- <cmdline>root=/dev/vda console=ttyS0</cmdline>
+ #set $root_device_name = $getVar('root_device_name', '/dev/vda')
+ <cmdline>root=${root_device_name} console=ttyS0</cmdline>
#end if
#if $getVar('ramdisk', None)
<initrd>${ramdisk}</initrd>
@@ -71,16 +70,30 @@
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk'/>
- <target dev='${disk_prefix}a' bus='${disk_bus}'/>
+ <target dev='${root_device}' bus='${disk_bus}'/>
</disk>
#end if
- #if $getVar('local', False)
+ #if $getVar('local_device', False)
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk.local'/>
- <target dev='${disk_prefix}b' bus='${disk_bus}'/>
+ <target dev='${local_device}' bus='${disk_bus}'/>
</disk>
#end if
+ #for $eph in $ephemerals
+ <disk type='block'>
+ <driver type='${driver_type}'/>
+ <source dev='${basepath}/${eph.device_path}'/>
+ <target dev='${eph.device}' bus='${disk_bus}'/>
+ </disk>
+ #end for
+ #if $getVar('swap_device', False)
+ <disk type='file'>
+ <driver type='${driver_type}'/>
+ <source file='${basepath}/disk.swap'/>
+ <target dev='${swap_device}' bus='${disk_bus}'/>
+ </disk>
+ #end if
#for $vol in $volumes
<disk type='${vol.type}'>
<driver type='raw'/>
diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py
index b9b247aab..16efa7292 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -54,7 +54,8 @@ from xml.etree import ElementTree
from eventlet import greenthread
from eventlet import tpool
-from nova import context
+from nova import block_device
+from nova import context as nova_context
from nova import db
from nova import exception
from nova import flags
@@ -121,8 +122,6 @@ flags.DEFINE_integer('live_migration_bandwidth', 0,
'Define live migration behavior')
flags.DEFINE_string('qemu_img', 'qemu-img',
'binary to use for qemu-img commands')
-flags.DEFINE_bool('start_guests_on_host_boot', False,
- 'Whether to restart guests when the host reboots')
flags.DEFINE_string('libvirt_vif_type', 'bridge',
'Type of VIF to create.')
flags.DEFINE_string('libvirt_vif_driver',
@@ -153,8 +152,8 @@ def _late_load_cheetah():
Template = t.Template
-def _strip_dev(mount_path):
- return re.sub(r'^/dev/', '', mount_path)
+def _get_eph_disk(ephemeral):
+ return 'disk.eph' + str(ephemeral['num'])
class LibvirtConnection(driver.ComputeDriver):
@@ -173,27 +172,8 @@ class LibvirtConnection(driver.ComputeDriver):
self.vif_driver = utils.import_object(FLAGS.libvirt_vif_driver)
def init_host(self, host):
- # Adopt existing VM's running here
- ctxt = context.get_admin_context()
- for instance in db.instance_get_all_by_host(ctxt, host):
- try:
- LOG.debug(_('Checking state of %s'), instance['name'])
- state = self.get_info(instance['name'])['state']
- except exception.NotFound:
- state = power_state.SHUTOFF
-
- LOG.debug(_('Current state of %(name)s was %(state)s.'),
- {'name': instance['name'], 'state': state})
- db.instance_set_state(ctxt, instance['id'], state)
-
- # NOTE(justinsb): We no longer delete SHUTOFF instances,
- # the user may want to power them back on
-
- if state != power_state.RUNNING:
- continue
- self.firewall_driver.setup_basic_filtering(instance)
- self.firewall_driver.prepare_instance_filter(instance)
- self.firewall_driver.apply_instance_filter(instance)
+ # NOTE(nsokolov): moved instance restarting to ComputeManager
+ pass
def _get_connection(self):
if not self._wrapped_conn or not self._test_connection():
@@ -370,7 +350,7 @@ class LibvirtConnection(driver.ComputeDriver):
"""Returns the xml for the disk mounted at device"""
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return None
ctx = doc.xpathNewContext()
try:
@@ -396,7 +376,7 @@ class LibvirtConnection(driver.ComputeDriver):
virt_dom.detachDevice(xml)
@exception.wrap_exception()
- def snapshot(self, instance, image_href):
+ def snapshot(self, context, instance, image_href):
"""Create snapshot from a running VM instance.
This command only works with qemu 0.14+, the qemu_img flag is
@@ -405,18 +385,15 @@ class LibvirtConnection(driver.ComputeDriver):
"""
virt_dom = self._lookup_by_name(instance['name'])
- elevated = context.get_admin_context()
(image_service, image_id) = nova.image.get_image_service(
instance['image_ref'])
- base = image_service.show(elevated, image_id)
+ base = image_service.show(context, image_id)
(snapshot_image_service, snapshot_image_id) = \
nova.image.get_image_service(image_href)
- snapshot = snapshot_image_service.show(elevated, snapshot_image_id)
+ snapshot = snapshot_image_service.show(context, snapshot_image_id)
- metadata = {'disk_format': base['disk_format'],
- 'container_format': base['container_format'],
- 'is_public': False,
+ metadata = {'is_public': False,
'status': 'active',
'name': snapshot['name'],
'properties': {
@@ -431,6 +408,12 @@ class LibvirtConnection(driver.ComputeDriver):
arch = base['properties']['architecture']
metadata['properties']['architecture'] = arch
+ if 'disk_format' in base:
+ metadata['disk_format'] = base['disk_format']
+
+ if 'container_format' in base:
+ metadata['container_format'] = base['container_format']
+
# Make the snapshot
snapshot_name = uuid.uuid4().hex
snapshot_xml = """
@@ -463,7 +446,7 @@ class LibvirtConnection(driver.ComputeDriver):
# Upload that image to the image service
with open(out_path) as image_file:
- image_service.update(elevated,
+ image_service.update(context,
image_href,
metadata,
image_file)
@@ -538,7 +521,7 @@ class LibvirtConnection(driver.ComputeDriver):
dom.create()
@exception.wrap_exception()
- def rescue(self, instance, callback, network_info):
+ def rescue(self, context, instance, callback, network_info):
"""Loads a VM using rescue images.
A rescue is normally performed when something goes wrong with the
@@ -553,7 +536,7 @@ class LibvirtConnection(driver.ComputeDriver):
rescue_images = {'image_id': FLAGS.rescue_image_id,
'kernel_id': FLAGS.rescue_kernel_id,
'ramdisk_id': FLAGS.rescue_ramdisk_id}
- self._create_image(instance, xml, '.rescue', rescue_images)
+ self._create_image(context, instance, xml, '.rescue', rescue_images)
self._create_new_domain(xml)
def _wait_for_rescue():
@@ -592,23 +575,18 @@ class LibvirtConnection(driver.ComputeDriver):
# NOTE(ilyaalekseyev): Implementation like in multinics
# for xenapi(tr3buchet)
@exception.wrap_exception()
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
xml = self.to_xml(instance, False, network_info=network_info,
- block_device_mapping=block_device_mapping)
- block_device_mapping = block_device_mapping or []
+ block_device_info=block_device_info)
self.firewall_driver.setup_basic_filtering(instance, network_info)
self.firewall_driver.prepare_instance_filter(instance, network_info)
- self._create_image(instance, xml, network_info=network_info,
- block_device_mapping=block_device_mapping)
+ 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)
- if FLAGS.start_guests_on_host_boot:
- LOG.debug(_("instance %s: setting autostart ON") %
- instance['name'])
- domain.setAutostart(1)
-
def _wait_for_boot():
"""Called at an interval until the VM is running."""
instance_name = instance['name']
@@ -769,9 +747,10 @@ class LibvirtConnection(driver.ComputeDriver):
else:
utils.execute('cp', base, target)
- def _fetch_image(self, target, image_id, user_id, project_id, size=None):
+ def _fetch_image(self, context, target, image_id, user_id, project_id,
+ size=None):
"""Grab image and optionally attempt to resize it"""
- images.fetch(image_id, target, user_id, project_id)
+ images.fetch(context, image_id, target, user_id, project_id)
if size:
disk.extend(target, size)
@@ -780,10 +759,14 @@ class LibvirtConnection(driver.ComputeDriver):
utils.execute('truncate', target, '-s', "%dG" % local_gb)
# TODO(vish): should we format disk by default?
- def _create_image(self, inst, libvirt_xml, suffix='', disk_images=None,
- network_info=None, block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
+ def _create_swap(self, target, swap_gb):
+ """Create a swap file of specified size"""
+ self._create_local(target, swap_gb)
+ utils.execute('mkswap', target)
+ def _create_image(self, context, inst, libvirt_xml, suffix='',
+ disk_images=None, network_info=None,
+ block_device_info=None):
if not suffix:
suffix = ''
@@ -817,6 +800,7 @@ class LibvirtConnection(driver.ComputeDriver):
if disk_images['kernel_id']:
fname = '%08x' % int(disk_images['kernel_id'])
self._cache_image(fn=self._fetch_image,
+ context=context,
target=basepath('kernel'),
fname=fname,
image_id=disk_images['kernel_id'],
@@ -825,6 +809,7 @@ class LibvirtConnection(driver.ComputeDriver):
if disk_images['ramdisk_id']:
fname = '%08x' % int(disk_images['ramdisk_id'])
self._cache_image(fn=self._fetch_image,
+ context=context,
target=basepath('ramdisk'),
fname=fname,
image_id=disk_images['ramdisk_id'],
@@ -840,9 +825,10 @@ class LibvirtConnection(driver.ComputeDriver):
size = None
root_fname += "_sm"
- if not self._volume_in_mapping(self.root_mount_device,
- block_device_mapping):
+ if not self._volume_in_mapping(self.default_root_device,
+ block_device_info):
self._cache_image(fn=self._fetch_image,
+ context=context,
target=basepath('disk'),
fname=root_fname,
cow=FLAGS.use_cow_images,
@@ -851,13 +837,38 @@ class LibvirtConnection(driver.ComputeDriver):
project_id=inst['project_id'],
size=size)
- if inst_type['local_gb'] and not self._volume_in_mapping(
- self.local_mount_device, block_device_mapping):
+ local_gb = inst['local_gb']
+ if local_gb and not self._volume_in_mapping(
+ self.default_local_device, block_device_info):
self._cache_image(fn=self._create_local,
target=basepath('disk.local'),
- fname="local_%s" % inst_type['local_gb'],
+ fname="local_%s" % local_gb,
cow=FLAGS.use_cow_images,
- local_gb=inst_type['local_gb'])
+ local_gb=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'])
+
+ swap_gb = 0
+
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ swap_gb = swap['swap_size']
+ elif (inst_type['swap'] > 0 and
+ not self._volume_in_mapping(self.default_swap_device,
+ block_device_info)):
+ swap_gb = inst_type['swap']
+
+ if swap_gb > 0:
+ self._cache_image(fn=self._create_swap,
+ target=basepath('disk.swap'),
+ fname="swap_%s" % swap_gb,
+ cow=FLAGS.use_cow_images,
+ swap_gb=swap_gb)
# For now, we assume that if we're not using a kernel, we're using a
# partitioned disk image where the target partition is the first
@@ -879,7 +890,7 @@ class LibvirtConnection(driver.ComputeDriver):
ifc_template = open(FLAGS.injected_network_template).read()
ifc_num = -1
have_injected_networks = False
- admin_context = context.get_admin_context()
+ admin_context = nova_context.get_admin_context()
for (network_ref, mapping) in network_info:
ifc_num += 1
@@ -901,7 +912,7 @@ class LibvirtConnection(driver.ComputeDriver):
'netmask': netmask,
'gateway': mapping['gateway'],
'broadcast': mapping['broadcast'],
- 'dns': mapping['dns'],
+ 'dns': ' '.join(mapping['dns']),
'address_v6': address_v6,
'gateway6': gateway_v6,
'netmask_v6': netmask_v6}
@@ -938,16 +949,35 @@ class LibvirtConnection(driver.ComputeDriver):
if FLAGS.libvirt_type == 'uml':
utils.execute('sudo', 'chown', 'root', basepath('disk'))
- root_mount_device = 'vda' # FIXME for now. it's hard coded.
- local_mount_device = 'vdb' # FIXME for now. it's hard coded.
-
- def _volume_in_mapping(self, mount_device, block_device_mapping):
- mount_device_ = _strip_dev(mount_device)
- for vol in block_device_mapping:
- vol_mount_device = _strip_dev(vol['mount_device'])
- if vol_mount_device == mount_device_:
- return True
- return False
+ if FLAGS.libvirt_type == 'uml':
+ _disk_prefix = 'ubd'
+ elif FLAGS.libvirt_type == 'xen':
+ _disk_prefix = 'sd'
+ elif FLAGS.libvirt_type == 'lxc':
+ _disk_prefix = ''
+ else:
+ _disk_prefix = 'vd'
+
+ default_root_device = _disk_prefix + 'a'
+ default_local_device = _disk_prefix + 'b'
+ default_swap_device = _disk_prefix + 'c'
+
+ def _volume_in_mapping(self, mount_device, block_device_info):
+ block_device_list = [block_device.strip_dev(vol['mount_device'])
+ for vol in
+ driver.block_device_info_get_mapping(
+ block_device_info)]
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ block_device_list.append(
+ block_device.strip_dev(swap['device_name']))
+ block_device_list += [block_device.strip_dev(ephemeral['device_name'])
+ for ephemeral in
+ driver.block_device_info_get_ephemerals(
+ block_device_info)]
+
+ LOG.debug(_("block_device_list %s"), block_device_list)
+ return block_device.strip_dev(mount_device) in block_device_list
def _get_volume_device_info(self, device_path):
if device_path.startswith('/dev/'):
@@ -959,8 +989,9 @@ class LibvirtConnection(driver.ComputeDriver):
raise exception.InvalidDevicePath(path=device_path)
def _prepare_xml_info(self, instance, rescue=False, network_info=None,
- block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
+ block_device_info=None):
+ block_device_mapping = driver.block_device_info_get_mapping(
+ block_device_info)
# TODO(adiantum) remove network_info creation code
# when multinics will be completed
if not network_info:
@@ -979,17 +1010,27 @@ class LibvirtConnection(driver.ComputeDriver):
driver_type = 'raw'
for vol in block_device_mapping:
- vol['mount_device'] = _strip_dev(vol['mount_device'])
+ vol['mount_device'] = block_device.strip_dev(vol['mount_device'])
(vol['type'], vol['protocol'], vol['name']) = \
self._get_volume_device_info(vol['device_path'])
- ebs_root = self._volume_in_mapping(self.root_mount_device,
- block_device_mapping)
- if self._volume_in_mapping(self.local_mount_device,
- block_device_mapping):
- local_gb = False
- else:
- local_gb = inst_type['local_gb']
+ ebs_root = self._volume_in_mapping(self.default_root_device,
+ block_device_info)
+
+ local_device = False
+ if not (self._volume_in_mapping(self.default_local_device,
+ block_device_info) or
+ 0 in [eph['num'] for eph in
+ driver.block_device_info_get_ephemerals(
+ block_device_info)]):
+ if instance['local_gb'] > 0:
+ local_device = self.default_local_device
+
+ ephemerals = []
+ for eph in driver.block_device_info_get_ephemerals(block_device_info):
+ ephemerals.append({'device_path': _get_eph_disk(eph),
+ 'device': block_device.strip_dev(
+ eph['device_name'])})
xml_info = {'type': FLAGS.libvirt_type,
'name': instance['name'],
@@ -998,12 +1039,35 @@ class LibvirtConnection(driver.ComputeDriver):
'memory_kb': inst_type['memory_mb'] * 1024,
'vcpus': inst_type['vcpus'],
'rescue': rescue,
- 'local': local_gb,
+ 'disk_prefix': self._disk_prefix,
'driver_type': driver_type,
'vif_type': FLAGS.libvirt_vif_type,
'nics': nics,
'ebs_root': ebs_root,
- 'volumes': block_device_mapping}
+ 'local_device': local_device,
+ 'volumes': block_device_mapping,
+ 'ephemerals': ephemerals}
+
+ root_device_name = driver.block_device_info_get_root(block_device_info)
+ if root_device_name:
+ xml_info['root_device'] = block_device.strip_dev(root_device_name)
+ xml_info['root_device_name'] = root_device_name
+ else:
+ # NOTE(yamahata):
+ # for nova.api.ec2.cloud.CloudController.get_metadata()
+ xml_info['root_device'] = self.default_root_device
+ db.instance_update(
+ nova_context.get_admin_context(), instance['id'],
+ {'root_device_name': '/dev/' + self.default_root_device})
+
+ swap = driver.block_device_info_get_swap(block_device_info)
+ if driver.swap_is_usable(swap):
+ xml_info['swap_device'] = block_device.strip_dev(
+ swap['device_name'])
+ elif (inst_type['swap'] > 0 and
+ not self._volume_in_mapping(self.default_swap_device,
+ block_device_info)):
+ xml_info['swap_device'] = self.default_swap_device
if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'):
xml_info['vncserver_host'] = FLAGS.vncserver_host
@@ -1019,12 +1083,11 @@ class LibvirtConnection(driver.ComputeDriver):
return xml_info
def to_xml(self, instance, rescue=False, network_info=None,
- block_device_mapping=None):
- block_device_mapping = block_device_mapping or []
+ block_device_info=None):
# TODO(termie): cache?
LOG.debug(_('instance %s: starting toXML method'), instance['name'])
xml_info = self._prepare_xml_info(instance, rescue, network_info,
- block_device_mapping)
+ block_device_info)
xml = str(Template(self.libvirt_xml, searchList=[xml_info]))
LOG.debug(_('instance %s: finished toXML method'), instance['name'])
return xml
@@ -1098,7 +1161,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1139,7 +1202,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1579,6 +1642,10 @@ class LibvirtConnection(driver.ComputeDriver):
"""See xenapi_conn.py implementation."""
pass
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py
index eef582fac..711b05bae 100644
--- a/nova/virt/libvirt/vif.py
+++ b/nova/virt/libvirt/vif.py
@@ -25,6 +25,7 @@ from nova.network import linux_net
from nova.virt.libvirt import netutils
from nova import utils
from nova.virt.vif import VIFDriver
+from nova import exception
LOG = logging.getLogger('nova.virt.libvirt.vif')
@@ -128,7 +129,7 @@ class LibvirtOpenVswitchDriver(VIFDriver):
utils.execute('sudo', 'ovs-vsctl', 'del-port',
network['bridge'], dev)
utils.execute('sudo', 'ip', 'link', 'delete', dev)
- except:
+ except exception.ProcessExecutionError:
LOG.warning(_("Failed while unplugging vif of instance '%s'"),
instance['name'])
raise
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py
index 1ee8fa1c0..07a6ba6ab 100644
--- a/nova/virt/vmwareapi/vmops.py
+++ b/nova/virt/vmwareapi/vmops.py
@@ -26,7 +26,7 @@ import urllib
import urllib2
import uuid
-from nova import context
+from nova import context as nova_context
from nova import db
from nova import exception
from nova import flags
@@ -89,7 +89,7 @@ class VMWareVMOps(object):
LOG.debug(_("Got total of %s instances") % str(len(lst_vm_names)))
return lst_vm_names
- def spawn(self, instance, network_info):
+ def spawn(self, context, instance, network_info):
"""
Creates a VM instance.
@@ -111,7 +111,7 @@ class VMWareVMOps(object):
client_factory = self._session._get_vim().client.factory
service_content = self._session._get_vim().get_service_content()
- network = db.network_get_by_instance(context.get_admin_context(),
+ network = db.network_get_by_instance(nova_context.get_admin_context(),
instance['id'])
net_name = network['bridge']
@@ -329,7 +329,7 @@ class VMWareVMOps(object):
LOG.debug(_("Powered on the VM instance %s") % instance.name)
_power_on_vm()
- def snapshot(self, instance, snapshot_name):
+ def snapshot(self, context, instance, snapshot_name):
"""
Create snapshot from a running VM instance.
Steps followed are:
@@ -721,11 +721,11 @@ class VMWareVMOps(object):
Set the machine id of the VM for guest tools to pick up and change
the IP.
"""
- admin_context = context.get_admin_context()
+ admin_context = nova_context.get_admin_context()
vm_ref = self._get_vm_ref_from_the_name(instance.name)
if vm_ref is None:
raise exception.InstanceNotFound(instance_id=instance.id)
- network = db.network_get_by_instance(context.get_admin_context(),
+ network = db.network_get_by_instance(nova_context.get_admin_context(),
instance['id'])
mac_address = None
if instance['mac_addresses']:
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py
index 10d80c0a0..243ee64f5 100644
--- a/nova/virt/vmwareapi_conn.py
+++ b/nova/virt/vmwareapi_conn.py
@@ -124,13 +124,14 @@ class VMWareESXConnection(driver.ComputeDriver):
"""List VM instances."""
return self._vmops.list_instances()
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance, network_info,
+ block_device_mapping=None):
"""Create VM instance."""
- self._vmops.spawn(instance, network_info)
+ self._vmops.spawn(context, instance, network_info)
- def snapshot(self, instance, name):
+ def snapshot(self, context, instance, name):
"""Create snapshot from a running VM instance."""
- self._vmops.snapshot(instance, name)
+ self._vmops.snapshot(context, instance, name)
def reboot(self, instance, network_info):
"""Reboot VM instance."""
@@ -190,6 +191,10 @@ class VMWareESXConnection(driver.ComputeDriver):
"""This method is supported only by libvirt."""
return
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 8a715af35..d20746076 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -342,7 +342,7 @@ class VMHelper(HelperBase):
return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid)
@classmethod
- def upload_image(cls, session, instance, vdi_uuids, image_id):
+ def upload_image(cls, context, session, instance, vdi_uuids, image_id):
""" Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name.
"""
@@ -360,29 +360,30 @@ class VMHelper(HelperBase):
'glance_host': glance_host,
'glance_port': glance_port,
'sr_path': cls.get_sr_path(session),
- 'os_type': os_type}
+ 'os_type': os_type,
+ 'auth_token': getattr(context, 'auth_token', None)}
kwargs = {'params': pickle.dumps(params)}
task = session.async_call_plugin('glance', 'upload_vhd', kwargs)
session.wait_for_task(task, instance.id)
@classmethod
- def fetch_image(cls, session, instance_id, image, user_id, project_id,
- image_type):
+ def fetch_image(cls, context, session, instance_id, image, user_id,
+ project_id, image_type):
"""Fetch image from glance based on image type.
Returns: A single filename if image_type is KERNEL or RAMDISK
A list of dictionaries that describe VDIs, otherwise
"""
if image_type == ImageType.DISK_VHD:
- return cls._fetch_image_glance_vhd(
+ return cls._fetch_image_glance_vhd(context,
session, instance_id, image, image_type)
else:
- return cls._fetch_image_glance_disk(
+ return cls._fetch_image_glance_disk(context,
session, instance_id, image, image_type)
@classmethod
- def _fetch_image_glance_vhd(cls, session, instance_id, image,
+ def _fetch_image_glance_vhd(cls, context, session, instance_id, image,
image_type):
"""Tell glance to download an image and put the VHDs into the SR
@@ -404,7 +405,8 @@ class VMHelper(HelperBase):
'glance_host': glance_host,
'glance_port': glance_port,
'uuid_stack': uuid_stack,
- 'sr_path': cls.get_sr_path(session)}
+ 'sr_path': cls.get_sr_path(session),
+ 'auth_token': getattr(context, 'auth_token', None)}
kwargs = {'params': pickle.dumps(params)}
task = session.async_call_plugin('glance', 'download_vhd', kwargs)
@@ -430,7 +432,7 @@ class VMHelper(HelperBase):
return vdis
@classmethod
- def _fetch_image_glance_disk(cls, session, instance_id, image,
+ def _fetch_image_glance_disk(cls, context, session, instance_id, image,
image_type):
"""Fetch the image from Glance
@@ -450,6 +452,7 @@ class VMHelper(HelperBase):
sr_ref = safe_find_sr(session)
glance_client, image_id = nova.image.get_glance_client(image)
+ glance_client.set_auth_token(getattr(context, 'auth_token', None))
meta, image_file = glance_client.get_image(image_id)
virtual_size = int(meta['size'])
vdi_size = virtual_size
@@ -1092,6 +1095,8 @@ def _prepare_injectables(inst, networks_info):
ip_v6 = info['ip6s'][0]
if len(info['dns']) > 0:
dns = info['dns'][0]
+ else:
+ dns = ''
interface_info = {'name': 'eth%d' % ifc_num,
'address': ip_v4 and ip_v4['ip'] or '',
'netmask': ip_v4 and ip_v4['netmask'] or '',
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 7e02e1def..50aa0d3b2 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -30,7 +30,7 @@ import sys
import time
import uuid
-from nova import context
+from nova import context as nova_context
from nova import db
from nova import exception
from nova import flags
@@ -113,16 +113,16 @@ class VMOps(object):
vm_ref = VMHelper.lookup(self._session, instance.name)
self._start(instance, vm_ref)
- def finish_migration(self, instance, disk_info, network_info,
+ def finish_migration(self, context, instance, disk_info, network_info,
resize_instance):
vdi_uuid = self.link_disks(instance, disk_info['base_copy'],
disk_info['cow'])
- vm_ref = self._create_vm(instance,
+ vm_ref = self._create_vm(context, instance,
[dict(vdi_type='os', vdi_uuid=vdi_uuid)],
network_info)
if resize_instance:
self.resize_instance(instance, vdi_uuid)
- self._spawn(instance, vm_ref)
+ self._start(instance, vm_ref=vm_ref)
def _start(self, instance, vm_ref=None):
"""Power on a VM instance"""
@@ -134,19 +134,19 @@ class VMOps(object):
LOG.debug(_("Starting instance %s"), instance.name)
self._session.call_xenapi('VM.start', vm_ref, False, False)
- def _create_disks(self, instance):
+ def _create_disks(self, context, instance):
disk_image_type = VMHelper.determine_disk_image_type(instance)
- vdis = VMHelper.fetch_image(self._session,
+ vdis = VMHelper.fetch_image(context, self._session,
instance.id, instance.image_ref,
instance.user_id, instance.project_id,
disk_image_type)
return vdis
- def spawn(self, instance, network_info):
+ def spawn(self, context, instance, network_info):
vdis = None
try:
- vdis = self._create_disks(instance)
- vm_ref = self._create_vm(instance, vdis, network_info)
+ vdis = self._create_disks(context, instance)
+ vm_ref = self._create_vm(context, instance, vdis, network_info)
self._spawn(instance, vm_ref)
except (self.XenAPI.Failure, OSError, IOError) as spawn_error:
LOG.exception(_("instance %s: Failed to spawn"),
@@ -156,11 +156,11 @@ class VMOps(object):
self._handle_spawn_error(vdis, spawn_error)
raise spawn_error
- def spawn_rescue(self, instance):
+ def spawn_rescue(self, context, instance, network_info):
"""Spawn a rescue instance."""
- self.spawn(instance)
+ self.spawn(context, instance, network_info)
- def _create_vm(self, instance, vdis, network_info):
+ def _create_vm(self, context, instance, vdis, network_info):
"""Create VM instance."""
instance_name = instance.name
vm_ref = VMHelper.lookup(self._session, instance_name)
@@ -171,7 +171,7 @@ class VMOps(object):
if not VMHelper.ensure_free_mem(self._session, instance):
LOG.exception(_('instance %(instance_name)s: not enough free '
'memory') % locals())
- db.instance_set_state(context.get_admin_context(),
+ db.instance_set_state(nova_context.get_admin_context(),
instance['id'],
power_state.SHUTDOWN)
return
@@ -181,12 +181,12 @@ class VMOps(object):
ramdisk = None
try:
if instance.kernel_id:
- kernel = VMHelper.fetch_image(self._session, instance.id,
- instance.kernel_id, instance.user_id,
+ kernel = VMHelper.fetch_image(context, self._session,
+ instance.id, instance.kernel_id, instance.user_id,
instance.project_id, ImageType.KERNEL)[0]
if instance.ramdisk_id:
- ramdisk = VMHelper.fetch_image(self._session, instance.id,
- instance.kernel_id, instance.user_id,
+ ramdisk = VMHelper.fetch_image(context, self._session,
+ instance.id, instance.kernel_id, instance.user_id,
instance.project_id, ImageType.RAMDISK)[0]
# Create the VM ref and attach the first disk
first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
@@ -206,7 +206,7 @@ class VMOps(object):
if instance.vm_mode != vm_mode:
# Update database with normalized (or determined) value
- db.instance_update(context.get_admin_context(),
+ db.instance_update(nova_context.get_admin_context(),
instance['id'], {'vm_mode': vm_mode})
vm_ref = VMHelper.create_vm(self._session, instance,
kernel and kernel.get('file', None) or None,
@@ -268,7 +268,7 @@ class VMOps(object):
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
- ctx = context.get_admin_context()
+ ctx = nova_context.get_admin_context()
agent_build = db.agent_build_get_by_triple(ctx, 'xen',
instance.os_type, instance.architecture)
if agent_build:
@@ -282,6 +282,7 @@ class VMOps(object):
'architecture': instance.architecture})
def _check_agent_version():
+ LOG.debug(_("Querying agent version"))
if instance.os_type == 'windows':
# Windows will generally perform a setup process on first boot
# that can take a couple of minutes and then reboot. So we
@@ -292,7 +293,6 @@ class VMOps(object):
else:
version = self.get_agent_version(instance)
if not version:
- LOG.info(_('No agent version returned by instance'))
return
LOG.info(_('Instance agent version: %s') % version)
@@ -327,6 +327,10 @@ class VMOps(object):
LOG.debug(_("Setting admin password"))
self.set_admin_password(instance, admin_password)
+ def _reset_network():
+ LOG.debug(_("Resetting network"))
+ self.reset_network(instance, vm_ref)
+
# NOTE(armando): Do we really need to do this in virt?
# NOTE(tr3buchet): not sure but wherever we do it, we need to call
# reset_network afterwards
@@ -341,7 +345,7 @@ class VMOps(object):
_check_agent_version()
_inject_files()
_set_admin_password()
- self.reset_network(instance, vm_ref)
+ _reset_network()
return True
except Exception, exc:
LOG.warn(exc)
@@ -412,7 +416,7 @@ class VMOps(object):
# if instance_or_vm is an int/long it must be instance id
elif isinstance(instance_or_vm, (int, long)):
- ctx = context.get_admin_context()
+ ctx = nova_context.get_admin_context()
instance_obj = db.instance_get(ctx, instance_or_vm)
instance_name = instance_obj.name
else:
@@ -437,9 +441,10 @@ class VMOps(object):
vm,
"start")
- def snapshot(self, instance, image_id):
+ def snapshot(self, context, instance, image_id):
"""Create snapshot from a running VM instance.
+ :param context: request context
:param instance: instance to be snapshotted
:param image_id: id of image to upload to
@@ -464,7 +469,7 @@ class VMOps(object):
try:
template_vm_ref, template_vdi_uuids = self._get_snapshot(instance)
# call plugin to ship snapshot off to glance
- VMHelper.upload_image(
+ VMHelper.upload_image(context,
self._session, instance, template_vdi_uuids, image_id)
finally:
if template_vm_ref:
@@ -596,13 +601,13 @@ class VMOps(object):
transaction_id = str(uuid.uuid4())
args = {'id': transaction_id}
resp = self._make_agent_call('version', instance, '', args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to query agent version: %(resp)r') %
+ locals())
+ return None
# Some old versions of the Windows agent have a trailing \\r\\n
# (ie CRLF escaped) for some reason. Strip that off.
- return resp_dict['message'].replace('\\r\\n', '')
+ return resp['message'].replace('\\r\\n', '')
if timeout:
vm_ref = self._get_vm_opaque_ref(instance)
@@ -633,13 +638,10 @@ class VMOps(object):
transaction_id = str(uuid.uuid4())
args = {'id': transaction_id, 'url': url, 'md5sum': md5sum}
resp = self._make_agent_call('agentupdate', instance, '', args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
- if resp_dict['returncode'] != '0':
- raise RuntimeError(resp_dict['message'])
- return resp_dict['message']
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to update agent: %(resp)r') % locals())
+ return None
+ return resp['message']
def set_admin_password(self, instance, new_pass):
"""Set the root/admin password on the VM instance.
@@ -658,18 +660,13 @@ class VMOps(object):
key_init_args = {'id': key_init_transaction_id,
'pub': str(dh.get_public())}
resp = self._make_agent_call('key_init', instance, '', key_init_args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
# Successful return code from key_init is 'D0'
- if resp_dict['returncode'] != 'D0':
- # There was some sort of error; the message will contain
- # a description of the error.
- raise RuntimeError(resp_dict['message'])
+ if resp['returncode'] != 'D0':
+ LOG.error(_('Failed to exchange keys: %(resp)r') % locals())
+ return None
# Some old versions of the Windows agent have a trailing \\r\\n
# (ie CRLF escaped) for some reason. Strip that off.
- agent_pub = int(resp_dict['message'].replace('\\r\\n', ''))
+ agent_pub = int(resp['message'].replace('\\r\\n', ''))
dh.compute_shared(agent_pub)
# Some old versions of Linux and Windows agent expect trailing \n
# on password to work correctly.
@@ -678,17 +675,14 @@ class VMOps(object):
password_transaction_id = str(uuid.uuid4())
password_args = {'id': password_transaction_id, 'enc_pass': enc_pass}
resp = self._make_agent_call('password', instance, '', password_args)
- if resp is None:
- # No response from the agent
- return
- resp_dict = json.loads(resp)
# Successful return code from password is '0'
- if resp_dict['returncode'] != '0':
- raise RuntimeError(resp_dict['message'])
- db.instance_update(context.get_admin_context(),
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to update password: %(resp)r') % locals())
+ return None
+ db.instance_update(nova_context.get_admin_context(),
instance['id'],
dict(admin_pass=new_pass))
- return resp_dict['message']
+ return resp['message']
def inject_file(self, instance, path, contents):
"""Write a file to the VM instance.
@@ -711,12 +705,10 @@ class VMOps(object):
# If the agent doesn't support file injection, a NotImplementedError
# will be raised with the appropriate message.
resp = self._make_agent_call('inject_file', instance, '', args)
- resp_dict = json.loads(resp)
- if resp_dict['returncode'] != '0':
- # There was some other sort of error; the message will contain
- # a description of the error.
- raise RuntimeError(resp_dict['message'])
- return resp_dict['message']
+ if resp['returncode'] != '0':
+ LOG.error(_('Failed to inject file: %(resp)r') % locals())
+ return None
+ return resp['message']
def _shutdown(self, instance, vm_ref, hard=True):
"""Shutdown an instance."""
@@ -742,6 +734,17 @@ class VMOps(object):
except self.XenAPI.Failure, exc:
LOG.exception(exc)
+ def _find_rescue_vbd_ref(self, vm_ref, rescue_vm_ref):
+ """Find and return the rescue VM's vbd_ref.
+
+ We use the second VBD here because swap is first with the root file
+ system coming in second."""
+ vbd_ref = self._session.get_xenapi().VM.get_VBDs(vm_ref)[1]
+ vdi_ref = self._session.get_xenapi().VBD.get_record(vbd_ref)["VDI"]
+
+ return VMHelper.create_vbd(self._session, rescue_vm_ref, vdi_ref, 1,
+ False)
+
def _shutdown_rescue(self, rescue_vm_ref):
"""Shutdown a rescue instance."""
self._session.call_xenapi("Async.VM.hard_shutdown", rescue_vm_ref)
@@ -913,7 +916,7 @@ class VMOps(object):
True)
self._wait_with_callback(instance.id, task, callback)
- def rescue(self, instance, callback):
+ def rescue(self, context, instance, _callback, network_info):
"""Rescue the specified instance.
- shutdown the instance VM.
@@ -931,17 +934,13 @@ class VMOps(object):
self._shutdown(instance, vm_ref)
self._acquire_bootlock(vm_ref)
instance._rescue = True
- self.spawn_rescue(instance)
+ self.spawn_rescue(context, instance, network_info)
rescue_vm_ref = VMHelper.lookup(self._session, instance.name)
-
- vbd_ref = self._session.get_xenapi().VM.get_VBDs(vm_ref)[0]
- vdi_ref = self._session.get_xenapi().VBD.get_record(vbd_ref)["VDI"]
- rescue_vbd_ref = VMHelper.create_vbd(self._session, rescue_vm_ref,
- vdi_ref, 1, False)
+ rescue_vbd_ref = self._find_rescue_vbd_ref(vm_ref, rescue_vm_ref)
self._session.call_xenapi("Async.VBD.plug", rescue_vbd_ref)
- def unrescue(self, instance, callback):
+ def unrescue(self, instance, _callback):
"""Unrescue the specified instance.
- unplug the instance VM's disk from the rescue VM.
@@ -1023,11 +1022,23 @@ class VMOps(object):
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
+ def host_power_action(self, host, action):
+ """Reboots or shuts down the host."""
+ args = {"action": json.dumps(action)}
+ methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"}
+ json_resp = self._call_xenhost(methods[action], args)
+ resp = json.loads(json_resp)
+ return resp["power_action"]
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
args = {"enabled": json.dumps(enabled)}
- json_resp = self._call_xenhost("set_host_enabled", args)
- resp = json.loads(json_resp)
+ xenapi_resp = self._call_xenhost("set_host_enabled", args)
+ try:
+ resp = json.loads(xenapi_resp)
+ except TypeError as e:
+ # Already logged; return the message
+ return xenapi_resp.details[-1]
return resp["status"]
def _call_xenhost(self, method, arg_dict):
@@ -1043,7 +1054,7 @@ class VMOps(object):
#args={"params": arg_dict})
ret = self._session.wait_for_task(task, task_id)
except self.XenAPI.Failure as e:
- ret = None
+ ret = e
LOG.error(_("The call to %(method)s returned an error: %(e)s.")
% locals())
return ret
@@ -1158,8 +1169,19 @@ class VMOps(object):
def _make_agent_call(self, method, vm, path, addl_args=None):
"""Abstracts out the interaction with the agent xenapi plugin."""
- return self._make_plugin_call('agent', method=method, vm=vm,
+ ret = self._make_plugin_call('agent', method=method, vm=vm,
path=path, addl_args=addl_args)
+ if isinstance(ret, dict):
+ return ret
+ try:
+ return json.loads(ret)
+ except TypeError:
+ instance_id = vm.id
+ LOG.error(_('The agent call to %(method)s returned an invalid'
+ ' response: %(ret)r. VM id=%(instance_id)s;'
+ ' path=%(path)s; args=%(addl_args)r') % locals())
+ return {'returncode': 'error',
+ 'message': 'unable to deserialize response'}
def _make_plugin_call(self, plugin, method, vm, path, addl_args=None,
vm_ref=None):
@@ -1177,20 +1199,20 @@ class VMOps(object):
ret = self._session.wait_for_task(task, instance_id)
except self.XenAPI.Failure, e:
ret = None
- err_trace = e.details[-1]
- err_msg = err_trace.splitlines()[-1]
- strargs = str(args)
+ err_msg = e.details[-1].splitlines()[-1]
if 'TIMEOUT:' in err_msg:
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
- 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ 'VM id=%(instance_id)s; args=%(args)r') % locals())
+ return {'returncode': 'timeout', 'message': err_msg}
elif 'NOT IMPLEMENTED:' in err_msg:
LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
' supported by the agent. VM id=%(instance_id)s;'
- ' args=%(strargs)s') % locals())
- raise NotImplementedError(err_msg)
+ ' args=%(args)r') % locals())
+ return {'returncode': 'notimplemented', 'message': err_msg}
else:
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
- 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ 'VM id=%(instance_id)s; args=%(args)r') % locals())
+ return {'returncode': 'error', 'message': err_msg}
return ret
def add_to_xenstore(self, vm, path, key, value):
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 571638496..76b6c57fc 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -184,23 +184,24 @@ class XenAPIConnection(driver.ComputeDriver):
def list_instances_detail(self):
return self._vmops.list_instances_detail()
- def spawn(self, instance, network_info, block_device_mapping=None):
+ def spawn(self, context, instance,
+ network_info=None, block_device_info=None):
"""Create VM instance"""
- self._vmops.spawn(instance, network_info)
+ self._vmops.spawn(context, instance, network_info)
def revert_migration(self, instance):
"""Reverts a resize, powering back on the instance"""
self._vmops.revert_migration(instance)
- def finish_migration(self, instance, disk_info, network_info,
+ def finish_migration(self, context, instance, disk_info, network_info,
resize_instance=False):
"""Completes a resize, turning on the migrated instance"""
- self._vmops.finish_migration(instance, disk_info, network_info,
- resize_instance)
+ self._vmops.finish_migration(context, instance, disk_info,
+ network_info, resize_instance)
- def snapshot(self, instance, image_id):
+ def snapshot(self, context, instance, image_id):
""" Create snapshot from a running VM instance """
- self._vmops.snapshot(instance, image_id)
+ self._vmops.snapshot(context, instance, image_id)
def reboot(self, instance, network_info):
"""Reboot VM instance"""
@@ -241,13 +242,13 @@ class XenAPIConnection(driver.ComputeDriver):
"""resume the specified instance"""
self._vmops.resume(instance, callback)
- def rescue(self, instance, callback, network_info):
+ def rescue(self, context, instance, _callback, network_info):
"""Rescue the specified instance"""
- self._vmops.rescue(instance, callback)
+ self._vmops.rescue(context, instance, _callback, network_info)
- def unrescue(self, instance, callback, network_info):
+ def unrescue(self, instance, _callback, network_info):
"""Unrescue the specified instance"""
- self._vmops.unrescue(instance, callback)
+ self._vmops.unrescue(instance, _callback)
def poll_rescued_instances(self, timeout):
"""Poll for rescued instances"""
@@ -331,6 +332,19 @@ class XenAPIConnection(driver.ComputeDriver):
True, run the update first."""
return self.HostState.get_host_stats(refresh=refresh)
+ def host_power_action(self, host, action):
+ """The only valid values for 'action' on XenServer are 'reboot' or
+ 'shutdown', even though the API also accepts 'startup'. As this is
+ not technically possible on XenServer, since the host is the same
+ physical machine as the hypervisor, if this is requested, we need to
+ raise an exception.
+ """
+ if action in ("reboot", "shutdown"):
+ return self._vmops.host_power_action(host, action)
+ else:
+ msg = _("Host startup on XenServer is not supported.")
+ raise NotImplementedError(msg)
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
return self._vmops.set_host_enabled(host, enabled)
@@ -393,11 +407,10 @@ class XenAPISession(object):
try:
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
+ # Ensure action is never > 255
+ action = dict(action=name[:255], error=None)
if id:
- action = dict(
- instance_id=int(id),
- action=name[0:255], # Ensure action is never > 255
- error=None)
+ action["instance_id"] = int(id)
if status == "pending":
return
elif status == "success":
@@ -440,7 +453,7 @@ class XenAPISession(object):
params = None
try:
params = eval(exc.details[3])
- except:
+ except Exception:
raise exc
raise self.XenAPI.Failure(params)
else:
diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py
index c4603803b..2e3e38ca9 100644
--- a/nova/vnc/proxy.py
+++ b/nova/vnc/proxy.py
@@ -60,7 +60,7 @@ class WebsocketVNCProxy(object):
break
d = base64.b64encode(d)
dest.send(d)
- except:
+ except Exception:
source.close()
dest.close()
@@ -72,7 +72,7 @@ class WebsocketVNCProxy(object):
break
d = base64.b64decode(d)
dest.sendall(d)
- except:
+ except Exception:
source.close()
dest.close()
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
index fbe080b22..a06312890 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
@@ -67,12 +67,17 @@ def _copy_kernel_vdi(dest, copy_args):
def _download_tarball(sr_path, staging_path, image_id, glance_host,
- glance_port):
+ glance_port, auth_token):
"""Download the tarball image from Glance and extract it into the staging
area.
"""
+ # Build request headers
+ headers = {}
+ if auth_token:
+ headers['x-auth-token'] = auth_token
+
conn = httplib.HTTPConnection(glance_host, glance_port)
- conn.request('GET', '/v1/images/%s' % image_id)
+ conn.request('GET', '/v1/images/%s' % image_id, headers=headers)
resp = conn.getresponse()
if resp.status == httplib.NOT_FOUND:
raise Exception("Image '%s' not found in Glance" % image_id)
@@ -236,12 +241,29 @@ def _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids):
os.link(source, link_name)
-def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type):
+def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type,
+ auth_token):
"""
Create a tarball of the image and then stream that into Glance
using chunked-transfer-encoded HTTP.
"""
conn = httplib.HTTPConnection(glance_host, glance_port)
+
+ # NOTE(dprince): We need to resend any existing Glance meta/property
+ # headers so they are preserved in Glance. We obtain them here with a
+ # HEAD request.
+ conn.request('HEAD', '/v1/images/%s' % image_id)
+ resp = conn.getresponse()
+ if resp.status != httplib.OK:
+ raise Exception("Unexpected response from Glance %i" % resp.status)
+ headers = {}
+ for header, value in resp.getheaders():
+ if header.lower().startswith("x-image-meta-property-"):
+ headers[header.lower()] = value
+
+ # Toss body so connection state-machine is ready for next request/response
+ resp.read()
+
# NOTE(sirp): httplib under python2.4 won't accept a file-like object
# to request
conn.putrequest('PUT', '/v1/images/%s' % image_id)
@@ -254,7 +276,7 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type):
# 2. We're currently uploading a vanilla tarball. In order to be OVF/OVA
# compliant, we'll need to embed a minimal OVF manifest as the first
# file.
- headers = {
+ ovf_headers = {
'content-type': 'application/octet-stream',
'transfer-encoding': 'chunked',
'x-image-meta-is-public': 'True',
@@ -263,6 +285,12 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type):
'x-image-meta-container-format': 'ovf',
'x-image-meta-property-os-type': os_type}
+ # If we have an auth_token, set an x-auth-token header
+ if auth_token:
+ ovf_headers['x-auth-token'] = auth_token
+
+ headers.update(ovf_headers)
+
for header, value in headers.iteritems():
conn.putheader(header, value)
conn.endheaders()
@@ -364,11 +392,12 @@ def download_vhd(session, args):
glance_port = params["glance_port"]
uuid_stack = params["uuid_stack"]
sr_path = params["sr_path"]
+ auth_token = params["auth_token"]
staging_path = _make_staging_area(sr_path)
try:
_download_tarball(sr_path, staging_path, image_id, glance_host,
- glance_port)
+ glance_port, auth_token)
# Right now, it's easier to return a single string via XenAPI,
# so we'll json encode the list of VHDs.
return json.dumps(_import_vhds(sr_path, staging_path, uuid_stack))
@@ -386,12 +415,13 @@ def upload_vhd(session, args):
glance_port = params["glance_port"]
sr_path = params["sr_path"]
os_type = params["os_type"]
+ auth_token = params["auth_token"]
staging_path = _make_staging_area(sr_path)
try:
_prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids)
_upload_tarball(staging_path, image_id, glance_host, glance_port,
- os_type)
+ os_type, auth_token)
finally:
_cleanup_staging_area(staging_path)
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
index 292bbce12..cd9694ce1 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost
@@ -39,6 +39,7 @@ import pluginlib_nova as pluginlib
pluginlib.configure_logging("xenhost")
host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)")
+config_file_path = "/usr/etc/xenhost.conf"
def jsonify(fnc):
@@ -103,6 +104,104 @@ def set_host_enabled(self, arg_dict):
return {"status": status}
+def _write_config_dict(dct):
+ conf_file = file(config_file_path, "w")
+ json.dump(dct, conf_file)
+ conf_file.close()
+
+
+def _get_config_dict():
+ """Returns a dict containing the key/values in the config file.
+ If the file doesn't exist, it is created, and an empty dict
+ is returned.
+ """
+ try:
+ conf_file = file(config_file_path)
+ config_dct = json.load(conf_file)
+ conf_file.close()
+ except IOError:
+ # File doesn't exist
+ config_dct = {}
+ # Create the file
+ _write_config_dict(config_dct)
+ return config_dct
+
+
+@jsonify
+def get_config(self, arg_dict):
+ """Return the value stored for the specified key, or None if no match."""
+ conf = _get_config_dict()
+ params = arg_dict["params"]
+ try:
+ dct = json.loads(params)
+ except Exception, e:
+ dct = params
+ key = dct["key"]
+ ret = conf.get(key)
+ if ret is None:
+ # Can't jsonify None
+ return "None"
+ return ret
+
+
+@jsonify
+def set_config(self, arg_dict):
+ """Write the specified key/value pair, overwriting any existing value."""
+ conf = _get_config_dict()
+ params = arg_dict["params"]
+ try:
+ dct = json.loads(params)
+ except Exception, e:
+ dct = params
+ key = dct["key"]
+ val = dct["value"]
+ if val is None:
+ # Delete the key, if present
+ conf.pop(key, None)
+ else:
+ conf.update({key: val})
+ _write_config_dict(conf)
+
+
+def _power_action(action):
+ host_uuid = _get_host_uuid()
+ # Host must be disabled first
+ result = _run_command("xe host-disable")
+ if result:
+ raise pluginlib.PluginError(result)
+ # All running VMs must be shutdown
+ result = _run_command("xe vm-shutdown --multiple power-state=running")
+ if result:
+ raise pluginlib.PluginError(result)
+ cmds = {"reboot": "xe host-reboot", "startup": "xe host-power-on",
+ "shutdown": "xe host-shutdown"}
+ result = _run_command(cmds[action])
+ # Should be empty string
+ if result:
+ raise pluginlib.PluginError(result)
+ return {"power_action": action}
+
+
+@jsonify
+def host_reboot(self, arg_dict):
+ """Reboots the host."""
+ return _power_action("reboot")
+
+
+@jsonify
+def host_shutdown(self, arg_dict):
+ """Reboots the host."""
+ return _power_action("shutdown")
+
+
+@jsonify
+def host_start(self, arg_dict):
+ """Starts the host. Currently not feasible, since the host
+ runs on the same machine as Xen.
+ """
+ return _power_action("startup")
+
+
@jsonify
def host_data(self, arg_dict):
"""Runs the commands on the xenstore host to return the current status
@@ -115,6 +214,9 @@ def host_data(self, arg_dict):
# We have the raw dict of values. Extract those that we need,
# and convert the data types as needed.
ret_dict = cleanup(parsed_data)
+ # Add any config settings
+ config = _get_config_dict()
+ ret_dict.update(config)
return ret_dict
@@ -217,4 +319,9 @@ def cleanup(dct):
if __name__ == "__main__":
XenAPIPlugin.dispatch(
{"host_data": host_data,
- "set_host_enabled": set_host_enabled})
+ "set_host_enabled": set_host_enabled,
+ "host_shutdown": host_shutdown,
+ "host_reboot": host_reboot,
+ "host_start": host_start,
+ "get_config": get_config,
+ "set_config": set_config})
diff --git a/smoketests/test_netadmin.py b/smoketests/test_netadmin.py
index 60086f065..8c8fa35b8 100644
--- a/smoketests/test_netadmin.py
+++ b/smoketests/test_netadmin.py
@@ -109,13 +109,17 @@ class SecurityGroupTests(base.UserSmokeTestCase):
def __public_instance_is_accessible(self):
id_url = "latest/meta-data/instance-id"
- options = "-s --max-time 1"
+ options = "-f -s --max-time 1"
command = "curl %s %s/%s" % (options, self.data['public_ip'], id_url)
- instance_id = commands.getoutput(command).strip()
+ status, output = commands.getstatusoutput(command)
+ instance_id = output.strip()
+ if status > 0:
+ return False
if not instance_id:
return False
if instance_id != self.data['instance'].id:
- raise Exception("Wrong instance id")
+ raise Exception("Wrong instance id. Expected: %s, Got: %s" %
+ (self.data['instance'].id, instance_id))
return True
def test_001_can_create_security_group(self):
diff --git a/tools/pip-requires b/tools/pip-requires
index 23e707034..60b502ffd 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -9,7 +9,8 @@ boto==1.9b
carrot==0.10.5
eventlet
lockfile==0.8
-python-novaclient==2.5.9
+lxml==2.3
+python-novaclient==2.6.0
python-daemon==1.5.5
python-gflags==1.3
redis==2.0.0