diff options
| author | Ryu Ishimoto <ryu@midokura.jp> | 2011-08-16 14:20:09 +0900 |
|---|---|---|
| committer | Ryu Ishimoto <ryu@midokura.jp> | 2011-08-16 14:20:09 +0900 |
| commit | 9d6b9c01a5652cea1aa51aa56eafada92fa82f7a (patch) | |
| tree | 82e2f37e43dcc3c2eeca0da24326fa3116eae5ea | |
| parent | 7407a1a86c4039bdc541e9a26cc68c9c93f49bc3 (diff) | |
| parent | ea53d0f37a4f478ffbe18516f99ca26192117e80 (diff) | |
| download | nova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.tar.gz nova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.tar.xz nova-9d6b9c01a5652cea1aa51aa56eafada92fa82f7a.zip | |
Merged trunk
121 files changed, 8042 insertions, 1683 deletions
@@ -18,6 +18,8 @@ <devin.carlen@gmail.com> <devcamcar@illian.local> <ewan.mellor@citrix.com> <emellor@silver> <itoumsn@nttdata.co.jp> <itoumsn@shayol> +<jake@ansolabs.com> <jake@markupisart.com> +<jake@ansolabs.com> <admin@jakedahn.com> <jaypipes@gmail.com> <jpipes@serialcoder> <jmckenty@gmail.com> <jmckenty@joshua-mckentys-macbook-pro.local> <jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com> @@ -27,6 +27,7 @@ David Pravec <David.Pravec@danix.org> Dean Troyer <dtroyer@gmail.com> Devendra Modium <dmodium@isi.edu> Devin Carlen <devin.carlen@gmail.com> +Donal Lafferty <donal.lafferty@citrix.com> Ed Leafe <ed@leafe.com> Eldar Nugaev <reldan@oscloud.ru> Eric Day <eday@oddments.org> @@ -37,6 +38,7 @@ Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp> Hisaki Ohara <hisaki.ohara@intel.com> Ilya Alekseyev <ilyaalekseyev@acm.org> Isaku Yamahata <yamahata@valinux.co.jp> +Jake Dahn <jake@ansolabs.com> Jason Cannavale <jason.cannavale@rackspace.com> Jason Koelker <jason@koelker.net> Jay Pipes <jaypipes@gmail.com> @@ -57,6 +59,7 @@ Joshua McKenty <jmckenty@gmail.com> Justin Santa Barbara <justin@fathomdb.com> Justin Shepherd <jshepher@rackspace.com> Kei Masumoto <masumotok@nttdata.co.jp> +masumoto<masumotok@nttdata.co.jp> Ken Pepple <ken.pepple@gmail.com> Kevin Bringard <kbringard@attinteractive.com> Kevin L. Mitchell <kevin.mitchell@rackspace.com> @@ -102,6 +105,7 @@ Tushar Patil <tushar.vitthal.patil@gmail.com> Vasiliy Shlykov <vash@vasiliyshlykov.org> Vishvananda Ishaya <vishvananda@gmail.com> Vivek Y S <vivek.ys@gmail.com> +Vladimir Popovski <vladimir@zadarastorage.com> William Wolf <throughnothing@gmail.com> Yoshiaki Tamura <yoshi@midokura.jp> Youcef Laribi <Youcef.Laribi@eu.citrix.com> diff --git a/MANIFEST.in b/MANIFEST.in index 421cd806a..883aba8a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,7 @@ graft bzrplugins graft contrib graft po graft plugins +graft nova/api/openstack/schemas include nova/api/openstack/notes.txt include nova/auth/*.schema include nova/auth/novarc.template diff --git a/bin/clear_rabbit_queues b/bin/clear_rabbit_queues new file mode 100755 index 000000000..7a000e5d8 --- /dev/null +++ b/bin/clear_rabbit_queues @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Admin/debug script to wipe rabbitMQ (AMQP) queues nova uses. + This can be used if you need to change durable options on queues, + or to wipe all messages in the queue system if things are in a + serious bad way. + +""" + +import datetime +import gettext +import os +import sys +import time + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')): + sys.path.insert(0, POSSIBLE_TOPDIR) + +gettext.install('nova', unicode=1) + + +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import rpc +from nova import utils + + +FLAGS = flags.FLAGS +flags.DEFINE_boolean('delete_exchange', False, 'delete nova exchange too.') + + +def delete_exchange(exch): + conn = rpc.create_connection() + x = conn.get_channel() + x.exchange_delete(exch) + + +def delete_queues(queues): + conn = rpc.create_connection() + x = conn.get_channel() + for q in queues: + x.queue_delete(q) + +if __name__ == '__main__': + utils.default_flagfile() + args = flags.FLAGS(sys.argv) + logging.setup() + delete_queues(args[1:]) + if FLAGS.delete_exchange: + delete_exchange(FLAGS.control_exchange) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 325642d52..621222d8f 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -53,7 +53,7 @@ flags.DEFINE_string('dnsmasq_interface', 'br0', 'Default Dnsmasq interface') LOG = logging.getLogger('nova.dhcpbridge') -def add_lease(mac, ip_address, _hostname, _interface): +def add_lease(mac, ip_address, _interface): """Set the IP that was assigned by the DHCP server.""" if FLAGS.fake_rabbit: LOG.debug(_("leasing ip")) @@ -67,13 +67,13 @@ def add_lease(mac, ip_address, _hostname, _interface): "args": {"address": ip_address}}) -def old_lease(mac, ip_address, hostname, interface): +def old_lease(mac, ip_address, interface): """Update just as add lease.""" - LOG.debug(_("Adopted old lease or got a change of mac/hostname")) - add_lease(mac, ip_address, hostname, interface) + LOG.debug(_("Adopted old lease or got a change of mac")) + add_lease(mac, ip_address, interface) -def del_lease(mac, ip_address, _hostname, _interface): +def del_lease(mac, ip_address, _interface): """Called when a lease expires.""" if FLAGS.fake_rabbit: LOG.debug(_("releasing ip")) @@ -115,11 +115,10 @@ def main(): if action in ['add', 'del', 'old']: mac = argv[2] ip = argv[3] - hostname = argv[4] - msg = _("Called %(action)s for mac %(mac)s with ip %(ip)s and" - " hostname %(hostname)s on interface %(interface)s") % locals() + msg = _("Called %(action)s for mac %(mac)s with ip %(ip)s" + " on interface %(interface)s") % locals() LOG.debug(msg) - globals()[action + '_lease'](mac, ip, hostname, interface) + globals()[action + '_lease'](mac, ip, interface) else: print init_leases(interface) diff --git a/bin/nova-import-canonical-imagestore b/bin/nova-import-canonical-imagestore deleted file mode 100755 index 404ae37f4..000000000 --- a/bin/nova-import-canonical-imagestore +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" - Download images from Canonical Image Store -""" - -import gettext -import json -import os -import tempfile -import shutil -import subprocess -import sys -import urllib2 - -# If ../nova/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) - -gettext.install('nova', unicode=1) - -from nova import flags -from nova import log as logging -from nova import utils -from nova.objectstore import image - -FLAGS = flags.FLAGS - -API_URL = 'https://imagestore.canonical.com/api/dashboard' - - -def get_images(): - """Get a list of the images from the imagestore URL.""" - images = json.load(urllib2.urlopen(API_URL))['images'] - images = [img for img in images if img['title'].find('amd64') > -1] - return images - - -def download(img): - """Download an image to the local filesystem.""" - # FIXME(ja): add checksum/signature checks - tempdir = tempfile.mkdtemp(prefix='cis-') - - kernel_id = None - ramdisk_id = None - - for f in img['files']: - if f['kind'] == 'kernel': - dest = os.path.join(tempdir, 'kernel') - subprocess.call(['curl', '--fail', f['url'], '-o', dest]) - kernel_id = image.Image.add(dest, - description='kernel/' + img['title'], kernel=True) - - for f in img['files']: - if f['kind'] == 'ramdisk': - dest = os.path.join(tempdir, 'ramdisk') - subprocess.call(['curl', '--fail', f['url'], '-o', dest]) - ramdisk_id = image.Image.add(dest, - description='ramdisk/' + img['title'], ramdisk=True) - - for f in img['files']: - if f['kind'] == 'image': - dest = os.path.join(tempdir, 'image') - subprocess.call(['curl', '--fail', f['url'], '-o', dest]) - ramdisk_id = image.Image.add(dest, - description=img['title'], kernel=kernel_id, ramdisk=ramdisk_id) - - shutil.rmtree(tempdir) - - -def main(): - """Main entry point.""" - utils.default_flagfile() - argv = FLAGS(sys.argv) - logging.setup() - images = get_images() - - if len(argv) == 2: - for img in images: - if argv[1] == 'all' or argv[1] == img['title']: - download(img) - else: - print 'usage: %s (title|all)' - print 'available images:' - for img in images: - print img['title'] - -if __name__ == '__main__': - main() diff --git a/bin/nova-manage b/bin/nova-manage index f272351c2..3959a236e 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -592,6 +592,31 @@ class FixedIpCommands(object): fixed_ip['address'], mac_address, hostname, host) + @args('--address', dest="address", metavar='<ip address>', + help='IP address') + def reserve(self, address): + """Mark fixed ip as reserved + arguments: address""" + self._set_reserved(address, True) + + @args('--address', dest="address", metavar='<ip address>', + help='IP address') + def unreserve(self, address): + """Mark fixed ip as free to use + arguments: address""" + self._set_reserved(address, False) + + def _set_reserved(self, address, reserved): + ctxt = context.get_admin_context() + + try: + fixed_ip = db.fixed_ip_get_by_address(ctxt, address) + db.fixed_ip_update(ctxt, fixed_ip['address'], + {'reserved': reserved}) + except exception.NotFound as ex: + print "error: %s" % ex + sys.exit(2) + class FloatingIpCommands(object): """Class for managing floating ip.""" @@ -701,8 +726,7 @@ class NetworkCommands(object): network_size = FLAGS.network_size subnet = 32 - int(math.log(network_size, 2)) oversize_msg = _('Subnet(s) too large, defaulting to /%s.' - ' To override, specify network_size flag.' - ) % subnet + ' To override, specify network_size flag.') % subnet print oversize_msg else: network_size = fixnet.size @@ -810,11 +834,13 @@ class VmCommands(object): instance['availability_zone'], instance['launch_index']) - @args('--ec2_id', dest='ec2_id', metavar='<ec2 id>', help='EC2 ID') - @args('--dest', dest='dest', metavar='<Destanation>', - help='destanation node') - def live_migration(self, ec2_id, dest): - """Migrates a running instance to a new machine.""" + def _migration(self, ec2_id, dest, block_migration=False): + """Migrates a running instance to a new machine. + :param ec2_id: instance id which comes from euca-describe-instance. + :param dest: destination host name. + :param block_migration: if True, do block_migration. + + """ ctxt = context.get_admin_context() instance_id = ec2utils.ec2_id_to_id(ec2_id) @@ -835,11 +861,28 @@ class VmCommands(object): {"method": "live_migration", "args": {"instance_id": instance_id, "dest": dest, - "topic": FLAGS.compute_topic}}) + "topic": FLAGS.compute_topic, + "block_migration": block_migration}}) print _('Migration of %s initiated.' 'Check its progress using euca-describe-instances.') % ec2_id + @args('--ec2_id', dest='ec2_id', metavar='<ec2 id>', help='EC2 ID') + @args('--dest', dest='dest', metavar='<Destanation>', + help='destanation node') + def live_migration(self, ec2_id, dest): + """Migrates a running instance to a new machine.""" + + self._migration(ec2_id, dest) + + @args('--ec2_id', dest='ec2_id', metavar='<ec2 id>', help='EC2 ID') + @args('--dest', dest='dest', metavar='<Destanation>', + help='destanation node') + def block_migration(self, ec2_id, dest): + """Migrates a running instance to a new machine with storage data.""" + + self._migration(ec2_id, dest, True) + class ServiceCommands(object): """Enable and disable running services""" @@ -858,6 +901,14 @@ class ServiceCommands(object): services = [s for s in services if s['host'] == host] if service: services = [s for s in services if s['binary'] == service] + print_format = "%-16s %-36s %-16s %-10s %-5s %-10s" + print print_format % ( + _('Binary'), + _('Host'), + _('Zone'), + _('Status'), + _('State'), + _('Updated_At')) for svc in services: delta = now - (svc['updated_at'] or svc['created_at']) alive = (delta.seconds <= 15) @@ -865,9 +916,9 @@ class ServiceCommands(object): active = 'enabled' if svc['disabled']: active = 'disabled' - print "%-10s %-10s %-8s %s %s" % (svc['host'], svc['binary'], - active, art, - svc['updated_at']) + print print_format % (svc['binary'], svc['host'], + svc['availability_zone'], active, art, + svc['updated_at']) @args('--host', dest='host', metavar='<host>', help='Host') @args('--service', dest='service', metavar='<service>', @@ -913,9 +964,19 @@ class ServiceCommands(object): mem_u = result['resource']['memory_mb_used'] hdd_u = result['resource']['local_gb_used'] + cpu_sum = 0 + mem_sum = 0 + hdd_sum = 0 print 'HOST\t\t\tPROJECT\t\tcpu\tmem(mb)\tdisk(gb)' print '%s(total)\t\t\t%s\t%s\t%s' % (host, cpu, mem, hdd) - print '%s(used)\t\t\t%s\t%s\t%s' % (host, cpu_u, mem_u, hdd_u) + print '%s(used_now)\t\t\t%s\t%s\t%s' % (host, cpu_u, mem_u, hdd_u) + for p_id, val in result['usage'].items(): + cpu_sum += val['vcpus'] + mem_sum += val['memory_mb'] + hdd_sum += val['local_gb'] + print '%s(used_max)\t\t\t%s\t%s\t%s' % (host, cpu_sum, + mem_sum, hdd_sum) + for p_id, val in result['usage'].items(): print '%s\t\t%s\t\t%s\t%s\t%s' % (host, p_id, @@ -1095,10 +1156,12 @@ class InstanceTypeCommands(object): @args('--name', dest='name', metavar='<name>', help='Name of instance type/flavor') - def delete(self, name, purge=None): + @args('--purge', action="store_true", dest='purge', default=False, + help='purge record from database') + def delete(self, name, purge): """Marks instance types / flavors as deleted""" try: - if purge == "--purge": + if purge: instance_types.purge(name) verb = "purged" else: @@ -1235,11 +1298,12 @@ class ImageCommands(object): is_public, architecture) def _lookup(self, old_image_id): + elevated = context.get_admin_context() try: internal_id = ec2utils.ec2_id_to_id(old_image_id) - image = self.image_service.show(context, internal_id) + image = self.image_service.show(elevated, internal_id) except (exception.InvalidEc2Id, exception.ImageNotFound): - image = self.image_service.show_by_name(context, old_image_id) + image = self.image_service.show_by_name(elevated, old_image_id) return image['id'] def _old_to_new(self, old): diff --git a/nova/api/direct.py b/nova/api/direct.py index 139c46d63..fdd2943d2 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -48,6 +48,7 @@ import nova.api.openstack.wsgi # Global storage for registering modules. ROUTES = {} + def register_service(path, handle): """Register a service handle at a given path. diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 804e54ef9..96df97393 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -354,6 +354,14 @@ class Executor(wsgi.Application): LOG.debug(_('KeyPairExists raised: %s'), unicode(ex), context=context) return self._error(req, context, type(ex).__name__, unicode(ex)) + except exception.InvalidParameterValue as ex: + LOG.debug(_('InvalidParameterValue raised: %s'), unicode(ex), + context=context) + return self._error(req, context, type(ex).__name__, unicode(ex)) + except exception.InvalidPortRange as ex: + LOG.debug(_('InvalidPortRange raised: %s'), unicode(ex), + context=context) + return self._error(req, context, type(ex).__name__, unicode(ex)) except Exception as ex: extra = {'environment': req.environ} LOG.exception(_('Unexpected error raised: %s'), unicode(ex), diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 0294c09c5..87bba58c3 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -25,11 +25,13 @@ datastore. import base64 import netaddr import os -import urllib +import re +import shutil import tempfile import time -import shutil +import urllib +from nova import block_device from nova import compute from nova import context @@ -78,6 +80,10 @@ def _gen_key(context, user_id, key_name): # TODO(yamahata): hypervisor dependent default device name _DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1' +_DEFAULT_MAPPINGS = {'ami': 'sda1', + 'ephemeral0': 'sda2', + 'root': _DEFAULT_ROOT_DEVICE_NAME, + 'swap': 'sda3'} def _parse_block_device_mapping(bdm): @@ -105,7 +111,7 @@ def _parse_block_device_mapping(bdm): def _properties_get_mappings(properties): - return ec2utils.mappings_prepend_dev(properties.get('mappings', [])) + return block_device.mappings_prepend_dev(properties.get('mappings', [])) def _format_block_device_mapping(bdm): @@ -144,8 +150,7 @@ def _format_mappings(properties, result): """Format multiple BlockDeviceMappingItemType""" mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} for m in _properties_get_mappings(properties) - if (m['virtual'] == 'swap' or - m['virtual'].startswith('ephemeral'))] + if block_device.is_swap_or_ephemeral(m['virtual'])] block_device_mapping = [_format_block_device_mapping(bdm) for bdm in properties.get('block_device_mapping', [])] @@ -208,8 +213,9 @@ class CloudController(object): def _get_mpi_data(self, context, project_id): result = {} + search_opts = {'project_id': project_id} for instance in self.compute_api.get_all(context, - project_id=project_id): + search_opts=search_opts): if instance['fixed_ips']: line = '%s slots=%d' % (instance['fixed_ips'][0]['address'], instance['vcpus']) @@ -233,10 +239,39 @@ class CloudController(object): state = 'available' return image['properties'].get('image_state', state) + def _format_instance_mapping(self, ctxt, instance_ref): + root_device_name = instance_ref['root_device_name'] + if root_device_name is None: + return _DEFAULT_MAPPINGS + + mappings = {} + mappings['ami'] = block_device.strip_dev(root_device_name) + mappings['root'] = root_device_name + + # 'ephemeralN' and 'swap' + for bdm in db.block_device_mapping_get_all_by_instance( + ctxt, instance_ref['id']): + if (bdm['volume_id'] or bdm['snapshot_id'] or bdm['no_device']): + continue + + virtual_name = bdm['virtual_name'] + if not virtual_name: + continue + + if block_device.is_swap_or_ephemeral(virtual_name): + mappings[virtual_name] = bdm['device_name'] + + return mappings + def get_metadata(self, address): ctxt = context.get_admin_context() - instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address) - if instance_ref is None: + search_opts = {'fixed_ip': address} + try: + instance_ref = self.compute_api.get_all(ctxt, + search_opts=search_opts) + except exception.NotFound: + instance_ref = None + if not instance_ref: return None # This ensures that all attributes of the instance @@ -259,18 +294,14 @@ class CloudController(object): security_groups = db.security_group_get_by_instance(ctxt, instance_ref['id']) security_groups = [x['name'] for x in security_groups] + mappings = self._format_instance_mapping(ctxt, instance_ref) data = { - 'user-data': base64.b64decode(instance_ref['user_data']), + 'user-data': self._format_user_data(instance_ref), 'meta-data': { 'ami-id': image_ec2_id, 'ami-launch-index': instance_ref['launch_index'], 'ami-manifest-path': 'FIXME', - 'block-device-mapping': { - # TODO(vish): replace with real data - 'ami': 'sda1', - 'ephemeral0': 'sda2', - 'root': _DEFAULT_ROOT_DEVICE_NAME, - 'swap': 'sda3'}, + 'block-device-mapping': mappings, 'hostname': hostname, 'instance-action': 'none', 'instance-id': ec2_id, @@ -765,6 +796,22 @@ class CloudController(object): return source_project_id def create_security_group(self, context, group_name, group_description): + if not re.match('^[a-zA-Z0-9_\- ]+$', str(group_name)): + # Some validation to ensure that values match API spec. + # - Alphanumeric characters, spaces, dashes, and underscores. + # TODO(Daviey): LP: #813685 extend beyond group_name checking, and + # probably create a param validator that can be used elsewhere. + err = _("Value (%s) for parameter GroupName is invalid." + " Content limited to Alphanumeric characters, " + "spaces, dashes, and underscores.") % group_name + # err not that of master ec2 implementation, as they fail to raise. + raise exception.InvalidParameterValue(err=err) + + if len(str(group_name)) > 255: + err = _("Value (%s) for parameter GroupName is invalid." + " Length exceeds maximum of 255.") % group_name + raise exception.InvalidParameterValue(err=err) + LOG.audit(_("Create Security Group %s"), group_name, context=context) self.compute_api.ensure_default_security_group(context) if db.security_group_exists(context, context.project_id, group_name): @@ -948,19 +995,113 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} - def _convert_to_set(self, lst, label): + @staticmethod + def _convert_to_set(lst, label): if lst is None or lst == []: return None if not isinstance(lst, list): lst = [lst] return [{label: x} for x in lst] + def _format_kernel_id(self, instance_ref, result, key): + kernel_id = instance_ref['kernel_id'] + if kernel_id is None: + return + result[key] = self.image_ec2_id(instance_ref['kernel_id'], 'aki') + + def _format_ramdisk_id(self, instance_ref, result, key): + ramdisk_id = instance_ref['ramdisk_id'] + if ramdisk_id is None: + return + result[key] = self.image_ec2_id(instance_ref['ramdisk_id'], 'ari') + + @staticmethod + def _format_user_data(instance_ref): + return base64.b64decode(instance_ref['user_data']) + + def describe_instance_attribute(self, context, instance_id, attribute, + **kwargs): + def _unsupported_attribute(instance, result): + raise exception.ApiError(_('attribute not supported: %s') % + attribute) + + def _format_attr_block_device_mapping(instance, result): + tmp = {} + self._format_instance_root_device_name(instance, tmp) + self._format_instance_bdm(context, instance_id, + tmp['rootDeviceName'], result) + + def _format_attr_disable_api_termination(instance, result): + _unsupported_attribute(instance, result) + + def _format_attr_group_set(instance, result): + CloudController._format_group_set(instance, result) + + def _format_attr_instance_initiated_shutdown_behavior(instance, + result): + state_description = instance['state_description'] + state_to_value = {'stopping': 'stop', + 'stopped': 'stop', + 'terminating': 'terminate'} + value = state_to_value.get(state_description) + if value: + result['instanceInitiatedShutdownBehavior'] = value + + def _format_attr_instance_type(instance, result): + self._format_instance_type(instance, result) + + def _format_attr_kernel(instance, result): + self._format_kernel_id(instance, result, 'kernel') + + def _format_attr_ramdisk(instance, result): + self._format_ramdisk_id(instance, result, 'ramdisk') + + def _format_attr_root_device_name(instance, result): + self._format_instance_root_device_name(instance, result) + + def _format_attr_source_dest_check(instance, result): + _unsupported_attribute(instance, result) + + def _format_attr_user_data(instance, result): + result['userData'] = self._format_user_data(instance) + + attribute_formatter = { + 'blockDeviceMapping': _format_attr_block_device_mapping, + 'disableApiTermination': _format_attr_disable_api_termination, + 'groupSet': _format_attr_group_set, + 'instanceInitiatedShutdownBehavior': + _format_attr_instance_initiated_shutdown_behavior, + 'instanceType': _format_attr_instance_type, + 'kernel': _format_attr_kernel, + 'ramdisk': _format_attr_ramdisk, + 'rootDeviceName': _format_attr_root_device_name, + 'sourceDestCheck': _format_attr_source_dest_check, + 'userData': _format_attr_user_data, + } + + fn = attribute_formatter.get(attribute) + if fn is None: + raise exception.ApiError( + _('attribute not supported: %s') % attribute) + + ec2_instance_id = instance_id + instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) + instance = self.compute_api.get(context, instance_id) + result = {'instance_id': ec2_instance_id} + fn(instance, result) + return result + def describe_instances(self, context, **kwargs): - return self._format_describe_instances(context, **kwargs) + # Optional DescribeInstances argument + instance_id = kwargs.get('instance_id', None) + return self._format_describe_instances(context, + instance_id=instance_id) def describe_instances_v6(self, context, **kwargs): - kwargs['use_v6'] = True - return self._format_describe_instances(context, **kwargs) + # Optional DescribeInstancesV6 argument + instance_id = kwargs.get('instance_id', None) + return self._format_describe_instances(context, + instance_id=instance_id, use_v6=True) def _format_describe_instances(self, context, **kwargs): return {'reservationSet': self._format_instances(context, **kwargs)} @@ -1001,7 +1142,29 @@ class CloudController(object): result['blockDeviceMapping'] = mapping result['rootDeviceType'] = root_device_type - def _format_instances(self, context, instance_id=None, **kwargs): + @staticmethod + def _format_instance_root_device_name(instance, result): + result['rootDeviceName'] = (instance.get('root_device_name') or + _DEFAULT_ROOT_DEVICE_NAME) + + @staticmethod + def _format_instance_type(instance, result): + if instance['instance_type']: + result['instanceType'] = instance['instance_type'].get('name') + else: + result['instanceType'] = None + + @staticmethod + def _format_group_set(instance, result): + security_group_names = [] + if instance.get('security_groups'): + for security_group in instance['security_groups']: + security_group_names.append(security_group['name']) + result['groupSet'] = CloudController._convert_to_set( + security_group_names, 'groupId') + + def _format_instances(self, context, instance_id=None, use_v6=False, + **search_opts): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls # rather than simply formatting a bunch of instances that @@ -1012,11 +1175,17 @@ class CloudController(object): instances = [] for ec2_id in instance_id: internal_id = ec2utils.ec2_id_to_id(ec2_id) - instance = self.compute_api.get(context, - instance_id=internal_id) + try: + instance = self.compute_api.get(context, internal_id) + except exception.NotFound: + continue instances.append(instance) else: - instances = self.compute_api.get_all(context, **kwargs) + try: + instances = self.compute_api.get_all(context, + search_opts=search_opts) + except exception.NotFound: + instances = [] for instance in instances: if not context.is_admin: if instance['image_ref'] == str(FLAGS.vpn_image_id): @@ -1026,6 +1195,8 @@ class CloudController(object): ec2_id = ec2utils.id_to_ec2_id(instance_id) i['instanceId'] = ec2_id i['imageId'] = self.image_ec2_id(instance['image_ref']) + self._format_kernel_id(instance, i, 'kernelId') + self._format_ramdisk_id(instance, i, 'ramdiskId') i['instanceState'] = { 'code': instance['state'], 'name': instance['state_description']} @@ -1036,7 +1207,7 @@ class CloudController(object): fixed_addr = fixed['address'] if fixed['floating_ips']: floating_addr = fixed['floating_ips'][0]['address'] - if fixed['network'] and 'use_v6' in kwargs: + if fixed['network'] and use_v6: i['dnsNameV6'] = ipv6.to_global( fixed['network']['cidr_v6'], fixed['virtual_interface']['address'], @@ -1054,16 +1225,12 @@ class CloudController(object): instance['project_id'], instance['host']) i['productCodesSet'] = self._convert_to_set([], 'product_codes') - if instance['instance_type']: - i['instanceType'] = instance['instance_type'].get('name') - else: - i['instanceType'] = None + self._format_instance_type(instance, i) i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] - i['rootDeviceName'] = (instance.get('root_device_name') or - _DEFAULT_ROOT_DEVICE_NAME) + self._format_instance_root_device_name(instance, i) self._format_instance_bdm(context, instance_id, i['rootDeviceName'], i) host = instance['host'] @@ -1073,12 +1240,7 @@ class CloudController(object): r = {} r['reservationId'] = instance['reservation_id'] r['ownerId'] = instance['project_id'] - security_group_names = [] - if instance.get('security_groups'): - for security_group in instance['security_groups']: - security_group_names.append(security_group['name']) - r['groupSet'] = self._convert_to_set(security_group_names, - 'groupId') + self._format_group_set(instance, r) r['instancesSet'] = [] reservations[instance['reservation_id']] = r reservations[instance['reservation_id']]['instancesSet'].append(i) @@ -1182,7 +1344,7 @@ class CloudController(object): 'AvailabilityZone'), block_device_mapping=kwargs.get('block_device_mapping', {})) return self._format_run_instances(context, - instances[0]['reservation_id']) + reservation_id=instances[0]['reservation_id']) def _do_instance(self, action, context, ec2_id): instance_id = ec2utils.ec2_id_to_id(ec2_id) @@ -1314,7 +1476,7 @@ class CloudController(object): i['architecture'] = image['properties'].get('architecture') properties = image['properties'] - root_device_name = ec2utils.properties_root_device_name(properties) + root_device_name = block_device.properties_root_device_name(properties) root_device_type = 'instance-store' for bdm in properties.get('block_device_mapping', []): if (bdm.get('device_name') == root_device_name and @@ -1387,7 +1549,7 @@ class CloudController(object): def _root_device_name_attribute(image, result): result['rootDeviceName'] = \ - ec2utils.properties_root_device_name(image['properties']) + block_device.properties_root_device_name(image['properties']) if result['rootDeviceName'] is None: result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME @@ -1520,8 +1682,7 @@ class CloudController(object): if virtual_name in ('ami', 'root'): continue - assert (virtual_name == 'swap' or - virtual_name.startswith('ephemeral')) + assert block_device.is_swap_or_ephemeral(virtual_name) device_name = m['device'] if device_name in [b['device_name'] for b in mapping if not b.get('no_device', False)]: diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bae1e0ee5..bcdf2ba78 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -135,32 +135,3 @@ def dict_from_dotted_str(items): args[key] = value return args - - -def properties_root_device_name(properties): - """get root device name from image meta data. - If it isn't specified, return None. - """ - root_device_name = None - - # NOTE(yamahata): see image_service.s3.s3create() - for bdm in properties.get('mappings', []): - if bdm['virtual'] == 'root': - root_device_name = bdm['device'] - - # NOTE(yamahata): register_image's command line can override - # <machine>.manifest.xml - if 'root_device_name' in properties: - root_device_name = properties['root_device_name'] - - return root_device_name - - -def mappings_prepend_dev(mappings): - """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type""" - for m in mappings: - virtual = m['virtual'] - if ((virtual == 'swap' or virtual.startswith('ephemeral')) and - (not m['device'].startswith('/'))): - m['device'] = '/dev/' + m['device'] - return mappings diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index d6a98c2cd..e0c1e9d04 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -50,6 +50,9 @@ FLAGS = flags.FLAGS flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') class FaultWrapper(base_wsgi.Middleware): @@ -82,7 +85,10 @@ class APIRouter(base_wsgi.Router): self._setup_routes(mapper) super(APIRouter, self).__init__(mapper) - def _setup_routes(self, mapper, version): + def _setup_routes(self, mapper): + raise NotImplementedError(_("You must implement _setup_routes.")) + + def _setup_base_routes(self, mapper, version): """Routes common to all versions.""" server_members = self.server_members @@ -153,7 +159,7 @@ class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" def _setup_routes(self, mapper): - super(APIRouterV10, self)._setup_routes(mapper, '1.0') + self._setup_base_routes(mapper, '1.0') mapper.resource("shared_ip_group", "shared_ip_groups", collection={'detail': 'GET'}, @@ -169,7 +175,7 @@ class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" def _setup_routes(self, mapper): - super(APIRouterV11, self)._setup_routes(mapper, '1.1') + self._setup_base_routes(mapper, '1.1') image_metadata_controller = image_metadata.create_resource() diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 715b9e4a4..b2a675653 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import re import urlparse from xml.dom import minidom @@ -24,7 +25,9 @@ import webob from nova import exception from nova import flags from nova import log as logging +from nova import quota from nova.api.openstack import wsgi +from nova.compute import power_state as compute_power_state LOG = logging.getLogger('nova.api.openstack.common') @@ -35,6 +38,38 @@ XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1' +_STATUS_MAP = { + None: 'BUILD', + compute_power_state.NOSTATE: 'BUILD', + compute_power_state.RUNNING: 'ACTIVE', + compute_power_state.BLOCKED: 'ACTIVE', + compute_power_state.SUSPENDED: 'SUSPENDED', + compute_power_state.PAUSED: 'PAUSED', + compute_power_state.SHUTDOWN: 'SHUTDOWN', + compute_power_state.SHUTOFF: 'SHUTOFF', + compute_power_state.CRASHED: 'ERROR', + compute_power_state.FAILED: 'ERROR', + compute_power_state.BUILDING: 'BUILD', +} + + +def status_from_power_state(power_state): + """Map the power state to the server status string""" + return _STATUS_MAP[power_state] + + +def power_states_from_status(status): + """Map the server status string to a list of power states""" + power_states = [] + for power_state, status_map in _STATUS_MAP.iteritems(): + # Skip the 'None' state + if power_state is None: + continue + if status.lower() == status_map.lower(): + power_states.append(power_state) + return power_states + + def get_pagination_params(request): """Return marker, limit tuple from request. @@ -134,13 +169,20 @@ def get_id_from_href(href): Returns: 123 """ - if re.match(r'\d+$', str(href)): + LOG.debug(_("Attempting to treat %(href)s as an integer ID.") % locals()) + + try: return int(href) + except ValueError: + pass + + LOG.debug(_("Attempting to treat %(href)s as a URL.") % locals()) + try: return int(urlparse.urlsplit(href).path.split('/')[-1]) - except ValueError, e: - LOG.debug(_("Error extracting id from href: %s") % href) - raise ValueError(_('could not parse id from href')) + except ValueError as error: + LOG.debug(_("Failed to parse ID from %(href)s: %(error)s") % locals()) + raise def remove_version_from_href(href): @@ -154,7 +196,8 @@ def remove_version_from_href(href): """ parsed_url = urlparse.urlsplit(href) - new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path, count=1) + new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path, + count=1) if new_path == parsed_url.path: msg = _('href %s does not contain version') % href @@ -191,6 +234,16 @@ def get_version_from_href(href): return version +def check_img_metadata_quota_limit(context, metadata): + if metadata is None: + return + num_metadata = len(metadata) + quota_metadata = quota.allowed_metadata_items(context, num_metadata) + if quota_metadata < num_metadata: + expl = _("Image metadata limit exceeded") + raise webob.exc.HTTPBadRequest(explanation=expl) + + class MetadataXMLDeserializer(wsgi.XMLDeserializer): def extract_metadata(self, metadata_node): @@ -279,3 +332,15 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer): def default(self, *args, **kwargs): return '' + + +def check_snapshots_enabled(f): + @functools.wraps(f) + def inner(*args, **kwargs): + if not FLAGS.allow_instance_snapshots: + LOG.warn(_('Rejecting snapshot request, snapshots currently' + ' disabled')) + msg = _("Instance snapshots are not permitted at this time.") + raise webob.exc.HTTPBadRequest(explanation=msg) + return f(*args, **kwargs) + return inner diff --git a/nova/api/openstack/contrib/admin_only.py b/nova/api/openstack/contrib/admin_only.py new file mode 100644 index 000000000..e821c9e1f --- /dev/null +++ b/nova/api/openstack/contrib/admin_only.py @@ -0,0 +1,30 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Decorator for limiting extensions that should be admin-only.""" + +from functools import wraps +from nova import flags +FLAGS = flags.FLAGS + + +def admin_only(fnc): + @wraps(fnc) + def _wrapped(self, *args, **kwargs): + if FLAGS.allow_admin_api: + return fnc(self, *args, **kwargs) + return [] + _wrapped.func_name = fnc.func_name + return _wrapped diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 3d8049324..44b35c385 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -18,12 +18,16 @@ from webob import exc from nova import exception +from nova import log as logging from nova import network from nova import rpc from nova.api.openstack import faults from nova.api.openstack import extensions +LOG = logging.getLogger('nova.api.openstack.contrib.floating_ips') + + def _translate_floating_ip_view(floating_ip): result = {'id': floating_ip['id'], 'ip': floating_ip['address']} @@ -39,8 +43,8 @@ def _translate_floating_ip_view(floating_ip): def _translate_floating_ips_view(floating_ips): - return {'floating_ips': [_translate_floating_ip_view(floating_ip) - for floating_ip in floating_ips]} + return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip'] + for ip in floating_ips]} class FloatingIPController(object): @@ -97,9 +101,12 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] - ip = self.network_api.get_floating_ip(context, id) - self.network_api.release_floating_ip(context, address=ip) + + if 'fixed_ip' in ip: + self.disassociate(req, id) + + self.network_api.release_floating_ip(context, address=ip['address']) return {'released': { "id": ip['id'], @@ -124,7 +131,7 @@ class FloatingIPController(object): "floating_ip": floating_ip, "fixed_ip": fixed_ip}} - def disassociate(self, req, id): + def disassociate(self, req, id, body=None): """ POST /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 55e57e1a4..ecaa365b7 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -24,6 +24,7 @@ from nova import log as logging from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import faults +from nova.api.openstack.contrib import admin_only from nova.scheduler import api as scheduler_api @@ -70,7 +71,7 @@ class HostController(object): key = raw_key.lower().strip() val = raw_val.lower().strip() # NOTE: (dabo) Right now only 'status' can be set, but other - # actions may follow. + # settings may follow. if key == "status": if val[:6] in ("enable", "disabl"): return self._set_enabled_status(req, id, @@ -89,8 +90,30 @@ class HostController(object): LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) result = self.compute_api.set_host_enabled(context, host=host, enabled=enabled) + if result not in ("enabled", "disabled"): + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) return {"host": host, "status": result} + def _host_power_action(self, req, host, action): + """Reboots, shuts down or powers up the host.""" + context = req.environ['nova.context'] + try: + result = self.compute_api.host_power_action(context, host=host, + action=action) + except NotImplementedError as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + return {"host": host, "power_action": result} + + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + class Hosts(extensions.ExtensionDescriptor): def get_name(self): @@ -108,7 +131,10 @@ class Hosts(extensions.ExtensionDescriptor): def get_updated(self): return "2011-06-29T00:00:00+00:00" + @admin_only.admin_only def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', HostController(), - collection_actions={'update': 'PUT'}, member_actions={})] + resources = [extensions.ResourceExtension('os-hosts', + HostController(), collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] return resources diff --git a/nova/api/openstack/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py new file mode 100644 index 000000000..201648ab5 --- /dev/null +++ b/nova/api/openstack/contrib/keypairs.py @@ -0,0 +1,145 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Keypair management extension""" + +import os +import shutil +import tempfile + +from webob import exc + +from nova import crypto +from nova import db +from nova import exception +from nova.api.openstack import extensions + + +class KeypairController(object): + """ Keypair API controller for the Openstack API """ + + # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic. + # move the common keypair logic to nova.compute.API? + + def _gen_key(self): + """ + Generate a key + """ + private_key, public_key, fingerprint = crypto.generate_key_pair() + return {'private_key': private_key, + 'public_key': public_key, + 'fingerprint': fingerprint} + + def create(self, req, body): + """ + Create or import keypair. + + Sending name will generate a key and return private_key + and fingerprint. + + You can send a public_key to add an existing ssh key + + params: keypair object with: + name (required) - string + public_key (optional) - string + """ + + context = req.environ['nova.context'] + params = body['keypair'] + name = params['name'] + + # NOTE(ja): generation is slow, so shortcut invalid name exception + try: + db.key_pair_get(context, context.user_id, name) + raise exception.KeyPairExists(key_name=name) + except exception.NotFound: + pass + + keypair = {'user_id': context.user_id, + 'name': name} + + # import if public_key is sent + if 'public_key' in params: + tmpdir = tempfile.mkdtemp() + fn = os.path.join(tmpdir, 'import.pub') + with open(fn, 'w') as pub: + pub.write(params['public_key']) + fingerprint = crypto.generate_fingerprint(fn) + shutil.rmtree(tmpdir) + keypair['public_key'] = params['public_key'] + keypair['fingerprint'] = fingerprint + else: + generated_key = self._gen_key() + keypair['private_key'] = generated_key['private_key'] + keypair['public_key'] = generated_key['public_key'] + keypair['fingerprint'] = generated_key['fingerprint'] + + db.key_pair_create(context, keypair) + return {'keypair': keypair} + + def delete(self, req, id): + """ + Delete a keypair with a given name + """ + context = req.environ['nova.context'] + db.key_pair_destroy(context, context.user_id, id) + return exc.HTTPAccepted() + + def index(self, req): + """ + List of keypairs for a user + """ + context = req.environ['nova.context'] + key_pairs = db.key_pair_get_all_by_user(context, context.user_id) + rval = [] + for key_pair in key_pairs: + rval.append({'keypair': { + 'name': key_pair['name'], + 'public_key': key_pair['public_key'], + 'fingerprint': key_pair['fingerprint'], + }}) + + return {'keypairs': rval} + + +class Keypairs(extensions.ExtensionDescriptor): + + def get_name(self): + return "Keypairs" + + def get_alias(self): + return "os-keypairs" + + def get_description(self): + return "Keypair Support" + + def get_namespace(self): + return \ + "http://docs.openstack.org/ext/keypairs/api/v1.1" + + def get_updated(self): + return "2011-08-08T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-keypairs', + KeypairController()) + + resources.append(res) + return resources diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py new file mode 100644 index 000000000..6c57fbb51 --- /dev/null +++ b/nova/api/openstack/contrib/security_groups.py @@ -0,0 +1,466 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The security groups extension.""" + +import netaddr +import urllib +from webob import exc +import webob + +from nova import compute +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi + + +from xml.dom import minidom + + +LOG = logging.getLogger("nova.api.contrib.security_groups") +FLAGS = flags.FLAGS + + +class SecurityGroupController(object): + """The Security group API controller for the OpenStack API.""" + + def __init__(self): + self.compute_api = compute.API() + super(SecurityGroupController, self).__init__() + + def _format_security_group_rule(self, context, rule): + sg_rule = {} + sg_rule['id'] = rule.id + sg_rule['parent_group_id'] = rule.parent_group_id + sg_rule['ip_protocol'] = rule.protocol + sg_rule['from_port'] = rule.from_port + sg_rule['to_port'] = rule.to_port + sg_rule['group'] = {} + sg_rule['ip_range'] = {} + if rule.group_id: + source_group = db.security_group_get(context, rule.group_id) + sg_rule['group'] = {'name': source_group.name, + 'tenant_id': source_group.project_id} + else: + sg_rule['ip_range'] = {'cidr': rule.cidr} + return sg_rule + + def _format_security_group(self, context, group): + security_group = {} + security_group['id'] = group.id + security_group['description'] = group.description + security_group['name'] = group.name + security_group['tenant_id'] = group.project_id + security_group['rules'] = [] + for rule in group.rules: + security_group['rules'] += [self._format_security_group_rule( + context, rule)] + return security_group + + def show(self, req, id): + """Return data about the given security group.""" + context = req.environ['nova.context'] + try: + id = int(id) + security_group = db.security_group_get(context, id) + except ValueError: + msg = _("Security group id is not integer") + return exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + + return {'security_group': self._format_security_group(context, + security_group)} + + def delete(self, req, id): + """Delete a security group.""" + context = req.environ['nova.context'] + try: + id = int(id) + security_group = db.security_group_get(context, id) + except ValueError: + msg = _("Security group id is not integer") + return exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + + LOG.audit(_("Delete security group %s"), id, context=context) + db.security_group_destroy(context, security_group.id) + + return exc.HTTPAccepted() + + def index(self, req): + """Returns a list of security groups""" + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + groups = db.security_group_get_by_project(context, + context.project_id) + limited_list = common.limited(groups, req) + result = [self._format_security_group(context, group) + for group in limited_list] + + return {'security_groups': + list(sorted(result, + key=lambda k: (k['tenant_id'], k['name'])))} + + def create(self, req, body): + """Creates a new security group.""" + context = req.environ['nova.context'] + if not body: + return exc.HTTPUnprocessableEntity() + + security_group = body.get('security_group', None) + + if security_group is None: + return exc.HTTPUnprocessableEntity() + + group_name = security_group.get('name', None) + group_description = security_group.get('description', None) + + self._validate_security_group_property(group_name, "name") + self._validate_security_group_property(group_description, + "description") + group_name = group_name.strip() + group_description = group_description.strip() + + LOG.audit(_("Create Security Group %s"), group_name, context=context) + self.compute_api.ensure_default_security_group(context) + if db.security_group_exists(context, context.project_id, group_name): + msg = _('Security group %s already exists') % group_name + raise exc.HTTPBadRequest(explanation=msg) + + group = {'user_id': context.user_id, + 'project_id': context.project_id, + 'name': group_name, + 'description': group_description} + group_ref = db.security_group_create(context, group) + + return {'security_group': self._format_security_group(context, + group_ref)} + + def _validate_security_group_property(self, value, typ): + """ typ will be either 'name' or 'description', + depending on the caller + """ + try: + val = value.strip() + except AttributeError: + msg = _("Security group %s is not a string or unicode") % typ + raise exc.HTTPBadRequest(explanation=msg) + if not val: + msg = _("Security group %s cannot be empty.") % typ + raise exc.HTTPBadRequest(explanation=msg) + if len(val) > 255: + msg = _("Security group %s should not be greater " + "than 255 characters.") % typ + raise exc.HTTPBadRequest(explanation=msg) + + +class SecurityGroupRulesController(SecurityGroupController): + + def create(self, req, body): + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'security_group_rule' in body: + raise exc.HTTPUnprocessableEntity() + + self.compute_api.ensure_default_security_group(context) + + sg_rule = body['security_group_rule'] + parent_group_id = sg_rule.get('parent_group_id', None) + try: + parent_group_id = int(parent_group_id) + security_group = db.security_group_get(context, parent_group_id) + except ValueError: + msg = _("Parent group id is not integer") + return exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Security group (%s) not found") % parent_group_id + return exc.HTTPNotFound(explanation=msg) + + msg = _("Authorize security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + try: + values = self._rule_args_to_dict(context, + to_port=sg_rule.get('to_port'), + from_port=sg_rule.get('from_port'), + parent_group_id=sg_rule.get('parent_group_id'), + ip_protocol=sg_rule.get('ip_protocol'), + cidr=sg_rule.get('cidr'), + group_id=sg_rule.get('group_id')) + except Exception as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + if values is None: + msg = _("Not enough parameters to build a " + "valid rule.") + raise exc.HTTPBadRequest(explanation=msg) + + values['parent_group_id'] = security_group.id + + if self._security_group_rule_exists(security_group, values): + msg = _('This rule already exists in group %s') % parent_group_id + raise exc.HTTPBadRequest(explanation=msg) + + security_group_rule = db.security_group_rule_create(context, values) + + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return {'security_group_rule': self._format_security_group_rule( + context, + security_group_rule)} + + def _security_group_rule_exists(self, security_group, values): + """Indicates whether the specified rule values are already + defined in the given security group. + """ + for rule in security_group.rules: + if 'group_id' in values: + if rule['group_id'] == values['group_id']: + return True + else: + is_duplicate = True + for key in ('cidr', 'from_port', 'to_port', 'protocol'): + if rule[key] != values[key]: + is_duplicate = False + break + if is_duplicate: + return True + return False + + def _rule_args_to_dict(self, context, to_port=None, from_port=None, + parent_group_id=None, ip_protocol=None, + cidr=None, group_id=None): + values = {} + + if group_id: + try: + parent_group_id = int(parent_group_id) + group_id = int(group_id) + except ValueError: + msg = _("Parent or group id is not integer") + raise exception.InvalidInput(reason=msg) + + if parent_group_id == group_id: + msg = _("Parent group id and group id cannot be same") + raise exception.InvalidInput(reason=msg) + + values['group_id'] = group_id + #check if groupId exists + db.security_group_get(context, group_id) + elif cidr: + # If this fails, it throws an exception. This is what we want. + try: + cidr = urllib.unquote(cidr).decode() + netaddr.IPNetwork(cidr) + except Exception: + raise exception.InvalidCidr(cidr=cidr) + values['cidr'] = cidr + else: + values['cidr'] = '0.0.0.0/0' + + if ip_protocol and from_port and to_port: + + try: + from_port = int(from_port) + to_port = int(to_port) + except ValueError: + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port) + ip_protocol = str(ip_protocol) + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exception.InvalidIpProtocol(protocol=ip_protocol) + if ((min(from_port, to_port) < -1) or + (max(from_port, to_port) > 65535)): + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port) + + values['protocol'] = ip_protocol + values['from_port'] = from_port + values['to_port'] = to_port + else: + # If cidr based filtering, protocol and ports are mandatory + if 'cidr' in values: + return None + + return values + + def delete(self, req, id): + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + try: + id = int(id) + rule = db.security_group_rule_get(context, id) + except ValueError: + msg = _("Rule id is not integer") + return exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Rule (%s) not found") % id + return exc.HTTPNotFound(explanation=msg) + + group_id = rule.parent_group_id + self.compute_api.ensure_default_security_group(context) + security_group = db.security_group_get(context, group_id) + + msg = _("Revoke security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + db.security_group_rule_destroy(context, rule['id']) + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return exc.HTTPAccepted() + + +class Security_groups(extensions.ExtensionDescriptor): + def get_name(self): + return "SecurityGroups" + + def get_alias(self): + return "security_groups" + + def get_description(self): + return "Security group support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/securitygroups/api/v1.1" + + def get_updated(self): + return "2011-07-21T00:00:00+00:00" + + def get_resources(self): + resources = [] + + metadata = _get_metadata() + body_serializers = { + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, + xmlns=wsgi.XMLNS_V11), + } + serializer = wsgi.ResponseSerializer(body_serializers, None) + + body_deserializers = { + 'application/xml': SecurityGroupXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('os-security-groups', + controller=SecurityGroupController(), + deserializer=deserializer, + serializer=serializer) + + resources.append(res) + + body_deserializers = { + 'application/xml': SecurityGroupRulesXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('os-security-group-rules', + controller=SecurityGroupRulesController(), + deserializer=deserializer, + serializer=serializer) + resources.append(res) + return resources + + +class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + def create(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group = {} + sg_node = self.find_first_child_named(dom, + 'security_group') + if sg_node is not None: + if sg_node.hasAttribute('name'): + security_group['name'] = sg_node.getAttribute('name') + desc_node = self.find_first_child_named(sg_node, + "description") + if desc_node: + security_group['description'] = self.extract_text(desc_node) + return {'body': {'security_group': security_group}} + + +class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + + def create(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group_rule = self._extract_security_group_rule(dom) + return {'body': {'security_group_rule': security_group_rule}} + + def _extract_security_group_rule(self, node): + """Marshal the security group rule attribute of a parsed request""" + sg_rule = {} + sg_rule_node = self.find_first_child_named(node, + 'security_group_rule') + if sg_rule_node is not None: + ip_protocol_node = self.find_first_child_named(sg_rule_node, + "ip_protocol") + if ip_protocol_node is not None: + sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node) + + from_port_node = self.find_first_child_named(sg_rule_node, + "from_port") + if from_port_node is not None: + sg_rule['from_port'] = self.extract_text(from_port_node) + + to_port_node = self.find_first_child_named(sg_rule_node, "to_port") + if to_port_node is not None: + sg_rule['to_port'] = self.extract_text(to_port_node) + + parent_group_id_node = self.find_first_child_named(sg_rule_node, + "parent_group_id") + if parent_group_id_node is not None: + sg_rule['parent_group_id'] = self.extract_text( + parent_group_id_node) + + group_id_node = self.find_first_child_named(sg_rule_node, + "group_id") + if group_id_node is not None: + sg_rule['group_id'] = self.extract_text(group_id_node) + + cidr_node = self.find_first_child_named(sg_rule_node, "cidr") + if cidr_node is not None: + sg_rule['cidr'] = self.extract_text(cidr_node) + + return sg_rule + + +def _get_metadata(): + metadata = { + "attributes": { + "security_group": ["id", "tenant_id", "name"], + "rule": ["id", "parent_group_id"], + "security_group_rule": ["id", "parent_group_id"], + } + } + return metadata diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 2a8e7fd7e..4e1da549e 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -14,8 +14,6 @@ # under the License. import base64 -import re -import webob from webob import exc from xml.dom import minidom @@ -124,6 +122,7 @@ class CreateInstanceHelper(object): raise exc.HTTPBadRequest(explanation=msg) zone_blob = server_dict.get('blob') + availability_zone = server_dict.get('availability_zone') name = server_dict['name'] self._validate_server_name(name) name = name.strip() @@ -163,7 +162,8 @@ class CreateInstanceHelper(object): zone_blob=zone_blob, reservation_id=reservation_id, min_count=min_count, - max_count=max_count)) + max_count=max_count, + availability_zone=availability_zone)) except quota.QuotaError as error: self._handle_quota_error(error) except exception.ImageNotFound as error: @@ -304,6 +304,54 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): metadata_deserializer = common.MetadataXMLDeserializer() + def create(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" + server = {} + server_node = self.find_first_child_named(node, 'server') + + attributes = ["name", "imageId", "flavorId", "adminPass"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + server["metadata"] = self.metadata_deserializer.extract_metadata( + metadata_node) + + server["personality"] = self._extract_personality(server_node) + + return server + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + node = self.find_first_child_named(server_node, "personality") + personality = [] + if node is not None: + for file_node in self.find_children_named(node, "file"): + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self.extract_text(file_node) + personality.append(item) + return personality + + +class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + metadata_deserializer = common.MetadataXMLDeserializer() + def action(self, string): dom = minidom.parseString(string) action_node = dom.childNodes[0] @@ -312,6 +360,12 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): action_deserializer = { 'createImage': self._action_create_image, 'createBackup': self._action_create_backup, + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'rebuild': self._action_rebuild, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, }.get(action_name, self.default) action_data = action_deserializer(action_node) @@ -325,6 +379,46 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): attributes = ('name', 'backup_type', 'rotation') return self._deserialize_image_action(node, attributes) + def _action_change_password(self, node): + if not node.hasAttribute("adminPass"): + raise AttributeError("No adminPass was specified in request") + return {"adminPass": node.getAttribute("adminPass")} + + def _action_reboot(self, node): + if not node.hasAttribute("type"): + raise AttributeError("No reboot type was specified in request") + return {"type": node.getAttribute("type")} + + def _action_rebuild(self, node): + rebuild = {} + if node.hasAttribute("name"): + rebuild['name'] = node.getAttribute("name") + + metadata_node = self.find_first_child_named(node, "metadata") + if metadata_node is not None: + rebuild["metadata"] = self.extract_metadata(metadata_node) + + personality = self._extract_personality(node) + if personality is not None: + rebuild["personality"] = personality + + if not node.hasAttribute("imageRef"): + raise AttributeError("No imageRef was specified in request") + rebuild["imageRef"] = node.getAttribute("imageRef") + + return rebuild + + def _action_resize(self, node): + if not node.hasAttribute("flavorRef"): + raise AttributeError("No flavorRef was specified in request") + return {"flavorRef": node.getAttribute("flavorRef")} + + def _action_confirm_resize(self, node): + return None + + def _action_revert_resize(self, node): + return None + def _deserialize_image_action(self, node, allowed_attributes): data = {} for attribute in allowed_attributes: @@ -332,8 +426,10 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): if value: data[attribute] = value metadata_node = self.find_first_child_named(node, 'metadata') - metadata = self.metadata_deserializer.extract_metadata(metadata_node) - data['metadata'] = metadata + if metadata_node is not None: + metadata = self.metadata_deserializer.extract_metadata( + metadata_node) + data['metadata'] = metadata return data def create(self, string): @@ -347,29 +443,32 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): server = {} server_node = self.find_first_child_named(node, 'server') - attributes = ["name", "imageId", "flavorId", "imageRef", - "flavorRef", "adminPass"] + attributes = ["name", "imageRef", "flavorRef", "adminPass"] for attr in attributes: if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) metadata_node = self.find_first_child_named(server_node, "metadata") - server["metadata"] = self.metadata_deserializer.extract_metadata( - metadata_node) + if metadata_node is not None: + server["metadata"] = self.extract_metadata(metadata_node) - server["personality"] = self._extract_personality(server_node) + personality = self._extract_personality(server_node) + if personality is not None: + server["personality"] = personality return server def _extract_personality(self, server_node): """Marshal the personality attribute of a parsed request""" node = self.find_first_child_named(server_node, "personality") - personality = [] if node is not None: + personality = [] for file_node in self.find_children_named(node, "file"): item = {} if file_node.hasAttribute("path"): item["path"] = file_node.getAttribute("path") item["contents"] = self.extract_text(file_node) personality.append(item) - return personality + return personality + else: + return None diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index cc889703e..bb407a045 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -23,7 +23,7 @@ import sys import routes import webob.dec import webob.exc -from xml.etree import ElementTree +from lxml import etree from nova import exception from nova import flags @@ -32,6 +32,7 @@ from nova import wsgi as base_wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil LOG = logging.getLogger('extensions') @@ -265,9 +266,13 @@ class ExtensionMiddleware(base_wsgi.Middleware): for resource in ext_mgr.get_resources(): LOG.debug(_('Extended resource: %s'), resource.collection) + if resource.serializer is None: + resource.serializer = serializer + mapper.resource(resource.collection, resource.collection, controller=wsgi.Resource( - resource.controller, serializer=serializer), + resource.controller, resource.deserializer, + resource.serializer), collection=resource.collection_actions, member=resource.member_actions, parent_resource=resource.parent) @@ -460,46 +465,55 @@ class ResourceExtension(object): """Add top level resources to the OpenStack API in nova.""" def __init__(self, collection, controller, parent=None, - collection_actions={}, member_actions={}): + collection_actions=None, member_actions=None, + deserializer=None, serializer=None): + if not collection_actions: + collection_actions = {} + if not member_actions: + member_actions = {} self.collection = collection self.controller = controller self.parent = parent self.collection_actions = collection_actions self.member_actions = member_actions + self.deserializer = deserializer + self.serializer = serializer class ExtensionsXMLSerializer(wsgi.XMLDictSerializer): + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + def show(self, ext_dict): - ext = self._create_ext_elem(ext_dict['extension']) + ext = etree.Element('extension', nsmap=self.NSMAP) + self._populate_ext(ext, ext_dict['extension']) return self._to_xml(ext) def index(self, exts_dict): - exts = ElementTree.Element('extensions') + exts = etree.Element('extensions', nsmap=self.NSMAP) for ext_dict in exts_dict['extensions']: - exts.append(self._create_ext_elem(ext_dict)) + ext = etree.SubElement(exts, 'extension') + self._populate_ext(ext, ext_dict) return self._to_xml(exts) - def _create_ext_elem(self, ext_dict): - """Create an extension xml element from a dict.""" - ext_elem = ElementTree.Element('extension') + def _populate_ext(self, ext_elem, ext_dict): + """Populate an extension xml element from a dict.""" + ext_elem.set('name', ext_dict['name']) ext_elem.set('namespace', ext_dict['namespace']) ext_elem.set('alias', ext_dict['alias']) ext_elem.set('updated', ext_dict['updated']) - desc = ElementTree.Element('description') + desc = etree.Element('description') desc.text = ext_dict['description'] ext_elem.append(desc) for link in ext_dict.get('links', []): - elem = ElementTree.Element('atom:link') + elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM) elem.set('rel', link['rel']) elem.set('href', link['href']) elem.set('type', link['type']) - ext_elem.append(elem) return ext_elem def _to_xml(self, root): - """Convert the xml tree object to an xml string.""" - root.set('xmlns', wsgi.XMLNS_V11) - root.set('xmlns:atom', wsgi.XMLNS_ATOM) - return ElementTree.tostring(root, encoding='UTF-8') + """Convert the xml object to an xml string.""" + + return etree.tostring(root, encoding='UTF-8') diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index aaf64a123..4d615ea96 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -19,7 +19,6 @@ from webob import exc from nova import flags from nova import image -from nova import quota from nova import utils from nova.api.openstack import common from nova.api.openstack import wsgi @@ -40,15 +39,6 @@ class Controller(object): metadata = image.get('properties', {}) return metadata - def _check_quota_limit(self, context, metadata): - if metadata is None: - return - num_metadata = len(metadata) - quota_metadata = quota.allowed_metadata_items(context, num_metadata) - if quota_metadata < num_metadata: - expl = _("Image metadata limit exceeded") - raise exc.HTTPBadRequest(explanation=expl) - def index(self, req, image_id): """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] @@ -70,7 +60,7 @@ class Controller(object): if 'metadata' in body: for key, value in body['metadata'].iteritems(): metadata[key] = value - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(metadata=metadata) @@ -93,7 +83,7 @@ class Controller(object): img = self.image_service.show(context, image_id) metadata = self._get_metadata(context, image_id, img) metadata[id] = meta[id] - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(meta=meta) @@ -102,7 +92,7 @@ class Controller(object): context = req.environ['nova.context'] img = self.image_service.show(context, image_id) metadata = body.get('metadata', {}) - self._check_quota_limit(context, metadata) + common.check_img_metadata_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(metadata=metadata) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index c76738d30..0aabb9e56 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -106,6 +106,7 @@ class Controller(object): class ControllerV10(Controller): """Version 1.0 specific controller logic.""" + @common.check_snapshots_enabled def create(self, req, body): """Snapshot a server instance and save the image.""" try: @@ -143,7 +144,7 @@ class ControllerV10(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - images = self._image_service.index(context, filters) + images = self._image_service.index(context, filters=filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=False) for image in images]) @@ -156,7 +157,7 @@ class ControllerV10(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - images = self._image_service.detail(context, filters) + images = self._image_service.detail(context, filters=filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=True) for image in images]) diff --git a/nova/api/openstack/schemas/atom-link.rng b/nova/api/openstack/schemas/atom-link.rng new file mode 100644 index 000000000..edba5eee6 --- /dev/null +++ b/nova/api/openstack/schemas/atom-link.rng @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + -*- rnc -*- + RELAX NG Compact Syntax Grammar for the + Atom Format Specification Version 11 +--> +<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <start> + <choice> + <ref name="atomLink"/> + </choice> + </start> + <!-- Common attributes --> + <define name="atomCommonAttributes"> + <optional> + <attribute name="xml:base"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="xml:lang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <zeroOrMore> + <ref name="undefinedAttribute"/> + </zeroOrMore> + </define> + <!-- atom:link --> + <define name="atomLink"> + <element name="atom:link"> + <ref name="atomCommonAttributes"/> + <attribute name="href"> + <ref name="atomUri"/> + </attribute> + <optional> + <attribute name="rel"> + <choice> + <ref name="atomNCName"/> + <ref name="atomUri"/> + </choice> + </attribute> + </optional> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <optional> + <attribute name="hreflang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <optional> + <attribute name="title"/> + </optional> + <optional> + <attribute name="length"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- Low-level simple types --> + <define name="atomNCName"> + <data type="string"> + <param name="minLength">1</param> + <param name="pattern">[^:]*</param> + </data> + </define> + <!-- Whatever a media type is, it contains at least one slash --> + <define name="atomMediaType"> + <data type="string"> + <param name="pattern">.+/.+</param> + </data> + </define> + <!-- As defined in RFC 3066 --> + <define name="atomLanguageTag"> + <data type="string"> + <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param> + </data> + </define> + <!-- + Unconstrained; it's not entirely clear how IRI fit into + xsd:anyURI so let's not try to constrain it here + --> + <define name="atomUri"> + <text/> + </define> + <!-- Other Extensibility --> + <define name="undefinedAttribute"> + <attribute> + <anyName> + <except> + <name>xml:base</name> + <name>xml:lang</name> + <nsName ns=""/> + </except> + </anyName> + </attribute> + </define> + <define name="undefinedContent"> + <zeroOrMore> + <choice> + <text/> + <ref name="anyForeignElement"/> + </choice> + </zeroOrMore> + </define> + <define name="anyElement"> + <element> + <anyName/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="anyForeignElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> +</grammar> diff --git a/nova/api/openstack/schemas/atom.rng b/nova/api/openstack/schemas/atom.rng new file mode 100644 index 000000000..c2df4e410 --- /dev/null +++ b/nova/api/openstack/schemas/atom.rng @@ -0,0 +1,597 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + -*- rnc -*- + RELAX NG Compact Syntax Grammar for the + Atom Format Specification Version 11 +--> +<grammar xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:s="http://www.ascc.net/xml/schematron" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <start> + <choice> + <ref name="atomFeed"/> + <ref name="atomEntry"/> + </choice> + </start> + <!-- Common attributes --> + <define name="atomCommonAttributes"> + <optional> + <attribute name="xml:base"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="xml:lang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <zeroOrMore> + <ref name="undefinedAttribute"/> + </zeroOrMore> + </define> + <!-- Text Constructs --> + <define name="atomPlainTextConstruct"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <choice> + <value>text</value> + <value>html</value> + </choice> + </attribute> + </optional> + <text/> + </define> + <define name="atomXHTMLTextConstruct"> + <ref name="atomCommonAttributes"/> + <attribute name="type"> + <value>xhtml</value> + </attribute> + <ref name="xhtmlDiv"/> + </define> + <define name="atomTextConstruct"> + <choice> + <ref name="atomPlainTextConstruct"/> + <ref name="atomXHTMLTextConstruct"/> + </choice> + </define> + <!-- Person Construct --> + <define name="atomPersonConstruct"> + <ref name="atomCommonAttributes"/> + <interleave> + <element name="atom:name"> + <text/> + </element> + <optional> + <element name="atom:uri"> + <ref name="atomUri"/> + </element> + </optional> + <optional> + <element name="atom:email"> + <ref name="atomEmailAddress"/> + </element> + </optional> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </define> + <!-- Date Construct --> + <define name="atomDateConstruct"> + <ref name="atomCommonAttributes"/> + <data type="dateTime"/> + </define> + <!-- atom:feed --> + <define name="atomFeed"> + <element name="atom:feed"> + <s:rule context="atom:feed"> + <s:assert test="atom:author or not(atom:entry[not(atom:author)])">An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author.</s:assert> + </s:rule> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <optional> + <ref name="atomGenerator"/> + </optional> + <optional> + <ref name="atomIcon"/> + </optional> + <ref name="atomId"/> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomLogo"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSubtitle"/> + </optional> + <ref name="atomTitle"/> + <ref name="atomUpdated"/> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + <zeroOrMore> + <ref name="atomEntry"/> + </zeroOrMore> + </element> + </define> + <!-- atom:entry --> + <define name="atomEntry"> + <element name="atom:entry"> + <s:rule context="atom:entry"> + <s:assert test="atom:link[@rel='alternate'] or atom:link[not(@rel)] or atom:content">An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content.</s:assert> + </s:rule> + <s:rule context="atom:entry"> + <s:assert test="atom:author or ../atom:author or atom:source/atom:author">An atom:entry must have an atom:author if its feed does not.</s:assert> + </s:rule> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <optional> + <ref name="atomContent"/> + </optional> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <ref name="atomId"/> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomPublished"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSource"/> + </optional> + <optional> + <ref name="atomSummary"/> + </optional> + <ref name="atomTitle"/> + <ref name="atomUpdated"/> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </element> + </define> + <!-- atom:content --> + <define name="atomInlineTextContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <choice> + <value>text</value> + <value>html</value> + </choice> + </attribute> + </optional> + <zeroOrMore> + <text/> + </zeroOrMore> + </element> + </define> + <define name="atomInlineXHTMLContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <attribute name="type"> + <value>xhtml</value> + </attribute> + <ref name="xhtmlDiv"/> + </element> + </define> + <define name="atomInlineOtherContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="atomOutOfLineContent"> + <element name="atom:content"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <attribute name="src"> + <ref name="atomUri"/> + </attribute> + <empty/> + </element> + </define> + <define name="atomContent"> + <choice> + <ref name="atomInlineTextContent"/> + <ref name="atomInlineXHTMLContent"/> + <ref name="atomInlineOtherContent"/> + <ref name="atomOutOfLineContent"/> + </choice> + </define> + <!-- atom:author --> + <define name="atomAuthor"> + <element name="atom:author"> + <ref name="atomPersonConstruct"/> + </element> + </define> + <!-- atom:category --> + <define name="atomCategory"> + <element name="atom:category"> + <ref name="atomCommonAttributes"/> + <attribute name="term"/> + <optional> + <attribute name="scheme"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="label"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- atom:contributor --> + <define name="atomContributor"> + <element name="atom:contributor"> + <ref name="atomPersonConstruct"/> + </element> + </define> + <!-- atom:generator --> + <define name="atomGenerator"> + <element name="atom:generator"> + <ref name="atomCommonAttributes"/> + <optional> + <attribute name="uri"> + <ref name="atomUri"/> + </attribute> + </optional> + <optional> + <attribute name="version"/> + </optional> + <text/> + </element> + </define> + <!-- atom:icon --> + <define name="atomIcon"> + <element name="atom:icon"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:id --> + <define name="atomId"> + <element name="atom:id"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:logo --> + <define name="atomLogo"> + <element name="atom:logo"> + <ref name="atomCommonAttributes"/> + <ref name="atomUri"/> + </element> + </define> + <!-- atom:link --> + <define name="atomLink"> + <element name="atom:link"> + <ref name="atomCommonAttributes"/> + <attribute name="href"> + <ref name="atomUri"/> + </attribute> + <optional> + <attribute name="rel"> + <choice> + <ref name="atomNCName"/> + <ref name="atomUri"/> + </choice> + </attribute> + </optional> + <optional> + <attribute name="type"> + <ref name="atomMediaType"/> + </attribute> + </optional> + <optional> + <attribute name="hreflang"> + <ref name="atomLanguageTag"/> + </attribute> + </optional> + <optional> + <attribute name="title"/> + </optional> + <optional> + <attribute name="length"/> + </optional> + <ref name="undefinedContent"/> + </element> + </define> + <!-- atom:published --> + <define name="atomPublished"> + <element name="atom:published"> + <ref name="atomDateConstruct"/> + </element> + </define> + <!-- atom:rights --> + <define name="atomRights"> + <element name="atom:rights"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:source --> + <define name="atomSource"> + <element name="atom:source"> + <ref name="atomCommonAttributes"/> + <interleave> + <zeroOrMore> + <ref name="atomAuthor"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomCategory"/> + </zeroOrMore> + <zeroOrMore> + <ref name="atomContributor"/> + </zeroOrMore> + <optional> + <ref name="atomGenerator"/> + </optional> + <optional> + <ref name="atomIcon"/> + </optional> + <optional> + <ref name="atomId"/> + </optional> + <zeroOrMore> + <ref name="atomLink"/> + </zeroOrMore> + <optional> + <ref name="atomLogo"/> + </optional> + <optional> + <ref name="atomRights"/> + </optional> + <optional> + <ref name="atomSubtitle"/> + </optional> + <optional> + <ref name="atomTitle"/> + </optional> + <optional> + <ref name="atomUpdated"/> + </optional> + <zeroOrMore> + <ref name="extensionElement"/> + </zeroOrMore> + </interleave> + </element> + </define> + <!-- atom:subtitle --> + <define name="atomSubtitle"> + <element name="atom:subtitle"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:summary --> + <define name="atomSummary"> + <element name="atom:summary"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:title --> + <define name="atomTitle"> + <element name="atom:title"> + <ref name="atomTextConstruct"/> + </element> + </define> + <!-- atom:updated --> + <define name="atomUpdated"> + <element name="atom:updated"> + <ref name="atomDateConstruct"/> + </element> + </define> + <!-- Low-level simple types --> + <define name="atomNCName"> + <data type="string"> + <param name="minLength">1</param> + <param name="pattern">[^:]*</param> + </data> + </define> + <!-- Whatever a media type is, it contains at least one slash --> + <define name="atomMediaType"> + <data type="string"> + <param name="pattern">.+/.+</param> + </data> + </define> + <!-- As defined in RFC 3066 --> + <define name="atomLanguageTag"> + <data type="string"> + <param name="pattern">[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*</param> + </data> + </define> + <!-- + Unconstrained; it's not entirely clear how IRI fit into + xsd:anyURI so let's not try to constrain it here + --> + <define name="atomUri"> + <text/> + </define> + <!-- Whatever an email address is, it contains at least one @ --> + <define name="atomEmailAddress"> + <data type="string"> + <param name="pattern">.+@.+</param> + </data> + </define> + <!-- Simple Extension --> + <define name="simpleExtensionElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <text/> + </element> + </define> + <!-- Structured Extension --> + <define name="structuredExtensionElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <choice> + <group> + <oneOrMore> + <attribute> + <anyName/> + </attribute> + </oneOrMore> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </group> + <group> + <zeroOrMore> + <attribute> + <anyName/> + </attribute> + </zeroOrMore> + <group> + <optional> + <text/> + </optional> + <oneOrMore> + <ref name="anyElement"/> + </oneOrMore> + <zeroOrMore> + <choice> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </group> + </group> + </choice> + </element> + </define> + <!-- Other Extensibility --> + <define name="extensionElement"> + <choice> + <ref name="simpleExtensionElement"/> + <ref name="structuredExtensionElement"/> + </choice> + </define> + <define name="undefinedAttribute"> + <attribute> + <anyName> + <except> + <name>xml:base</name> + <name>xml:lang</name> + <nsName ns=""/> + </except> + </anyName> + </attribute> + </define> + <define name="undefinedContent"> + <zeroOrMore> + <choice> + <text/> + <ref name="anyForeignElement"/> + </choice> + </zeroOrMore> + </define> + <define name="anyElement"> + <element> + <anyName/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="anyForeignElement"> + <element> + <anyName> + <except> + <nsName ns="http://www.w3.org/2005/Atom"/> + </except> + </anyName> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyElement"/> + </choice> + </zeroOrMore> + </element> + </define> + <!-- XHTML --> + <define name="anyXHTML"> + <element> + <nsName ns="http://www.w3.org/1999/xhtml"/> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyXHTML"/> + </choice> + </zeroOrMore> + </element> + </define> + <define name="xhtmlDiv"> + <element name="xhtml:div"> + <zeroOrMore> + <choice> + <attribute> + <anyName/> + </attribute> + <text/> + <ref name="anyXHTML"/> + </choice> + </zeroOrMore> + </element> + </define> +</grammar> diff --git a/nova/api/openstack/schemas/v1.1/extension.rng b/nova/api/openstack/schemas/v1.1/extension.rng new file mode 100644 index 000000000..336659755 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/extension.rng @@ -0,0 +1,11 @@ +<element name="extension" ns="http://docs.openstack.org/compute/api/v1.1" + xmlns="http://relaxng.org/ns/structure/1.0"> + <attribute name="alias"> <text/> </attribute> + <attribute name="name"> <text/> </attribute> + <attribute name="namespace"> <text/> </attribute> + <attribute name="updated"> <text/> </attribute> + <element name="description"> <text/> </element> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/schemas/v1.1/extensions.rng b/nova/api/openstack/schemas/v1.1/extensions.rng new file mode 100644 index 000000000..4d8bff646 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/extensions.rng @@ -0,0 +1,6 @@ +<element name="extensions" xmlns="http://relaxng.org/ns/structure/1.0" + ns="http://docs.openstack.org/compute/api/v1.1"> + <zeroOrMore> + <externalRef href="extension.rng"/> + </zeroOrMore> +</element> diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index b0b014f86..2b235f79a 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -57,18 +57,12 @@ class Controller(object): context = req.environ['nova.context'] - try: - self.compute_api.update_or_create_instance_metadata(context, - server_id, - metadata) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=False) - except quota.QuotaError as error: - self._handle_quota_error(error) - - return body + return {'metadata': new_metadata} def update(self, req, server_id, id, body): try: @@ -78,19 +72,22 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) try: - meta_value = meta_item.pop(id) + meta_value = meta_item[id] except (AttributeError, KeyError): expl = _('Request body and URI mismatch') raise exc.HTTPBadRequest(explanation=expl) - if len(meta_item) > 0: + if len(meta_item) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._set_instance_metadata(context, server_id, meta_item) + self._update_instance_metadata(context, + server_id, + meta_item, + delete=False) - return {'meta': {id: meta_value}} + return {'meta': meta_item} def update_all(self, req, server_id, body): try: @@ -100,20 +97,26 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._set_instance_metadata(context, server_id, metadata) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=True) - return {'metadata': metadata} + return {'metadata': new_metadata} - def _set_instance_metadata(self, context, server_id, metadata): + def _update_instance_metadata(self, context, server_id, metadata, + delete=False): try: - self.compute_api.update_or_create_instance_metadata(context, - server_id, - metadata) + return self.compute_api.update_instance_metadata(context, + server_id, + metadata, + delete) + except exception.InstanceNotFound: msg = _('Server does not exist') raise exc.HTTPNotFound(explanation=msg) - except ValueError: + except (ValueError, AttributeError): msg = _("Malformed request body") raise exc.HTTPBadRequest(explanation=msg) @@ -138,12 +141,12 @@ class Controller(object): metadata = self._get_metadata(context, server_id) try: - meta_key = metadata[id] + meta_value = metadata[id] except KeyError: msg = _("Metadata item was not found") raise exc.HTTPNotFound(explanation=msg) - self.compute_api.delete_instance_metadata(context, server_id, meta_key) + self.compute_api.delete_instance_metadata(context, server_id, id) def _handle_quota_error(self, error): """Reraise quota errors as api-specific http exceptions.""" diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3930982dc..335ecad86 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -44,7 +44,7 @@ FLAGS = flags.FLAGS class Controller(object): - """ The Server API controller for the OpenStack API """ + """ The Server API base controller class for the OpenStack API """ def __init__(self): self.compute_api = compute.API() @@ -53,17 +53,21 @@ class Controller(object): def index(self, req): """ Returns a list of server names and ids for a given user """ try: - servers = self._items(req, is_detail=False) + servers = self._get_servers(req, is_detail=False) except exception.Invalid as err: return exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound: + return exc.HTTPNotFound() return servers def detail(self, req): """ Returns a list of server details for a given user """ try: - servers = self._items(req, is_detail=True) + servers = self._get_servers(req, is_detail=True) except exception.Invalid as err: return exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound as err: + return exc.HTTPNotFound() return servers def _build_view(self, req, instance, is_detail=False): @@ -75,22 +79,55 @@ class Controller(object): def _action_rebuild(self, info, request, instance_id): raise NotImplementedError() - def _items(self, req, is_detail): - """Returns a list of servers for a given user. - - builder - the response model builder + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. """ - query_str = req.str_GET - reservation_id = query_str.get('reservation_id') - project_id = query_str.get('project_id') - fixed_ip = query_str.get('fixed_ip') - recurse_zones = utils.bool_from_str(query_str.get('recurse_zones')) + + search_opts = {} + search_opts.update(req.str_GET) + + context = req.environ['nova.context'] + remove_invalid_options(context, search_opts, + self._get_server_search_options()) + + # Convert recurse_zones into a boolean + search_opts['recurse_zones'] = utils.bool_from_str( + search_opts.get('recurse_zones', False)) + + # If search by 'status', we need to convert it to 'state' + # If the status is unknown, bail. + # Leave 'state' in search_opts so compute can pass it on to + # child zones.. + if 'status' in search_opts: + status = search_opts['status'] + search_opts['state'] = common.power_states_from_status(status) + if len(search_opts['state']) == 0: + reason = _('Invalid server status: %(status)s') % locals() + LOG.error(reason) + raise exception.InvalidInput(reason=reason) + + # By default, compute's get_all() will return deleted instances. + # If an admin hasn't specified a 'deleted' search option, we need + # to filter out deleted instances by setting the filter ourselves. + # ... Unless 'changes-since' is specified, because 'changes-since' + # should return recently deleted images according to the API spec. + + if 'deleted' not in search_opts: + # Admin hasn't specified deleted filter + if 'changes-since' not in search_opts: + # No 'changes-since', so we need to find non-deleted servers + search_opts['deleted'] = False + else: + # This is the default, but just in case.. + search_opts['deleted'] = True + instance_list = self.compute_api.get_all( - req.environ['nova.context'], - reservation_id=reservation_id, - project_id=project_id, - fixed_ip=fixed_ip, - recurse_zones=recurse_zones) + context, search_opts=search_opts) + + # FIXME(comstud): 'changes-since' is not fully implemented. Where + # should this be filtered? + limited_list = self._limit_items(instance_list, req) servers = [self._build_view(req, inst, is_detail)['server'] for inst in limited_list] @@ -126,7 +163,7 @@ class Controller(object): @scheduler_api.redirect_handler def update(self, req, id, body): - """ Updates the server name or password """ + """Update server name then pass on to version-specific controller""" if len(req.body) == 0: raise exc.HTTPUnprocessableEntity() @@ -141,17 +178,15 @@ class Controller(object): self.helper._validate_server_name(name) update_dict['display_name'] = name.strip() - self._parse_update(ctxt, id, body, update_dict) - try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: raise exc.HTTPNotFound() - return exc.HTTPNoContent() + return self._update(ctxt, req, id, body) - def _parse_update(self, context, id, inst_dict, update_dict): - pass + def _update(self, context, req, id, inst_dict): + return exc.HTTPNotImplemented() @scheduler_api.redirect_handler def action(self, req, id, body): @@ -173,11 +208,15 @@ class Controller(object): } self.actions.update(admin_actions) - for key in self.actions.keys(): - if key in body: + for key in body: + if key in self.actions: return self.actions[key](body, req, id) + else: + msg = _("There is no such server action: %s") % (key,) + raise exc.HTTPBadRequest(explanation=msg) - raise exc.HTTPNotImplemented() + msg = _("Invalid request body") + raise exc.HTTPBadRequest(explanation=msg) def _action_create_backup(self, input_dict, req, instance_id): """Backup a server instance. @@ -218,13 +257,14 @@ class Controller(object): props = {'instance_ref': server_ref} metadata = entity.get('metadata', {}) + context = req.environ["nova.context"] + common.check_img_metadata_quota_limit(context, metadata) try: props.update(metadata) except ValueError: msg = _("Invalid metadata") raise webob.exc.HTTPBadRequest(explanation=msg) - context = req.environ["nova.context"] image = self.compute_api.backup(context, instance_id, image_name, @@ -240,6 +280,7 @@ class Controller(object): resp.headers['Location'] = image_ref return resp + @common.check_snapshots_enabled def _action_create_image(self, input_dict, req, id): return exc.HTTPNotImplemented() @@ -267,10 +308,16 @@ class Controller(object): def _action_reboot(self, input_dict, req, id): if 'reboot' in input_dict and 'type' in input_dict['reboot']: - reboot_type = input_dict['reboot']['type'] + valid_reboot_types = ['HARD', 'SOFT'] + reboot_type = input_dict['reboot']['type'].upper() + if not valid_reboot_types.count(reboot_type): + msg = _("Argument 'type' for reboot is not HARD or SOFT") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) else: - LOG.exception(_("Missing argument 'type' for reboot")) - raise exc.HTTPUnprocessableEntity() + msg = _("Missing argument 'type' for reboot") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) try: # TODO(gundlach): pass reboot_type, support soft reboot in # virt driver @@ -498,6 +545,7 @@ class Controller(object): class ControllerV10(Controller): + """v1.0 OpenStack API controller""" @scheduler_api.redirect_handler def delete(self, req, id): @@ -522,10 +570,11 @@ class ControllerV10(Controller): def _limit_items(self, items, req): return common.limited(items, req) - def _parse_update(self, context, server_id, inst_dict, update_dict): + def _update(self, context, req, id, inst_dict): if 'adminPass' in inst_dict['server']: - self.compute_api.set_admin_password(context, server_id, + self.compute_api.set_admin_password(context, id, inst_dict['server']['adminPass']) + return exc.HTTPNoContent() def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ @@ -560,8 +609,13 @@ class ControllerV10(Controller): """ Determine the admin password for a server on creation """ return self.helper._get_server_admin_password_old_style(server) + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones' + class ControllerV11(Controller): + """v1.1 OpenStack API controller""" @scheduler_api.redirect_handler def delete(self, req, id): @@ -642,10 +696,17 @@ class ControllerV11(Controller): LOG.info(msg) raise exc.HTTPBadRequest(explanation=msg) + def _update(self, context, req, id, inst_dict): + instance = self.compute_api.routing_get(context, id) + return self._build_view(req, instance, is_detail=True) + def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ try: flavor_ref = input_dict["resize"]["flavorRef"] + if not flavor_ref: + msg = _("Resize request has invalid 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) except (KeyError, TypeError): msg = _("Resize requests require 'flavorRef' attribute.") raise exc.HTTPBadRequest(explanation=msg) @@ -680,6 +741,7 @@ class ControllerV11(Controller): return webob.Response(status_int=202) + @common.check_snapshots_enabled def _action_create_image(self, input_dict, req, instance_id): """Snapshot a server instance.""" entity = input_dict.get("createImage", {}) @@ -702,13 +764,14 @@ class ControllerV11(Controller): props = {'instance_ref': server_ref} metadata = entity.get('metadata', {}) + context = req.environ['nova.context'] + common.check_img_metadata_quota_limit(context, metadata) try: props.update(metadata) except ValueError: msg = _("Invalid metadata") raise webob.exc.HTTPBadRequest(explanation=msg) - context = req.environ['nova.context'] image = self.compute_api.snapshot(context, instance_id, image_name, @@ -729,9 +792,17 @@ class ControllerV11(Controller): """ Determine the admin password for a server on creation """ return self.helper._get_server_admin_password_new_style(server) + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return ('reservation_id', 'name', 'recurse_zones', + 'status', 'image', 'flavor', 'changes-since') + class HeadersSerializer(wsgi.ResponseHeadersSerializer): + def create(self, response, data): + response.status_int = 202 + def delete(self, response, data): response.status_int = 204 @@ -891,11 +962,31 @@ def create_resource(version='1.0'): 'application/xml': xml_serializer, } + xml_deserializer = { + '1.0': helper.ServerXMLDeserializer(), + '1.1': helper.ServerXMLDeserializerV11(), + }[version] + body_deserializers = { - 'application/xml': helper.ServerXMLDeserializer(), + 'application/xml': xml_deserializer, } serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) deserializer = wsgi.RequestDeserializer(body_deserializers) return wsgi.Resource(controller, deserializer, serializer) + + +def remove_invalid_options(context, search_options, allowed_search_options): + """Remove search options that are not valid for non-admin API/context""" + if FLAGS.allow_admin_api and context.is_admin: + # Allow all options + return + # Otherwise, strip out all unknown options + unknown_options = [opt for opt in search_options + if opt not in allowed_search_options] + unk_opt_str = ", ".join(unknown_options) + log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() + LOG.debug(log_msg) + for opt in unknown_options: + search_options.pop(opt, None) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 873ce212a..912303d14 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -77,7 +77,9 @@ class ViewBuilder(object): "status": image_obj.get("status"), }) - if image["status"] == "SAVING": + if image["status"].upper() == "ACTIVE": + image["progress"] = 100 + else: image["progress"] = 0 return image diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 2873a8e0f..8222f6766 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -20,7 +20,6 @@ import hashlib import os from nova import exception -from nova.compute import power_state import nova.compute import nova.context from nova.api.openstack import common @@ -61,24 +60,11 @@ class ViewBuilder(object): def _build_detail(self, inst): """Returns a detailed model of a server.""" - power_mapping = { - None: 'BUILD', - power_state.NOSTATE: 'BUILD', - power_state.RUNNING: 'ACTIVE', - power_state.BLOCKED: 'ACTIVE', - power_state.SUSPENDED: 'SUSPENDED', - power_state.PAUSED: 'PAUSED', - power_state.SHUTDOWN: 'SHUTDOWN', - power_state.SHUTOFF: 'SHUTOFF', - power_state.CRASHED: 'ERROR', - power_state.FAILED: 'ERROR', - power_state.BUILDING: 'BUILD', - } inst_dict = { 'id': inst['id'], 'name': inst['display_name'], - 'status': power_mapping[inst.get('state')]} + 'status': common.status_from_power_state(inst.get('state'))} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py new file mode 100644 index 000000000..97ad90ada --- /dev/null +++ b/nova/api/openstack/xmlutil.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os.path + +from lxml import etree + +from nova import utils + + +XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' +XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1' +XMLNS_ATOM = 'http://www.w3.org/2005/Atom' + + +def validate_schema(xml, schema_name): + if type(xml) is str: + xml = etree.fromstring(xml) + schema_path = os.path.join(utils.novadir(), + 'nova/api/openstack/schemas/v1.1/%s.rng' % schema_name) + schema_doc = etree.parse(schema_path) + relaxng = etree.RelaxNG(schema_doc) + relaxng.assertValid(xml) diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index f7fd87bcd..a2bf267ed 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -166,7 +166,7 @@ class Controller(object): return self.helper._get_server_admin_password_old_style(server) -class ControllerV11(object): +class ControllerV11(Controller): """Controller for 1.1 Zone resources.""" def _get_server_admin_password(self, server): diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template index d05c099d7..978ffb210 100644 --- a/nova/auth/novarc.template +++ b/nova/auth/novarc.template @@ -16,3 +16,4 @@ export NOVA_API_KEY="%(access)s" export NOVA_USERNAME="%(user)s" export NOVA_PROJECT_ID="%(project)s" export NOVA_URL="%(os)s" +export NOVA_VERSION="1.1" diff --git a/nova/block_device.py b/nova/block_device.py new file mode 100644 index 000000000..8d95e0029 --- /dev/null +++ b/nova/block_device.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata <yamahata@valinux co jp> +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + + +def properties_root_device_name(properties): + """get root device name from image meta data. + If it isn't specified, return None. + """ + root_device_name = None + + # NOTE(yamahata): see image_service.s3.s3create() + for bdm in properties.get('mappings', []): + if bdm['virtual'] == 'root': + root_device_name = bdm['device'] + + # NOTE(yamahata): register_image's command line can override + # <machine>.manifest.xml + if 'root_device_name' in properties: + root_device_name = properties['root_device_name'] + + return root_device_name + + +_ephemeral = re.compile('^ephemeral(\d|[1-9]\d+)$') + + +def is_ephemeral(device_name): + return _ephemeral.match(device_name) + + +def ephemeral_num(ephemeral_name): + assert is_ephemeral(ephemeral_name) + return int(_ephemeral.sub('\\1', ephemeral_name)) + + +def is_swap_or_ephemeral(device_name): + return device_name == 'swap' or is_ephemeral(device_name) + + +def mappings_prepend_dev(mappings): + """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type""" + for m in mappings: + virtual = m['virtual'] + if (is_swap_or_ephemeral(virtual) and + (not m['device'].startswith('/'))): + m['device'] = '/dev/' + m['device'] + return mappings + + +_dev = re.compile('^/dev/') + + +def strip_dev(device_name): + """remove leading '/dev/'""" + return _dev.sub('', device_name) diff --git a/nova/compute/api.py b/nova/compute/api.py index 80d54d029..e909e9959 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -19,9 +19,11 @@ """Handles all requests relating to instances (guest vms).""" import eventlet +import novaclient import re import time +from nova import block_device from nova import db from nova import exception from nova import flags @@ -32,7 +34,6 @@ from nova import quota from nova import rpc from nova import utils from nova import volume -from nova.api.ec2 import ec2utils from nova.compute import instance_types from nova.compute import power_state from nova.compute.utils import terminate_volumes @@ -121,8 +122,10 @@ class API(base.Base): if len(content) > content_limit: raise quota.QuotaError(code="OnsetFileContentLimitExceeded") - def _check_metadata_properties_quota(self, context, metadata={}): + def _check_metadata_properties_quota(self, context, metadata=None): """Enforce quota limits on metadata properties.""" + if not metadata: + metadata = {} num_metadata = len(metadata) quota_metadata = quota.allowed_metadata_items(context, num_metadata) if quota_metadata < num_metadata: @@ -148,7 +151,7 @@ class API(base.Base): min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', - availability_zone=None, user_data=None, metadata={}, + availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, reservation_id=None): """Verify all the input parameters regardless of the provisioning @@ -160,6 +163,8 @@ class API(base.Base): min_count = 1 if not max_count: max_count = min_count + if not metadata: + metadata = {} num_instances = quota.allowed_instances(context, max_count, instance_type) @@ -218,7 +223,7 @@ class API(base.Base): if reservation_id is None: reservation_id = utils.generate_uid('r') - root_device_name = ec2utils.properties_root_device_name( + root_device_name = block_device.properties_root_device_name( image['properties']) base_options = { @@ -250,34 +255,64 @@ class API(base.Base): return (num_instances, base_options, image) - def _update_image_block_device_mapping(self, elevated_context, instance_id, + @staticmethod + def _ephemeral_size(instance_type, ephemeral_name): + num = block_device.ephemeral_num(ephemeral_name) + + # TODO(yamahata): ephemeralN where N > 0 + # Only ephemeral0 is allowed for now because InstanceTypes + # table only allows single local disk, local_gb. + # In order to enhance it, we need to add a new columns to + # instance_types table. + if num > 0: + return 0 + + return instance_type.get('local_gb') + + def _update_image_block_device_mapping(self, elevated_context, + instance_type, instance_id, mappings): """tell vm driver to create ephemeral/swap device at boot time by updating BlockDeviceMapping """ - for bdm in ec2utils.mappings_prepend_dev(mappings): + instance_type = (instance_type or + instance_types.get_default_instance_type()) + + for bdm in block_device.mappings_prepend_dev(mappings): LOG.debug(_("bdm %s"), bdm) virtual_name = bdm['virtual'] if virtual_name == 'ami' or virtual_name == 'root': continue - assert (virtual_name == 'swap' or - virtual_name.startswith('ephemeral')) + if not block_device.is_swap_or_ephemeral(virtual_name): + continue + + size = 0 + if virtual_name == 'swap': + size = instance_type.get('swap', 0) + elif block_device.is_ephemeral(virtual_name): + size = self._ephemeral_size(instance_type, virtual_name) + + if size == 0: + continue + values = { 'instance_id': instance_id, 'device_name': bdm['device'], - 'virtual_name': virtual_name, } + 'virtual_name': virtual_name, + 'volume_size': size} self.db.block_device_mapping_update_or_create(elevated_context, values) - def _update_block_device_mapping(self, elevated_context, instance_id, + def _update_block_device_mapping(self, elevated_context, + instance_type, instance_id, block_device_mapping): """tell vm driver to attach volume at boot time by updating BlockDeviceMapping """ + LOG.debug(_("block_device_mapping %s"), block_device_mapping) for bdm in block_device_mapping: - LOG.debug(_('bdm %s'), bdm) assert 'device_name' in bdm values = {'instance_id': instance_id} @@ -286,10 +321,18 @@ class API(base.Base): 'no_device'): values[key] = bdm.get(key) + virtual_name = bdm.get('virtual_name') + if (virtual_name is not None and + block_device.is_ephemeral(virtual_name)): + size = self._ephemeral_size(instance_type, virtual_name) + if size == 0: + continue + values['volume_size'] = size + # NOTE(yamahata): NoDevice eliminates devices defined in image # files by command line option. # (--block-device-mapping) - if bdm.get('virtual_name') == 'NoDevice': + if virtual_name == 'NoDevice': values['no_device'] = True for k in ('delete_on_termination', 'volume_id', 'snapshot_id', 'volume_id', 'volume_size', @@ -299,8 +342,8 @@ class API(base.Base): self.db.block_device_mapping_update_or_create(elevated_context, values) - def create_db_entry_for_new_instance(self, context, image, base_options, - security_group, block_device_mapping, num=1): + def create_db_entry_for_new_instance(self, context, instance_type, image, + base_options, security_group, block_device_mapping, num=1): """Create an entry in the DB for this new instance, including any related table updates (such as security group, etc). @@ -333,12 +376,12 @@ class API(base.Base): security_group_id) # BlockDeviceMapping table - self._update_image_block_device_mapping(elevated, instance_id, - image['properties'].get('mappings', [])) - self._update_block_device_mapping(elevated, instance_id, + self._update_image_block_device_mapping(elevated, instance_type, + instance_id, image['properties'].get('mappings', [])) + self._update_block_device_mapping(elevated, instance_type, instance_id, image['properties'].get('block_device_mapping', [])) # override via command line option - self._update_block_device_mapping(elevated, instance_id, + self._update_block_device_mapping(elevated, instance_type, instance_id, block_device_mapping) # Set sane defaults if not specified @@ -350,16 +393,13 @@ class API(base.Base): updates['hostname'] = self.hostname_factory(instance) instance = self.update(context, instance_id, **updates) - - for group_id in security_groups: - self.trigger_security_group_members_refresh(elevated, group_id) - return instance def _ask_scheduler_to_create_instance(self, context, base_options, instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=None, num_instances=1): """Send the run_instance request to the schedulers for processing.""" pid = context.project_id @@ -373,6 +413,7 @@ class API(base.Base): filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter' request_spec = { + 'image': image, 'instance_properties': base_options, 'instance_type': instance_type, 'filter': filter_class, @@ -395,12 +436,16 @@ class API(base.Base): min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', - availability_zone=None, user_data=None, metadata={}, + availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, reservation_id=None, block_device_mapping=None): """Provision the instances by passing the whole request to the Scheduler for execution. Returns a Reservation ID related to the creation of all of these instances.""" + + if not metadata: + metadata = {} + num_instances, base_options, image = self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -415,6 +460,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, num_instances=num_instances) return base_options['reservation_id'] @@ -424,7 +470,7 @@ class API(base.Base): min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', - availability_zone=None, user_data=None, metadata={}, + availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, reservation_id=None, block_device_mapping=None): """ @@ -439,6 +485,9 @@ class API(base.Base): Returns a list of instance dicts. """ + if not metadata: + metadata = {} + num_instances, base_options, image = self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, @@ -453,7 +502,8 @@ class API(base.Base): instances = [] LOG.debug(_("Going to run %s instances..."), num_instances) for num in range(num_instances): - instance = self.create_db_entry_for_new_instance(context, image, + instance = self.create_db_entry_for_new_instance(context, + instance_type, image, base_options, security_group, block_device_mapping, num=num) instances.append(instance) @@ -463,6 +513,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=instance_id) return [dict(x.iteritems()) for x in instances] @@ -510,18 +561,20 @@ class API(base.Base): {"method": "refresh_security_group_rules", "args": {"security_group_id": security_group.id}}) - def trigger_security_group_members_refresh(self, context, group_id): + def trigger_security_group_members_refresh(self, context, group_ids): """Called when a security group gains a new or loses a member. Sends an update request to each compute node for whom this is relevant. """ - # First, we get the security group rules that reference this group as + # First, we get the security group rules that reference these groups as # the grantee.. - security_group_rules = \ + security_group_rules = set() + for group_id in group_ids: + security_group_rules.update( self.db.security_group_rule_get_by_security_group_grantee( context, - group_id) + group_id)) # ..then we distill the security groups to which they belong.. security_groups = set() @@ -669,59 +722,84 @@ class API(base.Base): """ return self.get(context, instance_id) - def get_all(self, context, project_id=None, reservation_id=None, - fixed_ip=None, recurse_zones=False): + def get_all(self, context, search_opts=None): """Get all instances filtered by one of the given parameters. If there is no filter and the context is an admin, it will retreive all instances in the system. """ - if reservation_id is not None: - recurse_zones = True - instances = self.db.instance_get_all_by_reservation( - context, reservation_id) - elif fixed_ip is not None: + if search_opts is None: + search_opts = {} + + LOG.debug(_("Searching by: %s") % str(search_opts)) + + # Fixups for the DB call + filters = {} + + def _remap_flavor_filter(flavor_id): + instance_type = self.db.instance_type_get_by_flavor_id( + context, flavor_id) + filters['instance_type_id'] = instance_type['id'] + + def _remap_fixed_ip_filter(fixed_ip): + # Turn fixed_ip into a regexp match. Since '.' matches + # any character, we need to use regexp escaping for it. + filters['ip'] = '^%s$' % fixed_ip.replace('.', '\\.') + + # search_option to filter_name mapping. + filter_mapping = { + 'image': 'image_ref', + 'name': 'display_name', + 'instance_name': 'name', + 'recurse_zones': None, + 'flavor': _remap_flavor_filter, + 'fixed_ip': _remap_fixed_ip_filter} + + # copy from search_opts, doing various remappings as necessary + for opt, value in search_opts.iteritems(): + # Do remappings. + # Values not in the filter_mapping table are copied as-is. + # If remapping is None, option is not copied + # If the remapping is a string, it is the filter_name to use try: - instances = self.db.fixed_ip_get_instance(context, fixed_ip) - except exception.FloatingIpNotFound, e: - if not recurse_zones: - raise - instances = None - elif project_id or not context.is_admin: - if not context.project_id: - instances = self.db.instance_get_all_by_user( - context, context.user_id) + remap_object = filter_mapping[opt] + except KeyError: + filters[opt] = value else: - if project_id is None: - project_id = context.project_id - instances = self.db.instance_get_all_by_project( - context, project_id) - else: - instances = self.db.instance_get_all(context) + if remap_object: + if isinstance(remap_object, basestring): + filters[remap_object] = value + else: + remap_object(value) + + recurse_zones = search_opts.get('recurse_zones', False) + if 'reservation_id' in filters: + recurse_zones = True - if instances is None: - instances = [] - elif not isinstance(instances, list): - instances = [instances] + instances = self.db.instance_get_all_by_filters(context, filters) if not recurse_zones: return instances + # Recurse zones. Need admin context for this. Send along + # the un-modified search options we received.. admin_context = context.elevated() children = scheduler_api.call_zone_method(admin_context, "list", + errors_to_ignore=[novaclient.exceptions.NotFound], novaclient_collection_name="servers", - reservation_id=reservation_id, - project_id=project_id, - fixed_ip=fixed_ip, - recurse_zones=True) + search_opts=search_opts) for zone, servers in children: + # 'servers' can be None if a 404 was returned by a zone + if servers is None: + continue for server in servers: # Results are ready to send to user. No need to scrub. server._info['_is_precooked'] = True instances.append(server._info) + return instances def _cast_compute_message(self, method, context, instance_id, host=None, @@ -993,7 +1071,12 @@ class API(base.Base): def set_host_enabled(self, context, host, enabled): """Sets the specified host's ability to accept new instances.""" return self._call_compute_message("set_host_enabled", context, - instance_id=None, host=host, params={"enabled": enabled}) + host=host, params={"enabled": enabled}) + + def host_power_action(self, context, host, action): + """Reboots, shuts down or powers up the host.""" + return self._call_compute_message("host_power_action", context, + host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): @@ -1166,11 +1249,20 @@ class API(base.Base): """Delete the given metadata item from an instance.""" self.db.instance_metadata_delete(context, instance_id, key) - def update_or_create_instance_metadata(self, context, instance_id, - metadata): - """Updates or creates instance metadata.""" - combined_metadata = self.get_instance_metadata(context, instance_id) - combined_metadata.update(metadata) - self._check_metadata_properties_quota(context, combined_metadata) - self.db.instance_metadata_update_or_create(context, instance_id, - metadata) + def update_instance_metadata(self, context, instance_id, + metadata, delete=False): + """Updates or creates instance metadata. + + If delete is True, metadata items that are not specified in the + `metadata` argument will be deleted. + + """ + if delete: + _metadata = metadata + else: + _metadata = self.get_instance_metadata(context, instance_id) + _metadata.update(metadata) + + self._check_metadata_properties_quota(context, _metadata) + self.db.instance_metadata_update(context, instance_id, _metadata, True) + return _metadata diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 9f566dea7..16b8e14b4 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -45,6 +45,7 @@ import functools from eventlet import greenthread import nova.context +from nova import block_device from nova import exception from nova import flags import nova.image @@ -169,7 +170,9 @@ class ComputeManager(manager.SchedulerDependentManager): elif drv_state == power_state.RUNNING: # Hyper-V and VMWareAPI drivers will raise and exception try: - self.driver.ensure_filtering_rules_for_instance(instance) + net_info = self._get_instance_nw_info(context, instance) + self.driver.ensure_filtering_rules_for_instance(instance, + net_info) except NotImplementedError: LOG.warning(_('Hypervisor driver does not ' 'support firewall rules')) @@ -260,6 +263,8 @@ class ComputeManager(manager.SchedulerDependentManager): volume_api = volume.API() block_device_mapping = [] + swap = None + ephemerals = [] for bdm in self.db.block_device_mapping_get_all_by_instance( context, instance_id): LOG.debug(_("setting up bdm %s"), bdm) @@ -267,11 +272,18 @@ class ComputeManager(manager.SchedulerDependentManager): if bdm['no_device']: continue if bdm['virtual_name']: - # TODO(yamahata): - # block devices for swap and ephemeralN will be - # created by virt driver locally in compute node. - assert (bdm['virtual_name'] == 'swap' or - bdm['virtual_name'].startswith('ephemeral')) + virtual_name = bdm['virtual_name'] + device_name = bdm['device_name'] + assert block_device.is_swap_or_ephemeral(virtual_name) + if virtual_name == 'swap': + swap = {'device_name': device_name, + 'swap_size': bdm['volume_size']} + elif block_device.is_ephemeral(virtual_name): + eph = {'num': block_device.ephemeral_num(virtual_name), + 'virtual_name': virtual_name, + 'device_name': device_name, + 'size': bdm['volume_size']} + ephemerals.append(eph) continue if ((bdm['snapshot_id'] is not None) and @@ -307,7 +319,7 @@ class ComputeManager(manager.SchedulerDependentManager): 'mount_device': bdm['device_name']}) - return block_device_mapping + return (swap, ephemerals, block_device_mapping) def _run_instance(self, context, instance_id, **kwargs): """Launch a new instance with specified options.""" @@ -348,13 +360,21 @@ class ComputeManager(manager.SchedulerDependentManager): # all vif creation and network injection, maybe this is correct network_info = [] - bd_mapping = self._setup_block_device_mapping(context, instance_id) + (swap, ephemerals, + block_device_mapping) = self._setup_block_device_mapping( + context, instance_id) + block_device_info = { + 'root_device_name': instance['root_device_name'], + 'swap': swap, + 'ephemerals': ephemerals, + 'block_device_mapping': block_device_mapping} # TODO(vish) check to make sure the availability zone matches self._update_state(context, instance_id, power_state.BUILDING) try: - self.driver.spawn(context, instance, network_info, bd_mapping) + self.driver.spawn(context, instance, + network_info, block_device_info) except Exception as ex: # pylint: disable=W0702 msg = _("Instance '%(instance_id)s' failed to spawn. Is " "virtualization enabled in the BIOS? Details: " @@ -748,7 +768,8 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref['host']) rpc.cast(context, topic, {'method': 'finish_revert_resize', - 'args': {'migration_id': migration_ref['id']}, + 'args': {'instance_id': instance_ref['uuid'], + 'migration_id': migration_ref['id']}, }) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @@ -957,8 +978,12 @@ class ComputeManager(manager.SchedulerDependentManager): result)) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def set_host_enabled(self, context, instance_id=None, host=None, - enabled=None): + def host_power_action(self, context, host=None, action=None): + """Reboots, shuts down or powers up the host.""" + return self.driver.host_power_action(host, action) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + def set_host_enabled(self, context, host=None, enabled=None): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) @@ -1201,6 +1226,7 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def check_shared_storage_test_file(self, context, filename): """Confirms existence of the tmpfile under FLAGS.instances_path. + Cannot confirm tmpfile return False. :param context: security context :param filename: confirm existence of FLAGS.instances_path/thisfile @@ -1208,7 +1234,9 @@ class ComputeManager(manager.SchedulerDependentManager): """ tmp_file = os.path.join(FLAGS.instances_path, filename) if not os.path.exists(tmp_file): - raise exception.FileNotFound(file_path=tmp_file) + return False + else: + return True @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def cleanup_shared_storage_test_file(self, context, filename): @@ -1231,11 +1259,13 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.update_available_resource(context, self.host) - def pre_live_migration(self, context, instance_id, time=None): + def pre_live_migration(self, context, instance_id, time=None, + block_migration=False, disk=None): """Preparations for live migration at dest host. :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param block_migration: if true, prepare for block migration """ if not time: @@ -1285,19 +1315,26 @@ class ComputeManager(manager.SchedulerDependentManager): # This nwfilter is necessary on the destination host. # In addition, this method is creating filtering rule # onto destination host. - self.driver.ensure_filtering_rules_for_instance(instance_ref) + self.driver.ensure_filtering_rules_for_instance(instance_ref, network_info) - def live_migration(self, context, instance_id, dest): + # Preparation for block migration + if block_migration: + self.driver.pre_block_migration(context, + instance_ref, + disk) + + def live_migration(self, context, instance_id, + dest, block_migration=False): """Executing live migration. :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param dest: destination host + :param block_migration: if true, do block migration """ # Get instance for error handling. instance_ref = self.db.instance_get(context, instance_id) - i_name = instance_ref.name try: # Checking volume node is working correctly when any volumes @@ -1308,16 +1345,25 @@ class ComputeManager(manager.SchedulerDependentManager): {"method": "check_for_export", "args": {'instance_id': instance_id}}) - # Asking dest host to preparing live migration. + if block_migration: + disk = self.driver.get_instance_disk_info(context, + instance_ref) + else: + disk = None + rpc.call(context, self.db.queue_get_for(context, FLAGS.compute_topic, dest), {"method": "pre_live_migration", - "args": {'instance_id': instance_id}}) + "args": {'instance_id': instance_id, + 'block_migration': block_migration, + 'disk': disk}}) except Exception: + i_name = instance_ref.name msg = _("Pre live migration for %(i_name)s failed at %(dest)s") LOG.error(msg % locals()) - self.recover_live_migration(context, instance_ref) + self.rollback_live_migration(context, instance_ref, + dest, block_migration) raise # Executing live migration @@ -1325,9 +1371,11 @@ class ComputeManager(manager.SchedulerDependentManager): # nothing must be recovered in this version. self.driver.live_migration(context, instance_ref, dest, self.post_live_migration, - self.recover_live_migration) + self.rollback_live_migration, + block_migration) - def post_live_migration(self, ctxt, instance_ref, dest): + def post_live_migration(self, ctxt, instance_ref, + dest, block_migration=False): """Post operations for live migration. This method is called from live_migration @@ -1336,6 +1384,7 @@ class ComputeManager(manager.SchedulerDependentManager): :param ctxt: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id :param dest: destination host + :param block_migration: if true, do block migration """ @@ -1378,8 +1427,29 @@ class ComputeManager(manager.SchedulerDependentManager): "%(i_name)s cannot inherit floating " "ip.\n%(e)s") % (locals())) - # Restore instance/volume state - self.recover_live_migration(ctxt, instance_ref, dest) + # Define domain at destination host, without doing it, + # pause/suspend/terminate do not work. + rpc.call(ctxt, + self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), + {"method": "post_live_migration_at_destination", + "args": {'instance_id': instance_ref.id, + 'block_migration': block_migration}}) + + # Restore instance state + self.db.instance_update(ctxt, + instance_ref['id'], + {'state_description': 'running', + 'state': power_state.RUNNING, + 'host': dest}) + # Restore volume state + for volume_ref in instance_ref['volumes']: + volume_id = volume_ref['id'] + self.db.volume_update(ctxt, volume_id, {'status': 'in-use'}) + + # No instance booting at source host, but instance dir + # must be deleted for preparing next block migration + if block_migration: + self.driver.destroy(instance_ref, network_info) LOG.info(_('Migrating %(i_name)s to %(dest)s finished successfully.') % locals()) @@ -1387,31 +1457,64 @@ class ComputeManager(manager.SchedulerDependentManager): "Domain not found: no domain with matching name.\" " "This error can be safely ignored.")) - def recover_live_migration(self, ctxt, instance_ref, host=None, dest=None): - """Recovers Instance/volume state from migrating -> running. + def post_live_migration_at_destination(self, context, + instance_id, block_migration=False): + """Post operations for live migration . - :param ctxt: security context + :param context: security context :param instance_id: nova.db.sqlalchemy.models.Instance.Id - :param host: DB column value is updated by this hostname. - If none, the host instance currently running is selected. + :param block_migration: block_migration """ - if not host: - host = instance_ref['host'] + instance_ref = self.db.instance_get(context, instance_id) + LOG.info(_('Post operation of migraton started for %s .') + % instance_ref.name) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.post_live_migration_at_destination(context, + instance_ref, + network_info, + block_migration) - self.db.instance_update(ctxt, + def rollback_live_migration(self, context, instance_ref, + dest, block_migration): + """Recovers Instance/volume state from migrating -> running. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param dest: + This method is called from live migration src host. + This param specifies destination host. + """ + host = instance_ref['host'] + self.db.instance_update(context, instance_ref['id'], {'state_description': 'running', 'state': power_state.RUNNING, 'host': host}) - if dest: - volume_api = volume.API() for volume_ref in instance_ref['volumes']: volume_id = volume_ref['id'] - self.db.volume_update(ctxt, volume_id, {'status': 'in-use'}) - if dest: - volume_api.remove_from_compute(ctxt, volume_id, dest) + self.db.volume_update(context, volume_id, {'status': 'in-use'}) + volume.API().remove_from_compute(context, volume_id, dest) + + # Block migration needs empty image at destination host + # before migration starts, so if any failure occurs, + # any empty images has to be deleted. + if block_migration: + rpc.cast(context, + self.db.queue_get_for(context, FLAGS.compute_topic, dest), + {"method": "rollback_live_migration_at_destination", + "args": {'instance_id': instance_ref['id']}}) + + def rollback_live_migration_at_destination(self, context, instance_id): + """ Cleaning up image directory that is created pre_live_migration. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + """ + instances_ref = self.db.instance_get(context, instance_id) + network_info = self._get_instance_nw_info(context, instances_ref) + self.driver.destroy(instances_ref, network_info) def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" diff --git a/nova/console/xvp.py b/nova/console/xvp.py index 3cd287183..2d6842044 100644 --- a/nova/console/xvp.py +++ b/nova/console/xvp.py @@ -20,7 +20,6 @@ import fcntl import os import signal -import subprocess from Cheetah import Template diff --git a/nova/crypto.py b/nova/crypto.py index 8d535f426..71bef80f2 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -104,6 +104,12 @@ def fetch_ca(project_id=None, chain=True): return buffer +def generate_fingerprint(public_key): + (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f', public_key) + fingerprint = out.split(' ')[1] + return fingerprint + + def generate_key_pair(bits=1024): # what is the magic 65537? @@ -111,9 +117,7 @@ def generate_key_pair(bits=1024): keyfile = os.path.join(tmpdir, 'temp') utils.execute('ssh-keygen', '-q', '-b', bits, '-N', '', '-f', keyfile) - (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f', - '%s.pub' % (keyfile)) - fingerprint = out.split(' ')[1] + fingerprint = generate_fingerprint('%s.pub' % (keyfile)) private_key = open(keyfile).read() public_key = open(keyfile + '.pub').read() diff --git a/nova/db/api.py b/nova/db/api.py index 47308bdba..b9ea8757c 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -387,15 +387,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id): return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id) -def fixed_ip_get_instance(context, address): - """Get an instance for a fixed ip by address.""" - return IMPL.fixed_ip_get_instance(context, address) - - -def fixed_ip_get_instance_v6(context, address): - return IMPL.fixed_ip_get_instance_v6(context, address) - - def fixed_ip_get_network(context, address): """Get a network for a fixed ip by address.""" return IMPL.fixed_ip_get_network(context, address) @@ -500,6 +491,11 @@ def instance_get_all(context): return IMPL.instance_get_all(context) +def instance_get_all_by_filters(context, filters): + """Get all instances that match all filters.""" + return IMPL.instance_get_all_by_filters(context, filters) + + def instance_get_active_by_window(context, begin, end=None): """Get instances active during a certain time window.""" return IMPL.instance_get_active_by_window(context, begin, end) @@ -521,10 +517,20 @@ def instance_get_all_by_host(context, host): def instance_get_all_by_reservation(context, reservation_id): - """Get all instance belonging to a reservation.""" + """Get all instances belonging to a reservation.""" return IMPL.instance_get_all_by_reservation(context, reservation_id) +def instance_get_by_fixed_ip(context, address): + """Get an instance for a fixed ip by address.""" + return IMPL.instance_get_by_fixed_ip(context, address) + + +def instance_get_by_fixed_ipv6(context, address): + """Get an instance for a fixed ip by IPv6 address.""" + return IMPL.instance_get_by_fixed_ipv6(context, address) + + def instance_get_fixed_addresses(context, instance_id): """Get the fixed ip address of an instance.""" return IMPL.instance_get_fixed_addresses(context, instance_id) @@ -564,27 +570,6 @@ def instance_add_security_group(context, instance_id, security_group_id): security_group_id) -def instance_get_vcpu_sum_by_host_and_project(context, hostname, proj_id): - """Get instances.vcpus by host and project.""" - return IMPL.instance_get_vcpu_sum_by_host_and_project(context, - hostname, - proj_id) - - -def instance_get_memory_sum_by_host_and_project(context, hostname, proj_id): - """Get amount of memory by host and project.""" - return IMPL.instance_get_memory_sum_by_host_and_project(context, - hostname, - proj_id) - - -def instance_get_disk_sum_by_host_and_project(context, hostname, proj_id): - """Get total amount of disk by host and project.""" - return IMPL.instance_get_disk_sum_by_host_and_project(context, - hostname, - proj_id) - - def instance_action_create(context, values): """Create an instance action from the values dictionary.""" return IMPL.instance_action_create(context, values) @@ -1096,6 +1081,11 @@ def security_group_rule_destroy(context, security_group_rule_id): return IMPL.security_group_rule_destroy(context, security_group_rule_id) +def security_group_rule_get(context, security_group_rule_id): + """Gets a security group rule.""" + return IMPL.security_group_rule_get(context, security_group_rule_id) + + ################### @@ -1381,9 +1371,9 @@ def instance_metadata_delete(context, instance_id, key): IMPL.instance_metadata_delete(context, instance_id, key) -def instance_metadata_update_or_create(context, instance_id, metadata): - """Create or update instance metadata.""" - IMPL.instance_metadata_update_or_create(context, instance_id, metadata) +def instance_metadata_update(context, instance_id, metadata, delete): + """Update metadata if it exists, otherwise create it.""" + IMPL.instance_metadata_update(context, instance_id, metadata, delete) #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ce12ba4e0..57a4370d8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -18,8 +18,10 @@ """ Implementation of SQLAlchemy backend. """ +import re import warnings +from nova import block_device from nova import db from nova import exception from nova import flags @@ -828,28 +830,6 @@ def fixed_ip_get_by_virtual_interface(context, vif_id): return rv -@require_context -def fixed_ip_get_instance(context, address): - fixed_ip_ref = fixed_ip_get_by_address(context, address) - return fixed_ip_ref.instance - - -@require_context -def fixed_ip_get_instance_v6(context, address): - session = get_session() - - # convert IPv6 address to mac - mac = ipv6.to_mac(address) - - # get virtual interface - vif_ref = virtual_interface_get_by_address(context, mac) - - # look up instance based on instance_id from vif row - result = session.query(models.Instance).\ - filter_by(id=vif_ref['instance_id']) - return result - - @require_admin_context def fixed_ip_get_network(context, address): fixed_ip_ref = fixed_ip_get_by_address(context, address) @@ -1159,7 +1139,10 @@ def instance_get_all(context): session = get_session() return session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ + options(joinedload_all('virtual_interfaces.network')).\ + options(joinedload_all( + 'virtual_interfaces.fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces.instance')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ @@ -1168,6 +1151,115 @@ def instance_get_all(context): all() +@require_context +def instance_get_all_by_filters(context, filters): + """Return instances that match all filters. Deleted instances + will be returned by default, unless there's a filter that says + otherwise""" + + def _regexp_filter_by_ipv6(instance, filter_re): + for interface in instance['virtual_interfaces']: + fixed_ipv6 = interface.get('fixed_ipv6') + if fixed_ipv6 and filter_re.match(fixed_ipv6): + return True + return False + + def _regexp_filter_by_ip(instance, filter_re): + for interface in instance['virtual_interfaces']: + for fixed_ip in interface['fixed_ips']: + if not fixed_ip or not fixed_ip['address']: + continue + if filter_re.match(fixed_ip['address']): + return True + for floating_ip in fixed_ip.get('floating_ips', []): + if not floating_ip or not floating_ip['address']: + continue + if filter_re.match(floating_ip['address']): + return True + return False + + def _regexp_filter_by_column(instance, filter_name, filter_re): + try: + v = getattr(instance, filter_name) + except AttributeError: + return True + if v and filter_re.match(str(v)): + return True + return False + + def _exact_match_filter(query, column, value): + """Do exact match against a column. value to match can be a list + so you can match any value in the list. + """ + if isinstance(value, list): + column_attr = getattr(models.Instance, column) + return query.filter(column_attr.in_(value)) + else: + filter_dict = {} + filter_dict[column] = value + return query.filter_by(**filter_dict) + + session = get_session() + query_prefix = session.query(models.Instance).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload_all('virtual_interfaces.network')).\ + options(joinedload_all( + 'virtual_interfaces.fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces.instance')).\ + options(joinedload('security_groups')).\ + options(joinedload_all('fixed_ips.network')).\ + options(joinedload('metadata')).\ + options(joinedload('instance_type')) + + # Make a copy of the filters dictionary to use going forward, as we'll + # be modifying it and we shouldn't affect the caller's use of it. + filters = filters.copy() + + if not context.is_admin: + # If we're not admin context, add appropriate filter.. + if context.project_id: + filters['project_id'] = context.project_id + else: + filters['user_id'] = context.user_id + + # Filters for exact matches that we can do along with the SQL query... + # For other filters that don't match this, we will do regexp matching + exact_match_filter_names = ['project_id', 'user_id', 'image_ref', + 'state', 'instance_type_id', 'deleted'] + + query_filters = [key for key in filters.iterkeys() + if key in exact_match_filter_names] + + for filter_name in query_filters: + # Do the matching and remove the filter from the dictionary + # so we don't try it again below.. + query_prefix = _exact_match_filter(query_prefix, filter_name, + filters.pop(filter_name)) + + instances = query_prefix.all() + + if not instances: + return [] + + # Now filter on everything else for regexp matching.. + # For filters not in the list, we'll attempt to use the filter_name + # as a column name in Instance.. + regexp_filter_funcs = {'ip6': _regexp_filter_by_ipv6, + 'ip': _regexp_filter_by_ip} + + for filter_name in filters.iterkeys(): + filter_func = regexp_filter_funcs.get(filter_name, None) + filter_re = re.compile(str(filters[filter_name])) + if filter_func: + filter_l = lambda instance: filter_func(instance, filter_re) + else: + filter_l = lambda instance: _regexp_filter_by_column(instance, + filter_name, filter_re) + instances = filter(filter_l, instances) + + return instances + + @require_admin_context def instance_get_active_by_window(context, begin, end=None): """Return instances that were continuously active over the given window""" @@ -1236,30 +1328,48 @@ def instance_get_all_by_project(context, project_id): @require_context def instance_get_all_by_reservation(context, reservation_id): session = get_session() + query = session.query(models.Instance).\ + filter_by(reservation_id=reservation_id).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ + options(joinedload('security_groups')).\ + options(joinedload_all('fixed_ips.network')).\ + options(joinedload('metadata')).\ + options(joinedload('instance_type')) if is_admin_context(context): - return session.query(models.Instance).\ - options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ - options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ips.network')).\ - options(joinedload('metadata')).\ - options(joinedload('instance_type')).\ - filter_by(reservation_id=reservation_id).\ - filter_by(deleted=can_read_deleted(context)).\ - all() + return query.\ + filter_by(deleted=can_read_deleted(context)).\ + all() elif is_user_context(context): - return session.query(models.Instance).\ - options(joinedload_all('fixed_ips.floating_ips')).\ - options(joinedload('virtual_interfaces')).\ - options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ips.network')).\ - options(joinedload('metadata')).\ - options(joinedload('instance_type')).\ - filter_by(project_id=context.project_id).\ - filter_by(reservation_id=reservation_id).\ - filter_by(deleted=False).\ - all() + return query.\ + filter_by(project_id=context.project_id).\ + filter_by(deleted=False).\ + all() + + +@require_context +def instance_get_by_fixed_ip(context, address): + """Return instance ref by exact match of FixedIP""" + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.instance + + +@require_context +def instance_get_by_fixed_ipv6(context, address): + """Return instance ref by exact match of IPv6""" + session = get_session() + + # convert IPv6 address to mac + mac = ipv6.to_mac(address) + + # get virtual interface + vif_ref = virtual_interface_get_by_address(context, mac) + + # look up instance based on instance_id from vif row + result = session.query(models.Instance).\ + filter_by(id=vif_ref['instance_id']) + return result @require_admin_context @@ -1301,7 +1411,7 @@ def instance_get_fixed_addresses_v6(context, instance_id): network_refs = network_get_all_by_instance(context, instance_id) # compile a list of cidr_v6 prefixes sorted by network id prefixes = [ref.cidr_v6 for ref in - sorted(network_refs, key=lambda ref: ref.id)] + sorted(network_refs, key=lambda ref: ref.id)] # get vifs associated with instance vif_refs = virtual_interface_get_by_instance(context, instance_ref.id) # compile list of the mac_addresses for vifs sorted by network id @@ -1345,9 +1455,10 @@ def instance_update(context, instance_id, values): session = get_session() metadata = values.get('metadata') if metadata is not None: - instance_metadata_delete_all(context, instance_id) - instance_metadata_update_or_create(context, instance_id, - values.pop('metadata')) + instance_metadata_update(context, + instance_id, + values.pop('metadata'), + delete=True) with session.begin(): if utils.is_uuid_like(instance_id): instance_ref = instance_get_by_uuid(context, instance_id, @@ -1372,45 +1483,6 @@ def instance_add_security_group(context, instance_id, security_group_id): @require_context -def instance_get_vcpu_sum_by_host_and_project(context, hostname, proj_id): - session = get_session() - result = session.query(models.Instance).\ - filter_by(host=hostname).\ - filter_by(project_id=proj_id).\ - filter_by(deleted=False).\ - value(func.sum(models.Instance.vcpus)) - if not result: - return 0 - return result - - -@require_context -def instance_get_memory_sum_by_host_and_project(context, hostname, proj_id): - session = get_session() - result = session.query(models.Instance).\ - filter_by(host=hostname).\ - filter_by(project_id=proj_id).\ - filter_by(deleted=False).\ - value(func.sum(models.Instance.memory_mb)) - if not result: - return 0 - return result - - -@require_context -def instance_get_disk_sum_by_host_and_project(context, hostname, proj_id): - session = get_session() - result = session.query(models.Instance).\ - filter_by(host=hostname).\ - filter_by(project_id=proj_id).\ - filter_by(deleted=False).\ - value(func.sum(models.Instance.local_gb)) - if not result: - return 0 - return result - - -@require_context def instance_action_create(context, values): """Create an instance action from the values dictionary.""" action_ref = models.InstanceActions() @@ -1426,9 +1498,14 @@ def instance_action_create(context, values): def instance_get_actions(context, instance_id): """Return the actions associated to the given instance id""" session = get_session() + + if utils.is_uuid_like(instance_id): + instance = instance_get_by_uuid(context, instance_id, session) + instance_id = instance.id + return session.query(models.InstanceActions).\ filter_by(instance_id=instance_id).\ - all() + all() ################### @@ -1681,7 +1758,9 @@ def network_get_by_cidr(context, cidr): session = get_session() result = session.query(models.Network).\ filter(or_(models.Network.cidr == cidr, - models.Network.cidr_v6 == cidr)).first() + models.Network.cidr_v6 == cidr)).\ + filter_by(deleted=False).\ + first() if not result: raise exception.NetworkNotFoundForCidr(cidr=cidr) @@ -2265,6 +2344,20 @@ def block_device_mapping_update_or_create(context, values): else: result.update(values) + # NOTE(yamahata): same virtual device name can be specified multiple + # times. So delete the existing ones. + virtual_name = values['virtual_name'] + if (virtual_name is not None and + block_device.is_swap_or_ephemeral(virtual_name)): + session.query(models.BlockDeviceMapping).\ + filter_by(instance_id=values['instance_id']).\ + filter_by(virtual_name=virtual_name).\ + filter(models.BlockDeviceMapping.device_name != + values['device_name']).\ + update({'deleted': True, + 'deleted_at': utils.utcnow(), + 'updated_at': literal_column('updated_at')}) + @require_context def block_device_mapping_get_all_by_instance(context, instance_id): @@ -3196,21 +3289,35 @@ def instance_metadata_get_item(context, instance_id, key, session=None): @require_context @require_instance_exists -def instance_metadata_update_or_create(context, instance_id, metadata): +def instance_metadata_update(context, instance_id, metadata, delete): session = get_session() - original_metadata = instance_metadata_get(context, instance_id) + # Set existing metadata to deleted if delete argument is True + if delete: + original_metadata = instance_metadata_get(context, instance_id) + for meta_key, meta_value in original_metadata.iteritems(): + if meta_key not in metadata: + meta_ref = instance_metadata_get_item(context, instance_id, + meta_key, session) + meta_ref.update({'deleted': True}) + meta_ref.save(session=session) meta_ref = None - for key, value in metadata.iteritems(): + + # Now update all existing items with new values, or create new meta objects + for meta_key, meta_value in metadata.iteritems(): + + # update the value whether it exists or not + item = {"value": meta_value} + try: - meta_ref = instance_metadata_get_item(context, instance_id, key, - session) + meta_ref = instance_metadata_get_item(context, instance_id, + meta_key, session) except exception.InstanceMetadataNotFound, e: meta_ref = models.InstanceMetadata() - meta_ref.update({"key": key, "value": value, - "instance_id": instance_id, - "deleted": False}) + item.update({"key": meta_key, "instance_id": instance_id}) + + meta_ref.update(item) meta_ref.save(session=session) return metadata @@ -3301,7 +3408,9 @@ def instance_type_extra_specs_delete(context, instance_type_id, key): @require_context -def instance_type_extra_specs_get_item(context, instance_type_id, key, session=None): +def instance_type_extra_specs_get_item(context, instance_type_id, key, + session=None): + if not session: session = get_session() @@ -3325,10 +3434,8 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id, spec_ref = None for key, value in specs.iteritems(): try: - spec_ref = instance_type_extra_specs_get_item(context, - instance_type_id, - key, - session) + spec_ref = instance_type_extra_specs_get_item( + context, instance_type_id, key, session) except exception.InstanceTypeExtraSpecsNotFound, e: spec_ref = models.InstanceTypeExtraSpecs() spec_ref.update({"key": key, "value": value, diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 3ab0a2b0c..32fe64b4f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -127,14 +127,14 @@ class ComputeNode(BASE, NovaBase): 'ComputeNode.service_id == Service.id,' 'ComputeNode.deleted == False)') - vcpus = Column(Integer, nullable=True) - memory_mb = Column(Integer, nullable=True) - local_gb = Column(Integer, nullable=True) - vcpus_used = Column(Integer, nullable=True) - memory_mb_used = Column(Integer, nullable=True) - local_gb_used = Column(Integer, nullable=True) - hypervisor_type = Column(Text, nullable=True) - hypervisor_version = Column(Integer, nullable=True) + vcpus = Column(Integer) + memory_mb = Column(Integer) + local_gb = Column(Integer) + vcpus_used = Column(Integer) + memory_mb_used = Column(Integer) + local_gb_used = Column(Integer) + hypervisor_type = Column(Text) + hypervisor_version = Column(Integer) # Note(masumotok): Expected Strings example: # @@ -180,6 +180,7 @@ class Instance(BASE, NovaBase): image_ref = Column(String(255)) kernel_id = Column(String(255)) ramdisk_id = Column(String(255)) + server_name = Column(String(255)) # image_ref = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) @@ -478,6 +479,11 @@ class SecurityGroupIngressRule(BASE, NovaBase): # Note: This is not the parent SecurityGroup. It's SecurityGroup we're # granting access for. group_id = Column(Integer, ForeignKey('security_groups.id')) + grantee_group = relationship("SecurityGroup", + foreign_keys=group_id, + primaryjoin='and_(' + 'SecurityGroupIngressRule.group_id == SecurityGroup.id,' + 'SecurityGroupIngressRule.deleted == False)') class ProviderFirewallRule(BASE, NovaBase): diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 4a9a28f43..07f281938 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -19,37 +19,79 @@ Session Handling for SQLAlchemy backend """ -from sqlalchemy import create_engine -from sqlalchemy import pool -from sqlalchemy.orm import sessionmaker +import eventlet.patcher +eventlet.patcher.monkey_patch() -from nova import exception -from nova import flags +import eventlet.db_pool +import sqlalchemy.orm +import sqlalchemy.pool + +import nova.exception +import nova.flags +import nova.log + + +FLAGS = nova.flags.FLAGS +LOG = nova.log.getLogger("nova.db.sqlalchemy") + + +try: + import MySQLdb +except ImportError: + MySQLdb = None -FLAGS = flags.FLAGS _ENGINE = None _MAKER = None def get_session(autocommit=True, expire_on_commit=False): - """Helper method to grab session""" - global _ENGINE - global _MAKER - if not _MAKER: - if not _ENGINE: - kwargs = {'pool_recycle': FLAGS.sql_idle_timeout, - 'echo': False} - - if FLAGS.sql_connection.startswith('sqlite'): - kwargs['poolclass'] = pool.NullPool - - _ENGINE = create_engine(FLAGS.sql_connection, - **kwargs) - _MAKER = (sessionmaker(bind=_ENGINE, - autocommit=autocommit, - expire_on_commit=expire_on_commit)) + """Return a SQLAlchemy session.""" + global _ENGINE, _MAKER + + if _MAKER is None or _ENGINE is None: + _ENGINE = get_engine() + _MAKER = get_maker(_ENGINE, autocommit, expire_on_commit) + session = _MAKER() - session.query = exception.wrap_db_error(session.query) - session.flush = exception.wrap_db_error(session.flush) + session.query = nova.exception.wrap_db_error(session.query) + session.flush = nova.exception.wrap_db_error(session.flush) return session + + +def get_engine(): + """Return a SQLAlchemy engine.""" + connection_dict = sqlalchemy.engine.url.make_url(FLAGS.sql_connection) + + engine_args = { + "pool_recycle": FLAGS.sql_idle_timeout, + "echo": False, + } + + if "sqlite" in connection_dict.drivername: + engine_args["poolclass"] = sqlalchemy.pool.NullPool + + elif MySQLdb and "mysql" in connection_dict.drivername: + LOG.info(_("Using mysql/eventlet db_pool.")) + pool_args = { + "db": connection_dict.database, + "passwd": connection_dict.password, + "host": connection_dict.host, + "user": connection_dict.username, + "min_size": FLAGS.sql_min_pool_size, + "max_size": FLAGS.sql_max_pool_size, + "max_idle": FLAGS.sql_idle_timeout, + } + creator = eventlet.db_pool.ConnectionPool(MySQLdb, **pool_args) + engine_args["pool_size"] = FLAGS.sql_max_pool_size + engine_args["pool_timeout"] = FLAGS.sql_pool_timeout + engine_args["creator"] = creator.create + + return sqlalchemy.create_engine(FLAGS.sql_connection, **engine_args) + + +def get_maker(engine, autocommit=True, expire_on_commit=False): + """Return a SQLAlchemy sessionmaker using the given engine.""" + return sqlalchemy.orm.sessionmaker(bind=engine, + autocommit=autocommit, + expire_on_commit=expire_on_commit) diff --git a/nova/exception.py b/nova/exception.py index 68e6ac937..3e2218863 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -25,6 +25,7 @@ SHOULD include dedicated exception logging. """ from functools import wraps +import sys from nova import log as logging @@ -96,6 +97,10 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None, try: return f(*args, **kw) except Exception, e: + # Save exception since it can be clobbered during processing + # below before we can re-raise + exc_info = sys.exc_info() + if notifier: payload = dict(args=args, exception=e) payload.update(kw) @@ -122,7 +127,9 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None, LOG.exception(_('Uncaught exception')) #logging.error(traceback.extract_stack(exc_traceback)) raise Error(str(e)) - raise + + # re-raise original exception since it may have been clobbered + raise exc_info[0], exc_info[1], exc_info[2] return wraps(f)(wrapped) return inner @@ -150,6 +157,10 @@ class NovaException(Exception): return self._error_string +class ImagePaginationFailed(NovaException): + message = _("Failed to paginate through images from image service") + + class VirtualInterfaceCreateException(NovaException): message = _("Virtual Interface creation failed") @@ -198,6 +209,16 @@ class InvalidContentType(Invalid): message = _("Invalid content type %(content_type)s.") +class InvalidCidr(Invalid): + message = _("Invalid cidr %(cidr)s.") + + +# Cannot be templated as the error syntax varies. +# msg needs to be constructed when raised. +class InvalidParameterValue(Invalid): + message = _("%(err)s") + + class InstanceNotRunning(Invalid): message = _("Instance %(instance_id)s is not running.") @@ -252,6 +273,11 @@ class DestinationHypervisorTooOld(Invalid): "has been provided.") +class DestinationDiskExists(Invalid): + message = _("The supplied disk path (%(path)s) already exists, " + "it is expected not to exist.") + + class InvalidDevicePath(Invalid): message = _("The supplied device path (%(path)s) is invalid.") @@ -678,6 +704,10 @@ class InstanceExists(Duplicate): message = _("Instance %(name)s already exists.") +class InvalidSharedStorage(NovaException): + message = _("%(path)s is on shared storage: %(reason)s") + + class MigrationError(NovaException): message = _("Migration error") + ": %(reason)s" diff --git a/nova/flags.py b/nova/flags.py index 12c6d1356..48d5e8168 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -305,6 +305,7 @@ DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_integer('rabbit_retry_interval', 10, 'rabbit connection retry interval') DEFINE_integer('rabbit_max_retries', 12, 'rabbit connection attempts') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') +DEFINE_boolean('rabbit_durable_queues', False, 'use durable queues') DEFINE_list('enabled_apis', ['ec2', 'osapi'], 'list of APIs to enable by default') DEFINE_string('ec2_host', '$my_ip', 'ip of api server') @@ -317,7 +318,7 @@ DEFINE_string('osapi_extensions_path', '/var/lib/nova/extensions', DEFINE_string('osapi_host', '$my_ip', 'ip of api server') DEFINE_string('osapi_scheme', 'http', 'prefix for openstack') DEFINE_integer('osapi_port', 8774, 'OpenStack API port') -DEFINE_string('osapi_path', '/v1.0/', 'suffix for openstack') +DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack') DEFINE_integer('osapi_max_limit', 1000, 'max number of items returned in a collection response') @@ -345,6 +346,12 @@ DEFINE_string('logdir', None, 'output to a per-service log file in named ' 'directory') DEFINE_integer('logfile_mode', 0644, 'Default file mode of the logs.') DEFINE_string('sqlite_db', 'nova.sqlite', 'file name for sqlite') +DEFINE_integer('sql_pool_timeout', 30, + 'seconds to wait for connection from pool before erroring') +DEFINE_integer('sql_min_pool_size', 10, + 'minimum number of SQL connections to pool') +DEFINE_integer('sql_max_pool_size', 10, + 'maximum number of SQL connections to pool') DEFINE_string('sql_connection', 'sqlite:///$state_path/$sqlite_db', 'connection string for sql database') @@ -392,3 +399,6 @@ DEFINE_bool('start_guests_on_host_boot', False, 'Whether to restart guests when the host reboots') DEFINE_bool('resume_guests_state_on_host_boot', False, 'Whether to start guests, that was running before the host reboot') + +DEFINE_string('root_helper', 'sudo', + 'Command prefix to use for running commands as root') diff --git a/nova/image/fake.py b/nova/image/fake.py index 28e912534..97af81711 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -45,9 +45,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': False, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel, 'architecture': 'x86_64'}} @@ -56,9 +59,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -66,9 +72,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -76,9 +85,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -86,9 +98,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -101,7 +116,11 @@ class _FakeImageService(service.BaseImageService): def index(self, context, filters=None, marker=None, limit=None): """Returns list of images.""" - return copy.deepcopy(self.images.values()) + retval = [] + for img in self.images.values(): + retval += [dict([(k, v) for k, v in img.iteritems() + if k in ['id', 'name']])] + return retval def detail(self, context, filters=None, marker=None, limit=None): """Return list of detailed image information.""" diff --git a/nova/image/glance.py b/nova/image/glance.py index 44a3c6f83..9060f6a91 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -19,7 +19,9 @@ from __future__ import absolute_import +import copy import datetime +import json import random from glance.common import exception as glance_exception @@ -87,42 +89,71 @@ class GlanceImageService(service.BaseImageService): """Sets the client's auth token.""" self.client.set_auth_token(context.auth_token) - def index(self, context, filters=None, marker=None, limit=None): + def index(self, context, **kwargs): """Calls out to Glance for a list of images available.""" - # NOTE(sirp): We need to use `get_images_detailed` and not - # `get_images` here because we need `is_public` and `properties` - # included so we can filter by user - self._set_client_context(context) - filtered = [] - filters = filters or {} - if 'is_public' not in filters: - # NOTE(vish): don't filter out private images - filters['is_public'] = 'none' - image_metas = self.client.get_images_detailed(filters=filters, - marker=marker, - limit=limit) + params = self._extract_query_params(kwargs) + image_metas = self._get_images(context, **params) + + images = [] for image_meta in image_metas: + # NOTE(sirp): We need to use `get_images_detailed` and not + # `get_images` here because we need `is_public` and `properties` + # included so we can filter by user if self._is_image_available(context, image_meta): meta_subset = utils.subset_dict(image_meta, ('id', 'name')) - filtered.append(meta_subset) - return filtered + images.append(meta_subset) + return images - def detail(self, context, filters=None, marker=None, limit=None): + def detail(self, context, **kwargs): """Calls out to Glance for a list of detailed image information.""" - self._set_client_context(context) - filtered = [] - filters = filters or {} - if 'is_public' not in filters: - # NOTE(vish): don't filter out private images - filters['is_public'] = 'none' - image_metas = self.client.get_images_detailed(filters=filters, - marker=marker, - limit=limit) + params = self._extract_query_params(kwargs) + image_metas = self._get_images(context, **params) + + images = [] for image_meta in image_metas: if self._is_image_available(context, image_meta): base_image_meta = self._translate_to_base(image_meta) - filtered.append(base_image_meta) - return filtered + images.append(base_image_meta) + return images + + def _extract_query_params(self, params): + _params = {} + accepted_params = ('filters', 'marker', 'limit', + 'sort_key', 'sort_dir') + for param in accepted_params: + if param in params: + _params[param] = params.get(param) + + return _params + + def _get_images(self, context, **kwargs): + """Get image entitites from images service""" + self._set_client_context(context) + + # ensure filters is a dict + kwargs['filters'] = kwargs.get('filters') or {} + # NOTE(vish): don't filter out private images + kwargs['filters'].setdefault('is_public', 'none') + + return self._fetch_images(self.client.get_images_detailed, **kwargs) + + def _fetch_images(self, fetch_func, **kwargs): + """Paginate through results from glance server""" + images = fetch_func(**kwargs) + + for image in images: + yield image + else: + # break out of recursive loop to end pagination + return + + try: + # attempt to advance the marker in order to fetch next page + kwargs['marker'] = images[-1]['id'] + except KeyError: + raise exception.ImagePaginationFailed() + + self._fetch_images(fetch_func, **kwargs) def show(self, context, image_id): """Returns a dict with image data for the given opaque image id.""" @@ -194,6 +225,7 @@ class GlanceImageService(service.BaseImageService): self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) + image_meta = _convert_to_string(image_meta) try: image_meta = self.client.update_image(image_id, image_meta, data) except glance_exception.NotFound: @@ -222,11 +254,19 @@ class GlanceImageService(service.BaseImageService): pass @classmethod + def _translate_to_service(cls, image_meta): + image_meta = super(GlanceImageService, + cls)._translate_to_service(image_meta) + image_meta = _convert_to_string(image_meta) + return image_meta + + @classmethod def _translate_to_base(cls, image_meta): """Override translation to handle conversion to datetime objects.""" image_meta = service.BaseImageService._propertify_metadata( image_meta, cls.SERVICE_IMAGE_ATTRS) image_meta = _convert_timestamps_to_datetimes(image_meta) + image_meta = _convert_from_string(image_meta) return image_meta @@ -252,3 +292,38 @@ def _parse_glance_iso8601_timestamp(timestamp): raise ValueError(_('%(timestamp)s does not follow any of the ' 'signatures: %(ISO_FORMATS)s') % locals()) + + +# TODO(yamahata): use block-device-mapping extension to glance +def _json_loads(properties, attr): + prop = properties[attr] + if isinstance(prop, basestring): + properties[attr] = json.loads(prop) + + +def _json_dumps(properties, attr): + prop = properties[attr] + if not isinstance(prop, basestring): + properties[attr] = json.dumps(prop) + + +_CONVERT_PROPS = ('block_device_mapping', 'mappings') + + +def _convert(method, metadata): + metadata = copy.deepcopy(metadata) # don't touch original metadata + properties = metadata.get('properties') + if properties: + for attr in _CONVERT_PROPS: + if attr in properties: + method(properties, attr) + + return metadata + + +def _convert_from_string(metadata): + return _convert(_json_loads, metadata) + + +def _convert_to_string(metadata): + return _convert(_json_dumps, metadata) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 8ace07884..4e1e1f85a 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -296,14 +296,14 @@ class IptablesManager(object): for cmd, tables in s: for table in tables: - current_table, _ = self.execute('sudo', - '%s-save' % (cmd,), + current_table, _ = self.execute('%s-save' % (cmd,), '-t', '%s' % (table,), + run_as_root=True, attempts=5) current_lines = current_table.split('\n') new_filter = self._modify_rules(current_lines, tables[table]) - self.execute('sudo', '%s-restore' % (cmd,), + self.execute('%s-restore' % (cmd,), run_as_root=True, process_input='\n'.join(new_filter), attempts=5) @@ -396,21 +396,22 @@ def init_host(): def bind_floating_ip(floating_ip, check_exit_code=True): """Bind ip to public interface.""" - _execute('sudo', 'ip', 'addr', 'add', floating_ip, + _execute('ip', 'addr', 'add', floating_ip, 'dev', FLAGS.public_interface, - check_exit_code=check_exit_code) + run_as_root=True, check_exit_code=check_exit_code) def unbind_floating_ip(floating_ip): """Unbind a public ip from public interface.""" - _execute('sudo', 'ip', 'addr', 'del', floating_ip, - 'dev', FLAGS.public_interface) + _execute('ip', 'addr', 'del', floating_ip, + 'dev', FLAGS.public_interface, run_as_root=True) def ensure_metadata_ip(): """Sets up local metadata ip.""" - _execute('sudo', 'ip', 'addr', 'add', '169.254.169.254/32', - 'scope', 'link', 'dev', 'lo', check_exit_code=False) + _execute('ip', 'addr', 'add', '169.254.169.254/32', + 'scope', 'link', 'dev', 'lo', + run_as_root=True, check_exit_code=False) def ensure_vlan_forward(public_ip, port, private_ip): @@ -464,9 +465,11 @@ def ensure_vlan(vlan_num, bridge_interface): interface = 'vlan%s' % vlan_num if not _device_exists(interface): LOG.debug(_('Starting VLAN inteface %s'), interface) - _execute('sudo', 'vconfig', 'set_name_type', 'VLAN_PLUS_VID_NO_PAD') - _execute('sudo', 'vconfig', 'add', bridge_interface, vlan_num) - _execute('sudo', 'ip', 'link', 'set', interface, 'up') + _execute('vconfig', 'set_name_type', 'VLAN_PLUS_VID_NO_PAD', + run_as_root=True) + _execute('vconfig', 'add', bridge_interface, vlan_num, + run_as_root=True) + _execute('ip', 'link', 'set', interface, 'up', run_as_root=True) return interface @@ -487,58 +490,62 @@ def ensure_bridge(bridge, interface, net_attrs=None): """ if not _device_exists(bridge): LOG.debug(_('Starting Bridge interface for %s'), interface) - _execute('sudo', 'brctl', 'addbr', bridge) - _execute('sudo', 'brctl', 'setfd', bridge, 0) - # _execute('sudo brctl setageing %s 10' % bridge) - _execute('sudo', 'brctl', 'stp', bridge, 'off') - _execute('sudo', 'ip', 'link', 'set', bridge, 'up') + _execute('brctl', 'addbr', bridge, run_as_root=True) + _execute('brctl', 'setfd', bridge, 0, run_as_root=True) + _execute('brctl', 'stp', bridge, 'off', run_as_root=True) + _execute('ip', 'link', 'set', bridge, 'up', run_as_root=True) if net_attrs: # NOTE(vish): The ip for dnsmasq has to be the first address on the # bridge for it to respond to reqests properly suffix = net_attrs['cidr'].rpartition('/')[2] - out, err = _execute('sudo', 'ip', 'addr', 'add', + out, err = _execute('ip', 'addr', 'add', '%s/%s' % (net_attrs['dhcp_server'], suffix), 'brd', net_attrs['broadcast'], 'dev', bridge, + run_as_root=True, check_exit_code=False) if err and err != 'RTNETLINK answers: File exists\n': raise exception.Error('Failed to add ip: %s' % err) if(FLAGS.use_ipv6): - _execute('sudo', 'ip', '-f', 'inet6', 'addr', + _execute('ip', '-f', 'inet6', 'addr', 'change', net_attrs['cidr_v6'], - 'dev', bridge) + 'dev', bridge, run_as_root=True) # NOTE(vish): If the public interface is the same as the # bridge, then the bridge has to be in promiscuous # to forward packets properly. if(FLAGS.public_interface == bridge): - _execute('sudo', 'ip', 'link', 'set', - 'dev', bridge, 'promisc', 'on') + _execute('ip', 'link', 'set', + 'dev', bridge, 'promisc', 'on', run_as_root=True) if interface: # NOTE(vish): This will break if there is already an ip on the # interface, so we move any ips to the bridge gateway = None - out, err = _execute('sudo', 'route', '-n') + out, err = _execute('route', '-n', run_as_root=True) for line in out.split('\n'): fields = line.split() if fields and fields[0] == '0.0.0.0' and fields[-1] == interface: gateway = fields[1] - _execute('sudo', 'route', 'del', 'default', 'gw', gateway, - 'dev', interface, check_exit_code=False) - out, err = _execute('sudo', 'ip', 'addr', 'show', 'dev', interface, - 'scope', 'global') + _execute('route', 'del', 'default', 'gw', gateway, + 'dev', interface, + check_exit_code=False, run_as_root=True) + out, err = _execute('ip', 'addr', 'show', 'dev', interface, + 'scope', 'global', run_as_root=True) for line in out.split('\n'): fields = line.split() if fields and fields[0] == 'inet': params = fields[1:-1] - _execute(*_ip_bridge_cmd('del', params, fields[-1])) - _execute(*_ip_bridge_cmd('add', params, bridge)) + _execute(*_ip_bridge_cmd('del', params, fields[-1]), + run_as_root=True) + _execute(*_ip_bridge_cmd('add', params, bridge), + run_as_root=True) if gateway: - _execute('sudo', 'route', 'add', 'default', 'gw', gateway) - out, err = _execute('sudo', 'brctl', 'addif', bridge, interface, - check_exit_code=False) + _execute('route', 'add', 'default', 'gw', gateway, + run_as_root=True) + out, err = _execute('brctl', 'addif', bridge, interface, + check_exit_code=False, run_as_root=True) if (err and err != "device %s is already a member of a bridge; can't " "enslave it to bridge %s.\n" % (interface, bridge)): @@ -602,18 +609,33 @@ def update_dhcp(context, network_ref): check_exit_code=False) if conffile in out: try: - _execute('sudo', 'kill', '-HUP', pid) + _execute('kill', '-HUP', pid, run_as_root=True) return except Exception as exc: # pylint: disable=W0703 LOG.debug(_('Hupping dnsmasq threw %s'), exc) else: LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), pid) - # FLAGFILE and DNSMASQ_INTERFACE in env - env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile, - 'DNSMASQ_INTERFACE': network_ref['bridge']} - command = _dnsmasq_cmd(network_ref) - _execute(*command, addl_env=env) + cmd = ['FLAGFILE=%s' % FLAGS.dhcpbridge_flagfile, + 'DNSMASQ_INTERFACE=%s' % network_ref['bridge'], + 'dnsmasq', + '--strict-order', + '--bind-interfaces', + '--interface=%s' % network_ref['bridge'], + '--conf-file=%s' % FLAGS.dnsmasq_config_file, + '--domain=%s' % FLAGS.dhcp_domain, + '--pid-file=%s' % _dhcp_file(network_ref['bridge'], 'pid'), + '--listen-address=%s' % network_ref['dhcp_server'], + '--except-interface=lo', + '--dhcp-range=%s,static,120s' % network_ref['dhcp_start'], + '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(network_ref['cidr'])), + '--dhcp-hostsfile=%s' % _dhcp_file(network_ref['bridge'], 'conf'), + '--dhcp-script=%s' % FLAGS.dhcpbridge, + '--leasefile-ro'] + if FLAGS.dns_server: + cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server] + + _execute(*cmd, run_as_root=True) @utils.synchronized('radvd_start') @@ -646,13 +668,17 @@ interface %s % pid, check_exit_code=False) if conffile in out: try: - _execute('sudo', 'kill', pid) + _execute('kill', pid, run_as_root=True) except Exception as exc: # pylint: disable=W0703 LOG.debug(_('killing radvd threw %s'), exc) else: LOG.debug(_('Pid %d is stale, relaunching radvd'), pid) - command = _ra_cmd(network_ref) - _execute(*command) + + cmd = ['radvd', + '-C', '%s' % _ra_file(network_ref['bridge'], 'conf'), + '-p', '%s' % _ra_file(network_ref['bridge'], 'pid')] + + _execute(*cmd, run_as_root=True) def _host_lease(fixed_ip_ref): @@ -696,43 +722,13 @@ def _device_exists(device): return not err -def _dnsmasq_cmd(net): - """Builds dnsmasq command.""" - cmd = ['sudo', '-E', 'dnsmasq', - '--strict-order', - '--bind-interfaces', - '--interface=%s' % net['bridge'], - '--conf-file=%s' % FLAGS.dnsmasq_config_file, - '--domain=%s' % FLAGS.dhcp_domain, - '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), - '--listen-address=%s' % net['dhcp_server'], - '--except-interface=lo', - '--dhcp-range=%s,static,120s' % net['dhcp_start'], - '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(net['cidr'])), - '--dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'), - '--dhcp-script=%s' % FLAGS.dhcpbridge, - '--leasefile-ro'] - if FLAGS.dns_server: - cmd += ['-h', '-R', '--server=%s' % FLAGS.dns_server] - return cmd - - -def _ra_cmd(net): - """Builds radvd command.""" - cmd = ['sudo', '-E', 'radvd', -# '-u', 'nobody', - '-C', '%s' % _ra_file(net['bridge'], 'conf'), - '-p', '%s' % _ra_file(net['bridge'], 'pid')] - return cmd - - def _stop_dnsmasq(network): """Stops the dnsmasq instance for a given network.""" pid = _dnsmasq_pid_for(network) if pid: try: - _execute('sudo', 'kill', '-TERM', pid) + _execute('kill', '-TERM', pid, run_as_root=True) except Exception as exc: # pylint: disable=W0703 LOG.debug(_('Killing dnsmasq threw %s'), exc) @@ -788,7 +784,7 @@ def _ra_pid_for(bridge): def _ip_bridge_cmd(action, params, device): """Build commands to add/del ips to bridges/devices.""" - cmd = ['sudo', 'ip', 'addr', action] + cmd = ['ip', 'addr', action] cmd.extend(params) cmd.extend(['dev', device]) return cmd diff --git a/nova/network/manager.py b/nova/network/manager.py index 8fc6a295f..b1b3f8ba2 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -61,6 +61,7 @@ from nova import quota from nova import utils from nova import rpc from nova.network import api as network_api +from nova.compute import api as compute_api import random @@ -313,6 +314,7 @@ class NetworkManager(manager.SchedulerDependentManager): network_driver = FLAGS.network_driver self.driver = utils.import_object(network_driver) self.network_api = network_api.API() + self.compute_api = compute_api.API() super(NetworkManager, self).__init__(service_name='network', *args, **kwargs) @@ -368,6 +370,15 @@ class NetworkManager(manager.SchedulerDependentManager): self.host) return host + def _do_trigger_security_group_members_refresh_for_instance(self, + instance_id): + admin_context = context.get_admin_context() + instance_ref = self.db.instance_get(admin_context, instance_id) + groups = instance_ref['security_groups'] + group_ids = [group['id'] for group in groups] + self.compute_api.trigger_security_group_members_refresh(admin_context, + group_ids) + def _get_networks_for_instance(self, context, instance_id, project_id): """Determine & return which networks an instance should connect to.""" # TODO(tr3buchet) maybe this needs to be updated in the future if @@ -559,6 +570,8 @@ class NetworkManager(manager.SchedulerDependentManager): address = self.db.fixed_ip_associate_pool(context.elevated(), network['id'], instance_id) + self._do_trigger_security_group_members_refresh_for_instance( + instance_id) get_vif = self.db.virtual_interface_get_by_instance_and_network vif = get_vif(context, instance_id, network['id']) values = {'allocated': True, @@ -573,6 +586,11 @@ class NetworkManager(manager.SchedulerDependentManager): self.db.fixed_ip_update(context, address, {'allocated': False, 'virtual_interface_id': None}) + fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) + instance_ref = fixed_ip_ref['instance'] + instance_id = instance_ref['id'] + self._do_trigger_security_group_members_refresh_for_instance( + instance_id) def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" @@ -614,6 +632,64 @@ class NetworkManager(manager.SchedulerDependentManager): network_ref = self.db.fixed_ip_get_network(context, address) self._setup_network(context, network_ref) + def _validate_cidrs(self, context, cidr, num_networks, network_size): + significant_bits = 32 - int(math.log(network_size, 2)) + req_net = netaddr.IPNetwork(cidr) + req_net_ip = str(req_net.ip) + req_size = network_size * num_networks + if req_size > req_net.size: + msg = _("network_size * num_networks exceeds cidr size") + raise ValueError(msg) + adjusted_cidr_str = req_net_ip + '/' + str(significant_bits) + adjusted_cidr = netaddr.IPNetwork(adjusted_cidr_str) + try: + used_nets = self.db.network_get_all(context) + except exception.NoNetworksFound: + used_nets = [] + used_cidrs = [netaddr.IPNetwork(net['cidr']) for net in used_nets] + if adjusted_cidr in used_cidrs: + raise ValueError(_("cidr already in use")) + for adjusted_cidr_supernet in adjusted_cidr.supernet(): + if adjusted_cidr_supernet in used_cidrs: + msg = _("requested cidr (%s) conflicts with existing supernet") + raise ValueError(msg % str(adjusted_cidr)) + # watch for smaller subnets conflicting + used_supernets = [] + for used_cidr in used_cidrs: + if not used_cidr: + continue + if used_cidr.size < network_size: + for ucsupernet in used_cidr.supernet(): + if ucsupernet.size == network_size: + used_supernets.append(ucsupernet) + all_req_nets = [] + if num_networks == 1: + if adjusted_cidr in used_supernets: + msg = _("requested cidr (%s) conflicts with existing smaller" + " cidr") + raise ValueError(msg % str(adjusted_cidr)) + else: + all_req_nets.append(adjusted_cidr) + elif num_networks >= 2: + # split supernet into subnets + next_cidr = adjusted_cidr + for index in range(num_networks): + if next_cidr.first > req_net.last: + msg = _("Not enough subnets avail to satisfy requested " + "num_net works - some subnets in requested range" + " already in use") + raise ValueError(msg) + while True: + used_values = used_cidrs + used_supernets + if next_cidr in used_values: + next_cidr = next_cidr.next() + else: + all_req_nets.append(next_cidr) + next_cidr = next_cidr.next() + break + all_req_nets = sorted(list(set(all_req_nets))) + return all_req_nets + def create_networks(self, context, label, cidr, multi_host, num_networks, network_size, cidr_v6, gateway_v6, bridge, bridge_interface, dns1=None, dns2=None, **kwargs): @@ -624,8 +700,8 @@ class NetworkManager(manager.SchedulerDependentManager): network_size_v6 = 1 << 64 if cidr: - fixed_net = netaddr.IPNetwork(cidr) - significant_bits = 32 - int(math.log(network_size, 2)) + req_cidrs = self._validate_cidrs(context, cidr, num_networks, + network_size) for index in range(num_networks): net = {} @@ -635,9 +711,7 @@ class NetworkManager(manager.SchedulerDependentManager): net['dns2'] = dns2 if cidr: - start = index * network_size - project_net = netaddr.IPNetwork('%s/%s' % (fixed_net[start], - significant_bits)) + project_net = req_cidrs[index] net['cidr'] = str(project_net) net['multi_host'] = multi_host net['netmask'] = str(project_net.netmask) @@ -857,7 +931,8 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): address = self.db.fixed_ip_associate_pool(context, network['id'], instance_id) - + self._do_trigger_security_group_members_refresh_for_instance( + instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, instance_id, network['id']) diff --git a/nova/objectstore/s3server.py b/nova/objectstore/s3server.py index 76025a1e3..1ab47b034 100644 --- a/nova/objectstore/s3server.py +++ b/nova/objectstore/s3server.py @@ -155,7 +155,10 @@ class BaseRequestHandler(object): self.finish('<?xml version="1.0" encoding="UTF-8"?>\n' + ''.join(parts)) - def _render_parts(self, value, parts=[]): + def _render_parts(self, value, parts=None): + if not parts: + parts = [] + if isinstance(value, basestring): parts.append(utils.xhtml_escape(value)) elif isinstance(value, int) or isinstance(value, long): diff --git a/nova/rpc/amqp.py b/nova/rpc/amqp.py index 61555795a..fe429b266 100644 --- a/nova/rpc/amqp.py +++ b/nova/rpc/amqp.py @@ -257,7 +257,7 @@ class TopicAdapterConsumer(AdapterConsumer): self.queue = topic self.routing_key = topic self.exchange = FLAGS.control_exchange - self.durable = False + self.durable = FLAGS.rabbit_durable_queues super(TopicAdapterConsumer, self).__init__(connection=connection, topic=topic, proxy=proxy) @@ -345,7 +345,7 @@ class TopicPublisher(Publisher): def __init__(self, connection=None, topic='broadcast'): self.routing_key = topic self.exchange = FLAGS.control_exchange - self.durable = False + self.durable = FLAGS.rabbit_durable_queues super(TopicPublisher, self).__init__(connection=connection) @@ -373,6 +373,7 @@ class DirectConsumer(Consumer): self.queue = msg_id self.routing_key = msg_id self.exchange = msg_id + self.durable = False self.auto_delete = True self.exclusive = True super(DirectConsumer, self).__init__(connection=connection) @@ -386,6 +387,7 @@ class DirectPublisher(Publisher): def __init__(self, connection=None, msg_id=None): self.routing_key = msg_id self.exchange = msg_id + self.durable = False self.auto_delete = True super(DirectPublisher, self).__init__(connection=connection) @@ -573,7 +575,7 @@ def send_message(topic, message, wait=True): publisher = messaging.Publisher(connection=Connection.instance(), exchange=FLAGS.control_exchange, - durable=False, + durable=FLAGS.rabbit_durable_queues, exchange_type='topic', routing_key=topic) publisher.send(message) diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 137b671c0..55cea5f8f 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -17,7 +17,8 @@ Handles all requests relating to schedulers. """ -import novaclient +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions from nova import db from nova import exception @@ -112,7 +113,7 @@ def _wrap_method(function, self): def _process(func, zone): """Worker stub for green thread pool. Give the worker an authenticated nova client and zone info.""" - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() return func(nova, zone) @@ -132,10 +133,10 @@ def call_zone_method(context, method_name, errors_to_ignore=None, zones = db.zone_get_all(context) for zone in zones: try: - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: url = zone.api_url LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s") % locals()) @@ -188,7 +189,7 @@ def _issue_novaclient_command(nova, zone, collection, if method_name in ['find', 'findall']: try: return getattr(manager, method_name)(**kwargs) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s.%(method_name)s didn't find " "anything matching '%(kwargs)s' on '%(url)s'" % @@ -200,7 +201,7 @@ def _issue_novaclient_command(nova, zone, collection, item = args.pop(0) try: result = manager.get(item) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" % locals())) diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 1bfa7740a..f28353f05 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -30,6 +30,7 @@ from nova import log as logging from nova import rpc from nova import utils from nova.compute import power_state +from nova.api.ec2 import ec2utils FLAGS = flags.FLAGS @@ -78,7 +79,8 @@ class Scheduler(object): """Must override at least this method for scheduler to work.""" raise NotImplementedError(_("Must implement a fallback schedule")) - def schedule_live_migration(self, context, instance_id, dest): + def schedule_live_migration(self, context, instance_id, dest, + block_migration=False): """Live migration scheduling method. :param context: @@ -87,9 +89,7 @@ class Scheduler(object): :return: The host where instance is running currently. Then scheduler send request that host. - """ - # Whether instance exists and is running. instance_ref = db.instance_get(context, instance_id) @@ -97,10 +97,11 @@ class Scheduler(object): self._live_migration_src_check(context, instance_ref) # Checking destination host. - self._live_migration_dest_check(context, instance_ref, dest) - + self._live_migration_dest_check(context, instance_ref, + dest, block_migration) # Common checking. - self._live_migration_common_check(context, instance_ref, dest) + self._live_migration_common_check(context, instance_ref, + dest, block_migration) # Changing instance_state. db.instance_set_state(context, @@ -130,7 +131,8 @@ class Scheduler(object): # Checking instance is running. if (power_state.RUNNING != instance_ref['state'] or \ 'running' != instance_ref['state_description']): - raise exception.InstanceNotRunning(instance_id=instance_ref['id']) + instance_id = ec2utils.id_to_ec2_id(instance_ref['id']) + raise exception.InstanceNotRunning(instance_id=instance_id) # Checing volume node is running when any volumes are mounted # to the instance. @@ -147,7 +149,8 @@ class Scheduler(object): if not self.service_is_up(services[0]): raise exception.ComputeServiceUnavailable(host=src) - def _live_migration_dest_check(self, context, instance_ref, dest): + def _live_migration_dest_check(self, context, instance_ref, dest, + block_migration): """Live migration check routine (for destination host). :param context: security context @@ -168,16 +171,18 @@ class Scheduler(object): # and dest is not same. src = instance_ref['host'] if dest == src: - raise exception.UnableToMigrateToSelf( - instance_id=instance_ref['id'], - host=dest) + instance_id = ec2utils.id_to_ec2_id(instance_ref['id']) + raise exception.UnableToMigrateToSelf(instance_id=instance_id, + host=dest) # Checking dst host still has enough capacities. self.assert_compute_node_has_enough_resources(context, instance_ref, - dest) + dest, + block_migration) - def _live_migration_common_check(self, context, instance_ref, dest): + def _live_migration_common_check(self, context, instance_ref, dest, + block_migration): """Live migration common check routine. Below checkings are followed by @@ -186,11 +191,26 @@ class Scheduler(object): :param context: security context :param instance_ref: nova.db.sqlalchemy.models.Instance object :param dest: destination host + :param block_migration if True, check for block_migration. """ # Checking shared storage connectivity - self.mounted_on_same_shared_storage(context, instance_ref, dest) + # if block migration, instances_paths should not be on shared storage. + try: + self.mounted_on_same_shared_storage(context, instance_ref, dest) + if block_migration: + reason = _("Block migration can not be used " + "with shared storage.") + raise exception.InvalidSharedStorage(reason=reason, path=dest) + except exception.FileNotFound: + if not block_migration: + src = instance_ref['host'] + ipath = FLAGS.instances_path + logging.error(_("Cannot confirm tmpfile at %(ipath)s is on " + "same shared storage between %(src)s " + "and %(dest)s.") % locals()) + raise # Checking dest exists. dservice_refs = db.service_get_all_compute_by_host(context, dest) @@ -229,14 +249,26 @@ class Scheduler(object): "original host %(src)s.") % locals()) raise - def assert_compute_node_has_enough_resources(self, context, - instance_ref, dest): + def assert_compute_node_has_enough_resources(self, context, instance_ref, + dest, block_migration): + """Checks if destination host has enough resource for live migration. - Currently, only memory checking has been done. - If storage migration(block migration, meaning live-migration - without any shared storage) will be available, local storage - checking is also necessary. + :param context: security context + :param instance_ref: nova.db.sqlalchemy.models.Instance object + :param dest: destination host + :param block_migration: if True, disk checking has been done + + """ + self.assert_compute_node_has_enough_memory(context, instance_ref, dest) + if not block_migration: + return + self.assert_compute_node_has_enough_disk(context, instance_ref, dest) + + def assert_compute_node_has_enough_memory(self, context, + instance_ref, dest): + """Checks if destination host has enough memory for live migration. + :param context: security context :param instance_ref: nova.db.sqlalchemy.models.Instance object @@ -244,23 +276,70 @@ class Scheduler(object): """ - # Getting instance information - hostname = instance_ref['hostname'] + # Getting total available memory and disk of host + avail = self._get_compute_info(context, dest, 'memory_mb') - # Getting host information - service_refs = db.service_get_all_compute_by_host(context, dest) - compute_node_ref = service_refs[0]['compute_node'][0] + # Getting total used memory and disk of host + # It should be sum of memories that are assigned as max value, + # because overcommiting is risky. + used = 0 + instance_refs = db.instance_get_all_by_host(context, dest) + used_list = [i['memory_mb'] for i in instance_refs] + if used_list: + used = reduce(lambda x, y: x + y, used_list) - mem_total = int(compute_node_ref['memory_mb']) - mem_used = int(compute_node_ref['memory_mb_used']) - mem_avail = mem_total - mem_used mem_inst = instance_ref['memory_mb'] - if mem_avail <= mem_inst: - reason = _("Unable to migrate %(hostname)s to destination: " - "%(dest)s (host:%(mem_avail)s <= instance:" - "%(mem_inst)s)") + avail = avail - used + if avail <= mem_inst: + instance_id = ec2utils.id_to_ec2_id(instance_ref['id']) + reason = _("Unable to migrate %(instance_id)s to %(dest)s: " + "Lack of disk(host:%(avail)s <= instance:%(mem_inst)s)") + raise exception.MigrationError(reason=reason % locals()) + + def assert_compute_node_has_enough_disk(self, context, + instance_ref, dest): + """Checks if destination host has enough disk for block migration. + + :param context: security context + :param instance_ref: nova.db.sqlalchemy.models.Instance object + :param dest: destination host + + """ + + # Getting total available memory and disk of host + avail = self._get_compute_info(context, dest, 'local_gb') + + # Getting total used memory and disk of host + # It should be sum of disks that are assigned as max value + # because overcommiting is risky. + used = 0 + instance_refs = db.instance_get_all_by_host(context, dest) + used_list = [i['local_gb'] for i in instance_refs] + if used_list: + used = reduce(lambda x, y: x + y, used_list) + + disk_inst = instance_ref['local_gb'] + avail = avail - used + if avail <= disk_inst: + instance_id = ec2utils.id_to_ec2_id(instance_ref['id']) + reason = _("Unable to migrate %(instance_id)s to %(dest)s: " + "Lack of disk(host:%(avail)s " + "<= instance:%(disk_inst)s)") raise exception.MigrationError(reason=reason % locals()) + def _get_compute_info(self, context, host, key): + """get compute node's infomation specified by key + + :param context: security context + :param host: hostname(must be compute node) + :param key: column name of compute_nodes + :return: value specified by key + + """ + compute_node_ref = db.service_get_all_compute_by_host(context, host) + compute_node_ref = compute_node_ref[0]['compute_node'][0] + return compute_node_ref[key] + def mounted_on_same_shared_storage(self, context, instance_ref, dest): """Check if the src and dest host mount same shared storage. @@ -283,15 +362,13 @@ class Scheduler(object): {"method": 'create_shared_storage_test_file'}) # make sure existence at src host. - rpc.call(context, src_t, - {"method": 'check_shared_storage_test_file', - "args": {'filename': filename}}) + ret = rpc.call(context, src_t, + {"method": 'check_shared_storage_test_file', + "args": {'filename': filename}}) + if not ret: + raise exception.FileNotFound(file_path=filename) - except rpc.RemoteError: - ipath = FLAGS.instances_path - logging.error(_("Cannot confirm tmpfile at %(ipath)s is on " - "same shared storage between %(src)s " - "and %(dest)s.") % locals()) + except exception.FileNotFound: raise finally: diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py index 8c400d476..329107efe 100644 --- a/nova/scheduler/least_cost.py +++ b/nova/scheduler/least_cost.py @@ -96,7 +96,8 @@ class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler): cost_fn_str=cost_fn_str) try: - weight = getattr(FLAGS, "%s_weight" % cost_fn.__name__) + flag_name = "%s_weight" % cost_fn.__name__ + weight = getattr(FLAGS, flag_name) except AttributeError: raise exception.SchedulerWeightFlagNotFound( flag_name=flag_name) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 749d66cad..0e395ee79 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -34,12 +34,13 @@ from nova.scheduler import zone_manager LOG = logging.getLogger('nova.scheduler.manager') FLAGS = flags.FLAGS flags.DEFINE_string('scheduler_driver', - 'nova.scheduler.chance.ChanceScheduler', - 'Driver to use for the scheduler') + 'nova.scheduler.multi.MultiScheduler', + 'Default driver to use for the scheduler') class SchedulerManager(manager.Manager): """Chooses a host to run instances on.""" + def __init__(self, scheduler_driver=None, *args, **kwargs): self.zone_manager = zone_manager.ZoneManager() if not scheduler_driver: @@ -69,8 +70,10 @@ class SchedulerManager(manager.Manager): return self.zone_manager.get_zone_capabilities(context) def update_service_capabilities(self, context=None, service_name=None, - host=None, capabilities={}): + host=None, capabilities=None): """Process a capability update from a service node.""" + if not capabilities: + capabilities = {} self.zone_manager.update_service_capabilities(service_name, host, capabilities) @@ -111,7 +114,7 @@ class SchedulerManager(manager.Manager): # NOTE (masumotok) : This method should be moved to nova.api.ec2.admin. # Based on bexar design summit discussion, # just put this here for bexar release. - def show_host_resources(self, context, host, *args): + def show_host_resources(self, context, host): """Shows the physical/usage resource given by hosts. :param context: security context @@ -119,43 +122,45 @@ class SchedulerManager(manager.Manager): :returns: example format is below. {'resource':D, 'usage':{proj_id1:D, proj_id2:D}} - D: {'vcpus':3, 'memory_mb':2048, 'local_gb':2048} + D: {'vcpus': 3, 'memory_mb': 2048, 'local_gb': 2048, + 'vcpus_used': 12, 'memory_mb_used': 10240, + 'local_gb_used': 64} """ + # Getting compute node info and related instances info compute_ref = db.service_get_all_compute_by_host(context, host) compute_ref = compute_ref[0] - - # Getting physical resource information - compute_node_ref = compute_ref['compute_node'][0] - resource = {'vcpus': compute_node_ref['vcpus'], - 'memory_mb': compute_node_ref['memory_mb'], - 'local_gb': compute_node_ref['local_gb'], - 'vcpus_used': compute_node_ref['vcpus_used'], - 'memory_mb_used': compute_node_ref['memory_mb_used'], - 'local_gb_used': compute_node_ref['local_gb_used']} - - # Getting usage resource information - usage = {} instance_refs = db.instance_get_all_by_host(context, compute_ref['host']) + + # Getting total available/used resource + compute_ref = compute_ref['compute_node'][0] + resource = {'vcpus': compute_ref['vcpus'], + 'memory_mb': compute_ref['memory_mb'], + 'local_gb': compute_ref['local_gb'], + 'vcpus_used': compute_ref['vcpus_used'], + 'memory_mb_used': compute_ref['memory_mb_used'], + 'local_gb_used': compute_ref['local_gb_used']} + usage = dict() if not instance_refs: return {'resource': resource, 'usage': usage} + # Getting usage resource per project project_ids = [i['project_id'] for i in instance_refs] project_ids = list(set(project_ids)) for project_id in project_ids: - vcpus = db.instance_get_vcpu_sum_by_host_and_project(context, - host, - project_id) - mem = db.instance_get_memory_sum_by_host_and_project(context, - host, - project_id) - hdd = db.instance_get_disk_sum_by_host_and_project(context, - host, - project_id) - usage[project_id] = {'vcpus': int(vcpus), - 'memory_mb': int(mem), - 'local_gb': int(hdd)} + vcpus = [i['vcpus'] for i in instance_refs \ + if i['project_id'] == project_id] + + mem = [i['memory_mb'] for i in instance_refs \ + if i['project_id'] == project_id] + + disk = [i['local_gb'] for i in instance_refs \ + if i['project_id'] == project_id] + + usage[project_id] = {'vcpus': reduce(lambda x, y: x + y, vcpus), + 'memory_mb': reduce(lambda x, y: x + y, mem), + 'local_gb': reduce(lambda x, y: x + y, disk)} return {'resource': resource, 'usage': usage} diff --git a/nova/scheduler/multi.py b/nova/scheduler/multi.py new file mode 100644 index 000000000..b1578033c --- /dev/null +++ b/nova/scheduler/multi.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Scheduler that allows routing some calls to one driver and others to another. +""" + +from nova import flags +from nova import utils +from nova.scheduler import driver + + +FLAGS = flags.FLAGS +flags.DEFINE_string('compute_scheduler_driver', + 'nova.scheduler.chance.ChanceScheduler', + 'Driver to use for scheduling compute calls') +flags.DEFINE_string('volume_scheduler_driver', + 'nova.scheduler.chance.ChanceScheduler', + 'Driver to use for scheduling volume calls') + + +# A mapping of methods to topics so we can figure out which driver to use. +_METHOD_MAP = {'run_instance': 'compute', + 'start_instance': 'compute', + 'create_volume': 'volume'} + + +class MultiScheduler(driver.Scheduler): + """A scheduler that holds multiple sub-schedulers. + + This exists to allow flag-driven composibility of schedulers, allowing + third parties to integrate custom schedulers more easily. + + """ + + def __init__(self): + super(MultiScheduler, self).__init__() + compute_driver = utils.import_object(FLAGS.compute_scheduler_driver) + volume_driver = utils.import_object(FLAGS.volume_scheduler_driver) + + self.drivers = {'compute': compute_driver, + 'volume': volume_driver} + + def __getattr__(self, key): + if not key.startswith('schedule_'): + raise AttributeError(key) + method = key[len('schedule_'):] + if method not in _METHOD_MAP: + raise AttributeError(key) + return getattr(self.drivers[_METHOD_MAP[method]], key) + + def set_zone_manager(self, zone_manager): + for k, v in self.drivers.iteritems(): + v.set_zone_manager(zone_manager) + + def schedule(self, context, topic, *_args, **_kwargs): + return self.drivers[topic].schedule(context, topic, *_args, **_kwargs) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index d99d7214c..d1924c9f9 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -24,7 +24,9 @@ import operator import json import M2Crypto -import novaclient + +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions from nova import crypto from nova import db @@ -58,12 +60,13 @@ class ZoneAwareScheduler(driver.Scheduler): """Create the requested resource in this Zone.""" host = build_plan_item['hostname'] base_options = request_spec['instance_properties'] + image = request_spec['image'] # TODO(sandy): I guess someone needs to add block_device_mapping # support at some point? Also, OS API has no concept of security # groups. instance = compute_api.API().create_db_entry_for_new_instance(context, - base_options, None, []) + image, base_options, None, []) instance_id = instance['id'] kwargs['instance_id'] = instance_id @@ -117,10 +120,9 @@ class ZoneAwareScheduler(driver.Scheduler): % locals()) nova = None try: - nova = novaclient.OpenStack(zone.username, zone.password, None, - url) + nova = novaclient.Client(zone.username, zone.password, None, url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: raise exception.NotAuthorized(_("Bad credentials attempting " "to talk to zone at %(url)s.") % locals()) @@ -264,8 +266,8 @@ class ZoneAwareScheduler(driver.Scheduler): """ if topic != "compute": - raise NotImplemented(_("Zone Aware Scheduler only understands " - "Compute nodes (for now)")) + raise NotImplementedError(_("Zone Aware Scheduler only understands" + " Compute nodes (for now)")) num_instances = request_spec.get('num_instances', 1) instance_type = request_spec['instance_type'] diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index efdac06e1..9d05ea42e 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -18,10 +18,11 @@ ZoneManager oversees all communications with child Zones. """ import datetime -import novaclient import thread import traceback +from novaclient import v1_1 as novaclient + from eventlet import greenpool from nova import db @@ -89,8 +90,8 @@ class ZoneState(object): def _call_novaclient(zone): """Call novaclient. Broken out for testing purposes.""" - client = novaclient.OpenStack(zone.username, zone.password, None, - zone.api_url) + client = novaclient.Client(zone.username, zone.password, None, + zone.api_url) return client.zones.info()._info @@ -197,7 +198,7 @@ class ZoneManager(object): def update_service_capabilities(self, service_name, host, capabilities): """Update the per-service capabilities based on this notification.""" logging.debug(_("Received %(service_name)s service update from " - "%(host)s: %(capabilities)s") % locals()) + "%(host)s.") % locals()) service_caps = self.service_states.get(host, {}) capabilities["timestamp"] = utils.utcnow() # Reported time service_caps[service_name] = capabilities diff --git a/nova/test.py b/nova/test.py index 5760d7a82..88f1489e8 100644 --- a/nova/test.py +++ b/nova/test.py @@ -60,11 +60,42 @@ class skip_test(object): self.message = msg def __call__(self, func): + @functools.wraps(func) def _skipper(*args, **kw): """Wrapped skipper function.""" raise nose.SkipTest(self.message) - _skipper.__name__ = func.__name__ - _skipper.__doc__ = func.__doc__ + return _skipper + + +class skip_if(object): + """Decorator that skips a test if contition is true.""" + def __init__(self, condition, msg): + self.condition = condition + self.message = msg + + def __call__(self, func): + @functools.wraps(func) + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if self.condition: + raise nose.SkipTest(self.message) + func(*args, **kw) + return _skipper + + +class skip_unless(object): + """Decorator that skips a test if condition is not true.""" + def __init__(self, condition, msg): + self.condition = condition + self.message = msg + + def __call__(self, func): + @functools.wraps(func) + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if not self.condition: + raise nose.SkipTest(self.message) + func(*args, **kw) return _skipper diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index ab7ae2e54..704d06582 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -116,14 +116,14 @@ class FloatingIpTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) - response = {'floating_ips': [{'floating_ip': {'instance_id': 11, - 'ip': '10.10.10.10', - 'fixed_ip': '10.0.0.1', - 'id': 1}}, - {'floating_ip': {'instance_id': None, - 'ip': '10.10.10.11', - 'fixed_ip': None, - 'id': 2}}]} + response = {'floating_ips': [{'instance_id': 11, + 'ip': '10.10.10.10', + 'fixed_ip': '10.0.0.1', + 'id': 1}, + {'instance_id': None, + 'ip': '10.10.10.11', + 'fixed_ip': None, + 'id': 2}]} self.assertEqual(res_dict, response) def test_floating_ip_show(self): @@ -177,8 +177,10 @@ class FloatingIpTest(test.TestCase): self.assertEqual(actual, expected) def test_floating_ip_disassociate(self): + body = dict() req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate') req.method = 'POST' + req.body = json.dumps(body) req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py new file mode 100644 index 000000000..eb3bc7af0 --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -0,0 +1,112 @@ +# Copyright 2011 Eldar Nugaev +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import webob + +from nova import context +from nova import db +from nova import test +from nova.api.openstack.contrib.keypairs import KeypairController +from nova.tests.api.openstack import fakes + + +def fake_keypair(name): + return {'public_key': 'FAKE_KEY', + 'fingerprint': 'FAKE_FINGERPRINT', + 'name': name} + + +def db_key_pair_get_all_by_user(self, user_id): + return [fake_keypair('FAKE')] + + +def db_key_pair_create(self, keypair): + pass + + +def db_key_pair_destroy(context, user_id, name): + if not (user_id and name): + raise Exception() + + +class KeypairsTest(test.TestCase): + + def setUp(self): + super(KeypairsTest, self).setUp() + self.controller = KeypairController() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(db, "key_pair_get_all_by_user", + db_key_pair_get_all_by_user) + self.stubs.Set(db, "key_pair_create", + db_key_pair_create) + self.stubs.Set(db, "key_pair_destroy", + db_key_pair_destroy) + self.context = context.get_admin_context() + + def test_keypair_list(self): + req = webob.Request.blank('/v1.1/os-keypairs') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'keypairs': [{'keypair': fake_keypair('FAKE')}]} + self.assertEqual(res_dict, response) + + def test_keypair_create(self): + body = {'keypair': {'name': 'create_test'}} + req = webob.Request.blank('/v1.1/os-keypairs') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertTrue(len(res_dict['keypair']['private_key']) > 0) + + def test_keypair_import(self): + body = { + 'keypair': { + 'name': 'create_test', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA' + 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4' + 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y' + 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi' + 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj' + 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1' + 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc' + 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB' + 'bHkXa6OciiJDvkRzJXzf', + }, + } + + req = webob.Request.blank('/v1.1/os-keypairs') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + # FIXME(ja): sholud we check that public_key was sent to create? + res_dict = json.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertFalse('private_key' in res_dict['keypair']) + + def test_keypair_delete(self): + req = webob.Request.blank('/v1.1/os-keypairs/FAKE') + req.method = 'DELETE' + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py new file mode 100644 index 000000000..4317880ca --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -0,0 +1,761 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +import json +import unittest +import webob +from xml.dom import minidom + +from nova import test +from nova.api.openstack.contrib import security_groups +from nova.tests.api.openstack import fakes + + +def _get_create_request_json(body_dict): + req = webob.Request.blank('/v1.1/os-security-groups') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body_dict) + return req + + +def _create_security_group_json(security_group): + body_dict = _create_security_group_request_dict(security_group) + request = _get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app()) + return response + + +def _create_security_group_request_dict(security_group): + sg = {} + if security_group is not None: + name = security_group.get('name', None) + description = security_group.get('description', None) + if name: + sg['name'] = security_group['name'] + if description: + sg['description'] = security_group['description'] + return {'security_group': sg} + + +class TestSecurityGroups(test.TestCase): + def setUp(self): + super(TestSecurityGroups, self).setUp() + + def tearDown(self): + super(TestSecurityGroups, self).tearDown() + + def _create_security_group_request_dict(self, security_group): + sg = {} + if security_group is not None: + name = security_group.get('name', None) + description = security_group.get('description', None) + if name: + sg['name'] = security_group['name'] + if description: + sg['description'] = security_group['description'] + return {'security_group': sg} + + def _format_create_xml_request_body(self, body_dict): + sg = body_dict['security_group'] + body_parts = [] + body_parts.extend([ + '<?xml version="1.0" encoding="UTF-8"?>', + '<security_group xmlns="http://docs.openstack.org/ext/' + 'securitygroups/api/v1.1"', + ' name="%s">' % (sg['name'])]) + if 'description' in sg: + body_parts.append('<description>%s</description>' + % sg['description']) + body_parts.append('</security_group>') + return ''.join(body_parts) + + def _get_create_request_xml(self, body_dict): + req = webob.Request.blank('/v1.1/os-security-groups') + req.headers['Content-Type'] = 'application/xml' + req.content_type = 'application/xml' + req.accept = 'application/xml' + req.method = 'POST' + req.body = self._format_create_xml_request_body(body_dict) + return req + + def _create_security_group_xml(self, security_group): + body_dict = self._create_security_group_request_dict(security_group) + request = self._get_create_request_xml(body_dict) + response = request.get_response(fakes.wsgi_app()) + return response + + def _delete_security_group(self, id): + request = webob.Request.blank('/v1.1/os-security-groups/%s' + % id) + request.method = 'DELETE' + response = request.get_response(fakes.wsgi_app()) + return response + + def test_create_security_group_json(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + res_dict = json.loads(response.body) + self.assertEqual(res_dict['security_group']['name'], "test") + self.assertEqual(res_dict['security_group']['description'], + "group-description") + self.assertEquals(response.status_int, 200) + + def test_create_security_group_xml(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = \ + self._create_security_group_xml(security_group) + + self.assertEquals(response.status_int, 200) + dom = minidom.parseString(response.body) + sg = dom.childNodes[0] + self.assertEquals(sg.nodeName, 'security_group') + self.assertEqual(security_group['name'], sg.getAttribute('name')) + + def test_create_security_group_with_no_name_json(self): + security_group = {} + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_no_description_json(self): + security_group = {} + security_group['name'] = "test" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_blank_name_json(self): + security_group = {} + security_group['name'] = "" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_whitespace_name_json(self): + security_group = {} + security_group['name'] = " " + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_blank_description_json(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_whitespace_description_json(self): + security_group = {} + security_group['name'] = "name" + security_group['description'] = " " + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_duplicate_name_json(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + + self.assertEquals(response.status_int, 200) + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_with_no_body_json(self): + request = _get_create_request_json(body_dict=None) + response = request.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_create_security_group_with_no_security_group(self): + body_dict = {} + body_dict['no-securityGroup'] = None + request = _get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_create_security_group_above_255_characters_name_json(self): + security_group = {} + security_group['name'] = ("1234567890123456" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890") + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + + self.assertEquals(response.status_int, 400) + + def test_create_security_group_above_255_characters_description_json(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = ("1234567890123456" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890") + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_non_string_name_json(self): + security_group = {} + security_group['name'] = 12 + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_create_security_group_non_string_description_json(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = 12 + response = _create_security_group_json(security_group) + self.assertEquals(response.status_int, 400) + + def test_get_security_group_list(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + + req = webob.Request.blank('/v1.1/os-security-groups') + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + response = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(response.body) + + expected = {'security_groups': [ + {'id': 1, + 'name':"default", + 'tenant_id': "fake", + "description":"default", + "rules": [] + }, + ] + } + expected['security_groups'].append( + { + 'id': 2, + 'name': "test", + 'tenant_id': "fake", + "description": "group-description", + "rules": [] + } + ) + self.assertEquals(response.status_int, 200) + self.assertEquals(res_dict, expected) + + def test_get_security_group_by_id(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + + res_dict = json.loads(response.body) + req = webob.Request.blank('/v1.1/os-security-groups/%s' % + res_dict['security_group']['id']) + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + response = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(response.body) + + expected = { + 'security_group': { + 'id': 2, + 'name': "test", + 'tenant_id': "fake", + 'description': "group-description", + 'rules': [] + } + } + self.assertEquals(response.status_int, 200) + self.assertEquals(res_dict, expected) + + def test_get_security_group_by_invalid_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/invalid') + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_get_security_group_by_non_existing_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/111111111') + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_delete_security_group_by_id(self): + security_group = {} + security_group['name'] = "test" + security_group['description'] = "group-description" + response = _create_security_group_json(security_group) + security_group = json.loads(response.body)['security_group'] + response = self._delete_security_group(security_group['id']) + self.assertEquals(response.status_int, 202) + + response = self._delete_security_group(security_group['id']) + self.assertEquals(response.status_int, 404) + + def test_delete_security_group_by_invalid_id(self): + response = self._delete_security_group('invalid') + self.assertEquals(response.status_int, 400) + + def test_delete_security_group_by_non_existing_id(self): + response = self._delete_security_group(11111111) + self.assertEquals(response.status_int, 404) + + +class TestSecurityGroupRules(test.TestCase): + def setUp(self): + super(TestSecurityGroupRules, self).setUp() + security_group = {} + security_group['name'] = "authorize-revoke" + security_group['description'] = ("Security group created for " + " authorize-revoke testing") + response = _create_security_group_json(security_group) + security_group = json.loads(response.body) + self.parent_security_group = security_group['security_group'] + + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "parent_group_id": self.parent_security_group['id'], + "cidr": "10.0.0.0/24" + } + } + res = self._create_security_group_rule_json(rules) + self.assertEquals(res.status_int, 200) + self.security_group_rule = json.loads(res.body)['security_group_rule'] + + def tearDown(self): + super(TestSecurityGroupRules, self).tearDown() + + def _create_security_group_rule_json(self, rules): + request = webob.Request.blank('/v1.1/os-security-group-rules') + request.headers['Content-Type'] = 'application/json' + request.method = 'POST' + request.body = json.dumps(rules) + response = request.get_response(fakes.wsgi_app()) + return response + + def _delete_security_group_rule(self, id): + request = webob.Request.blank('/v1.1/os-security-group-rules/%s' + % id) + request.method = 'DELETE' + response = request.get_response(fakes.wsgi_app()) + return response + + def test_create_by_cidr_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "parent_group_id": 2, + "cidr": "10.2.3.124/24" + } + } + + response = self._create_security_group_rule_json(rules) + security_group_rule = json.loads(response.body)['security_group_rule'] + self.assertEquals(response.status_int, 200) + self.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], 2) + self.assertEquals(security_group_rule['ip_range']['cidr'], + "10.2.3.124/24") + + def test_create_by_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "1", + "parent_group_id": "%s" + % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 200) + security_group_rule = json.loads(response.body)['security_group_rule'] + self.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], 2) + + def test_create_add_existing_rules_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "cidr": "10.0.0.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_no_body_json(self): + request = webob.Request.blank('/v1.1/os-security-group-rules') + request.headers['Content-Type'] = 'application/json' + request.method = 'POST' + request.body = json.dumps(None) + response = request.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_create_with_no_security_group_rule_in_body_json(self): + request = webob.Request.blank('/v1.1/os-security-group-rules') + request.headers['Content-Type'] = 'application/json' + request.method = 'POST' + body_dict = {'test': "test"} + request.body = json.dumps(body_dict) + response = request.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_create_with_invalid_parent_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "parent_group_id": "invalid" + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_non_existing_parent_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "invalid", + "parent_group_id": "1111111111111" + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 404) + + def test_create_with_invalid_protocol_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "invalid-protocol", + "from_port": "22", + "to_port": "22", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_no_protocol_json(self): + rules = { + "security_group_rule": { + "from_port": "22", + "to_port": "22", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_invalid_from_port_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "666666", + "to_port": "22", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_invalid_to_port_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "666666", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_non_numerical_from_port_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "invalid", + "to_port": "22", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_non_numerical_to_port_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "invalid", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_no_to_port_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "cidr": "10.2.2.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_invalid_cidr_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "cidr": "10.2.22222.0/24", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_no_cidr_group_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + security_group_rule = json.loads(response.body)['security_group_rule'] + self.assertEquals(response.status_int, 200) + self.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEquals(security_group_rule['ip_range']['cidr'], + "0.0.0.0/0") + + def test_create_with_invalid_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "invalid", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_empty_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "invalid", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_with_invalid_group_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "222222", + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_create_rule_with_same_group_parent_id_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "group_id": "%s" % self.parent_security_group['id'], + "parent_group_id": "%s" % self.parent_security_group['id'], + } + } + + response = self._create_security_group_rule_json(rules) + self.assertEquals(response.status_int, 400) + + def test_delete(self): + response = self._delete_security_group_rule( + self.security_group_rule['id']) + self.assertEquals(response.status_int, 202) + + response = self._delete_security_group_rule( + self.security_group_rule['id']) + self.assertEquals(response.status_int, 404) + + def test_delete_invalid_rule_id(self): + response = self._delete_security_group_rule('invalid') + self.assertEquals(response.status_int, 400) + + def test_delete_non_existing_rule_id(self): + response = self._delete_security_group_rule(22222222222222) + self.assertEquals(response.status_int, 404) + + +class TestSecurityGroupRulesXMLDeserializer(unittest.TestCase): + + def setUp(self): + self.deserializer = security_groups.SecurityGroupRulesXMLDeserializer() + + def test_create_request(self): + serial_request = """ +<security_group_rule> + <parent_group_id>12</parent_group_id> + <from_port>22</from_port> + <to_port>22</to_port> + <group_id></group_id> + <ip_protocol>tcp</ip_protocol> + <cidr>10.0.0.0/24</cidr> +</security_group_rule>""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group_rule": { + "parent_group_id": "12", + "from_port": "22", + "to_port": "22", + "ip_protocol": "tcp", + "group_id": "", + "cidr": "10.0.0.0/24", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_no_protocol_request(self): + serial_request = """ +<security_group_rule> + <parent_group_id>12</parent_group_id> + <from_port>22</from_port> + <to_port>22</to_port> + <group_id></group_id> + <cidr>10.0.0.0/24</cidr> +</security_group_rule>""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group_rule": { + "parent_group_id": "12", + "from_port": "22", + "to_port": "22", + "group_id": "", + "cidr": "10.0.0.0/24", + }, + } + self.assertEquals(request['body'], expected) + + +class TestSecurityGroupXMLDeserializer(unittest.TestCase): + + def setUp(self): + self.deserializer = security_groups.SecurityGroupXMLDeserializer() + + def test_create_request(self): + serial_request = """ +<security_group name="test"> + <description>test</description> +</security_group>""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group": { + "name": "test", + "description": "test", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_no_description_request(self): + serial_request = """ +<security_group name="test"> +</security_group>""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group": { + "name": "test", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_no_name_request(self): + serial_request = """ +<security_group> +<description>test</description> +</security_group>""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group": { + "description": "test", + }, + } + self.assertEquals(request['body'], expected) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index a67a28a4e..d11fbf788 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -71,14 +71,18 @@ def fake_wsgi(self, req): return self.application -def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True): +def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, + fake_auth_context=None): if not inner_app10: inner_app10 = openstack.APIRouterV10() if not inner_app11: inner_app11 = openstack.APIRouterV11() if fake_auth: - ctxt = context.RequestContext('fake', 'fake') + if fake_auth_context is not None: + ctxt = fake_auth_context + else: + ctxt = context.RequestContext('fake', 'fake') api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt, limits.RateLimitingMiddleware(inner_app10))) api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt, diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 5a6e43579..b422bc4d1 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -249,6 +249,10 @@ class MiscFunctionsTest(test.TestCase): common.get_id_from_href, fixture) + def test_get_id_from_href_int(self): + fixture = 1 + self.assertEqual(fixture, common.get_id_from_href(fixture)) + def test_get_version_from_href(self): fixture = 'http://www.testsite.com/v1.1/images' expected = '1.1' diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 409fa0e71..9a613681d 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -18,7 +18,7 @@ import json import os.path import webob -from xml.etree import ElementTree +from lxml import etree from nova import context from nova import test @@ -26,6 +26,7 @@ from nova.api import openstack from nova.api.openstack import extensions from nova.api.openstack import flavors from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil from nova.tests.api.openstack import fakes NS = "{http://docs.openstack.org/compute/api/v1.1}" @@ -96,7 +97,8 @@ class ExtensionControllerTest(test.TestCase): names = [x['name'] for x in data['extensions']] names.sort() self.assertEqual(names, ["FlavorExtraSpecs", "Floating_ips", - "Fox In Socks", "Hosts", "Multinic", "Volumes"]) + "Fox In Socks", "Hosts", "Keypairs", "Multinic", "SecurityGroups", + "Volumes"]) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [ @@ -107,7 +109,7 @@ class ExtensionControllerTest(test.TestCase): 'updated': '2011-01-22T13:25:27-06:00', 'description': 'The Fox In Socks Extension', 'alias': 'FOXNSOX', - 'links': [], + 'links': [] }, ) @@ -125,9 +127,7 @@ class ExtensionControllerTest(test.TestCase): "updated": "2011-01-22T13:25:27-06:00", "description": "The Fox In Socks Extension", "alias": "FOXNSOX", - "links": [], - }, - ) + "links": []}) def test_list_extensions_xml(self): app = openstack.APIRouterV11() @@ -138,12 +138,12 @@ class ExtensionControllerTest(test.TestCase): self.assertEqual(200, response.status_int) print response.body - root = ElementTree.XML(response.body) + root = etree.XML(response.body) self.assertEqual(root.tag.split('extensions')[0], NS) # Make sure we have all the extensions. exts = root.findall('{0}extension'.format(NS)) - self.assertEqual(len(exts), 6) + self.assertEqual(len(exts), 8) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [x for x in exts if x.get('alias') == 'FOXNSOX'] @@ -154,6 +154,8 @@ class ExtensionControllerTest(test.TestCase): self.assertEqual(fox_ext.findtext('{0}description'.format(NS)), 'The Fox In Socks Extension') + xmlutil.validate_schema(root, 'extensions') + def test_get_extension_xml(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) @@ -161,9 +163,10 @@ class ExtensionControllerTest(test.TestCase): request.accept = "application/xml" response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) - print response.body + xml = response.body + print xml - root = ElementTree.XML(response.body) + root = etree.XML(xml) self.assertEqual(root.tag.split('extension')[0], NS) self.assertEqual(root.get('alias'), 'FOXNSOX') self.assertEqual(root.get('name'), 'Fox In Socks') @@ -173,6 +176,8 @@ class ExtensionControllerTest(test.TestCase): self.assertEqual(root.findtext('{0}description'.format(NS)), 'The Fox In Socks Extension') + xmlutil.validate_schema(root, 'extension') + class ResourceExtensionTest(test.TestCase): @@ -274,7 +279,7 @@ class ActionExtensionTest(test.TestCase): def test_invalid_action_body(self): body = dict(blah=dict(name="test")) # Doesn't exist response = self._send_server_action_request("/servers/1/action", body) - self.assertEqual(501, response.status_int) + self.assertEqual(400, response.status_int) def test_invalid_action(self): body = dict(blah=dict(name="test")) @@ -329,30 +334,22 @@ class ExtensionsXMLSerializerTest(test.TestCase): def test_serialize_extenstion(self): serializer = extensions.ExtensionsXMLSerializer() - data = { - 'extension': { - 'name': 'ext1', - 'namespace': 'http://docs.rack.com/servers/api/ext/pie/v1.0', - 'alias': 'RS-PIE', - 'updated': '2011-01-22T13:25:27-06:00', - 'description': 'Adds the capability to share an image.', - 'links': [ - { - 'rel': 'describedby', - 'type': 'application/pdf', - 'href': 'http://docs.rack.com/servers/api/ext/cs.pdf', - }, - { - 'rel': 'describedby', - 'type': 'application/vnd.sun.wadl+xml', - 'href': 'http://docs.rack.com/servers/api/ext/cs.wadl', - }, - ], - }, - } + data = {'extension': { + 'name': 'ext1', + 'namespace': 'http://docs.rack.com/servers/api/ext/pie/v1.0', + 'alias': 'RS-PIE', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'Adds the capability to share an image.', + 'links': [{'rel': 'describedby', + 'type': 'application/pdf', + 'href': 'http://docs.rack.com/servers/api/ext/cs.pdf'}, + {'rel': 'describedby', + 'type': 'application/vnd.sun.wadl+xml', + 'href': 'http://docs.rack.com/servers/api/ext/cs.wadl'}]}} xml = serializer.serialize(data, 'show') - root = ElementTree.XML(xml) + print xml + root = etree.XML(xml) ext_dict = data['extension'] self.assertEqual(root.findtext('{0}description'.format(NS)), ext_dict['description']) @@ -366,54 +363,38 @@ class ExtensionsXMLSerializerTest(test.TestCase): for key, value in link.items(): self.assertEqual(link_nodes[i].get(key), value) + xmlutil.validate_schema(root, 'extension') + def test_serialize_extensions(self): serializer = extensions.ExtensionsXMLSerializer() - data = { - "extensions": [ - { - "name": "Public Image Extension", - "namespace": "http://foo.com/api/ext/pie/v1.0", - "alias": "RS-PIE", - "updated": "2011-01-22T13:25:27-06:00", - "description": "Adds the capability to share an image.", - "links": [ - { - "rel": "describedby", + data = {"extensions": [{ + "name": "Public Image Extension", + "namespace": "http://foo.com/api/ext/pie/v1.0", + "alias": "RS-PIE", + "updated": "2011-01-22T13:25:27-06:00", + "description": "Adds the capability to share an image.", + "links": [{"rel": "describedby", "type": "application/pdf", - "href": "http://foo.com/api/ext/cs-pie.pdf", - }, - { - "rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": "http://foo.com/api/ext/cs-pie.wadl", - }, - ], - }, - { - "name": "Cloud Block Storage", - "namespace": "http://foo.com/api/ext/cbs/v1.0", - "alias": "RS-CBS", - "updated": "2011-01-12T11:22:33-06:00", - "description": "Allows mounting cloud block storage.", - "links": [ - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://foo.com/api/ext/cs-cbs.pdf", - }, - { - "rel": "describedby", + "href": "http://foo.com/api/ext/cs-pie.pdf"}, + {"rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": "http://foo.com/api/ext/cs-cbs.wadl", - }, - ], - }, - ], - } + "href": "http://foo.com/api/ext/cs-pie.wadl"}]}, + {"name": "Cloud Block Storage", + "namespace": "http://foo.com/api/ext/cbs/v1.0", + "alias": "RS-CBS", + "updated": "2011-01-12T11:22:33-06:00", + "description": "Allows mounting cloud block storage.", + "links": [{"rel": "describedby", + "type": "application/pdf", + "href": "http://foo.com/api/ext/cs-cbs.pdf"}, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://foo.com/api/ext/cs-cbs.wadl"}]}]} xml = serializer.serialize(data, 'index') print xml - root = ElementTree.XML(xml) + root = etree.XML(xml) ext_elems = root.findall('{0}extension'.format(NS)) self.assertEqual(len(ext_elems), 2) for i, ext_elem in enumerate(ext_elems): @@ -429,3 +410,5 @@ class ExtensionsXMLSerializerTest(test.TestCase): for i, link in enumerate(ext_dict['links']): for key, value in link.items(): self.assertEqual(link_nodes[i].get(key), value) + + xmlutil.validate_schema(root, 'extensions') diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 8e2e3f390..383ed2e03 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -379,6 +379,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, "status": "ACTIVE", + "progress": 100, }, } @@ -402,6 +403,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, "status": "QUEUED", + "progress": 0, 'server': { 'id': 42, "links": [{ @@ -444,6 +446,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE" + progress="100" xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" /> """ % (locals())) @@ -463,6 +466,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE" + progress="100" xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" /> """ % (locals())) @@ -587,6 +591,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, }, { 'id': 124, @@ -594,6 +599,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'progress': 0, }, { 'id': 125, @@ -608,7 +614,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'name': 'active snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, - 'status': 'ACTIVE' + 'status': 'ACTIVE', + 'progress': 100, }, { 'id': 127, @@ -616,6 +623,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'progress': 0, }, { 'id': 129, @@ -623,6 +631,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, }] self.assertDictListMatch(expected, response_list) @@ -643,6 +652,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/123", @@ -662,6 +672,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'progress': 0, 'server': { 'id': 42, "links": [{ @@ -723,6 +734,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, 'server': { 'id': 42, "links": [{ @@ -753,6 +765,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'progress': 0, 'server': { 'id': 42, "links": [{ @@ -780,6 +793,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/129", @@ -1001,7 +1015,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): image_meta = json.loads(res.body)['image'] expected = {'id': 123, 'name': 'public image', 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE'} + 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100} self.assertDictMatch(image_meta, expected) def test_get_image_non_existent(self): @@ -1049,6 +1064,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) + def test_create_image_snapshots_disabled(self): + self.flags(allow_instance_snapshots=False) + body = dict(image=dict(serverId='123', name='Snapshot 1')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + @classmethod def _make_image_fixtures(cls): image_id = 123 diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 6c3d531e3..801b06230 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -819,12 +819,15 @@ class FakeHttplibConnection(object): self.app = app self.host = host - def request(self, method, path, body="", headers={}): + def request(self, method, path, body="", headers=None): """ Requests made via this connection actually get translated and routed into our WSGI app, we then wait for the response and turn it back into an `httplib.HTTPResponse`. """ + if not headers: + headers = {} + req = webob.Request.blank(path) req.method = method req.headers = headers @@ -912,86 +915,56 @@ class LimitsViewBuilderV11Test(test.TestCase): def setUp(self): self.view_builder = views.limits.ViewBuilderV11() - self.rate_limits = [ - { - "URI": "*", - "regex": ".*", - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "resetTime": 1311272226, - }, - { - "URI": "*/servers", - "regex": "^/servers", - "value": 50, - "verb": "POST", - "remaining": 10, - "unit": "DAY", - "resetTime": 1311272226, - }, - ] - self.absolute_limits = { - "metadata_items": 1, - "injected_files": 5, - "injected_file_content_bytes": 5, - } + self.rate_limits = [{"URI": "*", + "regex": ".*", + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "resetTime": 1311272226}, + {"URI": "*/servers", + "regex": "^/servers", + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "resetTime": 1311272226}] + self.absolute_limits = {"metadata_items": 1, + "injected_files": 5, + "injected_file_content_bytes": 5} def tearDown(self): pass def test_build_limits(self): - expected_limits = { - "limits": { - "rate": [ - { - "uri": "*", - "regex": ".*", - "limit": [ - { - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-07-21T18:17:06Z", - }, - ] - }, - { - "uri": "*/servers", - "regex": "^/servers", - "limit": [ - { - "value": 50, - "verb": "POST", - "remaining": 10, - "unit": "DAY", - "next-available": "2011-07-21T18:17:06Z", - }, - ] - }, - ], - "absolute": { - "maxServerMeta": 1, - "maxImageMeta": 1, - "maxPersonality": 5, - "maxPersonalitySize": 5 - } - } - } + expected_limits = {"limits": { + "rate": [{ + "uri": "*", + "regex": ".*", + "limit": [{"value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-07-21T18:17:06Z"}]}, + {"uri": "*/servers", + "regex": "^/servers", + "limit": [{"value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-07-21T18:17:06Z"}]}], + "absolute": {"maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 5}}} output = self.view_builder.build(self.rate_limits, self.absolute_limits) self.assertDictMatch(output, expected_limits) def test_build_limits_empty_limits(self): - expected_limits = { - "limits": { - "rate": [], - "absolute": {}, - } - } + expected_limits = {"limits": {"rate": [], + "absolute": {}}} abs_limits = {} rate_limits = [] @@ -1009,45 +982,28 @@ class LimitsXMLSerializationTest(test.TestCase): def test_index(self): serializer = limits.LimitsXMLSerializer() - - fixture = { - "limits": { - "rate": [ - { - "uri": "*", - "regex": ".*", - "limit": [ - { - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z", - }, - ] - }, - { - "uri": "*/servers", - "regex": "^/servers", - "limit": [ - { - "value": 50, - "verb": "POST", - "remaining": 10, - "unit": "DAY", - "next-available": "2011-12-15T22:42:45Z" - }, - ] - }, - ], - "absolute": { - "maxServerMeta": 1, - "maxImageMeta": 1, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - } - } - } + fixture = {"limits": { + "rate": [{ + "uri": "*", + "regex": ".*", + "limit": [{ + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z"}]}, + {"uri": "*/servers", + "regex": "^/servers", + "limit": [{ + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z"}]}], + "absolute": {"maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 10240}}} output = serializer.serialize(fixture, 'index') actual = minidom.parseString(output.replace(" ", "")) @@ -1080,12 +1036,9 @@ class LimitsXMLSerializationTest(test.TestCase): def test_index_no_limits(self): serializer = limits.LimitsXMLSerializer() - fixture = { - "limits": { - "rate": [], - "absolute": {}, - } - } + fixture = {"limits": { + "rate": [], + "absolute": {}}} output = serializer.serialize(fixture, 'index') actual = minidom.parseString(output.replace(" ", "")) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 562cefe90..687a19390 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -9,6 +9,7 @@ import webob from nova import context from nova import db from nova import utils +from nova import flags from nova.api.openstack import create_instance_helper from nova.compute import instance_types from nova.compute import power_state @@ -18,6 +19,9 @@ from nova.tests.api.openstack import common from nova.tests.api.openstack import fakes +FLAGS = flags.FLAGS + + def return_server_by_id(context, id): return _get_instance() @@ -348,7 +352,7 @@ class ServerActionsTest(test.TestCase): req.body = json.dumps(body) req.headers["content-type"] = "application/json" response = req.get_response(fakes.wsgi_app()) - self.assertEqual(501, response.status_int) + self.assertEqual(400, response.status_int) def test_create_backup_with_metadata(self): self.flags(allow_admin_api=True) @@ -370,6 +374,26 @@ class ServerActionsTest(test.TestCase): self.assertEqual(202, response.status_int) self.assertTrue(response.headers['Location']) + def test_create_backup_with_too_much_metadata(self): + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': {'123': 'asdf'}, + }, + } + for num in range(FLAGS.quota_metadata_items + 1): + body['createBackup']['metadata']['foo%i' % num] = "bar" + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_backup_no_name(self): """Name is required for backups""" self.flags(allow_admin_api=True) @@ -458,10 +482,29 @@ class ServerActionsTestV11(test.TestCase): self.service.delete_all() self.sent_to_glance = {} fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) + self.flags(allow_instance_snapshots=True) def tearDown(self): self.stubs.UnsetAll() + def test_server_bad_body(self): + body = {} + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_unknown_action(self): + body = {'sockTheFox': {'fakekey': '1234'}} + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_server_change_password(self): mock_method = MockSetAdminPassword() self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) @@ -475,6 +518,21 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(mock_method.instance_id, '1') self.assertEqual(mock_method.password, '1234pass') + def test_server_change_password_xml(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = "application/xml" + req.body = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1" + adminPass="1234pass"/>""" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(mock_method.instance_id, '1') + self.assertEqual(mock_method.password, '1234pass') + def test_server_change_password_not_a_string(self): body = {'changePassword': {'adminPass': 1234}} req = webob.Request.blank('/v1.1/servers/1/action') @@ -511,6 +569,42 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_server_reboot_hard(self): + body = dict(reboot=dict(type="HARD")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_reboot_soft(self): + body = dict(reboot=dict(type="SOFT")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_reboot_incorrect_type(self): + body = dict(reboot=dict(type="NOT_A_TYPE")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_reboot_missing_type(self): + body = dict(reboot=dict()) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_server_rebuild_accepted_minimum(self): body = { "rebuild": { @@ -653,6 +747,62 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 202) self.assertEqual(self.resize_called, True) + def test_resize_server_no_flavor(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(resize=dict()) + req.body = json.dumps(body_dict) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_resize_server_no_flavor_ref(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(resize=dict(flavorRef=None)) + req.body = json.dumps(body_dict) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_confirm_resize_server(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(confirmResize=None) + req.body = json.dumps(body_dict) + + self.confirm_resize_called = False + + def cr_mock(*args): + self.confirm_resize_called = True + + self.stubs.Set(nova.compute.api.API, 'confirm_resize', cr_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) + self.assertEqual(self.confirm_resize_called, True) + + def test_revert_resize_server(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(revertResize=None) + req.body = json.dumps(body_dict) + + self.revert_resize_called = False + + def revert_mock(*args): + self.revert_resize_called = True + + self.stubs.Set(nova.compute.api.API, 'revert_resize', revert_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(self.revert_resize_called, True) + def test_create_image(self): body = { 'createImage': { @@ -668,6 +818,23 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) + def test_create_image_snapshots_disabled(self): + """Don't permit a snapshot if the allow_instance_snapshots flag is + False + """ + self.flags(allow_instance_snapshots=False) + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_with_metadata(self): body = { 'createImage': { @@ -684,6 +851,22 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) + def test_create_image_with_too_much_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {}, + }, + } + for num in range(FLAGS.quota_metadata_items + 1): + body['createImage']['metadata']['foo%i' % num] = "bar" + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_no_name(self): body = { 'createImage': {}, @@ -730,10 +913,10 @@ class ServerActionsTestV11(test.TestCase): self.assertTrue(response.headers['Location']) -class TestServerActionXMLDeserializer(test.TestCase): +class TestServerActionXMLDeserializerV11(test.TestCase): def setUp(self): - self.deserializer = create_instance_helper.ServerXMLDeserializer() + self.deserializer = create_instance_helper.ServerXMLDeserializerV11() def tearDown(self): pass @@ -746,7 +929,6 @@ class TestServerActionXMLDeserializer(test.TestCase): expected = { "createImage": { "name": "new-server-test", - "metadata": {}, }, } self.assertEquals(request['body'], expected) @@ -767,3 +949,145 @@ class TestServerActionXMLDeserializer(test.TestCase): }, } self.assertEquals(request['body'], expected) + + def test_change_pass(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1" + adminPass="1234pass"/> """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "changePassword": { + "adminPass": "1234pass", + }, + } + self.assertEquals(request['body'], expected) + + def test_change_pass_no_pass(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1"/> """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_reboot(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <reboot + xmlns="http://docs.openstack.org/compute/api/v1.1" + type="HARD"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "reboot": { + "type": "HARD", + }, + } + self.assertEquals(request['body'], expected) + + def test_reboot_no_type(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <reboot + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <resize + xmlns="http://docs.openstack.org/compute/api/v1.1" + flavorRef="http://localhost/flavors/3"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "resize": {"flavorRef": "http://localhost/flavors/3"}, + } + self.assertEquals(request['body'], expected) + + def test_resize_no_flavor_ref(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <resize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_confirm_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <confirmResize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "confirmResize": None, + } + self.assertEquals(request['body'], expected) + + def test_revert_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <revertResize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "revertResize": None, + } + self.assertEquals(request['body'], expected) + + def test_rebuild(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test" + imageRef="http://localhost/images/1"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> + <personality> + <file path="/etc/banner.txt">Mg==</file> + </personality> + </rebuild>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "name": "new-server-test", + "imageRef": "http://localhost/images/1", + "metadata": { + "My Server Name": "Apache1", + }, + "personality": [ + {"path": "/etc/banner.txt", "contents": "Mg=="}, + ], + }, + } + self.assertDictMatch(request['body'], expected) + + def test_rebuild_minimum(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + imageRef="http://localhost/images/1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + }, + } + self.assertDictMatch(request['body'], expected) + + def test_rebuild_no_imageRef(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> + <personality> + <file path="/etc/banner.txt">Mg==</file> + </personality> + </rebuild>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index 08a6a062a..ec446f0f0 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -29,11 +29,11 @@ import nova.wsgi FLAGS = flags.FLAGS -def return_create_instance_metadata_max(context, server_id, metadata): +def return_create_instance_metadata_max(context, server_id, metadata, delete): return stub_max_server_metadata() -def return_create_instance_metadata(context, server_id, metadata): +def return_create_instance_metadata(context, server_id, metadata, delete): return stub_server_metadata() @@ -202,21 +202,30 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(404, res.status_int) def test_create(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_server_metadata) + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' req.content_type = "application/json" - expected = {"metadata": {"key1": "value1"}} - req.body = json.dumps(expected) + input = {"metadata": {"key9": "value9"}} + req.body = json.dumps(input) res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) - self.assertEqual(expected, res_dict) + input['metadata'].update({ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }) + self.assertEqual(input, res_dict) def test_create_xml(self): - self.stubs.Set(nova.db.api, "instance_metadata_update_or_create", + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_server_metadata) + self.stubs.Set(nova.db.api, "instance_metadata_update", return_create_instance_metadata) req = webob.Request.blank("/v1.1/servers/1/metadata") req.method = "POST" @@ -225,22 +234,29 @@ class ServerMetaDataTest(test.TestCase): request_metadata = minidom.parseString(""" <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="key3">value3</meta> - <meta key="key2">value2</meta> - <meta key="key1">value1</meta> + <meta key="key5">value5</meta> </metadata> """.replace(" ", "").replace("\n", "")) req.body = str(request_metadata.toxml()) response = req.get_response(fakes.wsgi_app()) + expected_metadata = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="key3">value3</meta> + <meta key="key2">value2</meta> + <meta key="key1">value1</meta> + <meta key="key5">value5</meta> + </metadata> + """.replace(" ", "").replace("\n", "")) + self.assertEqual(200, response.status_int) actual_metadata = minidom.parseString(response.body) - self.assertEqual(request_metadata.toxml(), actual_metadata.toxml()) + self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml()) def test_create_empty_body(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' @@ -258,7 +274,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(404, res.status_int) def test_update_all(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'PUT' @@ -276,7 +292,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(expected, res_dict) def test_update_all_empty_container(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'PUT' @@ -289,7 +305,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(expected, res_dict) def test_update_all_malformed_container(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'PUT' @@ -300,7 +316,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_all_malformed_data(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'PUT' @@ -320,7 +336,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(404, res.status_int) def test_update_item(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' @@ -334,7 +350,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(expected, res_dict) def test_update_item_xml(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key9') req.method = 'PUT' @@ -361,7 +377,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(404, res.status_int) def test_update_item_empty_body(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' @@ -370,7 +386,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_too_many_keys(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' @@ -380,7 +396,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/bad') req.method = 'PUT' @@ -390,7 +406,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_too_many_metadata_items_on_create(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) data = {"metadata": {}} for num in range(FLAGS.quota_metadata_items + 1): @@ -404,7 +420,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_to_many_metadata_items_on_update_item(self): - self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata_max) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 0477f6d92..a510d7d97 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -134,8 +134,8 @@ def return_security_group(context, instance_id, security_group_id): pass -def instance_update(context, instance_id, kwargs): - return stub_instance(instance_id) +def instance_update(context, instance_id, values): + return stub_instance(instance_id, name=values.get('display_name')) def instance_addresses(context, instance_id): @@ -145,7 +145,7 @@ def instance_addresses(context, instance_id): def stub_instance(id, user_id='fake', project_id='fake', private_address=None, public_addresses=None, host=None, power_state=0, reservation_id="", uuid=FAKE_UUID, image_ref="10", - flavor_id="1", interfaces=None): + flavor_id="1", interfaces=None, name=None): metadata = [] metadata.append(InstanceMetadata(key='seq', value=id)) @@ -161,7 +161,7 @@ def stub_instance(id, user_id='fake', project_id='fake', private_address=None, host = str(host) # ReservationID isn't sent back, hack it in there. - server_name = "server%s" % id + server_name = name or "server%s" % id if reservation_id != "": server_name = "reservation_%s" % (reservation_id, ) @@ -236,7 +236,8 @@ class ServersTest(test.TestCase): fakes.stub_out_key_pair_funcs(self.stubs) fakes.stub_out_image_service(self.stubs) self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid) - self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) + self.stubs.Set(nova.db.api, 'instance_get_all_by_filters', + return_servers) self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) self.stubs.Set(nova.db, 'instance_get_by_uuid', return_server_by_uuid) @@ -1098,6 +1099,277 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 400) self.assertTrue(res.body.find('marker param') > -1) + def test_get_servers_with_bad_option_v1_0(self): + # 1.0 API ignores unknown options + def fake_get_all(compute_self, context, search_opts=None): + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = webob.Request.blank('/v1.0/servers?unknownoption=whee') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_with_bad_option_v1_1(self): + # 1.1 API also ignores unknown options + def fake_get_all(compute_self, context, search_opts=None): + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = webob.Request.blank('/v1.1/servers?unknownoption=whee') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_allows_image_v1_1(self): + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('image' in search_opts) + self.assertEqual(search_opts['image'], '12345') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = webob.Request.blank('/v1.1/servers?image=12345') + res = req.get_response(fakes.wsgi_app()) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_allows_flavor_v1_1(self): + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('flavor' in search_opts) + # flavor is an integer ID + self.assertEqual(search_opts['flavor'], '12345') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = webob.Request.blank('/v1.1/servers?flavor=12345') + res = req.get_response(fakes.wsgi_app()) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_allows_status_v1_1(self): + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('state' in search_opts) + self.assertEqual(set(search_opts['state']), + set([power_state.RUNNING, power_state.BLOCKED])) + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = webob.Request.blank('/v1.1/servers?status=active') + res = req.get_response(fakes.wsgi_app()) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_invalid_status_v1_1(self): + """Test getting servers by invalid status""" + + self.flags(allow_admin_api=False) + + req = webob.Request.blank('/v1.1/servers?status=running') + res = req.get_response(fakes.wsgi_app()) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 400) + self.assertTrue(res.body.find('Invalid server status') > -1) + + def test_get_servers_allows_name_v1_1(self): + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('name' in search_opts) + self.assertEqual(search_opts['name'], 'whee.*') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = webob.Request.blank('/v1.1/servers?name=whee.*') + res = req.get_response(fakes.wsgi_app()) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_unknown_or_admin_options1(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is off. Make sure the admin and + unknown options are stripped before they get to + compute_api.get_all() + """ + + self.flags(allow_admin_api=False) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertFalse('ip' in search_opts) + self.assertFalse('unknown_option' in search_opts) + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = webob.Request.blank('/v1.1/servers?%s' % query_str) + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_unknown_or_admin_options2(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is on, but context is a user. + Make sure the admin and unknown options are stripped before + they get to compute_api.get_all() + """ + + self.flags(allow_admin_api=True) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertFalse('ip' in search_opts) + self.assertFalse('unknown_option' in search_opts) + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = webob.Request.blank('/v1.1/servers?%s' % query_str) + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=False) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_unknown_or_admin_options3(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is on and context is admin. + All options should be passed through to compute_api.get_all() + """ + + self.flags(allow_admin_api=True) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertTrue('ip' in search_opts) + self.assertTrue('unknown_option' in search_opts) + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = webob.Request.blank('/v1.1/servers?%s' % query_str) + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_admin_allows_ip_v1_1(self): + """Test getting servers by ip with admin_api enabled and + admin context + """ + self.flags(allow_admin_api=True) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('ip' in search_opts) + self.assertEqual(search_opts['ip'], '10\..*') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = webob.Request.blank('/v1.1/servers?ip=10\..*') + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + + def test_get_servers_admin_allows_ip6_v1_1(self): + """Test getting servers by ip6 with admin_api enabled and + admin context + """ + self.flags(allow_admin_api=True) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('ip6' in search_opts) + self.assertEqual(search_opts['ip6'], 'ffff.*') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = webob.Request.blank('/v1.1/servers?ip6=ffff.*') + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + res = req.get_response(fakes.wsgi_app(fake_auth_context=context)) + # The following assert will fail if either of the asserts in + # fake_get_all() fail + self.assertEqual(res.status_int, 200) + servers = json.loads(res.body)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], 100) + def _setup_for_create_instance(self): """Shared implementation for tests below that create instance""" def instance_create(context, inst): @@ -1159,7 +1431,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) + self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) @@ -1356,7 +1628,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) + self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] self.assertEqual(16, len(server['adminPass'])) self.assertEqual(1, server['id']) @@ -1381,6 +1653,22 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_v1_1_invalid_flavor_id_int(self): + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/images/2' + flavor_ref = -1 + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_create_instance_v1_1_bad_flavor_href(self): self._setup_for_create_instance() @@ -1451,7 +1739,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) + self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] self.assertEqual(expected_flavor, server['flavor']) self.assertEqual(expected_image, server['image']) @@ -1496,7 +1784,7 @@ class ServersTest(test.TestCase): req.body = json.dumps(body) req.headers['content-type'] = "application/json" res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) + self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] self.assertEqual(server['adminPass'], body['server']['adminPass']) @@ -1592,13 +1880,17 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 400) def test_update_server_name_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(name='server_test')) req = webob.Request.blank('/v1.1/servers/1') req.method = 'PUT' req.content_type = 'application/json' - req.body = json.dumps({'server': {'name': 'new-name'}}) + req.body = json.dumps({'server': {'name': 'server_test'}}) res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 204) - self.assertEqual(res.body, '') + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server_test') def test_update_server_adminPass_ignored_v1_1(self): inst_dict = dict(name='server_test', adminPass='bacon') @@ -1609,16 +1901,19 @@ class ServersTest(test.TestCase): self.assertEqual(params, filtered_dict) return filtered_dict - self.stubs.Set(nova.db.api, 'instance_update', - server_update) + self.stubs.Set(nova.db.api, 'instance_update', server_update) + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(name='server_test')) req = webob.Request.blank('/v1.1/servers/1') req.method = 'PUT' req.content_type = "application/json" req.body = self.body res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 204) - self.assertEqual(res.body, '') + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server_test') def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedule') @@ -1665,6 +1960,7 @@ class ServersTest(test.TestCase): def test_get_all_server_details_v1_0(self): req = webob.Request.blank('/v1.0/servers/detail') res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) for i, s in enumerate(res_dict['servers']): @@ -1720,7 +2016,7 @@ class ServersTest(test.TestCase): return [stub_instance(i, 'fake', 'fake', None, None, i % 2) for i in xrange(5)] - self.stubs.Set(nova.db.api, 'instance_get_all_by_project', + self.stubs.Set(nova.db.api, 'instance_get_all_by_filters', return_servers_with_host) req = webob.Request.blank('/v1.0/servers/detail') @@ -2177,7 +2473,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): def setUp(self): super(TestServerCreateRequestXMLDeserializerV11, self).setUp() - self.deserializer = create_instance_helper.ServerXMLDeserializer() + self.deserializer = create_instance_helper.ServerXMLDeserializerV11() def test_minimal_request(self): serial_request = """ @@ -2191,8 +2487,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2211,8 +2505,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "adminPass": "1234", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2229,8 +2521,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "http://localhost:8774/v1.1/images/2", "flavorRef": "3", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2247,8 +2537,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "http://localhost:8774/v1.1/flavors/3", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2292,7 +2580,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "metadata": {"one": "two", "open": "snack"}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2314,7 +2601,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "metadata": {}, "personality": [ {"path": "/etc/banner.txt", "contents": "MQ=="}, {"path": "/etc/hosts", "contents": "Mg=="}, @@ -2523,13 +2809,13 @@ class TestServerInstanceCreation(test.TestCase): def test_create_instance_with_no_personality(self): request, response, injected_files = \ self._create_instance_with_personality_json(personality=None) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, []) def test_create_instance_with_no_personality_xml(self): request, response, injected_files = \ self._create_instance_with_personality_xml(personality=None) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, []) def test_create_instance_with_personality(self): @@ -2539,7 +2825,7 @@ class TestServerInstanceCreation(test.TestCase): personality = [(path, b64contents)] request, response, injected_files = \ self._create_instance_with_personality_json(personality) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, [(path, contents)]) def test_create_instance_with_personality_xml(self): @@ -2549,7 +2835,7 @@ class TestServerInstanceCreation(test.TestCase): personality = [(path, b64contents)] request, response, injected_files = \ self._create_instance_with_personality_xml(personality) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, [(path, contents)]) def test_create_instance_with_personality_no_path(self): @@ -2612,7 +2898,7 @@ class TestServerInstanceCreation(test.TestCase): request = self._get_create_request_json(body_dict) compute_api, response = \ self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) def test_create_instance_with_three_personalities(self): files = [ @@ -2625,7 +2911,7 @@ class TestServerInstanceCreation(test.TestCase): personality.append((path, base64.b64encode(content))) request, response, injected_files = \ self._create_instance_with_personality_json(personality) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, files) def test_create_instance_personality_empty_content(self): @@ -2634,13 +2920,13 @@ class TestServerInstanceCreation(test.TestCase): personality = [(path, contents)] request, response, injected_files = \ self._create_instance_with_personality_json(personality) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) self.assertEquals(injected_files, [(path, contents)]) def test_create_instance_admin_pass_json(self): request, response, dummy = \ self._create_instance_with_personality_json(None) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) response = json.loads(response.body) self.assertTrue('adminPass' in response['server']) self.assertEqual(16, len(response['server']['adminPass'])) @@ -2648,7 +2934,7 @@ class TestServerInstanceCreation(test.TestCase): def test_create_instance_admin_pass_xml(self): request, response, dummy = \ self._create_instance_with_personality_xml(None) - self.assertEquals(response.status_int, 200) + self.assertEquals(response.status_int, 202) dom = minidom.parseString(response.body) server = dom.childNodes[0] self.assertEquals(server.nodeName, 'server') @@ -2770,8 +3056,7 @@ class ServersViewBuilderV11Test(test.TestCase): address_builder, flavor_builder, image_builder, - base_url, - ) + base_url) return view_builder def test_build_server(self): diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py index be59970c9..84ab641ea 100644 --- a/nova/tests/fake_utils.py +++ b/nova/tests/fake_utils.py @@ -64,8 +64,10 @@ def fake_execute(*cmd_parts, **kwargs): global _fake_execute_repliers process_input = kwargs.get('process_input', None) - addl_env = kwargs.get('addl_env', None) check_exit_code = kwargs.get('check_exit_code', 0) + delay_on_retry = kwargs.get('delay_on_retry', True) + attempts = kwargs.get('attempts', 1) + run_as_root = kwargs.get('run_as_root', False) cmd_str = ' '.join(str(part) for part in cmd_parts) LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str) @@ -87,7 +89,9 @@ def fake_execute(*cmd_parts, **kwargs): # Alternative is a function, so call it reply = reply_handler(cmd_parts, process_input=process_input, - addl_env=addl_env, + delay_on_retry=delay_on_retry, + attempts=attempts, + run_as_root=run_as_root, check_exit_code=check_exit_code) except exception.ProcessExecutionError as e: LOG.debug(_('Faked command raised an exception %s' % str(e))) diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index d51b19ccd..f2a19f22d 100644 --- a/nova/tests/glance/stubs.py +++ b/nova/tests/glance/stubs.py @@ -32,6 +32,7 @@ class FakeGlance(object): IMAGE_RAMDISK = 3 IMAGE_RAW = 4 IMAGE_VHD = 5 + IMAGE_ISO = 6 IMAGE_FIXTURES = { IMAGE_MACHINE: { @@ -58,6 +59,11 @@ class FakeGlance(object): 'image_meta': {'name': 'fakevhd', 'size': 0, 'disk_format': 'vhd', 'container_format': 'ovf'}, + 'image_data': StringIO.StringIO('')}, + IMAGE_ISO: { + 'image_meta': {'name': 'fakeiso', 'size': 0, + 'disk_format': 'iso', + 'container_format': 'bare'}, 'image_data': StringIO.StringIO('')}} def __init__(self, host, port=None, use_ssl=False, auth_tok=None): diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index 5a40f578f..0ff508ffa 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -235,3 +235,39 @@ class TestMutatorDateTimeTests(BaseGlanceTest): 'updated_at': None, 'deleted_at': None} return fixture + + +class TestGlanceSerializer(unittest.TestCase): + def test_serialize(self): + metadata = {'name': 'image1', + 'is_public': True, + 'foo': 'bar', + 'properties': { + 'prop1': 'propvalue1', + 'mappings': [ + {'virtual': 'aaa', + 'device': 'bbb'}, + {'virtual': 'xxx', + 'device': 'yyy'}], + 'block_device_mapping': [ + {'virtual_device': 'fake', + 'device_name': '/dev/fake'}, + {'virtual_device': 'ephemeral0', + 'device_name': '/dev/fake0'}]}} + + converted_expected = { + 'name': 'image1', + 'is_public': True, + 'foo': 'bar', + 'properties': { + 'prop1': 'propvalue1', + 'mappings': + '[{"device": "bbb", "virtual": "aaa"}, ' + '{"device": "yyy", "virtual": "xxx"}]', + 'block_device_mapping': + '[{"virtual_device": "fake", "device_name": "/dev/fake"}, ' + '{"virtual_device": "ephemeral0", ' + '"device_name": "/dev/fake0"}]'}} + converted = glance._convert_to_string(metadata) + self.assertEqual(converted, converted_expected) + self.assertEqual(glance._convert_from_string(converted), metadata) diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index f60eb6433..33461025f 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -21,9 +21,11 @@ Tests For Scheduler import datetime import mox -import novaclient.exceptions import stubout +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions + from mox import IgnoreArg from nova import context from nova import db @@ -34,8 +36,9 @@ from nova import test from nova import rpc from nova import utils from nova.scheduler import api -from nova.scheduler import manager from nova.scheduler import driver +from nova.scheduler import manager +from nova.scheduler import multi from nova.compute import power_state @@ -301,7 +304,7 @@ class SimpleDriverTestCase(test.TestCase): db.compute_node_create(self.context, dic) return db.service_get(self.context, s_ref['id']) - def test_doesnt_report_disabled_hosts_as_up(self): + def test_doesnt_report_disabled_hosts_as_up_no_queue(self): """Ensures driver doesn't find hosts before they are enabled""" # NOTE(vish): constructing service without create method # because we are going to use it without queue @@ -324,7 +327,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_reports_enabled_hosts_as_up(self): + def test_reports_enabled_hosts_as_up_no_queue(self): """Ensures driver can find the hosts that are up""" # NOTE(vish): constructing service without create method # because we are going to use it without queue @@ -343,7 +346,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_least_busy_host_gets_instance(self): + def test_least_busy_host_gets_instance_no_queue(self): """Ensures the host with less cores gets the next one""" compute1 = service.Service('host1', 'nova-compute', @@ -366,7 +369,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_specific_host_gets_instance(self): + def test_specific_host_gets_instance_no_queue(self): """Ensures if you set availability_zone it launches on that zone""" compute1 = service.Service('host1', 'nova-compute', @@ -389,7 +392,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_wont_sechedule_if_specified_host_is_down(self): + def test_wont_schedule_if_specified_host_is_down_no_queue(self): compute1 = service.Service('host1', 'nova-compute', 'compute', @@ -408,7 +411,7 @@ class SimpleDriverTestCase(test.TestCase): db.instance_destroy(self.context, instance_id2) compute1.kill() - def test_will_schedule_on_disabled_host_if_specified(self): + def test_will_schedule_on_disabled_host_if_specified_no_queue(self): compute1 = service.Service('host1', 'nova-compute', 'compute', @@ -423,7 +426,7 @@ class SimpleDriverTestCase(test.TestCase): db.instance_destroy(self.context, instance_id2) compute1.kill() - def test_too_many_cores(self): + def test_too_many_cores_no_queue(self): """Ensures we don't go over max cores""" compute1 = service.Service('host1', 'nova-compute', @@ -456,7 +459,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_least_busy_host_gets_volume(self): + def test_least_busy_host_gets_volume_no_queue(self): """Ensures the host with less gigabytes gets the next one""" volume1 = service.Service('host1', 'nova-volume', @@ -477,7 +480,7 @@ class SimpleDriverTestCase(test.TestCase): volume1.delete_volume(self.context, volume_id1) db.volume_destroy(self.context, volume_id2) - def test_doesnt_report_disabled_hosts_as_up(self): + def test_doesnt_report_disabled_hosts_as_up2(self): """Ensures driver doesn't find hosts before they are enabled""" compute1 = self.start_service('compute', host='host1') compute2 = self.start_service('compute', host='host2') @@ -641,10 +644,13 @@ class SimpleDriverTestCase(test.TestCase): self.mox.StubOutWithMock(driver_i, '_live_migration_dest_check') self.mox.StubOutWithMock(driver_i, '_live_migration_common_check') driver_i._live_migration_src_check(nocare, nocare) - driver_i._live_migration_dest_check(nocare, nocare, i_ref['host']) - driver_i._live_migration_common_check(nocare, nocare, i_ref['host']) + driver_i._live_migration_dest_check(nocare, nocare, + i_ref['host'], False) + driver_i._live_migration_common_check(nocare, nocare, + i_ref['host'], False) self.mox.StubOutWithMock(rpc, 'cast', use_mock_anything=True) - kwargs = {'instance_id': instance_id, 'dest': i_ref['host']} + kwargs = {'instance_id': instance_id, 'dest': i_ref['host'], + 'block_migration': False} rpc.cast(self.context, db.queue_get_for(nocare, FLAGS.compute_topic, i_ref['host']), {"method": 'live_migration', "args": kwargs}) @@ -652,7 +658,8 @@ class SimpleDriverTestCase(test.TestCase): self.mox.ReplayAll() self.scheduler.live_migration(self.context, FLAGS.compute_topic, instance_id=instance_id, - dest=i_ref['host']) + dest=i_ref['host'], + block_migration=False) i_ref = db.instance_get(self.context, instance_id) self.assertTrue(i_ref['state_description'] == 'migrating') @@ -733,7 +740,7 @@ class SimpleDriverTestCase(test.TestCase): self.assertRaises(exception.ComputeServiceUnavailable, self.scheduler.driver._live_migration_dest_check, - self.context, i_ref, i_ref['host']) + self.context, i_ref, i_ref['host'], False) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -746,7 +753,7 @@ class SimpleDriverTestCase(test.TestCase): self.assertRaises(exception.UnableToMigrateToSelf, self.scheduler.driver._live_migration_dest_check, - self.context, i_ref, i_ref['host']) + self.context, i_ref, i_ref['host'], False) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -754,15 +761,33 @@ class SimpleDriverTestCase(test.TestCase): def test_live_migration_dest_check_service_lack_memory(self): """Confirms exception raises when dest doesn't have enough memory.""" instance_id = self._create_instance() + instance_id2 = self._create_instance(host='somewhere', + memory_mb=12) i_ref = db.instance_get(self.context, instance_id) - s_ref = self._create_compute_service(host='somewhere', - memory_mb_used=12) + s_ref = self._create_compute_service(host='somewhere') self.assertRaises(exception.MigrationError, self.scheduler.driver._live_migration_dest_check, - self.context, i_ref, 'somewhere') + self.context, i_ref, 'somewhere', False) db.instance_destroy(self.context, instance_id) + db.instance_destroy(self.context, instance_id2) + db.service_destroy(self.context, s_ref['id']) + + def test_block_migration_dest_check_service_lack_disk(self): + """Confirms exception raises when dest doesn't have enough disk.""" + instance_id = self._create_instance() + instance_id2 = self._create_instance(host='somewhere', + local_gb=70) + i_ref = db.instance_get(self.context, instance_id) + s_ref = self._create_compute_service(host='somewhere') + + self.assertRaises(exception.MigrationError, + self.scheduler.driver._live_migration_dest_check, + self.context, i_ref, 'somewhere', True) + + db.instance_destroy(self.context, instance_id) + db.instance_destroy(self.context, instance_id2) db.service_destroy(self.context, s_ref['id']) def test_live_migration_dest_check_service_works_correctly(self): @@ -774,7 +799,8 @@ class SimpleDriverTestCase(test.TestCase): ret = self.scheduler.driver._live_migration_dest_check(self.context, i_ref, - 'somewhere') + 'somewhere', + False) self.assertTrue(ret is None) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -807,9 +833,10 @@ class SimpleDriverTestCase(test.TestCase): "args": {'filename': fpath}}) self.mox.ReplayAll() - self.assertRaises(exception.SourceHostUnavailable, + #self.assertRaises(exception.SourceHostUnavailable, + self.assertRaises(exception.FileNotFound, self.scheduler.driver._live_migration_common_check, - self.context, i_ref, dest) + self.context, i_ref, dest, False) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -833,7 +860,7 @@ class SimpleDriverTestCase(test.TestCase): self.mox.ReplayAll() self.assertRaises(exception.InvalidHypervisorType, self.scheduler.driver._live_migration_common_check, - self.context, i_ref, dest) + self.context, i_ref, dest, False) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -859,7 +886,7 @@ class SimpleDriverTestCase(test.TestCase): self.mox.ReplayAll() self.assertRaises(exception.DestinationHypervisorTooOld, self.scheduler.driver._live_migration_common_check, - self.context, i_ref, dest) + self.context, i_ref, dest, False) db.instance_destroy(self.context, instance_id) db.service_destroy(self.context, s_ref['id']) @@ -891,7 +918,8 @@ class SimpleDriverTestCase(test.TestCase): try: self.scheduler.driver._live_migration_common_check(self.context, i_ref, - dest) + dest, + False) except rpc.RemoteError, e: c = (e.message.find(_("doesn't have compatibility to")) >= 0) @@ -901,6 +929,25 @@ class SimpleDriverTestCase(test.TestCase): db.service_destroy(self.context, s_ref2['id']) +class MultiDriverTestCase(SimpleDriverTestCase): + """Test case for multi driver.""" + + def setUp(self): + super(MultiDriverTestCase, self).setUp() + self.flags(connection_type='fake', + stub_network=True, + max_cores=4, + max_gigabytes=4, + network_manager='nova.network.manager.FlatManager', + volume_driver='nova.volume.driver.FakeISCSIDriver', + compute_scheduler_driver=('nova.scheduler.simple' + '.SimpleScheduler'), + volume_scheduler_driver=('nova.scheduler.simple' + '.SimpleScheduler'), + scheduler_driver='nova.scheduler.multi.MultiScheduler') + self.scheduler = manager.SchedulerManager() + + class FakeZone(object): def __init__(self, id, api_url, username, password): self.id = id @@ -990,7 +1037,7 @@ class ZoneRedirectTest(test.TestCase): decorator = FakeRerouteCompute("foo", id_to_return=FAKE_UUID_NOT_FOUND) try: result = decorator(go_boom)(None, None, 1) - self.assertFail(_("Should have rerouted.")) + self.fail(_("Should have rerouted.")) except api.RedirectResult, e: self.assertEquals(e.results['magic'], 'found me') @@ -1036,10 +1083,10 @@ class FakeServerCollection(object): class FakeEmptyServerCollection(object): def get(self, f): - raise novaclient.NotFound(1) + raise novaclient_exceptions.NotFound(1) def find(self, name): - raise novaclient.NotFound(2) + raise novaclient_exceptions.NotFound(2) class FakeNovaClient(object): @@ -1078,14 +1125,14 @@ class DynamicNovaClientTest(test.TestCase): class FakeZonesProxy(object): - def do_something(*args, **kwargs): + def do_something(self, *args, **kwargs): return 42 - def raises_exception(*args, **kwargs): + def raises_exception(self, *args, **kwargs): raise Exception('testing') -class FakeNovaClientOpenStack(object): +class FakeNovaClientZones(object): def __init__(self, *args, **kwargs): self.zones = FakeZonesProxy() @@ -1098,7 +1145,7 @@ class CallZoneMethodTest(test.TestCase): super(CallZoneMethodTest, self).setUp() self.stubs = stubout.StubOutForTesting() self.stubs.Set(db, 'zone_get_all', zone_get_all) - self.stubs.Set(novaclient, 'OpenStack', FakeNovaClientOpenStack) + self.stubs.Set(novaclient, 'Client', FakeNovaClientZones) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py index 7833028c3..788efca52 100644 --- a/nova/tests/scheduler/test_zone_aware_scheduler.py +++ b/nova/tests/scheduler/test_zone_aware_scheduler.py @@ -21,7 +21,9 @@ import json import nova.db from nova import exception +from nova import rpc from nova import test +from nova.compute import api as compute_api from nova.scheduler import driver from nova.scheduler import zone_aware_scheduler from nova.scheduler import zone_manager @@ -114,7 +116,7 @@ def fake_provision_resource_from_blob(context, item, instance_id, def fake_decrypt_blob_returns_local_info(blob): - return {'foo': True} # values aren't important. + return {'hostname': 'foooooo'} # values aren't important. def fake_decrypt_blob_returns_child_info(blob): @@ -283,14 +285,29 @@ class ZoneAwareSchedulerTestCase(test.TestCase): global was_called sched = FakeZoneAwareScheduler() was_called = False + + def fake_create_db_entry_for_new_instance(self, context, + image, base_options, security_group, + block_device_mapping, num=1): + global was_called + was_called = True + # return fake instances + return {'id': 1, 'uuid': 'f874093c-7b17-49c0-89c3-22a5348497f9'} + + def fake_rpc_cast(*args, **kwargs): + pass + self.stubs.Set(sched, '_decrypt_blob', fake_decrypt_blob_returns_local_info) - self.stubs.Set(sched, '_provision_resource_locally', - fake_provision_resource_locally) + self.stubs.Set(compute_api.API, + 'create_db_entry_for_new_instance', + fake_create_db_entry_for_new_instance) + self.stubs.Set(rpc, 'cast', fake_rpc_cast) - request_spec = {'blob': "Non-None blob data"} + build_plan_item = {'blob': "Non-None blob data"} + request_spec = {'image': {}, 'instance_properties': {}} - sched._provision_resource_from_blob(None, request_spec, 1, + sched._provision_resource_from_blob(None, build_plan_item, 1, request_spec, {}) self.assertTrue(was_called) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index d9b1d39c9..2011ae756 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -27,6 +27,7 @@ import random import StringIO import webob +from nova import block_device from nova import context from nova import exception from nova import test @@ -147,10 +148,12 @@ class Ec2utilsTestCase(test.TestCase): properties0 = {'mappings': mappings} properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings} - root_device_name = ec2utils.properties_root_device_name(properties0) + root_device_name = block_device.properties_root_device_name( + properties0) self.assertEqual(root_device_name, '/dev/sda1') - root_device_name = ec2utils.properties_root_device_name(properties1) + root_device_name = block_device.properties_root_device_name( + properties1) self.assertEqual(root_device_name, '/dev/sdb') def test_mapping_prepend_dev(self): @@ -184,7 +187,7 @@ class Ec2utilsTestCase(test.TestCase): 'device': '/dev/sdc1'}, {'virtual': 'ephemeral1', 'device': '/dev/sdc1'}] - self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings), + self.assertDictListMatch(block_device.mappings_prepend_dev(mappings), expected_result) @@ -336,6 +339,33 @@ class ApiEc2TestCase(test.TestCase): self.ec2.delete_security_group(security_group_name) + def test_group_name_valid_chars_security_group(self): + """ Test that we sanely handle invalid security group names. + API Spec states we should only accept alphanumeric characters, + spaces, dashes, and underscores. """ + self.expect_http() + self.mox.ReplayAll() + + # Test block group_name of non alphanumeric characters, spaces, + # dashes, and underscores. + security_group_name = "aa #^% -=99" + + self.assertRaises(EC2ResponseError, self.ec2.create_security_group, + security_group_name, 'test group') + + def test_group_name_valid_length_security_group(self): + """Test that we sanely handle invalid security group names. + API Spec states that the length should not exceed 255 chars """ + self.expect_http() + self.mox.ReplayAll() + + # Test block group_name > 255 chars + security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") + for x in range(random.randint(256, 266))) + + self.assertRaises(EC2ResponseError, self.ec2.create_security_group, + security_group_name, 'test group') + def test_authorize_revoke_security_group_cidr(self): """ Test that we can add and remove CIDR based rules diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py index 2e24b7d6e..4561eb7f2 100644 --- a/nova/tests/test_auth.py +++ b/nova/tests/test_auth.py @@ -62,7 +62,12 @@ class project_generator(object): class user_and_project_generator(object): - def __init__(self, manager, user_state={}, project_state={}): + def __init__(self, manager, user_state=None, project_state=None): + if not user_state: + user_state = {} + if not project_state: + project_state = {} + self.manager = manager if 'name' not in user_state: user_state['name'] = 'test1' diff --git a/nova/tests/test_block_device.py b/nova/tests/test_block_device.py new file mode 100644 index 000000000..b8e9b35e2 --- /dev/null +++ b/nova/tests/test_block_device.py @@ -0,0 +1,87 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Tests for Block Device utility functions. +""" + +from nova import block_device +from nova import test + + +class BlockDeviceTestCase(test.TestCase): + def test_properties(self): + root_device0 = '/dev/sda' + root_device1 = '/dev/sdb' + mappings = [{'virtual': 'root', + 'device': root_device0}] + + properties0 = {'mappings': mappings} + properties1 = {'mappings': mappings, + 'root_device_name': root_device1} + + self.assertEqual(block_device.properties_root_device_name({}), None) + self.assertEqual( + block_device.properties_root_device_name(properties0), + root_device0) + self.assertEqual( + block_device.properties_root_device_name(properties1), + root_device1) + + def test_ephemeral(self): + self.assertFalse(block_device.is_ephemeral('ephemeral')) + self.assertTrue(block_device.is_ephemeral('ephemeral0')) + self.assertTrue(block_device.is_ephemeral('ephemeral1')) + self.assertTrue(block_device.is_ephemeral('ephemeral11')) + self.assertFalse(block_device.is_ephemeral('root')) + self.assertFalse(block_device.is_ephemeral('swap')) + self.assertFalse(block_device.is_ephemeral('/dev/sda1')) + + self.assertEqual(block_device.ephemeral_num('ephemeral0'), 0) + self.assertEqual(block_device.ephemeral_num('ephemeral1'), 1) + self.assertEqual(block_device.ephemeral_num('ephemeral11'), 11) + + self.assertFalse(block_device.is_swap_or_ephemeral('ephemeral')) + self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral0')) + self.assertTrue(block_device.is_swap_or_ephemeral('ephemeral1')) + self.assertTrue(block_device.is_swap_or_ephemeral('swap')) + self.assertFalse(block_device.is_swap_or_ephemeral('root')) + self.assertFalse(block_device.is_swap_or_ephemeral('/dev/sda1')) + + def test_mappings_prepend_dev(self): + mapping = [ + {'virtual': 'ami', 'device': '/dev/sda'}, + {'virtual': 'root', 'device': 'sda'}, + {'virtual': 'ephemeral0', 'device': 'sdb'}, + {'virtual': 'swap', 'device': 'sdc'}, + {'virtual': 'ephemeral1', 'device': 'sdd'}, + {'virtual': 'ephemeral2', 'device': 'sde'}] + + expected = [ + {'virtual': 'ami', 'device': '/dev/sda'}, + {'virtual': 'root', 'device': 'sda'}, + {'virtual': 'ephemeral0', 'device': '/dev/sdb'}, + {'virtual': 'swap', 'device': '/dev/sdc'}, + {'virtual': 'ephemeral1', 'device': '/dev/sdd'}, + {'virtual': 'ephemeral2', 'device': '/dev/sde'}] + + prepended = block_device.mappings_prepend_dev(mapping) + self.assertEqual(prepended.sort(), expected.sort()) + + def test_strip_dev(self): + self.assertEqual(block_device.strip_dev('/dev/sda'), 'sda') + self.assertEqual(block_device.strip_dev('sda'), 'sda') diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index e891fa197..b2afc53c9 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -17,6 +17,8 @@ # under the License. import mox +import functools + from base64 import b64decode from M2Crypto import BIO from M2Crypto import RSA @@ -892,13 +894,16 @@ class CloudTestCase(test.TestCase): def test_modify_image_attribute(self): modify_image_attribute = self.cloud.modify_image_attribute + fake_metadata = {'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + 'type': 'machine'}, 'is_public': False} + def fake_show(meh, context, id): - return {'id': 1, 'container_format': 'ami', - 'properties': {'kernel_id': 1, 'ramdisk_id': 1, - 'type': 'machine'}, 'is_public': False} + return fake_metadata def fake_update(meh, context, image_id, metadata, data=None): - return metadata + fake_metadata.update(metadata) + return fake_metadata self.stubs.Set(fake._FakeImageService, 'show', fake_show) self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show) @@ -1464,3 +1469,147 @@ class CloudTestCase(test.TestCase): # TODO(yamahata): clean up snapshot created by CreateImage. self._restart_compute_service() + + @staticmethod + def _fake_bdm_get(ctxt, id): + return [{'volume_id': 87654321, + 'snapshot_id': None, + 'no_device': None, + 'virtual_name': None, + 'delete_on_termination': True, + 'device_name': '/dev/sdh'}, + {'volume_id': None, + 'snapshot_id': 98765432, + 'no_device': None, + 'virtual_name': None, + 'delete_on_termination': True, + 'device_name': '/dev/sdi'}, + {'volume_id': None, + 'snapshot_id': None, + 'no_device': True, + 'virtual_name': None, + 'delete_on_termination': None, + 'device_name': None}, + {'volume_id': None, + 'snapshot_id': None, + 'no_device': None, + 'virtual_name': 'ephemeral0', + 'delete_on_termination': None, + 'device_name': '/dev/sdb'}, + {'volume_id': None, + 'snapshot_id': None, + 'no_device': None, + 'virtual_name': 'swap', + 'delete_on_termination': None, + 'device_name': '/dev/sdc'}, + {'volume_id': None, + 'snapshot_id': None, + 'no_device': None, + 'virtual_name': 'ephemeral1', + 'delete_on_termination': None, + 'device_name': '/dev/sdd'}, + {'volume_id': None, + 'snapshot_id': None, + 'no_device': None, + 'virtual_name': 'ephemeral2', + 'delete_on_termination': None, + 'device_name': '/dev/sd3'}, + ] + + def test_get_instance_mapping(self): + """Make sure that _get_instance_mapping works""" + ctxt = None + instance_ref0 = {'id': 0, + 'root_device_name': None} + instance_ref1 = {'id': 0, + 'root_device_name': '/dev/sda1'} + + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + self._fake_bdm_get) + + expected = {'ami': 'sda1', + 'root': '/dev/sda1', + 'ephemeral0': '/dev/sdb', + 'swap': '/dev/sdc', + 'ephemeral1': '/dev/sdd', + 'ephemeral2': '/dev/sd3'} + + self.assertEqual(self.cloud._format_instance_mapping(ctxt, + instance_ref0), + cloud._DEFAULT_MAPPINGS) + self.assertEqual(self.cloud._format_instance_mapping(ctxt, + instance_ref1), + expected) + + def test_describe_instance_attribute(self): + """Make sure that describe_instance_attribute works""" + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + self._fake_bdm_get) + + def fake_get(ctxt, instance_id): + return { + 'id': 0, + 'root_device_name': '/dev/sdh', + 'security_groups': [{'name': 'fake0'}, {'name': 'fake1'}], + 'state_description': 'stopping', + 'instance_type': {'name': 'fake_type'}, + 'kernel_id': 1, + 'ramdisk_id': 2, + 'user_data': 'fake-user data', + } + self.stubs.Set(self.cloud.compute_api, 'get', fake_get) + + def fake_volume_get(ctxt, volume_id, session=None): + if volume_id == 87654321: + return {'id': volume_id, + 'attach_time': '13:56:24', + 'status': 'in-use'} + raise exception.VolumeNotFound(volume_id=volume_id) + self.stubs.Set(db.api, 'volume_get', fake_volume_get) + + get_attribute = functools.partial( + self.cloud.describe_instance_attribute, + self.context, 'i-12345678') + + bdm = get_attribute('blockDeviceMapping') + bdm['blockDeviceMapping'].sort() + + expected_bdm = {'instance_id': 'i-12345678', + 'rootDeviceType': 'ebs', + 'blockDeviceMapping': [ + {'deviceName': '/dev/sdh', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': True, + 'volumeId': 87654321, + 'attachTime': '13:56:24'}}]} + expected_bdm['blockDeviceMapping'].sort() + self.assertEqual(bdm, expected_bdm) + # NOTE(yamahata): this isn't supported + # get_attribute('disableApiTermination') + groupSet = get_attribute('groupSet') + groupSet['groupSet'].sort() + expected_groupSet = {'instance_id': 'i-12345678', + 'groupSet': [{'groupId': 'fake0'}, + {'groupId': 'fake1'}]} + expected_groupSet['groupSet'].sort() + self.assertEqual(groupSet, expected_groupSet) + self.assertEqual(get_attribute('instanceInitiatedShutdownBehavior'), + {'instance_id': 'i-12345678', + 'instanceInitiatedShutdownBehavior': 'stop'}) + self.assertEqual(get_attribute('instanceType'), + {'instance_id': 'i-12345678', + 'instanceType': 'fake_type'}) + self.assertEqual(get_attribute('kernel'), + {'instance_id': 'i-12345678', + 'kernel': 'aki-00000001'}) + self.assertEqual(get_attribute('ramdisk'), + {'instance_id': 'i-12345678', + 'ramdisk': 'ari-00000002'}) + self.assertEqual(get_attribute('rootDeviceName'), + {'instance_id': 'i-12345678', + 'rootDeviceName': '/dev/sdh'}) + # NOTE(yamahata): this isn't supported + # get_attribute('sourceDestCheck') + self.assertEqual(get_attribute('userData'), + {'instance_id': 'i-12345678', + 'userData': '}\xa9\x1e\xba\xc7\xabu\xabZ'}) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index bbf9ddcc6..e2fa3b140 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -26,6 +26,7 @@ from nova.compute import power_state from nova import context from nova import db from nova.db.sqlalchemy import models +from nova.db.sqlalchemy import api as sqlalchemy_api from nova import exception from nova import flags import nova.image.fake @@ -73,8 +74,11 @@ class ComputeTestCase(test.TestCase): self.stubs.Set(nova.image.fake._FakeImageService, 'show', fake_show) - def _create_instance(self, params={}): + def _create_instance(self, params=None): """Create a test instance""" + if not params: + params = {} + inst = {} inst['image_ref'] = 1 inst['reservation_id'] = 'r-fakeres' @@ -87,8 +91,11 @@ class ComputeTestCase(test.TestCase): inst.update(params) return db.instance_create(self.context, inst)['id'] - def _create_instance_type(self, params={}): + def _create_instance_type(self, params=None): """Create a test instance""" + if not params: + params = {} + context = self.context.elevated() inst = {} inst['name'] = 'm1.small' @@ -625,7 +632,7 @@ class ComputeTestCase(test.TestCase): vid = i_ref['volumes'][i]['id'] volmock.setup_compute_volume(c, vid).InAnyOrder('g1') drivermock.plug_vifs(i_ref, []) - drivermock.ensure_filtering_rules_for_instance(i_ref) + drivermock.ensure_filtering_rules_for_instance(i_ref, []) self.compute.db = dbmock self.compute.volume_manager = volmock @@ -650,7 +657,7 @@ class ComputeTestCase(test.TestCase): self.mox.StubOutWithMock(compute_manager.LOG, 'info') compute_manager.LOG.info(_("%s has no volume."), i_ref['hostname']) drivermock.plug_vifs(i_ref, []) - drivermock.ensure_filtering_rules_for_instance(i_ref) + drivermock.ensure_filtering_rules_for_instance(i_ref, []) self.compute.db = dbmock self.compute.driver = drivermock @@ -707,11 +714,15 @@ class ComputeTestCase(test.TestCase): dbmock.queue_get_for(c, FLAGS.compute_topic, i_ref['host']).\ AndReturn(topic) rpc.call(c, topic, {"method": "pre_live_migration", - "args": {'instance_id': i_ref['id']}}) + "args": {'instance_id': i_ref['id'], + 'block_migration': False, + 'disk': None}}) + self.mox.StubOutWithMock(self.compute.driver, 'live_migration') self.compute.driver.live_migration(c, i_ref, i_ref['host'], self.compute.post_live_migration, - self.compute.recover_live_migration) + self.compute.rollback_live_migration, + False) self.compute.db = dbmock self.mox.ReplayAll() @@ -732,13 +743,18 @@ class ComputeTestCase(test.TestCase): dbmock.queue_get_for(c, FLAGS.compute_topic, i_ref['host']).\ AndReturn(topic) rpc.call(c, topic, {"method": "pre_live_migration", - "args": {'instance_id': i_ref['id']}}).\ + "args": {'instance_id': i_ref['id'], + 'block_migration': False, + 'disk': None}}).\ AndRaise(rpc.RemoteError('', '', '')) dbmock.instance_update(c, i_ref['id'], {'state_description': 'running', 'state': power_state.RUNNING, 'host': i_ref['host']}) for v in i_ref['volumes']: dbmock.volume_update(c, v['id'], {'status': 'in-use'}) + # mock for volume_api.remove_from_compute + rpc.call(c, topic, {"method": "remove_volume", + "args": {'volume_id': v['id']}}) self.compute.db = dbmock self.mox.ReplayAll() @@ -759,7 +775,9 @@ class ComputeTestCase(test.TestCase): AndReturn(topic) self.mox.StubOutWithMock(rpc, 'call') rpc.call(c, topic, {"method": "pre_live_migration", - "args": {'instance_id': i_ref['id']}}).\ + "args": {'instance_id': i_ref['id'], + 'block_migration': False, + 'disk': None}}).\ AndRaise(rpc.RemoteError('', '', '')) dbmock.instance_update(c, i_ref['id'], {'state_description': 'running', 'state': power_state.RUNNING, @@ -784,11 +802,14 @@ class ComputeTestCase(test.TestCase): dbmock.queue_get_for(c, FLAGS.compute_topic, i_ref['host']).\ AndReturn(topic) rpc.call(c, topic, {"method": "pre_live_migration", - "args": {'instance_id': i_ref['id']}}) + "args": {'instance_id': i_ref['id'], + 'block_migration': False, + 'disk': None}}) self.mox.StubOutWithMock(self.compute.driver, 'live_migration') self.compute.driver.live_migration(c, i_ref, i_ref['host'], self.compute.post_live_migration, - self.compute.recover_live_migration) + self.compute.rollback_live_migration, + False) self.compute.db = dbmock self.mox.ReplayAll() @@ -822,6 +843,10 @@ class ComputeTestCase(test.TestCase): self.compute.volume_manager.remove_compute_volume(c, v['id']) self.mox.StubOutWithMock(self.compute.driver, 'unfilter_instance') self.compute.driver.unfilter_instance(i_ref, []) + self.mox.StubOutWithMock(rpc, 'call') + rpc.call(c, db.queue_get_for(c, FLAGS.compute_topic, dest), + {"method": "post_live_migration_at_destination", + "args": {'instance_id': i_ref['id'], 'block_migration': False}}) # executing self.mox.ReplayAll() @@ -864,6 +889,458 @@ class ComputeTestCase(test.TestCase): self.assertEqual(len(instances), 1) self.assertEqual(power_state.SHUTOFF, instances[0]['state']) + def test_get_all_by_name_regexp(self): + """Test searching instances by name (display_name)""" + c = context.get_admin_context() + instance_id1 = self._create_instance({'display_name': 'woot'}) + instance_id2 = self._create_instance({ + 'display_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'display_name': 'not-woot', + 'id': 30}) + + instances = self.compute_api.get_all(c, + search_opts={'name': 'woo.*'}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id1 in instance_ids) + self.assertTrue(instance_id2 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'name': 'woot.*'}) + instance_ids = [instance.id for instance in instances] + self.assertEqual(len(instances), 1) + self.assertTrue(instance_id1 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'name': '.*oot.*'}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id1 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'name': 'n.*'}) + self.assertEqual(len(instances), 1) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id3 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'name': 'noth.*'}) + self.assertEqual(len(instances), 0) + + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_instance_name_regexp(self): + """Test searching instances by name""" + self.flags(instance_name_template='instance-%d') + + c = context.get_admin_context() + instance_id1 = self._create_instance() + instance_id2 = self._create_instance({'id': 2}) + instance_id3 = self._create_instance({'id': 10}) + + instances = self.compute_api.get_all(c, + search_opts={'instance_name': 'instance.*'}) + self.assertEqual(len(instances), 3) + + instances = self.compute_api.get_all(c, + search_opts={'instance_name': '.*\-\d$'}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id1 in instance_ids) + self.assertTrue(instance_id2 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'instance_name': 'i.*2'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id2) + + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_by_fixed_ip(self): + """Test getting 1 instance by Fixed IP""" + c = context.get_admin_context() + instance_id1 = self._create_instance() + instance_id2 = self._create_instance({'id': 20}) + instance_id3 = self._create_instance({'id': 30}) + + vif_ref1 = db.virtual_interface_create(c, + {'address': '12:34:56:78:90:12', + 'instance_id': instance_id1, + 'network_id': 1}) + vif_ref2 = db.virtual_interface_create(c, + {'address': '90:12:34:56:78:90', + 'instance_id': instance_id2, + 'network_id': 1}) + + db.fixed_ip_create(c, + {'address': '1.1.1.1', + 'instance_id': instance_id1, + 'virtual_interface_id': vif_ref1['id']}) + db.fixed_ip_create(c, + {'address': '1.1.2.1', + 'instance_id': instance_id2, + 'virtual_interface_id': vif_ref2['id']}) + + # regex not allowed + instances = self.compute_api.get_all(c, + search_opts={'fixed_ip': '.*'}) + self.assertEqual(len(instances), 0) + + instances = self.compute_api.get_all(c, + search_opts={'fixed_ip': '1.1.3.1'}) + self.assertEqual(len(instances), 0) + + instances = self.compute_api.get_all(c, + search_opts={'fixed_ip': '1.1.1.1'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) + + instances = self.compute_api.get_all(c, + search_opts={'fixed_ip': '1.1.2.1'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id2) + + db.virtual_interface_delete(c, vif_ref1['id']) + db.virtual_interface_delete(c, vif_ref2['id']) + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + + def test_get_all_by_ip_regexp(self): + """Test searching by Floating and Fixed IP""" + c = context.get_admin_context() + instance_id1 = self._create_instance({'display_name': 'woot'}) + instance_id2 = self._create_instance({ + 'display_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'display_name': 'not-woot', + 'id': 30}) + + vif_ref1 = db.virtual_interface_create(c, + {'address': '12:34:56:78:90:12', + 'instance_id': instance_id1, + 'network_id': 1}) + vif_ref2 = db.virtual_interface_create(c, + {'address': '90:12:34:56:78:90', + 'instance_id': instance_id2, + 'network_id': 1}) + vif_ref3 = db.virtual_interface_create(c, + {'address': '34:56:78:90:12:34', + 'instance_id': instance_id3, + 'network_id': 1}) + + db.fixed_ip_create(c, + {'address': '1.1.1.1', + 'instance_id': instance_id1, + 'virtual_interface_id': vif_ref1['id']}) + db.fixed_ip_create(c, + {'address': '1.1.2.1', + 'instance_id': instance_id2, + 'virtual_interface_id': vif_ref2['id']}) + fix_addr = db.fixed_ip_create(c, + {'address': '1.1.3.1', + 'instance_id': instance_id3, + 'virtual_interface_id': vif_ref3['id']}) + fix_ref = db.fixed_ip_get_by_address(c, fix_addr) + flo_ref = db.floating_ip_create(c, + {'address': '10.0.0.2', + 'fixed_ip_id': fix_ref['id']}) + + # ends up matching 2nd octet here.. so all 3 match + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*\.1'}) + self.assertEqual(len(instances), 3) + + instances = self.compute_api.get_all(c, + search_opts={'ip': '1.*'}) + self.assertEqual(len(instances), 3) + + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*\.1.\d+$'}) + self.assertEqual(len(instances), 1) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id1 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*\.2.+'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id2) + + instances = self.compute_api.get_all(c, + search_opts={'ip': '10.*'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id3) + + db.virtual_interface_delete(c, vif_ref1['id']) + db.virtual_interface_delete(c, vif_ref2['id']) + db.virtual_interface_delete(c, vif_ref3['id']) + db.floating_ip_destroy(c, '10.0.0.2') + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_ipv6_regexp(self): + """Test searching by IPv6 address""" + + c = context.get_admin_context() + instance_id1 = self._create_instance({'display_name': 'woot'}) + instance_id2 = self._create_instance({ + 'display_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'display_name': 'not-woot', + 'id': 30}) + + vif_ref1 = db.virtual_interface_create(c, + {'address': '12:34:56:78:90:12', + 'instance_id': instance_id1, + 'network_id': 1}) + vif_ref2 = db.virtual_interface_create(c, + {'address': '90:12:34:56:78:90', + 'instance_id': instance_id2, + 'network_id': 1}) + vif_ref3 = db.virtual_interface_create(c, + {'address': '34:56:78:90:12:34', + 'instance_id': instance_id3, + 'network_id': 1}) + + # This will create IPv6 addresses of: + # 1: fd00::1034:56ff:fe78:9012 + # 20: fd00::9212:34ff:fe56:7890 + # 30: fd00::3656:78ff:fe90:1234 + + instances = self.compute_api.get_all(c, + search_opts={'ip6': '.*1034.*'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) + + instances = self.compute_api.get_all(c, + search_opts={'ip6': '^fd00.*'}) + self.assertEqual(len(instances), 3) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id1 in instance_ids) + self.assertTrue(instance_id2 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'ip6': '^.*12.*34.*'}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id2 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + db.virtual_interface_delete(c, vif_ref1['id']) + db.virtual_interface_delete(c, vif_ref2['id']) + db.virtual_interface_delete(c, vif_ref3['id']) + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_multiple_options_at_once(self): + """Test searching by multiple options at once""" + c = context.get_admin_context() + instance_id1 = self._create_instance({'display_name': 'woot'}) + instance_id2 = self._create_instance({ + 'display_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'display_name': 'not-woot', + 'id': 30}) + + vif_ref1 = db.virtual_interface_create(c, + {'address': '12:34:56:78:90:12', + 'instance_id': instance_id1, + 'network_id': 1}) + vif_ref2 = db.virtual_interface_create(c, + {'address': '90:12:34:56:78:90', + 'instance_id': instance_id2, + 'network_id': 1}) + vif_ref3 = db.virtual_interface_create(c, + {'address': '34:56:78:90:12:34', + 'instance_id': instance_id3, + 'network_id': 1}) + + db.fixed_ip_create(c, + {'address': '1.1.1.1', + 'instance_id': instance_id1, + 'virtual_interface_id': vif_ref1['id']}) + db.fixed_ip_create(c, + {'address': '1.1.2.1', + 'instance_id': instance_id2, + 'virtual_interface_id': vif_ref2['id']}) + fix_addr = db.fixed_ip_create(c, + {'address': '1.1.3.1', + 'instance_id': instance_id3, + 'virtual_interface_id': vif_ref3['id']}) + fix_ref = db.fixed_ip_get_by_address(c, fix_addr) + flo_ref = db.floating_ip_create(c, + {'address': '10.0.0.2', + 'fixed_ip_id': fix_ref['id']}) + + # ip ends up matching 2nd octet here.. so all 3 match ip + # but 'name' only matches one + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*\.1', 'name': 'not.*'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id3) + + # ip ends up matching any ip with a '2' in it.. so instance + # 2 and 3.. but name should only match #2 + # but 'name' only matches one + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*2', 'name': '^woo.*'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id2) + + # same as above but no match on name (name matches instance_id1 + # but the ip query doesn't + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*2.*', 'name': '^woot.*'}) + self.assertEqual(len(instances), 0) + + # ip matches all 3... ipv6 matches #2+#3...name matches #3 + instances = self.compute_api.get_all(c, + search_opts={'ip': '.*\.1', + 'name': 'not.*', + 'ip6': '^.*12.*34.*'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id3) + + db.virtual_interface_delete(c, vif_ref1['id']) + db.virtual_interface_delete(c, vif_ref2['id']) + db.virtual_interface_delete(c, vif_ref3['id']) + db.floating_ip_destroy(c, '10.0.0.2') + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_image(self): + """Test searching instances by image""" + + c = context.get_admin_context() + instance_id1 = self._create_instance({'image_ref': '1234'}) + instance_id2 = self._create_instance({ + 'id': 2, + 'image_ref': '4567'}) + instance_id3 = self._create_instance({ + 'id': 10, + 'image_ref': '4567'}) + + instances = self.compute_api.get_all(c, + search_opts={'image': '123'}) + self.assertEqual(len(instances), 0) + + instances = self.compute_api.get_all(c, + search_opts={'image': '1234'}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) + + instances = self.compute_api.get_all(c, + search_opts={'image': '4567'}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id2 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + # Test passing a list as search arg + instances = self.compute_api.get_all(c, + search_opts={'image': ['1234', '4567']}) + self.assertEqual(len(instances), 3) + + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_flavor(self): + """Test searching instances by image""" + + c = context.get_admin_context() + instance_id1 = self._create_instance({'instance_type_id': 1}) + instance_id2 = self._create_instance({ + 'id': 2, + 'instance_type_id': 2}) + instance_id3 = self._create_instance({ + 'id': 10, + 'instance_type_id': 2}) + + # NOTE(comstud): Migrations set up the instance_types table + # for us. Therefore, we assume the following is true for + # these tests: + # instance_type_id 1 == flavor 3 + # instance_type_id 2 == flavor 1 + # instance_type_id 3 == flavor 4 + # instance_type_id 4 == flavor 5 + # instance_type_id 5 == flavor 2 + + instances = self.compute_api.get_all(c, + search_opts={'flavor': 5}) + self.assertEqual(len(instances), 0) + + self.assertRaises(exception.FlavorNotFound, + self.compute_api.get_all, + c, search_opts={'flavor': 99}) + + instances = self.compute_api.get_all(c, + search_opts={'flavor': 3}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) + + instances = self.compute_api.get_all(c, + search_opts={'flavor': 1}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id2 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + + def test_get_all_by_state(self): + """Test searching instances by state""" + + c = context.get_admin_context() + instance_id1 = self._create_instance({'state': power_state.SHUTDOWN}) + instance_id2 = self._create_instance({ + 'id': 2, + 'state': power_state.RUNNING}) + instance_id3 = self._create_instance({ + 'id': 10, + 'state': power_state.RUNNING}) + + instances = self.compute_api.get_all(c, + search_opts={'state': power_state.SUSPENDED}) + self.assertEqual(len(instances), 0) + + instances = self.compute_api.get_all(c, + search_opts={'state': power_state.SHUTDOWN}) + self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) + + instances = self.compute_api.get_all(c, + search_opts={'state': power_state.RUNNING}) + self.assertEqual(len(instances), 2) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id2 in instance_ids) + self.assertTrue(instance_id3 in instance_ids) + + # Test passing a list as search arg + instances = self.compute_api.get_all(c, + search_opts={'state': [power_state.SHUTDOWN, + power_state.RUNNING]}) + self.assertEqual(len(instances), 3) + + db.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + db.instance_destroy(c, instance_id3) + @staticmethod def _parse_db_block_device_mapping(bdm_ref): attr_list = ('delete_on_termination', 'device_name', 'no_device', @@ -877,15 +1354,17 @@ class ComputeTestCase(test.TestCase): return bdm def test_update_block_device_mapping(self): + swap_size = 1 + instance_type = {'swap': swap_size} instance_id = self._create_instance() mappings = [ {'virtual': 'ami', 'device': 'sda1'}, {'virtual': 'root', 'device': '/dev/sda1'}, - {'virtual': 'swap', 'device': 'sdb1'}, - {'virtual': 'swap', 'device': 'sdb2'}, - {'virtual': 'swap', 'device': 'sdb3'}, {'virtual': 'swap', 'device': 'sdb4'}, + {'virtual': 'swap', 'device': 'sdb3'}, + {'virtual': 'swap', 'device': 'sdb2'}, + {'virtual': 'swap', 'device': 'sdb1'}, {'virtual': 'ephemeral0', 'device': 'sdc1'}, {'virtual': 'ephemeral1', 'device': 'sdc2'}, @@ -927,32 +1406,36 @@ class ComputeTestCase(test.TestCase): 'no_device': True}] self.compute_api._update_image_block_device_mapping( - self.context, instance_id, mappings) + self.context, instance_type, instance_id, mappings) bdms = [self._parse_db_block_device_mapping(bdm_ref) for bdm_ref in db.block_device_mapping_get_all_by_instance( self.context, instance_id)] expected_result = [ - {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, - {'virtual_name': 'swap', 'device_name': '/dev/sdb2'}, - {'virtual_name': 'swap', 'device_name': '/dev/sdb3'}, - {'virtual_name': 'swap', 'device_name': '/dev/sdb4'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb1', + 'volume_size': swap_size}, {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'}, - {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'}, - {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}] + + # NOTE(yamahata): ATM only ephemeral0 is supported. + # they're ignored for now + #{'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'}, + #{'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'} + ] bdms.sort() expected_result.sort() self.assertDictListMatch(bdms, expected_result) self.compute_api._update_block_device_mapping( - self.context, instance_id, block_device_mapping) + self.context, instance_types.get_default_instance_type(), + instance_id, block_device_mapping) bdms = [self._parse_db_block_device_mapping(bdm_ref) for bdm_ref in db.block_device_mapping_get_all_by_instance( self.context, instance_id)] expected_result = [ {'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'}, - {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb1', + 'volume_size': swap_size}, {'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'}, {'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'}, {'no_device': True, 'device_name': '/dev/sdb4'}, @@ -974,3 +1457,13 @@ class ComputeTestCase(test.TestCase): self.context, instance_id): db.block_device_mapping_destroy(self.context, bdm['id']) self.compute.terminate_instance(self.context, instance_id) + + def test_ephemeral_size(self): + local_size = 2 + inst_type = {'local_gb': local_size} + self.assertEqual(self.compute_api._ephemeral_size(inst_type, + 'ephemeral0'), + local_size) + self.assertEqual(self.compute_api._ephemeral_size(inst_type, + 'ephemeral1'), + 0) diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 548f81f8b..a724db9da 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -48,6 +48,10 @@ def stub_set_host_enabled(context, host, enabled): return status +def stub_host_power_action(context, host, action): + return action + + class FakeRequest(object): environ = {"nova.context": context.get_admin_context()} @@ -62,6 +66,8 @@ class HostTestCase(test.TestCase): self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list) self.stubs.Set(self.controller.compute_api, 'set_host_enabled', stub_set_host_enabled) + self.stubs.Set(self.controller.compute_api, 'host_power_action', + stub_host_power_action) def test_list_hosts(self): """Verify that the compute hosts are returned.""" @@ -87,6 +93,18 @@ class HostTestCase(test.TestCase): result_c2 = self.controller.update(self.req, "host_c2", body=en_body) self.assertEqual(result_c2["status"], "disabled") + def test_host_startup(self): + result = self.controller.startup(self.req, "host_c1") + self.assertEqual(result["power_action"], "startup") + + def test_host_shutdown(self): + result = self.controller.shutdown(self.req, "host_c1") + self.assertEqual(result["power_action"], "shutdown") + + def test_host_reboot(self): + result = self.controller.reboot(self.req, "host_c1") + self.assertEqual(result["power_action"], "reboot") + def test_bad_status_value(self): bad_body = {"status": "bad"} self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py new file mode 100644 index 000000000..9680d6f2b --- /dev/null +++ b/nova/tests/test_image.py @@ -0,0 +1,134 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# Author: Soren Hansen +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from nova import context +from nova import exception +from nova import test +import nova.image + + +class _ImageTestCase(test.TestCase): + def setUp(self): + super(_ImageTestCase, self).setUp() + self.context = context.get_admin_context() + + def test_index(self): + res = self.image_service.index(self.context) + for image in res: + self.assertEquals(set(image.keys()), set(['id', 'name'])) + + def test_detail(self): + res = self.image_service.detail(self.context) + for image in res: + keys = set(image.keys()) + self.assertEquals(keys, set(['id', 'name', 'created_at', + 'updated_at', 'deleted_at', 'deleted', + 'status', 'is_public', 'properties'])) + self.assertTrue(isinstance(image['created_at'], datetime.datetime)) + self.assertTrue(isinstance(image['updated_at'], datetime.datetime)) + + if not (isinstance(image['deleted_at'], datetime.datetime) or + image['deleted_at'] is None): + self.fail('image\'s "deleted_at" attribute was neither a ' + 'datetime object nor None') + + def check_is_bool(image, key): + val = image.get('deleted') + if not isinstance(val, bool): + self.fail('image\'s "%s" attribute wasn\'t ' + 'a bool: %r' % (key, val)) + + check_is_bool(image, 'deleted') + check_is_bool(image, 'is_public') + + def test_index_and_detail_have_same_results(self): + index = self.image_service.index(self.context) + detail = self.image_service.detail(self.context) + index_set = set([(i['id'], i['name']) for i in index]) + detail_set = set([(i['id'], i['name']) for i in detail]) + self.assertEqual(index_set, detail_set) + + def test_show_raises_imagenotfound_for_invalid_id(self): + self.assertRaises(exception.ImageNotFound, + self.image_service.show, + self.context, + 'this image does not exist') + + def test_show_by_name(self): + self.assertRaises(exception.ImageNotFound, + self.image_service.show_by_name, + self.context, + 'this image does not exist') + + def test_create_adds_id(self): + index = self.image_service.index(self.context) + image_count = len(index) + + self.image_service.create(self.context, {}) + + index = self.image_service.index(self.context) + self.assertEquals(len(index), image_count + 1) + + self.assertTrue(index[0]['id']) + + def test_create_keeps_id(self): + self.image_service.create(self.context, {'id': '34'}) + self.image_service.show(self.context, '34') + + def test_create_rejects_duplicate_ids(self): + self.image_service.create(self.context, {'id': '34'}) + self.assertRaises(exception.Duplicate, + self.image_service.create, + self.context, + {'id': '34'}) + + # Make sure there's still one left + self.image_service.show(self.context, '34') + + def test_update(self): + self.image_service.create(self.context, + {'id': '34', 'foo': 'bar'}) + + self.image_service.update(self.context, '34', + {'id': '34', 'foo': 'baz'}) + + img = self.image_service.show(self.context, '34') + self.assertEquals(img['foo'], 'baz') + + def test_delete(self): + self.image_service.create(self.context, {'id': '34', 'foo': 'bar'}) + self.image_service.delete(self.context, '34') + self.assertRaises(exception.NotFound, + self.image_service.show, + self.context, + '34') + + def test_delete_all(self): + self.image_service.create(self.context, {'id': '32', 'foo': 'bar'}) + self.image_service.create(self.context, {'id': '33', 'foo': 'bar'}) + self.image_service.create(self.context, {'id': '34', 'foo': 'bar'}) + self.image_service.delete_all() + index = self.image_service.index(self.context) + self.assertEquals(len(index), 0) + + +class FakeImageTestCase(_ImageTestCase): + def setUp(self): + super(FakeImageTestCase, self).setUp() + self.image_service = nova.image.fake.FakeImageService() diff --git a/nova/tests/test_instance_types_extra_specs.py b/nova/tests/test_instance_types_extra_specs.py index 393ed1e36..205601277 100644 --- a/nova/tests/test_instance_types_extra_specs.py +++ b/nova/tests/test_instance_types_extra_specs.py @@ -136,7 +136,7 @@ class InstanceTypeExtraSpecsTestCase(test.TestCase): "m1.small") self.assertEquals(instance_type['extra_specs'], {}) - def test_instance_type_get_with_extra_specs(self): + def test_instance_type_get_by_flavor_id_with_extra_specs(self): instance_type = db.api.instance_type_get_by_flavor_id( context.get_admin_context(), 105) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index f8b866985..688518bb8 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -21,6 +21,7 @@ import os import re import shutil import sys +import tempfile from xml.etree.ElementTree import fromstring as xml_to_tree from xml.dom.minidom import parseString as xml_to_dom @@ -49,18 +50,19 @@ def _create_network_info(count=1, ipv6=None): if ipv6 is None: ipv6 = FLAGS.use_ipv6 fake = 'fake' - fake_ip = '0.0.0.0/0' - fake_ip_2 = '0.0.0.1/0' - fake_ip_3 = '0.0.0.1/0' + fake_ip = '10.11.12.13' + fake_ip_2 = '0.0.0.1' + fake_ip_3 = '0.0.0.1' fake_vlan = 100 fake_bridge_interface = 'eth0' network = {'bridge': fake, 'cidr': fake_ip, 'cidr_v6': fake_ip, + 'gateway_v6': fake, 'vlan': fake_vlan, 'bridge_interface': fake_bridge_interface} mapping = {'mac': fake, - 'dhcp_server': fake, + 'dhcp_server': '10.0.0.1', 'gateway': fake, 'gateway6': fake, 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} @@ -71,12 +73,12 @@ def _create_network_info(count=1, ipv6=None): return [(network, mapping) for x in xrange(0, count)] -def _setup_networking(instance_id, ip='1.2.3.4'): +def _setup_networking(instance_id, ip='1.2.3.4', mac='56:12:12:12:12:12'): ctxt = context.get_admin_context() network_ref = db.project_get_networks(ctxt, 'fake', associate=True)[0] - vif = {'address': '56:12:12:12:12:12', + vif = {'address': mac, 'network_id': network_ref['id'], 'instance_id': instance_id} vif_ref = db.virtual_interface_create(ctxt, vif) @@ -169,6 +171,7 @@ class LibvirtConnTestCase(test.TestCase): 'project_id': 'fake', 'bridge': 'br101', 'image_ref': '123456', + 'local_gb': 20, 'instance_type_id': '5'} # m1.small def lazy_load_library_exists(self): @@ -272,15 +275,14 @@ class LibvirtConnTestCase(test.TestCase): conn = connection.LibvirtConnection(True) instance_ref = db.instance_create(self.context, self.test_instance) - result = conn._prepare_xml_info(instance_ref, False) - self.assertFalse(result['nics']) - - result = conn._prepare_xml_info(instance_ref, False, - _create_network_info()) + result = conn._prepare_xml_info(instance_ref, + _create_network_info(), + False) self.assertTrue(len(result['nics']) == 1) - result = conn._prepare_xml_info(instance_ref, False, - _create_network_info(2)) + result = conn._prepare_xml_info(instance_ref, + _create_network_info(2), + False) self.assertTrue(len(result['nics']) == 2) def test_xml_and_uri_no_ramdisk_no_kernel(self): @@ -407,16 +409,16 @@ class LibvirtConnTestCase(test.TestCase): network_info = _create_network_info(2) conn = connection.LibvirtConnection(True) instance_ref = db.instance_create(self.context, instance_data) - xml = conn.to_xml(instance_ref, False, network_info) + xml = conn.to_xml(instance_ref, network_info, False) tree = xml_to_tree(xml) interfaces = tree.findall("./devices/interface") self.assertEquals(len(interfaces), 2) parameters = interfaces[0].findall('./filterref/parameter') self.assertEquals(interfaces[0].get('type'), 'bridge') self.assertEquals(parameters[0].get('name'), 'IP') - self.assertEquals(parameters[0].get('value'), '0.0.0.0/0') + self.assertEquals(parameters[0].get('value'), '10.11.12.13') self.assertEquals(parameters[1].get('name'), 'DHCPSERVER') - self.assertEquals(parameters[1].get('value'), 'fake') + self.assertEquals(parameters[1].get('value'), '10.0.0.1') def _check_xml_and_container(self, instance): user_context = context.RequestContext(self.user_id, @@ -430,7 +432,8 @@ class LibvirtConnTestCase(test.TestCase): uri = conn.get_uri() self.assertEquals(uri, 'lxc:///') - xml = conn.to_xml(instance_ref) + network_info = _create_network_info() + xml = conn.to_xml(instance_ref, network_info) tree = xml_to_tree(xml) check = [ @@ -527,17 +530,20 @@ class LibvirtConnTestCase(test.TestCase): uri = conn.get_uri() self.assertEquals(uri, expected_uri) - xml = conn.to_xml(instance_ref, rescue) + network_info = _create_network_info() + xml = conn.to_xml(instance_ref, network_info, rescue) tree = xml_to_tree(xml) for i, (check, expected_result) in enumerate(checks): self.assertEqual(check(tree), expected_result, - '%s failed check %d' % (xml, i)) + '%s != %s failed check %d' % + (check(tree), expected_result, i)) for i, (check, expected_result) in enumerate(common_checks): self.assertEqual(check(tree), expected_result, - '%s failed common check %d' % (xml, i)) + '%s != %s failed common check %d' % + (check(tree), expected_result, i)) # This test is supposed to make sure we don't # override a specifically set uri @@ -622,7 +628,7 @@ class LibvirtConnTestCase(test.TestCase): return # Preparing mocks - def fake_none(self): + def fake_none(self, *args): return def fake_raise(self): @@ -639,6 +645,7 @@ class LibvirtConnTestCase(test.TestCase): self.create_fake_libvirt_mock() instance_ref = db.instance_create(self.context, self.test_instance) + network_info = _create_network_info() # Start test self.mox.ReplayAll() @@ -648,6 +655,7 @@ class LibvirtConnTestCase(test.TestCase): conn.firewall_driver.setattr('prepare_instance_filter', fake_none) conn.firewall_driver.setattr('instance_filter_exists', fake_none) conn.ensure_filtering_rules_for_instance(instance_ref, + network_info, time=fake_timer) except exception.Error, e: c1 = (0 <= e.message.find('Timeout migrating for')) @@ -689,17 +697,20 @@ class LibvirtConnTestCase(test.TestCase): return vdmock self.create_fake_libvirt_mock(lookupByName=fake_lookup) - self.mox.StubOutWithMock(self.compute, "recover_live_migration") - self.compute.recover_live_migration(self.context, instance_ref, - dest='dest') - - # Start test +# self.mox.StubOutWithMock(self.compute, "recover_live_migration") + self.mox.StubOutWithMock(self.compute, "rollback_live_migration") +# self.compute.recover_live_migration(self.context, instance_ref, +# dest='dest') + self.compute.rollback_live_migration(self.context, instance_ref, + 'dest', False) + + #start test self.mox.ReplayAll() conn = connection.LibvirtConnection(False) self.assertRaises(libvirt.libvirtError, conn._live_migration, - self.context, instance_ref, 'dest', '', - self.compute.recover_live_migration) + self.context, instance_ref, 'dest', False, + self.compute.rollback_live_migration) instance_ref = db.instance_get(self.context, instance_ref['id']) self.assertTrue(instance_ref['state_description'] == 'running') @@ -710,6 +721,95 @@ class LibvirtConnTestCase(test.TestCase): db.volume_destroy(self.context, volume_ref['id']) db.instance_destroy(self.context, instance_ref['id']) + def test_pre_block_migration_works_correctly(self): + """Confirms pre_block_migration works correctly.""" + + # Skip if non-libvirt environment + if not self.lazy_load_library_exists(): + return + + # Replace instances_path since this testcase creates tmpfile + tmpdir = tempfile.mkdtemp() + store = FLAGS.instances_path + FLAGS.instances_path = tmpdir + + # Test data + instance_ref = db.instance_create(self.context, self.test_instance) + dummyjson = '[{"path": "%s/disk", "local_gb": "10G", "type": "raw"}]' + + # Preparing mocks + # qemu-img should be mockd since test environment might not have + # large disk space. + self.mox.StubOutWithMock(utils, "execute") + utils.execute('sudo', 'qemu-img', 'create', '-f', 'raw', + '%s/%s/disk' % (tmpdir, instance_ref.name), '10G') + + self.mox.ReplayAll() + conn = connection.LibvirtConnection(False) + conn.pre_block_migration(self.context, instance_ref, + dummyjson % tmpdir) + + self.assertTrue(os.path.exists('%s/%s/' % + (tmpdir, instance_ref.name))) + + shutil.rmtree(tmpdir) + db.instance_destroy(self.context, instance_ref['id']) + # Restore FLAGS.instances_path + FLAGS.instances_path = store + + def test_get_instance_disk_info_works_correctly(self): + """Confirms pre_block_migration works correctly.""" + # Skip if non-libvirt environment + if not self.lazy_load_library_exists(): + return + + # Test data + instance_ref = db.instance_create(self.context, self.test_instance) + dummyxml = ("<domain type='kvm'><name>instance-0000000a</name>" + "<devices>" + "<disk type='file'><driver name='qemu' type='raw'/>" + "<source file='/test/disk'/>" + "<target dev='vda' bus='virtio'/></disk>" + "<disk type='file'><driver name='qemu' type='qcow2'/>" + "<source file='/test/disk.local'/>" + "<target dev='vdb' bus='virtio'/></disk>" + "</devices></domain>") + + ret = ("image: /test/disk\nfile format: raw\n" + "virtual size: 20G (21474836480 bytes)\ndisk size: 3.1G\n") + + # Preparing mocks + vdmock = self.mox.CreateMock(libvirt.virDomain) + self.mox.StubOutWithMock(vdmock, "XMLDesc") + vdmock.XMLDesc(0).AndReturn(dummyxml) + + def fake_lookup(instance_name): + if instance_name == instance_ref.name: + return vdmock + self.create_fake_libvirt_mock(lookupByName=fake_lookup) + + self.mox.StubOutWithMock(os.path, "getsize") + # based on above testdata, one is raw image, so getsize is mocked. + os.path.getsize("/test/disk").AndReturn(10 * 1024 * 1024 * 1024) + # another is qcow image, so qemu-img should be mocked. + self.mox.StubOutWithMock(utils, "execute") + utils.execute('sudo', 'qemu-img', 'info', '/test/disk.local').\ + AndReturn((ret, '')) + + self.mox.ReplayAll() + conn = connection.LibvirtConnection(False) + info = conn.get_instance_disk_info(self.context, instance_ref) + info = utils.loads(info) + + self.assertTrue(info[0]['type'] == 'raw' and + info[1]['type'] == 'qcow2' and + info[0]['path'] == '/test/disk' and + info[1]['path'] == '/test/disk.local' and + info[0]['local_gb'] == '10G' and + info[1]['local_gb'] == '20G') + + db.instance_destroy(self.context, instance_ref['id']) + def test_spawn_with_network_info(self): # Skip if non-libvirt environment if not self.lazy_load_library_exists(): @@ -744,6 +844,42 @@ class LibvirtConnTestCase(test.TestCase): ip = conn.get_host_ip_addr() self.assertEquals(ip, FLAGS.my_ip) + def test_volume_in_mapping(self): + conn = connection.LibvirtConnection(False) + swap = {'device_name': '/dev/sdb', + 'swap_size': 1} + ephemerals = [{'num': 0, + 'virtual_name': 'ephemeral0', + 'device_name': '/dev/sdc1', + 'size': 1}, + {'num': 2, + 'virtual_name': 'ephemeral2', + 'device_name': '/dev/sdd', + 'size': 1}] + block_device_mapping = [{'mount_device': '/dev/sde', + 'device_path': 'fake_device'}, + {'mount_device': '/dev/sdf', + 'device_path': 'fake_device'}] + block_device_info = { + 'root_device_name': '/dev/sda', + 'swap': swap, + 'ephemerals': ephemerals, + 'block_device_mapping': block_device_mapping} + + def _assert_volume_in_mapping(device_name, true_or_false): + self.assertEquals(conn._volume_in_mapping(device_name, + block_device_info), + true_or_false) + + _assert_volume_in_mapping('sda', False) + _assert_volume_in_mapping('sdb', True) + _assert_volume_in_mapping('sdc1', True) + _assert_volume_in_mapping('sdd', True) + _assert_volume_in_mapping('sde', True) + _assert_volume_in_mapping('sdf', True) + _assert_volume_in_mapping('sdg', False) + _assert_volume_in_mapping('sdh1', False) + class NWFilterFakes: def __init__(self): @@ -847,7 +983,11 @@ class IptablesFirewallTestCase(test.TestCase): def test_static_filters(self): instance_ref = self._create_instance_ref() - _setup_networking(instance_ref['id'], self.test_ip) + src_instance_ref = self._create_instance_ref() + src_ip = '10.11.12.14' + src_mac = '56:12:12:12:12:13' + _setup_networking(instance_ref['id'], self.test_ip, src_mac) + _setup_networking(src_instance_ref['id'], src_ip) admin_ctxt = context.get_admin_context() secgroup = db.security_group_create(admin_ctxt, @@ -856,6 +996,12 @@ class IptablesFirewallTestCase(test.TestCase): 'name': 'testgroup', 'description': 'test group'}) + src_secgroup = db.security_group_create(admin_ctxt, + {'user_id': 'fake', + 'project_id': 'fake', + 'name': 'testsourcegroup', + 'description': 'src group'}) + db.security_group_rule_create(admin_ctxt, {'parent_group_id': secgroup['id'], 'protocol': 'icmp', @@ -877,25 +1023,35 @@ class IptablesFirewallTestCase(test.TestCase): 'to_port': 81, 'cidr': '192.168.10.0/24'}) + db.security_group_rule_create(admin_ctxt, + {'parent_group_id': secgroup['id'], + 'protocol': 'tcp', + 'from_port': 80, + 'to_port': 81, + 'group_id': src_secgroup['id']}) + db.instance_add_security_group(admin_ctxt, instance_ref['id'], secgroup['id']) + db.instance_add_security_group(admin_ctxt, src_instance_ref['id'], + src_secgroup['id']) instance_ref = db.instance_get(admin_ctxt, instance_ref['id']) + src_instance_ref = db.instance_get(admin_ctxt, src_instance_ref['id']) # self.fw.add_instance(instance_ref) def fake_iptables_execute(*cmd, **kwargs): process_input = kwargs.get('process_input', None) - if cmd == ('sudo', 'ip6tables-save', '-t', 'filter'): + if cmd == ('ip6tables-save', '-t', 'filter'): return '\n'.join(self.in6_filter_rules), None - if cmd == ('sudo', 'iptables-save', '-t', 'filter'): + if cmd == ('iptables-save', '-t', 'filter'): return '\n'.join(self.in_filter_rules), None - if cmd == ('sudo', 'iptables-save', '-t', 'nat'): + if cmd == ('iptables-save', '-t', 'nat'): return '\n'.join(self.in_nat_rules), None - if cmd == ('sudo', 'iptables-restore'): + if cmd == ('iptables-restore',): lines = process_input.split('\n') if '*filter' in lines: self.out_rules = lines return '', '' - if cmd == ('sudo', 'ip6tables-restore'): + if cmd == ('ip6tables-restore',): lines = process_input.split('\n') if '*filter' in lines: self.out6_rules = lines @@ -905,8 +1061,9 @@ class IptablesFirewallTestCase(test.TestCase): from nova.network import linux_net linux_net.iptables_manager.execute = fake_iptables_execute - self.fw.prepare_instance_filter(instance_ref) - self.fw.apply_instance_filter(instance_ref) + network_info = _create_network_info() + self.fw.prepare_instance_filter(instance_ref, network_info) + self.fw.apply_instance_filter(instance_ref, network_info) in_rules = filter(lambda l: not l.startswith('#'), self.in_filter_rules) @@ -932,17 +1089,22 @@ class IptablesFirewallTestCase(test.TestCase): self.assertTrue(security_group_chain, "The security group chain wasn't added") - regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -j ACCEPT') + regex = re.compile('-A .* -j ACCEPT -p icmp -s 192.168.11.0/24') self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "ICMP acceptance rule wasn't added") - regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -m icmp ' - '--icmp-type 8 -j ACCEPT') + regex = re.compile('-A .* -j ACCEPT -p icmp -m icmp --icmp-type 8' + ' -s 192.168.11.0/24') self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "ICMP Echo Request acceptance rule wasn't added") - regex = re.compile('-A .* -p tcp -s 192.168.10.0/24 -m multiport ' - '--dports 80:81 -j ACCEPT') + regex = re.compile('-A .* -j ACCEPT -p tcp -m multiport ' + '--dports 80:81 -s %s' % (src_ip,)) + self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, + "TCP port 80/81 acceptance rule wasn't added") + + regex = re.compile('-A .* -j ACCEPT -p tcp ' + '-m multiport --dports 80:81 -s 192.168.10.0/24') self.assertTrue(len(filter(regex.match, self.out_rules)) > 0, "TCP port 80/81 acceptance rule wasn't added") db.instance_destroy(admin_ctxt, instance_ref['id']) @@ -971,7 +1133,7 @@ class IptablesFirewallTestCase(test.TestCase): ipv6_len = len(self.fw.iptables.ipv6['filter'].rules) inst_ipv4, inst_ipv6 = self.fw.instance_rules(instance_ref, network_info) - self.fw.add_filters_for_instance(instance_ref, network_info) + self.fw.prepare_instance_filter(instance_ref, network_info) ipv4 = self.fw.iptables.ipv4['filter'].rules ipv6 = self.fw.iptables.ipv6['filter'].rules ipv4_network_rules = len(ipv4) - len(inst_ipv4) - ipv4_len @@ -986,7 +1148,7 @@ class IptablesFirewallTestCase(test.TestCase): self.mox.StubOutWithMock(self.fw, 'add_filters_for_instance', use_mock_anything=True) - self.fw.add_filters_for_instance(instance_ref, mox.IgnoreArg()) + self.fw.prepare_instance_filter(instance_ref, mox.IgnoreArg()) self.fw.instances[instance_ref['id']] = instance_ref self.mox.ReplayAll() self.fw.do_refresh_security_group_rules("fake") @@ -1006,11 +1168,12 @@ class IptablesFirewallTestCase(test.TestCase): instance_ref = self._create_instance_ref() _setup_networking(instance_ref['id'], self.test_ip) - self.fw.setup_basic_filtering(instance_ref) - self.fw.prepare_instance_filter(instance_ref) - self.fw.apply_instance_filter(instance_ref) + network_info = _create_network_info() + self.fw.setup_basic_filtering(instance_ref, network_info) + self.fw.prepare_instance_filter(instance_ref, network_info) + self.fw.apply_instance_filter(instance_ref, network_info) original_filter_count = len(fakefilter.filters) - self.fw.unfilter_instance(instance_ref) + self.fw.unfilter_instance(instance_ref, network_info) # should undefine just the instance filter self.assertEqual(original_filter_count - len(fakefilter.filters), 1) @@ -1020,14 +1183,14 @@ class IptablesFirewallTestCase(test.TestCase): def test_provider_firewall_rules(self): # setup basic instance data instance_ref = self._create_instance_ref() - nw_info = _create_network_info(1) _setup_networking(instance_ref['id'], self.test_ip) # FRAGILE: peeks at how the firewall names chains chain_name = 'inst-%s' % instance_ref['id'] # create a firewall via setup_basic_filtering like libvirt_conn.spawn # should have a chain with 0 rules - self.fw.setup_basic_filtering(instance_ref, network_info=nw_info) + network_info = _create_network_info(1) + self.fw.setup_basic_filtering(instance_ref, network_info) self.assertTrue('provider' in self.fw.iptables.ipv4['filter'].chains) rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules if rule.chain == 'provider'] @@ -1057,8 +1220,8 @@ class IptablesFirewallTestCase(test.TestCase): self.assertEqual(2, len(rules)) # create the instance filter and make sure it has a jump rule - self.fw.prepare_instance_filter(instance_ref, network_info=nw_info) - self.fw.apply_instance_filter(instance_ref) + self.fw.prepare_instance_filter(instance_ref, network_info) + self.fw.apply_instance_filter(instance_ref, network_info) inst_rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules if rule.chain == chain_name] jump_rules = [rule for rule in inst_rules if '-j' in rule.rule] @@ -1157,8 +1320,11 @@ class NWFilterTestCase(test.TestCase): 'project_id': 'fake', 'instance_type_id': 1}) - def _create_instance_type(self, params={}): + def _create_instance_type(self, params=None): """Create a test instance""" + if not params: + params = {} + context = self.context.elevated() inst = {} inst['name'] = 'm1.small' @@ -1207,7 +1373,7 @@ class NWFilterTestCase(test.TestCase): def _ensure_all_called(): instance_filter = 'nova-instance-%s-%s' % (instance_ref['name'], - '561212121212') + 'fake') secgroup_filter = 'nova-secgroup-%s' % self.security_group['id'] for required in [secgroup_filter, 'allow-dhcp-server', 'no-arp-spoofing', 'no-ip-spoofing', @@ -1223,9 +1389,10 @@ class NWFilterTestCase(test.TestCase): self.security_group.id) instance = db.instance_get(self.context, inst_id) - self.fw.setup_basic_filtering(instance) - self.fw.prepare_instance_filter(instance) - self.fw.apply_instance_filter(instance) + network_info = _create_network_info() + self.fw.setup_basic_filtering(instance, network_info) + self.fw.prepare_instance_filter(instance, network_info) + self.fw.apply_instance_filter(instance, network_info) _ensure_all_called() self.teardown_security_group() db.instance_destroy(context.get_admin_context(), instance_ref['id']) @@ -1256,11 +1423,12 @@ class NWFilterTestCase(test.TestCase): instance = db.instance_get(self.context, inst_id) _setup_networking(instance_ref['id'], self.test_ip) - self.fw.setup_basic_filtering(instance) - self.fw.prepare_instance_filter(instance) - self.fw.apply_instance_filter(instance) + network_info = _create_network_info() + self.fw.setup_basic_filtering(instance, network_info) + self.fw.prepare_instance_filter(instance, network_info) + self.fw.apply_instance_filter(instance, network_info) original_filter_count = len(fakefilter.filters) - self.fw.unfilter_instance(instance) + self.fw.unfilter_instance(instance, network_info) # should undefine 2 filters: instance and instance-secgroup self.assertEqual(original_filter_count - len(fakefilter.filters), 2) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index c862726ab..ad678714e 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -43,16 +43,21 @@ class MetadataTestCase(test.TestCase): 'reservation_id': 'r-xxxxxxxx', 'user_data': '', 'image_ref': 7, + 'fixed_ips': [], + 'root_device_name': '/dev/sda1', 'hostname': 'test'}) def instance_get(*args, **kwargs): return self.instance + def instance_get_list(*args, **kwargs): + return [self.instance] + def floating_get(*args, **kwargs): return '99.99.99.99' self.stubs.Set(api, 'instance_get', instance_get) - self.stubs.Set(api, 'fixed_ip_get_instance', instance_get) + self.stubs.Set(api, 'instance_get_all_by_filters', instance_get_list) self.stubs.Set(api, 'instance_get_floating_address', floating_get) self.app = metadatarequesthandler.MetadataRequestHandler() diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 2ca8b64f4..c673f5d06 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -210,7 +210,11 @@ class VlanNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(db, 'fixed_ip_update') self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance_and_network') + self.mox.StubOutWithMock(db, 'instance_get') + db.instance_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn({'security_groups': + [{'id': 0}]}) db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('192.168.0.1') @@ -247,6 +251,17 @@ class CommonNetworkTestCase(test.TestCase): return [dict(address='10.0.0.0'), dict(address='10.0.0.1'), dict(address='10.0.0.2')] + def network_get_by_cidr(self, context, cidr): + raise exception.NetworkNotFoundForCidr() + + def network_create_safe(self, context, net): + fakenet = {} + fakenet['id'] = 999 + return fakenet + + def network_get_all(self, context): + raise exception.NoNetworksFound() + def __init__(self): self.db = self.FakeDB() self.deallocate_called = None @@ -254,6 +269,9 @@ class CommonNetworkTestCase(test.TestCase): def deallocate_fixed_ip(self, context, address): self.deallocate_called = address + def fake_create_fixed_ips(self, context, network_id): + return None + def test_remove_fixed_ip_from_instance(self): manager = self.FakeNetworkManager() manager.remove_fixed_ip_from_instance(None, 99, '10.0.0.1') @@ -265,3 +283,165 @@ class CommonNetworkTestCase(test.TestCase): self.assertRaises(exception.FixedIpNotFoundForSpecificInstance, manager.remove_fixed_ip_from_instance, None, 99, 'bad input') + + def test_validate_cidrs(self): + manager = self.FakeNetworkManager() + nets = manager._validate_cidrs(None, '192.168.0.0/24', 1, 256) + self.assertEqual(1, len(nets)) + cidrs = [str(net) for net in nets] + self.assertTrue('192.168.0.0/24' in cidrs) + + def test_validate_cidrs_split_exact_in_half(self): + manager = self.FakeNetworkManager() + nets = manager._validate_cidrs(None, '192.168.0.0/24', 2, 128) + self.assertEqual(2, len(nets)) + cidrs = [str(net) for net in nets] + self.assertTrue('192.168.0.0/25' in cidrs) + self.assertTrue('192.168.0.128/25' in cidrs) + + def test_validate_cidrs_split_cidr_in_use_middle_of_range(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.2.0/24'}]) + self.mox.ReplayAll() + nets = manager._validate_cidrs(None, '192.168.0.0/16', 4, 256) + self.assertEqual(4, len(nets)) + cidrs = [str(net) for net in nets] + exp_cidrs = ['192.168.0.0/24', '192.168.1.0/24', '192.168.3.0/24', + '192.168.4.0/24'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr in cidrs) + self.assertFalse('192.168.2.0/24' in cidrs) + + def test_validate_cidrs_smaller_subnet_in_use(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.2.9/25'}]) + self.mox.ReplayAll() + # ValueError: requested cidr (192.168.2.0/24) conflicts with + # existing smaller cidr + args = [None, '192.168.2.0/24', 1, 256] + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_validate_cidrs_split_smaller_cidr_in_use(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.2.0/25'}]) + self.mox.ReplayAll() + nets = manager._validate_cidrs(None, '192.168.0.0/16', 4, 256) + self.assertEqual(4, len(nets)) + cidrs = [str(net) for net in nets] + exp_cidrs = ['192.168.0.0/24', '192.168.1.0/24', '192.168.3.0/24', + '192.168.4.0/24'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr in cidrs) + self.assertFalse('192.168.2.0/24' in cidrs) + + def test_validate_cidrs_split_smaller_cidr_in_use2(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.2.9/29'}]) + self.mox.ReplayAll() + nets = manager._validate_cidrs(None, '192.168.2.0/24', 3, 32) + self.assertEqual(3, len(nets)) + cidrs = [str(net) for net in nets] + exp_cidrs = ['192.168.2.32/27', '192.168.2.64/27', '192.168.2.96/27'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr in cidrs) + self.assertFalse('192.168.2.0/27' in cidrs) + + def test_validate_cidrs_split_all_in_use(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + in_use = [{'id': 1, 'cidr': '192.168.2.9/29'}, + {'id': 2, 'cidr': '192.168.2.64/26'}, + {'id': 3, 'cidr': '192.168.2.128/26'}] + manager.db.network_get_all(ctxt).AndReturn(in_use) + self.mox.ReplayAll() + args = [None, '192.168.2.0/24', 3, 64] + # ValueError: Not enough subnets avail to satisfy requested num_ + # networks - some subnets in requested range already + # in use + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_validate_cidrs_one_in_use(self): + manager = self.FakeNetworkManager() + args = [None, '192.168.0.0/24', 2, 256] + # ValueError: network_size * num_networks exceeds cidr size + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_validate_cidrs_already_used(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.0.0/24'}]) + self.mox.ReplayAll() + # ValueError: cidr already in use + args = [None, '192.168.0.0/24', 1, 256] + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_validate_cidrs_too_many(self): + manager = self.FakeNetworkManager() + args = [None, '192.168.0.0/24', 200, 256] + # ValueError: Not enough subnets avail to satisfy requested + # num_networks + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_validate_cidrs_split_partial(self): + manager = self.FakeNetworkManager() + nets = manager._validate_cidrs(None, '192.168.0.0/16', 2, 256) + returned_cidrs = [str(net) for net in nets] + self.assertTrue('192.168.0.0/24' in returned_cidrs) + self.assertTrue('192.168.1.0/24' in returned_cidrs) + + def test_validate_cidrs_conflict_existing_supernet(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + fakecidr = [{'id': 1, 'cidr': '192.168.0.0/8'}] + manager.db.network_get_all(ctxt).AndReturn(fakecidr) + self.mox.ReplayAll() + args = [None, '192.168.0.0/24', 1, 256] + # ValueError: requested cidr (192.168.0.0/24) conflicts + # with existing supernet + self.assertRaises(ValueError, manager._validate_cidrs, *args) + + def test_create_networks(self): + cidr = '192.168.0.0/24' + manager = self.FakeNetworkManager() + self.stubs.Set(manager, '_create_fixed_ips', + self.fake_create_fixed_ips) + args = [None, 'foo', cidr, None, 1, 256, 'fd00::/48', None, None, + None] + result = manager.create_networks(*args) + self.assertEqual(manager.create_networks(*args), None) + + def test_create_networks_cidr_already_used(self): + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_get_all') + ctxt = mox.IgnoreArg() + fakecidr = [{'id': 1, 'cidr': '192.168.0.0/24'}] + manager.db.network_get_all(ctxt).AndReturn(fakecidr) + self.mox.ReplayAll() + args = [None, 'foo', '192.168.0.0/24', None, 1, 256, + 'fd00::/48', None, None, None] + self.assertRaises(ValueError, manager.create_networks, *args) + + def test_create_networks_many(self): + cidr = '192.168.0.0/16' + manager = self.FakeNetworkManager() + self.stubs.Set(manager, '_create_fixed_ips', + self.fake_create_fixed_ips) + args = [None, 'foo', cidr, None, 10, 256, 'fd00::/48', None, None, + None] + self.assertEqual(manager.create_networks(*args), None) diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py new file mode 100644 index 000000000..9c6563f14 --- /dev/null +++ b/nova/tests/test_nova_manage.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# Copyright 2011 Ilya Alekseyev +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import sys + +TOPDIR = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir)) +NOVA_MANAGE_PATH = os.path.join(TOPDIR, 'bin', 'nova-manage') + +sys.dont_write_bytecode = True +import imp +nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH) +sys.dont_write_bytecode = False + +import netaddr +from nova import context +from nova import db +from nova import flags +from nova import test + +FLAGS = flags.FLAGS + + +class FixedIpCommandsTestCase(test.TestCase): + def setUp(self): + super(FixedIpCommandsTestCase, self).setUp() + cidr = '10.0.0.0/24' + net = netaddr.IPNetwork(cidr) + net_info = {'bridge': 'fakebr', + 'bridge_interface': 'fakeeth', + 'dns': FLAGS.flat_network_dns, + 'cidr': cidr, + 'netmask': str(net.netmask), + 'gateway': str(net[1]), + 'broadcast': str(net.broadcast), + 'dhcp_start': str(net[2])} + self.network = db.network_create_safe(context.get_admin_context(), + net_info) + num_ips = len(net) + for index in range(num_ips): + address = str(net[index]) + reserved = (index == 1 or index == 2) + db.fixed_ip_create(context.get_admin_context(), + {'network_id': self.network['id'], + 'address': address, + 'reserved': reserved}) + self.commands = nova_manage.FixedIpCommands() + + def tearDown(self): + db.network_delete_safe(context.get_admin_context(), self.network['id']) + super(FixedIpCommandsTestCase, self).tearDown() + + def test_reserve(self): + self.commands.reserve('10.0.0.100') + address = db.fixed_ip_get_by_address(context.get_admin_context(), + '10.0.0.100') + self.assertEqual(address['reserved'], True) + + def test_unreserve(self): + db.fixed_ip_update(context.get_admin_context(), '10.0.0.100', + {'reserved': True}) + self.commands.unreserve('10.0.0.100') + address = db.fixed_ip_get_by_address(context.get_admin_context(), + '10.0.0.100') + self.assertEqual(address['reserved'], False) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py new file mode 100644 index 000000000..8ca203442 --- /dev/null +++ b/nova/tests/test_skip_examples.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import test + + +class ExampleSkipTestCase(test.TestCase): + test_counter = 0 + + @test.skip_test("Example usage of @test.skip_test()") + def test_skip_test_example(self): + self.fail("skip_test failed to work properly.") + + @test.skip_if(True, "Example usage of @test.skip_if()") + def test_skip_if_example(self): + self.fail("skip_if failed to work properly.") + + @test.skip_unless(False, "Example usage of @test.skip_unless()") + def test_skip_unless_example(self): + self.fail("skip_unless failed to work properly.") + + @test.skip_if(False, "This test case should never be skipped.") + def test_001_increase_test_counter(self): + ExampleSkipTestCase.test_counter += 1 + + @test.skip_unless(True, "This test case should never be skipped.") + def test_002_increase_test_counter(self): + ExampleSkipTestCase.test_counter += 1 + + def test_003_verify_test_counter(self): + self.assertEquals(ExampleSkipTestCase.test_counter, 2, + "Tests were not skipped appropriately") diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py new file mode 100644 index 000000000..388f075af --- /dev/null +++ b/nova/tests/test_virt.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import flags +from nova import test +from nova.virt import driver + +FLAGS = flags.FLAGS + + +class TestVirtDriver(test.TestCase): + def test_block_device(self): + swap = {'device_name': '/dev/sdb', + 'swap_size': 1} + ephemerals = [{'num': 0, + 'virtual_name': 'ephemeral0', + 'device_name': '/dev/sdc1', + 'size': 1}] + block_device_mapping = [{'mount_device': '/dev/sde', + 'device_path': 'fake_device'}] + block_device_info = { + 'root_device_name': '/dev/sda', + 'swap': swap, + 'ephemerals': ephemerals, + 'block_device_mapping': block_device_mapping} + + empty_block_device_info = {} + + self.assertEqual( + driver.block_device_info_get_root(block_device_info), '/dev/sda') + self.assertEqual( + driver.block_device_info_get_root(empty_block_device_info), None) + self.assertEqual( + driver.block_device_info_get_root(None), None) + + self.assertEqual( + driver.block_device_info_get_swap(block_device_info), swap) + self.assertEqual(driver.block_device_info_get_swap( + empty_block_device_info)['device_name'], None) + self.assertEqual(driver.block_device_info_get_swap( + empty_block_device_info)['swap_size'], 0) + self.assertEqual( + driver.block_device_info_get_swap({'swap': None})['device_name'], + None) + self.assertEqual( + driver.block_device_info_get_swap({'swap': None})['swap_size'], + 0) + self.assertEqual( + driver.block_device_info_get_swap(None)['device_name'], None) + self.assertEqual( + driver.block_device_info_get_swap(None)['swap_size'], 0) + + self.assertEqual( + driver.block_device_info_get_ephemerals(block_device_info), + ephemerals) + self.assertEqual( + driver.block_device_info_get_ephemerals(empty_block_device_info), + []) + self.assertEqual( + driver.block_device_info_get_ephemerals(None), + []) + + def test_swap_is_usable(self): + self.assertFalse(driver.swap_is_usable(None)) + self.assertFalse(driver.swap_is_usable({'device_name': None})) + self.assertFalse(driver.swap_is_usable({'device_name': '/dev/sdb', + 'swap_size': 0})) + self.assertTrue(driver.swap_is_usable({'device_name': '/dev/sdb', + 'swap_size': 1})) diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index c0f89601f..7888b6b0b 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -414,8 +414,9 @@ class ISCSITestCase(DriverTestCase): self.mox.StubOutWithMock(self.volume.driver, '_execute') for i in volume_id_list: tid = db.volume_get_iscsi_target_num(self.context, i) - self.volume.driver._execute("sudo", "ietadm", "--op", "show", - "--tid=%(tid)d" % locals()) + self.volume.driver._execute("ietadm", "--op", "show", + "--tid=%(tid)d" % locals(), + run_as_root=True) self.stream.truncate(0) self.mox.ReplayAll() @@ -433,8 +434,9 @@ class ISCSITestCase(DriverTestCase): # the first vblade process isn't running tid = db.volume_get_iscsi_target_num(self.context, volume_id_list[0]) self.mox.StubOutWithMock(self.volume.driver, '_execute') - self.volume.driver._execute("sudo", "ietadm", "--op", "show", - "--tid=%(tid)d" % locals()).AndRaise( + self.volume.driver._execute("ietadm", "--op", "show", + "--tid=%(tid)d" % locals(), + run_as_root=True).AndRaise( exception.ProcessExecutionError()) self.mox.ReplayAll() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 39ab23d9b..2f0559366 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -519,6 +519,11 @@ class XenAPIVMTestCase(test.TestCase): os_type="windows", architecture="i386") self.check_vm_params_for_windows() + def test_spawn_iso_glance(self): + self._test_spawn(glance_stubs.FakeGlance.IMAGE_ISO, None, None, + os_type="windows", architecture="i386") + self.check_vm_params_for_windows() + def test_spawn_glance(self): self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE, glance_stubs.FakeGlance.IMAGE_KERNEL, @@ -548,8 +553,8 @@ class XenAPIVMTestCase(test.TestCase): return '', '' fake_utils.fake_execute_set_repliers([ - # Capture the sudo tee .../etc/network/interfaces command - (r'(sudo\s+)?tee.*interfaces', _tee_handler), + # Capture the tee .../etc/network/interfaces command + (r'tee.*interfaces', _tee_handler), ]) self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE, glance_stubs.FakeGlance.IMAGE_KERNEL, @@ -592,9 +597,9 @@ class XenAPIVMTestCase(test.TestCase): return '', '' fake_utils.fake_execute_set_repliers([ - (r'(sudo\s+)?mount', _mount_handler), - (r'(sudo\s+)?umount', _umount_handler), - (r'(sudo\s+)?tee.*interfaces', _tee_handler)]) + (r'mount', _mount_handler), + (r'umount', _umount_handler), + (r'tee.*interfaces', _tee_handler)]) self._test_spawn(1, 2, 3, check_injection=True) # tee must not run in this case, where an injection-capable @@ -654,6 +659,24 @@ class XenAPIVMTestCase(test.TestCase): # Ensure that it will not unrescue a non-rescued instance. self.assertRaises(Exception, conn.unrescue, instance, None) + def test_revert_migration(self): + instance = self._create_instance() + + class VMOpsMock(): + + def __init__(self): + self.revert_migration_called = False + + def revert_migration(self, instance): + self.revert_migration_called = True + + stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) + + conn = xenapi_conn.get_connection(False) + conn._vmops = VMOpsMock() + conn.revert_migration(instance) + self.assertTrue(conn._vmops.revert_migration_called) + def _create_instance(self, instance_id=1, spawn=True): """Creates and spawns a test instance.""" stubs.stubout_loopingcall_start(self.stubs) @@ -767,6 +790,52 @@ class XenAPIMigrateInstance(test.TestCase): conn = xenapi_conn.get_connection(False) conn.migrate_disk_and_power_off(instance, '127.0.0.1') + def test_revert_migrate(self): + instance = db.instance_create(self.context, self.values) + self.called = False + self.fake_vm_start_called = False + self.fake_revert_migration_called = False + + def fake_vm_start(*args, **kwargs): + self.fake_vm_start_called = True + + def fake_vdi_resize(*args, **kwargs): + self.called = True + + def fake_revert_migration(*args, **kwargs): + self.fake_revert_migration_called = True + + self.stubs.Set(stubs.FakeSessionForMigrationTests, + "VDI_resize_online", fake_vdi_resize) + self.stubs.Set(vmops.VMOps, '_start', fake_vm_start) + self.stubs.Set(vmops.VMOps, 'revert_migration', fake_revert_migration) + + stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) + stubs.stubout_loopingcall_start(self.stubs) + conn = xenapi_conn.get_connection(False) + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] + conn.finish_migration(self.context, instance, + dict(base_copy='hurr', cow='durr'), + network_info, resize_instance=True) + self.assertEqual(self.called, True) + self.assertEqual(self.fake_vm_start_called, True) + + conn.revert_migration(instance) + self.assertEqual(self.fake_revert_migration_called, True) + def test_finish_migrate(self): instance = db.instance_create(self.context, self.values) self.called = False diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py index a943fee27..9efa23015 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -18,7 +18,6 @@ Tests For ZoneManager import datetime import mox -import novaclient from nova import context from nova import db diff --git a/nova/utils.py b/nova/utils.py index 1e2dbebb1..7276b6bd5 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -28,6 +28,7 @@ import netaddr import os import random import re +import shlex import socket import struct import sys @@ -131,40 +132,42 @@ def execute(*cmd, **kwargs): :cmd Passed to subprocess.Popen. :process_input Send to opened process. - :addl_env Added to the processes env. :check_exit_code Defaults to 0. Raise exception.ProcessExecutionError unless program exits with this code. :delay_on_retry True | False. Defaults to True. If set to True, wait a short amount of time before retrying. :attempts How many times to retry cmd. + :run_as_root True | False. Defaults to False. If set to True, + the command is prefixed by the command specified + in the root_helper FLAG. :raises exception.Error on receiving unknown arguments :raises exception.ProcessExecutionError """ process_input = kwargs.pop('process_input', None) - addl_env = kwargs.pop('addl_env', None) check_exit_code = kwargs.pop('check_exit_code', 0) delay_on_retry = kwargs.pop('delay_on_retry', True) attempts = kwargs.pop('attempts', 1) + run_as_root = kwargs.pop('run_as_root', False) if len(kwargs): raise exception.Error(_('Got unknown keyword args ' 'to utils.execute: %r') % kwargs) + + if run_as_root: + cmd = shlex.split(FLAGS.root_helper) + list(cmd) cmd = map(str, cmd) while attempts > 0: attempts -= 1 try: LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd)) - env = os.environ.copy() - if addl_env: - env.update(addl_env) _PIPE = subprocess.PIPE # pylint: disable=E1101 obj = subprocess.Popen(cmd, stdin=_PIPE, stdout=_PIPE, stderr=_PIPE, - env=env) + close_fds=True) result = None if process_input is not None: result = obj.communicate(process_input) @@ -239,7 +242,7 @@ def abspath(s): def novadir(): import nova - return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0] + return os.path.abspath(nova.__file__).split('nova/__init__.py')[0] def default_flagfile(filename='nova.conf', args=None): diff --git a/nova/virt/disk.py b/nova/virt/disk.py index f8aea1f34..19f3ec185 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -73,7 +73,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): try: if not partition is None: # create partition - out, err = utils.execute('sudo', 'kpartx', '-a', device) + out, err = utils.execute('kpartx', '-a', device, run_as_root=True) if err: raise exception.Error(_('Failed to load partition: %s') % err) mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1], @@ -90,14 +90,14 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): mapped_device) # Configure ext2fs so that it doesn't auto-check every N boots - out, err = utils.execute('sudo', 'tune2fs', - '-c', 0, '-i', 0, mapped_device) + out, err = utils.execute('tune2fs', '-c', 0, '-i', 0, + mapped_device, run_as_root=True) tmpdir = tempfile.mkdtemp() try: # mount loopback to dir - out, err = utils.execute( - 'sudo', 'mount', mapped_device, tmpdir) + out, err = utils.execute('mount', mapped_device, tmpdir, + run_as_root=True) if err: raise exception.Error(_('Failed to mount filesystem: %s') % err) @@ -106,14 +106,14 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): inject_data_into_fs(tmpdir, key, net, utils.execute) finally: # unmount device - utils.execute('sudo', 'umount', mapped_device) + utils.execute('umount', mapped_device, run_as_root=True) finally: # remove temporary directory utils.execute('rmdir', tmpdir) finally: if not partition is None: # remove partitions - utils.execute('sudo', 'kpartx', '-d', device) + utils.execute('kpartx', '-d', device, run_as_root=True) finally: _unlink_device(device, nbd) @@ -128,7 +128,7 @@ def setup_container(image, container_dir=None, nbd=False): """ try: device = _link_device(image, nbd) - utils.execute('sudo', 'mount', device, container_dir) + utils.execute('mount', device, container_dir, run_as_root=True) except Exception, exn: LOG.exception(_('Failed to mount filesystem: %s'), exn) _unlink_device(device, nbd) @@ -144,9 +144,9 @@ def destroy_container(target, instance, nbd=False): """ try: container_dir = '%s/rootfs' % target - utils.execute('sudo', 'umount', container_dir) + utils.execute('umount', container_dir, run_as_root=True) finally: - out, err = utils.execute('sudo', 'losetup', '-a') + out, err = utils.execute('losetup', '-a', run_as_root=True) for loop in out.splitlines(): if instance['name'] in loop: device = loop.split(loop, ':') @@ -157,7 +157,7 @@ def _link_device(image, nbd): """Link image to device using loopback or nbd""" if nbd: device = _allocate_device() - utils.execute('sudo', 'qemu-nbd', '-c', device, image) + utils.execute('qemu-nbd', '-c', device, image, run_as_root=True) # NOTE(vish): this forks into another process, so give it a chance # to set up before continuuing for i in xrange(FLAGS.timeout_nbd): @@ -166,7 +166,8 @@ def _link_device(image, nbd): time.sleep(1) raise exception.Error(_('nbd device %s did not show up') % device) else: - out, err = utils.execute('sudo', 'losetup', '--find', '--show', image) + out, err = utils.execute('losetup', '--find', '--show', image, + run_as_root=True) if err: raise exception.Error(_('Could not attach image to loopback: %s') % err) @@ -176,10 +177,10 @@ def _link_device(image, nbd): def _unlink_device(device, nbd): """Unlink image from device using loopback or nbd""" if nbd: - utils.execute('sudo', 'qemu-nbd', '-d', device) + utils.execute('qemu-nbd', '-d', device, run_as_root=True) _free_device(device) else: - utils.execute('sudo', 'losetup', '--detach', device) + utils.execute('losetup', '--detach', device, run_as_root=True) _DEVICES = ['/dev/nbd%s' % i for i in xrange(FLAGS.max_nbd_devices)] @@ -220,12 +221,12 @@ def _inject_key_into_fs(key, fs, execute=None): fs is the path to the base of the filesystem into which to inject the key. """ sshdir = os.path.join(fs, 'root', '.ssh') - utils.execute('sudo', 'mkdir', '-p', sshdir) # existing dir doesn't matter - utils.execute('sudo', 'chown', 'root', sshdir) - utils.execute('sudo', 'chmod', '700', sshdir) + utils.execute('mkdir', '-p', sshdir, run_as_root=True) + utils.execute('chown', 'root', sshdir, run_as_root=True) + utils.execute('chmod', '700', sshdir, run_as_root=True) keyfile = os.path.join(sshdir, 'authorized_keys') - utils.execute('sudo', 'tee', '-a', keyfile, - process_input='\n' + key.strip() + '\n') + utils.execute('tee', '-a', keyfile, + process_input='\n' + key.strip() + '\n', run_as_root=True) def _inject_net_into_fs(net, fs, execute=None): @@ -234,8 +235,8 @@ def _inject_net_into_fs(net, fs, execute=None): net is the contents of /etc/network/interfaces. """ netdir = os.path.join(os.path.join(fs, 'etc'), 'network') - utils.execute('sudo', 'mkdir', '-p', netdir) # existing dir doesn't matter - utils.execute('sudo', 'chown', 'root:root', netdir) - utils.execute('sudo', 'chmod', 755, netdir) + utils.execute('mkdir', '-p', netdir, run_as_root=True) + utils.execute('chown', 'root:root', netdir, run_as_root=True) + utils.execute('chmod', 755, netdir, run_as_root=True) netfile = os.path.join(netdir, 'interfaces') - utils.execute('sudo', 'tee', netfile, process_input=net) + utils.execute('tee', netfile, process_input=net, run_as_root=True) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 4f3cfefad..20af2666d 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -32,6 +32,33 @@ class InstanceInfo(object): self.state = state +def block_device_info_get_root(block_device_info): + block_device_info = block_device_info or {} + return block_device_info.get('root_device_name') + + +def block_device_info_get_swap(block_device_info): + block_device_info = block_device_info or {} + return block_device_info.get('swap') or {'device_name': None, + 'swap_size': 0} + + +def swap_is_usable(swap): + return swap and swap['device_name'] and swap['swap_size'] > 0 + + +def block_device_info_get_ephemerals(block_device_info): + block_device_info = block_device_info or {} + ephemerals = block_device_info.get('ephemerals') or [] + return ephemerals + + +def block_device_info_get_mapping(block_device_info): + block_device_info = block_device_info or {} + block_device_mapping = block_device_info.get('block_device_mapping') or [] + return block_device_mapping + + class ComputeDriver(object): """Base class for compute drivers. @@ -65,8 +92,8 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() - def spawn(self, context, instance, network_info, - block_device_mapping=None): + def spawn(self, context, instance, + network_info=None, block_device_info=None): """Launch a VM for the specified instance""" raise NotImplementedError() @@ -225,7 +252,7 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token pass - def ensure_filtering_rules_for_instance(self, instance_ref): + def ensure_filtering_rules_for_instance(self, instance_ref, network_info): """Setting up filtering rules and waiting for its completion. To migrate an instance, filtering rules to hypervisors @@ -282,6 +309,10 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + raise NotImplementedError() + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" # TODO(Vek): Need to pass context in for access to auth_token diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 80abcc644..dc0628772 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -129,8 +129,8 @@ class FakeConnection(driver.ComputeDriver): info_list.append(self._map_to_instance_info(instance)) return info_list - def spawn(self, context, instance, network_info, - block_device_mapping=None): + def spawn(self, context, instance, + network_info=None, block_device_info=None): """ Create a new instance/VM/domain on the virtualization platform. @@ -294,7 +294,7 @@ class FakeConnection(driver.ComputeDriver): """ pass - def destroy(self, instance, network_info): + def destroy(self, instance, network_info, cleanup=True): key = instance.name if key in self.instances: del self.instances[key] @@ -487,16 +487,16 @@ class FakeConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') - def ensure_filtering_rules_for_instance(self, instance_ref): + def ensure_filtering_rules_for_instance(self, instance_ref, network_info): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') def live_migration(self, context, instance_ref, dest, - post_method, recover_method): + post_method, recover_method, block_migration=False): """This method is supported only by libvirt.""" return - def unfilter_instance(self, instance_ref, network_info=None): + def unfilter_instance(self, instance_ref, network_info): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') @@ -512,6 +512,10 @@ class FakeConnection(driver.ComputeDriver): """Return fake Host Status of ram, disk, network.""" return self.host_status + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 3428a7fc1..03a78db1f 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -138,8 +138,8 @@ class HyperVConnection(driver.ComputeDriver): return instance_infos - def spawn(self, context, instance, network_info, - block_device_mapping=None): + def spawn(self, context, instance, + network_info=None, block_device_info=None): """ Create a new VM and start it.""" vm = self._lookup(instance.name) if vm is not None: @@ -374,7 +374,7 @@ class HyperVConnection(driver.ComputeDriver): raise exception.InstanceNotFound(instance_id=instance.id) self._set_vm_state(instance.name, 'Reboot') - def destroy(self, instance, network_info): + def destroy(self, instance, network_info, cleanup=True): """Destroy the VM. Also destroy the associated VHD disk files""" LOG.debug(_("Got request to destroy vm %s"), instance.name) vm = self._lookup(instance.name) @@ -499,6 +499,10 @@ class HyperVConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index a75636390..210e2b0fb 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -3,24 +3,22 @@ <memory>${memory_kb}</memory> <os> #if $type == 'lxc' - #set $disk_prefix = '' #set $disk_bus = '' <type>exe</type> <init>/sbin/init</init> #else if $type == 'uml' - #set $disk_prefix = 'ubd' #set $disk_bus = 'uml' <type>uml</type> <kernel>/usr/bin/linux</kernel> - <root>/dev/ubda</root> + #set $root_device_name = $getVar('root_device_name', '/dev/ubda') + <root>${root_device_name}</root> #else #if $type == 'xen' - #set $disk_prefix = 'sd' #set $disk_bus = 'scsi' <type>linux</type> - <root>/dev/xvda</root> + #set $root_device_name = $getVar('root_device_name', '/dev/xvda') + <root>${root_device_name}</root> #else - #set $disk_prefix = 'vd' #set $disk_bus = 'virtio' <type>hvm</type> #end if @@ -33,7 +31,8 @@ #if $type == 'xen' <cmdline>ro</cmdline> #else - <cmdline>root=/dev/vda console=ttyS0</cmdline> + #set $root_device_name = $getVar('root_device_name', '/dev/vda') + <cmdline>root=${root_device_name} console=ttyS0</cmdline> #end if #if $getVar('ramdisk', None) <initrd>${ramdisk}</initrd> @@ -71,16 +70,30 @@ <disk type='file'> <driver type='${driver_type}'/> <source file='${basepath}/disk'/> - <target dev='${disk_prefix}a' bus='${disk_bus}'/> + <target dev='${root_device}' bus='${disk_bus}'/> </disk> #end if - #if $getVar('local', False) + #if $getVar('local_device', False) <disk type='file'> <driver type='${driver_type}'/> <source file='${basepath}/disk.local'/> - <target dev='${disk_prefix}b' bus='${disk_bus}'/> + <target dev='${local_device}' bus='${disk_bus}'/> </disk> #end if + #for $eph in $ephemerals + <disk type='block'> + <driver type='${driver_type}'/> + <source dev='${basepath}/${eph.device_path}'/> + <target dev='${eph.device}' bus='${disk_bus}'/> + </disk> + #end for + #if $getVar('swap_device', False) + <disk type='file'> + <driver type='${driver_type}'/> + <source file='${basepath}/disk.swap'/> + <target dev='${swap_device}' bus='${disk_bus}'/> + </disk> + #end if #for $vol in $volumes <disk type='${vol.type}'> <driver type='raw'/> diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0b93a8399..2b17e244a 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -32,7 +32,7 @@ Supports KVM, LXC, QEMU, UML, and XEN. :rescue_kernel_id: Rescue aki image (default: aki-rescue). :rescue_ramdisk_id: Rescue ari image (default: ari-rescue). :injected_network_template: Template file for injected network -:allow_project_net_traffic: Whether to allow in project network traffic +:allow_same_net_traffic: Whether to allow in project network traffic """ @@ -43,7 +43,6 @@ import os import random import re import shutil -import subprocess import sys import tempfile import time @@ -54,6 +53,7 @@ from xml.etree import ElementTree from eventlet import greenthread from eventlet import tpool +from nova import block_device from nova import context as nova_context from nova import db from nova import exception @@ -96,9 +96,9 @@ flags.DEFINE_string('libvirt_uri', '', 'Override the default libvirt URI (which is dependent' ' on libvirt_type)') -flags.DEFINE_bool('allow_project_net_traffic', +flags.DEFINE_bool('allow_same_net_traffic', True, - 'Whether to allow in project network traffic') + 'Whether to allow network traffic from same network') flags.DEFINE_bool('use_cow_images', True, 'Whether to use cow images') @@ -117,6 +117,10 @@ flags.DEFINE_string('live_migration_uri', flags.DEFINE_string('live_migration_flag', "VIR_MIGRATE_UNDEFINE_SOURCE, VIR_MIGRATE_PEER2PEER", 'Define live migration behavior.') +flags.DEFINE_string('block_migration_flag', + "VIR_MIGRATE_UNDEFINE_SOURCE, VIR_MIGRATE_PEER2PEER, " + "VIR_MIGRATE_NON_SHARED_INC", + 'Define block migration behavior.') flags.DEFINE_integer('live_migration_bandwidth', 0, 'Define live migration behavior') flags.DEFINE_string('qemu_img', 'qemu-img', @@ -151,8 +155,8 @@ def _late_load_cheetah(): Template = t.Template -def _strip_dev(mount_path): - return re.sub(r'^/dev/', '', mount_path) +def _get_eph_disk(ephemeral): + return 'disk.eph' + str(ephemeral['num']) class LibvirtConnection(driver.ComputeDriver): @@ -392,9 +396,7 @@ class LibvirtConnection(driver.ComputeDriver): nova.image.get_image_service(image_href) snapshot = snapshot_image_service.show(context, snapshot_image_id) - metadata = {'disk_format': base['disk_format'], - 'container_format': base['container_format'], - 'is_public': False, + metadata = {'is_public': False, 'status': 'active', 'name': snapshot['name'], 'properties': { @@ -409,6 +411,12 @@ class LibvirtConnection(driver.ComputeDriver): arch = base['properties']['architecture'] metadata['properties']['architecture'] = arch + if 'disk_format' in base: + metadata['disk_format'] = base['disk_format'] + + if 'container_format' in base: + metadata['container_format'] = base['container_format'] + # Make the snapshot snapshot_name = uuid.uuid4().hex snapshot_xml = """ @@ -459,18 +467,18 @@ class LibvirtConnection(driver.ComputeDriver): """ virt_dom = self._conn.lookupByName(instance['name']) # NOTE(itoumsn): Use XML delived from the running instance - # instead of using to_xml(instance). This is almost the ultimate - # stupid workaround. + # instead of using to_xml(instance, network_info). This is almost + # the ultimate stupid workaround. xml = virt_dom.XMLDesc(0) # NOTE(itoumsn): self.shutdown() and wait instead of self.destroy() is # better because we cannot ensure flushing dirty buffers # in the guest OS. But, in case of KVM, shutdown() does not work... self.destroy(instance, network_info, cleanup=False) self.plug_vifs(instance, network_info) - self.firewall_driver.setup_basic_filtering(instance) - self.firewall_driver.prepare_instance_filter(instance) + self.firewall_driver.setup_basic_filtering(instance, network_info) + self.firewall_driver.prepare_instance_filter(instance, network_info) self._create_new_domain(xml) - self.firewall_driver.apply_instance_filter(instance) + self.firewall_driver.apply_instance_filter(instance, network_info) def _wait_for_reboot(): """Called at an interval until the VM is running again.""" @@ -527,7 +535,7 @@ class LibvirtConnection(driver.ComputeDriver): """ self.destroy(instance, network_info, cleanup=False) - xml = self.to_xml(instance, rescue=True) + xml = self.to_xml(instance, network_info, rescue=True) rescue_images = {'image_id': FLAGS.rescue_image_id, 'kernel_id': FLAGS.rescue_kernel_id, 'ramdisk_id': FLAGS.rescue_ramdisk_id} @@ -571,17 +579,16 @@ class LibvirtConnection(driver.ComputeDriver): # for xenapi(tr3buchet) @exception.wrap_exception() def spawn(self, context, instance, network_info, - block_device_mapping=None): - xml = self.to_xml(instance, False, network_info=network_info, - block_device_mapping=block_device_mapping) - block_device_mapping = block_device_mapping or [] + block_device_info=None): + xml = self.to_xml(instance, network_info, False, + block_device_info=block_device_info) self.firewall_driver.setup_basic_filtering(instance, network_info) self.firewall_driver.prepare_instance_filter(instance, network_info) self._create_image(context, instance, xml, network_info=network_info, - block_device_mapping=block_device_mapping) + block_device_info=block_device_info) domain = self._create_new_domain(xml) LOG.debug(_("instance %s: is running"), instance['name']) - self.firewall_driver.apply_instance_filter(instance) + self.firewall_driver.apply_instance_filter(instance, network_info) def _wait_for_boot(): """Called at an interval until the VM is running.""" @@ -608,9 +615,10 @@ class LibvirtConnection(driver.ComputeDriver): if virsh_output.startswith('/dev/'): LOG.info(_("cool, it's a device")) - out, err = utils.execute('sudo', 'dd', + out, err = utils.execute('dd', "if=%s" % virsh_output, 'iflag=nonblock', + run_as_root=True, check_exit_code=False) return out else: @@ -633,7 +641,7 @@ class LibvirtConnection(driver.ComputeDriver): console_log = os.path.join(FLAGS.instances_path, instance['name'], 'console.log') - utils.execute('sudo', 'chown', os.getuid(), console_log) + utils.execute('chown', os.getuid(), console_log, run_as_root=True) if FLAGS.libvirt_type == 'xen': # Xen is special @@ -681,10 +689,10 @@ class LibvirtConnection(driver.ComputeDriver): ajaxterm_cmd = 'sudo socat - %s' \ % get_pty_for_instance(instance['name']) - cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \ - % (utils.novadir(), ajaxterm_cmd, token, port) + cmd = ['%s/tools/ajaxterm/ajaxterm.py' % utils.novadir(), + '--command', ajaxterm_cmd, '-t', token, '-p', port] - subprocess.Popen(cmd, shell=True) + utils.execute(cmd) return {'token': token, 'host': host, 'port': port} def get_host_ip_addr(self): @@ -723,6 +731,7 @@ class LibvirtConnection(driver.ComputeDriver): If cow is True, it will make a CoW image instead of a copy. """ + if not os.path.exists(target): base_dir = os.path.join(FLAGS.instances_path, '_base') if not os.path.exists(base_dir): @@ -755,11 +764,14 @@ class LibvirtConnection(driver.ComputeDriver): utils.execute('truncate', target, '-s', "%dG" % local_gb) # TODO(vish): should we format disk by default? + def _create_swap(self, target, swap_gb): + """Create a swap file of specified size""" + self._create_local(target, swap_gb) + utils.execute('mkswap', target) + def _create_image(self, context, inst, libvirt_xml, suffix='', disk_images=None, network_info=None, - block_device_mapping=None): - block_device_mapping = block_device_mapping or [] - + block_device_info=None): if not suffix: suffix = '' @@ -818,8 +830,8 @@ class LibvirtConnection(driver.ComputeDriver): size = None root_fname += "_sm" - if not self._volume_in_mapping(self.root_mount_device, - block_device_mapping): + if not self._volume_in_mapping(self.default_root_device, + block_device_info): self._cache_image(fn=self._fetch_image, context=context, target=basepath('disk'), @@ -830,13 +842,38 @@ class LibvirtConnection(driver.ComputeDriver): project_id=inst['project_id'], size=size) - if inst_type['local_gb'] and not self._volume_in_mapping( - self.local_mount_device, block_device_mapping): + local_gb = inst['local_gb'] + if local_gb and not self._volume_in_mapping( + self.default_local_device, block_device_info): self._cache_image(fn=self._create_local, target=basepath('disk.local'), - fname="local_%s" % inst_type['local_gb'], + fname="local_%s" % local_gb, cow=FLAGS.use_cow_images, - local_gb=inst_type['local_gb']) + local_gb=local_gb) + + for eph in driver.block_device_info_get_ephemerals(block_device_info): + self._cache_image(fn=self._create_local, + target=basepath(_get_eph_disk(eph)), + fname="local_%s" % eph['size'], + cow=FLAGS.use_cow_images, + local_gb=eph['size']) + + swap_gb = 0 + + swap = driver.block_device_info_get_swap(block_device_info) + if driver.swap_is_usable(swap): + swap_gb = swap['swap_size'] + elif (inst_type['swap'] > 0 and + not self._volume_in_mapping(self.default_swap_device, + block_device_info)): + swap_gb = inst_type['swap'] + + if swap_gb > 0: + self._cache_image(fn=self._create_swap, + target=basepath('disk.swap'), + fname="swap_%s" % swap_gb, + cow=FLAGS.use_cow_images, + swap_gb=swap_gb) # For now, we assume that if we're not using a kernel, we're using a # partitioned disk image where the target partition is the first @@ -880,7 +917,7 @@ class LibvirtConnection(driver.ComputeDriver): 'netmask': netmask, 'gateway': mapping['gateway'], 'broadcast': mapping['broadcast'], - 'dns': mapping['dns'], + 'dns': ' '.join(mapping['dns']), 'address_v6': address_v6, 'gateway6': gateway_v6, 'netmask_v6': netmask_v6} @@ -915,18 +952,37 @@ class LibvirtConnection(driver.ComputeDriver): ' data into image %(img_id)s (%(e)s)') % locals()) if FLAGS.libvirt_type == 'uml': - utils.execute('sudo', 'chown', 'root', basepath('disk')) - - root_mount_device = 'vda' # FIXME for now. it's hard coded. - local_mount_device = 'vdb' # FIXME for now. it's hard coded. - - def _volume_in_mapping(self, mount_device, block_device_mapping): - mount_device_ = _strip_dev(mount_device) - for vol in block_device_mapping: - vol_mount_device = _strip_dev(vol['mount_device']) - if vol_mount_device == mount_device_: - return True - return False + utils.execute('chown', 'root', basepath('disk'), run_as_root=True) + + if FLAGS.libvirt_type == 'uml': + _disk_prefix = 'ubd' + elif FLAGS.libvirt_type == 'xen': + _disk_prefix = 'sd' + elif FLAGS.libvirt_type == 'lxc': + _disk_prefix = '' + else: + _disk_prefix = 'vd' + + default_root_device = _disk_prefix + 'a' + default_local_device = _disk_prefix + 'b' + default_swap_device = _disk_prefix + 'c' + + def _volume_in_mapping(self, mount_device, block_device_info): + block_device_list = [block_device.strip_dev(vol['mount_device']) + for vol in + driver.block_device_info_get_mapping( + block_device_info)] + swap = driver.block_device_info_get_swap(block_device_info) + if driver.swap_is_usable(swap): + block_device_list.append( + block_device.strip_dev(swap['device_name'])) + block_device_list += [block_device.strip_dev(ephemeral['device_name']) + for ephemeral in + driver.block_device_info_get_ephemerals( + block_device_info)] + + LOG.debug(_("block_device_list %s"), block_device_list) + return block_device.strip_dev(mount_device) in block_device_list def _get_volume_device_info(self, device_path): if device_path.startswith('/dev/'): @@ -937,13 +993,10 @@ class LibvirtConnection(driver.ComputeDriver): else: raise exception.InvalidDevicePath(path=device_path) - def _prepare_xml_info(self, instance, rescue=False, network_info=None, - block_device_mapping=None): - block_device_mapping = block_device_mapping or [] - # TODO(adiantum) remove network_info creation code - # when multinics will be completed - if not network_info: - network_info = netutils.get_network_info(instance) + def _prepare_xml_info(self, instance, network_info, rescue, + block_device_info=None): + block_device_mapping = driver.block_device_info_get_mapping( + block_device_info) nics = [] for (network, mapping) in network_info: @@ -958,17 +1011,27 @@ class LibvirtConnection(driver.ComputeDriver): driver_type = 'raw' for vol in block_device_mapping: - vol['mount_device'] = _strip_dev(vol['mount_device']) + vol['mount_device'] = block_device.strip_dev(vol['mount_device']) (vol['type'], vol['protocol'], vol['name']) = \ self._get_volume_device_info(vol['device_path']) - ebs_root = self._volume_in_mapping(self.root_mount_device, - block_device_mapping) - if self._volume_in_mapping(self.local_mount_device, - block_device_mapping): - local_gb = False - else: - local_gb = inst_type['local_gb'] + ebs_root = self._volume_in_mapping(self.default_root_device, + block_device_info) + + local_device = False + if not (self._volume_in_mapping(self.default_local_device, + block_device_info) or + 0 in [eph['num'] for eph in + driver.block_device_info_get_ephemerals( + block_device_info)]): + if instance['local_gb'] > 0: + local_device = self.default_local_device + + ephemerals = [] + for eph in driver.block_device_info_get_ephemerals(block_device_info): + ephemerals.append({'device_path': _get_eph_disk(eph), + 'device': block_device.strip_dev( + eph['device_name'])}) xml_info = {'type': FLAGS.libvirt_type, 'name': instance['name'], @@ -977,12 +1040,35 @@ class LibvirtConnection(driver.ComputeDriver): 'memory_kb': inst_type['memory_mb'] * 1024, 'vcpus': inst_type['vcpus'], 'rescue': rescue, - 'local': local_gb, + 'disk_prefix': self._disk_prefix, 'driver_type': driver_type, 'vif_type': FLAGS.libvirt_vif_type, 'nics': nics, 'ebs_root': ebs_root, - 'volumes': block_device_mapping} + 'local_device': local_device, + 'volumes': block_device_mapping, + 'ephemerals': ephemerals} + + root_device_name = driver.block_device_info_get_root(block_device_info) + if root_device_name: + xml_info['root_device'] = block_device.strip_dev(root_device_name) + xml_info['root_device_name'] = root_device_name + else: + # NOTE(yamahata): + # for nova.api.ec2.cloud.CloudController.get_metadata() + xml_info['root_device'] = self.default_root_device + db.instance_update( + nova_context.get_admin_context(), instance['id'], + {'root_device_name': '/dev/' + self.default_root_device}) + + swap = driver.block_device_info_get_swap(block_device_info) + if driver.swap_is_usable(swap): + xml_info['swap_device'] = block_device.strip_dev( + swap['device_name']) + elif (inst_type['swap'] > 0 and + not self._volume_in_mapping(self.default_swap_device, + block_device_info)): + xml_info['swap_device'] = self.default_swap_device if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'): xml_info['vncserver_host'] = FLAGS.vncserver_host @@ -997,13 +1083,12 @@ class LibvirtConnection(driver.ComputeDriver): xml_info['disk'] = xml_info['basepath'] + "/disk" return xml_info - def to_xml(self, instance, rescue=False, network_info=None, - block_device_mapping=None): - block_device_mapping = block_device_mapping or [] + def to_xml(self, instance, network_info, rescue=False, + block_device_info=None): # TODO(termie): cache? LOG.debug(_('instance %s: starting toXML method'), instance['name']) - xml_info = self._prepare_xml_info(instance, rescue, network_info, - block_device_mapping) + xml_info = self._prepare_xml_info(instance, network_info, rescue, + block_device_info) xml = str(Template(self.libvirt_xml, searchList=[xml_info])) LOG.debug(_('instance %s: finished toXML method'), instance['name']) return xml @@ -1422,7 +1507,7 @@ class LibvirtConnection(driver.ComputeDriver): return - def ensure_filtering_rules_for_instance(self, instance_ref, + def ensure_filtering_rules_for_instance(self, instance_ref, network_info, time=None): """Setting up filtering rules and waiting for its completion. @@ -1452,14 +1537,15 @@ class LibvirtConnection(driver.ComputeDriver): # If any instances never launch at destination host, # basic-filtering must be set here. - self.firewall_driver.setup_basic_filtering(instance_ref) + self.firewall_driver.setup_basic_filtering(instance_ref, network_info) # setting up n)ova-instance-instance-xx mainly. - self.firewall_driver.prepare_instance_filter(instance_ref) + self.firewall_driver.prepare_instance_filter(instance_ref, network_info) # wait for completion timeout_count = range(FLAGS.live_migration_retry_count) while timeout_count: - if self.firewall_driver.instance_filter_exists(instance_ref): + if self.firewall_driver.instance_filter_exists(instance_ref, + network_info): break timeout_count.pop() if len(timeout_count) == 0: @@ -1468,7 +1554,7 @@ class LibvirtConnection(driver.ComputeDriver): time.sleep(1) def live_migration(self, ctxt, instance_ref, dest, - post_method, recover_method): + post_method, recover_method, block_migration=False): """Spawning live_migration operation for distributing high-load. :params ctxt: security context @@ -1476,20 +1562,22 @@ class LibvirtConnection(driver.ComputeDriver): nova.db.sqlalchemy.models.Instance object instance object that is migrated. :params dest: destination host + :params block_migration: destination host :params post_method: post operation method. expected nova.compute.manager.post_live_migration. :params recover_method: recovery method when any exception occurs. expected nova.compute.manager.recover_live_migration. + :params block_migration: if true, do block migration. """ greenthread.spawn(self._live_migration, ctxt, instance_ref, dest, - post_method, recover_method) + post_method, recover_method, block_migration) - def _live_migration(self, ctxt, instance_ref, dest, - post_method, recover_method): + def _live_migration(self, ctxt, instance_ref, dest, post_method, + recover_method, block_migration=False): """Do live migration. :params ctxt: security context @@ -1508,27 +1596,21 @@ class LibvirtConnection(driver.ComputeDriver): # Do live migration. try: - flaglist = FLAGS.live_migration_flag.split(',') + if block_migration: + flaglist = FLAGS.block_migration_flag.split(',') + else: + flaglist = FLAGS.live_migration_flag.split(',') flagvals = [getattr(libvirt, x.strip()) for x in flaglist] logical_sum = reduce(lambda x, y: x | y, flagvals) - if self.read_only: - tmpconn = self._connect(self.libvirt_uri, False) - dom = tmpconn.lookupByName(instance_ref.name) - dom.migrateToURI(FLAGS.live_migration_uri % dest, - logical_sum, - None, - FLAGS.live_migration_bandwidth) - tmpconn.close() - else: - dom = self._conn.lookupByName(instance_ref.name) - dom.migrateToURI(FLAGS.live_migration_uri % dest, - logical_sum, - None, - FLAGS.live_migration_bandwidth) + dom = self._conn.lookupByName(instance_ref.name) + dom.migrateToURI(FLAGS.live_migration_uri % dest, + logical_sum, + None, + FLAGS.live_migration_bandwidth) except Exception: - recover_method(ctxt, instance_ref, dest=dest) + recover_method(ctxt, instance_ref, dest, block_migration) raise # Waiting for completion of live_migration. @@ -1540,11 +1622,150 @@ class LibvirtConnection(driver.ComputeDriver): self.get_info(instance_ref.name)['state'] except exception.NotFound: timer.stop() - post_method(ctxt, instance_ref, dest) + post_method(ctxt, instance_ref, dest, block_migration) timer.f = wait_for_live_migration timer.start(interval=0.5, now=True) + def pre_block_migration(self, ctxt, instance_ref, disk_info_json): + """Preparation block migration. + + :params ctxt: security context + :params instance_ref: + nova.db.sqlalchemy.models.Instance object + instance object that is migrated. + :params disk_info_json: + json strings specified in get_instance_disk_info + + """ + disk_info = utils.loads(disk_info_json) + + # make instance directory + instance_dir = os.path.join(FLAGS.instances_path, instance_ref['name']) + if os.path.exists(instance_dir): + raise exception.DestinationDiskExists(path=instance_dir) + os.mkdir(instance_dir) + + for info in disk_info: + base = os.path.basename(info['path']) + # Get image type and create empty disk image. + instance_disk = os.path.join(instance_dir, base) + utils.execute('sudo', 'qemu-img', 'create', '-f', info['type'], + instance_disk, info['local_gb']) + + # if image has kernel and ramdisk, just download + # following normal way. + if instance_ref['kernel_id']: + user = manager.AuthManager().get_user(instance_ref['user_id']) + project = manager.AuthManager().get_project( + instance_ref['project_id']) + self._fetch_image(nova_context.get_admin_context(), + os.path.join(instance_dir, 'kernel'), + instance_ref['kernel_id'], + user, + project) + if instance_ref['ramdisk_id']: + self._fetch_image(nova_context.get_admin_context(), + os.path.join(instance_dir, 'ramdisk'), + instance_ref['ramdisk_id'], + user, + project) + + def post_live_migration_at_destination(self, ctxt, + instance_ref, + network_info, + block_migration): + """Post operation of live migration at destination host. + + :params ctxt: security context + :params instance_ref: + nova.db.sqlalchemy.models.Instance object + instance object that is migrated. + :params network_info: instance network infomation + :params : block_migration: if true, post operation of block_migraiton. + """ + # Define migrated instance, otherwise, suspend/destroy does not work. + dom_list = self._conn.listDefinedDomains() + if instance_ref.name not in dom_list: + instance_dir = os.path.join(FLAGS.instances_path, + instance_ref.name) + xml_path = os.path.join(instance_dir, 'libvirt.xml') + # In case of block migration, destination does not have + # libvirt.xml + if not os.path.isfile(xml_path): + xml = self.to_xml(instance_ref, network_info=network_info) + f = open(os.path.join(instance_dir, 'libvirt.xml'), 'w+') + f.write(xml) + f.close() + # libvirt.xml should be made by to_xml(), but libvirt + # does not accept to_xml() result, since uuid is not + # included in to_xml() result. + dom = self._lookup_by_name(instance_ref.name) + self._conn.defineXML(dom.XMLDesc(0)) + + def get_instance_disk_info(self, ctxt, instance_ref): + """Preparation block migration. + + :params ctxt: security context + :params instance_ref: + nova.db.sqlalchemy.models.Instance object + instance object that is migrated. + :return: + json strings with below format. + "[{'path':'disk', 'type':'raw', 'local_gb':'10G'},...]" + + """ + disk_info = [] + + virt_dom = self._lookup_by_name(instance_ref.name) + xml = virt_dom.XMLDesc(0) + doc = libxml2.parseDoc(xml) + disk_nodes = doc.xpathEval('//devices/disk') + path_nodes = doc.xpathEval('//devices/disk/source') + driver_nodes = doc.xpathEval('//devices/disk/driver') + + for cnt, path_node in enumerate(path_nodes): + disk_type = disk_nodes[cnt].get_properties().getContent() + path = path_node.get_properties().getContent() + + if disk_type != 'file': + LOG.debug(_('skipping %(path)s since it looks like volume') % + locals()) + continue + + # In case of libvirt.xml, disk type can be obtained + # by the below statement. + # -> disk_type = driver_nodes[cnt].get_properties().getContent() + # but this xml is generated by kvm, format is slightly different. + disk_type = \ + driver_nodes[cnt].get_properties().get_next().getContent() + if disk_type == 'raw': + size = int(os.path.getsize(path)) + else: + out, err = utils.execute('sudo', 'qemu-img', 'info', path) + size = [i.split('(')[1].split()[0] for i in out.split('\n') + if i.strip().find('virtual size') >= 0] + size = int(size[0]) + + # block migration needs same/larger size of empty image on the + # destination host. since qemu-img creates bit smaller size image + # depending on original image size, fixed value is necessary. + for unit, divisor in [('G', 1024 ** 3), ('M', 1024 ** 2), + ('K', 1024), ('', 1)]: + if size / divisor == 0: + continue + if size % divisor != 0: + size = size / divisor + 1 + else: + size = size / divisor + size = str(size) + unit + break + + disk_info.append({'type': disk_type, 'path': path, + 'local_gb': size}) + + return utils.dumps(disk_info) + def unfilter_instance(self, instance_ref, network_info): """See comments of same method in firewall_driver.""" self.firewall_driver.unfilter_instance(instance_ref, @@ -1558,6 +1779,10 @@ class LibvirtConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index 9ce57b6c9..c2f4f91e8 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -40,17 +40,17 @@ except ImportError: class FirewallDriver(object): - def prepare_instance_filter(self, instance, network_info=None): + def prepare_instance_filter(self, instance, network_info): """Prepare filters for the instance. At this point, the instance isn't running yet.""" raise NotImplementedError() - def unfilter_instance(self, instance, network_info=None): + def unfilter_instance(self, instance, network_info): """Stop filtering instance""" raise NotImplementedError() - def apply_instance_filter(self, instance): + def apply_instance_filter(self, instance, network_info): """Apply instance filter. Once this method returns, the instance should be firewalled @@ -60,9 +60,7 @@ class FirewallDriver(object): """ raise NotImplementedError() - def refresh_security_group_rules(self, - security_group_id, - network_info=None): + def refresh_security_group_rules(self, security_group_id): """Refresh security group rules from data store Gets called when a rule has been added to or removed from @@ -85,7 +83,7 @@ class FirewallDriver(object): """ raise NotImplementedError() - def setup_basic_filtering(self, instance, network_info=None): + def setup_basic_filtering(self, instance, network_info): """Create rules to block spoofing and allow dhcp. This gets called when spawning an instance, before @@ -94,7 +92,7 @@ class FirewallDriver(object): """ raise NotImplementedError() - def instance_filter_exists(self, instance): + def instance_filter_exists(self, instance, network_info): """Check nova-instance-instance-xxx exists""" raise NotImplementedError() @@ -150,7 +148,7 @@ class NWFilterFirewall(FirewallDriver): self.static_filters_configured = False self.handle_security_groups = False - def apply_instance_filter(self, instance): + def apply_instance_filter(self, instance, network_info): """No-op. Everything is done in prepare_instance_filter""" pass @@ -189,13 +187,10 @@ class NWFilterFirewall(FirewallDriver): </rule> </filter>''' - def setup_basic_filtering(self, instance, network_info=None): + def setup_basic_filtering(self, instance, network_info): """Set up basic filtering (MAC, IP, and ARP spoofing protection)""" logging.info('called setup_basic_filtering in nwfilter') - if not network_info: - network_info = netutils.get_network_info(instance) - if self.handle_security_groups: # No point in setting up a filter set that we'll be overriding # anyway. @@ -237,7 +232,7 @@ class NWFilterFirewall(FirewallDriver): self._define_filter(self.nova_base_ipv6_filter) self._define_filter(self.nova_dhcp_filter) self._define_filter(self.nova_ra_filter) - if FLAGS.allow_project_net_traffic: + if FLAGS.allow_same_net_traffic: self._define_filter(self.nova_project_filter) if FLAGS.use_ipv6: self._define_filter(self.nova_project_filter_v6) @@ -300,10 +295,8 @@ class NWFilterFirewall(FirewallDriver): # execute in a native thread and block current greenthread until done tpool.execute(self._conn.nwfilterDefineXML, xml) - def unfilter_instance(self, instance, network_info=None): + def unfilter_instance(self, instance, network_info): """Clear out the nwfilter rules.""" - if not network_info: - network_info = netutils.get_network_info(instance) instance_name = instance.name for (network, mapping) in network_info: nic_id = mapping['mac'].replace(':', '') @@ -326,16 +319,13 @@ class NWFilterFirewall(FirewallDriver): LOG.debug(_('The nwfilter(%(instance_secgroup_filter_name)s) ' 'for %(instance_name)s is not found.') % locals()) - def prepare_instance_filter(self, instance, network_info=None): + def prepare_instance_filter(self, instance, network_info): """Creates an NWFilter for the given instance. In the process, it makes sure the filters for the provider blocks, security groups, and base filter are all in place. """ - if not network_info: - network_info = netutils.get_network_info(instance) - self.refresh_provider_fw_rules() ctxt = context.get_admin_context() @@ -388,7 +378,7 @@ class NWFilterFirewall(FirewallDriver): instance_filter_children = [base_filter, 'nova-provider-rules', instance_secgroup_filter_name] - if FLAGS.allow_project_net_traffic: + if FLAGS.allow_same_net_traffic: instance_filter_children.append('nova-project') if FLAGS.use_ipv6: instance_filter_children.append('nova-project-v6') @@ -401,9 +391,7 @@ class NWFilterFirewall(FirewallDriver): self._define_filter(self._filter_container(filter_name, filter_children)) - def refresh_security_group_rules(self, - security_group_id, - network_info=None): + def refresh_security_group_rules(self, security_group_id): return self._define_filter( self.security_group_to_nwfilter_xml(security_group_id)) @@ -500,9 +488,8 @@ class NWFilterFirewall(FirewallDriver): return 'nova-instance-%s' % (instance['name']) return 'nova-instance-%s-%s' % (instance['name'], nic_id) - def instance_filter_exists(self, instance): + def instance_filter_exists(self, instance, network_info): """Check nova-instance-instance-xxx exists""" - network_info = netutils.get_network_info(instance) for (network, mapping) in network_info: nic_id = mapping['mac'].replace(':', '') instance_filter_name = self._instance_filter_name(instance, nic_id) @@ -521,6 +508,7 @@ class IptablesFirewallDriver(FirewallDriver): from nova.network import linux_net self.iptables = linux_net.iptables_manager self.instances = {} + self.network_infos = {} self.nwfilter = NWFilterFirewall(kwargs['get_connection']) self.basicly_filtered = False @@ -529,22 +517,22 @@ class IptablesFirewallDriver(FirewallDriver): self.iptables.ipv6['filter'].add_chain('sg-fallback') self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP') - def setup_basic_filtering(self, instance, network_info=None): + def setup_basic_filtering(self, instance, network_info): """Set up provider rules and basic NWFilter.""" - if not network_info: - network_info = netutils.get_network_info(instance) self.nwfilter.setup_basic_filtering(instance, network_info) if not self.basicly_filtered: LOG.debug(_('iptables firewall: Setup Basic Filtering')) self.refresh_provider_fw_rules() self.basicly_filtered = True - def apply_instance_filter(self, instance): + def apply_instance_filter(self, instance, network_info): """No-op. Everything is done in prepare_instance_filter""" pass - def unfilter_instance(self, instance, network_info=None): + def unfilter_instance(self, instance, network_info): if self.instances.pop(instance['id'], None): + # NOTE(vish): use the passed info instead of the stored info + self.network_infos.pop(instance['id']) self.remove_filters_for_instance(instance) self.iptables.apply() self.nwfilter.unfilter_instance(instance, network_info) @@ -552,11 +540,10 @@ class IptablesFirewallDriver(FirewallDriver): LOG.info(_('Attempted to unfilter instance %s which is not ' 'filtered'), instance['id']) - def prepare_instance_filter(self, instance, network_info=None): - if not network_info: - network_info = netutils.get_network_info(instance) + def prepare_instance_filter(self, instance, network_info): self.instances[instance['id']] = instance - self.add_filters_for_instance(instance, network_info) + self.network_infos[instance['id']] = network_info + self.add_filters_for_instance(instance) self.iptables.apply() def _create_filter(self, ips, chain_name): @@ -583,7 +570,8 @@ class IptablesFirewallDriver(FirewallDriver): for rule in ipv6_rules: self.iptables.ipv6['filter'].add_rule(chain_name, rule) - def add_filters_for_instance(self, instance, network_info=None): + def add_filters_for_instance(self, instance): + network_info = self.network_infos[instance['id']] chain_name = self._instance_chain_name(instance) if FLAGS.use_ipv6: self.iptables.ipv6['filter'].add_chain(chain_name) @@ -601,9 +589,7 @@ class IptablesFirewallDriver(FirewallDriver): if FLAGS.use_ipv6: self.iptables.ipv6['filter'].remove_chain(chain_name) - def instance_rules(self, instance, network_info=None): - if not network_info: - network_info = netutils.get_network_info(instance) + def instance_rules(self, instance, network_info): ctxt = context.get_admin_context() ipv4_rules = [] @@ -621,14 +607,14 @@ class IptablesFirewallDriver(FirewallDriver): ipv4_rules += ['-j $provider'] ipv6_rules += ['-j $provider'] - dhcp_servers = [info['gateway'] for (_n, info) in network_info] + dhcp_servers = [info['dhcp_server'] for (_n, info) in network_info] for dhcp_server in dhcp_servers: ipv4_rules.append('-s %s -p udp --sport 67 --dport 68 ' '-j ACCEPT' % (dhcp_server,)) #Allow project network traffic - if FLAGS.allow_project_net_traffic: + if FLAGS.allow_same_net_traffic: cidrs = [network['cidr'] for (network, _m) in network_info] for cidr in cidrs: ipv4_rules.append('-s %s -j ACCEPT' % (cidr,)) @@ -645,7 +631,7 @@ class IptablesFirewallDriver(FirewallDriver): '-s %s/128 -p icmpv6 -j ACCEPT' % (gateway_v6,)) #Allow project network traffic - if FLAGS.allow_project_net_traffic: + if FLAGS.allow_same_net_traffic: cidrv6s = [network['cidr_v6'] for (network, _m) in network_info] @@ -664,11 +650,10 @@ class IptablesFirewallDriver(FirewallDriver): LOG.debug(_('Adding security group rule: %r'), rule) if not rule.cidr: - # Eventually, a mechanism to grant access for security - # groups will turn up here. It'll use ipsets. - continue + version = 4 + else: + version = netutils.get_ip_version(rule.cidr) - version = netutils.get_ip_version(rule.cidr) if version == 4: fw_rules = ipv4_rules else: @@ -678,16 +663,16 @@ class IptablesFirewallDriver(FirewallDriver): if version == 6 and rule.protocol == 'icmp': protocol = 'icmpv6' - args = ['-p', protocol, '-s', rule.cidr] + args = ['-j ACCEPT', '-p', protocol] - if rule.protocol in ['udp', 'tcp']: + if protocol in ['udp', 'tcp']: if rule.from_port == rule.to_port: args += ['--dport', '%s' % (rule.from_port,)] else: args += ['-m', 'multiport', '--dports', '%s:%s' % (rule.from_port, rule.to_port)] - elif rule.protocol == 'icmp': + elif protocol == 'icmp': icmp_type = rule.from_port icmp_code = rule.to_port @@ -706,34 +691,44 @@ class IptablesFirewallDriver(FirewallDriver): args += ['-m', 'icmp6', '--icmpv6-type', icmp_type_arg] - args += ['-j ACCEPT'] - fw_rules += [' '.join(args)] - + if rule.cidr: + LOG.info('Using cidr %r', rule.cidr) + args += ['-s', rule.cidr] + fw_rules += [' '.join(args)] + else: + if rule['grantee_group']: + for instance in rule['grantee_group']['instances']: + LOG.info('instance: %r', instance) + ips = db.instance_get_fixed_addresses(ctxt, + instance['id']) + LOG.info('ips: %r', ips) + for ip in ips: + subrule = args + ['-s %s' % ip] + fw_rules += [' '.join(subrule)] + + LOG.info('Using fw_rules: %r', fw_rules) ipv4_rules += ['-j $sg-fallback'] ipv6_rules += ['-j $sg-fallback'] return ipv4_rules, ipv6_rules - def instance_filter_exists(self, instance): + def instance_filter_exists(self, instance, network_info): """Check nova-instance-instance-xxx exists""" - return self.nwfilter.instance_filter_exists(instance) + return self.nwfilter.instance_filter_exists(instance, network_info) def refresh_security_group_members(self, security_group): - pass + self.do_refresh_security_group_rules(security_group) + self.iptables.apply() - def refresh_security_group_rules(self, security_group, network_info=None): - self.do_refresh_security_group_rules(security_group, network_info) + def refresh_security_group_rules(self, security_group): + self.do_refresh_security_group_rules(security_group) self.iptables.apply() @utils.synchronized('iptables', external=True) - def do_refresh_security_group_rules(self, - security_group, - network_info=None): + def do_refresh_security_group_rules(self, security_group): for instance in self.instances.values(): self.remove_filters_for_instance(instance) - if not network_info: - network_info = netutils.get_network_info(instance) - self.add_filters_for_instance(instance, network_info) + self.add_filters_for_instance(instance) def refresh_provider_fw_rules(self): """See class:FirewallDriver: docs.""" diff --git a/nova/virt/libvirt/netutils.py b/nova/virt/libvirt/netutils.py index a8e88fc07..6f303072d 100644 --- a/nova/virt/libvirt/netutils.py +++ b/nova/virt/libvirt/netutils.py @@ -23,12 +23,7 @@ import netaddr -from nova import context -from nova import db -from nova import exception from nova import flags -from nova import ipv6 -from nova import utils FLAGS = flags.FLAGS @@ -47,65 +42,3 @@ def get_net_and_prefixlen(cidr): def get_ip_version(cidr): net = netaddr.IPNetwork(cidr) return int(net.version) - - -def get_network_info(instance): - # TODO(tr3buchet): this function needs to go away! network info - # MUST be passed down from compute - # TODO(adiantum) If we will keep this function - # we should cache network_info - admin_context = context.get_admin_context() - - try: - fixed_ips = db.fixed_ip_get_by_instance(admin_context, instance['id']) - except exception.FixedIpNotFoundForInstance: - fixed_ips = [] - - vifs = db.virtual_interface_get_by_instance(admin_context, instance['id']) - flavor = db.instance_type_get(admin_context, - instance['instance_type_id']) - network_info = [] - - for vif in vifs: - network = vif['network'] - - # determine which of the instance's IPs belong to this network - network_ips = [fixed_ip['address'] for fixed_ip in fixed_ips if - fixed_ip['network_id'] == network['id']] - - def ip_dict(ip): - return { - 'ip': ip, - 'netmask': network['netmask'], - 'enabled': '1'} - - def ip6_dict(): - prefix = network['cidr_v6'] - mac = vif['address'] - project_id = instance['project_id'] - return { - 'ip': ipv6.to_global(prefix, mac, project_id), - 'netmask': network['netmask_v6'], - 'enabled': '1'} - - mapping = { - 'label': network['label'], - 'gateway': network['gateway'], - 'broadcast': network['broadcast'], - 'dhcp_server': network['gateway'], - 'mac': vif['address'], - 'rxtx_cap': flavor['rxtx_cap'], - 'dns': [], - 'ips': [ip_dict(ip) for ip in network_ips]} - - if network['dns1']: - mapping['dns'].append(network['dns1']) - if network['dns2']: - mapping['dns'].append(network['dns2']) - - if FLAGS.use_ipv6: - mapping['ip6s'] = [ip6_dict()] - mapping['gateway6'] = network['gateway_v6'] - - network_info.append((network, mapping)) - return network_info diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 711b05bae..4cb9abda4 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -44,7 +44,7 @@ class LibvirtBridgeDriver(VIFDriver): gateway6 = mapping.get('gateway6') mac_id = mapping['mac'].replace(':', '') - if FLAGS.allow_project_net_traffic: + if FLAGS.allow_same_net_traffic: template = "<parameter name=\"%s\"value=\"%s\" />\n" net, mask = netutils.get_net_and_mask(network['cidr']) values = [("PROJNET", net), ("PROJMASK", mask)] @@ -103,16 +103,18 @@ class LibvirtOpenVswitchDriver(VIFDriver): dev = "tap-%s" % vif_id iface_id = "nova-" + vif_id if not linux_net._device_exists(dev): - utils.execute('sudo', 'ip', 'tuntap', 'add', dev, 'mode', 'tap') - utils.execute('sudo', 'ip', 'link', 'set', dev, 'up') - utils.execute('sudo', 'ovs-vsctl', '--', '--may-exist', 'add-port', + utils.execute('ip', 'tuntap', 'add', dev, 'mode', 'tap', + run_as_root=True) + utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) + utils.execute('ovs-vsctl', '--', '--may-exist', 'add-port', FLAGS.libvirt_ovs_bridge, dev, '--', 'set', 'Interface', dev, "external-ids:iface-id=%s" % iface_id, '--', 'set', 'Interface', dev, "external-ids:iface-status=active", '--', 'set', 'Interface', dev, - "external-ids:attached-mac=%s" % mapping['mac']) + "external-ids:attached-mac=%s" % mapping['mac'], + run_as_root=True) result = { 'script': '', @@ -126,9 +128,9 @@ class LibvirtOpenVswitchDriver(VIFDriver): vif_id = str(instance['id']) + "-" + str(network['id']) dev = "tap-%s" % vif_id try: - utils.execute('sudo', 'ovs-vsctl', 'del-port', - network['bridge'], dev) - utils.execute('sudo', 'ip', 'link', 'delete', dev) + utils.execute('ovs-vsctl', 'del-port', + network['bridge'], dev, run_as_root=True) + utils.execute('ip', 'link', 'delete', dev, run_as_root=True) except exception.ProcessExecutionError: LOG.warning(_("Failed while unplugging vif of instance '%s'"), instance['name']) diff --git a/nova/virt/vmwareapi/io_util.py b/nova/virt/vmwareapi/io_util.py index 2ec773b7b..409242800 100644 --- a/nova/virt/vmwareapi/io_util.py +++ b/nova/virt/vmwareapi/io_util.py @@ -68,7 +68,10 @@ class GlanceWriteThread(object): """Ensures that image data is written to in the glance client and that
it is in correct ('active')state."""
- def __init__(self, input, glance_client, image_id, image_meta={}):
+ def __init__(self, input, glance_client, image_id, image_meta=None):
+ if not image_meta:
+ image_meta = {}
+
self.input = input
self.glance_client = glance_client
self.image_id = image_id
diff --git a/nova/virt/vmwareapi/vif.py b/nova/virt/vmwareapi/vif.py index b3e43b209..fb6548b34 100644 --- a/nova/virt/vmwareapi/vif.py +++ b/nova/virt/vmwareapi/vif.py @@ -63,7 +63,7 @@ class VMWareVlanBridgeDriver(VIFDriver): vswitch_associated = network_utils.get_vswitch_for_vlan_interface( session, vlan_interface) if vswitch_associated is None: - raise exception.SwicthNotFoundForNetworkAdapter( + raise exception.SwitchNotFoundForNetworkAdapter( adapter=vlan_interface) # Check whether bridge already exists and retrieve the the ref of the # network whose name_label is "bridge" diff --git a/nova/virt/vmwareapi/vim_util.py b/nova/virt/vmwareapi/vim_util.py index 11214231c..e03daddac 100644 --- a/nova/virt/vmwareapi/vim_util.py +++ b/nova/virt/vmwareapi/vim_util.py @@ -95,9 +95,12 @@ def build_recursive_traversal_spec(client_factory): def build_property_spec(client_factory, type="VirtualMachine",
- properties_to_collect=["name"],
+ properties_to_collect=None,
all_properties=False):
"""Builds the Property Spec."""
+ if not properties_to_collect:
+ properties_to_collect = ["name"]
+
property_spec = client_factory.create('ns0:PropertySpec')
property_spec.all = all_properties
property_spec.pathSet = properties_to_collect
@@ -155,8 +158,11 @@ def get_dynamic_property(vim, mobj, type, property_name): return property_value
-def get_objects(vim, type, properties_to_collect=["name"], all=False):
+def get_objects(vim, type, properties_to_collect=None, all=False):
"""Gets the list of objects of the type specified."""
+ if not properties_to_collect:
+ properties_to_collect = ["name"]
+
client_factory = vim.client.factory
object_spec = build_object_spec(client_factory,
vim.get_service_content().rootFolder,
diff --git a/nova/virt/vmwareapi/vmware_images.py b/nova/virt/vmwareapi/vmware_images.py index 70adba74f..f5f75dae2 100644 --- a/nova/virt/vmwareapi/vmware_images.py +++ b/nova/virt/vmwareapi/vmware_images.py @@ -33,11 +33,15 @@ QUEUE_BUFFER_SIZE = 10 def start_transfer(read_file_handle, data_size, write_file_handle=None,
- glance_client=None, image_id=None, image_meta={}):
+ glance_client=None, image_id=None, image_meta=None):
"""Start the data transfer from the reader to the writer.
Reader writes to the pipe and the writer reads from the pipe. This means
that the total transfer time boils down to the slower of the read/write
and not the addition of the two times."""
+
+ if not image_meta:
+ image_meta = {}
+
# The pipe that acts as an intermediate store of data for reader to write
# to and writer to grab from.
thread_safe_pipe = io_util.ThreadSafePipe(QUEUE_BUFFER_SIZE, data_size)
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 3d209fa99..243ee64f5 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -137,7 +137,7 @@ class VMWareESXConnection(driver.ComputeDriver): """Reboot VM instance."""
self._vmops.reboot(instance, network_info)
- def destroy(self, instance, network_info):
+ def destroy(self, instance, network_info, cleanup=True):
"""Destroy VM instance."""
self._vmops.destroy(instance, network_info)
@@ -191,6 +191,10 @@ class VMWareESXConnection(driver.ComputeDriver): """This method is supported only by libvirt."""
return
+ def host_power_action(self, host, action):
+ """Reboots, shuts down or powers up the host."""
+ pass
+
def set_host_enabled(self, host, enabled):
"""Sets the specified host's ability to accept new instances."""
pass
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index d5ac39473..1aa642e4e 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -194,6 +194,7 @@ def create_local_pifs(): Do this one per host.""" for host_ref in _db_content['host'].keys(): _create_local_pif(host_ref) + _create_local_sr_iso(host_ref) def create_local_srs(): @@ -222,6 +223,25 @@ def _create_local_sr(host_ref): return sr_ref +def _create_local_sr_iso(host_ref): + sr_ref = _create_object( + 'SR', + {'name_label': 'Local storage ISO', + 'type': 'lvm', + 'content_type': 'iso', + 'shared': False, + 'physical_size': str(1 << 30), + 'physical_utilisation': str(0), + 'virtual_allocation': str(0), + 'other_config': { + 'i18n-original-value-name_label': 'Local storage ISO', + 'i18n-key': 'local-storage-iso'}, + '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', diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 6d2340ccd..ba5cf4b49 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -77,6 +77,7 @@ class ImageType: 3 - raw disk image (local SR, NOT partitioned by plugin) 4 - vhd disk image (local SR, NOT inspected by XS, PV assumed for linux, HVM assumed for Windows) + 5 - ISO disk image (local SR, NOT partitioned by plugin) """ KERNEL = 0 @@ -84,14 +85,17 @@ class ImageType: DISK = 2 DISK_RAW = 3 DISK_VHD = 4 - _ids = (KERNEL, RAMDISK, DISK, DISK_RAW, DISK_VHD) + DISK_ISO = 5 + _ids = (KERNEL, RAMDISK, DISK, DISK_RAW, DISK_VHD, DISK_ISO) KERNEL_STR = "kernel" RAMDISK_STR = "ramdisk" DISK_STR = "os" DISK_RAW_STR = "os_raw" DISK_VHD_STR = "vhd" - _strs = (KERNEL_STR, RAMDISK_STR, DISK_STR, DISK_RAW_STR, DISK_VHD_STR) + DISK_ISO_STR = "iso" + _strs = (KERNEL_STR, RAMDISK_STR, DISK_STR, DISK_RAW_STR, DISK_VHD_STR, + DISK_ISO_STR) @classmethod def to_string(cls, image_type): @@ -223,6 +227,30 @@ class VMHelper(HelperBase): return vbd_ref @classmethod + def create_cd_vbd(cls, session, vm_ref, vdi_ref, userdevice, bootable): + """Create a VBD record. Returns a Deferred that gives the new + VBD reference specific to CDRom devices.""" + vbd_rec = {} + vbd_rec['VM'] = vm_ref + vbd_rec['VDI'] = vdi_ref + vbd_rec['userdevice'] = str(userdevice) + vbd_rec['bootable'] = bootable + vbd_rec['mode'] = 'RO' + vbd_rec['type'] = 'CD' + vbd_rec['unpluggable'] = True + vbd_rec['empty'] = False + vbd_rec['other_config'] = {} + vbd_rec['qos_algorithm_type'] = '' + vbd_rec['qos_algorithm_params'] = {} + vbd_rec['qos_supported_algorithms'] = [] + LOG.debug(_('Creating a CDROM-specific VBD for VM %(vm_ref)s,' + ' VDI %(vdi_ref)s ... ') % locals()) + vbd_ref = session.call_xenapi('VBD.create', vbd_rec) + LOG.debug(_('Created a CDROM-specific VBD %(vbd_ref)s ' + ' for VM %(vm_ref)s, VDI %(vdi_ref)s.') % locals()) + return vbd_ref + + @classmethod def find_vbd_by_number(cls, session, vm_ref, number): """Get the VBD reference from the device number""" vbd_refs = session.get_xenapi().VM.get_VBDs(vm_ref) @@ -368,6 +396,23 @@ class VMHelper(HelperBase): session.wait_for_task(task, instance.id) @classmethod + def fetch_blank_disk(cls, session, instance_type_id): + # Size the blank harddrive to suit the machine type: + one_gig = 1024 * 1024 * 1024 + req_type = instance_types.get_instance_type(instance_type_id) + req_size = req_type['local_gb'] + + LOG.debug("Creating blank HD of size %(req_size)d gigs" + % locals()) + vdi_size = one_gig * req_size + + LOG.debug("ISO vm create: Looking for the SR") + sr_ref = safe_find_sr(session) + + vdi_ref = cls.create_vdi(session, sr_ref, 'blank HD', vdi_size, False) + return vdi_ref + + @classmethod def fetch_image(cls, context, session, instance_id, image, user_id, project_id, image_type): """Fetch image from glance based on image type. @@ -449,7 +494,12 @@ class VMHelper(HelperBase): # DISK restores LOG.debug(_("Fetching image %(image)s") % locals()) LOG.debug(_("Image Type: %s"), ImageType.to_string(image_type)) - sr_ref = safe_find_sr(session) + + if image_type == ImageType.DISK_ISO: + sr_ref = safe_find_iso_sr(session) + LOG.debug(_("ISO: Found sr possibly containing the ISO image")) + else: + sr_ref = safe_find_sr(session) glance_client, image_id = nova.image.get_glance_client(image) glance_client.set_auth_token(getattr(context, 'auth_token', None)) @@ -527,7 +577,8 @@ class VMHelper(HelperBase): ImageType.RAMDISK: 'RAMDISK', ImageType.DISK: 'DISK', ImageType.DISK_RAW: 'DISK_RAW', - ImageType.DISK_VHD: 'DISK_VHD'} + ImageType.DISK_VHD: 'DISK_VHD', + ImageType.DISK_ISO: 'DISK_ISO'} disk_format = pretty_format[image_type] image_ref = instance.image_ref instance_id = instance.id @@ -540,7 +591,8 @@ class VMHelper(HelperBase): 'aki': ImageType.KERNEL, 'ari': ImageType.RAMDISK, 'raw': ImageType.DISK_RAW, - 'vhd': ImageType.DISK_VHD} + 'vhd': ImageType.DISK_VHD, + 'iso': ImageType.DISK_ISO} image_ref = instance.image_ref glance_client, image_id = nova.image.get_glance_client(image_ref) meta = glance_client.get_image_meta(image_id) @@ -574,6 +626,8 @@ class VMHelper(HelperBase): available 3. Glance (DISK): pv is assumed + + 4. Glance (DISK_ISO): no pv is assumed """ LOG.debug(_("Looking up vdi %s for PV kernel"), vdi_ref) @@ -589,6 +643,9 @@ class VMHelper(HelperBase): elif disk_image_type == ImageType.DISK: # 3. Disk is_pv = True + elif disk_image_type == ImageType.DISK_ISO: + # 4. ISO + is_pv = False else: raise exception.Error(_("Unknown image format %(disk_image_type)s") % locals()) @@ -797,7 +854,7 @@ def get_vdi_for_vm_safely(session, vm_ref): else: num_vdis = len(vdi_refs) if num_vdis != 1: - raise exception.Exception(_("Unexpected number of VDIs" + raise exception.Error(_("Unexpected number of VDIs" "(%(num_vdis)s) found" " for VM %(vm_ref)s") % locals()) @@ -832,6 +889,48 @@ def find_sr(session): return None +def safe_find_iso_sr(session): + """Same as find_iso_sr except raises a NotFound exception if SR cannot be + determined + """ + sr_ref = find_iso_sr(session) + if sr_ref is None: + raise exception.NotFound(_('Cannot find SR of content-type ISO')) + return sr_ref + + +def find_iso_sr(session): + """Return the storage repository to hold ISO images""" + host = session.get_xenapi_host() + sr_refs = session.get_xenapi().SR.get_all() + for sr_ref in sr_refs: + sr_rec = session.get_xenapi().SR.get_record(sr_ref) + + LOG.debug(_("ISO: looking at SR %(sr_rec)s") % locals()) + if not sr_rec['content_type'] == 'iso': + LOG.debug(_("ISO: not iso content")) + continue + if not 'i18n-key' in sr_rec['other_config']: + LOG.debug(_("ISO: iso content_type, no 'i18n-key' key")) + continue + if not sr_rec['other_config']['i18n-key'] == 'local-storage-iso': + LOG.debug(_("ISO: iso content_type, i18n-key value not " + "'local-storage-iso'")) + continue + + LOG.debug(_("ISO: SR MATCHing our criteria")) + for pbd_ref in sr_rec['PBDs']: + LOG.debug(_("ISO: ISO, looking to see if it is host local")) + pbd_rec = session.get_xenapi().PBD.get_record(pbd_ref) + pbd_rec_host = pbd_rec['host'] + LOG.debug(_("ISO: PBD matching, want %(pbd_rec)s, have %(host)s") % + locals()) + if pbd_rec_host == host: + LOG.debug(_("ISO: SR with local PBD")) + return sr_ref + return None + + def remap_vbd_dev(dev): """Return the appropriate location for a plugged-in VBD device @@ -967,7 +1066,7 @@ def _stream_disk(dev, image_type, virtual_size, image_file): offset = MBR_SIZE_BYTES _write_partition(virtual_size, dev) - utils.execute('sudo', 'chown', os.getuid(), '/dev/%s' % dev) + utils.execute('chown', os.getuid(), '/dev/%s' % dev, run_as_root=True) with open('/dev/%s' % dev, 'wb') as f: f.seek(offset) @@ -986,10 +1085,11 @@ def _write_partition(virtual_size, dev): def execute(*cmd, **kwargs): return utils.execute(*cmd, **kwargs) - execute('sudo', 'parted', '--script', dest, 'mklabel', 'msdos') - execute('sudo', 'parted', '--script', dest, 'mkpart', 'primary', + execute('parted', '--script', dest, 'mklabel', 'msdos', run_as_root=True) + execute('parted', '--script', dest, 'mkpart', 'primary', '%ds' % primary_first, - '%ds' % primary_last) + '%ds' % primary_last, + run_as_root=True) LOG.debug(_('Writing partition table %s done.'), dest) @@ -1002,9 +1102,9 @@ def get_name_label_for_image(image): def _mount_filesystem(dev_path, dir): """mounts the device specified by dev_path in dir""" try: - out, err = utils.execute('sudo', 'mount', + out, err = utils.execute('mount', '-t', 'ext2,ext3', - dev_path, dir) + dev_path, dir, run_as_root=True) except exception.ProcessExecutionError as e: err = str(e) return err @@ -1056,7 +1156,7 @@ def _mounted_processing(device, key, net): disk.inject_data_into_fs(tmpdir, key, net, utils.execute) finally: - utils.execute('sudo', 'umount', dev_path) + utils.execute('umount', dev_path, run_as_root=True) else: LOG.info(_('Failed to mount filesystem (expected for ' 'non-linux instances): %s') % err) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index a78413370..1fefd1291 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -186,7 +186,7 @@ class VMOps(object): instance.project_id, ImageType.KERNEL)[0] if instance.ramdisk_id: ramdisk = VMHelper.fetch_image(context, self._session, - instance.id, instance.kernel_id, instance.user_id, + instance.id, instance.ramdisk_id, instance.user_id, instance.project_id, ImageType.RAMDISK)[0] # Create the VM ref and attach the first disk first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', @@ -235,12 +235,51 @@ class VMOps(object): raise vm_create_error - VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, - vdi_ref=first_vdi_ref, userdevice=0, bootable=True) + # Add disks to VM + self._attach_disks(instance, disk_image_type, vm_ref, first_vdi_ref, + vdis) + + # Alter the image before VM start for, e.g. network injection + if FLAGS.flat_injected: + VMHelper.preconfigure_instance(self._session, instance, + first_vdi_ref, network_info) + + self.create_vifs(vm_ref, instance, network_info) + self.inject_network_info(instance, network_info, vm_ref) + return vm_ref + + def _attach_disks(self, instance, disk_image_type, vm_ref, first_vdi_ref, + vdis): + # device 0 reserved for RW disk + userdevice = 0 + + # DISK_ISO needs two VBDs: the ISO disk and a blank RW disk + if disk_image_type == ImageType.DISK_ISO: + LOG.debug("detected ISO image type, going to create blank VM for " + "install") + + cd_vdi_ref = first_vdi_ref + first_vdi_ref = VMHelper.fetch_blank_disk(session=self._session, + instance_type_id=instance.instance_type_id) + + VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, + vdi_ref=first_vdi_ref, userdevice=userdevice, bootable=False) + + # device 1 reserved for rescue disk and we've used '0' + userdevice = 2 + VMHelper.create_cd_vbd(session=self._session, vm_ref=vm_ref, + vdi_ref=cd_vdi_ref, userdevice=userdevice, bootable=True) + + # set user device to next free value + userdevice += 1 + else: + VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, + vdi_ref=first_vdi_ref, userdevice=userdevice, bootable=True) + # set user device to next free value + # userdevice 1 is reserved for rescue and we've used '0' + userdevice = 2 # Attach any other disks - # userdevice 1 is reserved for rescue - userdevice = 2 for vdi in vdis[1:]: # vdi['vdi_type'] is either 'os' or 'swap', but we don't # really care what it is right here. @@ -251,15 +290,6 @@ class VMOps(object): bootable=False) userdevice += 1 - # Alter the image before VM start for, e.g. network injection - if FLAGS.flat_injected: - VMHelper.preconfigure_instance(self._session, instance, - first_vdi_ref, network_info) - - self.create_vifs(vm_ref, instance, network_info) - self.inject_network_info(instance, network_info, vm_ref) - return vm_ref - def _spawn(self, instance, vm_ref): """Spawn a new instance.""" LOG.debug(_('Starting VM %s...'), vm_ref) @@ -282,6 +312,7 @@ class VMOps(object): 'architecture': instance.architecture}) def _check_agent_version(): + LOG.debug(_("Querying agent version")) if instance.os_type == 'windows': # Windows will generally perform a setup process on first boot # that can take a couple of minutes and then reboot. So we @@ -292,7 +323,6 @@ class VMOps(object): else: version = self.get_agent_version(instance) if not version: - LOG.info(_('No agent version returned by instance')) return LOG.info(_('Instance agent version: %s') % version) @@ -327,6 +357,10 @@ class VMOps(object): LOG.debug(_("Setting admin password")) self.set_admin_password(instance, admin_password) + def _reset_network(): + LOG.debug(_("Resetting network")) + self.reset_network(instance, vm_ref) + # NOTE(armando): Do we really need to do this in virt? # NOTE(tr3buchet): not sure but wherever we do it, we need to call # reset_network afterwards @@ -341,7 +375,7 @@ class VMOps(object): _check_agent_version() _inject_files() _set_admin_password() - self.reset_network(instance, vm_ref) + _reset_network() return True except Exception, exc: LOG.warn(exc) @@ -597,13 +631,13 @@ class VMOps(object): transaction_id = str(uuid.uuid4()) args = {'id': transaction_id} resp = self._make_agent_call('version', instance, '', args) - if resp is None: - # No response from the agent - return - resp_dict = json.loads(resp) + if resp['returncode'] != '0': + LOG.error(_('Failed to query agent version: %(resp)r') % + locals()) + return None # Some old versions of the Windows agent have a trailing \\r\\n # (ie CRLF escaped) for some reason. Strip that off. - return resp_dict['message'].replace('\\r\\n', '') + return resp['message'].replace('\\r\\n', '') if timeout: vm_ref = self._get_vm_opaque_ref(instance) @@ -634,13 +668,10 @@ class VMOps(object): transaction_id = str(uuid.uuid4()) args = {'id': transaction_id, 'url': url, 'md5sum': md5sum} resp = self._make_agent_call('agentupdate', instance, '', args) - if resp is None: - # No response from the agent - return - resp_dict = json.loads(resp) - if resp_dict['returncode'] != '0': - raise RuntimeError(resp_dict['message']) - return resp_dict['message'] + if resp['returncode'] != '0': + LOG.error(_('Failed to update agent: %(resp)r') % locals()) + return None + return resp['message'] def set_admin_password(self, instance, new_pass): """Set the root/admin password on the VM instance. @@ -659,18 +690,13 @@ class VMOps(object): key_init_args = {'id': key_init_transaction_id, 'pub': str(dh.get_public())} resp = self._make_agent_call('key_init', instance, '', key_init_args) - if resp is None: - # No response from the agent - return - resp_dict = json.loads(resp) # Successful return code from key_init is 'D0' - if resp_dict['returncode'] != 'D0': - # There was some sort of error; the message will contain - # a description of the error. - raise RuntimeError(resp_dict['message']) + if resp['returncode'] != 'D0': + LOG.error(_('Failed to exchange keys: %(resp)r') % locals()) + return None # Some old versions of the Windows agent have a trailing \\r\\n # (ie CRLF escaped) for some reason. Strip that off. - agent_pub = int(resp_dict['message'].replace('\\r\\n', '')) + agent_pub = int(resp['message'].replace('\\r\\n', '')) dh.compute_shared(agent_pub) # Some old versions of Linux and Windows agent expect trailing \n # on password to work correctly. @@ -679,17 +705,14 @@ class VMOps(object): password_transaction_id = str(uuid.uuid4()) password_args = {'id': password_transaction_id, 'enc_pass': enc_pass} resp = self._make_agent_call('password', instance, '', password_args) - if resp is None: - # No response from the agent - return - resp_dict = json.loads(resp) # Successful return code from password is '0' - if resp_dict['returncode'] != '0': - raise RuntimeError(resp_dict['message']) + if resp['returncode'] != '0': + LOG.error(_('Failed to update password: %(resp)r') % locals()) + return None db.instance_update(nova_context.get_admin_context(), instance['id'], dict(admin_pass=new_pass)) - return resp_dict['message'] + return resp['message'] def inject_file(self, instance, path, contents): """Write a file to the VM instance. @@ -712,12 +735,10 @@ class VMOps(object): # If the agent doesn't support file injection, a NotImplementedError # will be raised with the appropriate message. resp = self._make_agent_call('inject_file', instance, '', args) - resp_dict = json.loads(resp) - if resp_dict['returncode'] != '0': - # There was some other sort of error; the message will contain - # a description of the error. - raise RuntimeError(resp_dict['message']) - return resp_dict['message'] + if resp['returncode'] != '0': + LOG.error(_('Failed to inject file: %(resp)r') % locals()) + return None + return resp['message'] def _shutdown(self, instance, vm_ref, hard=True): """Shutdown an instance.""" @@ -1031,11 +1052,23 @@ class VMOps(object): # TODO: implement this! return 'http://fakeajaxconsole/fake_url' + def host_power_action(self, host, action): + """Reboots or shuts down the host.""" + args = {"action": json.dumps(action)} + methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"} + json_resp = self._call_xenhost(methods[action], args) + resp = json.loads(json_resp) + return resp["power_action"] + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" args = {"enabled": json.dumps(enabled)} - json_resp = self._call_xenhost("set_host_enabled", args) - resp = json.loads(json_resp) + xenapi_resp = self._call_xenhost("set_host_enabled", args) + try: + resp = json.loads(xenapi_resp) + except TypeError as e: + # Already logged; return the message + return xenapi_resp.details[-1] return resp["status"] def _call_xenhost(self, method, arg_dict): @@ -1051,7 +1084,7 @@ class VMOps(object): #args={"params": arg_dict}) ret = self._session.wait_for_task(task, task_id) except self.XenAPI.Failure as e: - ret = None + ret = e LOG.error(_("The call to %(method)s returned an error: %(e)s.") % locals()) return ret @@ -1166,8 +1199,19 @@ class VMOps(object): def _make_agent_call(self, method, vm, path, addl_args=None): """Abstracts out the interaction with the agent xenapi plugin.""" - return self._make_plugin_call('agent', method=method, vm=vm, + ret = self._make_plugin_call('agent', method=method, vm=vm, path=path, addl_args=addl_args) + if isinstance(ret, dict): + return ret + try: + return json.loads(ret) + except TypeError: + instance_id = vm.id + LOG.error(_('The agent call to %(method)s returned an invalid' + ' response: %(ret)r. VM id=%(instance_id)s;' + ' path=%(path)s; args=%(addl_args)r') % locals()) + return {'returncode': 'error', + 'message': 'unable to deserialize response'} def _make_plugin_call(self, plugin, method, vm, path, addl_args=None, vm_ref=None): @@ -1185,20 +1229,20 @@ class VMOps(object): ret = self._session.wait_for_task(task, instance_id) except self.XenAPI.Failure, e: ret = None - err_trace = e.details[-1] - err_msg = err_trace.splitlines()[-1] - strargs = str(args) + err_msg = e.details[-1].splitlines()[-1] if 'TIMEOUT:' in err_msg: LOG.error(_('TIMEOUT: The call to %(method)s timed out. ' - 'VM id=%(instance_id)s; args=%(strargs)s') % locals()) + 'VM id=%(instance_id)s; args=%(args)r') % locals()) + return {'returncode': 'timeout', 'message': err_msg} elif 'NOT IMPLEMENTED:' in err_msg: LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not' ' supported by the agent. VM id=%(instance_id)s;' - ' args=%(strargs)s') % locals()) - raise NotImplementedError(err_msg) + ' args=%(args)r') % locals()) + return {'returncode': 'notimplemented', 'message': err_msg} else: LOG.error(_('The call to %(method)s returned an error: %(e)s. ' - 'VM id=%(instance_id)s; args=%(strargs)s') % locals()) + 'VM id=%(instance_id)s; args=%(args)r') % locals()) + return {'returncode': 'error', 'message': err_msg} return ret def add_to_xenstore(self, vm, path, key, value): @@ -1320,12 +1364,6 @@ class VMOps(object): ######################################################################## -def _runproc(cmd): - pipe = subprocess.PIPE - return subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe, - stderr=pipe, close_fds=True) - - class SimpleDH(object): """ This class wraps all the functionality needed to implement @@ -1382,22 +1420,18 @@ class SimpleDH(object): mpi = M2Crypto.m2.bn_to_mpi(bn) return mpi - def _run_ssl(self, text, extra_args=None): - if not extra_args: - extra_args = '' - cmd = 'enc -aes-128-cbc -A -a -pass pass:%s -nosalt %s' % ( - self._shared, extra_args) - proc = _runproc('openssl %s' % cmd) - proc.stdin.write(text) - proc.stdin.close() - proc.wait() - err = proc.stderr.read() + def _run_ssl(self, text, decrypt=False): + cmd = ['openssl', 'aes-128-cbc', '-A', '-a', '-pass', + 'pass:%s' % self._shared, '-nosalt'] + if decrypt: + cmd.append('-d') + out, err = utils.execute(*cmd, process_input=text) if err: raise RuntimeError(_('OpenSSL error: %s') % err) - return proc.stdout.read() + return out def encrypt(self, text): return self._run_ssl(text).strip('\n') def decrypt(self, text): - return self._run_ssl(text, '-d') + return self._run_ssl(text, decrypt=True) diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py index 7821a4f7e..5d5eb824f 100644 --- a/nova/virt/xenapi/volume_utils.py +++ b/nova/virt/xenapi/volume_utils.py @@ -252,10 +252,10 @@ def _get_target(volume_id): volume_id) result = (None, None) try: - (r, _e) = utils.execute('sudo', 'iscsiadm', + (r, _e) = utils.execute('iscsiadm', '-m', 'discovery', '-t', 'sendtargets', - '-p', volume_ref['host']) + '-p', volume_ref['host'], run_as_root=True) except exception.ProcessExecutionError, exc: LOG.exception(exc) else: diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index f63f9707e..0d23e7689 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -184,14 +184,14 @@ class XenAPIConnection(driver.ComputeDriver): def list_instances_detail(self): return self._vmops.list_instances_detail() - def spawn(self, context, instance, network_info, - block_device_mapping=None): + def spawn(self, context, instance, + network_info=None, block_device_info=None): """Create VM instance""" self._vmops.spawn(context, instance, network_info) def revert_migration(self, instance): """Reverts a resize, powering back on the instance""" - self._vmops.revert_resize(instance) + self._vmops.revert_migration(instance) def finish_migration(self, context, instance, disk_info, network_info, resize_instance=False): @@ -217,7 +217,7 @@ class XenAPIConnection(driver.ComputeDriver): """ self._vmops.inject_file(instance, b64_path, b64_contents) - def destroy(self, instance, network_info): + def destroy(self, instance, network_info, cleanup=True): """Destroy VM instance""" self._vmops.destroy(instance, network_info) @@ -309,12 +309,12 @@ class XenAPIConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') - def ensure_filtering_rules_for_instance(self, instance_ref): + def ensure_filtering_rules_for_instance(self, instance_ref, network_info): """This method is supported only libvirt.""" return def live_migration(self, context, instance_ref, dest, - post_method, recover_method): + post_method, recover_method, block_migration=False): """This method is supported only by libvirt.""" return @@ -332,6 +332,19 @@ class XenAPIConnection(driver.ComputeDriver): True, run the update first.""" return self.HostState.get_host_stats(refresh=refresh) + def host_power_action(self, host, action): + """The only valid values for 'action' on XenServer are 'reboot' or + 'shutdown', even though the API also accepts 'startup'. As this is + not technically possible on XenServer, since the host is the same + physical machine as the hypervisor, if this is requested, we need to + raise an exception. + """ + if action in ("reboot", "shutdown"): + return self._vmops.host_power_action(host, action) + else: + msg = _("Host startup on XenServer is not supported.") + raise NotImplementedError(msg) + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" return self._vmops.set_host_enabled(host, enabled) diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 23e845deb..c99534c07 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -65,14 +65,14 @@ class VolumeDriver(object): self._execute = execute self._sync_exec = sync_exec - def _try_execute(self, *command): + def _try_execute(self, *command, **kwargs): # NOTE(vish): Volume commands can partially fail due to timing, but # running them a second time on failure will usually # recover nicely. tries = 0 while True: try: - self._execute(*command) + self._execute(*command, **kwargs) return True except exception.ProcessExecutionError: tries = tries + 1 @@ -84,24 +84,26 @@ class VolumeDriver(object): def check_for_setup_error(self): """Returns an error if prerequisites aren't met""" - out, err = self._execute('sudo', 'vgs', '--noheadings', '-o', 'name') + out, err = self._execute('vgs', '--noheadings', '-o', 'name', + run_as_root=True) volume_groups = out.split() if not FLAGS.volume_group in volume_groups: raise exception.Error(_("volume group %s doesn't exist") % FLAGS.volume_group) def _create_volume(self, volume_name, sizestr): - self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n', - volume_name, FLAGS.volume_group) + self._try_execute('lvcreate', '-L', sizestr, '-n', + volume_name, FLAGS.volume_group, run_as_root=True) def _copy_volume(self, srcstr, deststr, size_in_g): - self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr, - 'count=%d' % (size_in_g * 1024), 'bs=1M') + self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr, + 'count=%d' % (size_in_g * 1024), 'bs=1M', + run_as_root=True) def _volume_not_present(self, volume_name): path_name = '%s/%s' % (FLAGS.volume_group, volume_name) try: - self._try_execute('sudo', 'lvdisplay', path_name) + self._try_execute('lvdisplay', path_name, run_as_root=True) except Exception as e: # If the volume isn't present return True @@ -112,9 +114,10 @@ class VolumeDriver(object): # zero out old volumes to prevent data leaking between users # TODO(ja): reclaiming space should be done lazy and low priority self._copy_volume('/dev/zero', self.local_path(volume), size_in_g) - self._try_execute('sudo', 'lvremove', '-f', "%s/%s" % + self._try_execute('lvremove', '-f', "%s/%s" % (FLAGS.volume_group, - self._escape_snapshot(volume['name']))) + self._escape_snapshot(volume['name'])), + run_as_root=True) def _sizestr(self, size_in_g): if int(size_in_g) == 0: @@ -147,10 +150,11 @@ class VolumeDriver(object): # TODO(yamahata): lvm can't delete origin volume only without # deleting derived snapshots. Can we do something fancy? - out, err = self._execute('sudo', 'lvdisplay', '--noheading', + out, err = self._execute('lvdisplay', '--noheading', '-C', '-o', 'Attr', '%s/%s' % (FLAGS.volume_group, - volume['name'])) + volume['name']), + run_as_root=True) # fake_execute returns None resulting unit test error if out: out = out.strip() @@ -162,10 +166,10 @@ class VolumeDriver(object): def create_snapshot(self, snapshot): """Creates a snapshot.""" orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name']) - self._try_execute('sudo', 'lvcreate', '-L', + self._try_execute('lvcreate', '-L', self._sizestr(snapshot['volume_size']), '--name', self._escape_snapshot(snapshot['name']), - '--snapshot', orig_lv_name) + '--snapshot', orig_lv_name, run_as_root=True) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" @@ -233,13 +237,14 @@ class AOEDriver(VolumeDriver): blade_id) = self.db.volume_allocate_shelf_and_blade(context, volume['id']) self._try_execute( - 'sudo', 'vblade-persist', 'setup', + 'vblade-persist', 'setup', shelf_id, blade_id, FLAGS.aoe_eth_dev, "/dev/%s/%s" % (FLAGS.volume_group, - volume['name'])) + volume['name']), + run_as_root=True) # NOTE(vish): The standard _try_execute does not work here # because these methods throw errors if other # volumes on this host are in the process of @@ -248,28 +253,29 @@ class AOEDriver(VolumeDriver): # just wait a bit for the current volume to # be ready and ignore any errors. time.sleep(2) - self._execute('sudo', 'vblade-persist', 'auto', 'all', - check_exit_code=False) - self._execute('sudo', 'vblade-persist', 'start', 'all', - check_exit_code=False) + self._execute('vblade-persist', 'auto', 'all', + check_exit_code=False, run_as_root=True) + self._execute('vblade-persist', 'start', 'all', + check_exit_code=False, run_as_root=True) def remove_export(self, context, volume): """Removes an export for a logical volume.""" (shelf_id, blade_id) = self.db.volume_get_shelf_and_blade(context, volume['id']) - self._try_execute('sudo', 'vblade-persist', 'stop', - shelf_id, blade_id) - self._try_execute('sudo', 'vblade-persist', 'destroy', - shelf_id, blade_id) + self._try_execute('vblade-persist', 'stop', + shelf_id, blade_id, run_as_root=True) + self._try_execute('vblade-persist', 'destroy', + shelf_id, blade_id, run_as_root=True) def discover_volume(self, context, _volume): """Discover volume on a remote host.""" (shelf_id, blade_id) = self.db.volume_get_shelf_and_blade(context, _volume['id']) - self._execute('sudo', 'aoe-discover') - out, err = self._execute('sudo', 'aoe-stat', check_exit_code=False) + self._execute('aoe-discover', run_as_root=True) + out, err = self._execute('aoe-stat', check_exit_code=False, + run_as_root=True) device_path = 'e%(shelf_id)d.%(blade_id)d' % locals() if out.find(device_path) >= 0: return "/dev/etherd/%s" % device_path @@ -285,8 +291,8 @@ class AOEDriver(VolumeDriver): (shelf_id, blade_id) = self.db.volume_get_shelf_and_blade(context, volume_id) - cmd = ('sudo', 'vblade-persist', 'ls', '--no-header') - out, _err = self._execute(*cmd) + cmd = ('vblade-persist', 'ls', '--no-header') + out, _err = self._execute(*cmd, run_as_root=True) exported = False for line in out.split('\n'): param = line.split(' ') @@ -348,16 +354,18 @@ class ISCSIDriver(VolumeDriver): iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name']) - self._sync_exec('sudo', 'ietadm', '--op', 'new', + self._sync_exec('ietadm', '--op', 'new', "--tid=%s" % iscsi_target, '--params', "Name=%s" % iscsi_name, + run_as_root=True, check_exit_code=False) - self._sync_exec('sudo', 'ietadm', '--op', 'new', + self._sync_exec('ietadm', '--op', 'new', "--tid=%s" % iscsi_target, '--lun=0', '--params', "Path=%s,Type=fileio" % volume_path, + run_as_root=True, check_exit_code=False) def _ensure_iscsi_targets(self, context, host): @@ -378,13 +386,13 @@ class ISCSIDriver(VolumeDriver): volume['host']) iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name']) - self._execute('sudo', 'ietadm', '--op', 'new', + self._execute('ietadm', '--op', 'new', '--tid=%s' % iscsi_target, - '--params', 'Name=%s' % iscsi_name) - self._execute('sudo', 'ietadm', '--op', 'new', + '--params', 'Name=%s' % iscsi_name, run_as_root=True) + self._execute('ietadm', '--op', 'new', '--tid=%s' % iscsi_target, '--lun=0', '--params', - 'Path=%s,Type=fileio' % volume_path) + 'Path=%s,Type=fileio' % volume_path, run_as_root=True) def remove_export(self, context, volume): """Removes an export for a logical volume.""" @@ -399,18 +407,18 @@ class ISCSIDriver(VolumeDriver): try: # ietadm show will exit with an error # this export has already been removed - self._execute('sudo', 'ietadm', '--op', 'show', - '--tid=%s' % iscsi_target) + self._execute('ietadm', '--op', 'show', + '--tid=%s' % iscsi_target, run_as_root=True) except Exception as e: LOG.info(_("Skipping remove_export. No iscsi_target " + "is presently exported for volume: %d"), volume['id']) return - self._execute('sudo', 'ietadm', '--op', 'delete', + self._execute('ietadm', '--op', 'delete', '--tid=%s' % iscsi_target, - '--lun=0') - self._execute('sudo', 'ietadm', '--op', 'delete', - '--tid=%s' % iscsi_target) + '--lun=0', run_as_root=True) + self._execute('ietadm', '--op', 'delete', + '--tid=%s' % iscsi_target, run_as_root=True) def _do_iscsi_discovery(self, volume): #TODO(justinsb): Deprecate discovery and use stored info @@ -419,8 +427,9 @@ class ISCSIDriver(VolumeDriver): volume_name = volume['name'] - (out, _err) = self._execute('sudo', 'iscsiadm', '-m', 'discovery', - '-t', 'sendtargets', '-p', volume['host']) + (out, _err) = self._execute('iscsiadm', '-m', 'discovery', + '-t', 'sendtargets', '-p', volume['host'], + run_as_root=True) for target in out.splitlines(): if FLAGS.iscsi_ip_prefix in target and volume_name in target: return target @@ -483,10 +492,10 @@ class ISCSIDriver(VolumeDriver): return properties def _run_iscsiadm(self, iscsi_properties, iscsi_command): - (out, err) = self._execute('sudo', 'iscsiadm', '-m', 'node', '-T', + (out, err) = self._execute('iscsiadm', '-m', 'node', '-T', iscsi_properties['target_iqn'], '-p', iscsi_properties['target_portal'], - iscsi_command) + iscsi_command, run_as_root=True) LOG.debug("iscsiadm %s: stdout=%s stderr=%s" % (iscsi_command, out, err)) return (out, err) @@ -560,8 +569,8 @@ class ISCSIDriver(VolumeDriver): tid = self.db.volume_get_iscsi_target_num(context, volume_id) try: - self._execute('sudo', 'ietadm', '--op', 'show', - '--tid=%(tid)d' % locals()) + self._execute('ietadm', '--op', 'show', + '--tid=%(tid)d' % locals(), run_as_root=True) except exception.ProcessExecutionError, e: # Instances remount read-only in this case. # /etc/init.d/iscsitarget restart and rebooting nova-volume diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 292bbce12..cd9694ce1 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -39,6 +39,7 @@ import pluginlib_nova as pluginlib pluginlib.configure_logging("xenhost") host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)") +config_file_path = "/usr/etc/xenhost.conf" def jsonify(fnc): @@ -103,6 +104,104 @@ def set_host_enabled(self, arg_dict): return {"status": status} +def _write_config_dict(dct): + conf_file = file(config_file_path, "w") + json.dump(dct, conf_file) + conf_file.close() + + +def _get_config_dict(): + """Returns a dict containing the key/values in the config file. + If the file doesn't exist, it is created, and an empty dict + is returned. + """ + try: + conf_file = file(config_file_path) + config_dct = json.load(conf_file) + conf_file.close() + except IOError: + # File doesn't exist + config_dct = {} + # Create the file + _write_config_dict(config_dct) + return config_dct + + +@jsonify +def get_config(self, arg_dict): + """Return the value stored for the specified key, or None if no match.""" + conf = _get_config_dict() + params = arg_dict["params"] + try: + dct = json.loads(params) + except Exception, e: + dct = params + key = dct["key"] + ret = conf.get(key) + if ret is None: + # Can't jsonify None + return "None" + return ret + + +@jsonify +def set_config(self, arg_dict): + """Write the specified key/value pair, overwriting any existing value.""" + conf = _get_config_dict() + params = arg_dict["params"] + try: + dct = json.loads(params) + except Exception, e: + dct = params + key = dct["key"] + val = dct["value"] + if val is None: + # Delete the key, if present + conf.pop(key, None) + else: + conf.update({key: val}) + _write_config_dict(conf) + + +def _power_action(action): + host_uuid = _get_host_uuid() + # Host must be disabled first + result = _run_command("xe host-disable") + if result: + raise pluginlib.PluginError(result) + # All running VMs must be shutdown + result = _run_command("xe vm-shutdown --multiple power-state=running") + if result: + raise pluginlib.PluginError(result) + cmds = {"reboot": "xe host-reboot", "startup": "xe host-power-on", + "shutdown": "xe host-shutdown"} + result = _run_command(cmds[action]) + # Should be empty string + if result: + raise pluginlib.PluginError(result) + return {"power_action": action} + + +@jsonify +def host_reboot(self, arg_dict): + """Reboots the host.""" + return _power_action("reboot") + + +@jsonify +def host_shutdown(self, arg_dict): + """Reboots the host.""" + return _power_action("shutdown") + + +@jsonify +def host_start(self, arg_dict): + """Starts the host. Currently not feasible, since the host + runs on the same machine as Xen. + """ + return _power_action("startup") + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -115,6 +214,9 @@ def host_data(self, arg_dict): # We have the raw dict of values. Extract those that we need, # and convert the data types as needed. ret_dict = cleanup(parsed_data) + # Add any config settings + config = _get_config_dict() + ret_dict.update(config) return ret_dict @@ -217,4 +319,9 @@ def cleanup(dct): if __name__ == "__main__": XenAPIPlugin.dispatch( {"host_data": host_data, - "set_host_enabled": set_host_enabled}) + "set_host_enabled": set_host_enabled, + "host_shutdown": host_shutdown, + "host_reboot": host_reboot, + "host_start": host_start, + "get_config": get_config, + "set_config": set_config}) @@ -123,7 +123,6 @@ setup(name='nova', 'bin/nova-console', 'bin/nova-dhcpbridge', 'bin/nova-direct-api', - 'bin/nova-import-canonical-imagestore', 'bin/nova-logspool', 'bin/nova-manage', 'bin/nova-network', @@ -133,4 +132,5 @@ setup(name='nova', 'bin/stack', 'bin/nova-volume', 'bin/nova-vncproxy', - 'tools/nova-debug']) + 'tools/nova-debug'], + py_modules=[]) diff --git a/smoketests/openwrt-x86-ext2.image b/smoketests/openwrt-x86-ext2.image Binary files differdeleted file mode 100644 index cd2dfa426..000000000 --- a/smoketests/openwrt-x86-ext2.image +++ /dev/null diff --git a/smoketests/openwrt-x86-vmlinuz b/smoketests/openwrt-x86-vmlinuz Binary files differdeleted file mode 100644 index 59cc9bb1f..000000000 --- a/smoketests/openwrt-x86-vmlinuz +++ /dev/null diff --git a/smoketests/random.image b/smoketests/random.image Binary files differnew file mode 100644 index 000000000..f2c0c30bb --- /dev/null +++ b/smoketests/random.image diff --git a/smoketests/random.kernel b/smoketests/random.kernel Binary files differnew file mode 100644 index 000000000..01a6284dd --- /dev/null +++ b/smoketests/random.kernel diff --git a/smoketests/test_netadmin.py b/smoketests/test_netadmin.py index 60086f065..8c8fa35b8 100644 --- a/smoketests/test_netadmin.py +++ b/smoketests/test_netadmin.py @@ -109,13 +109,17 @@ class SecurityGroupTests(base.UserSmokeTestCase): def __public_instance_is_accessible(self): id_url = "latest/meta-data/instance-id" - options = "-s --max-time 1" + options = "-f -s --max-time 1" command = "curl %s %s/%s" % (options, self.data['public_ip'], id_url) - instance_id = commands.getoutput(command).strip() + status, output = commands.getstatusoutput(command) + instance_id = output.strip() + if status > 0: + return False if not instance_id: return False if instance_id != self.data['instance'].id: - raise Exception("Wrong instance id") + raise Exception("Wrong instance id. Expected: %s, Got: %s" % + (self.data['instance'].id, instance_id)) return True def test_001_can_create_security_group(self): diff --git a/smoketests/test_sysadmin.py b/smoketests/test_sysadmin.py index 454f6f1d5..29cda1a9b 100644 --- a/smoketests/test_sysadmin.py +++ b/smoketests/test_sysadmin.py @@ -35,9 +35,9 @@ from smoketests import flags from smoketests import base FLAGS = flags.FLAGS -flags.DEFINE_string('bundle_kernel', 'openwrt-x86-vmlinuz', +flags.DEFINE_string('bundle_kernel', 'random.kernel', 'Local kernel file to use for bundling tests') -flags.DEFINE_string('bundle_image', 'openwrt-x86-ext2.image', +flags.DEFINE_string('bundle_image', 'random.image', 'Local image file to use for bundling tests') TEST_PREFIX = 'test%s' % int(random.random() * 1000000) diff --git a/tools/pip-requires b/tools/pip-requires index 23e707034..60b502ffd 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -9,7 +9,8 @@ boto==1.9b carrot==0.10.5 eventlet lockfile==0.8 -python-novaclient==2.5.9 +lxml==2.3 +python-novaclient==2.6.0 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 |
