summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNaveed Massjouni <naveedm9@gmail.com>2011-08-08 20:25:31 -0400
committerNaveed Massjouni <naveedm9@gmail.com>2011-08-08 20:25:31 -0400
commit72dc7939f4bfd05588b09046cbd25be09413c4eb (patch)
tree92d23ee75262e44ec5c149db5705442f6607cc9b
parent543a783cefc3b34fa4a5d4ae5b9034090666d182 (diff)
parentd63bfef32d33edca440038c978f61bd303db23aa (diff)
downloadnova-72dc7939f4bfd05588b09046cbd25be09413c4eb.tar.gz
nova-72dc7939f4bfd05588b09046cbd25be09413c4eb.tar.xz
nova-72dc7939f4bfd05588b09046cbd25be09413c4eb.zip
Merge from trunk.
-rwxr-xr-xbin/nova-manage43
-rw-r--r--nova/api/direct.py4
-rw-r--r--nova/api/ec2/__init__.py4
-rw-r--r--nova/api/ec2/apirequest.py2
-rw-r--r--nova/api/openstack/__init__.py3
-rw-r--r--nova/api/openstack/common.py41
-rw-r--r--nova/api/openstack/contrib/admin_only.py30
-rw-r--r--nova/api/openstack/contrib/hosts.py32
-rw-r--r--nova/api/openstack/create_instance_helper.py117
-rw-r--r--nova/api/openstack/images.py5
-rw-r--r--nova/api/openstack/servers.py46
-rw-r--r--nova/api/openstack/views/images.py4
-rw-r--r--nova/api/openstack/zones.py2
-rw-r--r--nova/auth/novarc.template1
-rw-r--r--nova/cloudpipe/pipelib.py17
-rw-r--r--nova/compute/api.py11
-rw-r--r--nova/compute/manager.py11
-rw-r--r--nova/db/sqlalchemy/api.py22
-rw-r--r--nova/exception.py13
-rw-r--r--nova/flags.py2
-rw-r--r--nova/image/__init__.py3
-rw-r--r--nova/image/fake.py41
-rw-r--r--nova/image/glance.py83
-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.py35
-rw-r--r--nova/tests/api/openstack/test_images.py29
-rw-r--r--nova/tests/api/openstack/test_server_actions.py274
-rw-r--r--nova/tests/api/openstack/test_servers.py12
-rw-r--r--nova/tests/scheduler/test_scheduler.py12
-rw-r--r--nova/tests/scheduler/test_zone_aware_scheduler.py27
-rw-r--r--nova/tests/test_hosts.py18
-rw-r--r--nova/tests/test_image.py134
-rw-r--r--nova/tests/test_nova_manage.py82
-rw-r--r--nova/tests/test_skip_examples.py47
-rw-r--r--nova/tests/test_xenapi.py46
-rw-r--r--nova/tests/test_zones.py1
-rw-r--r--nova/utils.py18
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py4
-rw-r--r--nova/virt/hyperv.py4
-rw-r--r--nova/virt/libvirt/connection.py22
-rw-r--r--nova/virt/libvirt/vif.py3
-rw-r--r--nova/virt/vmwareapi_conn.py4
-rw-r--r--nova/virt/xenapi/vmops.py18
-rw-r--r--nova/virt/xenapi_conn.py17
-rw-r--r--nova/vnc/proxy.py4
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/glance3
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost109
-rw-r--r--smoketests/test_netadmin.py3
-rw-r--r--tools/pip-requires2
52 files changed, 1322 insertions, 179 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index 807753a2e..40f22c19c 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."""
@@ -694,7 +720,17 @@ class NetworkCommands(object):
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:
@@ -1224,11 +1260,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..804e54ef9 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)
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/openstack/__init__.py b/nova/api/openstack/__init__.py
index d6a98c2cd..4d49df2ad 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -50,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):
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 829028169..ac2104a5f 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
@@ -137,8 +138,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 +154,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):
@@ -284,3 +281,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/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 2a8e7fd7e..894d47beb 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -304,6 +304,54 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
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]
@@ -312,6 +360,12 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
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)
@@ -325,6 +379,46 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
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:
@@ -332,8 +426,10 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
if value:
data[attribute] = value
metadata_node = self.find_first_child_named(node, 'metadata')
- metadata = self.metadata_deserializer.extract_metadata(metadata_node)
- data['metadata'] = metadata
+ if metadata_node is not None:
+ metadata = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+ data['metadata'] = metadata
return data
def create(self, string):
@@ -347,29 +443,32 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
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.metadata_deserializer.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/images.py b/nova/api/openstack/images.py
index c76738d30..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:
@@ -143,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])
@@ -156,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/servers.py b/nova/api/openstack/servers.py
index d17714371..f1a27a98c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -240,6 +240,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()
@@ -267,10 +268,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
@@ -290,7 +297,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()
@@ -306,7 +313,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()
@@ -321,7 +328,7 @@ 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()
@@ -336,7 +343,7 @@ 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()
@@ -351,7 +358,7 @@ 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()
@@ -363,7 +370,7 @@ class Controller(object):
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()
@@ -375,7 +382,7 @@ class Controller(object):
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()
@@ -387,7 +394,7 @@ class Controller(object):
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()
@@ -399,7 +406,7 @@ class Controller(object):
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()
@@ -420,7 +427,7 @@ class Controller(object):
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 +439,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()
@@ -646,6 +653,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 +690,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", {})
@@ -891,8 +902,13 @@ 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)
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/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/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 80d54d029..d2c08678b 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -360,6 +360,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
@@ -373,6 +374,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,
@@ -415,6 +417,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']
@@ -463,6 +466,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]
@@ -993,7 +997,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):
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 9f566dea7..cb6617c33 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -748,7 +748,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())
@@ -957,8 +958,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/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 4f1445217..003eb8bcb 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -1426,9 +1426,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()
###################
@@ -3178,8 +3183,9 @@ def instance_metadata_delete_all(context, instance_id):
@require_context
@require_instance_exists
-def instance_metadata_get_item(context, instance_id, key):
- session = get_session()
+def instance_metadata_get_item(context, instance_id, key, session=None):
+ if not session:
+ session = get_session()
meta_result = session.query(models.InstanceMetadata).\
filter_by(instance_id=instance_id).\
@@ -3205,7 +3211,7 @@ def instance_metadata_update_or_create(context, instance_id, metadata):
try:
meta_ref = instance_metadata_get_item(context, instance_id, key,
session)
- except:
+ except exception.InstanceMetadataNotFound, e:
meta_ref = models.InstanceMetadata()
meta_ref.update({"key": key, "value": value,
"instance_id": instance_id,
@@ -3300,8 +3306,10 @@ def instance_type_extra_specs_delete(context, instance_type_id, key):
@require_context
-def instance_type_extra_specs_get_item(context, instance_type_id, key):
- session = get_session()
+def instance_type_extra_specs_get_item(context, instance_type_id, key,
+ session=None):
+ if not session:
+ session = get_session()
spec_result = session.query(models.InstanceTypeExtraSpecs).\
filter_by(instance_type_id=instance_type_id).\
@@ -3327,7 +3335,7 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id,
instance_type_id,
key,
session)
- except:
+ except exception.InstanceTypeExtraSpecsNotFound, e:
spec_ref = models.InstanceTypeExtraSpecs()
spec_ref.update({"key": key, "value": value,
"instance_type_id": instance_type_id,
diff --git a/nova/exception.py b/nova/exception.py
index 68e6ac937..a87728fff 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")
diff --git a/nova/flags.py b/nova/flags.py
index 12c6d1356..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')
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 44a3c6f83..da93f0d1c 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -87,42 +87,71 @@ class GlanceImageService(service.BaseImageService):
"""Sets the client's auth token."""
self.client.set_auth_token(context.auth_token)
- def index(self, context, filters=None, marker=None, limit=None):
+ 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
- self._set_client_context(context)
- 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."""
- self._set_client_context(context)
- 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."""
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 d99d7214c..047dafa6f 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 5760d7a82..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
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 8e2e3f390..383ed2e03 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -379,6 +379,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"updated": self.NOW_API_FORMAT,
"created": self.NOW_API_FORMAT,
"status": "ACTIVE",
+ "progress": 100,
},
}
@@ -402,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": [{
@@ -444,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()))
@@ -463,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()))
@@ -587,6 +591,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'ACTIVE',
+ 'progress': 100,
},
{
'id': 124,
@@ -594,6 +599,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'QUEUED',
+ 'progress': 0,
},
{
'id': 125,
@@ -608,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,
@@ -616,6 +623,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
'updated': self.NOW_API_FORMAT,
'created': self.NOW_API_FORMAT,
'status': 'FAILED',
+ 'progress': 0,
},
{
'id': 129,
@@ -623,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)
@@ -643,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",
@@ -662,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": [{
@@ -723,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": [{
@@ -753,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": [{
@@ -780,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",
@@ -1001,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):
@@ -1049,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
index 562cefe90..bf18bc1b0 100644
--- a/nova/tests/api/openstack/test_server_actions.py
+++ b/nova/tests/api/openstack/test_server_actions.py
@@ -458,6 +458,7 @@ class ServerActionsTestV11(test.TestCase):
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()
@@ -475,6 +476,21 @@ class ServerActionsTestV11(test.TestCase):
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')
@@ -511,6 +527,42 @@ class ServerActionsTestV11(test.TestCase):
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": {
@@ -653,6 +705,62 @@ class ServerActionsTestV11(test.TestCase):
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': {
@@ -668,6 +776,23 @@ class ServerActionsTestV11(test.TestCase):
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': {
@@ -730,10 +855,10 @@ class ServerActionsTestV11(test.TestCase):
self.assertTrue(response.headers['Location'])
-class TestServerActionXMLDeserializer(test.TestCase):
+class TestServerActionXMLDeserializerV11(test.TestCase):
def setUp(self):
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
def tearDown(self):
pass
@@ -746,7 +871,6 @@ class TestServerActionXMLDeserializer(test.TestCase):
expected = {
"createImage": {
"name": "new-server-test",
- "metadata": {},
},
}
self.assertEquals(request['body'], expected)
@@ -767,3 +891,147 @@ class TestServerActionXMLDeserializer(test.TestCase):
},
}
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_servers.py b/nova/tests/api/openstack/test_servers.py
index 0477f6d92..fd06b2e64 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -2177,7 +2177,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
def setUp(self):
super(TestServerCreateRequestXMLDeserializerV11, self).setUp()
- self.deserializer = create_instance_helper.ServerXMLDeserializer()
+ self.deserializer = create_instance_helper.ServerXMLDeserializerV11()
def test_minimal_request(self):
serial_request = """
@@ -2191,8 +2191,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2211,8 +2209,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"imageRef": "1",
"flavorRef": "2",
"adminPass": "1234",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2229,8 +2225,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"name": "new-server-test",
"imageRef": "http://localhost:8774/v1.1/images/2",
"flavorRef": "3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2247,8 +2241,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "http://localhost:8774/v1.1/flavors/3",
- "metadata": {},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2292,7 +2284,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"imageRef": "1",
"flavorRef": "2",
"metadata": {"one": "two", "open": "snack"},
- "personality": [],
},
}
self.assertEquals(request['body'], expected)
@@ -2314,7 +2305,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase):
"name": "new-server-test",
"imageRef": "1",
"flavorRef": "2",
- "metadata": {},
"personality": [
{"path": "/etc/banner.txt", "contents": "MQ=="},
{"path": "/etc/hosts", "contents": "Mg=="},
diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py
index f60eb6433..330dab5e5 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
@@ -1036,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):
@@ -1085,7 +1087,7 @@ class FakeZonesProxy(object):
raise Exception('testing')
-class FakeNovaClientOpenStack(object):
+class FakeNovaClientZones(object):
def __init__(self, *args, **kwargs):
self.zones = FakeZonesProxy()
@@ -1098,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_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_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_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_xenapi.py b/nova/tests/test_xenapi.py
index 39ab23d9b..dfc1eeb0a 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -767,6 +767,52 @@ 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
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/utils.py b/nova/utils.py
index da8826f1f..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)
@@ -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 4f3cfefad..5d73eefc7 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -282,6 +282,10 @@ class ComputeDriver(object):
# 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
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 80abcc644..89ad20494 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -512,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 3428a7fc1..ae30c62f0 100644
--- a/nova/virt/hyperv.py
+++ b/nova/virt/hyperv.py
@@ -499,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/libvirt/connection.py b/nova/virt/libvirt/connection.py
index f7aae4112..7655bf386 100644
--- a/nova/virt/libvirt/connection.py
+++ b/nova/virt/libvirt/connection.py
@@ -349,7 +349,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:
@@ -392,9 +392,7 @@ class LibvirtConnection(driver.ComputeDriver):
nova.image.get_image_service(image_href)
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': {
@@ -409,6 +407,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 = """
@@ -880,7 +884,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}
@@ -1077,7 +1081,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1118,7 +1122,7 @@ class LibvirtConnection(driver.ComputeDriver):
try:
doc = libxml2.parseDoc(xml)
- except:
+ except Exception:
return []
ctx = doc.xpathNewContext()
@@ -1558,6 +1562,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_conn.py b/nova/virt/vmwareapi_conn.py
index 3d209fa99..aaa384374 100644
--- a/nova/virt/vmwareapi_conn.py
+++ b/nova/virt/vmwareapi_conn.py
@@ -191,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/vmops.py b/nova/virt/xenapi/vmops.py
index a78413370..b913e764e 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -1031,11 +1031,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):
@@ -1051,7 +1063,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
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 91d9d8530..a1c9a1e30 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -191,7 +191,7 @@ class XenAPIConnection(driver.ComputeDriver):
def revert_migration(self, instance):
"""Reverts a resize, powering back on the instance"""
- self._vmops.revert_resize(instance)
+ self._vmops.revert_migration(instance)
def finish_migration(self, context, instance, disk_info, network_info,
resize_instance=False):
@@ -332,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)
@@ -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 17a5dbbfb..a06312890 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
@@ -261,6 +261,9 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type,
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)
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..de69c98a2 100644
--- a/smoketests/test_netadmin.py
+++ b/smoketests/test_netadmin.py
@@ -115,7 +115,8 @@ class SecurityGroupTests(base.UserSmokeTestCase):
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 b98b70937..60b502ffd 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -10,7 +10,7 @@ carrot==0.10.5
eventlet
lockfile==0.8
lxml==2.3
-python-novaclient==2.5.9
+python-novaclient==2.6.0
python-daemon==1.5.5
python-gflags==1.3
redis==2.0.0