summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormasumotok <masumotok@nttdata.co.jp>2010-12-31 12:18:58 +0900
committermasumotok <masumotok@nttdata.co.jp>2010-12-31 12:18:58 +0900
commit5bcdb373a3f79bfc74d824c60c43f9af06091537 (patch)
treed7e343dc9416ad9b28303e3ff901202ce2ef413d
parentbf7bc8725fcc26cc5074ea1628bcba6ac6093768 (diff)
parent03a45dc892f65a0778e5ae0151a63223340279a4 (diff)
downloadnova-5bcdb373a3f79bfc74d824c60c43f9af06091537.tar.gz
nova-5bcdb373a3f79bfc74d824c60c43f9af06091537.tar.xz
nova-5bcdb373a3f79bfc74d824c60c43f9af06091537.zip
最新バージョンにマージ。変更点は以下の通り。
Authorsに自分の所属を追加 utils.pyのgenerate_uidがおかしいのでインスタンスIDがオーバーフローしていたが、 その処理を一時撤廃。後で試験しなおしとすることにした。
-rw-r--r--.mailmap3
-rw-r--r--Authors6
-rw-r--r--nova/api/openstack/__init__.py6
-rw-r--r--nova/api/openstack/auth.py3
-rw-r--r--nova/api/openstack/backup_schedules.py15
-rw-r--r--nova/api/openstack/images.py95
-rw-r--r--nova/api/openstack/ratelimiting/__init__.py4
-rw-r--r--nova/api/openstack/servers.py72
-rw-r--r--nova/api/openstack/sharedipgroups.py39
-rw-r--r--nova/compute/api.py52
-rw-r--r--nova/compute/manager.py63
-rw-r--r--nova/compute/power_state.py4
-rw-r--r--nova/db/api.py5
-rw-r--r--nova/db/sqlalchemy/api.py12
-rw-r--r--nova/db/sqlalchemy/models.py28
-rw-r--r--nova/flags.py3
-rw-r--r--nova/image/glance.py1
-rw-r--r--nova/tests/api/openstack/fakes.py9
-rw-r--r--nova/tests/api/openstack/test_images.py29
-rw-r--r--nova/tests/api/openstack/test_servers.py48
-rw-r--r--nova/tests/test_compute.py20
-rw-r--r--nova/tests/test_scheduler.py1
-rw-r--r--nova/tests/test_virt.py2
-rw-r--r--nova/tests/test_xenapi.py110
-rw-r--r--nova/tests/xenapi/stubs.py66
-rw-r--r--nova/utils.py16
-rw-r--r--nova/virt/fake.py29
-rw-r--r--nova/virt/libvirt_conn.py50
-rw-r--r--nova/virt/xenapi/fake.py65
-rw-r--r--nova/virt/xenapi/vm_utils.py228
-rw-r--r--nova/virt/xenapi/vmops.py125
-rw-r--r--nova/virt/xenapi/volume_utils.py6
-rw-r--r--nova/virt/xenapi_conn.py24
-rw-r--r--nova/wsgi.py2
-rw-r--r--plugins/xenserver/doc/networking.rst144
-rwxr-xr-xplugins/xenserver/networking/etc/init.d/host-rules106
-rw-r--r--plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch22
-rwxr-xr-xplugins/xenserver/networking/etc/xensource/scripts/vif_rules.py119
-rw-r--r--plugins/xenserver/xenapi/README (renamed from plugins/xenapi/README)0
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/glance132
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore (renamed from plugins/xenapi/etc/xapi.d/plugins/objectstore)41
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py (renamed from plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py)0
-rw-r--r--run_tests.py68
-rwxr-xr-xrun_tests.sh13
44 files changed, 1681 insertions, 205 deletions
diff --git a/.mailmap b/.mailmap
index 9ab7db743..010678569 100644
--- a/.mailmap
+++ b/.mailmap
@@ -27,3 +27,6 @@
<vishvananda@gmail.com> <root@ubuntu>
<sleepsonthefloor@gmail.com> <root@tonbuntu>
<rlane@wikimedia.org> <laner@controller>
+<rconradharris@gmail.com> <rick.harris@rackspace.com>
+<corywright@gmail.com> <cory.wright@rackspace.com>
+<ant@openstack.org> <amesserl@rackspace.com>
diff --git a/Authors b/Authors
index 851c81e54..79b1b6f68 100644
--- a/Authors
+++ b/Authors
@@ -1,9 +1,11 @@
Andy Smith <code@term.ie>
Anne Gentle <anne@openstack.org>
Anthony Young <sleepsonthefloor@gmail.com>
+Antony Messerli <ant@openstack.org>
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
Chris Behrens <cbehrens@codestud.com>
Chmouel Boudjnah <chmouel@chmouel.com>
+Cory Wright <corywright@gmail.com>
David Pravec <David.Pravec@danix.org>
Dean Troyer <dtroyer@gmail.com>
Devin Carlen <devin.carlen@gmail.com>
@@ -20,13 +22,16 @@ Jonathan Bryce <jbryce@jbryce.com>
Josh Kearney <josh.kearney@rackspace.com>
Joshua McKenty <jmckenty@gmail.com>
Justin Santa Barbara <justin@fathomdb.com>
+Kei Masumoto <masumotok@nttdata.co.jp>
Matt Dietz <matt.dietz@rackspace.com>
Michael Gundlach <michael.gundlach@rackspace.com>
Monty Taylor <mordred@inaugust.com>
Paul Voccio <paul@openstack.org>
Rick Clark <rick@openstack.org>
+Rick Harris <rconradharris@gmail.com>
Ryan Lane <rlane@wikimedia.org>
Ryan Lucio <rlucio@internap.com>
+Salvatore Orlando <salvatore.orlando@eu.citrix.com>
Sandy Walsh <sandy.walsh@rackspace.com>
Soren Hansen <soren.hansen@rackspace.com>
Thierry Carrez <thierry@openstack.org>
@@ -35,3 +40,4 @@ Trey Morris <trey.morris@rackspace.com>
Vishvananda Ishaya <vishvananda@gmail.com>
Youcef Laribi <Youcef.Laribi@eu.citrix.com>
Zhixue Wu <Zhixue.Wu@citrix.com>
+
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index c49399f28..ea6dff004 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -93,12 +93,16 @@ class APIRouter(wsgi.Router):
logging.debug("Including admin operations in API.")
server_members['pause'] = 'POST'
server_members['unpause'] = 'POST'
+ server_members["diagnostics"] = "GET"
+ server_members["actions"] = "GET"
+ server_members['suspend'] = 'POST'
+ server_members['resume'] = 'POST'
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
member=server_members)
- mapper.resource("backup_schedule", "backup_schedules",
+ mapper.resource("backup_schedule", "backup_schedule",
controller=backup_schedules.Controller(),
parent_resource=dict(member_name='server',
collection_name='servers'))
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index e24e58fd3..1dfdd5318 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -55,7 +55,8 @@ class AuthMiddleware(wsgi.Middleware):
if not user:
return faults.Fault(webob.exc.HTTPUnauthorized())
- req.environ['nova.context'] = context.RequestContext(user, user)
+ project = self.auth.get_project(FLAGS.default_project)
+ req.environ['nova.context'] = context.RequestContext(user, project)
return self.application
def has_authentication(self, req):
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index fc70b5c6c..fcc07bdd3 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -23,13 +23,25 @@ from nova.api.openstack import faults
import nova.image.service
+def _translate_keys(inst):
+ """ Coerces the backup schedule into proper dictionary format """
+ return dict(backupSchedule=inst)
+
+
class Controller(wsgi.Controller):
+ """ The backup schedule API controller for the Openstack API """
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'backupSchedule': []}}}
def __init__(self):
pass
def index(self, req, server_id):
- return faults.Fault(exc.HTTPNotFound())
+ """ Returns the list of backup schedules for a given instance """
+ return _translate_keys({})
def create(self, req, server_id):
""" No actual update method required, since the existing API allows
@@ -37,4 +49,5 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPNotFound())
def delete(self, req, server_id, id):
+ """ Deletes an existing backup schedule """
return faults.Fault(exc.HTTPNotFound())
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index d3312aba8..867ee5a7e 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -25,11 +25,70 @@ import nova.image.service
from nova.api.openstack import common
from nova.api.openstack import faults
-
+from nova.compute import api as compute_api
FLAGS = flags.FLAGS
+def _translate_keys(item):
+ """
+ Maps key names to Rackspace-like attributes for return
+ also pares down attributes to those we want
+ item is a dict
+
+ Note: should be removed when the set of keys expected by the api
+ and the set of keys returned by the image service are equivalent
+
+ """
+ # TODO(tr3buchet): this map is specific to s3 object store,
+ # replace with a list of keys for _filter_keys later
+ mapped_keys = {'status': 'imageState',
+ 'id': 'imageId',
+ 'name': 'imageLocation'}
+
+ mapped_item = {}
+ # TODO(tr3buchet):
+ # this chunk of code works with s3 and the local image service/glance
+ # when we switch to glance/local image service it can be replaced with
+ # a call to _filter_keys, and mapped_keys can be changed to a list
+ try:
+ for k, v in mapped_keys.iteritems():
+ # map s3 fields
+ mapped_item[k] = item[v]
+ except KeyError:
+ # return only the fields api expects
+ mapped_item = _filter_keys(item, mapped_keys.keys())
+
+ return mapped_item
+
+
+def _translate_status(item):
+ """
+ Translates status of image to match current Rackspace api bindings
+ item is a dict
+
+ Note: should be removed when the set of statuses expected by the api
+ and the set of statuses returned by the image service are equivalent
+
+ """
+ status_mapping = {
+ 'pending': 'queued',
+ 'decrypting': 'preparing',
+ 'untarring': 'saving',
+ 'available': 'active'}
+ item['status'] = status_mapping[item['status']]
+ return item
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
class Controller(wsgi.Controller):
_serialization_metadata = {
@@ -42,25 +101,25 @@ class Controller(wsgi.Controller):
self._service = utils.import_object(FLAGS.image_service)
def index(self, req):
- """Return all public images in brief."""
- return dict(images=[dict(id=img['id'], name=img['name'])
- for img in self.detail(req)['images']])
+ """Return all public images in brief"""
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_filter_keys(item, ('id', 'name')) for item in items]
+ return dict(images=items)
def detail(self, req):
- """Return all public images in detail."""
+ """Return all public images in detail"""
try:
- images = self._service.detail(req.environ['nova.context'])
- images = common.limited(images, req)
+ items = self._service.detail(req.environ['nova.context'])
except NotImplementedError:
- # Emulate detail() using repeated calls to show()
- ctxt = req.environ['nova.context']
- images = self._service.index(ctxt)
- images = common.limited(images, req)
- images = [self._service.show(ctxt, i['id']) for i in images]
- return dict(images=images)
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_translate_keys(item) for item in items]
+ items = [_translate_status(item) for item in items]
+ return dict(images=items)
def show(self, req, id):
- """Return data about the given image id."""
+ """Return data about the given image id"""
return dict(image=self._service.show(req.environ['nova.context'], id))
def delete(self, req, id):
@@ -68,9 +127,11 @@ class Controller(wsgi.Controller):
raise faults.Fault(exc.HTTPNotFound())
def create(self, req):
- # Only public images are supported for now, so a request to
- # make a backup of a server cannot be supproted.
- raise faults.Fault(exc.HTTPNotFound())
+ context = req.environ['nova.context']
+ env = self._deserialize(req.body, req)
+ instance_id = env["image"]["serverId"]
+ name = env["image"]["name"]
+ return compute_api.ComputeAPI().snapshot(context, instance_id, name)
def update(self, req, id):
# Users may not modify public images, and that's all that
diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py
index 91a8b2e55..cbb4b897e 100644
--- a/nova/api/openstack/ratelimiting/__init__.py
+++ b/nova/api/openstack/ratelimiting/__init__.py
@@ -64,9 +64,9 @@ class RateLimitingMiddleware(wsgi.Middleware):
If the request should be rate limited, return a 413 status with a
Retry-After header giving the time when the request would succeed.
"""
- return self.limited_request(req, self.application)
+ return self.rate_limited_request(req, self.application)
- def limited_request(self, req, application):
+ def rate_limited_request(self, req, application):
"""Rate limit the request.
If the request should be rate limited, return a 413 status with a
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 8d60e2cab..c5cbe21ef 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -35,18 +35,16 @@ LOG = logging.getLogger('server')
LOG.setLevel(logging.DEBUG)
-def _entity_list(entities):
- """ Coerces a list of servers into proper dictionary format """
- return dict(servers=entities)
-
-
-def _entity_detail(inst):
- """ Maps everything to Rackspace-like attributes for return"""
+def _translate_detail_keys(inst):
+ """ Coerces into dictionary format, mapping everything to Rackspace-like
+ attributes for return"""
power_mapping = {
+ None: 'build',
power_state.NOSTATE: 'build',
power_state.RUNNING: 'active',
power_state.BLOCKED: 'active',
- power_state.PAUSED: 'suspended',
+ power_state.SUSPENDED: 'suspended',
+ power_state.PAUSED: 'error',
power_state.SHUTDOWN: 'active',
power_state.SHUTOFF: 'active',
power_state.CRASHED: 'error'}
@@ -66,8 +64,9 @@ def _entity_detail(inst):
return dict(server=inst_dict)
-def _entity_inst(inst):
- """ Filters all model attributes save for id and name """
+def _translate_keys(inst):
+ """ Coerces into dictionary format, excluding all model attributes
+ save for id and name """
return dict(server=dict(id=inst['internal_id'], name=inst['display_name']))
@@ -86,29 +85,29 @@ class Controller(wsgi.Controller):
def index(self, req):
""" Returns a list of server names and ids for a given user """
- return self._items(req, entity_maker=_entity_inst)
+ return self._items(req, entity_maker=_translate_keys)
def detail(self, req):
""" Returns a list of server details for a given user """
- return self._items(req, entity_maker=_entity_detail)
+ return self._items(req, entity_maker=_translate_detail_keys)
def _items(self, req, entity_maker):
"""Returns a list of servers for a given user.
- entity_maker - either _entity_detail or _entity_inst
+ entity_maker - either _translate_detail_keys or _translate_keys
"""
instance_list = self.compute_api.get_instances(
req.environ['nova.context'])
limited_list = common.limited(instance_list, req)
res = [entity_maker(inst)['server'] for inst in limited_list]
- return _entity_list(res)
+ return dict(servers=res)
def show(self, req, id):
""" Returns server details by server id """
try:
instance = self.compute_api.get_instance(
req.environ['nova.context'], int(id))
- return _entity_detail(instance)
+ return _translate_detail_keys(instance)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -137,7 +136,7 @@ class Controller(wsgi.Controller):
description=env['server']['name'],
key_name=key_pair['name'],
key_data=key_pair['public_key'])
- return _entity_inst(instances[0])
+ return _translate_keys(instances[0])
def update(self, req, id):
""" Updates the server name or password """
@@ -152,8 +151,9 @@ class Controller(wsgi.Controller):
update_dict['display_name'] = inst_dict['server']['name']
try:
- self.compute_api.update_instance(req.environ['nova.context'],
- instance['id'],
+ ctxt = req.environ['nova.context']
+ self.compute_api.update_instance(ctxt,
+ id,
**update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -182,7 +182,7 @@ class Controller(wsgi.Controller):
self.compute_api.pause(ctxt, id)
except:
readable = traceback.format_exc()
- logging.error("Compute.api::pause %s", readable)
+ logging.error(_("Compute.api::pause %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@@ -193,6 +193,38 @@ class Controller(wsgi.Controller):
self.compute_api.unpause(ctxt, id)
except:
readable = traceback.format_exc()
- logging.error("Compute.api::unpause %s", readable)
+ logging.error(_("Compute.api::unpause %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def suspend(self, req, id):
+ """permit admins to suspend the server"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.suspend(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::suspend %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+
+ def resume(self, req, id):
+ """permit admins to resume the server from suspend"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.resume(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::resume %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def diagnostics(self, req, id):
+ """Permit Admins to retrieve server diagnostics."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_diagnostics(ctxt, id)
+
+ def actions(self, req, id):
+ """Permit Admins to retrieve server actions."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_actions(ctxt, id)
diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py
index 75d02905c..845f5bead 100644
--- a/nova/api/openstack/sharedipgroups.py
+++ b/nova/api/openstack/sharedipgroups.py
@@ -15,26 +15,51 @@
# License for the specific language governing permissions and limitations
# under the License.
+from webob import exc
+
from nova import wsgi
+from nova.api.openstack import faults
+
+
+def _translate_keys(inst):
+ """ Coerces a shared IP group instance into proper dictionary format """
+ return dict(sharedIpGroup=inst)
+
+
+def _translate_detail_keys(inst):
+ """ Coerces a shared IP group instance into proper dictionary format with
+ correctly mapped attributes """
+ return dict(sharedIpGroup=inst)
class Controller(wsgi.Controller):
""" The Shared IP Groups Controller for the Openstack API """
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'sharedIpGroup': []}}}
+
def index(self, req):
- raise NotImplementedError
+ """ Returns a list of Shared IP Groups for the user """
+ return dict(sharedIpGroups=[])
def show(self, req, id):
- raise NotImplementedError
+ """ Shows in-depth information on a specific Shared IP Group """
+ return _translate_keys({})
def update(self, req, id):
- raise NotImplementedError
+ """ You can't update a Shared IP Group """
+ raise faults.Fault(exc.HTTPNotImplemented())
def delete(self, req, id):
- raise NotImplementedError
+ """ Deletes a Shared IP Group """
+ raise faults.Fault(exc.HTTPNotFound())
- def detail(self, req):
- raise NotImplementedError
+ def detail(self, req, id):
+ """ Returns a complete list of Shared IP Groups """
+ return _translate_detail_keys({})
def create(self, req):
- raise NotImplementedError
+ """ Creates a new Shared IP group """
+ raise faults.Fault(exc.HTTPNotFound())
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 038f52046..5a776afa5 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -91,15 +91,16 @@ class ComputeAPI(base.Base):
is_vpn = image_id == FLAGS.vpn_image_id
if not is_vpn:
image = self.image_service.show(context, image_id)
-
- # If kernel_id/ramdisk_id isn't explicitly set in API call
- # we take the defaults from the image's metadata
if kernel_id is None:
kernel_id = image.get('kernelId', None)
if ramdisk_id is None:
ramdisk_id = image.get('ramdiskId', None)
-
- # Make sure we have access to kernel and ramdisk
+ #No kernel and ramdisk for raw images
+ if kernel_id == str(FLAGS.null_kernel):
+ kernel_id = None
+ ramdisk_id = None
+ logging.debug("Creating a raw instance")
+ # Make sure we have access to kernel and ramdisk (if not raw)
if kernel_id:
self.image_service.show(context, kernel_id)
if ramdisk_id:
@@ -257,6 +258,15 @@ class ComputeAPI(base.Base):
def get_instance(self, context, instance_id):
return self.db.instance_get_by_internal_id(context, instance_id)
+ def snapshot(self, context, instance_id, name):
+ """Snapshot the given instance."""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "snapshot_instance",
+ "args": {"instance_id": instance['id'], "name": name}})
+
def reboot(self, context, instance_id):
"""Reboot the given instance."""
instance = self.db.instance_get_by_internal_id(context, instance_id)
@@ -284,6 +294,38 @@ class ComputeAPI(base.Base):
{"method": "unpause_instance",
"args": {"instance_id": instance['id']}})
+ def get_diagnostics(self, context, instance_id):
+ """Retrieve diagnostics for the given instance."""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance["host"]
+ return rpc.call(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "get_diagnostics",
+ "args": {"instance_id": instance["id"]}})
+
+ def get_actions(self, context, instance_id):
+ """Retrieve actions for the given instance."""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ return self.db.instance_get_actions(context, instance["id"])
+
+ def suspend(self, context, instance_id):
+ """suspend the instance with instance_id"""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "suspend_instance",
+ "args": {"instance_id": instance['id']}})
+
+ def resume(self, context, instance_id):
+ """resume the instance with instance_id"""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "resume_instance",
+ "args": {"instance_id": instance['id']}})
+
def rescue(self, context, instance_id):
"""Rescue the given instance."""
instance = self.db.instance_get_by_internal_id(context, instance_id)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index dc660d2a6..1e8e11d04 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -233,6 +233,27 @@ class ComputeManager(manager.Manager):
self._update_state(context, instance_id)
@exception.wrap_exception
+ def snapshot_instance(self, context, instance_id, name):
+ """Snapshot an instance on this server."""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ #NOTE(sirp): update_state currently only refreshes the state field
+ # if we add is_snapshotting, we will need this refreshed too,
+ # potentially?
+ self._update_state(context, instance_id)
+
+ logging.debug(_('instance %s: snapshotting'), instance_ref['name'])
+ if instance_ref['state'] != power_state.RUNNING:
+ logging.warn(_('trying to snapshot a non-running '
+ 'instance: %s (state: %s excepted: %s)'),
+ instance_ref['internal_id'],
+ instance_ref['state'],
+ power_state.RUNNING)
+
+ self.driver.snapshot(instance_ref, name)
+
+ @exception.wrap_exception
def rescue_instance(self, context, instance_id):
"""Rescue an instance on this server."""
context = context.elevated()
@@ -305,6 +326,48 @@ class ComputeManager(manager.Manager):
result))
@exception.wrap_exception
+ def get_diagnostics(self, context, instance_id):
+ """Retrieve diagnostics for an instance on this server."""
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ if instance_ref["state"] == power_state.RUNNING:
+ logging.debug(_("instance %s: retrieving diagnostics"),
+ instance_ref["internal_id"])
+ return self.driver.get_diagnostics(instance_ref)
+
+ def suspend_instance(self, context, instance_id):
+ """suspend the instance with instance_id"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug(_('instance %s: suspending'),
+ instance_ref['internal_id'])
+ self.db.instance_set_state(context, instance_id,
+ power_state.NOSTATE,
+ 'suspending')
+ self.driver.suspend(instance_ref,
+ lambda result: self._update_state_callback(self,
+ context,
+ instance_id,
+ result))
+
+ @exception.wrap_exception
+ def resume_instance(self, context, instance_id):
+ """resume the suspended instance with instance_id"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug(_('instance %s: resuming'), instance_ref['internal_id'])
+ self.db.instance_set_state(context, instance_id,
+ power_state.NOSTATE,
+ 'resuming')
+ self.driver.resume(instance_ref,
+ lambda result: self._update_state_callback(self,
+ context,
+ instance_id,
+ result))
+
+ @exception.wrap_exception
def get_console_output(self, context, instance_id):
"""Send the console output for an instance."""
context = context.elevated()
diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py
index cefdf2d9e..37039d2ec 100644
--- a/nova/compute/power_state.py
+++ b/nova/compute/power_state.py
@@ -26,6 +26,7 @@ PAUSED = 0x03
SHUTDOWN = 0x04
SHUTOFF = 0x05
CRASHED = 0x06
+SUSPENDED = 0x07
def name(code):
@@ -36,5 +37,6 @@ def name(code):
PAUSED: 'paused',
SHUTDOWN: 'shutdown',
SHUTOFF: 'shutdown',
- CRASHED: 'crashed'}
+ CRASHED: 'crashed',
+ SUSPENDED: 'suspended'}
return d[code]
diff --git a/nova/db/api.py b/nova/db/api.py
index e0f48d07e..cd35a2bd4 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -413,6 +413,11 @@ def instance_action_create(context, values):
return IMPL.instance_action_create(context, values)
+def instance_get_actions(context, instance_id):
+ """Get instance actions by instance id."""
+ return IMPL.instance_get_actions(context, instance_id)
+
+
###################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 64af223c4..b0d1ec1a7 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -912,6 +912,18 @@ def instance_action_create(context, values):
return action_ref
+@require_admin_context
+def instance_get_actions(context, instance_id):
+ """Return the actions associated to the given instance id"""
+ session = get_session()
+ actions = {}
+ for action in session.query(models.InstanceActions).\
+ filter_by(instance_id=instance_id).\
+ all():
+ actions[action.action] = action.error
+ return actions
+
+
###################
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index a173f25db..994165ad1 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -22,7 +22,7 @@ SQLAlchemy models for nova data.
import datetime
from sqlalchemy.orm import relationship, backref, object_mapper
-from sqlalchemy import Column, Integer, Float, String, schema
+from sqlalchemy import Column, Integer, String, schema
from sqlalchemy import ForeignKey, DateTime, Boolean, Text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
@@ -260,21 +260,6 @@ class Instance(BASE, NovaBase):
# 'shutdown', 'shutoff', 'crashed'])
-class InstanceDiagnostics(BASE, NovaBase):
- """Represents a guest VM's diagnostics"""
- __tablename__ = "instance_diagnostics"
- id = Column(Integer, primary_key=True)
- instance_id = Column(Integer, ForeignKey('instances.id'))
-
- memory_available = Column(Float)
- memory_free = Column(Float)
- cpu_load = Column(Float)
- disk_read = Column(Float)
- disk_write = Column(Float)
- net_tx = Column(Float)
- net_rx = Column(Float)
-
-
class InstanceActions(BASE, NovaBase):
"""Represents a guest VM's actions and results"""
__tablename__ = "instance_actions"
@@ -476,7 +461,7 @@ class AuthToken(BASE, NovaBase):
"""
__tablename__ = 'auth_tokens'
token_hash = Column(String(255), primary_key=True)
- user_id = Column(Integer)
+ user_id = Column(String(255))
server_manageent_url = Column(String(255))
storage_url = Column(String(255))
cdn_management_url = Column(String(255))
@@ -585,10 +570,11 @@ def register_models():
it will never need to be called explicitly elsewhere.
"""
from sqlalchemy import create_engine
- models = (Service, Instance, Volume, ExportDevice, IscsiTarget, FixedIp,
- FloatingIp, Network, SecurityGroup,
- SecurityGroupIngressRule, SecurityGroupInstanceAssociation,
- AuthToken, User, Project, Host) # , Image
+ models = (Service, Instance, InstanceActions,
+ Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
+ Network, SecurityGroup, SecurityGroupIngressRule,
+ SecurityGroupInstanceAssociation, AuthToken, User,
+ Project, Certificate, Host) # , Image
engine = create_engine(FLAGS.sql_connection, echo=False)
for model in models:
model.metadata.create_all(engine)
diff --git a/nova/flags.py b/nova/flags.py
index 76a98d35a..d773a7e4c 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -212,6 +212,8 @@ DEFINE_list('region_list',
DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake')
DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID')
DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key')
+DEFINE_integer('glance_port', 9292, 'glance port')
+DEFINE_string('glance_host', utils.get_my_ip(), 'glance host')
DEFINE_integer('s3_port', 3333, 's3 port')
DEFINE_string('s3_host', utils.get_my_ip(), 's3 host (for infrastructure)')
DEFINE_string('s3_dmz', utils.get_my_ip(), 's3 dmz ip (for instances)')
@@ -239,6 +241,7 @@ DEFINE_string('cc_dmz', utils.get_my_ip(), 'internal ip of api server')
DEFINE_integer('cc_port', 8773, 'cloud controller port')
DEFINE_string('ec2_suffix', '/services/Cloud', 'suffix for ec2')
+DEFINE_string('default_project', 'openstack', 'default project for openstack')
DEFINE_string('default_image', 'ami-11111',
'default image to use, testing only')
DEFINE_string('default_instance_type', 'm1.small',
diff --git a/nova/image/glance.py b/nova/image/glance.py
index cb3936df1..cc3192e7c 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -24,6 +24,7 @@ import urlparse
import webob.exc
+from nova.compute import api as compute_api
from nova import utils
from nova import flags
from nova import exception
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 79663e43a..961431154 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -110,6 +110,12 @@ def stub_out_networking(stubs):
stubs.Set(nova.utils, 'get_my_ip', get_my_ip)
+def stub_out_compute_api_snapshot(stubs):
+ def snapshot(self, context, instance_id, name):
+ return 123
+ stubs.Set(nova.compute.api.ComputeAPI, 'snapshot', snapshot)
+
+
def stub_out_glance(stubs, initial_fixtures=[]):
class FakeParallaxClient:
@@ -213,6 +219,9 @@ class FakeAuthManager(object):
return v
return None
+ def get_project(self, pid):
+ return None
+
def get_user_from_access_key(self, key):
return FakeAuthManager.auth_data.get(key, None)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index f610cbf9c..0f274bd15 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -50,7 +50,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
num_images = len(self.service.index(self.context))
@@ -67,7 +67,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
num_images = len(self.service.index(self.context))
@@ -87,7 +87,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
id = self.service.create(self.context, fixture)
@@ -105,13 +105,13 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None},
{'name': 'test image 2',
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}]
num_images = len(self.service.index(self.context))
@@ -155,6 +155,7 @@ class GlanceImageServiceTest(unittest.TestCase,
def setUp(self):
self.stubs = stubout.StubOutForTesting()
fakes.stub_out_glance(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
service_class = 'nova.image.glance.GlanceImageService'
self.service = utils.import_object(service_class)
self.context = context.RequestContext(None, None)
@@ -223,6 +224,20 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase):
res = req.get_response(nova.api.API('os'))
res_dict = json.loads(res.body)
+ def _is_equivalent_subset(x, y):
+ if set(x) <= set(y):
+ for k, v in x.iteritems():
+ if x[k] != y[k]:
+ if x[k] == 'active' and y[k] == 'available':
+ continue
+ return False
+ return True
+ return False
+
for image in res_dict['images']:
- self.assertEquals(1, self.IMAGE_FIXTURES.count(image),
- "image %s not in fixtures!" % str(image))
+ for image_fixture in self.IMAGE_FIXTURES:
+ if _is_equivalent_subset(image, image_fixture):
+ break
+ else:
+ self.assertEquals(1, 2, "image %s not in fixtures!" %
+ str(image))
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 3820f5f27..70ff714e6 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -88,9 +88,17 @@ class ServersTest(unittest.TestCase):
self.stubs.Set(nova.db.api, 'instance_get_floating_address',
instance_address)
self.stubs.Set(nova.compute.api.ComputeAPI, 'pause',
- fake_compute_api)
+ fake_compute_api)
self.stubs.Set(nova.compute.api.ComputeAPI, 'unpause',
- fake_compute_api)
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'suspend',
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'resume',
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, "get_diagnostics",
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, "get_actions",
+ fake_compute_api)
self.allow_admin = FLAGS.allow_admin_api
def tearDown(self):
@@ -246,6 +254,42 @@ class ServersTest(unittest.TestCase):
res = req.get_response(nova.api.API('os'))
self.assertEqual(res.status_int, 202)
+ def test_server_suspend(self):
+ FLAGS.allow_admin_api = True
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/suspend')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(nova.api.API('os'))
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_resume(self):
+ FLAGS.allow_admin_api = True
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/resume')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(nova.api.API('os'))
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_diagnostics(self):
+ req = webob.Request.blank("/v1.0/servers/1/diagnostics")
+ req.method = "GET"
+ res = req.get_response(nova.api.API("os"))
+ self.assertEqual(res.status_int, 404)
+
+ def test_server_actions(self):
+ req = webob.Request.blank("/v1.0/servers/1/actions")
+ req.method = "GET"
+ res = req.get_response(nova.api.API("os"))
+ self.assertEqual(res.status_int, 404)
+
def test_server_reboot(self):
body = dict(server=dict(
name='server_test', imageId=2, flavorId=2, metadata={},
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 348bb3351..1fb9143f1 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -101,13 +101,13 @@ class ComputeTestCase(test.TestCase):
self.compute.run_instance(self.context, instance_id)
instances = db.instance_get_all(context.get_admin_context())
- logging.info("Running instances: %s", instances)
+ logging.info(_("Running instances: %s"), instances)
self.assertEqual(len(instances), 1)
self.compute.terminate_instance(self.context, instance_id)
instances = db.instance_get_all(context.get_admin_context())
- logging.info("After terminating instances: %s", instances)
+ logging.info(_("After terminating instances: %s"), instances)
self.assertEqual(len(instances), 0)
def test_run_terminate_timestamps(self):
@@ -136,6 +136,14 @@ class ComputeTestCase(test.TestCase):
self.compute.unpause_instance(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_suspend(self):
+ """ensure instance can be suspended"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.suspend_instance(self.context, instance_id)
+ self.compute.resume_instance(self.context, instance_id)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_reboot(self):
"""Ensure instance can be rebooted"""
instance_id = self._create_instance()
@@ -143,6 +151,14 @@ class ComputeTestCase(test.TestCase):
self.compute.reboot_instance(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_snapshot(self):
+ """Ensure instance can be snapshotted"""
+ instance_id = self._create_instance()
+ name = "myfakesnapshot"
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.snapshot_instance(self.context, instance_id, name)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_console_output(self):
"""Make sure we can get console output from instance"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_scheduler.py b/nova/tests/test_scheduler.py
index 91517cc5d..78e4a1c77 100644
--- a/nova/tests/test_scheduler.py
+++ b/nova/tests/test_scheduler.py
@@ -33,6 +33,7 @@ from nova.scheduler import driver
FLAGS = flags.FLAGS
flags.DECLARE('max_cores', 'nova.scheduler.simple')
+flags.DECLARE('stub_network', 'nova.compute.manager')
class TestDriver(driver.Scheduler):
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
index 8dab8de2f..4aa489d08 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_virt.py
@@ -33,6 +33,7 @@ flags.DECLARE('instances_path', 'nova.compute.manager')
class LibvirtConnTestCase(test.TestCase):
def setUp(self):
super(LibvirtConnTestCase, self).setUp()
+ libvirt_conn._late_load_cheetah()
self.flags(fake_call=True)
self.manager = manager.AuthManager()
self.user = self.manager.create_user('fake', 'fake', 'fake',
@@ -157,7 +158,6 @@ class LibvirtConnTestCase(test.TestCase):
(lambda t: t.find('./devices/serial/source').get(
'path').split('/')[1], 'console.log'),
(lambda t: t.find('./memory').text, '2097152')]
-
if rescue:
common_checks += [
(lambda t: t.findall('./devices/disk/source')[0].get(
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index ed2e4ffde..c95a53af3 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -29,9 +29,9 @@ from nova.auth import manager
from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import xenapi_conn
-from nova.virt.xenapi import fake
+from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import volume_utils
-from nova.tests.db import fakes
+from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
FLAGS = flags.FLAGS
@@ -47,9 +47,9 @@ class XenAPIVolumeTestCase(test.TestCase):
FLAGS.target_host = '127.0.0.1'
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
- fakes.stub_out_db_instance_api(self.stubs)
+ db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
- fake.reset()
+ xenapi_fake.reset()
self.values = {'name': 1, 'id': 1,
'project_id': 'fake',
'user_id': 'fake',
@@ -83,7 +83,7 @@ class XenAPIVolumeTestCase(test.TestCase):
label = 'SR-%s' % vol['ec2_id']
description = 'Test-SR'
sr_ref = helper.create_iscsi_storage(session, info, label, description)
- srs = fake.get_all('SR')
+ srs = xenapi_fake.get_all('SR')
self.assertEqual(sr_ref, srs[0])
db.volume_destroy(context.get_admin_context(), vol['id'])
@@ -107,17 +107,17 @@ class XenAPIVolumeTestCase(test.TestCase):
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
- fake.create_vm(instance.name, 'Running')
+ xenapi_fake.create_vm(instance.name, 'Running')
result = conn.attach_volume(instance.name, volume['ec2_id'],
'/dev/sdc')
def check():
# check that the VM has a VBD attached to it
# Get XenAPI reference for the VM
- vms = fake.get_all('VM')
+ vms = xenapi_fake.get_all('VM')
# Get XenAPI record for VBD
- vbds = fake.get_all('VBD')
- vbd = fake.get_record('VBD', vbds[0])
+ vbds = xenapi_fake.get_all('VBD')
+ vbd = xenapi_fake.get_record('VBD', vbds[0])
vm_ref = vbd['VM']
self.assertEqual(vm_ref, vms[0])
@@ -130,7 +130,7 @@ class XenAPIVolumeTestCase(test.TestCase):
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
- fake.create_vm(instance.name, 'Running')
+ xenapi_fake.create_vm(instance.name, 'Running')
self.assertRaises(Exception,
conn.attach_volume,
instance.name,
@@ -156,41 +156,70 @@ class XenAPIVMTestCase(test.TestCase):
self.stubs = stubout.StubOutForTesting()
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
- fake.reset()
- fakes.stub_out_db_instance_api(self.stubs)
- fake.create_network('fake', FLAGS.flat_network_bridge)
+ xenapi_fake.reset()
+ db_fakes.stub_out_db_instance_api(self.stubs)
+ xenapi_fake.create_network('fake', FLAGS.flat_network_bridge)
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
+ self.conn = xenapi_conn.get_connection(False)
def test_list_instances_0(self):
- stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
- conn = xenapi_conn.get_connection(False)
- instances = conn.list_instances()
+ instances = self.conn.list_instances()
self.assertEquals(instances, [])
+ def test_get_diagnostics(self):
+ instance = self._create_instance()
+ self.conn.get_diagnostics(instance)
+
+ def test_instance_snapshot(self):
+ stubs.stubout_instance_snapshot(self.stubs)
+ instance = self._create_instance()
+
+ name = "MySnapshot"
+ template_vm_ref = self.conn.snapshot(instance, name)
+
+ def ensure_vm_was_torn_down():
+ vm_labels = []
+ for vm_ref in xenapi_fake.get_all('VM'):
+ vm_rec = xenapi_fake.get_record('VM', vm_ref)
+ if not vm_rec["is_control_domain"]:
+ vm_labels.append(vm_rec["name_label"])
+
+ self.assertEquals(vm_labels, [1])
+
+ def ensure_vbd_was_torn_down():
+ vbd_labels = []
+ for vbd_ref in xenapi_fake.get_all('VBD'):
+ vbd_rec = xenapi_fake.get_record('VBD', vbd_ref)
+ vbd_labels.append(vbd_rec["vm_name_label"])
+
+ self.assertEquals(vbd_labels, [1])
+
+ def ensure_vdi_was_torn_down():
+ for vdi_ref in xenapi_fake.get_all('VDI'):
+ vdi_rec = xenapi_fake.get_record('VDI', vdi_ref)
+ name_label = vdi_rec["name_label"]
+ self.assert_(not name_label.endswith('snapshot'))
+
+ def check():
+ ensure_vm_was_torn_down()
+ ensure_vbd_was_torn_down()
+ ensure_vdi_was_torn_down()
+
+ check()
+
def test_spawn(self):
- stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
- values = {'name': 1, 'id': 1,
- 'project_id': self.project.id,
- 'user_id': self.user.id,
- 'image_id': 1,
- 'kernel_id': 2,
- 'ramdisk_id': 3,
- 'instance_type': 'm1.large',
- 'mac_address': 'aa:bb:cc:dd:ee:ff',
- }
- conn = xenapi_conn.get_connection(False)
- instance = db.instance_create(values)
- conn.spawn(instance)
+ instance = self._create_instance()
def check():
- instances = conn.list_instances()
+ instances = self.conn.list_instances()
self.assertEquals(instances, [1])
# Get Nova record for VM
- vm_info = conn.get_info(1)
+ vm_info = self.conn.get_info(1)
# Get XenAPI record for VM
- vms = fake.get_all('VM')
- vm = fake.get_record('VM', vms[0])
+ vms = xenapi_fake.get_all('VM')
+ vm = xenapi_fake.get_record('VM', vms[0])
# Check that m1.large above turned into the right thing.
instance_type = instance_types.INSTANCE_TYPES['m1.large']
@@ -218,3 +247,18 @@ class XenAPIVMTestCase(test.TestCase):
self.manager.delete_project(self.project)
self.manager.delete_user(self.user)
self.stubs.UnsetAll()
+
+ def _create_instance(self):
+ """Creates and spawns a test instance"""
+ values = {'name': 1, 'id': 1,
+ 'project_id': self.project.id,
+ 'user_id': self.user.id,
+ 'image_id': 1,
+ 'kernel_id': 2,
+ 'ramdisk_id': 3,
+ 'instance_type': 'm1.large',
+ 'mac_address': 'aa:bb:cc:dd:ee:ff'
+ }
+ instance = db.instance_create(values)
+ self.conn.spawn(instance)
+ return instance
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index a7e592fee..55f751f11 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -19,6 +19,54 @@
from nova.virt import xenapi_conn
from nova.virt.xenapi import fake
from nova.virt.xenapi import volume_utils
+from nova.virt.xenapi import vm_utils
+
+
+def stubout_instance_snapshot(stubs):
+ @classmethod
+ def fake_fetch_image(cls, session, instance_id, image, user, project,
+ type):
+ # Stubout wait_for_task
+ def fake_wait_for_task(self, id, task):
+ class FakeEvent:
+
+ def send(self, value):
+ self.rv = value
+
+ def wait(self):
+ return self.rv
+
+ done = FakeEvent()
+ self._poll_task(id, task, done)
+ rv = done.wait()
+ return rv
+
+ stubs.Set(xenapi_conn.XenAPISession, 'wait_for_task',
+ fake_wait_for_task)
+
+ from nova.virt.xenapi.fake import create_vdi
+ name_label = "instance-%s" % instance_id
+ #TODO: create fake SR record
+ sr_ref = "fakesr"
+ vdi_ref = create_vdi(name_label=name_label, read_only=False,
+ sr_ref=sr_ref, sharable=False)
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ vdi_uuid = vdi_rec['uuid']
+ return vdi_uuid
+
+ stubs.Set(vm_utils.VMHelper, 'fetch_image', fake_fetch_image)
+
+ def fake_parse_xmlrpc_value(val):
+ return val
+
+ stubs.Set(xenapi_conn, '_parse_xmlrpc_value', fake_parse_xmlrpc_value)
+
+ def fake_wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
+ original_parent_uuid):
+ #TODO(sirp): Should we actually fake out the data here
+ return "fakeparent"
+
+ stubs.Set(vm_utils, 'wait_for_vhd_coalesce', fake_wait_for_vhd_coalesce)
def stubout_session(stubs, cls):
@@ -63,6 +111,24 @@ class FakeSessionForVMTests(fake.SessionBase):
vm['is_a_template'] = False
vm['is_control_domain'] = False
+ def VM_snapshot(self, session_ref, vm_ref, label):
+ status = "Running"
+ template_vm_ref = fake.create_vm(label, status, is_a_template=True,
+ is_control_domain=False)
+
+ sr_ref = "fakesr"
+ template_vdi_ref = fake.create_vdi(label, read_only=True,
+ sr_ref=sr_ref, sharable=False)
+
+ template_vbd_ref = fake.create_vbd(template_vm_ref, template_vdi_ref)
+ return template_vm_ref
+
+ def VDI_destroy(self, session_ref, vdi_ref):
+ fake.destroy_vdi(vdi_ref)
+
+ def VM_destroy(self, session_ref, vm_ref):
+ fake.destroy_vm(vm_ref)
+
class FakeSessionForVolumeTests(fake.SessionBase):
""" Stubs out a XenAPISession for Volume tests """
diff --git a/nova/utils.py b/nova/utils.py
index 55542f265..0a339995e 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -48,7 +48,8 @@ def import_class(import_str):
try:
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
- except (ImportError, ValueError, AttributeError):
+ except (ImportError, ValueError, AttributeError), exc:
+ logging.debug(_('Inner Exception: %s'), exc)
raise exception.NotFound(_('Class %s cannot be found') % class_str)
@@ -175,16 +176,9 @@ def runthis(prompt, cmd, check_exit_code=True):
def generate_uid(topic, size=8):
- #modified by masumotok
- #characters = '01234567890abcdefghijklmnopqrstuvwxyz'
- #choices = [random.choice(characters) for x in xrange(size)]
- #return '%s-%s' % (topic, ''.join(choices))
- if topic == "i":
- return random.randint(0, 2 ** 28 - 1)
- else:
- characters = '01234567890abcdefghijklmnopqrstuvwxyz'
- choices = [random.choice(characters) for x in xrange(size)]
- return '%s-%s' % (topic, ''.join(choices))
+ characters = '01234567890abcdefghijklmnopqrstuvwxyz'
+ choices = [random.choice(characters) for x in xrange(size)]
+ return '%s-%s' % (topic, ''.join(choices))
def generate_mac():
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 238acf798..32541f5b4 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -112,6 +112,20 @@ class FakeConnection(object):
self.instances[instance.name] = fake_instance
fake_instance._state = power_state.RUNNING
+ def snapshot(self, instance, name):
+ """
+ Snapshots the specified instance.
+
+ The given parameter is an instance of nova.compute.service.Instance,
+ and so the instance is being specified as instance.name.
+
+ The second parameter is the name of the snapshot.
+
+ The work will be done asynchronously. This function returns a
+ Deferred that allows the caller to detect when it is complete.
+ """
+ pass
+
def reboot(self, instance):
"""
Reboot the specified instance.
@@ -148,6 +162,18 @@ class FakeConnection(object):
"""
pass
+ def suspend(self, instance, callback):
+ """
+ suspend the specified instance
+ """
+ pass
+
+ def resume(self, instance, callback):
+ """
+ resume the specified instance
+ """
+ pass
+
def destroy(self, instance):
"""
Destroy (shutdown and delete) the specified instance.
@@ -190,6 +216,9 @@ class FakeConnection(object):
'num_cpu': 2,
'cpu_time': 0}
+ def get_diagnostics(self, instance_name):
+ pass
+
def list_disks(self, instance_name):
"""
Return the IDs of all the virtual disks attached to the specified
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 8c2d52bb6..d1a53f275 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -59,10 +59,9 @@ from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import images
-from Cheetah.Template import Template
-
libvirt = None
libxml2 = None
+Template = None
FLAGS = flags.FLAGS
@@ -70,6 +69,9 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image')
flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image')
flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image')
+flags.DEFINE_string('injected_network_template',
+ utils.abspath('virt/interfaces.template'),
+ 'Template file for injected network')
flags.DEFINE_string('libvirt_xml_template',
utils.abspath('virt/libvirt.xml.template'),
'Libvirt XML Template')
@@ -92,15 +94,26 @@ flags.DEFINE_bool('allow_project_net_traffic',
def get_connection(read_only):
# These are loaded late so that there's no need to install these
# libraries when not using libvirt.
+ # Cheetah is separate because the unit tests want to load Cheetah,
+ # but not libvirt.
global libvirt
global libxml2
if libvirt is None:
libvirt = __import__('libvirt')
if libxml2 is None:
libxml2 = __import__('libxml2')
+ _late_load_cheetah()
return LibvirtConnection(read_only)
+def _late_load_cheetah():
+ global Template
+ if Template is None:
+ t = __import__('Cheetah.Template', globals(), locals(), ['Template'],
+ -1)
+ Template = t.Template
+
+
def _get_net_and_mask(cidr):
net = IPy.IP(cidr)
return str(net.net()), str(net.netmask())
@@ -251,6 +264,13 @@ class LibvirtConnection(object):
virt_dom.detachDevice(xml)
@exception.wrap_exception
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance """
+ raise NotImplementedError(
+ _("Instance snapshotting is not supported for libvirt"
+ "at this time"))
+
+ @exception.wrap_exception
def reboot(self, instance):
self.destroy(instance, False)
xml = self.to_xml(instance)
@@ -284,6 +304,14 @@ class LibvirtConnection(object):
raise exception.APIError("unpause not supported for libvirt.")
@exception.wrap_exception
+ def suspend(self, instance, callback):
+ raise exception.APIError("suspend not supported for libvirt")
+
+ @exception.wrap_exception
+ def resume(self, instance, callback):
+ raise exception.APIError("resume not supported for libvirt")
+
+ @exception.wrap_exception
def rescue(self, instance):
self.destroy(instance, False)
@@ -516,9 +544,10 @@ class LibvirtConnection(object):
if FLAGS.allow_project_net_traffic:
net, mask = _get_net_and_mask(network['cidr'])
- extra_params = ("<parameter name=\"PROJNET\" value=\"%s\" />\n"
- "<parameter name=\"PROJMASK\" value=\"%s\" />\n"
- ) % (net, mask)
+ extra_params = ("<parameter name=\"PROJNET\" "
+ "value=\"%s\" />\n"
+ "<parameter name=\"PROJMASK\" "
+ "value=\"%s\" />\n") % (net, mask)
else:
extra_params = "\n"
@@ -562,6 +591,9 @@ class LibvirtConnection(object):
'num_cpu': num_cpu,
'cpu_time': cpu_time}
+ def get_diagnostics(self, instance_name):
+ raise exception.APIError("diagnostics are not supported for libvirt")
+
def get_disks(self, instance_name):
"""
Note that this function takes an instance name, not an Instance, so
@@ -941,8 +973,8 @@ class NWFilterFirewall(object):
the base filter are all in place.
"""
- nwfilter_xml = ("<filter name='nova-instance-%s' chain='root'>\n"
- ) % instance['name']
+ nwfilter_xml = ("<filter name='nova-instance-%s' "
+ "chain='root'>\n") % instance['name']
if instance['image_id'] == FLAGS.vpn_image_id:
nwfilter_xml += " <filterref filter='nova-vpn' />\n"
@@ -955,8 +987,8 @@ class NWFilterFirewall(object):
for security_group in instance.security_groups:
self.ensure_security_group_filter(security_group['id'])
- nwfilter_xml += (" <filterref filter='nova-secgroup-%d' />\n"
- ) % security_group['id']
+ nwfilter_xml += (" <filterref filter='nova-secgroup-%d' "
+ "/>\n") % security_group['id']
nwfilter_xml += "</filter>"
self._define_filter(nwfilter_xml)
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
index 7a6c9ee71..aa4026f97 100644
--- a/nova/virt/xenapi/fake.py
+++ b/nova/virt/xenapi/fake.py
@@ -55,6 +55,8 @@ import datetime
import logging
import uuid
+from pprint import pformat
+
from nova import exception
@@ -64,6 +66,10 @@ _CLASSES = ['host', 'network', 'session', 'SR', 'VBD',\
_db_content = {}
+def log_db_contents(msg=None):
+ logging.debug(_("%s: _db_content => %s"), msg or "", pformat(_db_content))
+
+
def reset():
for c in _CLASSES:
_db_content[c] = {}
@@ -93,6 +99,24 @@ def create_vm(name_label, status,
})
+def destroy_vm(vm_ref):
+ vm_rec = _db_content['VM'][vm_ref]
+
+ vbd_refs = vm_rec['VBDs']
+ for vbd_ref in vbd_refs:
+ destroy_vbd(vbd_ref)
+
+ del _db_content['VM'][vm_ref]
+
+
+def destroy_vbd(vbd_ref):
+ del _db_content['VBD'][vbd_ref]
+
+
+def destroy_vdi(vdi_ref):
+ del _db_content['VDI'][vdi_ref]
+
+
def create_vdi(name_label, read_only, sr_ref, sharable):
return _create_object('VDI', {
'name_label': name_label,
@@ -109,6 +133,23 @@ def create_vdi(name_label, read_only, sr_ref, sharable):
})
+def create_vbd(vm_ref, vdi_ref):
+ vbd_rec = {'VM': vm_ref, 'VDI': vdi_ref}
+ vbd_ref = _create_object('VBD', vbd_rec)
+ after_VBD_create(vbd_ref, vbd_rec)
+ return vbd_ref
+
+
+def after_VBD_create(vbd_ref, vbd_rec):
+ """Create backref from VM to VBD when VBD is created"""
+ vm_ref = vbd_rec['VM']
+ vm_rec = _db_content['VM'][vm_ref]
+ vm_rec['VBDs'] = [vbd_ref]
+
+ vm_name_label = _db_content['VM'][vm_ref]['name_label']
+ vbd_rec['vm_name_label'] = vm_name_label
+
+
def create_pbd(config, sr_ref, attached):
return _create_object('PBD', {
'device-config': config,
@@ -235,6 +276,7 @@ class SessionBase(object):
elif '.' in name:
impl = getattr(self, name.replace('.', '_'))
if impl is not None:
+
def callit(*params):
logging.warn('Calling %s %s', name, impl)
self._check_session(params)
@@ -276,11 +318,12 @@ class SessionBase(object):
self._check_arg_count(params, 2)
return get_record(cls, params[1])
- if (func == 'get_by_name_label' or
- func == 'get_by_uuid'):
+ if func in ('get_by_name_label', 'get_by_uuid'):
self._check_arg_count(params, 2)
+ return_singleton = (func == 'get_by_uuid')
return self._get_by_field(
- _db_content[cls], func[len('get_by_'):], params[1])
+ _db_content[cls], func[len('get_by_'):], params[1],
+ return_singleton=return_singleton)
if len(params) == 2:
field = func[len('get_'):]
@@ -323,6 +366,13 @@ class SessionBase(object):
(cls, _) = name.split('.')
ref = is_sr_create and \
_create_sr(cls, params) or _create_object(cls, params[1])
+
+ # Call hook to provide any fixups needed (ex. creating backrefs)
+ try:
+ globals()["after_%s_create" % cls](ref, params[1])
+ except KeyError:
+ pass
+
obj = get_record(cls, ref)
# Add RO fields
@@ -358,11 +408,18 @@ class SessionBase(object):
raise Failure(['MESSAGE_PARAMETER_COUNT_MISMATCH',
expected, actual])
- def _get_by_field(self, recs, k, v):
+ def _get_by_field(self, recs, k, v, return_singleton):
result = []
for ref, rec in recs.iteritems():
if rec.get(k) == v:
result.append(ref)
+
+ if return_singleton:
+ try:
+ return result[0]
+ except IndexError:
+ return None
+
return result
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 89e02c917..9d1b51848 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -20,11 +20,14 @@ their attributes like VDIs, VIFs, as well as their lookup functions.
"""
import logging
+import pickle
import urllib
from xml.dom import minidom
+from eventlet import event
from nova import exception
from nova import flags
+from nova import utils
from nova.auth.manager import AuthManager
from nova.compute import instance_types
from nova.compute import power_state
@@ -39,19 +42,35 @@ XENAPI_POWER_STATE = {
'Halted': power_state.SHUTDOWN,
'Running': power_state.RUNNING,
'Paused': power_state.PAUSED,
- 'Suspended': power_state.SHUTDOWN, # FIXME
+ 'Suspended': power_state.SUSPENDED,
'Crashed': power_state.CRASHED}
+class ImageType:
+ """
+ Enumeration class for distinguishing different image types
+ 0 - kernel/ramdisk image (goes on dom0's filesystem)
+ 1 - disk image (local SR, partitioned by objectstore plugin)
+ 2 - raw disk image (local SR, NOT partitioned by plugin)
+ """
+
+ KERNEL_RAMDISK = 0
+ DISK = 1
+ DISK_RAW = 2
+
+
class VMHelper(HelperBase):
"""
The class that wraps the helper methods together.
"""
@classmethod
- def create_vm(cls, session, instance, kernel, ramdisk):
+ def create_vm(cls, session, instance, kernel, ramdisk, pv_kernel=False):
"""Create a VM record. Returns a Deferred that gives the new
- VM reference."""
+ VM reference.
+ the pv_kernel flag indicates whether the guest is HVM or PV
+ """
+
instance_type = instance_types.INSTANCE_TYPES[instance.instance_type]
mem = str(long(instance_type['memory_mb']) * 1024 * 1024)
vcpus = str(instance_type['vcpus'])
@@ -70,9 +89,9 @@ class VMHelper(HelperBase):
'actions_after_reboot': 'restart',
'actions_after_crash': 'destroy',
'PV_bootloader': '',
- 'PV_kernel': kernel,
- 'PV_ramdisk': ramdisk,
- 'PV_args': 'root=/dev/xvda1',
+ 'PV_kernel': '',
+ 'PV_ramdisk': '',
+ 'PV_args': '',
'PV_bootloader_args': '',
'PV_legacy_args': '',
'HVM_boot_policy': '',
@@ -84,7 +103,25 @@ class VMHelper(HelperBase):
'user_version': '0',
'other_config': {},
}
- logging.debug(_('Created VM %s...'), instance.name)
+ #Complete VM configuration record according to the image type
+ #non-raw/raw with PV kernel/raw in HVM mode
+ if instance.kernel_id:
+ rec['PV_bootloader'] = ''
+ rec['PV_kernel'] = kernel
+ rec['PV_ramdisk'] = ramdisk
+ rec['PV_args'] = 'root=/dev/xvda1'
+ rec['PV_bootloader_args'] = ''
+ rec['PV_legacy_args'] = ''
+ else:
+ if pv_kernel:
+ rec['PV_args'] = 'noninteractive'
+ rec['PV_bootloader'] = 'pygrub'
+ else:
+ rec['HVM_boot_policy'] = 'BIOS order'
+ rec['HVM_boot_params'] = {'order': 'dc'}
+ rec['platform'] = {'acpi': 'true', 'apic': 'true',
+ 'pae': 'true', 'viridian': 'true'}
+ logging.debug('Created VM %s...', instance.name)
vm_ref = session.call_xenapi('VM.create', rec)
logging.debug(_('Created VM %s as %s.'), instance.name, vm_ref)
return vm_ref
@@ -170,29 +207,92 @@ class VMHelper(HelperBase):
return vif_ref
@classmethod
- def fetch_image(cls, session, image, user, project, use_sr):
- """use_sr: True to put the image as a VDI in an SR, False to place
- it on dom0's filesystem. The former is for VM disks, the latter for
- its kernel and ramdisk (if external kernels are being used).
- Returns a Deferred that gives the new VDI UUID."""
+ def create_snapshot(cls, session, instance_id, vm_ref, label):
+ """ Creates Snapshot (Template) VM, Snapshot VBD, Snapshot VDI,
+ Snapshot VHD
+ """
+ #TODO(sirp): Add quiesce and VSS locking support when Windows support
+ # is added
+ logging.debug(_("Snapshotting VM %s with label '%s'..."),
+ vm_ref, label)
+
+ vm_vdi_ref, vm_vdi_rec = get_vdi_for_vm_safely(session, vm_ref)
+ vm_vdi_uuid = vm_vdi_rec["uuid"]
+ sr_ref = vm_vdi_rec["SR"]
+
+ original_parent_uuid = get_vhd_parent_uuid(session, vm_vdi_ref)
+
+ task = session.call_xenapi('Async.VM.snapshot', vm_ref, label)
+ template_vm_ref = session.wait_for_task(instance_id, task)
+ template_vdi_rec = get_vdi_for_vm_safely(session, template_vm_ref)[1]
+ template_vdi_uuid = template_vdi_rec["uuid"]
+
+ logging.debug(_('Created snapshot %s from VM %s.'), template_vm_ref,
+ vm_ref)
+
+ parent_uuid = wait_for_vhd_coalesce(
+ session, instance_id, sr_ref, vm_vdi_ref, original_parent_uuid)
+
+ #TODO(sirp): we need to assert only one parent, not parents two deep
+ return template_vm_ref, [template_vdi_uuid, parent_uuid]
+ @classmethod
+ def upload_image(cls, session, instance_id, vdi_uuids, image_name):
+ """ Requests that the Glance plugin bundle the specified VDIs and
+ push them into Glance using the specified human-friendly name.
+ """
+ logging.debug(_("Asking xapi to upload %s as '%s'"),
+ vdi_uuids, image_name)
+
+ params = {'vdi_uuids': vdi_uuids,
+ 'image_name': image_name,
+ 'glance_host': FLAGS.glance_host,
+ 'glance_port': FLAGS.glance_port}
+
+ kwargs = {'params': pickle.dumps(params)}
+ task = session.async_call_plugin('glance', 'put_vdis', kwargs)
+ session.wait_for_task(instance_id, task)
+
+ @classmethod
+ def fetch_image(cls, session, instance_id, image, user, project, type):
+ """
+ type is interpreted as an ImageType instance
+ """
url = images.image_url(image)
access = AuthManager().get_access_key(user, project)
- logging.debug(_("Asking xapi to fetch %s as %s"), url, access)
- fn = use_sr and 'get_vdi' or 'get_kernel'
+ logging.debug("Asking xapi to fetch %s as %s", url, access)
+ fn = (type != ImageType.KERNEL_RAMDISK) and 'get_vdi' or 'get_kernel'
args = {}
args['src_url'] = url
args['username'] = access
args['password'] = user.secret
- if use_sr:
+ args['add_partition'] = 'false'
+ args['raw'] = 'false'
+ if type != ImageType.KERNEL_RAMDISK:
args['add_partition'] = 'true'
+ if type == ImageType.DISK_RAW:
+ args['raw'] = 'true'
task = session.async_call_plugin('objectstore', fn, args)
- #FIXME(armando): find a solution to missing instance_id
- #with Josh Kearney
- uuid = session.wait_for_task(0, task)
+ uuid = session.wait_for_task(instance_id, task)
return uuid
@classmethod
+ def lookup_image(cls, session, vdi_ref):
+ logging.debug("Looking up vdi %s for PV kernel", vdi_ref)
+ fn = "is_vdi_pv"
+ args = {}
+ args['vdi-ref'] = vdi_ref
+ #TODO: Call proper function in plugin
+ task = session.async_call_plugin('objectstore', fn, args)
+ pv_str = session.wait_for_task(task)
+ if pv_str.lower() == 'true':
+ pv = True
+ elif pv_str.lower() == 'false':
+ pv = False
+ logging.debug("PV Kernel in VDI:%d", pv)
+ return pv
+
+ @classmethod
def lookup(cls, session, i):
"""Look the instance i up, and returns it if available"""
vms = session.get_xenapi().VM.get_by_name_label(i)
@@ -231,6 +331,10 @@ class VMHelper(HelperBase):
@classmethod
def compile_info(cls, record):
"""Fill record with VM status information"""
+ logging.info(_("(VM_UTILS) xenserver vm state -> |%s|"),
+ record['power_state'])
+ logging.info(_("(VM_UTILS) xenapi power_state -> |%s|"),
+ XENAPI_POWER_STATE[record['power_state']])
return {'state': XENAPI_POWER_STATE[record['power_state']],
'max_mem': long(record['memory_static_max']) >> 10,
'mem': long(record['memory_dynamic_max']) >> 10,
@@ -243,6 +347,10 @@ class VMHelper(HelperBase):
try:
host = session.get_xenapi_host()
host_ip = session.get_xenapi().host.get_record(host)["address"]
+ except (cls.XenAPI.Failure, KeyError) as e:
+ return {"Unable to retrieve diagnostics": e}
+
+ try:
diags = {}
xml = get_rrd(host_ip, record["uuid"])
if xml:
@@ -269,3 +377,87 @@ def get_rrd(host, uuid):
return xml.read()
except IOError:
return None
+
+
+#TODO(sirp): This code comes from XS5.6 pluginlib.py, we should refactor to
+# use that implmenetation
+def get_vhd_parent(session, vdi_rec):
+ """
+ Returns the VHD parent of the given VDI record, as a (ref, rec) pair.
+ Returns None if we're at the root of the tree.
+ """
+ if 'vhd-parent' in vdi_rec['sm_config']:
+ parent_uuid = vdi_rec['sm_config']['vhd-parent']
+ #NOTE(sirp): changed xenapi -> get_xenapi()
+ parent_ref = session.get_xenapi().VDI.get_by_uuid(parent_uuid)
+ parent_rec = session.get_xenapi().VDI.get_record(parent_ref)
+ #NOTE(sirp): changed log -> logging
+ logging.debug(_("VHD %s has parent %s"), vdi_rec['uuid'], parent_ref)
+ return parent_ref, parent_rec
+ else:
+ return None
+
+
+def get_vhd_parent_uuid(session, vdi_ref):
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ ret = get_vhd_parent(session, vdi_rec)
+ if ret:
+ parent_ref, parent_rec = ret
+ return parent_rec["uuid"]
+ else:
+ return None
+
+
+def scan_sr(session, instance_id, sr_ref):
+ logging.debug(_("Re-scanning SR %s"), sr_ref)
+ task = session.call_xenapi('Async.SR.scan', sr_ref)
+ session.wait_for_task(instance_id, task)
+
+
+def wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
+ original_parent_uuid):
+ """ Spin until the parent VHD is coalesced into its parent VHD
+
+ Before coalesce:
+ * original_parent_vhd
+ * parent_vhd
+ snapshot
+
+ Atter coalesce:
+ * parent_vhd
+ snapshot
+ """
+ #TODO(sirp): we need to timeout this req after a while
+
+ def _poll_vhds():
+ scan_sr(session, instance_id, sr_ref)
+ parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
+ if original_parent_uuid and (parent_uuid != original_parent_uuid):
+ logging.debug(
+ _("Parent %s doesn't match original parent %s, "
+ "waiting for coalesce..."),
+ parent_uuid, original_parent_uuid)
+ else:
+ done.send(parent_uuid)
+
+ done = event.Event()
+ loop = utils.LoopingCall(_poll_vhds)
+ loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)
+ parent_uuid = done.wait()
+ loop.stop()
+ return parent_uuid
+
+
+def get_vdi_for_vm_safely(session, vm_ref):
+ vdi_refs = VMHelper.lookup_vm_vdis(session, vm_ref)
+ if vdi_refs is None:
+ raise Exception(_("No VDIs found for VM %s") % vm_ref)
+ else:
+ num_vdis = len(vdi_refs)
+ if num_vdis != 1:
+ raise Exception(_("Unexpected number of VDIs (%s) found for "
+ "VM %s") % (num_vdis, vm_ref))
+
+ vdi_ref = vdi_refs[0]
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ return vdi_ref, vdi_rec
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 39bb0f1e8..76f31635a 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -29,6 +29,7 @@ from nova.auth.manager import AuthManager
from nova.compute import power_state
from nova.virt.xenapi.network_utils import NetworkHelper
from nova.virt.xenapi.vm_utils import VMHelper
+from nova.virt.xenapi.vm_utils import ImageType
class VMOps(object):
@@ -64,16 +65,30 @@ class VMOps(object):
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
- vdi_uuid = VMHelper.fetch_image(
- self._session, instance.image_id, user, project, True)
- kernel = VMHelper.fetch_image(
- self._session, instance.kernel_id, user, project, False)
- ramdisk = VMHelper.fetch_image(
- self._session, instance.ramdisk_id, user, project, False)
+ #if kernel is not present we must download a raw disk
+ if instance.kernel_id:
+ disk_image_type = ImageType.DISK
+ else:
+ disk_image_type = ImageType.DISK_RAW
+ vdi_uuid = VMHelper.fetch_image(self._session, instance.id,
+ instance.image_id, user, project, disk_image_type)
vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
- vm_ref = VMHelper.create_vm(
- self._session, instance, kernel, ramdisk)
+ #Have a look at the VDI and see if it has a PV kernel
+ pv_kernel = False
+ if not instance.kernel_id:
+ pv_kernel = VMHelper.lookup_image(self._session, vdi_ref)
+ kernel = None
+ if instance.kernel_id:
+ kernel = VMHelper.fetch_image(self._session, instance.id,
+ instance.kernel_id, user, project, ImageType.KERNEL_RAMDISK)
+ ramdisk = None
+ if instance.ramdisk_id:
+ ramdisk = VMHelper.fetch_image(self._session, instance.id,
+ instance.ramdisk_id, user, project, ImageType.KERNEL_RAMDISK)
+ vm_ref = VMHelper.create_vm(self._session,
+ instance, kernel, ramdisk, pv_kernel)
VMHelper.create_vbd(self._session, vm_ref, vdi_ref, 0, True)
+
if network_ref:
VMHelper.create_vif(self._session, vm_ref,
network_ref, instance.mac_address)
@@ -105,6 +120,52 @@ class VMOps(object):
timer.f = _wait_for_boot
return timer.start(interval=0.5, now=True)
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance
+
+ :param instance: instance to be snapshotted
+ :param name: name/label to be given to the snapshot
+
+ Steps involved in a XenServer snapshot:
+
+ 1. XAPI-Snapshot: Snapshotting the instance using XenAPI. This
+ creates: Snapshot (Template) VM, Snapshot VBD, Snapshot VDI,
+ Snapshot VHD
+
+ 2. Wait-for-coalesce: The Snapshot VDI and Instance VDI both point to
+ a 'base-copy' VDI. The base_copy is immutable and may be chained
+ with other base_copies. If chained, the base_copies
+ coalesce together, so, we must wait for this coalescing to occur to
+ get a stable representation of the data on disk.
+
+ 3. Push-to-glance: Once coalesced, we call a plugin on the XenServer
+ that will bundle the VHDs together and then push the bundle into
+ Glance.
+ """
+
+ #TODO(sirp): Add quiesce and VSS locking support when Windows support
+ # is added
+
+ logging.debug(_("Starting snapshot for VM %s"), instance)
+ vm_ref = VMHelper.lookup(self._session, instance.name)
+
+ label = "%s-snapshot" % instance.name
+ try:
+ template_vm_ref, template_vdi_uuids = VMHelper.create_snapshot(
+ self._session, instance.id, vm_ref, label)
+ except self.XenAPI.Failure, exc:
+ logging.error(_("Unable to Snapshot %s: %s"), vm_ref, exc)
+ return
+
+ try:
+ # call plugin to ship snapshot off to glance
+ VMHelper.upload_image(
+ self._session, instance.id, template_vdi_uuids, name)
+ finally:
+ self._destroy(instance, template_vm_ref, shutdown=False)
+
+ logging.debug(_("Finished snapshot and upload for VM %s"), instance)
+
def reboot(self, instance):
"""Reboot VM instance"""
instance_name = instance.name
@@ -118,31 +179,36 @@ class VMOps(object):
def destroy(self, instance):
"""Destroy VM instance"""
vm = VMHelper.lookup(self._session, instance.name)
+ return self._destroy(instance, vm, shutdown=True)
+
+ def _destroy(self, instance, vm, shutdown=True):
+ """ Destroy VM instance """
if vm is None:
# Don't complain, just return. This lets us clean up instances
# that have already disappeared from the underlying platform.
return
# Get the VDIs related to the VM
vdis = VMHelper.lookup_vm_vdis(self._session, vm)
- try:
- task = self._session.call_xenapi('Async.VM.hard_shutdown',
- vm)
- self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
- logging.warn(exc)
+ if shutdown:
+ try:
+ task = self._session.call_xenapi('Async.VM.hard_shutdown', vm)
+ self._session.wait_for_task(instance.id, task)
+ except self.XenAPI.Failure, exc:
+ logging.warn(exc)
+
# Disk clean-up
if vdis:
for vdi in vdis:
try:
task = self._session.call_xenapi('Async.VDI.destroy', vdi)
self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
# VM Destroy
try:
task = self._session.call_xenapi('Async.VM.destroy', vm)
self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
def _wait_with_callback(self, instance_id, task, callback):
@@ -173,6 +239,26 @@ class VMOps(object):
task = self._session.call_xenapi('Async.VM.unpause', vm)
self._wait_with_callback(instance.id, task, callback)
+ def suspend(self, instance, callback):
+ """suspend the specified instance"""
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception(_("suspend: instance not present %s") %
+ instance_name)
+ task = self._session.call_xenapi('Async.VM.suspend', vm)
+ self._wait_with_callback(task, callback)
+
+ def resume(self, instance, callback):
+ """resume the specified instance"""
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception(_("resume: instance not present %s") %
+ instance_name)
+ task = self._session.call_xenapi('Async.VM.resume', vm, False, True)
+ self._wait_with_callback(task, callback)
+
def get_info(self, instance_id):
"""Return data about VM instance"""
vm = VMHelper.lookup(self._session, instance_id)
@@ -182,11 +268,12 @@ class VMOps(object):
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_info(rec)
- def get_diagnostics(self, instance_id):
+ def get_diagnostics(self, instance):
"""Return data about VM diagnostics"""
- vm = VMHelper.lookup(self._session, instance_id)
+ vm = VMHelper.lookup(self._session, instance.name)
if vm is None:
- raise exception.NotFound(_("Instance not found %s") % instance_id)
+ raise exception.NotFound(_("Instance not found %s") %
+ instance.name)
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_diagnostics(self._session, rec)
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
index a0c0a67d4..1ca813bcf 100644
--- a/nova/virt/xenapi/volume_utils.py
+++ b/nova/virt/xenapi/volume_utils.py
@@ -60,13 +60,11 @@ class VolumeHelper(HelperBase):
'port': info['targetPort'],
'targetIQN': info['targetIQN'],
'chapuser': info['chapuser'],
- 'chappassword': info['chappassword']
- }
+ 'chappassword': info['chappassword']}
else:
record = {'target': info['targetHost'],
'port': info['targetPort'],
- 'targetIQN': info['targetIQN']
- }
+ 'targetIQN': info['targetIQN']}
try:
sr_ref = session.get_xenapi().SR.create(
session.get_xenapi_host(),
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 11c66c974..f17c8f39d 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -83,6 +83,10 @@ flags.DEFINE_float('xenapi_task_poll_interval',
'The interval used for polling of remote tasks '
'(Async.VM.start, etc). Used only if '
'connection_type=xenapi.')
+flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',
+ 5.0,
+ 'The interval used for polling of coalescing vhds.'
+ ' Used only if connection_type=xenapi.')
flags.DEFINE_string('target_host',
None,
'iSCSI Target Host')
@@ -131,6 +135,10 @@ class XenAPIConnection(object):
"""Create VM instance"""
self._vmops.spawn(instance)
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance """
+ self._vmops.snapshot(instance, name)
+
def reboot(self, instance):
"""Reboot VM instance"""
self._vmops.reboot(instance)
@@ -147,13 +155,21 @@ class XenAPIConnection(object):
"""Unpause paused VM instance"""
self._vmops.unpause(instance, callback)
+ def suspend(self, instance, callback):
+ """suspend the specified instance"""
+ self._vmops.suspend(instance, callback)
+
+ def resume(self, instance, callback):
+ """resume the specified instance"""
+ self._vmops.resume(instance, callback)
+
def get_info(self, instance_id):
"""Return data about VM instance"""
return self._vmops.get_info(instance_id)
- def get_diagnostics(self, instance_id):
+ def get_diagnostics(self, instance):
"""Return data about VM diagnostics"""
- return self._vmops.get_diagnostics(instance_id)
+ return self._vmops.get_diagnostics(instance)
def get_console_output(self, instance):
"""Return snapshot of console"""
@@ -225,8 +241,8 @@ class XenAPISession(object):
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
action = dict(
- id=int(id),
- action=name,
+ instance_id=int(id),
+ action=name[0:255], # Ensure action is never > 255
error=None)
if status == "pending":
return
diff --git a/nova/wsgi.py b/nova/wsgi.py
index c7ee9ed14..b5d6b96c1 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -270,7 +270,7 @@ class Serializer(object):
needed to serialize a dictionary to that type.
"""
self.metadata = metadata or {}
- req = webob.Request(environ)
+ req = webob.Request.blank('', environ)
suffix = req.path_info.split('.')[-1].lower()
if suffix == 'json':
self.handler = self._to_json
diff --git a/plugins/xenserver/doc/networking.rst b/plugins/xenserver/doc/networking.rst
new file mode 100644
index 000000000..67f2d9af3
--- /dev/null
+++ b/plugins/xenserver/doc/networking.rst
@@ -0,0 +1,144 @@
+Multi Tenancy Networking Protections in XenServer
+=================================================
+
+The purpose of the vif_rules script is to allow multi-tenancy on a XenServer
+host. In a multi-tenant cloud environment a host machine needs to be able to
+enforce network isolation amongst guest instances, at both layer two and layer
+three. The rules prevent guests from taking and using unauthorized IP addresses,
+sniffing other guests traffic, and prevents ARP poisoning attacks. This current
+revision only supports IPv4, but will support IPv6 in the future.
+
+Kernel Requirements
+===================
+
+- physdev module
+- arptables support
+- ebtables support
+- iptables support
+
+If the kernel doesn't support these, you will need to obtain the Source RPMS for
+the proper version of XenServer to recompile the dom0 kernel.
+
+XenServer Requirements (32-bit dom0)
+====================================
+
+- arptables 32-bit rpm
+- ebtables 32-bit rpm
+- python-simplejson
+
+XenServer Environment Specific Notes
+====================================
+
+- XenServer 5.5 U1 based on the 2.6.18 kernel didn't include physdev module
+ support. Support for this had to be recompiled into the kernel.
+- XenServer 5.6 based on the 2.6.27 kernel didn't include physdev, ebtables, or
+ arptables.
+- XenServer 5.6 FP1 didn't include physdev, ebtables, or arptables but they do
+ have a Cloud Supplemental pack available to partners which swaps out the
+ kernels for kernels that support the networking rules.
+
+How it works - tl;dr
+====================
+
+iptables, ebtables, and arptables drop rules are applied to all forward chains
+on the host. These are applied at boot time with an init script. They ensure
+all forwarded packets are dropped by default. Allow rules are then applied to
+the instances to ensure they have permission to talk on the internet.
+
+How it works - Long
+===================
+
+Any time an underprivileged domain or domU is started or stopped, it gets a
+unique domain id (dom_id). This dom_id is utilized in a number of places, one
+of which is it's assigned to the virtual interface (vif). The vifs are attached
+to the bridge that is attached to the physical network. For instance, if you
+had a public bridge attached to eth0 and your domain id was 5, your vif would be
+vif5.0.
+
+The networking rules are applied to the VIF directly so they apply at the lowest
+level of the networking stack. Because the VIF changes along with the domain id
+on any start, stop, or reboot of the instance, the rules need to be removed and
+re-added any time that occurs.
+
+Because the dom_id can change often, the vif_rules script is hooked into the
+/etc/xensource/scripts/vif script that gets called anytime an instance is
+started, or stopped, which includes pauses and resumes.
+
+Examples of the rules ran for the host on boot:
+
+iptables -P FORWARD DROP
+iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT
+ebtables -P FORWARD DROP
+ebtables -A FORWARD -o eth0 -j ACCEPT
+arptables -P FORWARD DROP
+arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT
+arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
+
+Examples of the rules that are ran per instance state change:
+
+iptables -A FORWARD -m physdev --physdev-in vif1.0 -s 10.1.135.22/32 -j ACCEPT
+arptables -A FORWARD --opcode Request --in-interface "vif1.0" \
+ --source-ip 10.1.135.22 -j ACCEPT
+arptables -A FORWARD --opcode Reply --in-interface "vif1.0" \
+ --source-ip 10.1.135.22 --source-mac 9e:6e:cc:19:7f:fe -j ACCEPT
+ebtables -A FORWARD -p 0806 -o vif1.0 --arp-ip-dst 10.1.135.22 -j ACCEPT
+ebtables -A FORWARD -p 0800 -o vif1.0 --ip-dst 10.1.135.22 -j ACCEPT
+ebtables -I FORWARD 1 -s ! 9e:6e:cc:19:7f:fe -i vif1.0 -j DROP
+
+Typically when you see a vif, it'll look like vif<domain id>.<network bridge>.
+vif2.1 for example would be domain 2 on the second interface.
+
+The vif_rules.py script needs to pull information about the IPs and MAC
+addresses assigned to the instance. The current implementation assumes that
+information is put into the VM Record into the xenstore-data key in a JSON
+string. The vif_rules.py script reads out of the JSON string to determine the
+IPs, and MAC addresses to protect.
+
+An example format is given below:
+
+# xe vm-param-get uuid=<uuid> param-name=xenstore-data
+xenstore-data (MRW):
+vm-data/networking/4040fa7292e4:
+{"label": "public",
+ "ips": [{"netmask":"255.255.255.0",
+ "enabled":"1",
+ "ip":"173.200.100.10"}],
+ "mac":"40:40:fa:72:92:e4",
+ "gateway":"173.200.100.1",
+ "vm_id":"123456",
+ "dns":["72.3.128.240","72.3.128.241"]};
+
+vm-data/networking/40402321c9b8:
+{"label":"private",
+ "ips":[{"netmask":"255.255.224.0",
+ "enabled":"1",
+ "ip":"10.177.10.10"}],
+ "routes":[{"route":"10.176.0.0",
+ "netmask":"255.248.0.0",
+ "gateway":"10.177.10.1"},
+ {"route":"10.191.192.0",
+ "netmask":"255.255.192.0",
+ "gateway":"10.177.10.1"}],
+ "mac":"40:40:23:21:c9:b8"}
+
+The key is used for two purposes. One, the vif_rules.py script will read from
+it to apply the rules needed after parsing the JSON. The second is that because
+it's put into the xenstore-data field, the xenstore will be populated with this
+data on boot. This allows a guest agent the ability to read out data about the
+instance and apply configurations as needed.
+
+Installation
+============
+
+- Copy host-rules into /etc/init.d/ and make sure to chmod +x host-rules.
+- Run 'chkconfig host-rules on' to add the init script to start up.
+- Copy vif_rules.py into /etc/xensource/scripts
+- Patch /etc/xensource/scripts/vif using the supplied patch file. It may vary
+ for different versions of XenServer but it should be pretty self explanatory.
+ It calls the vif_rules.py script on domain creation and tear down.
+- Run '/etc/init.d/host-rules start' to start up the host based rules.
+- The instance rules will then fire on creation of the VM as long as the correct
+ JSON is in place.
+- You can check to see if the rules are in place with: iptables --list,
+ arptables --list, or ebtables --list
+
diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules
new file mode 100755
index 000000000..798da9552
--- /dev/null
+++ b/plugins/xenserver/networking/etc/init.d/host-rules
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# host-rules Start/Stop the networking host rules
+#
+# chkconfig: 2345 85 15
+# description: Networking Host Rules for Multi Tenancy Protections
+
+# Copyright 2010 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.
+
+IPTABLES=/sbin/iptables
+EBTABLES=/sbin/ebtables
+ARPTABLES=/sbin/arptables
+
+iptables-up()
+{
+ $IPTABLES -P FORWARD DROP
+ $IPTABLES -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT
+ $IPTABLES -A FORWARD -m physdev --physdev-in eth1 -j ACCEPT
+}
+
+ebtables-up()
+{
+ $EBTABLES -P FORWARD DROP
+ $EBTABLES -A FORWARD -o eth0 -j ACCEPT
+ $EBTABLES -A FORWARD -o eth1 -j ACCEPT
+}
+
+arptables-up()
+{
+ $ARPTABLES -P FORWARD DROP
+ $ARPTABLES -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT
+ $ARPTABLES -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
+ $ARPTABLES -A FORWARD --opcode Request --in-interface eth1 -j ACCEPT
+ $ARPTABLES -A FORWARD --opcode Reply --in-interface eth1 -j ACCEPT
+}
+
+iptables-down()
+{
+ $IPTABLES -P FORWARD ACCEPT
+ $IPTABLES -D FORWARD -m physdev --physdev-in eth0 -j ACCEPT
+ $IPTABLES -D FORWARD -m physdev --physdev-in eth1 -j ACCEPT
+}
+
+ebtables-down()
+{
+ $EBTABLES -P FORWARD ACCEPT
+ $EBTABLES -D FORWARD -o eth0 -j ACCEPT
+ $EBTABLES -D FORWARD -o eth1 -j ACCEPT
+}
+
+arptables-down()
+{
+ $ARPTABLES -P FORWARD ACCEPT
+ $ARPTABLES -D FORWARD --opcode Request --in-interface eth0 -j ACCEPT
+ $ARPTABLES -D FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
+ $ARPTABLES -D FORWARD --opcode Request --in-interface eth1 -j ACCEPT
+ $ARPTABLES -D FORWARD --opcode Reply --in-interface eth1 -j ACCEPT
+}
+
+start()
+{
+ iptables-up
+ ebtables-up
+ arptables-up
+}
+
+stop()
+{
+ iptables-down
+ ebtables-down
+ arptables-down
+}
+
+case "$1" in
+ start)
+ start
+ RETVAL=$?
+ ;;
+ stop)
+ stop
+ RETVAL=$?
+ ;;
+ restart)
+ stop
+ start
+ RETVAL=$?
+ ;;
+ *)
+ echo $"Usage: $0 {start|stop|restart}"
+ exit 1
+ ;;
+esac
+exit $RETVAL
diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch
new file mode 100644
index 000000000..feaf1312d
--- /dev/null
+++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch
@@ -0,0 +1,22 @@
+--- vif 2010-12-20 16:39:46.000000000 +0000
++++ vif_modified 2010-11-19 23:24:37.000000000 +0000
+@@ -213,6 +213,7 @@
+
+ # xs-xen.pq.hq:91e986b8e49f netback-wait-for-hotplug
+ xenstore-write "/local/domain/0/backend/vif/${DOMID}/${DEVID}/hotplug-status" "connected"
++ python /etc/xensource/scripts/vif_rules.py ${DOMID} online 2>&1 > /dev/null
+ fi
+ ;;
+
+@@ -224,9 +225,11 @@
+
+ remove)
+ if [ "${TYPE}" = "vif" ] ;then
++ python /etc/xensource/scripts/vif_rules.py ${DOMID} offline 2>&1 > /dev/null
+ xenstore-rm "${HOTPLUG}/hotplug"
+ fi
+ logger -t scripts-vif "${dev} has been removed"
+ remove_from_bridge
+ ;;
+ esac
++
diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py
new file mode 100755
index 000000000..d60816ce7
--- /dev/null
+++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 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.
+
+"""
+This script is used to configure iptables, ebtables, and arptables rules on
+XenServer hosts.
+"""
+
+import os
+import subprocess
+import sys
+
+# This is written to Python 2.4, since that is what is available on XenServer
+import simplejson as json
+
+
+def main(dom_id, command, only_this_vif=None):
+ xsls = execute("/usr/bin/xenstore-ls /local/domain/%s/vm-data/networking" \
+ % dom_id, True)
+ macs = [line.split("=")[0].strip() for line in xsls.splitlines()]
+
+ for mac in macs:
+ xsr = "/usr/bin/xenstore-read /local/domain/%s/vm-data/networking/%s"
+ xsread = execute(xsr % (dom_id, mac), True)
+ data = json.loads(xsread)
+ for ip in data['ips']:
+ if data["label"] == "public":
+ vif = "vif%s.0" % dom_id
+ else:
+ vif = "vif%s.1" % dom_id
+
+ if (only_this_vif is None) or (vif == only_this_vif):
+ params = dict(IP=ip['ip'], VIF=vif, MAC=data['mac'])
+ apply_ebtables_rules(command, params)
+ apply_arptables_rules(command, params)
+ apply_iptables_rules(command, params)
+
+
+def execute(command, return_stdout=False):
+ devnull = open(os.devnull, 'w')
+ proc = subprocess.Popen(command, shell=True, close_fds=True,
+ stdout=subprocess.PIPE, stderr=devnull)
+ devnull.close()
+ if return_stdout:
+ return proc.stdout.read()
+ else:
+ return None
+
+# A note about adding rules:
+# Whenever we add any rule to iptables, arptables or ebtables we first
+# delete the same rule to ensure the rule only exists once.
+
+
+def apply_iptables_rules(command, params):
+ iptables = lambda rule: execute("/sbin/iptables %s" % rule)
+
+ iptables("-D FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \
+ -j ACCEPT" % params)
+ if command == 'online':
+ iptables("-A FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \
+ -j ACCEPT" % params)
+
+
+def apply_arptables_rules(command, params):
+ arptables = lambda rule: execute("/sbin/arptables %s" % rule)
+
+ arptables("-D FORWARD --opcode Request --in-interface %(VIF)s \
+ --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
+ arptables("-D FORWARD --opcode Reply --in-interface %(VIF)s \
+ --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
+ if command == 'online':
+ arptables("-A FORWARD --opcode Request --in-interface %(VIF)s \
+ --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
+ arptables("-A FORWARD --opcode Reply --in-interface %(VIF)s \
+ --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
+
+
+def apply_ebtables_rules(command, params):
+ ebtables = lambda rule: execute("/sbin/ebtables %s" % rule)
+
+ ebtables("-D FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s -j ACCEPT" %
+ params)
+ ebtables("-D FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s -j ACCEPT" %
+ params)
+ if command == 'online':
+ ebtables("-A FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s \
+ -j ACCEPT" % params)
+ ebtables("-A FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s \
+ -j ACCEPT" % params)
+
+ ebtables("-D FORWARD -s ! %(MAC)s -i %(VIF)s -j DROP" % params)
+ if command == 'online':
+ ebtables("-I FORWARD 1 -s ! %(MAC)s -i %(VIF)s -j DROP" % params)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print "usage: %s dom_id online|offline [vif]" % \
+ os.path.basename(sys.argv[0])
+ sys.exit(1)
+ else:
+ dom_id, command = sys.argv[1:3]
+ vif = len(sys.argv) == 4 and sys.argv[3] or None
+ main(dom_id, command, vif)
diff --git a/plugins/xenapi/README b/plugins/xenserver/xenapi/README
index fbd471035..fbd471035 100644
--- a/plugins/xenapi/README
+++ b/plugins/xenserver/xenapi/README
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
new file mode 100644
index 000000000..5e648b970
--- /dev/null
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+# 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.
+
+#
+# XenAPI plugin for putting images into glance
+#
+
+import base64
+import errno
+import hmac
+import httplib
+import os
+import os.path
+import pickle
+import sha
+import subprocess
+import time
+import urlparse
+
+import XenAPIPlugin
+
+#FIXME(sirp): should this use pluginlib from 5.6?
+from pluginlib_nova import *
+configure_logging('glance')
+
+CHUNK_SIZE = 8192
+FILE_SR_PATH = '/var/run/sr-mount'
+
+def put_vdis(session, args):
+ params = pickle.loads(exists(args, 'params'))
+ vdi_uuids = params["vdi_uuids"]
+ image_name = params["image_name"]
+ glance_host = params["glance_host"]
+ glance_port = params["glance_port"]
+
+ sr_path = get_sr_path(session)
+ #FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs
+ tmp_file = "%s.tar.gz" % os.path.join('/tmp', image_name)
+ tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path]
+ paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ]
+ tar_cmd.extend(paths)
+ logging.debug("Bundling image with cmd: %s", tar_cmd)
+ subprocess.call(tar_cmd)
+ logging.debug("Writing to test file %s", tmp_file)
+ put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port)
+ return "" # FIXME(sirp): return anything useful here?
+
+
+def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port):
+ size = os.path.getsize(tmp_file)
+ basename = os.path.basename(tmp_file)
+
+ bundle = open(tmp_file, 'r')
+ try:
+ headers = {
+ 'x-image-meta-store': 'file',
+ 'x-image-meta-is_public': 'True',
+ 'x-image-meta-type': 'raw',
+ 'x-image-meta-name': image_name,
+ 'x-image-meta-size': size,
+ 'content-length': size,
+ 'content-type': 'application/octet-stream',
+ }
+ conn = httplib.HTTPConnection(glance_host, glance_port)
+ #NOTE(sirp): httplib under python2.4 won't accept a file-like object
+ # to request
+ conn.putrequest('POST', '/images')
+
+ for header, value in headers.iteritems():
+ conn.putheader(header, value)
+ conn.endheaders()
+
+ chunk = bundle.read(CHUNK_SIZE)
+ while chunk:
+ conn.send(chunk)
+ chunk = bundle.read(CHUNK_SIZE)
+
+
+ res = conn.getresponse()
+ #FIXME(sirp): should this be 201 Created?
+ if res.status != httplib.OK:
+ raise Exception("Unexpected response from Glance %i" % res.status)
+ finally:
+ bundle.close()
+
+def get_sr_path(session):
+ sr_ref = find_sr(session)
+
+ if sr_ref is None:
+ raise Exception('Cannot find SR to read VDI from')
+
+ sr_rec = session.xenapi.SR.get_record(sr_ref)
+ sr_uuid = sr_rec["uuid"]
+ sr_path = os.path.join(FILE_SR_PATH, sr_uuid)
+ return sr_path
+
+
+#TODO(sirp): both objectstore and glance need this, should this be refactored
+#into common lib
+def find_sr(session):
+ host = get_this_host(session)
+ srs = session.xenapi.SR.get_all()
+ for sr in srs:
+ sr_rec = session.xenapi.SR.get_record(sr)
+ if not ('i18n-key' in sr_rec['other_config'] and
+ sr_rec['other_config']['i18n-key'] == 'local-storage'):
+ continue
+ for pbd in sr_rec['PBDs']:
+ pbd_rec = session.xenapi.PBD.get_record(pbd)
+ if pbd_rec['host'] == host:
+ return sr
+ return None
+
+
+if __name__ == '__main__':
+ XenAPIPlugin.dispatch({'put_vdis': put_vdis})
diff --git a/plugins/xenapi/etc/xapi.d/plugins/objectstore b/plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore
index 271e7337f..8ee2f748d 100644
--- a/plugins/xenapi/etc/xapi.d/plugins/objectstore
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore
@@ -43,24 +43,43 @@ SECTOR_SIZE = 512
MBR_SIZE_SECTORS = 63
MBR_SIZE_BYTES = MBR_SIZE_SECTORS * SECTOR_SIZE
-
+def is_vdi_pv(session,args):
+ logging.debug("Checking wheter VDI has PV kernel")
+ vdi = exists(args, 'vdi-ref')
+ pv=with_vdi_in_dom0(session, vdi, False,
+ lambda dev: _is_vdi_pv('/dev/%s' % dev))
+ if pv:
+ return 'true'
+ else:
+ return 'false'
+
+def _is_vdi_pv(dest):
+ logging.debug("Running pygrub against %s",dest)
+ output=os.popen('pygrub -qn %s' % dest)
+ pv=False
+ for line in output.readlines():
+ #try to find kernel string
+ m=re.search('(?<=kernel:)/.*(?:>)',line)
+ if m:
+ if m.group(0).find('xen')!=-1:
+ pv=True
+ logging.debug("PV:%d",pv)
+ return pv
+
def get_vdi(session, args):
src_url = exists(args, 'src_url')
username = exists(args, 'username')
password = exists(args, 'password')
+ raw_image=validate_bool(args, 'raw', 'false')
add_partition = validate_bool(args, 'add_partition', 'false')
-
(proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url)
-
sr = find_sr(session)
if sr is None:
raise Exception('Cannot find SR to write VDI to')
-
virtual_size = \
get_content_length(proto, netloc, url_path, username, password)
if virtual_size < 0:
raise Exception('Cannot get VDI size')
-
vdi_size = virtual_size
if add_partition:
# Make room for MBR.
@@ -69,18 +88,19 @@ def get_vdi(session, args):
vdi = create_vdi(session, sr, src_url, vdi_size, False)
with_vdi_in_dom0(session, vdi, False,
lambda dev: get_vdi_(proto, netloc, url_path,
- username, password, add_partition,
+ username, password, add_partition,raw_image,
virtual_size, '/dev/%s' % dev))
return session.xenapi.VDI.get_uuid(vdi)
-def get_vdi_(proto, netloc, url_path, username, password, add_partition,
+def get_vdi_(proto, netloc, url_path, username, password, add_partition,raw_image,
virtual_size, dest):
- if add_partition:
+ #Salvatore: vdi should not be partitioned for raw images
+ if (add_partition and not raw_image):
write_partition(virtual_size, dest)
- offset = add_partition and MBR_SIZE_BYTES or 0
+ offset = (add_partition and not raw_image and MBR_SIZE_BYTES) or 0
get(proto, netloc, url_path, username, password, dest, offset)
@@ -228,4 +248,5 @@ def download_all(response, length, dest_file, offset):
if __name__ == '__main__':
XenAPIPlugin.dispatch({'get_vdi': get_vdi,
- 'get_kernel': get_kernel})
+ 'get_kernel': get_kernel,
+ 'is_vdi_pv': is_vdi_pv})
diff --git a/plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py
index 2d323a016..2d323a016 100755
--- a/plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py
diff --git a/run_tests.py b/run_tests.py
new file mode 100644
index 000000000..5b8617f63
--- /dev/null
+++ b/run_tests.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+# 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.
+
+
+import os
+import unittest
+import sys
+
+from nose import config
+from nose import result
+from nose import core
+
+
+class NovaTestResult(result.TextTestResult):
+ def __init__(self, *args, **kw):
+ result.TextTestResult.__init__(self, *args, **kw)
+ self._last_case = None
+
+ def getDescription(self, test):
+ return str(test)
+
+ def startTest(self, test):
+ unittest.TestResult.startTest(self, test)
+ current_case = test.test.__class__.__name__
+
+ if self.showAll:
+ if current_case != self._last_case:
+ self.stream.writeln(current_case)
+ self._last_case = current_case
+
+ self.stream.write(
+ ' %s' % str(test.test._testMethodName).ljust(60))
+ self.stream.flush()
+
+
+class NovaTestRunner(core.TextTestRunner):
+ def _makeResult(self):
+ return NovaTestResult(self.stream,
+ self.descriptions,
+ self.verbosity,
+ self.config)
+
+
+if __name__ == '__main__':
+ c = config.Config(stream=sys.stdout,
+ env=os.environ,
+ verbosity=3)
+
+ runner = NovaTestRunner(stream=c.stream,
+ verbosity=c.verbosity,
+ config=c)
+ sys.exit(not core.run(config=c, testRunner=runner))
diff --git a/run_tests.sh b/run_tests.sh
index 67214996d..fe703fece 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -21,6 +21,7 @@ function process_option {
-V|--virtual-env) let always_venv=1; let never_venv=0;;
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
-f|--force) let force=1;;
+ *) noseargs="$noseargs $1"
esac
}
@@ -29,15 +30,19 @@ with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
force=0
+noseargs=
+
for arg in "$@"; do
process_option $arg
done
+NOSETESTS="python run_tests.py $noseargs"
+
if [ $never_venv -eq 1 ]; then
# Just run the test suites in current environment
rm -f nova.sqlite
- nosetests -v
+ $NOSETESTS 2> run_tests.err.log
exit
fi
@@ -49,7 +54,7 @@ fi
if [ -e ${venv} ]; then
${with_venv} rm -f nova.sqlite
- ${with_venv} nosetests -v $@
+ ${with_venv} $NOSETESTS 2> run_tests.err.log
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
@@ -62,10 +67,10 @@ else
python tools/install_venv.py
else
rm -f nova.sqlite
- nosetests -v
+ $NOSETESTS 2> run_tests.err.log
exit
fi
fi
${with_venv} rm -f nova.sqlite
- ${with_venv} nosetests -v $@
+ ${with_venv} $NOSETESTS 2> run_tests.err.log
fi