summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustin Santa Barbara <justin@fathomdb.com>2011-03-29 19:09:42 -0700
committerJustin Santa Barbara <justin@fathomdb.com>2011-03-29 19:09:42 -0700
commit93b43cfcaeffa93b2f8ce50f473840c77be532c9 (patch)
treeab3c9f97697c157be300b58ee702e8f4ffab7fb4
parent2315682856f420ff0b781bead142e1aff82071a4 (diff)
parente5f108058f9b085571330dff3c3e3e3e57d2e5ed (diff)
Merged with trunk
-rw-r--r--Authors1
-rwxr-xr-xbin/nova-dhcpbridge4
-rwxr-xr-xbin/nova-manage11
-rw-r--r--nova/api/openstack/__init__.py11
-rw-r--r--nova/api/openstack/images.py307
-rw-r--r--nova/api/openstack/views/images.py98
-rw-r--r--nova/image/glance.py17
-rw-r--r--nova/network/linux_net.py3
-rw-r--r--nova/network/manager.py2
-rw-r--r--nova/network/xenapi_net.py85
-rw-r--r--nova/tests/api/openstack/test_images.py425
-rw-r--r--nova/tests/db/fakes.py65
-rw-r--r--nova/tests/fake_utils.py17
-rw-r--r--nova/tests/image/test_glance.py55
-rw-r--r--nova/tests/test_virt.py43
-rw-r--r--nova/tests/test_xenapi.py99
-rw-r--r--nova/virt/disk.py35
-rw-r--r--nova/virt/libvirt.xml.template31
-rw-r--r--nova/virt/libvirt_conn.py32
-rw-r--r--nova/virt/xenapi/fake.py172
-rw-r--r--nova/virt/xenapi/network_utils.py19
-rw-r--r--nova/virt/xenapi/vm_utils.py10
-rw-r--r--nova/virt/xenapi/vmops.py189
23 files changed, 1223 insertions, 508 deletions
diff --git a/Authors b/Authors
index 09759ddcb..eccf38a43 100644
--- a/Authors
+++ b/Authors
@@ -12,6 +12,7 @@ Chiradeep Vittal <chiradeep@cloud.com>
Chmouel Boudjnah <chmouel@chmouel.com>
Chris Behrens <cbehrens@codestud.com>
Christian Berendt <berendt@b1-systems.de>
+Chuck Short <zulcss@ubuntu.com>
Cory Wright <corywright@gmail.com>
Dan Prince <dan.prince@rackspace.com>
David Pravec <David.Pravec@danix.org>
diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge
index 7ef51feba..f42dfd6b5 100755
--- a/bin/nova-dhcpbridge
+++ b/bin/nova-dhcpbridge
@@ -48,6 +48,7 @@ flags.DECLARE('auth_driver', 'nova.auth.manager')
flags.DECLARE('network_size', 'nova.network.manager')
flags.DECLARE('num_networks', 'nova.network.manager')
flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager')
+flags.DEFINE_string('dnsmasq_interface', 'br0', 'Default Dnsmasq interface')
LOG = logging.getLogger('nova.dhcpbridge')
@@ -103,7 +104,8 @@ def main():
utils.default_flagfile(flagfile)
argv = FLAGS(sys.argv)
logging.setup()
- interface = os.environ.get('DNSMASQ_INTERFACE', 'br0')
+ # check ENV first so we don't break any older deploys
+ interface = os.environ.get('DNSMASQ_INTERFACE', FLAGS.dnsmasq_interface)
if int(os.environ.get('TESTING', '0')):
from nova.tests import fake_flags
action = argv[1]
diff --git a/bin/nova-manage b/bin/nova-manage
index cf0caf47e..25695482f 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -1098,8 +1098,8 @@ def main():
script_name = argv.pop(0)
if len(argv) < 1:
print script_name + " category action [<args>]"
- print "Available categories:"
- for k, _ in CATEGORIES:
+ print _("Available categories:")
+ for k, _v in CATEGORIES:
print "\t%s" % k
sys.exit(2)
category = argv.pop(0)
@@ -1110,7 +1110,7 @@ def main():
actions = methods_of(command_object)
if len(argv) < 1:
print script_name + " category action [<args>]"
- print "Available actions for %s category:" % category
+ print _("Available actions for %s category:") % category
for k, _v in actions:
print "\t%s" % k
sys.exit(2)
@@ -1122,9 +1122,12 @@ def main():
fn(*argv)
sys.exit(0)
except TypeError:
- print "Possible wrong number of arguments supplied"
+ print _("Possible wrong number of arguments supplied")
print "%s %s: %s" % (category, action, fn.__doc__)
raise
+ except Exception:
+ print _("Command failed, please check log for more info")
+ raise
if __name__ == '__main__':
main()
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 8b9acc11d..7545eb0c9 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -111,9 +111,6 @@ class APIRouter(wsgi.Router):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("image", "images", controller=images.Controller(),
- collection={'detail': 'GET'})
-
_limits = limits.LimitsController()
mapper.resource("limit", "limits", controller=_limits)
@@ -130,6 +127,10 @@ class APIRouterV10(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.ControllerV10(),
+ collection={'detail': 'GET'})
+
mapper.resource("flavor", "flavors",
controller=flavors.ControllerV10(),
collection={'detail': 'GET'})
@@ -154,6 +155,10 @@ class APIRouterV11(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.ControllerV11(),
+ collection={'detail': 'GET'})
+
mapper.resource("image_meta", "meta",
controller=image_metadata.Controller(),
parent_resource=dict(member_name='image',
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 79852ecc6..e77100d7b 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -1,6 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 OpenStack LLC.
+# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -17,7 +15,7 @@
import datetime
-from webob import exc
+import webob.exc
from nova import compute
from nova import exception
@@ -25,238 +23,133 @@ from nova import flags
from nova import log
from nova import utils
from nova import wsgi
-import nova.api.openstack
from nova.api.openstack import common
from nova.api.openstack import faults
-import nova.image.service
+from nova.api.openstack.views import images as images_view
LOG = log.getLogger('nova.api.openstack.images')
-
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'}
- try:
- item['status'] = status_mapping[item['status']]
- except KeyError:
- # TODO(sirp): Performing translation of status (if necessary) here for
- # now. Perhaps this should really be done in EC2 API and
- # S3ImageService
- pass
-
-
-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)
-
-
-def _convert_image_id_to_hash(image):
- if 'imageId' in image:
- # Convert EC2-style ID (i-blah) to Rackspace-style (int)
- image_id = abs(hash(image['imageId']))
- image['imageId'] = image_id
- image['id'] = image_id
-
-
-def _translate_s3_like_images(image_metadata):
- """Work-around for leaky S3ImageService abstraction"""
- api_metadata = image_metadata.copy()
- _convert_image_id_to_hash(api_metadata)
- api_metadata = _translate_keys(api_metadata)
- _translate_status(api_metadata)
- return api_metadata
-
-
-def _translate_from_image_service_to_api(image_metadata):
- """Translate from ImageService to OpenStack API style attribute names
-
- This involves 4 steps:
-
- 1. Filter out attributes that the OpenStack API doesn't need
-
- 2. Translate from base image attributes from names used by
- BaseImageService to names used by OpenStack API
-
- 3. Add in any image properties
-
- 4. Format values according to API spec (for example dates must
- look like "2010-08-10T12:00:00Z")
- """
- service_metadata = image_metadata.copy()
- properties = service_metadata.pop('properties', {})
-
- # 1. Filter out unecessary attributes
- api_keys = ['id', 'name', 'updated_at', 'created_at', 'status']
- api_metadata = utils.subset_dict(service_metadata, api_keys)
-
- # 2. Translate base image attributes
- api_map = {'updated_at': 'updated', 'created_at': 'created'}
- api_metadata = utils.map_dict_keys(api_metadata, api_map)
-
- # 3. Add in any image properties
- # 3a. serverId is used for backups and snapshots
- try:
- api_metadata['serverId'] = int(properties['instance_id'])
- except KeyError:
- pass # skip if it's not present
- except ValueError:
- pass # skip if it's not an integer
-
- # 3b. Progress special case
- # TODO(sirp): ImageService doesn't have a notion of progress yet, so for
- # now just fake it
- if service_metadata['status'] == 'saving':
- api_metadata['progress'] = 0
-
- # 4. Format values
- # 4a. Format Image Status (API requires uppercase)
- api_metadata['status'] = _format_status_for_api(api_metadata['status'])
-
- # 4b. Format timestamps
- for attr in ('created', 'updated'):
- if attr in api_metadata:
- api_metadata[attr] = _format_datetime_for_api(
- api_metadata[attr])
-
- return api_metadata
-
-
-def _format_status_for_api(status):
- """Return status in a format compliant with OpenStack API"""
- mapping = {'queued': 'QUEUED',
- 'preparing': 'PREPARING',
- 'saving': 'SAVING',
- 'active': 'ACTIVE',
- 'killed': 'FAILED'}
- return mapping[status]
-
-
-def _format_datetime_for_api(datetime_):
- """Stringify datetime objects in a format compliant with OpenStack API"""
- API_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
- return datetime_.strftime(API_DATETIME_FMT)
-
-
-def _safe_translate(image_metadata):
- """Translate attributes for OpenStack API, temporary workaround for
- S3ImageService attribute leakage.
- """
- # FIXME(sirp): The S3ImageService appears to be leaking implementation
- # details, including its internal attribute names, and internal
- # `status` values. Working around it for now.
- s3_like_image = ('imageId' in image_metadata)
- if s3_like_image:
- translate = _translate_s3_like_images
- else:
- translate = _translate_from_image_service_to_api
- return translate(image_metadata)
-
-
class Controller(wsgi.Controller):
+ """Base `wsgi.Controller` for retrieving/displaying images."""
_serialization_metadata = {
'application/xml': {
"attributes": {
"image": ["id", "name", "updated", "created", "status",
- "serverId", "progress"]}}}
+ "serverId", "progress"],
+ "link": ["rel", "type", "href"],
+ },
+ },
+ }
+
+ def __init__(self, image_service=None, compute_service=None):
+ """Initialize new `ImageController`.
- def __init__(self):
- self._service = utils.import_object(FLAGS.image_service)
+ :param compute_service: `nova.compute.api:API`
+ :param image_service: `nova.image.service:BaseImageService`
+ """
+ _default_service = utils.import_object(flags.FLAGS.image_service)
+
+ self._compute_service = compute_service or compute.API()
+ self._image_service = image_service or _default_service
def index(self, req):
- """Return all public images in brief"""
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+ """
context = req.environ['nova.context']
- image_metas = self._service.index(context)
- image_metas = common.limited(image_metas, req)
- return dict(images=image_metas)
+ images = self._image_service.index(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=False) for image in images])
def detail(self, req):
- """Return all public images in detail"""
+ """Return a detailed index listing of images available to the request.
+
+ :param req: `wsgi.Request` object.
+ """
context = req.environ['nova.context']
- image_metas = self._service.detail(context)
- image_metas = common.limited(image_metas, req)
- api_image_metas = [_safe_translate(image_meta)
- for image_meta in image_metas]
- return dict(images=api_image_metas)
+ images = self._image_service.detail(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=True) for image in images])
def show(self, req, id):
- """Return data about the given image id"""
+ """Return detailed information about a specific image.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
context = req.environ['nova.context']
+
+ try:
+ image_id = int(id)
+ except ValueError:
+ explanation = _("Image not found.")
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
+
try:
- image_id = common.get_image_id_from_image_hash(
- self._service, context, id)
+ image = self._image_service.show(context, image_id)
except exception.NotFound:
- raise faults.Fault(exc.HTTPNotFound())
+ explanation = _("Image '%d' not found.") % (image_id)
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
- image_meta = self._service.show(context, image_id)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
+ return dict(image=self.get_builder(req).build(image, detail=True))
def delete(self, req, id):
- # Only public images are supported for now.
- raise faults.Fault(exc.HTTPNotFound())
+ """Delete an image, if allowed.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ image_id = id
+ context = req.environ['nova.context']
+ self._image_service.delete(context, image_id)
+ return webob.exc.HTTPNoContent()
def create(self, req):
+ """Snapshot a server instance and save the image.
+
+ :param req: `wsgi.Request` object
+ """
context = req.environ['nova.context']
- env = self._deserialize(req.body, req.get_content_type())
- instance_id = env["image"]["serverId"]
- name = env["image"]["name"]
- image_meta = compute.API().snapshot(
- context, instance_id, name)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
-
- def update(self, req, id):
- # Users may not modify public images, and that's all that
- # we support for now.
- raise faults.Fault(exc.HTTPNotFound())
+ content_type = req.get_content_type()
+ image = self._deserialize(req.body, content_type)
+
+ if not image:
+ raise webob.exc.HTTPBadRequest()
+
+ try:
+ server_id = image["image"]["serverId"]
+ image_name = image["image"]["name"]
+ except KeyError:
+ raise webob.exc.HTTPBadRequest()
+
+ image = self._compute_service.snapshot(context, server_id, image_name)
+ return self.get_builder(req).build(image, detail=True)
+
+ def get_builder(self, request):
+ """Indicates that you must use a Controller subclass."""
+ raise NotImplementedError
+
+
+class ControllerV10(Controller):
+ """Version 1.0 specific controller logic."""
+
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV10(base_url)
+
+
+class ControllerV11(Controller):
+ """Version 1.1 specific controller logic."""
+
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV11(base_url)
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index a6c6ad7d1..3807fa95f 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -15,20 +15,100 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova.api.openstack import common
+import os.path
class ViewBuilder(object):
- def __init__(self):
- pass
+ """Base class for generating responses to OpenStack API image requests."""
- def build(self, image_obj):
- raise NotImplementedError()
+ def __init__(self, base_url):
+ """Initialize new `ViewBuilder`."""
+ self._url = base_url
+ def _format_dates(self, image):
+ """Update all date fields to ensure standardized formatting."""
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
-class ViewBuilderV11(ViewBuilder):
- def __init__(self, base_url):
- self.base_url = base_url
+ def _format_status(self, image):
+ """Update the status field to standardize format."""
+ status_mapping = {
+ 'pending': 'queued',
+ 'decrypting': 'preparing',
+ 'untarring': 'saving',
+ 'available': 'active',
+ 'killed': 'failed',
+ }
+
+ try:
+ image['status'] = status_mapping[image['status']].upper()
+ except KeyError:
+ image['status'] = image['status'].upper()
def generate_href(self, image_id):
- return "%s/images/%s" % (self.base_url, image_id)
+ """Return an href string pointing to this object."""
+ return os.path.join(self._url, "images", str(image_id))
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ properties = image_obj.get("properties", {})
+
+ self._format_dates(image_obj)
+
+ if "status" in image_obj:
+ self._format_status(image_obj)
+
+ image = {
+ "id": image_obj["id"],
+ "name": image_obj["name"],
+ }
+
+ if "instance_id" in properties:
+ try:
+ image["serverId"] = int(properties["instance_id"])
+ except ValueError:
+ pass
+
+ if detail:
+ image.update({
+ "created": image_obj["created_at"],
+ "updated": image_obj["updated_at"],
+ "status": image_obj["status"],
+ })
+
+ if image["status"] == "SAVING":
+ image["progress"] = 0
+
+ return image
+
+
+class ViewBuilderV10(ViewBuilder):
+ """OpenStack API v1.0 Image Builder"""
+ pass
+
+
+class ViewBuilderV11(ViewBuilder):
+ """OpenStack API v1.1 Image Builder"""
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ image = ViewBuilder.build(self, image_obj, detail)
+ href = self.generate_href(image_obj["id"])
+
+ image["links"] = [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }]
+
+ return image
diff --git a/nova/image/glance.py b/nova/image/glance.py
index be9805b69..fdf468594 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -220,7 +220,7 @@ def _convert_timestamps_to_datetimes(image_meta):
Returns image with known timestamp fields converted to datetime objects
"""
for attr in ['created_at', 'updated_at', 'deleted_at']:
- if image_meta.get(attr) is not None:
+ if image_meta.get(attr):
image_meta[attr] = _parse_glance_iso8601_timestamp(
image_meta[attr])
return image_meta
@@ -230,8 +230,13 @@ def _parse_glance_iso8601_timestamp(timestamp):
"""
Parse a subset of iso8601 timestamps into datetime objects
"""
- GLANCE_FMT = "%Y-%m-%dT%H:%M:%S"
- ISO_FMT = "%Y-%m-%dT%H:%M:%S.%f"
- # FIXME(sirp): Glance is not returning in ISO format, we should fix Glance
- # to do so, and then switch to parsing it here
- return datetime.datetime.strptime(timestamp, GLANCE_FMT)
+ iso_formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"]
+
+ for iso_format in iso_formats:
+ try:
+ return datetime.datetime.strptime(timestamp, iso_format)
+ except ValueError:
+ pass
+
+ raise ValueError(_("%(timestamp)s does not follow any of the "
+ "signatures: %(ISO_FORMATS)s") % locals())
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 06b05366a..d11d21dad 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -44,13 +44,10 @@ flags.DEFINE_string('dhcpbridge_flagfile',
flags.DEFINE_string('dhcp_domain',
'novalocal',
'domain to use for building the hostnames')
-
flags.DEFINE_string('networks_path', '$state_path/networks',
'Location to keep network config files')
flags.DEFINE_string('public_interface', 'eth0',
'Interface for public IP addresses')
-flags.DEFINE_string('vlan_interface', 'eth0',
- 'network device for vlans')
flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'),
'location of nova-dhcpbridge')
flags.DEFINE_string('routing_source_ip', '$my_ip',
diff --git a/nova/network/manager.py b/nova/network/manager.py
index d994f7dc8..86ee4fc00 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -73,6 +73,8 @@ flags.DEFINE_string('flat_interface', None,
flags.DEFINE_string('flat_network_dhcp_start', '10.0.0.2',
'Dhcp start for FlatDhcp')
flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks')
+flags.DEFINE_string('vlan_interface', 'eth0',
+ 'network device for vlans')
flags.DEFINE_integer('num_networks', 1, 'Number of networks to support')
flags.DEFINE_string('vpn_ip', '$my_ip',
'Public IP for the cloudpipe VPN servers')
diff --git a/nova/network/xenapi_net.py b/nova/network/xenapi_net.py
new file mode 100644
index 000000000..9a99602d9
--- /dev/null
+++ b/nova/network/xenapi_net.py
@@ -0,0 +1,85 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 Citrix Systems, Inc.
+# Copyright 2011 OpenStack LLC.
+#
+# 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.
+
+"""
+Implements vlans, bridges, and iptables rules using linux utilities.
+"""
+
+import os
+
+from nova import db
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import utils
+from nova.virt.xenapi_conn import XenAPISession
+from nova.virt.xenapi import network_utils
+
+LOG = logging.getLogger("nova.xenapi_net")
+
+FLAGS = flags.FLAGS
+
+
+def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
+ """Create a vlan and bridge unless they already exist."""
+ # Open xenapi session
+ LOG.debug("ENTERING ensure_vlan_bridge in xenapi net")
+ url = FLAGS.xenapi_connection_url
+ username = FLAGS.xenapi_connection_username
+ password = FLAGS.xenapi_connection_password
+ session = XenAPISession(url, username, password)
+ # Check whether bridge already exists
+ # Retrieve network whose name_label is "bridge"
+ network_ref = network_utils.NetworkHelper.find_network_with_name_label(
+ session,
+ bridge)
+ if network_ref == None:
+ # If bridge does not exists
+ # 1 - create network
+ description = "network for nova bridge %s" % bridge
+ network_rec = {'name_label': bridge,
+ 'name_description': description,
+ 'other_config': {}}
+ network_ref = session.call_xenapi('network.create', network_rec)
+ # 2 - find PIF for VLAN
+ expr = 'field "device" = "%s" and \
+ field "VLAN" = "-1"' % FLAGS.vlan_interface
+ pifs = session.call_xenapi('PIF.get_all_records_where', expr)
+ pif_ref = None
+ # Multiple PIF are ok: we are dealing with a pool
+ if len(pifs) == 0:
+ raise Exception(
+ _('Found no PIF for device %s') % FLAGS.vlan_interface)
+ # 3 - create vlan for network
+ for pif_ref in pifs.keys():
+ session.call_xenapi('VLAN.create',
+ pif_ref,
+ str(vlan_num),
+ network_ref)
+ else:
+ # Check VLAN tag is appropriate
+ network_rec = session.call_xenapi('network.get_record', network_ref)
+ # Retrieve PIFs from network
+ for pif_ref in network_rec['PIFs']:
+ # Retrieve VLAN from PIF
+ pif_rec = session.call_xenapi('PIF.get_record', pif_ref)
+ pif_vlan = int(pif_rec['VLAN'])
+ # Raise an exception if VLAN != vlan_num
+ if pif_vlan != vlan_num:
+ raise Exception(_("PIF %(pif_rec['uuid'])s for network "
+ "%(bridge)s has VLAN id %(pif_vlan)d. "
+ "Expected %(vlan_num)d") % locals())
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 738bdda19..57e447dce 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -20,11 +20,13 @@ Tests of the new image services, both as a service layer,
and as a WSGI layer
"""
+import copy
import json
import datetime
import os
import shutil
import tempfile
+import xml.dom.minidom as minidom
import stubout
import webob
@@ -214,12 +216,14 @@ class GlanceImageServiceTest(_BaseImageServiceTests):
class ImageControllerWithGlanceServiceTest(test.TestCase):
- """Test of the OpenStack API /images application controller"""
-
+ """
+ Test of the OpenStack API /images application controller w/Glance.
+ """
NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
NOW_API_FORMAT = "2010-10-11T10:30:22Z"
def setUp(self):
+ """Run before each test."""
super(ImageControllerWithGlanceServiceTest, self).setUp()
self.orig_image_service = FLAGS.image_service
FLAGS.image_service = 'nova.image.glance.GlanceImageService'
@@ -230,18 +234,30 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_auth(self.stubs)
fakes.stub_out_key_pair_funcs(self.stubs)
- fixtures = self._make_image_fixtures()
- fakes.stub_out_glance(self.stubs, initial_fixtures=fixtures)
+ self.fixtures = self._make_image_fixtures()
+ fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures)
def tearDown(self):
+ """Run after each test."""
self.stubs.UnsetAll()
FLAGS.image_service = self.orig_image_service
super(ImageControllerWithGlanceServiceTest, self).tearDown()
+ def _applicable_fixture(self, fixture, user_id):
+ """Determine if this fixture is applicable for given user id."""
+ is_public = fixture["is_public"]
+ try:
+ uid = int(fixture["properties"]["user_id"])
+ except KeyError:
+ uid = None
+ return uid == user_id or is_public
+
def test_get_image_index(self):
- req = webob.Request.blank('/v1.0/images')
- res = req.get_response(fakes.wsgi_app())
- image_metas = json.loads(res.body)['images']
+ request = webob.Request.blank('/v1.0/images')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
expected = [{'id': 123, 'name': 'public image'},
{'id': 124, 'name': 'queued backup'},
@@ -249,32 +265,379 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{'id': 126, 'name': 'active backup'},
{'id': 127, 'name': 'killed backup'}]
- self.assertDictListMatch(image_metas, expected)
+ self.assertDictListMatch(response_list, expected)
+
+ def test_get_image(self):
+ request = webob.Request.blank('/v1.0/images/123')
+ response = request.get_response(fakes.wsgi_app())
+
+ self.assertEqual(200, response.status_int)
+
+ actual_image = json.loads(response.body)
+
+ expected_image = {
+ "image": {
+ "id": 123,
+ "name": "public image",
+ "updated": self.NOW_API_FORMAT,
+ "created": self.NOW_API_FORMAT,
+ "status": "ACTIVE",
+ },
+ }
+
+ self.assertEqual(expected_image, actual_image)
+
+ def test_get_image_v1_1(self):
+ request = webob.Request.blank('/v1.1/images/123')
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = json.loads(response.body)
+
+ href = "http://localhost/v1.1/images/123"
+
+ expected_image = {
+ "image": {
+ "id": 123,
+ "name": "public image",
+ "updated": self.NOW_API_FORMAT,
+ "created": self.NOW_API_FORMAT,
+ "status": "ACTIVE",
+ "links": [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }],
+ },
+ }
+
+ self.assertEqual(expected_image, actual_image)
+
+ def test_get_image_xml(self):
+ request = webob.Request.blank('/v1.0/images/123')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_now = self.NOW_API_FORMAT
+ expected_image = minidom.parseString("""
+ <image id="123"
+ name="public image"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE" />
+ """ % (locals()))
+
+ self.assertEqual(expected_image.toxml(), actual_image.toxml())
+
+ def test_get_image_v1_1_xml(self):
+ request = webob.Request.blank('/v1.1/images/123')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+
+ actual_image = minidom.parseString(response.body.replace(" ", ""))
+
+ expected_href = "http://localhost/v1.1/images/123"
+ expected_now = self.NOW_API_FORMAT
+ expected_image = minidom.parseString("""
+ <image id="123"
+ name="public image"
+ updated="%(expected_now)s"
+ created="%(expected_now)s"
+ status="ACTIVE">
+ <links>
+ <link href="%(expected_href)s" rel="self"/>
+ <link href="%(expected_href)s" rel="bookmark"
+ type="application/json" />
+ <link href="%(expected_href)s" rel="bookmark"
+ type="application/xml" />
+ </links>
+ </image>
+ """.replace(" ", "") % (locals()))
+
+ self.assertEqual(expected_image.toxml(), actual_image.toxml())
+
+ def test_get_image_404_json(self):
+ request = webob.Request.blank('/v1.0/images/NonExistantImage')
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = {
+ "itemNotFound": {
+ "message": "Image not found.",
+ "code": 404,
+ },
+ }
+
+ actual = json.loads(response.body)
+
+ self.assertEqual(expected, actual)
+
+ def test_get_image_404_xml(self):
+ request = webob.Request.blank('/v1.0/images/NonExistantImage')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = minidom.parseString("""
+ <itemNotFound code="404">
+ <message>
+ Image not found.
+ </message>
+ </itemNotFound>
+ """.replace(" ", ""))
+
+ actual = minidom.parseString(response.body.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_get_image_404_v1_1_json(self):
+ request = webob.Request.blank('/v1.1/images/NonExistantImage')
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = {
+ "itemNotFound": {
+ "message": "Image not found.",
+ "code": 404,
+ },
+ }
+
+ actual = json.loads(response.body)
+
+ self.assertEqual(expected, actual)
+
+ def test_get_image_404_v1_1_xml(self):
+ request = webob.Request.blank('/v1.1/images/NonExistantImage')
+ request.accept = "application/xml"
+ response = request.get_response(fakes.wsgi_app())
+ self.assertEqual(404, response.status_int)
+
+ expected = minidom.parseString("""
+ <itemNotFound code="404">
+ <message>
+ Image not found.
+ </message>
+ </itemNotFound>
+ """.replace(" ", ""))
+
+ actual = minidom.parseString(response.body.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_get_image_index_v1_1(self):
+ request = webob.Request.blank('/v1.1/images')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ fixtures = copy.copy(self.fixtures)
+
+ for image in fixtures:
+ if not self._applicable_fixture(image, 1):
+ fixtures.remove(image)
+ continue
+
+ href = "http://localhost/v1.1/images/%s" % image["id"]
+ test_image = {
+ "id": image["id"],
+ "name": image["name"],
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/%s" % image["id"],
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }],
+ }
+ self.assertTrue(test_image in response_list)
+
+ self.assertEqual(len(response_list), len(fixtures))
def test_get_image_details(self):
- req = webob.Request.blank('/v1.0/images/detail')
- res = req.get_response(fakes.wsgi_app())
- image_metas = json.loads(res.body)['images']
-
- now = self.NOW_API_FORMAT
- expected = [
- {'id': 123, 'name': 'public image', 'updated': now,
- 'created': now, 'status': 'ACTIVE'},
- {'id': 124, 'name': 'queued backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'QUEUED'},
- {'id': 125, 'name': 'saving backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'SAVING', 'progress': 0},
- {'id': 126, 'name': 'active backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'ACTIVE'},
- {'id': 127, 'name': 'killed backup', 'serverId': 42,
- 'updated': now, 'created': now,
- 'status': 'FAILED'}
- ]
-
- self.assertDictListMatch(image_metas, expected)
+ request = webob.Request.blank('/v1.0/images/detail')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ expected = [{
+ 'id': 123,
+ 'name': 'public image',
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ },
+ {
+ 'id': 124,
+ 'name': 'queued backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'QUEUED',
+ },
+ {
+ 'id': 125,
+ 'name': 'saving backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 0,
+ },
+ {
+ 'id': 126,
+ 'name': 'active backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE'
+ },
+ {
+ 'id': 127,
+ 'name': 'killed backup', 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'FAILED',
+ }]
+
+ self.assertDictListMatch(expected, response_list)
+
+ def test_get_image_details_v1_1(self):
+ request = webob.Request.blank('/v1.1/images/detail')
+ response = request.get_response(fakes.wsgi_app())
+
+ response_dict = json.loads(response.body)
+ response_list = response_dict["images"]
+
+ expected = [{
+ 'id': 123,
+ 'name': 'public image',
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/123",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/123",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/123",
+ }],
+ },
+ {
+ 'id': 124,
+ 'name': 'queued backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'QUEUED',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/124",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/124",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/124",
+ }],
+ },
+ {
+ 'id': 125,
+ 'name': 'saving backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 0,
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/125",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/125",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/125",
+ }],
+ },
+ {
+ 'id': 126,
+ 'name': 'active backup',
+ 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/126",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/126",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/126",
+ }],
+ },
+ {
+ 'id': 127,
+ 'name': 'killed backup', 'serverId': 42,
+ 'updated': self.NOW_API_FORMAT,
+ 'created': self.NOW_API_FORMAT,
+ 'status': 'FAILED',
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v1.1/images/127",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": "http://localhost/v1.1/images/127",
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": "http://localhost/v1.1/images/127",
+ }],
+ }]
+
+ self.assertDictListMatch(expected, response_list)
def test_get_image_found(self):
req = webob.Request.blank('/v1.0/images/123')
diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py
index 21a5481bd..7ddfe377a 100644
--- a/nova/tests/db/fakes.py
+++ b/nova/tests/db/fakes.py
@@ -25,7 +25,7 @@ from nova import utils
def stub_out_db_instance_api(stubs, injected=True):
- """ Stubs out the db API for creating Instances """
+ """Stubs out the db API for creating Instances."""
INSTANCE_TYPES = {
'm1.tiny': dict(memory_mb=512,
@@ -56,27 +56,39 @@ def stub_out_db_instance_api(stubs, injected=True):
flavorid=5,
rxtx_cap=5)}
- network_fields = {
- 'id': 'test',
- 'bridge': 'xenbr0',
- 'label': 'test_network',
- 'netmask': '255.255.255.0',
- 'cidr_v6': 'fe80::a00:0/120',
- 'netmask_v6': '120',
- 'gateway': '10.0.0.1',
- 'gateway_v6': 'fe80::a00:1',
- 'broadcast': '10.0.0.255',
- 'dns': '10.0.0.2',
- 'ra_server': None,
- 'injected': injected}
-
- fixed_ip_fields = {
- 'address': '10.0.0.3',
- 'address_v6': 'fe80::a00:3',
- 'network_id': 'test'}
+ flat_network_fields = {'id': 'fake_flat',
+ 'bridge': 'xenbr0',
+ 'label': 'fake_flat_network',
+ 'netmask': '255.255.255.0',
+ 'cidr_v6': 'fe80::a00:0/120',
+ 'netmask_v6': '120',
+ 'gateway': '10.0.0.1',
+ 'gateway_v6': 'fe80::a00:1',
+ 'broadcast': '10.0.0.255',
+ 'dns': '10.0.0.2',
+ 'ra_server': None,
+ 'injected': injected}
+
+ vlan_network_fields = {'id': 'fake_vlan',
+ 'bridge': 'br111',
+ 'label': 'fake_vlan_network',
+ 'netmask': '255.255.255.0',
+ 'cidr_v6': 'fe80::a00:0/120',
+ 'netmask_v6': '120',
+ 'gateway': '10.0.0.1',
+ 'gateway_v6': 'fe80::a00:1',
+ 'broadcast': '10.0.0.255',
+ 'dns': '10.0.0.2',
+ 'ra_server': None,
+ 'vlan': 111,
+ 'injected': False}
+
+ fixed_ip_fields = {'address': '10.0.0.3',
+ 'address_v6': 'fe80::a00:3',
+ 'network_id': 'fake_flat'}
class FakeModel(object):
- """ Stubs out for model """
+ """Stubs out for model."""
def __init__(self, values):
self.values = values
@@ -96,10 +108,19 @@ def stub_out_db_instance_api(stubs, injected=True):
return INSTANCE_TYPES[name]
def fake_network_get_by_instance(context, instance_id):
+ # Even instance numbers are on vlan networks
+ if instance_id % 2 == 0:
+ return FakeModel(vlan_network_fields)
+ else:
+ return FakeModel(flat_network_fields)
return FakeModel(network_fields)
def fake_network_get_all_by_instance(context, instance_id):
- return [FakeModel(network_fields)]
+ # Even instance numbers are on vlan networks
+ if instance_id % 2 == 0:
+ return [FakeModel(vlan_network_fields)]
+ else:
+ return [FakeModel(flat_network_fields)]
def fake_instance_get_fixed_address(context, instance_id):
return FakeModel(fixed_ip_fields).address
@@ -111,6 +132,8 @@ def stub_out_db_instance_api(stubs, injected=True):
return [FakeModel(fixed_ip_fields)]
stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance)
+ stubs.Set(db, 'network_get_all_by_instance',
+ fake_network_get_all_by_instance)
stubs.Set(db, 'instance_type_get_all', fake_instance_type_get_all)
stubs.Set(db, 'instance_type_get_by_name', fake_instance_type_get_by_name)
stubs.Set(db, 'instance_get_fixed_address',
diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py
index 823c775cb..be59970c9 100644
--- a/nova/tests/fake_utils.py
+++ b/nova/tests/fake_utils.py
@@ -14,8 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""This modules stubs out functions in nova.utils
-"""
+"""This modules stubs out functions in nova.utils."""
import re
import types
@@ -42,21 +41,25 @@ def fake_execute_clear_log():
def fake_execute_set_repliers(repliers):
- """Allows the client to configure replies to commands"""
+ """Allows the client to configure replies to commands."""
global _fake_execute_repliers
_fake_execute_repliers = repliers
def fake_execute_default_reply_handler(*ignore_args, **ignore_kwargs):
- """A reply handler for commands that haven't been added to the reply
- list. Returns empty strings for stdout and stderr
+ """A reply handler for commands that haven't been added to the reply list.
+
+ Returns empty strings for stdout and stderr.
+
"""
return '', ''
def fake_execute(*cmd_parts, **kwargs):
- """This function stubs out execute, optionally executing
- a preconfigued function to return expected data
+ """This function stubs out execute.
+
+ It optionally executes a preconfigued function to return expected data.
+
"""
global _fake_execute_repliers
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
index d03aa9cc8..9d0b14613 100644
--- a/nova/tests/image/test_glance.py
+++ b/nova/tests/image/test_glance.py
@@ -55,7 +55,8 @@ class NullWriter(object):
class BaseGlanceTest(unittest.TestCase):
- NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
+ NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22"
+ NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000"
NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22)
def setUp(self):
@@ -74,6 +75,10 @@ class BaseGlanceTest(unittest.TestCase):
self.assertEqual(image_meta['updated_at'], None)
self.assertEqual(image_meta['deleted_at'], None)
+ def assertDateTimesBlank(self, image_meta):
+ self.assertEqual(image_meta['updated_at'], '')
+ self.assertEqual(image_meta['deleted_at'], '')
+
class TestGlanceImageServiceProperties(BaseGlanceTest):
def test_show_passes_through_to_client(self):
@@ -108,38 +113,72 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest):
image_meta = self.service.show(self.context, 'image1')
self.assertDateTimesEmpty(image_meta)
+ def test_show_handles_blank_datetimes(self):
+ self.client.images = self._make_blank_datetime_fixtures()
+ image_meta = self.service.show(self.context, 'image1')
+ self.assertDateTimesBlank(image_meta)
+
def test_detail_handles_none_datetimes(self):
self.client.images = self._make_none_datetime_fixtures()
image_meta = self.service.detail(self.context)[0]
self.assertDateTimesEmpty(image_meta)
+ def test_detail_handles_blank_datetimes(self):
+ self.client.images = self._make_blank_datetime_fixtures()
+ image_meta = self.service.detail(self.context)[0]
+ self.assertDateTimesBlank(image_meta)
+
def test_get_handles_none_datetimes(self):
self.client.images = self._make_none_datetime_fixtures()
writer = NullWriter()
image_meta = self.service.get(self.context, 'image1', writer)
self.assertDateTimesEmpty(image_meta)
+ def test_get_handles_blank_datetimes(self):
+ self.client.images = self._make_blank_datetime_fixtures()
+ writer = NullWriter()
+ image_meta = self.service.get(self.context, 'image1', writer)
+ self.assertDateTimesBlank(image_meta)
+
def test_show_makes_datetimes(self):
self.client.images = self._make_datetime_fixtures()
image_meta = self.service.show(self.context, 'image1')
self.assertDateTimesFilled(image_meta)
+ image_meta = self.service.show(self.context, 'image2')
+ self.assertDateTimesFilled(image_meta)
def test_detail_makes_datetimes(self):
self.client.images = self._make_datetime_fixtures()
image_meta = self.service.detail(self.context)[0]
self.assertDateTimesFilled(image_meta)
+ image_meta = self.service.detail(self.context)[1]
+ self.assertDateTimesFilled(image_meta)
def test_get_makes_datetimes(self):
self.client.images = self._make_datetime_fixtures()
writer = NullWriter()
image_meta = self.service.get(self.context, 'image1', writer)
self.assertDateTimesFilled(image_meta)
+ image_meta = self.service.get(self.context, 'image2', writer)
+ self.assertDateTimesFilled(image_meta)
def _make_datetime_fixtures(self):
- fixtures = {'image1': {'name': 'image1', 'is_public': True,
- 'created_at': self.NOW_GLANCE_FORMAT,
- 'updated_at': self.NOW_GLANCE_FORMAT,
- 'deleted_at': self.NOW_GLANCE_FORMAT}}
+ fixtures = {
+ 'image1': {
+ 'name': 'image1',
+ 'is_public': True,
+ 'created_at': self.NOW_GLANCE_FORMAT,
+ 'updated_at': self.NOW_GLANCE_FORMAT,
+ 'deleted_at': self.NOW_GLANCE_FORMAT,
+ },
+ 'image2': {
+ 'name': 'image2',
+ 'is_public': True,
+ 'created_at': self.NOW_GLANCE_OLD_FORMAT,
+ 'updated_at': self.NOW_GLANCE_OLD_FORMAT,
+ 'deleted_at': self.NOW_GLANCE_OLD_FORMAT,
+ },
+ }
return fixtures
def _make_none_datetime_fixtures(self):
@@ -148,6 +187,12 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest):
'deleted_at': None}}
return fixtures
+ def _make_blank_datetime_fixtures(self):
+ fixtures = {'image1': {'name': 'image1', 'is_public': True,
+ 'updated_at': '',
+ 'deleted_at': ''}}
+ return fixtures
+
class TestMutatorDateTimeTests(BaseGlanceTest):
"""Tests create(), update()"""
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
index 3a03159ff..958c8e3e2 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_virt.py
@@ -225,6 +225,49 @@ class LibvirtConnTestCase(test.TestCase):
self._check_xml_and_uri(instance_data, expect_kernel=True,
expect_ramdisk=True, rescue=True)
+ def test_lxc_container_and_uri(self):
+ instance_data = dict(self.test_instance)
+ self._check_xml_and_container(instance_data)
+
+ def _check_xml_and_container(self, instance):
+ user_context = context.RequestContext(project=self.project,
+ user=self.user)
+ instance_ref = db.instance_create(user_context, instance)
+ host = self.network.get_network_host(user_context.elevated())
+ network_ref = db.project_get_network(context.get_admin_context(),
+ self.project.id)
+
+ fixed_ip = {'address': self.test_ip,
+ 'network_id': network_ref['id']}
+
+ ctxt = context.get_admin_context()
+ fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip)
+ db.fixed_ip_update(ctxt, self.test_ip,
+ {'allocated': True,
+ 'instance_id': instance_ref['id']})
+
+ self.flags(libvirt_type='lxc')
+ conn = libvirt_conn.LibvirtConnection(True)
+
+ uri = conn.get_uri()
+ self.assertEquals(uri, 'lxc:///')
+
+ xml = conn.to_xml(instance_ref)
+ tree = xml_to_tree(xml)
+
+ check = [
+ (lambda t: t.find('.').get('type'), 'lxc'),
+ (lambda t: t.find('./os/type').text, 'exe'),
+ (lambda t: t.find('./devices/filesystem/target').get('dir'), '/')]
+
+ for i, (check, expected_result) in enumerate(check):
+ self.assertEqual(check(tree),
+ expected_result,
+ '%s failed common check %d' % (xml, i))
+
+ target = tree.find('./devices/filesystem/source').get('dir')
+ self.assertTrue(len(target) > 0)
+
def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel,
rescue=False):
user_context = context.RequestContext(project=self.project,
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index 36c88b020..17e3f55e9 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -14,9 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Test suite for XenAPI
-"""
+"""Test suite for XenAPI."""
import functools
import os
@@ -65,9 +63,7 @@ def stub_vm_utils_with_vdi_attached_here(function, should_return=True):
class XenAPIVolumeTestCase(test.TestCase):
- """
- Unit tests for Volume operations
- """
+ """Unit tests for Volume operations."""
def setUp(self):
super(XenAPIVolumeTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
@@ -101,7 +97,7 @@ class XenAPIVolumeTestCase(test.TestCase):
return db.volume_create(self.context, vol)
def test_create_iscsi_storage(self):
- """ This shows how to test helper classes' methods """
+ """This shows how to test helper classes' methods."""
stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
session = xenapi_conn.XenAPISession('test_url', 'root', 'test_pass')
helper = volume_utils.VolumeHelper
@@ -116,7 +112,7 @@ class XenAPIVolumeTestCase(test.TestCase):
db.volume_destroy(context.get_admin_context(), vol['id'])
def test_parse_volume_info_raise_exception(self):
- """ This shows how to test helper classes' methods """
+ """This shows how to test helper classes' methods."""
stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
session = xenapi_conn.XenAPISession('test_url', 'root', 'test_pass')
helper = volume_utils.VolumeHelper
@@ -130,7 +126,7 @@ class XenAPIVolumeTestCase(test.TestCase):
db.volume_destroy(context.get_admin_context(), vol['id'])
def test_attach_volume(self):
- """ This shows how to test Ops classes' methods """
+ """This shows how to test Ops classes' methods."""
stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
@@ -149,7 +145,7 @@ class XenAPIVolumeTestCase(test.TestCase):
check()
def test_attach_volume_raise_exception(self):
- """ This shows how to test when exceptions are raised """
+ """This shows how to test when exceptions are raised."""
stubs.stubout_session(self.stubs,
stubs.FakeSessionForVolumeFailedTests)
conn = xenapi_conn.get_connection(False)
@@ -172,9 +168,7 @@ def reset_network(*args):
class XenAPIVMTestCase(test.TestCase):
- """
- Unit tests for VM operations
- """
+ """Unit tests for VM operations."""
def setUp(self):
super(XenAPIVMTestCase, self).setUp()
self.manager = manager.AuthManager()
@@ -188,6 +182,7 @@ class XenAPIVMTestCase(test.TestCase):
instance_name_template='%d')
xenapi_fake.reset()
xenapi_fake.create_local_srs()
+ xenapi_fake.create_local_pifs()
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)
@@ -247,12 +242,12 @@ class XenAPIVMTestCase(test.TestCase):
check()
- def create_vm_record(self, conn, os_type):
+ def create_vm_record(self, conn, os_type, instance_id=1):
instances = conn.list_instances()
- self.assertEquals(instances, ['1'])
+ self.assertEquals(instances, [str(instance_id)])
# Get Nova record for VM
- vm_info = conn.get_info(1)
+ vm_info = conn.get_info(instance_id)
# Get XenAPI record for VM
vms = [rec for ref, rec
in xenapi_fake.get_all_records('VM').iteritems()
@@ -286,19 +281,19 @@ class XenAPIVMTestCase(test.TestCase):
key = 'vm-data/networking/aabbccddeeff'
xenstore_value = xenstore_data[key]
tcpip_data = ast.literal_eval(xenstore_value)
- self.assertEquals(tcpip_data, {
- 'label': 'test_network',
- 'broadcast': '10.0.0.255',
- 'ips': [{'ip': '10.0.0.3',
- 'netmask':'255.255.255.0',
- 'enabled':'1'}],
- 'ip6s': [{'ip': 'fe80::a8bb:ccff:fedd:eeff',
- 'netmask': '120',
- 'enabled': '1',
- 'gateway': 'fe80::a00:1'}],
- 'mac': 'aa:bb:cc:dd:ee:ff',
- 'dns': ['10.0.0.2'],
- 'gateway': '10.0.0.1'})
+ self.assertEquals(tcpip_data,
+ {'label': 'fake_flat_network',
+ 'broadcast': '10.0.0.255',
+ 'ips': [{'ip': '10.0.0.3',
+ 'netmask':'255.255.255.0',
+ 'enabled':'1'}],
+ 'ip6s': [{'ip': 'fe80::a8bb:ccff:fedd:eeff',
+ 'netmask': '120',
+ 'enabled': '1',
+ 'gateway': 'fe80::a00:1'}],
+ 'mac': 'aa:bb:cc:dd:ee:ff',
+ 'dns': ['10.0.0.2'],
+ 'gateway': '10.0.0.1'})
def check_vm_params_for_windows(self):
self.assertEquals(self.vm['platform']['nx'], 'true')
@@ -334,9 +329,9 @@ class XenAPIVMTestCase(test.TestCase):
def _test_spawn(self, image_id, kernel_id, ramdisk_id,
instance_type="m1.large", os_type="linux",
- check_injection=False):
+ instance_id=1, check_injection=False):
stubs.stubout_loopingcall_start(self.stubs)
- values = {'id': 1,
+ values = {'id': instance_id,
'project_id': self.project.id,
'user_id': self.user.id,
'image_id': image_id,
@@ -347,7 +342,7 @@ class XenAPIVMTestCase(test.TestCase):
'os_type': os_type}
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
- self.create_vm_record(self.conn, os_type)
+ self.create_vm_record(self.conn, os_type, instance_id)
self.check_vm_record(self.conn, check_injection)
def test_spawn_not_enough_memory(self):
@@ -468,6 +463,28 @@ class XenAPIVMTestCase(test.TestCase):
# guest agent is detected
self.assertFalse(self._tee_executed)
+ def test_spawn_vlanmanager(self):
+ self.flags(xenapi_image_service='glance',
+ network_manager='nova.network.manager.VlanManager',
+ network_driver='nova.network.xenapi_net',
+ vlan_interface='fake0')
+ # Reset network table
+ xenapi_fake.reset_table('network')
+ # Instance id = 2 will use vlan network (see db/fakes.py)
+ fake_instance_id = 2
+ network_bk = self.network
+ # Ensure we use xenapi_net driver
+ self.network = utils.import_object(FLAGS.network_manager)
+ self.network.setup_compute_network(None, fake_instance_id)
+ self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE,
+ glance_stubs.FakeGlance.IMAGE_KERNEL,
+ glance_stubs.FakeGlance.IMAGE_RAMDISK,
+ instance_id=fake_instance_id)
+ # TODO(salvatore-orlando): a complete test here would require
+ # a check for making sure the bridge for the VM's VIF is
+ # consistent with bridge specified in nova db
+ self.network = network_bk
+
def test_spawn_with_network_qos(self):
self._create_instance()
for vif_ref in xenapi_fake.get_all('VIF'):
@@ -497,7 +514,7 @@ class XenAPIVMTestCase(test.TestCase):
self.stubs.UnsetAll()
def _create_instance(self):
- """Creates and spawns a test instance"""
+ """Creates and spawns a test instance."""
stubs.stubout_loopingcall_start(self.stubs)
values = {
'id': 1,
@@ -515,9 +532,7 @@ class XenAPIVMTestCase(test.TestCase):
class XenAPIDiffieHellmanTestCase(test.TestCase):
- """
- Unit tests for Diffie-Hellman code
- """
+ """Unit tests for Diffie-Hellman code."""
def setUp(self):
super(XenAPIDiffieHellmanTestCase, self).setUp()
self.alice = SimpleDH()
@@ -541,9 +556,7 @@ class XenAPIDiffieHellmanTestCase(test.TestCase):
class XenAPIMigrateInstance(test.TestCase):
- """
- Unit test for verifying migration-related actions
- """
+ """Unit test for verifying migration-related actions."""
def setUp(self):
super(XenAPIMigrateInstance, self).setUp()
@@ -598,9 +611,7 @@ class XenAPIMigrateInstance(test.TestCase):
class XenAPIDetermineDiskImageTestCase(test.TestCase):
- """
- Unit tests for code that detects the ImageType
- """
+ """Unit tests for code that detects the ImageType."""
def setUp(self):
super(XenAPIDetermineDiskImageTestCase, self).setUp()
glance_stubs.stubout_glance_client(self.stubs,
@@ -619,9 +630,7 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase):
self.assertEqual(disk_type, dt)
def test_instance_disk(self):
- """
- If a kernel is specified then the image type is DISK (aka machine)
- """
+ """If a kernel is specified, the image type is DISK (aka machine)."""
FLAGS.xenapi_image_service = 'objectstore'
self.fake_instance.image_id = glance_stubs.FakeGlance.IMAGE_MACHINE
self.fake_instance.kernel_id = glance_stubs.FakeGlance.IMAGE_KERNEL
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
index 25e4f54a9..ddea1a1f7 100644
--- a/nova/virt/disk.py
+++ b/nova/virt/disk.py
@@ -116,6 +116,41 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
_unlink_device(device, nbd)
+def setup_container(image, container_dir=None, nbd=False):
+ """Setup the LXC container.
+
+ It will mount the loopback image to the container directory in order
+ to create the root filesystem for the container.
+
+ LXC does not support qcow2 images yet.
+ """
+ try:
+ device = _link_device(image, nbd)
+ utils.execute('sudo', 'mount', device, container_dir)
+ except Exception, exn:
+ LOG.exception(_('Failed to mount filesystem: %s'), exn)
+ _unlink_device(device, nbd)
+
+
+def destroy_container(target, instance, nbd=False):
+ """Destroy the container once it terminates.
+
+ It will umount the container that is mounted, try to find the loopback
+ device associated with the container and delete it.
+
+ LXC does not support qcow2 images yet.
+ """
+ try:
+ container_dir = '%s/rootfs' % target
+ utils.execute('sudo', 'umount', container_dir)
+ finally:
+ out, err = utils.execute('sudo', 'losetup', '-a')
+ for loop in out.splitlines():
+ if instance['name'] in loop:
+ device = loop.split(loop, ':')
+ _unlink_device(device, nbd)
+
+
def _link_device(image, nbd):
"""Link image to device using loopback or nbd"""
if nbd:
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index d74a9e85b..36d18ed95 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -2,7 +2,12 @@
<name>${name}</name>
<memory>${memory_kb}</memory>
<os>
-#if $type == 'uml'
+#if $type == 'lxc'
+ #set $disk_prefix = ''
+ #set $disk_bus = ''
+ <type>exe</type>
+ <init>/sbin/init</init>
+#else if $type == 'uml'
#set $disk_prefix = 'ubd'
#set $disk_bus = 'uml'
<type>uml</type>
@@ -44,7 +49,13 @@
</features>
<vcpu>${vcpus}</vcpu>
<devices>
-#if $getVar('rescue', False)
+#if $type == 'lxc'
+ <filesystem type='mount'>
+ <source dir='${basepath}/rootfs'/>
+ <target dir='/'/>
+ </filesystem>
+#else
+ #if $getVar('rescue', False)
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk.rescue'/>
@@ -55,18 +66,19 @@
<source file='${basepath}/disk'/>
<target dev='${disk_prefix}b' bus='${disk_bus}'/>
</disk>
-#else
+ #else
<disk type='file'>
<driver type='${driver_type}'/>
<source file='${basepath}/disk'/>
<target dev='${disk_prefix}a' bus='${disk_bus}'/>
</disk>
- #if $getVar('local', False)
- <disk type='file'>
- <driver type='${driver_type}'/>
- <source file='${basepath}/disk.local'/>
- <target dev='${disk_prefix}b' bus='${disk_bus}'/>
- </disk>
+ #if $getVar('local', False)
+ <disk type='file'>
+ <driver type='${driver_type}'/>
+ <source file='${basepath}/disk.local'/>
+ <target dev='${disk_prefix}b' bus='${disk_bus}'/>
+ </disk>
+ #end if
#end if
#end if
@@ -87,7 +99,6 @@
</filterref>
</interface>
#end for
-
<!-- The order is significant here. File must be defined first -->
<serial type="file">
<source path='${basepath}/console.log'/>
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 80eb64f3c..c144e827e 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -20,7 +20,7 @@
"""
A connection to a hypervisor through libvirt.
-Supports KVM, QEMU, UML, and XEN.
+Supports KVM, LXC, QEMU, UML, and XEN.
**Related Flags**
@@ -86,7 +86,7 @@ flags.DEFINE_string('libvirt_xml_template',
flags.DEFINE_string('libvirt_type',
'kvm',
'Libvirt domain type (valid options are: '
- 'kvm, qemu, uml, xen)')
+ 'kvm, lxc, qemu, uml, xen)')
flags.DEFINE_string('libvirt_uri',
'',
'Override the default libvirt URI (which is dependent'
@@ -266,6 +266,8 @@ class LibvirtConnection(driver.ComputeDriver):
uri = FLAGS.libvirt_uri or 'uml:///system'
elif FLAGS.libvirt_type == 'xen':
uri = FLAGS.libvirt_uri or 'xen:///'
+ elif FLAGS.libvirt_type == 'lxc':
+ uri = FLAGS.libvirt_uri or 'lxc:///'
else:
uri = FLAGS.libvirt_uri or 'qemu:///system'
return uri
@@ -344,6 +346,8 @@ class LibvirtConnection(driver.ComputeDriver):
instance_name = instance['name']
LOG.info(_('instance %(instance_name)s: deleting instance files'
' %(target)s') % locals())
+ if FLAGS.libvirt_type == 'lxc':
+ disk.destroy_container(target, instance, nbd=FLAGS.use_cow_images)
if os.path.exists(target):
shutil.rmtree(target)
@@ -625,6 +629,9 @@ class LibvirtConnection(driver.ComputeDriver):
instance['name'])
data = self._flush_xen_console(virsh_output)
fpath = self._append_to_file(data, console_log)
+ elif FLAGS.libvirt_type == 'lxc':
+ # LXC is also special
+ LOG.info(_("Unable to read LXC console"))
else:
fpath = console_log
@@ -738,6 +745,10 @@ class LibvirtConnection(driver.ComputeDriver):
f.write(libvirt_xml)
f.close()
+ if FLAGS.libvirt_type == 'lxc':
+ container_dir = '%s/rootfs' % basepath(suffix='')
+ utils.execute('mkdir', '-p', container_dir)
+
# NOTE(vish): No need add the suffix to console.log
os.close(os.open(basepath('console.log', ''),
os.O_CREAT | os.O_WRONLY, 0660))
@@ -797,12 +808,16 @@ class LibvirtConnection(driver.ComputeDriver):
if not inst['kernel_id']:
target_partition = "1"
+ if FLAGS.libvirt_type == 'lxc':
+ target_partition = None
+
key = str(inst['key_data'])
net = None
nets = []
ifc_template = open(FLAGS.injected_network_template).read()
ifc_num = -1
+ have_injected_networks = False
admin_context = context.get_admin_context()
for (network_ref, mapping) in network_info:
ifc_num += 1
@@ -810,6 +825,7 @@ class LibvirtConnection(driver.ComputeDriver):
if not 'injected' in network_ref:
continue
+ have_injected_networks = True
address = mapping['ips'][0]['ip']
address_v6 = None
if FLAGS.use_ipv6:
@@ -825,9 +841,10 @@ class LibvirtConnection(driver.ComputeDriver):
'netmask_v6': network_ref['netmask_v6']}
nets.append(net_info)
- net = str(Template(ifc_template,
- searchList=[{'interfaces': nets,
- 'use_ipv6': FLAGS.use_ipv6}]))
+ if have_injected_networks:
+ net = str(Template(ifc_template,
+ searchList=[{'interfaces': nets,
+ 'use_ipv6': FLAGS.use_ipv6}]))
if key or net:
inst_name = inst['name']
@@ -842,6 +859,11 @@ class LibvirtConnection(driver.ComputeDriver):
disk.inject_data(basepath('disk'), key, net,
partition=target_partition,
nbd=FLAGS.use_cow_images)
+
+ if FLAGS.libvirt_type == 'lxc':
+ disk.setup_container(basepath('disk'),
+ container_dir=container_dir,
+ nbd=FLAGS.use_cow_images)
except Exception as e:
# This could be a windows image, or a vmdk format disk
LOG.warn(_('instance %(inst_name)s: ignoring error injecting'
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
index 18d558058..4434dbf0b 100644
--- a/nova/virt/xenapi/fake.py
+++ b/nova/virt/xenapi/fake.py
@@ -60,8 +60,8 @@ from nova import exception
from nova import log as logging
-_CLASSES = ['host', 'network', 'session', 'SR', 'VBD',\
- 'PBD', 'VDI', 'VIF', 'VM', 'task']
+_CLASSES = ['host', 'network', 'session', 'SR', 'VBD',
+ 'PBD', 'VDI', 'VIF', 'PIF', 'VM', 'VLAN', 'task']
_db_content = {}
@@ -78,30 +78,36 @@ def reset():
for c in _CLASSES:
_db_content[c] = {}
create_host('fake')
- create_vm('fake', 'Running', is_a_template=False, is_control_domain=True)
+ create_vm('fake',
+ 'Running',
+ is_a_template=False,
+ is_control_domain=True)
+
+
+def reset_table(table):
+ if not table in _CLASSES:
+ return
+ _db_content[table] = {}
def create_host(name_label):
- return _create_object('host', {
- 'name_label': name_label,
- })
+ return _create_object('host',
+ {'name_label': name_label})
def create_network(name_label, bridge):
- return _create_object('network', {
- 'name_label': name_label,
- 'bridge': bridge,
- })
+ return _create_object('network',
+ {'name_label': name_label,
+ 'bridge': bridge})
def create_vm(name_label, status,
is_a_template=False, is_control_domain=False):
- return _create_object('VM', {
- 'name_label': name_label,
- 'power-state': status,
- 'is_a_template': is_a_template,
- 'is_control_domain': is_control_domain,
- })
+ return _create_object('VM',
+ {'name_label': name_label,
+ 'power-state': status,
+ 'is_a_template': is_a_template,
+ 'is_control_domain': is_control_domain})
def destroy_vm(vm_ref):
@@ -123,27 +129,24 @@ def destroy_vdi(vdi_ref):
def create_vdi(name_label, read_only, sr_ref, sharable):
- return _create_object('VDI', {
- 'name_label': name_label,
- 'read_only': read_only,
- 'SR': sr_ref,
- 'type': '',
- 'name_description': '',
- 'sharable': sharable,
- 'other_config': {},
- 'location': '',
- 'xenstore_data': '',
- 'sm_config': {},
- 'VBDs': {},
- })
+ return _create_object('VDI',
+ {'name_label': name_label,
+ 'read_only': read_only,
+ 'SR': sr_ref,
+ 'type': '',
+ 'name_description': '',
+ 'sharable': sharable,
+ 'other_config': {},
+ 'location': '',
+ 'xenstore_data': '',
+ 'sm_config': {},
+ 'VBDs': {}})
def create_vbd(vm_ref, vdi_ref):
- vbd_rec = {
- 'VM': vm_ref,
- 'VDI': vdi_ref,
- 'currently_attached': False,
- }
+ vbd_rec = {'VM': vm_ref,
+ 'VDI': vdi_ref,
+ 'currently_attached': False}
vbd_ref = _create_object('VBD', vbd_rec)
after_VBD_create(vbd_ref, vbd_rec)
return vbd_ref
@@ -169,19 +172,24 @@ def after_VM_create(vm_ref, vm_rec):
def create_pbd(config, host_ref, sr_ref, attached):
- return _create_object('PBD', {
- 'device-config': config,
- 'host': host_ref,
- 'SR': sr_ref,
- 'currently-attached': attached,
- })
+ return _create_object('PBD',
+ {'device-config': config,
+ 'host': host_ref,
+ 'SR': sr_ref,
+ 'currently-attached': attached})
def create_task(name_label):
- return _create_object('task', {
- 'name_label': name_label,
- 'status': 'pending',
- })
+ return _create_object('task',
+ {'name_label': name_label,
+ 'status': 'pending'})
+
+
+def create_local_pifs():
+ """Adds a PIF for each to the local database with VLAN=-1.
+ Do this one per host."""
+ for host_ref in _db_content['host'].keys():
+ _create_local_pif(host_ref)
def create_local_srs():
@@ -192,25 +200,34 @@ def create_local_srs():
def _create_local_sr(host_ref):
- sr_ref = _create_object('SR', {
- 'name_label': 'Local storage',
- 'type': 'lvm',
- 'content_type': 'user',
- 'shared': False,
- 'physical_size': str(1 << 30),
- 'physical_utilisation': str(0),
- 'virtual_allocation': str(0),
- 'other_config': {
- 'i18n-original-value-name_label': 'Local storage',
- 'i18n-key': 'local-storage',
- },
- 'VDIs': []
- })
+ sr_ref = _create_object(
+ 'SR',
+ {'name_label': 'Local storage',
+ 'type': 'lvm',
+ 'content_type': 'user',
+ 'shared': False,
+ 'physical_size': str(1 << 30),
+ 'physical_utilisation': str(0),
+ 'virtual_allocation': str(0),
+ 'other_config': {
+ 'i18n-original-value-name_label': 'Local storage',
+ 'i18n-key': 'local-storage'},
+ 'VDIs': []})
pbd_ref = create_pbd('', host_ref, sr_ref, True)
_db_content['SR'][sr_ref]['PBDs'] = [pbd_ref]
return sr_ref
+def _create_local_pif(host_ref):
+ pif_ref = _create_object('PIF',
+ {'name-label': 'Fake PIF',
+ 'MAC': '00:11:22:33:44:55',
+ 'physical': True,
+ 'VLAN': -1,
+ 'device': 'fake0',
+ 'host_uuid': host_ref})
+
+
def _create_object(table, obj):
ref = str(uuid.uuid4())
obj['uuid'] = str(uuid.uuid4())
@@ -234,6 +251,21 @@ def _create_sr(table, obj):
return sr_ref
+def _create_vlan(pif_ref, vlan_num, network_ref):
+ pif_rec = get_record('PIF', pif_ref)
+ vlan_pif_ref = _create_object('PIF',
+ {'name-label': 'Fake VLAN PIF',
+ 'MAC': '00:11:22:33:44:55',
+ 'physical': True,
+ 'VLAN': vlan_num,
+ 'device': pif_rec['device'],
+ 'host_uuid': pif_rec['host_uuid']})
+ return _create_object('VLAN',
+ {'tagged-pif': pif_ref,
+ 'untagged-pif': vlan_pif_ref,
+ 'tag': vlan_num})
+
+
def get_all(table):
return _db_content[table].keys()
@@ -292,6 +324,10 @@ class SessionBase(object):
rec['currently_attached'] = False
rec['device'] = ''
+ def PIF_get_all_records_where(self, _1, _2):
+ # TODO (salvatore-orlando): filter table on _2
+ return _db_content['PIF']
+
def VM_get_xenstore_data(self, _1, vm_ref):
return _db_content['VM'][vm_ref].get('xenstore_data', '')
@@ -302,7 +338,7 @@ class SessionBase(object):
db_ref['xenstore_data'][key] = None
def network_get_all_records_where(self, _1, _2):
- # TODO (salvatore-orlando):filter table on _2
+ # TODO (salvatore-orlando): filter table on _2
return _db_content['network']
def VM_add_to_xenstore_data(self, _1, vm_ref, key, value):
@@ -318,6 +354,9 @@ class SessionBase(object):
def host_call_plugin(*args):
return 'herp'
+ def network_get_all_records_where(self, _1, filter):
+ return self.xenapi.network.get_all_records()
+
def xenapi_request(self, methodname, params):
if methodname.startswith('login'):
self._login(methodname, params)
@@ -337,10 +376,9 @@ class SessionBase(object):
def _login(self, method, params):
self._session = str(uuid.uuid4())
- _db_content['session'][self._session] = {
- 'uuid': str(uuid.uuid4()),
- 'this_host': _db_content['host'].keys()[0],
- }
+ _db_content['session'][self._session] = \
+ {'uuid': str(uuid.uuid4()),
+ 'this_host': _db_content['host'].keys()[0]}
def _logout(self):
s = self._session
@@ -456,12 +494,16 @@ class SessionBase(object):
def _create(self, name, params):
self._check_session(params)
is_sr_create = name == 'SR.create'
+ is_vlan_create = name == 'VLAN.create'
# Storage Repositories have a different API
- expected = is_sr_create and 10 or 2
+ expected = is_sr_create and 10 or is_vlan_create and 4 or 2
self._check_arg_count(params, expected)
(cls, _) = name.split('.')
ref = is_sr_create and \
- _create_sr(cls, params) or _create_object(cls, params[1])
+ _create_sr(cls, params) or \
+ is_vlan_create and \
+ _create_vlan(params[1], params[2], params[3]) or \
+ _create_object(cls, params[1])
# Call hook to provide any fixups needed (ex. creating backrefs)
after_hook = 'after_%s_create' % cls
diff --git a/nova/virt/xenapi/network_utils.py b/nova/virt/xenapi/network_utils.py
index c0406d8f0..94d8e5199 100644
--- a/nova/virt/xenapi/network_utils.py
+++ b/nova/virt/xenapi/network_utils.py
@@ -28,11 +28,26 @@ class NetworkHelper(HelperBase):
"""
The class that wraps the helper methods together.
"""
+ @classmethod
+ def find_network_with_name_label(cls, session, name_label):
+ networks = session.call_xenapi('network.get_by_name_label', name_label)
+ if len(networks) == 1:
+ return networks[0]
+ elif len(networks) > 1:
+ raise Exception(_('Found non-unique network'
+ ' for name_label %s') % name_label)
+ else:
+ return None
@classmethod
def find_network_with_bridge(cls, session, bridge):
- """Return the network on which the bridge is attached, if found."""
- expr = 'field "bridge" = "%s"' % bridge
+ """
+ Return the network on which the bridge is attached, if found.
+ The bridge is defined in the nova db and can be found either in the
+ 'bridge' or 'name_label' fields of the XenAPI network record.
+ """
+ expr = 'field "name__label" = "%s" or ' \
+ 'field "bridge" = "%s"' % (bridge, bridge)
networks = session.call_xenapi('network.get_all_records_where', expr)
if len(networks) == 1:
return networks.keys()[0]
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 2288ea8a5..d07d60800 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -1108,11 +1108,13 @@ def _prepare_injectables(inst, networks_info):
if networks_info:
ifc_num = -1
interfaces_info = []
+ have_injected_networks = False
for (network_ref, info) in networks_info:
ifc_num += 1
if not network_ref['injected']:
continue
+ have_injected_networks = True
ip_v4 = ip_v6 = None
if 'ips' in info and len(info['ips']) > 0:
ip_v4 = info['ips'][0]
@@ -1131,7 +1133,9 @@ def _prepare_injectables(inst, networks_info):
'gateway_v6': ip_v6 and ip_v6['gateway'] or '',
'use_ipv6': FLAGS.use_ipv6}
interfaces_info.append(interface_info)
- net = str(template(template_data,
- searchList=[{'interfaces': interfaces_info,
- 'use_ipv6': FLAGS.use_ipv6}]))
+
+ if have_injected_networks:
+ net = str(template(template_data,
+ searchList=[{'interfaces': interfaces_info,
+ 'use_ipv6': FLAGS.use_ipv6}]))
return key, net
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 0235e2dc4..c96c35a6e 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -58,7 +58,7 @@ class VMOps(object):
VMHelper.XenAPI = self.XenAPI
def list_instances(self):
- """List VM instances"""
+ """List VM instances."""
# TODO(justinsb): Should we just always use the details method?
# Seems to be the same number of API calls..
vm_refs = []
@@ -69,7 +69,7 @@ class VMOps(object):
return vm_refs
def list_instances_detail(self):
- """List VM instances, returning InstanceInfo objects"""
+ """List VM instances, returning InstanceInfo objects."""
instance_infos = []
for vm_ref in self._session.get_xenapi().VM.get_all():
vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
@@ -119,11 +119,11 @@ class VMOps(object):
self._spawn(instance, vm_ref)
def spawn_rescue(self, instance):
- """Spawn a rescue instance"""
+ """Spawn a rescue instance."""
self.spawn(instance)
def _create_vm(self, instance, vdi_uuid, network_info=None):
- """Create VM instance"""
+ """Create VM instance."""
instance_name = instance.name
vm_ref = VMHelper.lookup(self._session, instance_name)
if vm_ref is not None:
@@ -180,7 +180,7 @@ class VMOps(object):
return vm_ref
def _spawn(self, instance, vm_ref):
- """Spawn a new instance"""
+ """Spawn a new instance."""
LOG.debug(_('Starting VM %s...'), vm_ref)
self._start(instance, vm_ref)
instance_name = instance.name
@@ -236,7 +236,8 @@ class VMOps(object):
return timer.start(interval=0.5, now=True)
def _get_vm_opaque_ref(self, instance_or_vm):
- """Refactored out the common code of many methods that receive either
+ """
+ Refactored out the common code of many methods that receive either
a vm name or a vm instance, and want a vm instance in return.
"""
# if instance_or_vm is a string it must be opaque ref or instance name
@@ -264,21 +265,21 @@ class VMOps(object):
return vm_ref
def _acquire_bootlock(self, vm):
- """Prevent an instance from booting"""
+ """Prevent an instance from booting."""
self._session.call_xenapi(
"VM.set_blocked_operations",
vm,
{"start": ""})
def _release_bootlock(self, vm):
- """Allow an instance to boot"""
+ """Allow an instance to boot."""
self._session.call_xenapi(
"VM.remove_from_blocked_operations",
vm,
"start")
def snapshot(self, instance, image_id):
- """Create snapshot from a running VM instance
+ """Create snapshot from a running VM instance.
:param instance: instance to be snapshotted
:param image_id: id of image to upload to
@@ -298,6 +299,7 @@ class VMOps(object):
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.
+
"""
template_vm_ref = None
try:
@@ -330,11 +332,12 @@ class VMOps(object):
return
def migrate_disk_and_power_off(self, instance, dest):
- """Copies a VHD from one host machine to another
+ """Copies a VHD from one host machine to another.
+
+ :param instance: the instance that owns the VHD in question.
+ :param dest: the destination host machine.
+ :param disk_type: values are 'primary' or 'cow'.
- :param instance: the instance that owns the VHD in question
- :param dest: the destination host machine
- :param disk_type: values are 'primary' or 'cow'
"""
vm_ref = VMHelper.lookup(self._session, instance.name)
@@ -383,7 +386,7 @@ class VMOps(object):
return {'base_copy': base_copy_uuid, 'cow': cow_uuid}
def link_disks(self, instance, base_copy_uuid, cow_uuid):
- """Links the base copy VHD to the COW via the XAPI plugin"""
+ """Links the base copy VHD to the COW via the XAPI plugin."""
vm_ref = VMHelper.lookup(self._session, instance.name)
new_base_copy_uuid = str(uuid.uuid4())
new_cow_uuid = str(uuid.uuid4())
@@ -404,7 +407,7 @@ class VMOps(object):
return new_cow_uuid
def resize_instance(self, instance, vdi_uuid):
- """Resize a running instance by changing it's RAM and disk size """
+ """Resize a running instance by changing it's RAM and disk size."""
#TODO(mdietz): this will need to be adjusted for swap later
#The new disk size must be in bytes
@@ -418,18 +421,20 @@ class VMOps(object):
LOG.debug(_("Resize instance %s complete") % (instance.name))
def reboot(self, instance):
- """Reboot VM instance"""
+ """Reboot VM instance."""
vm_ref = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.clean_reboot', vm_ref)
self._session.wait_for_task(task, instance.id)
def set_admin_password(self, instance, new_pass):
- """Set the root/admin password on the VM instance. This is done via
- an agent running on the VM. Communication between nova and the agent
- is done via writing xenstore records. Since communication is done over
- the XenAPI RPC calls, we need to encrypt the password. We're using a
- simple Diffie-Hellman class instead of the more advanced one in
- M2Crypto for compatibility with the agent code.
+ """Set the root/admin password on the VM instance.
+
+ This is done via an agent running on the VM. Communication between nova
+ and the agent is done via writing xenstore records. Since communication
+ is done over the XenAPI RPC calls, we need to encrypt the password.
+ We're using a simple Diffie-Hellman class instead of the more advanced
+ one in M2Crypto for compatibility with the agent code.
+
"""
# Need to uniquely identify this request.
transaction_id = str(uuid.uuid4())
@@ -462,11 +467,14 @@ class VMOps(object):
return resp_dict['message']
def inject_file(self, instance, path, contents):
- """Write a file to the VM instance. The path to which it is to be
- written and the contents of the file need to be supplied; both will
- be base64-encoded to prevent errors with non-ASCII characters being
- transmitted. If the agent does not support file injection, or the user
- has disabled it, a NotImplementedError will be raised.
+ """Write a file to the VM instance.
+
+ The path to which it is to be written and the contents of the file
+ need to be supplied; both will be base64-encoded to prevent errors
+ with non-ASCII characters being transmitted. If the agent does not
+ support file injection, or the user has disabled it, a
+ NotImplementedError will be raised.
+
"""
# Files/paths must be base64-encoded for transmission to agent
b64_path = base64.b64encode(path)
@@ -487,7 +495,7 @@ class VMOps(object):
return resp_dict['message']
def _shutdown(self, instance, vm_ref, hard=True):
- """Shutdown an instance"""
+ """Shutdown an instance."""
state = self.get_info(instance['name'])['state']
if state == power_state.SHUTDOWN:
instance_name = instance.name
@@ -511,11 +519,11 @@ class VMOps(object):
LOG.exception(exc)
def _shutdown_rescue(self, rescue_vm_ref):
- """Shutdown a rescue instance"""
+ """Shutdown a rescue instance."""
self._session.call_xenapi("Async.VM.hard_shutdown", rescue_vm_ref)
def _destroy_vdis(self, instance, vm_ref):
- """Destroys all VDIs associated with a VM"""
+ """Destroys all VDIs associated with a VM."""
instance_id = instance.id
LOG.debug(_("Destroying VDIs for Instance %(instance_id)s")
% locals())
@@ -532,7 +540,7 @@ class VMOps(object):
LOG.exception(exc)
def _destroy_rescue_vdis(self, rescue_vm_ref):
- """Destroys all VDIs associated with a rescued VM"""
+ """Destroys all VDIs associated with a rescued VM."""
vdi_refs = VMHelper.lookup_vm_vdis(self._session, rescue_vm_ref)
for vdi_ref in vdi_refs:
try:
@@ -541,7 +549,7 @@ class VMOps(object):
continue
def _destroy_rescue_vbds(self, rescue_vm_ref):
- """Destroys all VBDs tied to a rescue VM"""
+ """Destroys all VBDs tied to a rescue VM."""
vbd_refs = self._session.get_xenapi().VM.get_VBDs(rescue_vm_ref)
for vbd_ref in vbd_refs:
vbd_rec = self._session.get_xenapi().VBD.get_record(vbd_ref)
@@ -550,8 +558,7 @@ class VMOps(object):
VMHelper.destroy_vbd(self._session, vbd_ref)
def _destroy_kernel_ramdisk(self, instance, vm_ref):
- """
- Three situations can occur:
+ """Three situations can occur:
1. We have neither a ramdisk nor a kernel, in which case we are a
RAW image and can omit this step
@@ -561,6 +568,7 @@ class VMOps(object):
3. We have both, in which case we safely remove both the kernel
and the ramdisk.
+
"""
instance_id = instance.id
if not instance.kernel_id and not instance.ramdisk_id:
@@ -589,7 +597,7 @@ class VMOps(object):
LOG.debug(_("kernel/ramdisk files removed"))
def _destroy_vm(self, instance, vm_ref):
- """Destroys a VM record"""
+ """Destroys a VM record."""
instance_id = instance.id
try:
task = self._session.call_xenapi('Async.VM.destroy', vm_ref)
@@ -600,7 +608,7 @@ class VMOps(object):
LOG.debug(_("Instance %(instance_id)s VM destroyed") % locals())
def _destroy_rescue_instance(self, rescue_vm_ref):
- """Destroy a rescue instance"""
+ """Destroy a rescue instance."""
self._destroy_rescue_vbds(rescue_vm_ref)
self._shutdown_rescue(rescue_vm_ref)
self._destroy_rescue_vdis(rescue_vm_ref)
@@ -608,11 +616,11 @@ class VMOps(object):
self._session.call_xenapi("Async.VM.destroy", rescue_vm_ref)
def destroy(self, instance):
- """
- Destroy VM instance
+ """Destroy VM instance.
This is the method exposed by xenapi_conn.destroy(). The rest of the
destroy_* methods are internal.
+
"""
instance_id = instance.id
LOG.info(_("Destroying VM for Instance %(instance_id)s") % locals())
@@ -621,13 +629,13 @@ class VMOps(object):
def _destroy(self, instance, vm_ref, shutdown=True,
destroy_kernel_ramdisk=True):
- """
- Destroys VM instance by performing:
+ """Destroys VM instance by performing:
+
+ 1. A shutdown if requested.
+ 2. Destroying associated VDIs.
+ 3. Destroying kernel and ramdisk files (if necessary).
+ 4. Destroying that actual VM record.
- 1. A shutdown if requested
- 2. Destroying associated VDIs
- 3. Destroying kernel and ramdisk files (if necessary)
- 4. Destroying that actual VM record
"""
if vm_ref is None:
LOG.warning(_("VM is not present, skipping destroy..."))
@@ -650,35 +658,36 @@ class VMOps(object):
callback(ret)
def pause(self, instance, callback):
- """Pause VM instance"""
+ """Pause VM instance."""
vm_ref = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.pause', vm_ref)
self._wait_with_callback(instance.id, task, callback)
def unpause(self, instance, callback):
- """Unpause VM instance"""
+ """Unpause VM instance."""
vm_ref = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.unpause', vm_ref)
self._wait_with_callback(instance.id, task, callback)
def suspend(self, instance, callback):
- """suspend the specified instance"""
+ """Suspend the specified instance."""
vm_ref = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.suspend', vm_ref)
self._wait_with_callback(instance.id, task, callback)
def resume(self, instance, callback):
- """resume the specified instance"""
+ """Resume the specified instance."""
vm_ref = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.resume', vm_ref, False,
True)
self._wait_with_callback(instance.id, task, callback)
def rescue(self, instance, callback):
- """Rescue the specified instance
- - shutdown the instance VM
- - set 'bootlock' to prevent the instance from starting in rescue
- - spawn a rescue VM (the vm name-label will be instance-N-rescue)
+ """Rescue the specified instance.
+
+ - shutdown the instance VM.
+ - set 'bootlock' to prevent the instance from starting in rescue.
+ - spawn a rescue VM (the vm name-label will be instance-N-rescue).
"""
rescue_vm_ref = VMHelper.lookup(self._session,
@@ -702,10 +711,11 @@ class VMOps(object):
self._session.call_xenapi("Async.VBD.plug", rescue_vbd_ref)
def unrescue(self, instance, callback):
- """Unrescue the specified instance
- - unplug the instance VM's disk from the rescue VM
- - teardown the rescue VM
- - release the bootlock to allow the instance VM to start
+ """Unrescue the specified instance.
+
+ - unplug the instance VM's disk from the rescue VM.
+ - teardown the rescue VM.
+ - release the bootlock to allow the instance VM to start.
"""
rescue_vm_ref = VMHelper.lookup(self._session,
@@ -723,9 +733,11 @@ class VMOps(object):
self._start(instance, original_vm_ref)
def poll_rescued_instances(self, timeout):
- """Look for expirable rescued instances
+ """Look for expirable rescued instances.
+
- forcibly exit rescue mode for any instances that have been
in rescue mode for >= the provided timeout
+
"""
last_ran = self.poll_rescue_last_ran
if not last_ran:
@@ -761,30 +773,30 @@ class VMOps(object):
False)
def get_info(self, instance):
- """Return data about VM instance"""
+ """Return data about VM instance."""
vm_ref = self._get_vm_opaque_ref(instance)
vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
return VMHelper.compile_info(vm_rec)
def get_diagnostics(self, instance):
- """Return data about VM diagnostics"""
+ """Return data about VM diagnostics."""
vm_ref = self._get_vm_opaque_ref(instance)
vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
return VMHelper.compile_diagnostics(self._session, vm_rec)
def get_console_output(self, instance):
- """Return snapshot of console"""
+ """Return snapshot of console."""
# TODO: implement this to fix pylint!
return 'FAKE CONSOLE OUTPUT of instance'
def get_ajax_console(self, instance):
- """Return link to instance's ajax console"""
+ """Return link to instance's ajax console."""
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
# TODO(tr3buchet) - remove this function after nova multi-nic
def _get_network_info(self, instance):
- """creates network info list for instance"""
+ """Creates network info list for instance."""
admin_context = context.get_admin_context()
IPs = db.fixed_ip_get_all_by_instance(admin_context,
instance['id'])
@@ -826,7 +838,7 @@ class VMOps(object):
def inject_network_info(self, instance, vm_ref, network_info):
"""
Generate the network info and make calls to place it into the
- xenstore and the xenstore param list
+ xenstore and the xenstore param list.
"""
logging.debug(_("injecting network info to xs for vm: |%s|"), vm_ref)
@@ -847,7 +859,7 @@ class VMOps(object):
pass
def create_vifs(self, vm_ref, network_info):
- """Creates vifs for an instance"""
+ """Creates vifs for an instance."""
logging.debug(_("creating vif(s) for vm: |%s|"), vm_ref)
# this function raises if vm_ref is not a vm_opaque_ref
@@ -858,8 +870,8 @@ class VMOps(object):
bridge = network['bridge']
rxtx_cap = info.pop('rxtx_cap')
network_ref = \
- NetworkHelper.find_network_with_bridge(self._session, bridge)
-
+ NetworkHelper.find_network_with_bridge(self._session,
+ bridge)
VMHelper.create_vif(self._session, vm_ref, network_ref,
mac_address, device, rxtx_cap)
@@ -872,7 +884,8 @@ class VMOps(object):
args, vm_ref)
def list_from_xenstore(self, vm, path):
- """Runs the xenstore-ls command to get a listing of all records
+ """
+ Runs the xenstore-ls command to get a listing of all records
from 'path' downward. Returns a dict with the sub-paths as keys,
and the value stored in those paths as values. If nothing is
found at that path, returns None.
@@ -881,7 +894,8 @@ class VMOps(object):
return json.loads(ret)
def read_from_xenstore(self, vm, path):
- """Returns the value stored in the xenstore record for the given VM
+ """
+ Returns the value stored in the xenstore record for the given VM
at the specified location. A XenAPIPlugin.PluginError will be raised
if any error is encountered in the read process.
"""
@@ -897,7 +911,8 @@ class VMOps(object):
return ret
def write_to_xenstore(self, vm, path, value):
- """Writes the passed value to the xenstore record for the given VM
+ """
+ Writes the passed value to the xenstore record for the given VM
at the specified location. A XenAPIPlugin.PluginError will be raised
if any error is encountered in the write process.
"""
@@ -905,7 +920,8 @@ class VMOps(object):
{'value': json.dumps(value)})
def clear_xenstore(self, vm, path):
- """Deletes the VM's xenstore record for the specified path.
+ """
+ Deletes the VM's xenstore record for the specified path.
If there is no such record, the request is ignored.
"""
self._make_xenstore_call('delete_record', vm, path)
@@ -922,7 +938,8 @@ class VMOps(object):
def _make_plugin_call(self, plugin, method, vm, path, addl_args=None,
vm_ref=None):
- """Abstracts out the process of calling a method of a xenapi plugin.
+ """
+ Abstracts out the process of calling a method of a xenapi plugin.
Any errors raised by the plugin will in turn raise a RuntimeError here.
"""
instance_id = vm.id
@@ -952,7 +969,8 @@ class VMOps(object):
return ret
def add_to_xenstore(self, vm, path, key, value):
- """Adds the passed key/value pair to the xenstore record for
+ """
+ Adds the passed key/value pair to the xenstore record for
the given VM at the specified location. A XenAPIPlugin.PluginError
will be raised if any error is encountered in the write process.
"""
@@ -965,7 +983,8 @@ class VMOps(object):
self.write_to_xenstore(vm, path, current)
def remove_from_xenstore(self, vm, path, key_or_keys):
- """Takes either a single key or a list of keys and removes
+ """
+ Takes either a single key or a list of keys and removes
them from the xenstoreirecord data for the given VM.
If the key doesn't exist, the request is ignored.
"""
@@ -992,7 +1011,8 @@ class VMOps(object):
###### names to distinguish them. (dabo)
########################################################################
def read_partial_from_param_xenstore(self, instance_or_vm, key_prefix):
- """Returns a dict of all the keys in the xenstore parameter record
+ """
+ Returns a dict of all the keys in the xenstore parameter record
for the given instance that begin with the key_prefix.
"""
data = self.read_from_param_xenstore(instance_or_vm)
@@ -1003,7 +1023,8 @@ class VMOps(object):
return data
def read_from_param_xenstore(self, instance_or_vm, keys=None):
- """Returns the xenstore parameter record data for the specified VM
+ """
+ Returns the xenstore parameter record data for the specified VM
instance as a dict. Accepts an optional key or list of keys; if a
value for 'keys' is passed, the returned dict is filtered to only
return the values for those keys.
@@ -1025,9 +1046,11 @@ class VMOps(object):
return ret
def add_to_param_xenstore(self, instance_or_vm, key, val):
- """Takes a key/value pair and adds it to the xenstore parameter
+ """
+ Takes a key/value pair and adds it to the xenstore parameter
record for the given vm instance. If the key exists in xenstore,
- it is overwritten"""
+ it is overwritten
+ """
vm_ref = self._get_vm_opaque_ref(instance_or_vm)
self.remove_from_param_xenstore(instance_or_vm, key)
jsonval = json.dumps(val)
@@ -1035,7 +1058,8 @@ class VMOps(object):
(vm_ref, key, jsonval))
def write_to_param_xenstore(self, instance_or_vm, mapping):
- """Takes a dict and writes each key/value pair to the xenstore
+ """
+ Takes a dict and writes each key/value pair to the xenstore
parameter record for the given vm instance. Any existing data for
those keys is overwritten.
"""
@@ -1043,7 +1067,8 @@ class VMOps(object):
self.add_to_param_xenstore(instance_or_vm, k, v)
def remove_from_param_xenstore(self, instance_or_vm, key_or_keys):
- """Takes either a single key or a list of keys and removes
+ """
+ Takes either a single key or a list of keys and removes
them from the xenstore parameter record data for the given VM.
If the key doesn't exist, the request is ignored.
"""
@@ -1069,7 +1094,8 @@ def _runproc(cmd):
class SimpleDH(object):
- """This class wraps all the functionality needed to implement
+ """
+ This class wraps all the functionality needed to implement
basic Diffie-Hellman-Merkle key exchange in Python. It features
intelligent defaults for the prime and base numbers needed for the
calculation, while allowing you to supply your own. It requires that
@@ -1078,7 +1104,8 @@ class SimpleDH(object):
is not available, a RuntimeError will be raised.
"""
def __init__(self, prime=None, base=None, secret=None):
- """You can specify the values for prime and base if you wish;
+ """
+ You can specify the values for prime and base if you wish;
otherwise, reasonable default values will be used.
"""
if prime is None: