diff options
| author | Justin Santa Barbara <justin@fathomdb.com> | 2011-03-29 19:09:42 -0700 |
|---|---|---|
| committer | Justin Santa Barbara <justin@fathomdb.com> | 2011-03-29 19:09:42 -0700 |
| commit | 93b43cfcaeffa93b2f8ce50f473840c77be532c9 (patch) | |
| tree | ab3c9f97697c157be300b58ee702e8f4ffab7fb4 | |
| parent | 2315682856f420ff0b781bead142e1aff82071a4 (diff) | |
| parent | e5f108058f9b085571330dff3c3e3e3e57d2e5ed (diff) | |
Merged with trunk
| -rw-r--r-- | Authors | 1 | ||||
| -rwxr-xr-x | bin/nova-dhcpbridge | 4 | ||||
| -rwxr-xr-x | bin/nova-manage | 11 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 11 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 307 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 98 | ||||
| -rw-r--r-- | nova/image/glance.py | 17 | ||||
| -rw-r--r-- | nova/network/linux_net.py | 3 | ||||
| -rw-r--r-- | nova/network/manager.py | 2 | ||||
| -rw-r--r-- | nova/network/xenapi_net.py | 85 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_images.py | 425 | ||||
| -rw-r--r-- | nova/tests/db/fakes.py | 65 | ||||
| -rw-r--r-- | nova/tests/fake_utils.py | 17 | ||||
| -rw-r--r-- | nova/tests/image/test_glance.py | 55 | ||||
| -rw-r--r-- | nova/tests/test_virt.py | 43 | ||||
| -rw-r--r-- | nova/tests/test_xenapi.py | 99 | ||||
| -rw-r--r-- | nova/virt/disk.py | 35 | ||||
| -rw-r--r-- | nova/virt/libvirt.xml.template | 31 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 32 | ||||
| -rw-r--r-- | nova/virt/xenapi/fake.py | 172 | ||||
| -rw-r--r-- | nova/virt/xenapi/network_utils.py | 19 | ||||
| -rw-r--r-- | nova/virt/xenapi/vm_utils.py | 10 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 189 |
23 files changed, 1223 insertions, 508 deletions
@@ -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: |
