From fe97e74eea29c990038c0dd36012054d34d704ac Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Fri, 6 May 2011 15:57:01 +0100 Subject: Changes to allow a VM to boot from iso image. A blank HD is also attached with a size corresponding to the instance type. --- nova/virt/xenapi/vm_utils.py | 104 +++++++++++++++++++++++++++++++++++++++++-- nova/virt/xenapi/vmops.py | 17 ++++++- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index d2045a557..60dddb263 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -74,12 +74,14 @@ class ImageType: 2 - raw disk image (local SR, NOT partitioned by plugin) 3 - vhd disk image (local SR, NOT inspected by XS, PV assumed for linux, HVM assumed for Windows) + 4 - ISO disk image (local SR, NOT partitioned by plugin) """ KERNEL_RAMDISK = 0 DISK = 1 DISK_RAW = 2 DISK_VHD = 3 + DISK_ISO = 4 class VMHelper(HelperBase): @@ -204,6 +206,30 @@ class VMHelper(HelperBase): ' VDI %(vdi_ref)s.') % locals()) 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""" @@ -369,6 +395,23 @@ class VMHelper(HelperBase): task = session.async_call_plugin('glance', 'upload_vhd', kwargs) 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, session, instance_id, image, user, project, image_type): @@ -437,7 +480,12 @@ class VMHelper(HelperBase): # FIXME(sirp): Since the Glance plugin seems to be required for the # VHD disk, it may be worth using the plugin for both VHD and RAW and # DISK restores - sr_ref = safe_find_sr(session) + sr_ref = None + 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) client = glance.client.Client(FLAGS.glance_host, FLAGS.glance_port) meta, image_file = client.get_image(image) @@ -452,6 +500,8 @@ class VMHelper(HelperBase): name_label = get_name_label_for_image(image) vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False) + LOG.debug(_("Loading image of size %(virtual_size)d. Full set of" + "properties is %(meta)s" % locals())) with_vdi_attached_here(session, vdi_ref, False, lambda dev: _stream_disk(dev, image_type, @@ -490,7 +540,8 @@ class VMHelper(HelperBase): pretty_format = {ImageType.KERNEL_RAMDISK: 'KERNEL_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_id = instance.image_id instance_id = instance.id @@ -503,7 +554,8 @@ class VMHelper(HelperBase): 'aki': ImageType.KERNEL_RAMDISK, 'ari': ImageType.KERNEL_RAMDISK, 'raw': ImageType.DISK_RAW, - 'vhd': ImageType.DISK_VHD} + 'vhd': ImageType.DISK_VHD, + 'iso': ImageType.DISK_ISO} client = glance.client.Client(FLAGS.glance_host, FLAGS.glance_port) meta = client.get_image_meta(instance.image_id) disk_format = meta['disk_format'] @@ -579,9 +631,11 @@ class VMHelper(HelperBase): available 4. Glance (DISK): pv is assumed + + 5. Glance (ISO): use 'os_type', raise if not set """ if FLAGS.xenapi_image_service == 'glance': - # 2, 3, 4: Glance + # 2, 3, 4, 5: Glance return cls._determine_is_pv_glance( session, vdi_ref, disk_image_type, os_type) else: @@ -618,6 +672,8 @@ class VMHelper(HelperBase): available 4. Glance (DISK): pv is assumed + + 5. Glance (DISK_ISO): no pv is assumed """ LOG.debug(_("Looking up vdi %s for PV kernel"), vdi_ref) @@ -633,6 +689,9 @@ class VMHelper(HelperBase): elif disk_image_type == ImageType.DISK: # 4. Disk is_pv = True + elif disk_image_type == ImageType.DISK_ISO: + # 5. ISO + is_pv = False else: raise exception.Error(_("Unknown image format %(disk_image_type)s") % locals()) @@ -877,6 +936,43 @@ 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()) + #TODO: use ['other_config']['il8n-key-iso-sr'] == 'local-storage' + # or something similar to identify the iso sr + if not (sr_rec['content_type'] == 'iso' and + sr_rec['name_label'] == 'Local ISOs'): + LOG.debug(_("ISO: No match")) + 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 diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 7c7aa8e98..a2ce38c7a 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -162,7 +162,22 @@ class VMOps(object): vm_ref = VMHelper.create_vm(self._session, instance, kernel, ramdisk, use_pv_kernel) - VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, + # 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") + # device 0 reserved for RW disk, so use '1' + cd_vdi_ref = vdi_ref + VMHelper.create_cd_vbd(session=self._session, vm_ref=vm_ref, + vdi_ref=cd_vdi_ref, userdevice=1, bootable=True) + + 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=vdi_ref, userdevice=0, bootable=False) + else: + VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, vdi_ref=vdi_ref, userdevice=0, bootable=True) # TODO(tr3buchet) - check to make sure we have network info, otherwise -- cgit From 11a7736f32b0f26fb2dc496b495aee682bf1bf18 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Fri, 6 May 2011 16:54:57 +0100 Subject: New author in town. --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index f4b40a853..427ada3bc 100644 --- a/Authors +++ b/Authors @@ -18,6 +18,7 @@ Dan Prince David Pravec Dean Troyer Devin Carlen +Donal Lafferty Ed Leafe Eldar Nugaev Eric Day -- cgit From 09bd503a9842857480bd4703d27335e83dd30571 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Sat, 11 Jun 2011 19:48:48 +0900 Subject: block migration feature added --- bin/nova-manage | 36 +++++++++- nova/compute/manager.py | 55 ++++++++++++--- nova/db/api.py | 21 ------ nova/db/sqlalchemy/api.py | 39 ----------- nova/db/sqlalchemy/models.py | 16 ++--- nova/exception.py | 5 ++ nova/scheduler/driver.py | 120 +++++++++++++++++++++++++------- nova/scheduler/manager.py | 54 ++++++++------- nova/tests/scheduler/test_scheduler.py | 52 ++++++++++---- nova/tests/test_compute.py | 6 +- nova/tests/test_libvirt.py | 90 ++++++++++++++++++++++++ nova/virt/libvirt/connection.py | 122 +++++++++++++++++++++++++++------ 12 files changed, 449 insertions(+), 167 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index b0cd343f5..220f6e77a 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -649,11 +649,12 @@ class VmCommands(object): instance['availability_zone'], instance['launch_index']) - def live_migration(self, ec2_id, dest): + 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. """ @@ -676,11 +677,30 @@ 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 + def live_migration(self, ec2_id, dest): + """Migrates a running instance to a new machine. + + :param ec2_id: instance id which comes from euca-describe-instance. + :param dest: destination host name. + + """ + self._migration(ec2_id, dest) + + def block_migration(self, ec2_id, dest): + """Migrates a running instance to a new machine with storage data. + + :param ec2_id: instance id which comes from euca-describe-instance. + :param dest: destination host name. + + """ + self._migration(ec2_id, dest, True) + class ServiceCommands(object): """Enable and disable running services""" @@ -749,9 +769,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, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 245958de7..f7604a253 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -921,11 +921,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, **kwargs): """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: @@ -977,17 +979,24 @@ class ComputeManager(manager.SchedulerDependentManager): # onto destination host. self.driver.ensure_filtering_rules_for_instance(instance_ref) - def live_migration(self, context, instance_id, dest): + # Preparation for block migration + if block_migration: + self.driver.pre_block_migration(context, + instance_ref, + kwargs.get('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 @@ -998,13 +1007,20 @@ class ComputeManager(manager.SchedulerDependentManager): {"method": "check_for_export", "args": {'instance_id': instance_id}}) - # Asking dest host to preparing live migration. + args = {} + args['instance_id'] = instance_id + if block_migration: + args['block_migration'] = block_migration + args['disk'] = \ + self.driver.get_instance_disk_info(context, instance_ref) + rpc.call(context, self.db.queue_get_for(context, FLAGS.compute_topic, dest), {"method": "pre_live_migration", - "args": {'instance_id': instance_id}}) + "args": args}) 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) @@ -1015,9 +1031,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.recover_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 @@ -1068,6 +1086,10 @@ class ComputeManager(manager.SchedulerDependentManager): # Restore instance/volume state self.recover_live_migration(ctxt, instance_ref, dest) + # 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) LOG.info(_('Migrating %(i_name)s to %(dest)s finished successfully.') % locals()) @@ -1075,14 +1097,20 @@ 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): + def recover_live_migration(self, ctxt, instance_ref, host=None, + dest=None, delete=True): """Recovers Instance/volume state from migrating -> running. :param ctxt: 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 dest: + This method is called from live migration src host. + This param specifies destination host. + :param delete: + If true, ask destination host to remove instance dir, + since empty disk image was created for block migration """ if not host: host = instance_ref['host'] @@ -1101,6 +1129,15 @@ class ComputeManager(manager.SchedulerDependentManager): if dest: volume_api.remove_from_compute(ctxt, volume_id, dest) + # TODO: Block migration needs empty image at destination host + # before migration starts, so if any failure occurs, + # any empty images has to be deleted. but not sure adding below + # method is appropreate here. for now, admin has to delete manually. + # rpc.call(ctxt, + # self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), + # {"method": "self.driver.destroy", + # "args": {'instance':instance_ref}) + def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" error_list = super(ComputeManager, self).periodic_tasks(context) diff --git a/nova/db/api.py b/nova/db/api.py index 4e0aa60a2..b4cc110b1 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -483,27 +483,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) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 103668b94..00c61bfe3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1040,45 +1040,6 @@ def instance_add_security_group(context, instance_id, security_group_id): instance_ref.save(session=session) -@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.""" diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 55efe6886..1e057516b 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -125,14 +125,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: # diff --git a/nova/exception.py b/nova/exception.py index 69b3e0359..80e8293b6 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -211,6 +211,11 @@ class DestinationHypervisorTooOld(Invalid): "has been provided.") +class DestinatioinDiskExists(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.") diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 0b257c5d8..889baa567 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -77,7 +77,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: @@ -88,7 +89,6 @@ class Scheduler(object): Then scheduler send request that host. """ - # Whether instance exists and is running. instance_ref = db.instance_get(context, instance_id) @@ -96,10 +96,12 @@ 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, @@ -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 @@ -175,9 +178,11 @@ class Scheduler(object): # 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,19 @@ 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: + raise + except rpc.RemoteError: + if not block_migration: + raise # Checking dest exists. dservice_refs = db.service_get_all_compute_by_host(context, dest) @@ -229,14 +242,24 @@ 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,22 +267,69 @@ class Scheduler(object): """ - # Getting instance information - ec2_id = 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 %(ec2_id)s to destination: %(dest)s " - "(host:%(mem_avail)s <= instance:%(mem_inst)s)") + avail = avail - used + if avail <= mem_inst: + ec2_id = instance_ref['hostname'] + reason = _("Unable to migrate %(ec2_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: + ec2_id = instance_ref['hostname'] + reason = _("Unable to migrate %(ec2_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. diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index bd40e73c0..1f663ca4b 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -97,7 +97,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 @@ -105,43 +105,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': 10240, + 'local_gb': 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/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 50b6b52c6..384f6fb00 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -652,10 +652,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}) @@ -663,7 +666,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') @@ -744,7 +748,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']) @@ -757,7 +761,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']) @@ -765,15 +769,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', 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') + 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): @@ -785,7 +807,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']) @@ -820,7 +843,7 @@ class SimpleDriverTestCase(test.TestCase): self.mox.ReplayAll() self.assertRaises(exception.SourceHostUnavailable, 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']) @@ -844,7 +867,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']) @@ -870,7 +893,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']) @@ -902,7 +925,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) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index b4ac2dbc4..1f48a6dce 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -545,7 +545,8 @@ class ComputeTestCase(test.TestCase): 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.recover_live_migration, + False) self.compute.db = dbmock self.mox.ReplayAll() @@ -622,7 +623,8 @@ class ComputeTestCase(test.TestCase): 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.recover_live_migration, + False) self.compute.db = dbmock self.mox.ReplayAll() diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index b6b36745a..000b383f2 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -20,6 +20,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 @@ -674,6 +675,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": 10, "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), 10) + + self.mox.ReplayAll() + conn = connection.LibvirtConnection(False) + conn.pre_block_migration(self.context, instance_ref, + dummyjson % tmpdir) + + self.assertTrue(os.path.exists('%s/%s/libvirt.xml' % + (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 = ("instance-0000000a" + "" + "" + "" + "" + "" + "" + "" + "") + + 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'] == 10 and + info[1]['local_gb'] == 20) + + 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(): diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index c491418ae..86e388ff7 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -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_DISK", + 'Define block migration behavior.') flags.DEFINE_integer('live_migration_bandwidth', 0, 'Define live migration behavior') flags.DEFINE_string('qemu_img', 'qemu-img', @@ -727,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): @@ -1458,7 +1463,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 @@ -1466,20 +1471,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 @@ -1498,24 +1505,18 @@ 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) @@ -1530,11 +1531,92 @@ 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.DestinatioinDiskExists(path=instance_dir) + os.mkdir(instance_dir) + + for disk in disk_info: + base = os.path.basename(disk['path']) + # Get image type and create empty disk image. + instance_disk = os.path.join(instance_dir, base) + utils.execute('sudo', 'qemu-img', 'create', '-f', disk['type'], + instance_disk, str(disk['local_gb'])+'G') + + # block migration does not migrate libvirt.xml, + # to avoid any confusion of admins, create it now. + xml = self.to_xml(instance_ref) + f = open(os.path.join(instance_dir, 'libvirt.xml'), 'w+') + f.write(xml) + f.close() + + 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':10},...]" + + """ + 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 + + # xml is generated by kvm, so format is slightly different + # from libvirt.xml that nova generated. + #disk_type = driver_nodes[cnt].get_properties().getContent() + disk_type = \ + driver_nodes[cnt].get_properties().get_next().getContent() + if disk_type == 'raw': + size = int(os.path.getsize(path)) / 1024 / 1024 / 1024 + 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]) / 1024 / 1024 / 1024 + + disk_info.append({'type': disk_type, 'path': path, + 'local_gb': size}) + + return utils.dumps(disk_info) + def unfilter_instance(self, instance_ref): """See comments of same method in firewall_driver.""" self.firewall_driver.unfilter_instance(instance_ref) -- cgit From c16e1af623ef8baa1098510a8a04adb06f2da81b Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Wed, 15 Jun 2011 01:35:54 +0900 Subject: added kernel/ramdisk migrate support --- nova/virt/libvirt/connection.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 86e388ff7..73ca6e0d9 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1555,12 +1555,28 @@ class LibvirtConnection(driver.ComputeDriver): raise exception.DestinatioinDiskExists(path=instance_dir) os.mkdir(instance_dir) - for disk in disk_info: - base = os.path.basename(disk['path']) + 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', disk['type'], - instance_disk, str(disk['local_gb'])+'G') + utils.execute('sudo', 'qemu-img', 'create', '-f', info['type'], + instance_disk, str(info['local_gb']) + 'G') + + # 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(os.path.join(instance_dir, 'kernel'), + instance_ref['kernel_id'], + user, + project) + if instance_ref['ramdisk_id']: + self._fetch_image(os.path.join(instance_dir, 'ramdisk'), + instance_ref['ramdisk_id'], + user, + project) # block migration does not migrate libvirt.xml, # to avoid any confusion of admins, create it now. -- cgit From b03b3145a18f8f4717fdc55ab50dc714516d2c54 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Mon, 20 Jun 2011 08:32:34 +0900 Subject: fix comments at nova.virt.libvirt.connection --- nova/virt/libvirt/connection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 313b19194..8fe8148d5 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1617,9 +1617,10 @@ class LibvirtConnection(driver.ComputeDriver): locals()) continue - # xml is generated by kvm, so format is slightly different - # from libvirt.xml that nova generated. - #disk_type = driver_nodes[cnt].get_properties().getContent() + # 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': -- cgit From 9b52343f792d83647978c7edbfe700258e3ddae2 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Mon, 20 Jun 2011 08:51:25 +0900 Subject: fix pep8 check --- nova/compute/manager.py | 2 +- nova/virt/libvirt/connection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index f7604a253..4410ff27e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1133,7 +1133,7 @@ class ComputeManager(manager.SchedulerDependentManager): # before migration starts, so if any failure occurs, # any empty images has to be deleted. but not sure adding below # method is appropreate here. for now, admin has to delete manually. - # rpc.call(ctxt, + # rpc.cast(ctxt, # self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), # {"method": "self.driver.destroy", # "args": {'instance':instance_ref}) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 8fe8148d5..d0c52c763 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1617,7 +1617,7 @@ class LibvirtConnection(driver.ComputeDriver): locals()) continue - # In case of libvirt.xml, disk type can be obtained + # 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. -- cgit From a6d527646184889863de5ab1082695a29f70988a Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Mon, 20 Jun 2011 09:43:34 +0900 Subject: nova.virt.libvirt.connection._live_migration is changed --- nova/compute/manager.py | 20 +++++++++----------- nova/virt/libvirt/connection.py | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 402d5829e..f32dfe6ab 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1220,8 +1220,7 @@ 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, delete=True): + def recover_live_migration(self, ctxt, instance_ref, host=None, dest=None): """Recovers Instance/volume state from migrating -> running. :param ctxt: security context @@ -1231,9 +1230,6 @@ class ComputeManager(manager.SchedulerDependentManager): :param dest: This method is called from live migration src host. This param specifies destination host. - :param delete: - If true, ask destination host to remove instance dir, - since empty disk image was created for block migration """ if not host: host = instance_ref['host'] @@ -1254,12 +1250,14 @@ class ComputeManager(manager.SchedulerDependentManager): # TODO: Block migration needs empty image at destination host # before migration starts, so if any failure occurs, - # any empty images has to be deleted. but not sure adding below - # method is appropreate here. for now, admin has to delete manually. - # rpc.cast(ctxt, - # self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), - # {"method": "self.driver.destroy", - # "args": {'instance':instance_ref}) + # any empty images has to be deleted. + # In current version argument dest != None means this method is + # called for error recovering + #if dest: + # rpc.cast(ctxt, + # self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), + # {"method": "self.driver.destroy", + # "args": {'instance':instance_ref}) def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index b0517e32a..2ddaa5971 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1573,7 +1573,7 @@ class LibvirtConnection(driver.ComputeDriver): self.get_info(instance_ref.name)['state'] except exception.NotFound: timer.stop() - post_method(ctxt, instance_ref, dest, block_migration) + post_method(ctxt, instance_ref, dest) timer.f = wait_for_live_migration timer.start(interval=0.5, now=True) -- cgit From c184fa5d03f3f8d7faaff7b583404874de409aa6 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Tue, 21 Jun 2011 20:51:07 +0900 Subject: fixed reviewer's comment. 1. adding dest-instance-dir deleting operation to nova.compute.manager, 2. fix invalid raise statement --- nova/compute/manager.py | 36 +++++++++++++++++++++++------------- nova/exception.py | 4 ++++ nova/scheduler/driver.py | 3 ++- nova/tests/test_compute.py | 17 +++++++++++++---- nova/tests/test_libvirt.py | 6 +++--- nova/virt/libvirt/connection.py | 14 ++++++++++++-- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index f32dfe6ab..eac543251 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1045,7 +1045,7 @@ class ComputeManager(manager.SchedulerDependentManager): return self.driver.update_available_resource(context, self.host) def pre_live_migration(self, context, instance_id, time=None, - block_migration=False, **kwargs): + block_migration=False, disk=None): """Preparations for live migration at dest host. :param context: security context @@ -1106,7 +1106,7 @@ class ComputeManager(manager.SchedulerDependentManager): if block_migration: self.driver.pre_block_migration(context, instance_ref, - kwargs.get('disk')) + disk) def live_migration(self, context, instance_id, dest, block_migration=False): @@ -1130,17 +1130,18 @@ class ComputeManager(manager.SchedulerDependentManager): {"method": "check_for_export", "args": {'instance_id': instance_id}}) - args = {} - args['instance_id'] = instance_id if block_migration: - args['block_migration'] = block_migration - args['disk'] = \ - self.driver.get_instance_disk_info(context, instance_ref) + 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": args}) + "args": {'instance_id': instance_id, + 'block_migration': block_migration, + 'disk': disk}}) except Exception: i_name = instance_ref.name @@ -1253,11 +1254,20 @@ class ComputeManager(manager.SchedulerDependentManager): # any empty images has to be deleted. # In current version argument dest != None means this method is # called for error recovering - #if dest: - # rpc.cast(ctxt, - # self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), - # {"method": "self.driver.destroy", - # "args": {'instance':instance_ref}) + if dest: + rpc.cast(ctxt, + self.db.queue_get_for(ctxt, FLAGS.compute_topic, dest), + {"method": "cleanup", + "args": {'instance_id': instance_ref['id']}}) + + def cleanup(self, ctxt, instance_id): + """ Cleaning up image directory that is created pre_live_migration. + + :param ctxt: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + """ + instances_ref = self.db.instance_get(ctxt, instance_id) + self.driver.cleanup(instance_ref) def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" diff --git a/nova/exception.py b/nova/exception.py index 689a797fd..4ebf0e169 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -588,6 +588,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/scheduler/driver.py b/nova/scheduler/driver.py index 889baa567..55762cf85 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -200,7 +200,8 @@ class Scheduler(object): try: self.mounted_on_same_shared_storage(context, instance_ref, dest) if block_migration: - raise + reason = "Block migration can not be used with shared storage." + raise exception.InvalidSharedStorage(reason=reason, path=dest) except rpc.RemoteError: if not block_migration: raise diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 840961771..e9f5eb0e8 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -556,7 +556,10 @@ 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, @@ -582,7 +585,9 @@ 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, @@ -609,7 +614,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, @@ -634,7 +641,9 @@ 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, diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 79a2b84b7..2e57dae08 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -734,7 +734,7 @@ class LibvirtConnTestCase(test.TestCase): # large disk space. self.mox.StubOutWithMock(utils, "execute") utils.execute('sudo', 'qemu-img', 'create', '-f', 'raw', - '%s/%s/disk' % (tmpdir, instance_ref.name), 10) + '%s/%s/disk' % (tmpdir, instance_ref.name), '10G') self.mox.ReplayAll() conn = connection.LibvirtConnection(False) @@ -759,10 +759,10 @@ class LibvirtConnTestCase(test.TestCase): instance_ref = db.instance_create(self.context, self.test_instance) dummyxml = ("instance-0000000a" "" - "" + "" "" "" - "" + "" "" "" "") diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 2ddaa5971..8d23df45f 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -120,7 +120,7 @@ flags.DEFINE_string('live_migration_flag', 'Define live migration behavior.') flags.DEFINE_string('block_migration_flag', "VIR_MIGRATE_UNDEFINE_SOURCE, VIR_MIGRATE_PEER2PEER, " - "VIR_MIGRATE_NON_SHARED_DISK", + "VIR_MIGRATE_NON_SHARED_INC", 'Define block migration behavior.') flags.DEFINE_integer('live_migration_bandwidth', 0, 'Define live migration behavior') @@ -295,7 +295,10 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(justinsb): We remove the domain definition. We probably # would do better to keep it if cleanup=False (e.g. volumes?) # (e.g. #2 - not losing machines on failure) - virt_dom.undefine() + # NOTE(masumotok): Migrated instances does not have domain + # definitions. + if instance.name in self._conn.listDefinedDomains(): + virt_dom.undefine() except libvirt.libvirtError as e: errcode = e.get_error_code() LOG.warning(_("Error from libvirt during undefine of " @@ -335,6 +338,13 @@ class LibvirtConnection(driver.ComputeDriver): if os.path.exists(target): shutil.rmtree(target) + def cleanup(self, instance): + """ Cleaning up image directory that is created pre_live_migration. + + :param instance: nova.db.sqlalchemy.models.Instance + """ + self._cleanup(instance) + @exception.wrap_exception def attach_volume(self, instance_name, device_path, mountpoint): virt_dom = self._lookup_by_name(instance_name) -- cgit From e01848ed64c4523bb9e375da07e962b5ea1ea6ee Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Tue, 21 Jun 2011 21:10:08 +0900 Subject: erase unnecessary TODO: statement --- nova/compute/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index eac543251..16dbda47d 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1249,7 +1249,7 @@ class ComputeManager(manager.SchedulerDependentManager): if dest: volume_api.remove_from_compute(ctxt, volume_id, dest) - # TODO: Block migration needs empty image at destination host + # Block migration needs empty image at destination host # before migration starts, so if any failure occurs, # any empty images has to be deleted. # In current version argument dest != None means this method is -- cgit From 0a3b50d7b754af26b68f81617cab9aa588484362 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Mon, 27 Jun 2011 16:03:14 +0100 Subject: Pulled changes, passed the unit tests. --- .../migrate_repo/versions/027_add_provider_firewall_rules.py | 3 +-- nova/virt/xenapi/vmops.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py index 5aa30f7a8..cb3c73170 100644 --- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py +++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py @@ -58,8 +58,7 @@ provider_fw_rules = Table('provider_fw_rules', meta, Column('to_port', Integer()), Column('cidr', String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False)) - ) + unicode_error=None, _warn_on_bytestring=False))) def upgrade(migrate_engine): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 5cf99b9ac..eef354aec 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -196,7 +196,7 @@ class VMOps(object): kernel, ramdisk, use_pv_kernel) # device 0 reserved for RW disk - userdevice = 0; + userdevice = 0 # DISK_ISO needs two VBDs: the ISO disk and a blank RW disk if disk_image_type == ImageType.DISK_ISO: @@ -214,15 +214,12 @@ class VMOps(object): userdevice = userdevice + 2 VMHelper.create_cd_vbd(session=self._session, vm_ref=vm_ref, vdi_ref=cd_vdi_ref, userdevice=userdevice, bootable=True) - - else: VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, vdi_ref=first_vdi_ref, userdevice=userdevice, bootable=True) # userdevice 1 is reserved for rescue userdevice = userdevice + 1 - # Attach any other disks for vdi in vdis[1:]: # vdi['vdi_type'] is either 'os' or 'swap', but we don't -- cgit From 32b06d654922e08a53c6a4fc49fd2ad40e7a5d20 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Tue, 28 Jun 2011 12:32:23 +0100 Subject: Revise key used to identify the SR used to store ISO images streamed from Glance. --- nova/virt/xenapi/vm_utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 0d08e459c..ede8114c4 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -984,12 +984,14 @@ def find_iso_sr(session): sr_rec = session.get_xenapi().SR.get_record(sr_ref) LOG.debug(_("ISO: looking at SR %(sr_rec)s") % locals()) - #TODO: use ['other_config']['il8n-key-iso-sr'] == 'local-storage' - # or something similar to identify the iso sr if not (sr_rec['content_type'] == 'iso' and - sr_rec['name_label'] == 'Local ISOs'): - LOG.debug(_("ISO: No match")) + 'i18n-key' in sr_rec['other_config'] and + sr_rec['other_config']['i18n-key'] == 'local-storage-iso'): + LOG.debug(_("ISO: SR with 'content_type' of 'iso' and" + " 'other-config' key 'i18n-key' of value" + " '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")) -- cgit From 8878f6433cebcac963ed8789200f38a5ac4dfddd Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Tue, 28 Jun 2011 12:33:12 +0100 Subject: Add fake SR with ISO content type. --- nova/virt/xenapi/fake.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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', -- cgit From 6ca1845582334d69474f5f9d661177e77cd769fe Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Tue, 28 Jun 2011 12:34:17 +0100 Subject: Add test for spawn from an ISO. --- nova/tests/glance/stubs.py | 6 ++++++ nova/tests/test_xenapi.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index 1e0b90d82..37fb7a9d7 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): diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index d9a514745..b8f53b735 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -446,6 +446,12 @@ class XenAPIVMTestCase(test.TestCase): os_type="windows", architecture="i386") self.check_vm_params_for_windows() + def test_spawn_iso_glance(self): + FLAGS.xenapi_image_service = 'glance' + 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): FLAGS.xenapi_image_service = 'glance' self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE, -- cgit From 3794d8889ed933fc776d7541ef25e2c9583a6cf6 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Wed, 29 Jun 2011 16:17:33 +0100 Subject: clean up logging for iso SR search --- nova/virt/xenapi/vm_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index ede8114c4..4a0e97e2e 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -984,12 +984,14 @@ def find_iso_sr(session): 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' and - 'i18n-key' in sr_rec['other_config'] and - sr_rec['other_config']['i18n-key'] == 'local-storage-iso'): - LOG.debug(_("ISO: SR with 'content_type' of 'iso' and" - " 'other-config' key 'i18n-key' of value" - " 'local-storage-iso'")) + 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")) -- cgit From 848fd99f378976f99fa883fec85f30c4e9f46bca Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Wed, 29 Jun 2011 17:38:02 +0100 Subject: pep8 fix --- nova/virt/xenapi/vm_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 4a0e97e2e..5f6387acc 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -991,7 +991,8 @@ def find_iso_sr(session): 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'")) + LOG.debug(_("ISO: iso content_type, i18n-key value not " + "'local-storage-iso'")) continue LOG.debug(_("ISO: SR MATCHing our criteria")) -- cgit From 7a8ecdc03f838184b2e6eeac62d7f57ddc64967b Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 8 Jul 2011 01:39:58 -0700 Subject: start of re-work of compute/api's 'get_all' to handle more search options --- nova/compute/api.py | 83 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index b0eedcd64..42ccc9f9e 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -622,34 +622,71 @@ 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_by_reservation_id(self, context, search_opts): + search_opts['recurse_zones'] = True + return self.db.instance_get_all_by_reservation( + context, reservation_id) + + def _get_all_by_fixed_ip(self, context, search_opts): + try: + instances = self.db.fixed_ip_get_instance(context, fixed_ip) + except exception.FloatingIpNotFound, e: + instances = None + return instances + + def _get_all_by_project_id(self, context, search_opts): + return self.db.instance_get_all_by_project( + context, project_id) + + def _get_all_by_ip(self, context, search_opts): + pass + + def _get_all_by_ip6(self, context, search_opts): + pass + + def _get_all_by_name(self, context, search_opts): + pass + + 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: - try: - instances = self.db.fixed_ip_get_instance(context, fixed_ip) - except exception.FloatingIpNotFound, e: - if not recurse_zones: + if search_opts is None: + search_opts = {} + + exclusive_opts = ['reservation_id', + 'project_id', + 'fixed_ip', + 'ip', + 'ip6', + 'name'] + + # See if a valud search option was passed in. + # Ignore unknown search options for possible forward compatability. + # Raise an exception if more than 1 search option is specified + option = None + for k in exclusive_opts.iterkeys(): + v = search_opts.get(k, None) + if v: + if option is None: + option = k + else: raise - instances = None - elif project_id or not context.is_admin: - if not context.project: + + if option: + method_name = '_get_all_by_%s' % option + method = getattr(self, method_name, None) + instances = method(context, search_opts) + elif not context.is_admin: + if context.project: + instances = self.db.instance_get_all_by_project( + context, context.project_id) + else: instances = self.db.instance_get_all_by_user( context, context.user_id) - 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) @@ -658,17 +695,15 @@ class API(base.Base): elif not isinstance(instances, list): instances = [instances] - if not recurse_zones: + if not search_opts.get('recurse_zones', False): return instances + # Recurse zones. Need admin context for this. admin_context = context.elevated() children = scheduler_api.call_zone_method(admin_context, "list", novaclient_collection_name="servers", - reservation_id=reservation_id, - project_id=project_id, - fixed_ip=fixed_ip, - recurse_zones=True) + **search_opts) for zone, servers in children: for server in servers: -- cgit From 04b50db56ee90c0f4dd685a8f45883522260164f Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 11 Jul 2011 14:27:01 -0700 Subject: Replace 'like' support with 'regexp' matching done in python. Since 'like' would result in a full table scan anyway, this is a bit more flexible. Make search options and matching a little more generic Return 404 when --fixed_ip doesn't match any instance, instead of a 500 only when the IP isn't in the FixedIps table. --- nova/api/ec2/cloud.py | 23 +++- nova/api/openstack/servers.py | 20 ++-- nova/compute/api.py | 75 ++++++++----- nova/db/api.py | 44 ++++++-- nova/db/sqlalchemy/api.py | 239 +++++++++++++++++++++++++++++++----------- nova/db/sqlalchemy/models.py | 1 + 6 files changed, 292 insertions(+), 110 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9be30cf75..9efbb5985 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -118,8 +118,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']) @@ -145,7 +146,12 @@ class CloudController(object): def get_metadata(self, address): ctxt = context.get_admin_context() - instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address) + 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 instance_ref is None: return None @@ -816,11 +822,18 @@ 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, + instance_id=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=kwargs) + except exception.NotFound: + instances = [] for instance in instances: if not context.is_admin: if instance['image_ref'] == str(FLAGS.vpn_image_id): diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fc1ab8d46..d259590a5 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -52,6 +52,8 @@ class Controller(object): servers = self._items(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): @@ -60,6 +62,8 @@ class Controller(object): servers = self._items(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 _get_view_builder(self, req): @@ -77,16 +81,14 @@ class Controller(object): builder - the response model builder """ 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')) + recurse_zones = utils.bool_from_str( + query_str.get('recurse_zones', False)) + # Pass all of the options on to compute's 'get_all' + search_opts = query_str + # Reset this after converting from string to bool + search_opts['recurse_zones'] = recurse_zones 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) + req.environ['nova.context'], search_opts=search_opts) limited_list = self._limit_items(instance_list, req) builder = self._get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] diff --git a/nova/compute/api.py b/nova/compute/api.py index 42ccc9f9e..e80e52566 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -625,27 +625,34 @@ class API(base.Base): def _get_all_by_reservation_id(self, context, search_opts): search_opts['recurse_zones'] = True return self.db.instance_get_all_by_reservation( - context, reservation_id) + context, search_opts['reservation_id']) + + def _get_all_by_project_id(self, context, search_opts): + return self.db.instance_get_all_by_project( + context, search_opts['project_id']) def _get_all_by_fixed_ip(self, context, search_opts): + fixed_ip = search_opts['fixed_ip'] try: - instances = self.db.fixed_ip_get_instance(context, fixed_ip) - except exception.FloatingIpNotFound, e: - instances = None + instances = self.db.instance_get_by_fixed_ip(context, fixed_ip) + except exception.FixedIpNotFound, e: + raise + if not instances: + raise exception.FixedIpNotFoundForAddress(address=fixed_ip) return instances - def _get_all_by_project_id(self, context, search_opts): - return self.db.instance_get_all_by_project( - context, project_id) def _get_all_by_ip(self, context, search_opts): - pass + return self.db.instance_get_all_by_ip_regexp( + context, search_opts['ip']) def _get_all_by_ip6(self, context, search_opts): - pass + return self.db.instance_get_all_by_ipv6_regexp( + context, search_opts['ip6']) - def _get_all_by_name(self, context, search_opts): - pass + def _get_all_by_column(self, context, column, search_opts): + return self.db.instance_get_all_by_column_regexp( + context, column, search_opts[column]) def get_all(self, context, search_opts=None): """Get all instances filtered by one of the given parameters. @@ -657,29 +664,45 @@ class API(base.Base): if search_opts is None: search_opts = {} + LOG.debug(_("Searching by: %s") % str(search_opts)) + + # Columns we can do a generic search on + search_columns = ['display_name', + 'server_name'] + + # Options that are mutually exclusive exclusive_opts = ['reservation_id', 'project_id', 'fixed_ip', 'ip', - 'ip6', - 'name'] + 'ip6'] + search_columns - # See if a valud search option was passed in. + # See if a valid search option was passed in. # Ignore unknown search options for possible forward compatability. # Raise an exception if more than 1 search option is specified - option = None - for k in exclusive_opts.iterkeys(): - v = search_opts.get(k, None) + found_opt = None + for opt in exclusive_opts: + v = search_opts.get(opt, None) if v: - if option is None: - option = k + if found_opt is None: + found_opt = opt else: - raise - - if option: - method_name = '_get_all_by_%s' % option - method = getattr(self, method_name, None) - instances = method(context, search_opts) + LOG.error(_("More than 1 mutually exclusive " + "search option specified (%(found_opt)s and " + "%(opt)s were both specified") % locals()) + raise exception.InvalidInput(reason= + _("More than 1 mutually exclusive " + "search option specified (%(found_opt)s and " + "%(opt)s were both specified") % locals()) + + if found_opt: + if found_opt in search_columns: + instances = self._get_all_by_column(context, + found_opt, search_opts) + else: + method_name = '_get_all_by_%s' % found_opt + method = getattr(self, method_name, None) + instances = method(context, search_opts) elif not context.is_admin: if context.project: instances = self.db.instance_get_all_by_project( @@ -703,7 +726,7 @@ class API(base.Base): children = scheduler_api.call_zone_method(admin_context, "list", novaclient_collection_name="servers", - **search_opts) + search_opts=search_opts) for zone, servers in children: for server in servers: diff --git a/nova/db/api.py b/nova/db/api.py index b7c5700e5..c0f49d98d 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -381,15 +381,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) @@ -515,10 +506,43 @@ 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_all_by_column_regexp(context, column, column_regexp): + """Get all instances by using regular expression matching against + a particular DB column + """ + return IMPL.instance_get_all_by_column_regexp(context, + column, + column_regexp) + + +def instance_get_all_by_ip_regexp(context, ip_regexp): + """Get all instances by using regular expression matching against + Floating and Fixed IP Addresses + """ + return IMPL.instance_get_all_by_ip_regexp(context, ip_regexp) + + +def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): + """Get all instances by using regular expression matching against + IPv6 Addresses + """ + return IMPL.instance_get_all_by_ipv6_regexp(context, ipv6_regexp) + + 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) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ffd009513..af2acbcb3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -18,6 +18,7 @@ """ Implementation of SQLAlchemy backend. """ +import re import traceback import warnings @@ -797,28 +798,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) @@ -1204,30 +1183,166 @@ 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): + 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): + 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_context +def instance_get_all_by_column_regexp(context, column, column_regexp): + """Get all instances by using regular expression matching against + a particular DB column + """ + session = get_session() + + # MySQL 'regexp' is not portable, so we must do our own matching. + # First... grab all Instances. + query = session.query(models.Instance).\ + options(joinedload('metadata')) + if is_admin_context(context): + all_instances = query.\ + filter_by(deleted=can_read_deleted(context)).\ + all() + elif is_user_context(context): + all_instances = query.\ + filter_by(project_id=context.project_id).\ + filter_by(deleted=False).\ + all() + else: + return [] + + if all_instances is None: + all_instances = [] + + # Now do the regexp matching + compiled_regexp = re.compile(column_regexp) + instances = [] + + for instance in all_instances: + v = getattr(instance, column) + if v and compiled_regexp.match(v): + instances.append(instance) + return instances + + +@require_context +def instance_get_all_by_ip_regexp(context, ip_regexp): + """Get all instances by using regular expression matching against + Floating and Fixed IP Addresses + """ + session = get_session() + + fixed_ip_query = session.query(models.FixedIp).\ + options(joinedload('instance.metadata')) + floating_ip_query = session.query(models.FloatingIp).\ + options(joinedload_all('fixed_ip.instance.metadata')) + + # Query both FixedIp and FloatingIp tables to get matches. + # Since someone could theoretically search for something that matches + # instances in both tables... we need to use a dictionary keyed + # on instance ID to make sure we return only 1. We can't key off + # of 'instance' because it's just a reference and will be different + # addresses even though they might point to the same instance ID. + instances = {} + + # MySQL 'regexp' is not portable, so we must do our own matching. + # First... grab all of the IP entries. + if is_admin_context(context): + fixed_ips = fixed_ip_query.\ + filter_by(deleted=can_read_deleted(context)).\ + all() + floating_ips = floating_ip_query.\ + filter_by(deleted=can_read_deleted(context)).\ + all() + elif is_user_context(context): + fixed_ips = fixed_ip_query.filter_by(deleted=False).all() + floating_ips = floating_ip_query.filter_by(deleted=False).all() + else: + return None + + if fixed_ips is None: + fixed_ips = [] + if floating_ips is None: + floating_ips = [] + + compiled_regexp = re.compile(ip_regexp) + instances = {} + + # Now do the regexp matching + for fixed_ip in fixed_ips: + if fixed_ip.instance and compiled_regexp.match(fixed_ip.address): + instances[fixed_ip.instance.uuid] = fixed_ip.instance + for floating_ip in floating_ips: + fixed_ip = floating_ip.fixed_ip + if fixed_ip and fixed_ip.instance and\ + compiled_regexp.match(floating_ip.address): + instances[fixed_ip.instance.uuid] = fixed_ip.instance + + return instances.values() + +@require_context +def instance_get_all_by_ipv6_regex(context, ipv6_regexp): + """Get all instances by using regular expression matching against + IPv6 Addresses + """ + + session = get_session() + with session.begin(): + # get instances + + all_instances = session.query(models.Instance).\ + options(joinedload('metadata')).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + if not all_instances: + return [] + + instances = [] + compiled_regexp = re.compile(ipv6_regexp) + for instance in all_instances: + ipv6_addrs = _ipv6_get_by_instance_ref(context, instance) + for ipv6 in ipv6_addrs: + if compiled_regexp.match(ipv6): + instances.append(instance) + break + return instances @require_admin_context @@ -1258,29 +1373,33 @@ def instance_get_fixed_addresses(context, instance_id): return [fixed_ip.address for fixed_ip in fixed_ips] +def _ipv6_get_by_instance_ref(context, instance_ref): + # assume instance has 1 mac for each network associated with it + # get networks associated with instance + 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)] + # 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 + macs = [vif_ref['address'] for vif_ref in + sorted(vif_refs, key=lambda vif_ref: vif_ref['network_id'])] + # get project id from instance + project_id = instance_ref.project_id + # combine prefixes, macs, and project_id into (prefix,mac,p_id) tuples + prefix_mac_tuples = zip(prefixes, macs, [project_id for m in macs]) + # return list containing ipv6 address for each tuple + return [ipv6.to_global_ipv6(*t) for t in prefix_mac_tuples] + + @require_context def instance_get_fixed_addresses_v6(context, instance_id): session = get_session() with session.begin(): # get instance instance_ref = instance_get(context, instance_id, session=session) - # assume instance has 1 mac for each network associated with it - # get networks associated with instance - 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)] - # 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 - macs = [vif_ref['address'] for vif_ref in - sorted(vif_refs, key=lambda vif_ref: vif_ref['network_id'])] - # get project id from instance - project_id = instance_ref.project_id - # combine prefixes, macs, and project_id into (prefix,mac,p_id) tuples - prefix_mac_tuples = zip(prefixes, macs, [project_id for m in macs]) - # return list containing ipv6 address for each tuple - return [ipv6.to_global_ipv6(*t) for t in prefix_mac_tuples] + return _ipv6_get_by_instance_ref(context, instance_ref) @require_context diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index d29d3d6f1..e42da193f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -187,6 +187,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) -- cgit From 04804aba3c995260cf376b8d979f032942cd0988 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 11 Jul 2011 14:45:27 -0700 Subject: pep8 fixes --- nova/compute/api.py | 5 ++--- nova/db/sqlalchemy/api.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index e80e52566..fba56a2bb 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -641,7 +641,6 @@ class API(base.Base): raise exception.FixedIpNotFoundForAddress(address=fixed_ip) return instances - def _get_all_by_ip(self, context, search_opts): return self.db.instance_get_all_by_ip_regexp( context, search_opts['ip']) @@ -690,8 +689,8 @@ class API(base.Base): LOG.error(_("More than 1 mutually exclusive " "search option specified (%(found_opt)s and " "%(opt)s were both specified") % locals()) - raise exception.InvalidInput(reason= - _("More than 1 mutually exclusive " + raise exception.InvalidInput(reason=_( + "More than 1 mutually exclusive " "search option specified (%(found_opt)s and " "%(opt)s were both specified") % locals()) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index af2acbcb3..99e96e679 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1317,6 +1317,7 @@ def instance_get_all_by_ip_regexp(context, ip_regexp): return instances.values() + @require_context def instance_get_all_by_ipv6_regex(context, ipv6_regexp): """Get all instances by using regular expression matching against -- cgit From a3096d593fbe21625e3c4102e69d12950e9d2ef2 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Tue, 12 Jul 2011 02:01:09 -0700 Subject: added searching by instance name added unit tests --- nova/compute/api.py | 5 + nova/db/api.py | 7 ++ nova/db/sqlalchemy/api.py | 24 +++- nova/tests/test_compute.py | 265 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 1 deletion(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index fba56a2bb..605b0d29c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -641,6 +641,10 @@ class API(base.Base): raise exception.FixedIpNotFoundForAddress(address=fixed_ip) return instances + def _get_all_by_name(self, context, search_opts): + return self.db.instance_get_all_by_name_regexp( + context, search_opts['name']) + def _get_all_by_ip(self, context, search_opts): return self.db.instance_get_all_by_ip_regexp( context, search_opts['ip']) @@ -673,6 +677,7 @@ class API(base.Base): exclusive_opts = ['reservation_id', 'project_id', 'fixed_ip', + 'name', 'ip', 'ip6'] + search_columns diff --git a/nova/db/api.py b/nova/db/api.py index c0f49d98d..6aa44f06b 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -529,6 +529,13 @@ def instance_get_all_by_column_regexp(context, column, column_regexp): column_regexp) +def instance_get_all_by_name_regexp(context, name_regexp): + """Get all instances by using regular expression matching against + its name + """ + return IMPL.instance_get_all_by_name_regexp(context, name_regexp) + + def instance_get_all_by_ip_regexp(context, ip_regexp): """Get all instances by using regular expression matching against Floating and Fixed IP Addresses diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 99e96e679..93614a307 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1262,6 +1262,28 @@ def instance_get_all_by_column_regexp(context, column, column_regexp): return instances +@require_context +def instance_get_all_by_name_regexp(context, ipv6_regexp): + """Get all instances by using regular expression matching against + its name + """ + + session = get_session() + with session.begin(): + # get instances + + all_instances = session.query(models.Instance).\ + options(joinedload('metadata')).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + if not all_instances: + return [] + + compiled_regexp = re.compile(ipv6_regexp) + return [instance for instance in all_instances + if compiled_regexp.match(instance.name)] + + @require_context def instance_get_all_by_ip_regexp(context, ip_regexp): """Get all instances by using regular expression matching against @@ -1319,7 +1341,7 @@ def instance_get_all_by_ip_regexp(context, ip_regexp): @require_context -def instance_get_all_by_ipv6_regex(context, ipv6_regexp): +def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): """Get all instances by using regular expression matching against IPv6 Addresses """ diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 45cd2f764..0190a5f73 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -30,6 +30,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 @@ -810,3 +811,267 @@ class ComputeTestCase(test.TestCase): LOG.info(_("After force-killing instances: %s"), instances) self.assertEqual(len(instances), 1) self.assertEqual(power_state.SHUTOFF, instances[0]['state']) + + def test_get_all_by_display_name_regexp(self): + """Test searching instances by 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={'display_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={'display_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={'display_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={'display_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={'display_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_server_name_regexp(self): + """Test searching instances by server_name""" + c = context.get_admin_context() + instance_id1 = self._create_instance({'server_name': 'woot'}) + instance_id2 = self._create_instance({ + 'server_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'server_name': 'not-woot', + 'id': 30}) + + instances = self.compute_api.get_all(c, + search_opts={'server_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={'server_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={'server_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={'server_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={'server_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_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={'name': 'instance.*'}) + self.assertEqual(len(instances), 3) + + instances = self.compute_api.get_all(c, + search_opts={'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={'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({'server_name': 'woot'}) + instance_id2 = self._create_instance({ + 'server_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'server_name': 'not-woot', + 'id': 30}) + + db.fixed_ip_create(c, + {'address': '1.1.1.1', + 'instance_id': instance_id1}) + db.fixed_ip_create(c, + {'address': '1.1.2.1', + 'instance_id': instance_id2}) + + # regex not allowed + self.assertRaises(exception.NotFound, + self.compute_api.get_all, + c, + search_opts={'fixed_ip': '.*'}) + + self.assertRaises(exception.NotFound, + self.compute_api.get_all, + c, + search_opts={'fixed_ip': '1.1.3.1'}) + + 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.instance_destroy(c, instance_id1) + db.instance_destroy(c, instance_id2) + + def test_get_all_by_ip_regex(self): + """Test searching by Floating and Fixed IP""" + c = context.get_admin_context() + instance_id1 = self._create_instance({'server_name': 'woot'}) + instance_id2 = self._create_instance({ + 'server_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'server_name': 'not-woot', + 'id': 30}) + + db.fixed_ip_create(c, + {'address': '1.1.1.1', + 'instance_id': instance_id1}) + db.fixed_ip_create(c, + {'address': '1.1.2.1', + 'instance_id': instance_id2}) + fix_addr = db.fixed_ip_create(c, + {'address': '1.1.3.1', + 'instance_id': instance_id3}) + 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']}) + + 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) + instance_ids = [instance.id for instance in instances] + self.assertTrue(instance_id2 in instance_ids) + + instances = self.compute_api.get_all(c, + search_opts={'ip': '10.*'}) + self.assertEqual(len(instances), 1) + instance_ids = [instance.id for instance in instances] + 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) + db.floating_ip_destroy(c, '10.0.0.2') + + def test_get_all_by_ipv6_regex(self): + """Test searching by IPv6 address""" + def fake_ipv6_get_by_instance_ref(context, instance): + if instance.id == 1: + return ['ffff:ffff::1'] + if instance.id == 20: + return ['dddd:dddd::1'] + if instance.id == 30: + return ['cccc:cccc::1', 'eeee:eeee::1', 'dddd:dddd::1'] + + self.stubs.Set(sqlalchemy_api, '_ipv6_get_by_instance_ref', + fake_ipv6_get_by_instance_ref) + + c = context.get_admin_context() + instance_id1 = self._create_instance({'server_name': 'woot'}) + instance_id2 = self._create_instance({ + 'server_name': 'woo', + 'id': 20}) + instance_id3 = self._create_instance({ + 'server_name': 'not-woot', + 'id': 30}) + + instances = self.compute_api.get_all(c, + search_opts={'ip6': 'ff.*'}) + 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={'ip6': '.*::1'}) + 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': '.*dd:.*'}) + 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) -- cgit From edccef06c24df2fa785005f7a3c1f52a45bfc071 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Tue, 12 Jul 2011 03:13:43 -0700 Subject: fix bugs with fixed_ip returning a 404 instance searching needs to joinload more stuff --- nova/compute/api.py | 17 +++++++- nova/db/sqlalchemy/api.py | 102 +++++++++++++++++++++++----------------------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 605b0d29c..511c17e7a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -19,6 +19,7 @@ """Handles all requests relating to instances (guest vms).""" import eventlet +import novaclient import re import time @@ -636,7 +637,10 @@ class API(base.Base): try: instances = self.db.instance_get_by_fixed_ip(context, fixed_ip) except exception.FixedIpNotFound, e: - raise + if search_opts['recurse_zones']: + return [] + else: + raise if not instances: raise exception.FixedIpNotFoundForAddress(address=fixed_ip) return instances @@ -729,14 +733,25 @@ class API(base.Base): admin_context = context.elevated() children = scheduler_api.call_zone_method(admin_context, "list", + errors_to_ignore=[novaclient.exceptions.NotFound], novaclient_collection_name="servers", 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) + + # Fixed IP returns a FixedIpNotFound when an instance is not + # found... + fixed_ip = search_opts.get('fixed_ip', None) + if fixed_ip and not instances: + raise exception.FixedIpNotFoundForAddress(address=fixed_ip) + return instances def _cast_compute_message(self, method, context, instance_id, host=None, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 93614a307..59db56a5c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1234,27 +1234,20 @@ def instance_get_all_by_column_regexp(context, column, column_regexp): # MySQL 'regexp' is not portable, so we must do our own matching. # First... grab all Instances. - query = session.query(models.Instance).\ - options(joinedload('metadata')) - if is_admin_context(context): - all_instances = query.\ - filter_by(deleted=can_read_deleted(context)).\ - all() - elif is_user_context(context): - all_instances = query.\ - filter_by(project_id=context.project_id).\ - filter_by(deleted=False).\ - all() - else: + all_instances = 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(deleted=can_read_deleted(context)).\ + all() + if not all_instances: return [] - - if all_instances is None: - all_instances = [] - # Now do the regexp matching compiled_regexp = re.compile(column_regexp) instances = [] - for instance in all_instances: v = getattr(instance, column) if v and compiled_regexp.match(v): @@ -1269,19 +1262,24 @@ def instance_get_all_by_name_regexp(context, ipv6_regexp): """ session = get_session() - with session.begin(): - # get instances - - all_instances = session.query(models.Instance).\ - options(joinedload('metadata')).\ - filter_by(deleted=can_read_deleted(context)).\ - all() - if not all_instances: - return [] - compiled_regexp = re.compile(ipv6_regexp) - return [instance for instance in all_instances - if compiled_regexp.match(instance.name)] + # MySQL 'regexp' is not portable, so we must do our own matching. + # First... grab all Instances. + all_instances = 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(deleted=can_read_deleted(context)).\ + all() + if not all_instances: + return [] + # Now do the regexp matching + compiled_regexp = re.compile(ipv6_regexp) + return [instance for instance in all_instances + if compiled_regexp.match(instance.name)] @require_context @@ -1291,11 +1289,6 @@ def instance_get_all_by_ip_regexp(context, ip_regexp): """ session = get_session() - fixed_ip_query = session.query(models.FixedIp).\ - options(joinedload('instance.metadata')) - floating_ip_query = session.query(models.FloatingIp).\ - options(joinedload_all('fixed_ip.instance.metadata')) - # Query both FixedIp and FloatingIp tables to get matches. # Since someone could theoretically search for something that matches # instances in both tables... we need to use a dictionary keyed @@ -1304,20 +1297,26 @@ def instance_get_all_by_ip_regexp(context, ip_regexp): # addresses even though they might point to the same instance ID. instances = {} - # MySQL 'regexp' is not portable, so we must do our own matching. - # First... grab all of the IP entries. - if is_admin_context(context): - fixed_ips = fixed_ip_query.\ - filter_by(deleted=can_read_deleted(context)).\ - all() - floating_ips = floating_ip_query.\ - filter_by(deleted=can_read_deleted(context)).\ - all() - elif is_user_context(context): - fixed_ips = fixed_ip_query.filter_by(deleted=False).all() - floating_ips = floating_ip_query.filter_by(deleted=False).all() - else: - return None + fixed_ips = session.query(models.FixedIp).\ + options(joinedload_all('instance.fixed_ips.floating_ips')).\ + options(joinedload('instance.virtual_interfaces')).\ + options(joinedload('instance.security_groups')).\ + options(joinedload_all('instance.fixed_ips.network')).\ + options(joinedload('instance.metadata')).\ + options(joinedload('instance.instance_type')).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + floating_ips = session.query(models.FloatingIp).\ + options(joinedload_all( + 'fixed_ip.instance.fixed_ips.floating_ips')).\ + options(joinedload('fixed_ip.instance.virtual_interfaces')).\ + options(joinedload('fixed_ip.instance.security_groups')).\ + options(joinedload_all( + 'fixed_ip.instance.fixed_ips.network')).\ + options(joinedload('fixed_ip.instance.metadata')).\ + options(joinedload('fixed_ip.instance.instance_type')).\ + filter_by(deleted=can_read_deleted(context)).\ + all() if fixed_ips is None: fixed_ips = [] @@ -1348,10 +1347,13 @@ def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): session = get_session() with session.begin(): - # get instances - all_instances = 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(deleted=can_read_deleted(context)).\ all() if not all_instances: -- cgit From bbd8f482b916168871d1d83192b354355858e77c Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Tue, 12 Jul 2011 15:16:16 -0700 Subject: python-novaclient 2.5.8 is required --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index dec93c351..db9e950a8 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -9,7 +9,7 @@ boto==1.9b carrot==0.10.5 eventlet lockfile==0.8 -python-novaclient==2.5.7 +python-novaclient==2.5.8 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 -- cgit From 1dec3d7c3380d83398be0588b58c1cad13252807 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 14 Jul 2011 12:13:13 -0700 Subject: clean up checking for exclusive search options fix a cut n paste error with instance_get_all_by_name_regexp --- nova/compute/api.py | 32 +++++++++++++++----------------- nova/db/sqlalchemy/api.py | 4 ++-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index f795e345a..a445ef6eb 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -689,21 +689,19 @@ class API(base.Base): # Ignore unknown search options for possible forward compatability. # Raise an exception if more than 1 search option is specified found_opt = None - for opt in exclusive_opts: - v = search_opts.get(opt, None) - if v: - if found_opt is None: - found_opt = opt - else: - LOG.error(_("More than 1 mutually exclusive " - "search option specified (%(found_opt)s and " - "%(opt)s were both specified") % locals()) - raise exception.InvalidInput(reason=_( - "More than 1 mutually exclusive " - "search option specified (%(found_opt)s and " - "%(opt)s were both specified") % locals()) - - if found_opt: + found_opts = [opt for opt in exclusive_opts + if search_opts.get(opt, None)] + if len(found_opts) > 1: + found_opt_str = ", ".join(found_opts) + msg = _("More than 1 mutually exclusive " + "search option specified: %(found_opt_str)s") \ + % locals() + logger.error(msg) + raise exception.InvalidInput(reason=msg) + + # Found a search option? + if found_opts: + found_opt = found_opts[0] if found_opt in search_columns: instances = self._get_all_by_column(context, found_opt, search_opts) @@ -714,10 +712,10 @@ class API(base.Base): elif not context.is_admin: if context.project: instances = self.db.instance_get_all_by_project( - context, context.project_id) + context, context.project_id) else: instances = self.db.instance_get_all_by_user( - context, context.user_id) + context, context.user_id) else: instances = self.db.instance_get_all(context) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 59db56a5c..735d63be9 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1256,7 +1256,7 @@ def instance_get_all_by_column_regexp(context, column, column_regexp): @require_context -def instance_get_all_by_name_regexp(context, ipv6_regexp): +def instance_get_all_by_name_regexp(context, name_regexp): """Get all instances by using regular expression matching against its name """ @@ -1277,7 +1277,7 @@ def instance_get_all_by_name_regexp(context, ipv6_regexp): if not all_instances: return [] # Now do the regexp matching - compiled_regexp = re.compile(ipv6_regexp) + compiled_regexp = re.compile(name_regexp) return [instance for instance in all_instances if compiled_regexp.match(instance.name)] -- cgit From d2265cbe65f1b3940b37966245da13b9714234ef Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Sun, 17 Jul 2011 16:12:59 -0700 Subject: Refactored OS API code to allow checking of invalid query string paremeters and admin api/context to the index/detail calls. v1.0 still ignores unknown parameters, but v1.1 will return 400/BadRequest on unknown options. admin_api only commands are treated as unknown parameters if FLAGS.enable_admin_api is False. If enable_admin_api is True, non-admin context requests return 403/Forbidden. Fixed EC2 API code to handle search options to compute_api.get_all() more correctly. Reverted compute_api.get_all to ignore unknown options, since the OS API now does the verification. Updated tests. --- nova/api/ec2/cloud.py | 23 ++++--- nova/api/openstack/servers.py | 123 +++++++++++++++++++++++++++++---- nova/compute/api.py | 154 ++++++++++++++++++++++++------------------ nova/tests/test_compute.py | 8 +-- 4 files changed, 215 insertions(+), 93 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 0d24f0938..76725370a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -800,11 +800,16 @@ class CloudController(object): return [{label: x} for x in lst] 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)} @@ -814,7 +819,8 @@ class CloudController(object): assert len(i) == 1 return i[0] - def _format_instances(self, context, instance_id=None, **kwargs): + 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 @@ -827,14 +833,15 @@ class CloudController(object): internal_id = ec2utils.ec2_id_to_id(ec2_id) try: instance = self.compute_api.get(context, - instance_id=internal_id) + instance_id=internal_id, + search_opts=search_opts) except exception.NotFound: continue instances.append(instance) else: try: instances = self.compute_api.get_all(context, - search_opts=kwargs) + search_opts=search_opts) except exception.NotFound: instances = [] for instance in instances: @@ -856,7 +863,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'], @@ -1014,7 +1021,7 @@ class CloudController(object): 'AvailabilityZone'), block_device_mapping=kwargs.get('block_device_mapping', {})) return self._format_run_instances(context, - instances[0]['reservation_id']) + instance_id=instances[0]['reservation_id']) def _do_instance(self, action, context, ec2_id): instance_id = ec2utils.ec2_id_to_id(ec2_id) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8a947c0e0..218037d14 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -39,6 +39,41 @@ LOG = logging.getLogger('nova.api.openstack.servers') FLAGS = flags.FLAGS +def check_option_permissions(context, specified_options, + user_api_options, admin_api_options): + """Check whether or not entries in 'specified_options' are valid + based on the allowed 'user_api_options' and 'admin_api_options'. + + All inputs are lists of option names + + Returns: exception.InvalidInput for an invalid option or + exception.AdminRequired for needing admin privs + """ + + # We pretend we don't know about admin_api_options if the admin + # API is not enabled. + if FLAGS.enable_admin_api: + known_options = user_api_options + admin_api_options + else: + known_options = user_api_options + + # Check for unknown query string params. + spec_unknown_opts = [for opt in specified_options + if opt not in known_options] + if spec_unknown_opts: + unknown_opt_str = ", ".join(spec_unknown_opts) + raise exception.InvalidInput(reason=_( + "Unknown options specified: %(unknown_opt_str)")) + + # Check for admin context for the admin commands + if not context.is_admin: + spec_admin_opts = [for opt in specified_options + if opt in admin_api_options] + if spec_admin_opts: + admin_opt_str = ", ".join(admin_opts) + raise exception.AdminRequired() + + class Controller(object): """ The Server API controller for the OpenStack API """ @@ -51,9 +86,9 @@ class Controller(object): try: servers = self._items(req, is_detail=False) except exception.Invalid as err: - return exc.HTTPBadRequest(explanation=str(err)) + return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) except exception.NotFound: - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) return servers def detail(self, req): @@ -61,9 +96,9 @@ class Controller(object): try: servers = self._items(req, is_detail=True) except exception.Invalid as err: - return exc.HTTPBadRequest(explanation=str(err)) + return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) except exception.NotFound as err: - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) return servers def _get_view_builder(self, req): @@ -75,20 +110,17 @@ 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. + def _get_items(self, context, req, is_detail, search_opts=None): + """Returns a list of servers. builder - the response model builder """ - query_str = req.str_GET - recurse_zones = utils.bool_from_str( - query_str.get('recurse_zones', False)) - # Pass all of the options on to compute's 'get_all' - search_opts = query_str - # Reset this after converting from string to bool - search_opts['recurse_zones'] = recurse_zones + + if search_opts is None: + search_opts = {} + instance_list = self.compute_api.get_all( - req.environ['nova.context'], search_opts=search_opts) + context, search_opts=search_opts) limited_list = self._limit_items(instance_list, req) builder = self._get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] @@ -422,6 +454,41 @@ class ControllerV10(Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() + def _items(self, req, is_detail): + """Returns a list of servers based on the request. + + Checks for search options and permissions on the options. + """ + + search_opts = {} + search_opts.update(req.str_GET) + + user_api = ['project_id', 'fixed_ip', 'recurse_zones', + 'reservation_id', 'name', 'fresh', 'ip', 'ip6'] + admin_api = ['instance_name'] + + context = req.environ['nova.context'] + + try: + check_option_permissions(context, search_opt.keys(), + user_api, admin_api) + except exception.InvalidInput: + # FIXME(comstud): I refactored code in here to support + # new search options, and the original code ignored + # invalid options. So, I've left it this way for now. + # The v1.1 implementation will return an error in this + # case.. + pass + except exception.AdminRequired, e: + raise faults.Fault(exc.HTTPForbidden(detail=str(e))) + + # Convert recurse_zones into a boolean + search_opts['recurse_zones'] = utils.bool_from_str( + search_opts.get('recurse_zones', False)) + + return self._get_items(context, req, is_detail, + search_opts=search_opts) + def _image_ref_from_req_data(self, data): return data['server']['imageId'] @@ -493,6 +560,34 @@ class ControllerV11(Controller): except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + def _items(self, req, is_detail): + """Returns a list of servers based on the request. + + Checks for search options and permissions on the options. + """ + + search_opts = {} + search_opts.update(req.str_GET) + + user_api = ['image', 'flavor', 'name', 'status', + 'reservation_id', 'changes-since', 'ip', 'ip6'] + admin_api = ['ip', 'ip6', 'instance_name'] + + context = req.environ['nova.context'] + + try: + check_option_permissions(context, search_opt.keys(), + user_api, admin_api) + except exception.InvalidInput, e: + raise faults.Fault(exc.HTTPBadRequest(detail=str(e))) + except exception.AdminRequired, e: + raise faults.Fault(exc.HTTPForbidden(detail=str(e))) + + # NOTE(comstud): Making recurse_zones always be True in v1.1 + search_opts['recurse_zones'] = True + return self._get_items(context, req, is_detail, + search_opts=search_opts) + def _image_ref_from_req_data(self, data): return data['server']['imageRef'] diff --git a/nova/compute/api.py b/nova/compute/api.py index a445ef6eb..0c76f4ad6 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -623,44 +623,6 @@ class API(base.Base): """ return self.get(context, instance_id) - def _get_all_by_reservation_id(self, context, search_opts): - search_opts['recurse_zones'] = True - return self.db.instance_get_all_by_reservation( - context, search_opts['reservation_id']) - - def _get_all_by_project_id(self, context, search_opts): - return self.db.instance_get_all_by_project( - context, search_opts['project_id']) - - def _get_all_by_fixed_ip(self, context, search_opts): - fixed_ip = search_opts['fixed_ip'] - try: - instances = self.db.instance_get_by_fixed_ip(context, fixed_ip) - except exception.FixedIpNotFound, e: - if search_opts['recurse_zones']: - return [] - else: - raise - if not instances: - raise exception.FixedIpNotFoundForAddress(address=fixed_ip) - return instances - - def _get_all_by_name(self, context, search_opts): - return self.db.instance_get_all_by_name_regexp( - context, search_opts['name']) - - def _get_all_by_ip(self, context, search_opts): - return self.db.instance_get_all_by_ip_regexp( - context, search_opts['ip']) - - def _get_all_by_ip6(self, context, search_opts): - return self.db.instance_get_all_by_ipv6_regexp( - context, search_opts['ip6']) - - def _get_all_by_column(self, context, column, search_opts): - return self.db.instance_get_all_by_column_regexp( - context, column, search_opts[column]) - def get_all(self, context, search_opts=None): """Get all instances filtered by one of the given parameters. @@ -668,57 +630,115 @@ class API(base.Base): all instances in the system. """ + def _get_all_by_reservation_id(reservation_id): + """Get instances by reservation ID""" + # reservation_id implies recurse_zones + search_opts['recurse_zones'] = True + return self.db.instance_get_all_by_reservation(context, + reservation_id) + + def _get_all_by_project_id(project_id): + """Get instances by project ID""" + return self.db.instance_get_all_by_project(context, project_id) + + def _get_all_by_fixed_ip(fixed_ip): + """Get instance by fixed IP""" + try: + instances = self.db.instance_get_by_fixed_ip(context, + fixed_ip) + except exception.FixedIpNotFound, e: + if search_opts.get('recurse_zones', False): + return [] + else: + raise + if not instances: + raise exception.FixedIpNotFoundForAddress(address=fixed_ip) + return instances + + def _get_all_by_instance_name(instance_name_regexp): + """Get instances by matching the Instance.name property""" + return self.db.instance_get_all_by_name_regexp( + context, instance_name_regexp) + + def _get_all_by_ip(ip_regexp): + """Get instances by matching IPv4 addresses""" + return self.db.instance_get_all_by_ip_regexp(context, ip_regexp) + + def _get_all_by_ipv6(ipv6_regexp): + """Get instances by matching IPv6 addresses""" + return self.db.instance_get_all_by_ipv6_regexp(context, + ipv6_regexp) + + def _get_all_by_column(column_regexp, column): + """Get instances by matching Instance.""" + return self.db.instance_get_all_by_column_regexp( + context, column, column_regexp) + + # Define the search params that we will allow. This is a mapping + # of the search param to tuple of (function_to_call, (function_args)) + # A 'None' function means it's an optional parameter that will + # influence the search results, but itself is not a search option. + # Search options are mutually exclusive + known_params = { + 'recurse_zones': (None, None), + # v1.0 API? + 'fresh': (None, None), + # v1.1 API + 'changes-since': (None, None), + # Mutually exclusive options + 'display_name': (_get_all_by_column, ('display_name',)), + 'reservation_id': (_get_all_by_reservation_id, ()), + # Needed for EC2 API + 'fixed_ip': (_get_all_by_fixed_ip, ()), + # Needed for EC2 API + 'project_id': (_get_all_by_project_id, ()), + 'ip': (_get_all_by_ip, ()), + 'ip6': (_get_all_by_ipv6, ()), + 'instance_name': (_get_all_by_instance_name, ()), + 'server_name': (_get_all_by_column, ('server_name',))} + + # FIXME(comstud): 'fresh' and 'changes-since' are currently not + # implemented... + if search_opts is None: search_opts = {} LOG.debug(_("Searching by: %s") % str(search_opts)) - # Columns we can do a generic search on - search_columns = ['display_name', - 'server_name'] - - # Options that are mutually exclusive - exclusive_opts = ['reservation_id', - 'project_id', - 'fixed_ip', - 'name', - 'ip', - 'ip6'] + search_columns - - # See if a valid search option was passed in. - # Ignore unknown search options for possible forward compatability. - # Raise an exception if more than 1 search option is specified - found_opt = None - found_opts = [opt for opt in exclusive_opts - if search_opts.get(opt, None)] + # Mutually exclusive serach options are any options that have + # a function to call. Raise an exception if more than 1 is + # specified... + # NOTE(comstud): Ignore unknown options. The OS API will + # do it's own verification on options.. + found_opts = [opt for opt in search_opts.iterkeys() + if opt in known_params and \ + known_params[opt][0] is not None] if len(found_opts) > 1: found_opt_str = ", ".join(found_opts) msg = _("More than 1 mutually exclusive " "search option specified: %(found_opt_str)s") \ % locals() - logger.error(msg) + LOG.error(msg) raise exception.InvalidInput(reason=msg) # Found a search option? if found_opts: found_opt = found_opts[0] - if found_opt in search_columns: - instances = self._get_all_by_column(context, - found_opt, search_opts) - else: - method_name = '_get_all_by_%s' % found_opt - method = getattr(self, method_name, None) - instances = method(context, search_opts) - elif not context.is_admin: + f, f_args = known_params[found_opt] + instances = f(search_opts[found_opt], *f_args) + # Nope. Return all instances if the request is in admin context.. + elif context.is_admin: + instances = self.db.instance_get_all(context) + # Nope. Return all instances for the user/project + else: if context.project: instances = self.db.instance_get_all_by_project( context, context.project_id) else: instances = self.db.instance_get_all_by_user( context, context.user_id) - else: - instances = self.db.instance_get_all(context) + # Convert any responses into a list of instances if instances is None: instances = [] elif not isinstance(instances, list): diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index bdf2edd50..fc075b6c7 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -910,7 +910,7 @@ class ComputeTestCase(test.TestCase): db.instance_destroy(c, instance_id2) db.instance_destroy(c, instance_id3) - def test_get_all_by_name_regexp(self): + def test_get_all_by_instance_name_regexp(self): """Test searching instances by name""" self.flags(instance_name_template='instance-%d') @@ -920,18 +920,18 @@ class ComputeTestCase(test.TestCase): instance_id3 = self._create_instance({'id': 10}) instances = self.compute_api.get_all(c, - search_opts={'name': 'instance.*'}) + search_opts={'instance_name': 'instance.*'}) self.assertEqual(len(instances), 3) instances = self.compute_api.get_all(c, - search_opts={'name': '.*\-\d$'}) + 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={'name': 'i.*2'}) + search_opts={'instance_name': 'i.*2'}) self.assertEqual(len(instances), 1) self.assertEqual(instances[0].id, instance_id2) -- cgit From 491c90924ac87e533ce61e3bf949a50bfdd6a31d Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Sun, 17 Jul 2011 16:35:11 -0700 Subject: compute's get_all should accept 'name' not 'display_name' for searching Instance.display_name. Removed 'server_name' searching.. Fixed DB calls for searching to filter results based on context --- nova/compute/api.py | 5 ++-- nova/db/sqlalchemy/api.py | 58 ++++++++++++++++++++++++++++++++------ nova/tests/test_compute.py | 69 +++++++--------------------------------------- 3 files changed, 61 insertions(+), 71 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 0c76f4ad6..6661775a5 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -686,7 +686,7 @@ class API(base.Base): # v1.1 API 'changes-since': (None, None), # Mutually exclusive options - 'display_name': (_get_all_by_column, ('display_name',)), + 'name': (_get_all_by_column, ('display_name',)), 'reservation_id': (_get_all_by_reservation_id, ()), # Needed for EC2 API 'fixed_ip': (_get_all_by_fixed_ip, ()), @@ -694,8 +694,7 @@ class API(base.Base): 'project_id': (_get_all_by_project_id, ()), 'ip': (_get_all_by_ip, ()), 'ip6': (_get_all_by_ipv6, ()), - 'instance_name': (_get_all_by_instance_name, ()), - 'server_name': (_get_all_by_column, ('server_name',))} + 'instance_name': (_get_all_by_instance_name, ())} # FIXME(comstud): 'fresh' and 'changes-since' are currently not # implemented... diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 735d63be9..feccba389 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1234,15 +1234,25 @@ def instance_get_all_by_column_regexp(context, column, column_regexp): # MySQL 'regexp' is not portable, so we must do our own matching. # First... grab all Instances. - all_instances = session.query(models.Instance).\ + prefix = 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(deleted=can_read_deleted(context)).\ - all() + filter_by(deleted=can_read_deleted(context)) + + if context.is_admin: + all_instances = prefix.all() + elif context.project: + all_instances = prefix.\ + filter_by(project_id=context.project_id).\ + all() + else: + all_instances = prefix.\ + filter_by(user_id=context.user_id).\ + all() if not all_instances: return [] # Now do the regexp matching @@ -1265,15 +1275,25 @@ def instance_get_all_by_name_regexp(context, name_regexp): # MySQL 'regexp' is not portable, so we must do our own matching. # First... grab all Instances. - all_instances = session.query(models.Instance).\ + prefix = 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(deleted=can_read_deleted(context)).\ - all() + filter_by(deleted=can_read_deleted(context)) + + if context.is_admin: + all_instances = prefix.all() + elif context.project: + all_instances = prefix.\ + filter_by(project_id=context.project_id).\ + all() + else: + all_instances = prefix.\ + filter_by(user_id=context.user_id).\ + all() if not all_instances: return [] # Now do the regexp matching @@ -1336,6 +1356,15 @@ def instance_get_all_by_ip_regexp(context, ip_regexp): compiled_regexp.match(floating_ip.address): instances[fixed_ip.instance.uuid] = fixed_ip.instance + if context.is_admin: + return instances.values() + elif context.project: + return [instance for instance in instances.values() + if instance.project_id == context.project_id] + else: + return [instance for instance in instances.values() + if instance.user_id == context.user_id] + return instances.values() @@ -1347,15 +1376,26 @@ def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): session = get_session() with session.begin(): - all_instances = session.query(models.Instance).\ + prefix = 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(deleted=can_read_deleted(context)).\ - all() + filter_by(deleted=can_read_deleted(context)) + + if context.is_admin: + all_instances = prefix.all() + elif context.project: + all_instances = prefix.\ + filter_by(project_id=context.project_id).\ + all() + else: + all_instances = prefix.\ + filter_by(user_id=context.user_id).\ + all() + if not all_instances: return [] diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index fc075b6c7..152687083 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -820,8 +820,8 @@ class ComputeTestCase(test.TestCase): self.assertEqual(len(instances), 1) self.assertEqual(power_state.SHUTOFF, instances[0]['state']) - def test_get_all_by_display_name_regexp(self): - """Test searching instances by display_name""" + 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({ @@ -832,78 +832,33 @@ class ComputeTestCase(test.TestCase): 'id': 30}) instances = self.compute_api.get_all(c, - search_opts={'display_name': 'woo.*'}) + 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={'display_name': 'woot.*'}) + 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={'display_name': '.*oot.*'}) + 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={'display_name': 'n.*'}) + 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={'display_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_server_name_regexp(self): - """Test searching instances by server_name""" - c = context.get_admin_context() - instance_id1 = self._create_instance({'server_name': 'woot'}) - instance_id2 = self._create_instance({ - 'server_name': 'woo', - 'id': 20}) - instance_id3 = self._create_instance({ - 'server_name': 'not-woot', - 'id': 30}) - - instances = self.compute_api.get_all(c, - search_opts={'server_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={'server_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={'server_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={'server_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={'server_name': 'noth.*'}) + search_opts={'name': 'noth.*'}) self.assertEqual(len(instances), 0) db.instance_destroy(c, instance_id1) @@ -942,13 +897,9 @@ class ComputeTestCase(test.TestCase): def test_get_by_fixed_ip(self): """Test getting 1 instance by Fixed IP""" c = context.get_admin_context() - instance_id1 = self._create_instance({'server_name': 'woot'}) - instance_id2 = self._create_instance({ - 'server_name': 'woo', - 'id': 20}) - instance_id3 = self._create_instance({ - 'server_name': 'not-woot', - 'id': 30}) + instance_id1 = self._create_instance() + instance_id2 = self._create_instance({'id': 20}) + instance_id3 = self._create_instance({'id': 30}) db.fixed_ip_create(c, {'address': '1.1.1.1', -- cgit From 102a0e5b9d6ce22a5fc5a00fc260bbe1e3592222 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 02:45:10 -0700 Subject: added searching by 'image', 'flavor', and 'status' reverted ip/ip6 searching to be admin only --- nova/api/openstack/servers.py | 18 +++-- nova/api/openstack/views/servers.py | 15 +--- nova/compute/api.py | 47 +++++++++-- nova/compute/power_state.py | 29 +++++++ nova/db/api.py | 5 ++ nova/db/sqlalchemy/api.py | 42 ++++++++++ nova/tests/test_compute.py | 156 ++++++++++++++++++++++++++++++++---- 7 files changed, 271 insertions(+), 41 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 218037d14..fb1ce2529 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -119,6 +119,14 @@ class Controller(object): if search_opts is None: search_opts = {} + # If search by 'status', we need to convert it to 'state' + # If the status is unknown, bail + status = search_opts.pop('status', None) + if status is not None: + search_opts['state'] = power_state.states_from_status(status) + if len(search_opts['state']) == 0: + raise exception.InvalidInput(reason=_( + 'Invalid server status')) instance_list = self.compute_api.get_all( context, search_opts=search_opts) limited_list = self._limit_items(instance_list, req) @@ -464,8 +472,8 @@ class ControllerV10(Controller): search_opts.update(req.str_GET) user_api = ['project_id', 'fixed_ip', 'recurse_zones', - 'reservation_id', 'name', 'fresh', 'ip', 'ip6'] - admin_api = ['instance_name'] + 'reservation_id', 'name', 'fresh', 'status'] + admin_api = ['ip', 'ip6', 'instance_name'] context = req.environ['nova.context'] @@ -570,7 +578,7 @@ class ControllerV11(Controller): search_opts.update(req.str_GET) user_api = ['image', 'flavor', 'name', 'status', - 'reservation_id', 'changes-since', 'ip', 'ip6'] + 'reservation_id', 'changes-since'] admin_api = ['ip', 'ip6', 'instance_name'] context = req.environ['nova.context'] @@ -579,9 +587,9 @@ class ControllerV11(Controller): check_option_permissions(context, search_opt.keys(), user_api, admin_api) except exception.InvalidInput, e: - raise faults.Fault(exc.HTTPBadRequest(detail=str(e))) + raise faults.Fault(exc.HTTPBadRequest(explanation=str(e))) except exception.AdminRequired, e: - raise faults.Fault(exc.HTTPForbidden(detail=str(e))) + raise faults.Fault(exc.HTTPForbidden(explanation=str(e))) # NOTE(comstud): Making recurse_zones always be True in v1.1 search_opts['recurse_zones'] = True diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 67fb6a84e..1883ce2a5 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -60,25 +60,12 @@ 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'], 'addresses': self.addresses_builder.build(inst), - 'status': power_mapping[inst.get('state')]} + 'status': power_state.status_from_state(inst.get('state'))} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() diff --git a/nova/compute/api.py b/nova/compute/api.py index 6661775a5..cd2aaed96 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -669,11 +669,32 @@ class API(base.Base): return self.db.instance_get_all_by_ipv6_regexp(context, ipv6_regexp) - def _get_all_by_column(column_regexp, column): - """Get instances by matching Instance.""" + def _get_all_by_column_regexp(column_regexp, column): + """Get instances by regular expression matching + Instance. + """ return self.db.instance_get_all_by_column_regexp( context, column, column_regexp) + def _get_all_by_column(column_data, column): + """Get instances by regular expression matching + Instance. + """ + return self.db.instance_get_all_by_column( + context, column, column_data) + + def _get_all_by_flavor(flavor_id): + """Get instances by regular expression matching + Instance. + """ + try: + instance_type = self.db.instance_type_get_by_flavor_id( + context, flavor_id) + except exception.FlavorNotFound: + return [] + return self.db.instance_get_all_by_column( + context, 'instance_type_id', instance_type['id']) + # Define the search params that we will allow. This is a mapping # of the search param to tuple of (function_to_call, (function_args)) # A 'None' function means it's an optional parameter that will @@ -686,7 +707,7 @@ class API(base.Base): # v1.1 API 'changes-since': (None, None), # Mutually exclusive options - 'name': (_get_all_by_column, ('display_name',)), + 'name': (_get_all_by_column_regexp, ('display_name',)), 'reservation_id': (_get_all_by_reservation_id, ()), # Needed for EC2 API 'fixed_ip': (_get_all_by_fixed_ip, ()), @@ -694,7 +715,10 @@ class API(base.Base): 'project_id': (_get_all_by_project_id, ()), 'ip': (_get_all_by_ip, ()), 'ip6': (_get_all_by_ipv6, ()), - 'instance_name': (_get_all_by_instance_name, ())} + 'instance_name': (_get_all_by_instance_name, ()), + 'image': (_get_all_by_column, ('image_ref',)), + 'state': (_get_all_by_column, ('state',)), + 'flavor': (_get_all_by_flavor, ())} # FIXME(comstud): 'fresh' and 'changes-since' are currently not # implemented... @@ -746,13 +770,26 @@ class API(base.Base): if not search_opts.get('recurse_zones', False): return instances + new_search_opts = {} + new_search_opts.update(search_opts) + # API does state search by status, instead of the real power + # state. So if we're searching by 'state', we need to + # convert this back into 'status' + state = new_search_opts.pop('state', None) + if state: + # Might be a list.. we can only use 1. + if isinstance(state, list): + state = state[0] + new_search_opts['status'] = power_state.status_from_state( + state) + # Recurse zones. Need admin context for this. admin_context = context.elevated() children = scheduler_api.call_zone_method(admin_context, "list", errors_to_ignore=[novaclient.exceptions.NotFound], novaclient_collection_name="servers", - search_opts=search_opts) + search_opts=new_search_opts) for zone, servers in children: # 'servers' can be None if a 404 was returned by a zone diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index c468fe6b3..834ad1c0a 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -55,3 +55,32 @@ def name(code): def valid_states(): return _STATE_MAP.keys() + +_STATUS_MAP = { + None: 'BUILD', + NOSTATE: 'BUILD', + RUNNING: 'ACTIVE', + BLOCKED: 'ACTIVE', + SUSPENDED: 'SUSPENDED', + PAUSED: 'PAUSED', + SHUTDOWN: 'SHUTDOWN', + SHUTOFF: 'SHUTOFF', + CRASHED: 'ERROR', + FAILED: 'ERROR', + BUILDING: 'BUILD', +} + +def status_from_state(power_state): + """Map the power state to the server status string""" + return _STATUS_MAP[power_state] + +def 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 not None: + continue + if status.lower() == status_map.lower(): + power_states.append(power_state) + return power_states diff --git a/nova/db/api.py b/nova/db/api.py index 6aa44f06b..b1b9b4544 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -520,6 +520,11 @@ def instance_get_by_fixed_ipv6(context, address): return IMPL.instance_get_by_fixed_ipv6(context, address) +def instance_get_all_by_column(context, column, column_data): + """Get all instances by exact match against the specified DB column""" + return IMPL.instance_get_all_by_column(context, column, column_data) + + def instance_get_all_by_column_regexp(context, column, column_regexp): """Get all instances by using regular expression matching against a particular DB column diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index feccba389..5209e9c8d 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1225,6 +1225,48 @@ def instance_get_by_fixed_ipv6(context, address): return result +@require_context +def instance_get_all_by_column(context, column, column_data): + """Get all instances by exact match against the specified DB column + 'column_data' can be a list. + """ + session = get_session() + + + prefix = 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(deleted=can_read_deleted(context)) + + if isinstance(column_data, list): + column_attr = getattr(models.Instance, column) + prefix = prefix.filter(column_attr.in_(column_data)) + else: + # Set up the dictionary for filter_by() + query_filter = {} + query_filter[column] = column_data + prefix = prefix.filter_by(**query_filter) + + if context.is_admin: + all_instances = prefix.all() + elif context.project: + all_instances = prefix.\ + filter_by(project_id=context.project_id).\ + all() + else: + all_instances = prefix.\ + filter_by(user_id=context.user_id).\ + all() + if not all_instances: + return [] + + return all_instances + + @require_context def instance_get_all_by_column_regexp(context, column, column_regexp): """Get all instances by using regular expression matching against diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 152687083..bab58a7b1 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -84,8 +84,11 @@ class ComputeTestCase(test.TestCase): self.manager.delete_project(self.project) super(ComputeTestCase, self).tearDown() - def _create_instance(self, params={}): + def _create_instance(self, params=None): """Create a test instance""" + + if params is None: + params = {} inst = {} inst['image_ref'] = 1 inst['reservation_id'] = 'r-fakeres' @@ -825,11 +828,11 @@ class ComputeTestCase(test.TestCase): c = context.get_admin_context() instance_id1 = self._create_instance({'display_name': 'woot'}) instance_id2 = self._create_instance({ - 'display_name': 'woo', - 'id': 20}) + 'display_name': 'woo', + 'id': 20}) instance_id3 = self._create_instance({ - 'display_name': 'not-woot', - 'id': 30}) + 'display_name': 'not-woot', + 'id': 30}) instances = self.compute_api.get_all(c, search_opts={'name': 'woo.*'}) @@ -937,11 +940,11 @@ class ComputeTestCase(test.TestCase): c = context.get_admin_context() instance_id1 = self._create_instance({'server_name': 'woot'}) instance_id2 = self._create_instance({ - 'server_name': 'woo', - 'id': 20}) + 'server_name': 'woo', + 'id': 20}) instance_id3 = self._create_instance({ - 'server_name': 'not-woot', - 'id': 30}) + 'server_name': 'not-woot', + 'id': 30}) db.fixed_ip_create(c, {'address': '1.1.1.1', @@ -974,14 +977,12 @@ class ComputeTestCase(test.TestCase): instances = self.compute_api.get_all(c, search_opts={'ip': '.*\.2.+'}) self.assertEqual(len(instances), 1) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id2 in instance_ids) + self.assertEqual(instances[0].id, instance_id2) instances = self.compute_api.get_all(c, search_opts={'ip': '10.*'}) self.assertEqual(len(instances), 1) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id3 in instance_ids) + self.assertEqual(instances[0].id, instance_id3) db.instance_destroy(c, instance_id1) db.instance_destroy(c, instance_id2) @@ -1004,15 +1005,16 @@ class ComputeTestCase(test.TestCase): c = context.get_admin_context() instance_id1 = self._create_instance({'server_name': 'woot'}) instance_id2 = self._create_instance({ - 'server_name': 'woo', - 'id': 20}) + 'server_name': 'woo', + 'id': 20}) instance_id3 = self._create_instance({ - 'server_name': 'not-woot', - 'id': 30}) + 'server_name': 'not-woot', + 'id': 30}) instances = self.compute_api.get_all(c, search_opts={'ip6': 'ff.*'}) self.assertEqual(len(instances), 1) + self.assertEqual(instances[0].id, instance_id1) instance_ids = [instance.id for instance in instances] self.assertTrue(instance_id1 in instance_ids) @@ -1034,3 +1036,123 @@ class ComputeTestCase(test.TestCase): 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) + + instances = self.compute_api.get_all(c, + search_opts={'flavor': 99}) + self.assertEqual(len(instances), 0) + + 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) -- cgit From 68ca0a6e770eadf1ed56aa9d0bef14c5ca16e172 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 02:49:42 -0700 Subject: add image and flavor searching to v1.0 api fixed missing updates from cut n paste in some doc strings --- nova/api/openstack/servers.py | 3 ++- nova/compute/api.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fb1ce2529..b9347a014 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -472,7 +472,8 @@ class ControllerV10(Controller): search_opts.update(req.str_GET) user_api = ['project_id', 'fixed_ip', 'recurse_zones', - 'reservation_id', 'name', 'fresh', 'status'] + 'reservation_id', 'name', 'fresh', 'status', + 'image', 'flavor'] admin_api = ['ip', 'ip6', 'instance_name'] context = req.environ['nova.context'] diff --git a/nova/compute/api.py b/nova/compute/api.py index cd2aaed96..c4d6f8b8b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -677,16 +677,12 @@ class API(base.Base): context, column, column_regexp) def _get_all_by_column(column_data, column): - """Get instances by regular expression matching - Instance. - """ + """Get instances by exact matching Instance.""" return self.db.instance_get_all_by_column( context, column, column_data) def _get_all_by_flavor(flavor_id): - """Get instances by regular expression matching - Instance. - """ + """Get instances by flavor ID""" try: instance_type = self.db.instance_type_get_by_flavor_id( context, flavor_id) -- cgit From a6968a100d2a2409094f7b434a88c700ebb876f3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 02:59:03 -0700 Subject: flavor needs to be converted to int from query string value --- nova/api/openstack/servers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b9347a014..f470c59e3 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -494,6 +494,9 @@ class ControllerV10(Controller): # Convert recurse_zones into a boolean search_opts['recurse_zones'] = utils.bool_from_str( search_opts.get('recurse_zones', False)) + # convert flavor into an int + if 'flavor' in search_opts: + search_opts['flavor'] = int(search_opts['flavor']) return self._get_items(context, req, is_detail, search_opts=search_opts) @@ -594,6 +597,9 @@ class ControllerV11(Controller): # NOTE(comstud): Making recurse_zones always be True in v1.1 search_opts['recurse_zones'] = True + # convert flavor into an int + if 'flavor' in search_opts: + search_opts['flavor'] = int(search_opts['flavor']) return self._get_items(context, req, is_detail, search_opts=search_opts) -- cgit From bfee5105a2e557a28a605778599e99308f2a126e Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 03:02:50 -0700 Subject: typos --- nova/api/openstack/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f470c59e3..9bfcf585b 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -58,7 +58,7 @@ def check_option_permissions(context, specified_options, known_options = user_api_options # Check for unknown query string params. - spec_unknown_opts = [for opt in specified_options + spec_unknown_opts = [opt for opt in specified_options if opt not in known_options] if spec_unknown_opts: unknown_opt_str = ", ".join(spec_unknown_opts) @@ -67,7 +67,7 @@ def check_option_permissions(context, specified_options, # Check for admin context for the admin commands if not context.is_admin: - spec_admin_opts = [for opt in specified_options + spec_admin_opts = [opt for opt in specified_options if opt in admin_api_options] if spec_admin_opts: admin_opt_str = ", ".join(admin_opts) -- cgit From edaeb96d6ce9c14b1f70a71c219d0353b59ed270 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 03:08:23 -0700 Subject: more typos --- nova/api/openstack/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 9bfcf585b..771939624 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -52,7 +52,7 @@ def check_option_permissions(context, specified_options, # We pretend we don't know about admin_api_options if the admin # API is not enabled. - if FLAGS.enable_admin_api: + if FLAGS.allow_admin_api: known_options = user_api_options + admin_api_options else: known_options = user_api_options @@ -479,7 +479,7 @@ class ControllerV10(Controller): context = req.environ['nova.context'] try: - check_option_permissions(context, search_opt.keys(), + check_option_permissions(context, search_opts.keys(), user_api, admin_api) except exception.InvalidInput: # FIXME(comstud): I refactored code in here to support -- cgit From 043cfae7737a977f7f03d75910742f741b832323 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 03:36:08 -0700 Subject: missed power_state import in api fixed reversed compare in power_state --- nova/api/openstack/servers.py | 8 +++++--- nova/compute/power_state.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 771939624..5a4dcbd9e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -32,6 +32,7 @@ import nova.api.openstack.views.images import nova.api.openstack.views.servers from nova.api.openstack import wsgi import nova.api.openstack +from nova.compute import power_state from nova.scheduler import api as scheduler_api @@ -125,8 +126,9 @@ class Controller(object): if status is not None: search_opts['state'] = power_state.states_from_status(status) if len(search_opts['state']) == 0: - raise exception.InvalidInput(reason=_( - 'Invalid server status')) + reason = _('Invalid server status: %(status)s') % locals() + LOG.error(reason) + raise exception.InvalidInput(reason=reason) instance_list = self.compute_api.get_all( context, search_opts=search_opts) limited_list = self._limit_items(instance_list, req) @@ -482,7 +484,7 @@ class ControllerV10(Controller): check_option_permissions(context, search_opts.keys(), user_api, admin_api) except exception.InvalidInput: - # FIXME(comstud): I refactored code in here to support + # NOTE(comstud): I refactored code in here to support # new search options, and the original code ignored # invalid options. So, I've left it this way for now. # The v1.1 implementation will return an error in this diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index 834ad1c0a..8018c5270 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -79,7 +79,7 @@ def states_from_status(status): power_states = [] for power_state, status_map in _STATUS_MAP.iteritems(): # Skip the 'None' state - if power_state is not None: + if power_state is None: continue if status.lower() == status_map.lower(): power_states.append(power_state) -- cgit From 5a2add5c6011ce94f4727037c193274d21351cb2 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 18 Jul 2011 04:13:22 -0700 Subject: another typo --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8df4ce31d..8c1638e21 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -588,7 +588,7 @@ class ControllerV11(Controller): context = req.environ['nova.context'] try: - check_option_permissions(context, search_opt.keys(), + check_option_permissions(context, search_opts.keys(), user_api, admin_api) except exception.InvalidInput, e: raise faults.Fault(exc.HTTPBadRequest(explanation=str(e))) -- cgit From 0b9048bc3285b86a073da9aa9327815319aaa184 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Tue, 19 Jul 2011 12:44:00 -0700 Subject: allow 'marker' and 'limit' in search options. fix log format error --- nova/api/openstack/servers.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8c1638e21..17a3df344 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -64,8 +64,10 @@ def check_option_permissions(context, specified_options, if opt not in known_options] if spec_unknown_opts: unknown_opt_str = ", ".join(spec_unknown_opts) + LOG.error(_("Received request for unknown options " + "'%(unknown_opt_str)s'") % locals()) raise exception.InvalidInput(reason=_( - "Unknown options specified: %(unknown_opt_str)")) + "Unknown options specified: %(unknown_opt_str)s")) # Check for admin context for the admin commands if not context.is_admin: @@ -73,6 +75,9 @@ def check_option_permissions(context, specified_options, if opt in admin_api_options] if spec_admin_opts: admin_opt_str = ", ".join(admin_opts) + LOG.error(_("Received request for admin options " + "'%(admin_opt_str)s' from non-admin context") % + locals()) raise exception.AdminRequired() @@ -471,9 +476,9 @@ class ControllerV10(Controller): search_opts = {} search_opts.update(req.str_GET) - user_api = ['project_id', 'fixed_ip', 'recurse_zones', - 'reservation_id', 'name', 'fresh', 'status', - 'image', 'flavor'] + user_api = ['marker', 'limit', 'project_id', 'fixed_ip', + 'recurse_zones', 'reservation_id', 'name', 'fresh', + 'status', 'image', 'flavor'] admin_api = ['ip', 'ip6', 'instance_name'] context = req.environ['nova.context'] @@ -581,8 +586,8 @@ class ControllerV11(Controller): search_opts = {} search_opts.update(req.str_GET) - user_api = ['image', 'flavor', 'name', 'status', - 'reservation_id', 'changes-since'] + user_api = ['marker', 'limit', 'image', 'flavor', 'name', + 'status', 'reservation_id', 'changes-since'] admin_api = ['ip', 'ip6', 'instance_name'] context = req.environ['nova.context'] -- cgit From 7630aa8acc376364375ef48a3d955a7c21f50b04 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 11:02:00 -0700 Subject: added API tests for search options fixed a couple of bugs the tests caught --- nova/api/openstack/servers.py | 5 +- nova/tests/api/openstack/fakes.py | 3 +- nova/tests/api/openstack/test_servers.py | 204 ++++++++++++++++++++++++++++++- 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 17a3df344..ed3f82039 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -67,14 +67,15 @@ def check_option_permissions(context, specified_options, LOG.error(_("Received request for unknown options " "'%(unknown_opt_str)s'") % locals()) raise exception.InvalidInput(reason=_( - "Unknown options specified: %(unknown_opt_str)s")) + "Unknown options specified: %(unknown_opt_str)s") % + locals()) # Check for admin context for the admin commands if not context.is_admin: spec_admin_opts = [opt for opt in specified_options if opt in admin_api_options] if spec_admin_opts: - admin_opt_str = ", ".join(admin_opts) + admin_opt_str = ", ".join(spec_admin_opts) LOG.error(_("Received request for admin options " "'%(admin_opt_str)s' from non-admin context") % locals()) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 26b1de818..205a701ab 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -68,7 +68,8 @@ def fake_auth_init(self, application): @webob.dec.wsgify def fake_wsgi(self, req): - req.environ['nova.context'] = context.RequestContext(1, 1) + if 'nova.context' not in req.environ: + req.environ['nova.context'] = context.RequestContext(1, 1) return self.application diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 1577c922b..0ca42a0b5 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -724,6 +724,208 @@ 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): + req = webob.Request.blank('/v1.1/servers?unknownoption=whee') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + self.assertTrue(res.body.find( + "Unknown options specified: unknownoption") > -1) + + 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) + + 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) + + 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(search_opts['state'], + [power_state.RUNNING, power_state.BLOCKED]) + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + 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_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) + + 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_allows_instance_name1_v1_1(self): + """Test getting servers by instance_name with admin_api + disabled + """ + FLAGS.allow_admin_api = False + req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + self.assertTrue(res.body.find( + "Unknown options specified: instance_name") > -1) + + def test_get_servers_allows_instance_name2_v1_1(self): + """Test getting servers by instance_name with admin_api + enabled but non-admin context + """ + FLAGS.allow_admin_api = True + + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=False) + req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') + req.environ["nova.context"] = context + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 403) + self.assertTrue(res.body.find( + "User does not have admin privileges") > -1) + + def test_get_servers_allows_instance_name3_v1_1(self): + """Test getting servers by instance_name with admin_api + enabled and admin context + """ + FLAGS.allow_admin_api = True + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('instance_name' in search_opts) + self.assertEqual(search_opts['instance_name'], 'whee.*') + return [stub_instance(100)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') + # Request admin context + context = nova.context.RequestContext('testuser', 'testproject', + is_admin=True) + req.environ["nova.context"] = context + 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_ip_v1_1(self): + """Test getting servers by ip with admin_api enabled and + admin context + """ + 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) + req.environ["nova.context"] = context + 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_ip6_v1_1(self): + """Test getting servers by ip6 with admin_api enabled and + admin context + """ + 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) + req.environ["nova.context"] = context + 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 _setup_for_create_instance(self): """Shared implementation for tests below that create instance""" def instance_create(context, inst): @@ -1665,7 +1867,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 202) self.assertEqual(self.resize_called, True) - def test_resize_server_v11(self): + def test_resize_server_v1_1(self): req = webob.Request.blank('/v1.1/servers/1/action') req.content_type = 'application/json' -- cgit From b1099b43f34e41676b0508267e9ad40b2c3415e3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 11:32:43 -0700 Subject: ec2 fixes --- nova/api/ec2/cloud.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 76725370a..9aef5079c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -832,9 +832,7 @@ class CloudController(object): for ec2_id in instance_id: internal_id = ec2utils.ec2_id_to_id(ec2_id) try: - instance = self.compute_api.get(context, - instance_id=internal_id, - search_opts=search_opts) + instance = self.compute_api.get(context, internal_id) except exception.NotFound: continue instances.append(instance) @@ -1021,7 +1019,7 @@ class CloudController(object): 'AvailabilityZone'), block_device_mapping=kwargs.get('block_device_mapping', {})) return self._format_run_instances(context, - instance_id=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) -- cgit From 6ebd04c9c971e3be63cb3d6122bbca7c95004085 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 11:43:56 -0700 Subject: test fix for renamed get_by_fixed_ip call --- nova/tests/test_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index c862726ab..b9b14d1ea 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -52,7 +52,7 @@ class MetadataTestCase(test.TestCase): 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_by_fixed_ip', instance_get) self.stubs.Set(api, 'instance_get_floating_address', floating_get) self.app = metadatarequesthandler.MetadataRequestHandler() -- cgit From bc2a2f30e4b8ab92d6893ec333e756c92e96a932 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 11:48:52 -0700 Subject: pep8 fixes --- nova/compute/power_state.py | 18 ++++++++++-------- nova/db/sqlalchemy/api.py | 1 - 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index 8018c5270..bdedd8da8 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -48,14 +48,6 @@ _STATE_MAP = { BUILDING: 'building', } - -def name(code): - return _STATE_MAP[code] - - -def valid_states(): - return _STATE_MAP.keys() - _STATUS_MAP = { None: 'BUILD', NOSTATE: 'BUILD', @@ -70,10 +62,20 @@ _STATUS_MAP = { BUILDING: 'BUILD', } + +def name(code): + return _STATE_MAP[code] + + +def valid_states(): + return _STATE_MAP.keys() + + def status_from_state(power_state): """Map the power state to the server status string""" return _STATUS_MAP[power_state] + def states_from_status(status): """Map the server status string to a list of power states""" power_states = [] diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 331c58fa3..7eb724785 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1248,7 +1248,6 @@ def instance_get_all_by_column(context, column, column_data): """ session = get_session() - prefix = session.query(models.Instance).\ options(joinedload_all('fixed_ips.floating_ips')).\ options(joinedload('virtual_interfaces')).\ -- cgit From a1cb17bf98359fae760800f8467c897d859b6994 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 12:16:23 -0700 Subject: minor fixups --- nova/api/openstack/servers.py | 14 +++++++++----- nova/compute/api.py | 7 ------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index ed3f82039..8ec74b387 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -64,11 +64,10 @@ def check_option_permissions(context, specified_options, if opt not in known_options] if spec_unknown_opts: unknown_opt_str = ", ".join(spec_unknown_opts) - LOG.error(_("Received request for unknown options " - "'%(unknown_opt_str)s'") % locals()) - raise exception.InvalidInput(reason=_( - "Unknown options specified: %(unknown_opt_str)s") % - locals()) + reason = _("Received request for unknown options " + "'%(unknown_opt_str)s'") % locals() + LOG.error(reason) + raise exception.InvalidInput(reason=reason) # Check for admin context for the admin commands if not context.is_admin: @@ -136,6 +135,11 @@ class Controller(object): reason = _('Invalid server status: %(status)s') % locals() LOG.error(reason) raise exception.InvalidInput(reason=reason) + + # Don't pass these along to compute API, if they exist. + search_opts.pop('changes-since', None) + search_opts.pop('fresh', None) + instance_list = self.compute_api.get_all( context, search_opts=search_opts) limited_list = self._limit_items(instance_list, req) diff --git a/nova/compute/api.py b/nova/compute/api.py index 6675fff52..a031f8ab5 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -744,10 +744,6 @@ class API(base.Base): # Search options are mutually exclusive known_params = { 'recurse_zones': (None, None), - # v1.0 API? - 'fresh': (None, None), - # v1.1 API - 'changes-since': (None, None), # Mutually exclusive options 'name': (_get_all_by_column_regexp, ('display_name',)), 'reservation_id': (_get_all_by_reservation_id, ()), @@ -762,9 +758,6 @@ class API(base.Base): 'state': (_get_all_by_column, ('state',)), 'flavor': (_get_all_by_flavor, ())} - # FIXME(comstud): 'fresh' and 'changes-since' are currently not - # implemented... - if search_opts is None: search_opts = {} -- cgit From 11101dfb47a7c3a37d3d3ec04f36e33fff9f59e2 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 12:22:02 -0700 Subject: test fixes after unknown option string changes --- nova/tests/api/openstack/test_servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 0ca42a0b5..df951c5a0 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -743,7 +743,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) self.assertTrue(res.body.find( - "Unknown options specified: unknownoption") > -1) + "unknown options 'unknownoption'") > -1) def test_get_servers_allows_image_v1_1(self): def fake_get_all(compute_self, context, search_opts=None): @@ -828,7 +828,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) self.assertTrue(res.body.find( - "Unknown options specified: instance_name") > -1) + "unknown options 'instance_name'") > -1) def test_get_servers_allows_instance_name2_v1_1(self): """Test getting servers by instance_name with admin_api -- cgit From ff1c882d7b12aa77895c549769a27ff4913b29c8 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 12:29:42 -0700 Subject: clarify a couple comments --- nova/compute/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index a031f8ab5..9a606add2 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -747,9 +747,9 @@ class API(base.Base): # Mutually exclusive options 'name': (_get_all_by_column_regexp, ('display_name',)), 'reservation_id': (_get_all_by_reservation_id, ()), - # Needed for EC2 API + # 'fixed_ip' needed for EC2 API 'fixed_ip': (_get_all_by_fixed_ip, ()), - # Needed for EC2 API + # 'project_id' needed for EC2 API 'project_id': (_get_all_by_project_id, ()), 'ip': (_get_all_by_ip, ()), 'ip6': (_get_all_by_ipv6, ()), -- cgit From 970b37ff9e9aef987f6e87df7d2c2e73c484e439 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 20 Jul 2011 12:35:42 -0700 Subject: missing doc strings for fixed_ip calls I renamed --- nova/db/sqlalchemy/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 43b9e195b..a3fc6d733 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1221,12 +1221,14 @@ def instance_get_all_by_reservation(context, reservation_id): @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 -- cgit From 994e219ab0b25d48b31484a43a0ac12099cf226e Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 3 Aug 2011 00:46:38 -0700 Subject: rework OS API checking of search options --- nova/api/openstack/servers.py | 181 ++++++++++++------------------- nova/tests/api/openstack/test_servers.py | 29 +++-- 2 files changed, 84 insertions(+), 126 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 2573fc48c..b028d3a40 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -41,44 +41,46 @@ LOG = logging.getLogger('nova.api.openstack.servers') FLAGS = flags.FLAGS -def check_option_permissions(context, specified_options, - user_api_options, admin_api_options): - """Check whether or not entries in 'specified_options' are valid - based on the allowed 'user_api_options' and 'admin_api_options'. +def check_admin_search_options(context, search_options, admin_api_options): + """Check for any 'admin_api_options' specified in 'search_options'. + + If admin api is not enabled, we should pretend that we know nothing + about those options.. Ie, they don't exist in user-facing API. To + achieve this, we will strip any admin options that we find from + search_options + + If admin api is enabled, we should require admin context for any + admin options specified, and return an exception in this case. + + If any exist and admin api is not enabled, strip them from + search_options (has the effect of treating them like they don't exist). + + search_options is a dictionary of "search_option": value + admin_api_options is a list + + Returns: None if options are okay. + Modifies: admin options could be stripped from search_options + Raises: exception.AdminRequired for needing admin context + """ - All inputs are lists of option names + if not FLAGS.allow_admin_api: + # Remove any admin_api_options from search_options + for option in admin_api_options: + search_options.pop(option, None) + return - Returns: exception.InvalidInput for an invalid option or - exception.AdminRequired for needing admin privs - """ + # allow_admin_api is True and admin context? Any command is okay. + if context.is_admin: + return - # We pretend we don't know about admin_api_options if the admin - # API is not enabled. - if FLAGS.allow_admin_api: - known_options = user_api_options + admin_api_options - else: - known_options = user_api_options - - # Check for unknown query string params. - spec_unknown_opts = [opt for opt in specified_options - if opt not in known_options] - if spec_unknown_opts: - unknown_opt_str = ", ".join(spec_unknown_opts) - reason = _("Received request for unknown options " - "'%(unknown_opt_str)s'") % locals() - LOG.error(reason) - raise exception.InvalidInput(reason=reason) - - # Check for admin context for the admin commands - if not context.is_admin: - spec_admin_opts = [opt for opt in specified_options - if opt in admin_api_options] - if spec_admin_opts: - admin_opt_str = ", ".join(spec_admin_opts) - LOG.error(_("Received request for admin options " - "'%(admin_opt_str)s' from non-admin context") % - locals()) - raise exception.AdminRequired() + spec_admin_opts = [opt for opt in search_options.iterkeys() + if opt in admin_api_options] + if spec_admin_opts: + admin_opt_str = ", ".join(spec_admin_opts) + LOG.error(_("Received request for admin-only search options " + "'%(admin_opt_str)s' from non-admin context") % + locals()) + raise exception.AdminRequired() class Controller(object): @@ -91,7 +93,7 @@ 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._servers_from_request(req, is_detail=False) except exception.Invalid as err: return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) except exception.NotFound: @@ -101,7 +103,7 @@ class Controller(object): def detail(self, req): """ Returns a list of server details for a given user """ try: - servers = self._items(req, is_detail=True) + servers = self._servers_from_request(req, is_detail=True) except exception.Invalid as err: return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) except exception.NotFound as err: @@ -117,10 +119,9 @@ class Controller(object): def _action_rebuild(self, info, request, instance_id): raise NotImplementedError() - def _get_items(self, context, req, is_detail, search_opts=None): - """Returns a list of servers. - - builder - the response model builder + def _servers_search(self, context, req, is_detail, search_opts=None): + """Returns a list of servers, taking into account any search + options specified. """ if search_opts is None: @@ -147,6 +148,34 @@ class Controller(object): for inst in limited_list] return dict(servers=servers) + def _servers_from_request(self, req, is_detail): + """Returns a list of servers based on the request. + + Checks for search options and permissions on the options. + """ + + search_opts = {} + search_opts.update(req.str_GET) + + admin_api = ['ip', 'ip6', 'instance_name'] + + context = req.environ['nova.context'] + + try: + check_admin_search_options(context, search_opts, admin_api) + except exception.AdminRequired, e: + raise exc.HTTPForbidden(detail=str(e)) + + # Convert recurse_zones into a boolean + search_opts['recurse_zones'] = utils.bool_from_str( + search_opts.get('recurse_zones', False)) + # convert flavor into an int + if 'flavor' in search_opts: + search_opts['flavor'] = int(search_opts['flavor']) + + return self._servers_search(context, req, is_detail, + search_opts=search_opts) + @scheduler_api.redirect_handler def show(self, req, id): """ Returns server details by server id """ @@ -469,45 +498,6 @@ class ControllerV10(Controller): raise exc.HTTPNotFound() return webob.Response(status_int=202) - def _items(self, req, is_detail): - """Returns a list of servers based on the request. - - Checks for search options and permissions on the options. - """ - - search_opts = {} - search_opts.update(req.str_GET) - - user_api = ['marker', 'limit', 'project_id', 'fixed_ip', - 'recurse_zones', 'reservation_id', 'name', 'fresh', - 'status', 'image', 'flavor'] - admin_api = ['ip', 'ip6', 'instance_name'] - - context = req.environ['nova.context'] - - try: - check_option_permissions(context, search_opts.keys(), - user_api, admin_api) - except exception.InvalidInput: - # NOTE(comstud): I refactored code in here to support - # new search options, and the original code ignored - # invalid options. So, I've left it this way for now. - # The v1.1 implementation will return an error in this - # case.. - pass - except exception.AdminRequired, e: - raise faults.Fault(exc.HTTPForbidden(detail=str(e))) - - # Convert recurse_zones into a boolean - search_opts['recurse_zones'] = utils.bool_from_str( - search_opts.get('recurse_zones', False)) - # convert flavor into an int - if 'flavor' in search_opts: - search_opts['flavor'] = int(search_opts['flavor']) - - return self._get_items(context, req, is_detail, - search_opts=search_opts) - def _image_ref_from_req_data(self, data): return data['server']['imageId'] @@ -572,37 +562,6 @@ class ControllerV11(Controller): except exception.NotFound: raise exc.HTTPNotFound() - def _items(self, req, is_detail): - """Returns a list of servers based on the request. - - Checks for search options and permissions on the options. - """ - - search_opts = {} - search_opts.update(req.str_GET) - - user_api = ['marker', 'limit', 'image', 'flavor', 'name', - 'status', 'reservation_id', 'changes-since'] - admin_api = ['ip', 'ip6', 'instance_name'] - - context = req.environ['nova.context'] - - try: - check_option_permissions(context, search_opts.keys(), - user_api, admin_api) - except exception.InvalidInput, e: - raise faults.Fault(exc.HTTPBadRequest(explanation=str(e))) - except exception.AdminRequired, e: - raise faults.Fault(exc.HTTPForbidden(explanation=str(e))) - - # NOTE(comstud): Making recurse_zones always be True in v1.1 - search_opts['recurse_zones'] = True - # convert flavor into an int - if 'flavor' in search_opts: - search_opts['flavor'] = int(search_opts['flavor']) - return self._get_items(context, req, is_detail, - search_opts=search_opts) - def _image_ref_from_req_data(self, data): return data['server']['imageRef'] diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 821b055c4..a52940888 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1053,11 +1053,18 @@ class ServersTest(test.TestCase): 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, 400) - self.assertTrue(res.body.find( - "unknown options 'unknownoption'") > -1) + 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): @@ -1134,17 +1141,6 @@ class ServersTest(test.TestCase): self.assertEqual(servers[0]['id'], 100) def test_get_servers_allows_instance_name1_v1_1(self): - """Test getting servers by instance_name with admin_api - disabled - """ - FLAGS.allow_admin_api = False - req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - self.assertTrue(res.body.find( - "unknown options 'instance_name'") > -1) - - def test_get_servers_allows_instance_name2_v1_1(self): """Test getting servers by instance_name with admin_api enabled but non-admin context """ @@ -1156,10 +1152,13 @@ class ServersTest(test.TestCase): req.environ["nova.context"] = context res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 403) + print '*' * 80 + print res.body + print '*' * 80 self.assertTrue(res.body.find( "User does not have admin privileges") > -1) - def test_get_servers_allows_instance_name3_v1_1(self): + def test_get_servers_allows_instance_name2_v1_1(self): """Test getting servers by instance_name with admin_api enabled and admin context """ -- cgit From a5390a5b1cb95ca9aee6e2f99572498dd60b48e5 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 3 Aug 2011 00:53:13 -0700 Subject: remove faults.Fault wrapper on exceptions --- nova/api/openstack/servers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b028d3a40..6ad549c97 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -95,9 +95,9 @@ class Controller(object): try: servers = self._servers_from_request(req, is_detail=False) except exception.Invalid as err: - return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) + return exc.HTTPBadRequest(explanation=str(err)) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNotFound() return servers def detail(self, req): @@ -105,9 +105,9 @@ class Controller(object): try: servers = self._servers_from_request(req, is_detail=True) except exception.Invalid as err: - return faults.Fault(exc.HTTPBadRequest(explanation=str(err))) + return exc.HTTPBadRequest(explanation=str(err)) except exception.NotFound as err: - return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNotFound() return servers def _build_view(self, req, instance, is_detail=False): -- cgit From 450a21d8c1bed9cf6d1bcee9bcde7e88b9c3c6b9 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 3 Aug 2011 00:55:33 -0700 Subject: remove debug from failing test --- nova/tests/api/openstack/test_servers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index a52940888..cdf36cabb 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1152,9 +1152,6 @@ class ServersTest(test.TestCase): req.environ["nova.context"] = context res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 403) - print '*' * 80 - print res.body - print '*' * 80 self.assertTrue(res.body.find( "User does not have admin privileges") > -1) -- cgit From b5ac286fade15a61326068e5ef0959352f885efe Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 3 Aug 2011 23:08:42 -0700 Subject: a lot of major re-work.. still things to finish up --- nova/compute/api.py | 157 ++++------------------ nova/db/api.py | 39 +----- nova/db/sqlalchemy/api.py | 328 ++++++++++++++-------------------------------- 3 files changed, 129 insertions(+), 395 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 5862b6d45..4d0654d20 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -677,155 +677,44 @@ class API(base.Base): all instances in the system. """ - def _get_all_by_reservation_id(reservation_id): - """Get instances by reservation ID""" - # reservation_id implies recurse_zones - search_opts['recurse_zones'] = True - return self.db.instance_get_all_by_reservation(context, - reservation_id) - - def _get_all_by_project_id(project_id): - """Get instances by project ID""" - return self.db.instance_get_all_by_project(context, project_id) - - def _get_all_by_fixed_ip(fixed_ip): - """Get instance by fixed IP""" - try: - instances = self.db.instance_get_by_fixed_ip(context, - fixed_ip) - except exception.FixedIpNotFound, e: - if search_opts.get('recurse_zones', False): - return [] - else: - raise - if not instances: - raise exception.FixedIpNotFoundForAddress(address=fixed_ip) - return instances - - def _get_all_by_instance_name(instance_name_regexp): - """Get instances by matching the Instance.name property""" - return self.db.instance_get_all_by_name_regexp( - context, instance_name_regexp) - - def _get_all_by_ip(ip_regexp): - """Get instances by matching IPv4 addresses""" - return self.db.instance_get_all_by_ip_regexp(context, ip_regexp) - - def _get_all_by_ipv6(ipv6_regexp): - """Get instances by matching IPv6 addresses""" - return self.db.instance_get_all_by_ipv6_regexp(context, - ipv6_regexp) - - def _get_all_by_column_regexp(column_regexp, column): - """Get instances by regular expression matching - Instance. - """ - return self.db.instance_get_all_by_column_regexp( - context, column, column_regexp) - - def _get_all_by_column(column_data, column): - """Get instances by exact matching Instance.""" - return self.db.instance_get_all_by_column( - context, column, column_data) - - def _get_all_by_flavor(flavor_id): - """Get instances by flavor ID""" - try: - instance_type = self.db.instance_type_get_by_flavor_id( - context, flavor_id) - except exception.FlavorNotFound: - return [] - return self.db.instance_get_all_by_column( - context, 'instance_type_id', instance_type['id']) - - # Define the search params that we will allow. This is a mapping - # of the search param to tuple of (function_to_call, (function_args)) - # A 'None' function means it's an optional parameter that will - # influence the search results, but itself is not a search option. - # Search options are mutually exclusive - known_params = { - 'recurse_zones': (None, None), - # Mutually exclusive options - 'name': (_get_all_by_column_regexp, ('display_name',)), - 'reservation_id': (_get_all_by_reservation_id, ()), - # 'fixed_ip' needed for EC2 API - 'fixed_ip': (_get_all_by_fixed_ip, ()), - # 'project_id' needed for EC2 API - 'project_id': (_get_all_by_project_id, ()), - 'ip': (_get_all_by_ip, ()), - 'ip6': (_get_all_by_ipv6, ()), - 'instance_name': (_get_all_by_instance_name, ()), - 'image': (_get_all_by_column, ('image_ref',)), - 'state': (_get_all_by_column, ('state',)), - 'flavor': (_get_all_by_flavor, ())} - if search_opts is None: search_opts = {} LOG.debug(_("Searching by: %s") % str(search_opts)) - # Mutually exclusive serach options are any options that have - # a function to call. Raise an exception if more than 1 is - # specified... - # NOTE(comstud): Ignore unknown options. The OS API will - # do it's own verification on options.. - found_opts = [opt for opt in search_opts.iterkeys() - if opt in known_params and \ - known_params[opt][0] is not None] - if len(found_opts) > 1: - found_opt_str = ", ".join(found_opts) - msg = _("More than 1 mutually exclusive " - "search option specified: %(found_opt_str)s") \ - % locals() - LOG.error(msg) - raise exception.InvalidInput(reason=msg) - - # Found a search option? - if found_opts: - found_opt = found_opts[0] - f, f_args = known_params[found_opt] - instances = f(search_opts[found_opt], *f_args) - # Nope. Return all instances if the request is in admin context.. - elif context.is_admin: - instances = self.db.instance_get_all(context) - # Nope. Return all instances for the user/project - else: - if context.project_id: - instances = self.db.instance_get_all_by_project( - context, context.project_id) + # Fixups for the DB call + filters = search_opts.copy() + if 'image' in filters: + filters['image_ref'] = filters['image'] + del filters['image'] + if 'flavor' in filters: + flavor_id = int(filters['flavor']) + try: + instance_type = self.db.instance_type_get_by_flavor_id( + context, flavor_id) + except exception.FlavorNotFound: + pass else: - instances = self.db.instance_get_all_by_user( - context, context.user_id) + filters['instance_type_id'] = instance_type['id'] + del filters['flavor'] + + recurse_zones = filters.pop('recurse_zones', False) + if 'reservation_id' in filters: + recurse_zones = True - # Convert any responses into a list of instances - if instances is None: - instances = [] - elif not isinstance(instances, list): - instances = [instances] + instances = self.db.instance_get_all_by_filters(context, filters) - if not search_opts.get('recurse_zones', False): + if not recurse_zones: return instances - new_search_opts = {} - new_search_opts.update(search_opts) - # API does state search by status, instead of the real power - # state. So if we're searching by 'state', we need to - # convert this back into 'status' - state = new_search_opts.pop('state', None) - if state: - # Might be a list.. we can only use 1. - if isinstance(state, list): - state = state[0] - new_search_opts['status'] = power_state.status_from_state( - state) - - # Recurse zones. Need admin context for this. + # 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", - search_opts=new_search_opts) + search_opts=search_opts) for zone, servers in children: # 'servers' can be None if a 404 was returned by a zone diff --git a/nova/db/api.py b/nova/db/api.py index 22dd8a4b4..c7d5420e1 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -491,6 +491,10 @@ 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) @@ -526,41 +530,6 @@ def instance_get_by_fixed_ipv6(context, address): return IMPL.instance_get_by_fixed_ipv6(context, address) -def instance_get_all_by_column(context, column, column_data): - """Get all instances by exact match against the specified DB column""" - return IMPL.instance_get_all_by_column(context, column, column_data) - - -def instance_get_all_by_column_regexp(context, column, column_regexp): - """Get all instances by using regular expression matching against - a particular DB column - """ - return IMPL.instance_get_all_by_column_regexp(context, - column, - column_regexp) - - -def instance_get_all_by_name_regexp(context, name_regexp): - """Get all instances by using regular expression matching against - its name - """ - return IMPL.instance_get_all_by_name_regexp(context, name_regexp) - - -def instance_get_all_by_ip_regexp(context, ip_regexp): - """Get all instances by using regular expression matching against - Floating and Fixed IP Addresses - """ - return IMPL.instance_get_all_by_ip_regexp(context, ip_regexp) - - -def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): - """Get all instances by using regular expression matching against - IPv6 Addresses - """ - return IMPL.instance_get_all_by_ipv6_regexp(context, ipv6_regexp) - - 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) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e4250a6cb..25a486b9c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1147,6 +1147,108 @@ def instance_get_all(context): all() +@require_context +def instance_get_all_by_filters(context, filters): + """Return instances the match all filters""" + + def _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 _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 _filter_by_display_name(instance, filter_re): + if filter_re.match(instance.display_name): + return True + return False + + def _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 + + 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('security_groups')).\ + options(joinedload_all('fixed_ips.network')).\ + options(joinedload('metadata')).\ + options(joinedload('instance_type')).\ + filter_by(deleted=can_read_deleted(context)) + + 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 that we can do along with the SQL query... + query_filter_funcs = { + 'project_id': lambda query, value: query.filter_by( + project_id=value), + 'user_id': lambda query, value: query.filter_by( + user_id=value), + 'reservation_id': lambda query, value: query.filter_by( + reservation_id=value), + 'state': lambda query, value: query.filter_by(state=value)} + + query_filters = [key for key in filters.iterkeys() + if key in query_filter_funcs] + + for filter_name in query_filters: + query_prefix = query_filter_funcs[filter_name](query_prefix, + filters[filter_name]) + # Remove this from filters, so it doesn't get tried below + del filters[filter_name] + + instances = query_prefix.all() + + if not instances: + return [] + + # Now filter on everything else for regexp matching.. + filter_funcs = {'ip6': _filter_by_ipv6, + 'ip': _filter_by_ip, + 'name': _filter_by_display_name} + + for filter_name in filters.iterkeys(): + filter_func = filter_funcs.get(filter_name, None) + filter_re = re.compile(filters[filter_name]) + if filter_func: + filter_l = lambda instance: filter_func(instance, filter_re) + else: + filter_l = lambda instance: _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""" @@ -1259,232 +1361,6 @@ def instance_get_by_fixed_ipv6(context, address): return result -@require_context -def instance_get_all_by_column(context, column, column_data): - """Get all instances by exact match against the specified DB column - 'column_data' can be a list. - """ - session = get_session() - - prefix = 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(deleted=can_read_deleted(context)) - - if isinstance(column_data, list): - column_attr = getattr(models.Instance, column) - prefix = prefix.filter(column_attr.in_(column_data)) - else: - # Set up the dictionary for filter_by() - query_filter = {} - query_filter[column] = column_data - prefix = prefix.filter_by(**query_filter) - - if context.is_admin: - all_instances = prefix.all() - elif context.project: - all_instances = prefix.\ - filter_by(project_id=context.project_id).\ - all() - else: - all_instances = prefix.\ - filter_by(user_id=context.user_id).\ - all() - if not all_instances: - return [] - - return all_instances - - -@require_context -def instance_get_all_by_column_regexp(context, column, column_regexp): - """Get all instances by using regular expression matching against - a particular DB column - """ - session = get_session() - - # MySQL 'regexp' is not portable, so we must do our own matching. - # First... grab all Instances. - prefix = 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(deleted=can_read_deleted(context)) - - if context.is_admin: - all_instances = prefix.all() - elif context.project: - all_instances = prefix.\ - filter_by(project_id=context.project_id).\ - all() - else: - all_instances = prefix.\ - filter_by(user_id=context.user_id).\ - all() - if not all_instances: - return [] - # Now do the regexp matching - compiled_regexp = re.compile(column_regexp) - instances = [] - for instance in all_instances: - v = getattr(instance, column) - if v and compiled_regexp.match(v): - instances.append(instance) - return instances - - -@require_context -def instance_get_all_by_name_regexp(context, name_regexp): - """Get all instances by using regular expression matching against - its name - """ - - session = get_session() - - # MySQL 'regexp' is not portable, so we must do our own matching. - # First... grab all Instances. - prefix = 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(deleted=can_read_deleted(context)) - - if context.is_admin: - all_instances = prefix.all() - elif context.project: - all_instances = prefix.\ - filter_by(project_id=context.project_id).\ - all() - else: - all_instances = prefix.\ - filter_by(user_id=context.user_id).\ - all() - if not all_instances: - return [] - # Now do the regexp matching - compiled_regexp = re.compile(name_regexp) - return [instance for instance in all_instances - if compiled_regexp.match(instance.name)] - - -@require_context -def instance_get_all_by_ip_regexp(context, ip_regexp): - """Get all instances by using regular expression matching against - Floating and Fixed IP Addresses - """ - session = get_session() - - # Query both FixedIp and FloatingIp tables to get matches. - # Since someone could theoretically search for something that matches - # instances in both tables... we need to use a dictionary keyed - # on instance ID to make sure we return only 1. We can't key off - # of 'instance' because it's just a reference and will be different - # addresses even though they might point to the same instance ID. - instances = {} - - fixed_ips = session.query(models.FixedIp).\ - options(joinedload_all('instance.fixed_ips.floating_ips')).\ - options(joinedload('instance.virtual_interfaces')).\ - options(joinedload('instance.security_groups')).\ - options(joinedload_all('instance.fixed_ips.network')).\ - options(joinedload('instance.metadata')).\ - options(joinedload('instance.instance_type')).\ - filter_by(deleted=can_read_deleted(context)).\ - all() - floating_ips = session.query(models.FloatingIp).\ - options(joinedload_all( - 'fixed_ip.instance.fixed_ips.floating_ips')).\ - options(joinedload('fixed_ip.instance.virtual_interfaces')).\ - options(joinedload('fixed_ip.instance.security_groups')).\ - options(joinedload_all( - 'fixed_ip.instance.fixed_ips.network')).\ - options(joinedload('fixed_ip.instance.metadata')).\ - options(joinedload('fixed_ip.instance.instance_type')).\ - filter_by(deleted=can_read_deleted(context)).\ - all() - - if fixed_ips is None: - fixed_ips = [] - if floating_ips is None: - floating_ips = [] - - compiled_regexp = re.compile(ip_regexp) - instances = {} - - # Now do the regexp matching - for fixed_ip in fixed_ips: - if fixed_ip.instance and compiled_regexp.match(fixed_ip.address): - instances[fixed_ip.instance.uuid] = fixed_ip.instance - for floating_ip in floating_ips: - fixed_ip = floating_ip.fixed_ip - if fixed_ip and fixed_ip.instance and\ - compiled_regexp.match(floating_ip.address): - instances[fixed_ip.instance.uuid] = fixed_ip.instance - - if context.is_admin: - return instances.values() - elif context.project: - return [instance for instance in instances.values() - if instance.project_id == context.project_id] - else: - return [instance for instance in instances.values() - if instance.user_id == context.user_id] - - return instances.values() - - -@require_context -def instance_get_all_by_ipv6_regexp(context, ipv6_regexp): - """Get all instances by using regular expression matching against - IPv6 Addresses - """ - - session = get_session() - with session.begin(): - prefix = 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(deleted=can_read_deleted(context)) - - if context.is_admin: - all_instances = prefix.all() - elif context.project: - all_instances = prefix.\ - filter_by(project_id=context.project_id).\ - all() - else: - all_instances = prefix.\ - filter_by(user_id=context.user_id).\ - all() - - if not all_instances: - return [] - - instances = [] - compiled_regexp = re.compile(ipv6_regexp) - for instance in all_instances: - ipv6_addrs = _ipv6_get_by_instance_ref(context, instance) - for ipv6 in ipv6_addrs: - if compiled_regexp.match(ipv6): - instances.append(instance) - break - return instances - - @require_admin_context def instance_get_project_vpn(context, project_id): session = get_session() -- cgit From c0851f2ec5be12c43cc96367e22220d25589e4ae Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 01:36:12 -0700 Subject: cleanup checking of options in the API before calling compute_api's get_all() --- nova/api/openstack/servers.py | 104 ++++++++++++++++++------------------------ nova/db/sqlalchemy/api.py | 7 +-- 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 965cf0bfc..e3e829e81 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -44,55 +44,25 @@ LOG = logging.getLogger('nova.api.openstack.servers') FLAGS = flags.FLAGS -def check_admin_search_options(context, search_options, admin_api_options): - """Check for any 'admin_api_options' specified in 'search_options'. - - If admin api is not enabled, we should pretend that we know nothing - about those options.. Ie, they don't exist in user-facing API. To - achieve this, we will strip any admin options that we find from - search_options - - If admin api is enabled, we should require admin context for any - admin options specified, and return an exception in this case. - - If any exist and admin api is not enabled, strip them from - search_options (has the effect of treating them like they don't exist). - - search_options is a dictionary of "search_option": value - admin_api_options is a list - - Returns: None if options are okay. - Modifies: admin options could be stripped from search_options - Raises: exception.AdminRequired for needing admin context - """ - - if not FLAGS.allow_admin_api: - # Remove any admin_api_options from search_options - for option in admin_api_options: - search_options.pop(option, None) - return - - # allow_admin_api is True and admin context? Any command is okay. - if context.is_admin: - return - - spec_admin_opts = [opt for opt in search_options.iterkeys() - if opt in admin_api_options] - if spec_admin_opts: - admin_opt_str = ", ".join(spec_admin_opts) - LOG.error(_("Received request for admin-only search options " - "'%(admin_opt_str)s' from non-admin context") % - locals()) - raise exception.AdminRequired() - - class Controller(object): - """ The Server API controller for the OpenStack API """ + """ The Server API base controller class for the OpenStack API """ + + servers_search_options = [] def __init__(self): self.compute_api = compute.API() self.helper = helper.CreateInstanceHelper(self) + def _check_servers_options(self, search_options): + 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 self.servers_search_options] + for opt in unknown_options: + search_options.pop(opt, None) + def index(self, req): """ Returns a list of server names and ids for a given user """ try: @@ -131,21 +101,38 @@ class Controller(object): search_opts = {} # If search by 'status', we need to convert it to 'state' - # If the status is unknown, bail - status = search_opts.pop('status', None) - if status is not None: + # 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'] = power_state.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) - # Don't pass these along to compute API, if they exist. - search_opts.pop('changes-since', None) - search_opts.pop('fresh', None) + # 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( 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] @@ -160,21 +147,12 @@ class Controller(object): search_opts = {} search_opts.update(req.str_GET) - admin_api = ['ip', 'ip6', 'instance_name'] - context = req.environ['nova.context'] - - try: - check_admin_search_options(context, search_opts, admin_api) - except exception.AdminRequired, e: - raise exc.HTTPForbidden(detail=str(e)) + self._check_servers_options(context, search_opts) # Convert recurse_zones into a boolean search_opts['recurse_zones'] = utils.bool_from_str( search_opts.get('recurse_zones', False)) - # convert flavor into an int - if 'flavor' in search_opts: - search_opts['flavor'] = int(search_opts['flavor']) return self._servers_search(context, req, is_detail, search_opts=search_opts) @@ -581,6 +559,10 @@ class Controller(object): class ControllerV10(Controller): + """v1.0 OpenStack API controller""" + + servers_search_options = ["reservation_id", "fixed_ip", + "name", "recurse_zones"] @scheduler_api.redirect_handler def delete(self, req, id): @@ -645,6 +627,10 @@ class ControllerV10(Controller): class ControllerV11(Controller): + """v1.1 OpenStack API controller""" + + servers_search_options = ["reservation_id", "name", "recurse_zones", + "status", "image", "flavor", "changes-since"] @scheduler_api.redirect_handler def delete(self, req, id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 25a486b9c..04c9273b0 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1149,7 +1149,9 @@ def instance_get_all(context): @require_context def instance_get_all_by_filters(context, filters): - """Return instances the match all filters""" + """Return instances the match all filters. Deleted instances + will be returned by default, unless there's a filter that says + otherwise""" def _filter_by_ipv6(instance, filter_re): for interface in instance['virtual_interfaces']: @@ -1195,8 +1197,7 @@ def instance_get_all_by_filters(context, filters): options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ - options(joinedload('instance_type')).\ - filter_by(deleted=can_read_deleted(context)) + options(joinedload('instance_type')) filters = filters.copy() -- cgit From b9ecf869761ee0506872b0d44d93d453be4c3477 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 01:45:42 -0700 Subject: typos --- nova/compute/api.py | 4 ++-- nova/db/api.py | 2 +- nova/db/sqlalchemy/api.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 4d0654d20..4d577b578 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -725,8 +725,8 @@ class API(base.Base): server._info['_is_precooked'] = True instances.append(server._info) - # Fixed IP returns a FixedIpNotFound when an instance is not - # found... + # fixed_ip searching should return a FixedIpNotFound exception + # when an instance is not found... fixed_ip = search_opts.get('fixed_ip', None) if fixed_ip and not instances: raise exception.FixedIpNotFoundForAddress(address=fixed_ip) diff --git a/nova/db/api.py b/nova/db/api.py index c7d5420e1..23fac9921 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -491,7 +491,7 @@ def instance_get_all(context): return IMPL.instance_get_all(context) -def instance_get_all_by-filters(context, filters): +def instance_get_all_by_filters(context, filters): """Get all instances that match all filters.""" return IMPL.instance_get_all_by_filters(context, filters) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 04c9273b0..55c804ae9 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1149,7 +1149,7 @@ def instance_get_all(context): @require_context def instance_get_all_by_filters(context, filters): - """Return instances the match all filters. Deleted instances + """Return instances that match all filters. Deleted instances will be returned by default, unless there's a filter that says otherwise""" -- cgit From b1b919d42d8c359fc9ae981b44466d269fc688a6 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 13:59:57 -0700 Subject: test fixes and typos --- nova/api/openstack/common.py | 33 +++++++++++++++++++++ nova/api/openstack/servers.py | 9 +++--- nova/api/openstack/views/servers.py | 3 +- nova/compute/power_state.py | 31 -------------------- nova/db/sqlalchemy/api.py | 49 +++++++++++++++++++------------- nova/tests/api/openstack/fakes.py | 2 -- nova/tests/api/openstack/test_servers.py | 13 +++++---- 7 files changed, 75 insertions(+), 65 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index efa4ab385..bbf46261b 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -25,6 +25,7 @@ from nova import exception from nova import flags from nova import log as logging 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 +36,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. diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e3e829e81..c842fcc01 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -31,7 +31,6 @@ from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import ips from nova.api.openstack import wsgi from nova.compute import instance_types -from nova.compute import power_state from nova.scheduler import api as scheduler_api import nova.api.openstack import nova.api.openstack.views.addresses @@ -53,7 +52,7 @@ class Controller(object): self.compute_api = compute.API() self.helper = helper.CreateInstanceHelper(self) - def _check_servers_options(self, search_options): + def _check_servers_options(self, context, search_options): if FLAGS.allow_admin_api and context.is_admin: # Allow all options return @@ -106,7 +105,7 @@ class Controller(object): # child zones.. if 'status' in search_opts: status = search_opts['status'] - search_opts['state'] = power_state.states_from_status(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) @@ -122,10 +121,10 @@ class Controller(object): # 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$' + search_opts['deleted'] = False else: # This is the default, but just in case.. - search_opts['deleted'] = '^True$' + search_opts['deleted'] = True instance_list = self.compute_api.get_all( context, search_opts=search_opts) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 64be00f86..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 @@ -65,7 +64,7 @@ class ViewBuilder(object): inst_dict = { 'id': inst['id'], 'name': inst['display_name'], - 'status': power_state.status_from_state(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/compute/power_state.py b/nova/compute/power_state.py index bdedd8da8..c468fe6b3 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -48,20 +48,6 @@ _STATE_MAP = { BUILDING: 'building', } -_STATUS_MAP = { - None: 'BUILD', - NOSTATE: 'BUILD', - RUNNING: 'ACTIVE', - BLOCKED: 'ACTIVE', - SUSPENDED: 'SUSPENDED', - PAUSED: 'PAUSED', - SHUTDOWN: 'SHUTDOWN', - SHUTOFF: 'SHUTOFF', - CRASHED: 'ERROR', - FAILED: 'ERROR', - BUILDING: 'BUILD', -} - def name(code): return _STATE_MAP[code] @@ -69,20 +55,3 @@ def name(code): def valid_states(): return _STATE_MAP.keys() - - -def status_from_state(power_state): - """Map the power state to the server status string""" - return _STATUS_MAP[power_state] - - -def 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 diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 55c804ae9..f8920e62c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1153,14 +1153,14 @@ def instance_get_all_by_filters(context, filters): will be returned by default, unless there's a filter that says otherwise""" - def _filter_by_ipv6(instance, filter_re): + 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 _filter_by_ip(instance, filter_re): + 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']: @@ -1174,12 +1174,12 @@ def instance_get_all_by_filters(context, filters): return True return False - def _filter_by_display_name(instance, filter_re): + def _regexp_filter_by_display_name(instance, filter_re): if filter_re.match(instance.display_name): return True return False - def _filter_by_column(instance, filter_name, filter_re): + def _regexp_filter_by_column(instance, filter_name, filter_re): try: v = getattr(instance, filter_name) except AttributeError: @@ -1188,6 +1188,18 @@ def instance_get_all_by_filters(context, filters): 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')).\ @@ -1208,21 +1220,16 @@ def instance_get_all_by_filters(context, filters): else: filters['user_id'] = context.user_id - # Filters that we can do along with the SQL query... - query_filter_funcs = { - 'project_id': lambda query, value: query.filter_by( - project_id=value), - 'user_id': lambda query, value: query.filter_by( - user_id=value), - 'reservation_id': lambda query, value: query.filter_by( - reservation_id=value), - 'state': lambda query, value: query.filter_by(state=value)} + # 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 query_filter_funcs] + if key in exact_match_filter_names] for filter_name in query_filters: - query_prefix = query_filter_funcs[filter_name](query_prefix, + query_prefix = _exact_match_filter(query_prefix, filter_name, filters[filter_name]) # Remove this from filters, so it doesn't get tried below del filters[filter_name] @@ -1233,17 +1240,19 @@ def instance_get_all_by_filters(context, filters): return [] # Now filter on everything else for regexp matching.. - filter_funcs = {'ip6': _filter_by_ipv6, - 'ip': _filter_by_ip, - 'name': _filter_by_display_name} + # 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, + 'name': _regexp_filter_by_display_name} for filter_name in filters.iterkeys(): - filter_func = filter_funcs.get(filter_name, None) + filter_func = regexp_filter_funcs.get(filter_name, None) filter_re = re.compile(filters[filter_name]) if filter_func: filter_l = lambda instance: filter_func(instance, filter_re) else: - filter_l = lambda instance: _filter_by_column(instance, + filter_l = lambda instance: _regexp_filter_by_column(instance, filter_name, filter_re) instances = filter(filter_l, instances) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 33419d359..a67a28a4e 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -68,8 +68,6 @@ def fake_auth_init(self, application): @webob.dec.wsgify def fake_wsgi(self, req): - if 'nova.context' not in req.environ: - req.environ['nova.context'] = context.RequestContext(1, 1) return self.application diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index cc439f024..2ae45b791 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1149,6 +1149,7 @@ class ServersTest(test.TestCase): 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()) @@ -1168,6 +1169,7 @@ class ServersTest(test.TestCase): 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()) @@ -1187,6 +1189,7 @@ class ServersTest(test.TestCase): 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()) @@ -1205,6 +1208,7 @@ class ServersTest(test.TestCase): 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()) @@ -1219,8 +1223,7 @@ class ServersTest(test.TestCase): """Test getting servers by instance_name with admin_api enabled but non-admin context """ - FLAGS.allow_admin_api = True - + self.flags(allow_admin_api=True) context = nova.context.RequestContext('testuser', 'testproject', is_admin=False) req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') @@ -1234,8 +1237,6 @@ class ServersTest(test.TestCase): """Test getting servers by instance_name with admin_api enabled and admin context """ - FLAGS.allow_admin_api = True - def fake_get_all(compute_self, context, search_opts=None): self.assertNotEqual(search_opts, None) self.assertTrue('instance_name' in search_opts) @@ -1243,6 +1244,7 @@ class ServersTest(test.TestCase): return [stub_instance(100)] self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=True) req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') # Request admin context @@ -1878,6 +1880,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']): @@ -1933,7 +1936,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') -- cgit From e36232aed703eca43c6eb6df02a5c2aa0a1ac649 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 14:40:06 -0700 Subject: fix OS API tests --- nova/api/openstack/servers.py | 5 ++ nova/tests/api/openstack/fakes.py | 8 +- nova/tests/api/openstack/test_servers.py | 124 ++++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c842fcc01..e10e5bc86 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -59,9 +59,14 @@ class Controller(object): # Otherwise, strip out all unknown options unknown_options = [opt for opt in search_options if opt not in self.servers_search_options] + unk_opt_str = ", ".join(unknown_options) + log_msg = _("Stripping out options '%(unk_opt_str)s' from servers " + "query") % locals() + LOG.debug(log_msg) for opt in unknown_options: search_options.pop(opt, None) + def index(self, req): """ Returns a list of server names and ids for a given user """ try: 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_servers.py b/nova/tests/api/openstack/test_servers.py index 2ae45b791..cc2afa57c 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -240,7 +240,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) @@ -1165,7 +1166,7 @@ class ServersTest(test.TestCase): self.assertNotEqual(search_opts, None) self.assertTrue('flavor' in search_opts) # flavor is an integer ID - self.assertEqual(search_opts['flavor'], 12345) + self.assertEqual(search_opts['flavor'], '12345') return [stub_instance(100)] self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) @@ -1200,6 +1201,18 @@ class ServersTest(test.TestCase): 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) @@ -1219,39 +1232,100 @@ class ServersTest(test.TestCase): self.assertEqual(len(servers), 1) self.assertEqual(servers[0]['id'], 100) - def test_get_servers_allows_instance_name1_v1_1(self): - """Test getting servers by instance_name with admin_api - enabled but non-admin context + 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) - req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') - req.environ["nova.context"] = context - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 403) - self.assertTrue(res.body.find( - "User does not have admin privileges") > -1) + 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_allows_instance_name2_v1_1(self): - """Test getting servers by instance_name with admin_api - enabled and admin context + 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) - self.assertTrue('instance_name' in search_opts) - self.assertEqual(search_opts['instance_name'], 'whee.*') + # 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) - self.flags(allow_admin_api=True) - req = webob.Request.blank('/v1.1/servers?instance_name=whee.*') + 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) - req.environ["nova.context"] = context - res = req.get_response(fakes.wsgi_app()) + 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) @@ -1259,7 +1333,7 @@ class ServersTest(test.TestCase): self.assertEqual(len(servers), 1) self.assertEqual(servers[0]['id'], 100) - def test_get_servers_allows_ip_v1_1(self): + def test_get_servers_admin_allows_ip_v1_1(self): """Test getting servers by ip with admin_api enabled and admin context """ @@ -1277,8 +1351,7 @@ class ServersTest(test.TestCase): # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) - req.environ["nova.context"] = context - res = req.get_response(fakes.wsgi_app()) + 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) @@ -1286,7 +1359,7 @@ class ServersTest(test.TestCase): self.assertEqual(len(servers), 1) self.assertEqual(servers[0]['id'], 100) - def test_get_servers_allows_ip6_v1_1(self): + def test_get_servers_admin_allows_ip6_v1_1(self): """Test getting servers by ip6 with admin_api enabled and admin context """ @@ -1304,8 +1377,7 @@ class ServersTest(test.TestCase): # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) - req.environ["nova.context"] = context - res = req.get_response(fakes.wsgi_app()) + 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) -- cgit From 2b45204e593f9330c8b961cfae3ad5af0bd36642 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 14:47:05 -0700 Subject: doc string fix --- nova/api/openstack/servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e10e5bc86..b89b24047 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -145,7 +145,8 @@ class Controller(object): def _servers_from_request(self, req, is_detail): """Returns a list of servers based on the request. - Checks for search options and permissions on the options. + Checks for search options and strips out options that should + not be available to non-admins. """ search_opts = {} -- cgit From 04a2a64d42e6acf0addd8918acd3139dc4aff7c7 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 15:04:36 -0700 Subject: resolved conflict incorrectly from trunk merge --- nova/tests/api/openstack/test_servers.py | 193 ------------------------------- 1 file changed, 193 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 37546dab4..41bbb91f5 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2142,199 +2142,6 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 204) self.assertEqual(self.server_delete_called, True) - def test_resize_server(self): - req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) - - self.resize_called = False - - def resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - self.assertEqual(self.resize_called, True) - - def test_resize_server_v1_1(self): - req = webob.Request.blank('/v1.1/servers/1/action') - req.content_type = 'application/json' - req.method = 'POST' - body_dict = { - "resize": { - "flavorRef": 3, - }, - } - req.body = json.dumps(body_dict) - - self.resize_called = False - - def resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - self.assertEqual(self.resize_called, True) - - def test_resize_bad_flavor_data(self): - req = self.webreq('/1/action', 'POST', {"resize": "bad_data"}) - - self.resize_called = False - - def resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - self.assertEqual(self.resize_called, False) - - def test_resize_invalid_flavorid(self): - req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": 300}}) - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_resize_nonint_flavorid(self): - req = self.webreq('/1/action', 'POST', {"resize": {"flavorId": "a"}}) - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_resize_invalid_flavorid_v1_1(self): - req = webob.Request.blank('/v1.1/servers/1/action') - req.content_type = 'application/json' - req.method = 'POST' - resize_body = { - "resize": { - "image": { - "id": 300, - }, - }, - } - req.body = json.dumps(resize_body) - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_resize_nonint_flavorid_v1_1(self): - req = webob.Request.blank('/v1.1/servers/1/action') - req.content_type = 'application/json' - req.method = 'POST' - resize_body = { - "resize": { - "image": { - "id": "a", - }, - }, - } - req.body = json.dumps(resize_body) - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_resize_raises_fails(self): - req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) - - def resize_mock(*args): - raise Exception("An error occurred.") - - self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 500) - - def test_resized_server_has_correct_status(self): - req = self.webreq('/1', 'GET') - - def fake_migration_get(*args): - return {} - - self.stubs.Set(nova.db, 'migration_get_by_instance_and_status', - fake_migration_get) - res = req.get_response(fakes.wsgi_app()) - body = json.loads(res.body) - self.assertEqual(body['server']['status'], 'RESIZE-CONFIRM') - - def test_confirm_resize_server(self): - req = self.webreq('/1/action', 'POST', dict(confirmResize=None)) - - self.resize_called = False - - def confirm_resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'confirm_resize', - confirm_resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 204) - self.assertEqual(self.resize_called, True) - - def test_confirm_resize_server_fails(self): - req = self.webreq('/1/action', 'POST', dict(confirmResize=None)) - - def confirm_resize_mock(*args): - raise Exception("An error occurred.") - - self.stubs.Set(nova.compute.api.API, 'confirm_resize', - confirm_resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_revert_resize_server(self): - req = self.webreq('/1/action', 'POST', dict(revertResize=None)) - - self.resize_called = False - - def revert_resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'revert_resize', - revert_resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - self.assertEqual(self.resize_called, True) - - def test_revert_resize_server_fails(self): - req = self.webreq('/1/action', 'POST', dict(revertResize=None)) - - def revert_resize_mock(*args): - raise Exception("An error occurred.") - - self.stubs.Set(nova.compute.api.API, 'revert_resize', - revert_resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) - - def test_migrate_server(self): - """This is basically the same as resize, only we provide the `migrate` - attribute in the body's dict. - """ - req = self.webreq('/1/migrate', 'POST') - - FLAGS.allow_admin_api = True - self.resize_called = False - - def resize_mock(*args): - self.resize_called = True - - self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - self.assertEqual(self.resize_called, True) - - def test_migrate_server_no_admin_api_fails(self): - req = self.webreq('/1/migrate', 'POST') - - FLAGS.allow_admin_api = False - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - def test_shutdown_status(self): new_server = return_server_with_power_state(power_state.SHUTDOWN) self.stubs.Set(nova.db.api, 'instance_get', new_server) -- cgit From 6e791e8b773565b62c4b8ba35cec455cb8c13ac8 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 16:30:55 -0700 Subject: test fixes.. one more to go --- nova/compute/api.py | 38 ++++++++++++++++++++++++++++---------- nova/db/sqlalchemy/api.py | 8 +------- nova/tests/test_compute.py | 35 ++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 02e9f3e06..382f3c541 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -684,25 +684,43 @@ class API(base.Base): # Fixups for the DB call filters = search_opts.copy() + recurse_zones = filters.pop('recurse_zones', False) if 'image' in filters: filters['image_ref'] = filters['image'] del filters['image'] + invalid_flavor = False if 'flavor' in filters: - flavor_id = int(filters['flavor']) - try: - instance_type = self.db.instance_type_get_by_flavor_id( - context, flavor_id) - except exception.FlavorNotFound: - pass - else: - filters['instance_type_id'] = instance_type['id'] + instance_type = self.db.instance_type_get_by_flavor_id( + context, filters['flavor']) + filters['instance_type_id'] = instance_type['id'] del filters['flavor'] + # 'name' means Instance.display_name + # 'instance_name' means Instance.name + if 'name' in filters: + filters['display_name'] = filters['name'] + del filters['name'] + if 'instance_name' in filters: + filters['name'] = filters['instance_name'] + del filters['instance_name'] - recurse_zones = filters.pop('recurse_zones', False) if 'reservation_id' in filters: recurse_zones = True - instances = self.db.instance_get_all_by_filters(context, filters) + if 'fixed_ip' in search_opts: + # special cased for ec2. we end up ignoring all other + # search options. + try: + instance = self.db.instance_get_by_fixed_ip(context, + search_opts['fixed_ip']) + except exception.FloatingIpNotFound, e: + if not recurse_zones: + raise + if instance: + return [instance] + instances = [] + # fall through + else: + instances = self.db.instance_get_all_by_filters(context, filters) if not recurse_zones: return instances diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index f8920e62c..4d724dbdb 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1174,11 +1174,6 @@ def instance_get_all_by_filters(context, filters): return True return False - def _regexp_filter_by_display_name(instance, filter_re): - if filter_re.match(instance.display_name): - return True - return False - def _regexp_filter_by_column(instance, filter_name, filter_re): try: v = getattr(instance, filter_name) @@ -1243,8 +1238,7 @@ def instance_get_all_by_filters(context, filters): # 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, - 'name': _regexp_filter_by_display_name} + 'ip': _regexp_filter_by_ip} for filter_name in filters.iterkeys(): filter_func = regexp_filter_funcs.get(filter_name, None) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 6732df154..0957981ed 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -980,7 +980,7 @@ class ComputeTestCase(test.TestCase): db.instance_destroy(c, instance_id1) db.instance_destroy(c, instance_id2) - def test_get_all_by_ip_regex(self): + 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({'server_name': 'woot'}) @@ -991,20 +991,34 @@ class ComputeTestCase(test.TestCase): 'server_name': 'not-woot', 'id': 30}) + vif_ref1 = db.virtual_interface_create(c, + {'instance_id': instance_id1, + 'network_id': 1}) + vif_ref2 = db.virtual_interface_create(c, + {'instance_id': instance_id2, + 'network_id': 2}) + vif_ref3 = db.virtual_interface_create(c, + {'instance_id': instance_id3, + 'network_id': 3}) + db.fixed_ip_create(c, {'address': '1.1.1.1', - 'instance_id': instance_id1}) + 'instance_id': instance_id1, + 'virtual_interface_id': vif_ref1['id']}) db.fixed_ip_create(c, {'address': '1.1.2.1', - 'instance_id': instance_id2}) + '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}) + '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) @@ -1029,12 +1043,15 @@ class ComputeTestCase(test.TestCase): 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) - db.floating_ip_destroy(c, '10.0.0.2') - def test_get_all_by_ipv6_regex(self): + def test_get_all_by_ipv6_regexp(self): """Test searching by IPv6 address""" def fake_ipv6_get_by_instance_ref(context, instance): if instance.id == 1: @@ -1144,9 +1161,9 @@ class ComputeTestCase(test.TestCase): search_opts={'flavor': 5}) self.assertEqual(len(instances), 0) - instances = self.compute_api.get_all(c, - search_opts={'flavor': 99}) - 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}) -- cgit From d722d6f635c99a758910f24e7681753599894e70 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 17:09:36 -0700 Subject: fix ipv6 search test and add test for multiple options at once --- nova/tests/test_compute.py | 142 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 26 deletions(-) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 0957981ed..7792f5909 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -983,23 +983,26 @@ class ComputeTestCase(test.TestCase): 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({'server_name': 'woot'}) + instance_id1 = self._create_instance({'display_name': 'woot'}) instance_id2 = self._create_instance({ - 'server_name': 'woo', + 'display_name': 'woo', 'id': 20}) instance_id3 = self._create_instance({ - 'server_name': 'not-woot', + 'display_name': 'not-woot', 'id': 30}) vif_ref1 = db.virtual_interface_create(c, - {'instance_id': instance_id1, + {'address': '12:34:56:78:90:12', + 'instance_id': instance_id1, 'network_id': 1}) vif_ref2 = db.virtual_interface_create(c, - {'instance_id': instance_id2, - 'network_id': 2}) + {'address': '90:12:34:56:78:90', + 'instance_id': instance_id2, + 'network_id': 1}) vif_ref3 = db.virtual_interface_create(c, - {'instance_id': instance_id3, - 'network_id': 3}) + {'address': '34:56:78:90:12:34', + 'instance_id': instance_id3, + 'network_id': 1}) db.fixed_ip_create(c, {'address': '1.1.1.1', @@ -1053,35 +1056,41 @@ class ComputeTestCase(test.TestCase): def test_get_all_by_ipv6_regexp(self): """Test searching by IPv6 address""" - def fake_ipv6_get_by_instance_ref(context, instance): - if instance.id == 1: - return ['ffff:ffff::1'] - if instance.id == 20: - return ['dddd:dddd::1'] - if instance.id == 30: - return ['cccc:cccc::1', 'eeee:eeee::1', 'dddd:dddd::1'] - - self.stubs.Set(sqlalchemy_api, '_ipv6_get_by_instance_ref', - fake_ipv6_get_by_instance_ref) c = context.get_admin_context() - instance_id1 = self._create_instance({'server_name': 'woot'}) + instance_id1 = self._create_instance({'display_name': 'woot'}) instance_id2 = self._create_instance({ - 'server_name': 'woo', + 'display_name': 'woo', 'id': 20}) instance_id3 = self._create_instance({ - 'server_name': 'not-woot', + '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': 'ff.*'}) + search_opts={'ip6': '.*1034.*'}) self.assertEqual(len(instances), 1) self.assertEqual(instances[0].id, instance_id1) - instance_ids = [instance.id for instance in instances] - self.assertTrue(instance_id1 in instance_ids) instances = self.compute_api.get_all(c, - search_opts={'ip6': '.*::1'}) + search_opts={'ip6': '^fd00.*'}) self.assertEqual(len(instances), 3) instance_ids = [instance.id for instance in instances] self.assertTrue(instance_id1 in instance_ids) @@ -1089,12 +1098,93 @@ class ComputeTestCase(test.TestCase): self.assertTrue(instance_id3 in instance_ids) instances = self.compute_api.get_all(c, - search_opts={'ip6': '.*dd:.*'}) + 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) -- cgit From 64966cbd83cbde6a240dad4ac786fe7a6a116f2f Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 17:30:07 -0700 Subject: pep8 fixes --- nova/api/openstack/servers.py | 1 - nova/db/api.py | 1 + nova/db/sqlalchemy/api.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 63791bcd1..b54897b8a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -66,7 +66,6 @@ class Controller(object): for opt in unknown_options: search_options.pop(opt, None) - def index(self, req): """ Returns a list of server names and ids for a given user """ try: diff --git a/nova/db/api.py b/nova/db/api.py index 23fac9921..4c8f25f5d 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -495,6 +495,7 @@ 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) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4d724dbdb..36fae1be1 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1205,7 +1205,7 @@ def instance_get_all_by_filters(context, filters): options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')) - + filters = filters.copy() if not context.is_admin: -- cgit From 8aeec07c2a5f8a5f1cfb049e20caa29295496606 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 17:36:39 -0700 Subject: add comment for servers_search_options list in the OS API Controllers. --- nova/api/openstack/servers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b54897b8a..ae0b103bd 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -46,6 +46,10 @@ FLAGS = flags.FLAGS class Controller(object): """ The Server API base controller class for the OpenStack API """ + # These are a list of possible query string paramters to the + # /servers query that a user should be able to do. Specify this + # in your subclasses. When admin api is off, unknown options will + # get filtered out without error. servers_search_options = [] def __init__(self): @@ -565,6 +569,9 @@ class Controller(object): class ControllerV10(Controller): """v1.0 OpenStack API controller""" + # These are a list of possible query string paramters to the + # /servers query that a user should be able to do. When admin api + # is off, unknown options will get filtered out without error. servers_search_options = ["reservation_id", "fixed_ip", "name", "recurse_zones"] @@ -633,6 +640,9 @@ class ControllerV10(Controller): class ControllerV11(Controller): """v1.1 OpenStack API controller""" + # These are a list of possible query string paramters to the + # /servers query that a user should be able to do. When admin api + # is off, unknown options will get filtered out without error. servers_search_options = ["reservation_id", "name", "recurse_zones", "status", "image", "flavor", "changes-since"] -- cgit From 4d0125b34a4796fd6d3312a4144a0834ba318469 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 17:50:59 -0700 Subject: convert filter value to a string just in case before running re.compile --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 36fae1be1..d6e7204b4 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1242,7 +1242,7 @@ def instance_get_all_by_filters(context, filters): for filter_name in filters.iterkeys(): filter_func = regexp_filter_funcs.get(filter_name, None) - filter_re = re.compile(filters[filter_name]) + filter_re = re.compile(str(filters[filter_name])) if filter_func: filter_l = lambda instance: filter_func(instance, filter_re) else: -- cgit From fd9a761f25c6095d1ea47e09cdac503683b44bfc Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 4 Aug 2011 17:59:22 -0700 Subject: pep8 fix --- nova/api/openstack/servers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index ae0b103bd..b3776ed44 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -46,7 +46,7 @@ FLAGS = flags.FLAGS class Controller(object): """ The Server API base controller class for the OpenStack API """ - # These are a list of possible query string paramters to the + # These are a list of possible query string paramters to the # /servers query that a user should be able to do. Specify this # in your subclasses. When admin api is off, unknown options will # get filtered out without error. @@ -569,7 +569,7 @@ class Controller(object): class ControllerV10(Controller): """v1.0 OpenStack API controller""" - # These are a list of possible query string paramters to the + # These are a list of possible query string paramters to the # /servers query that a user should be able to do. When admin api # is off, unknown options will get filtered out without error. servers_search_options = ["reservation_id", "fixed_ip", @@ -640,7 +640,7 @@ class ControllerV10(Controller): class ControllerV11(Controller): """v1.1 OpenStack API controller""" - # These are a list of possible query string paramters to the + # These are a list of possible query string paramters to the # /servers query that a user should be able to do. When admin api # is off, unknown options will get filtered out without error. servers_search_options = ["reservation_id", "name", "recurse_zones", -- cgit From b2fe710c59ba266b9afd67db1cae60a6db5c71e3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 5 Aug 2011 15:06:07 -0700 Subject: rename _check_servers_options, add some comments and small cleanup in the db get_by_filters call --- nova/api/openstack/servers.py | 4 ++-- nova/db/sqlalchemy/api.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b3776ed44..9a3872113 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -56,7 +56,7 @@ class Controller(object): self.compute_api = compute.API() self.helper = helper.CreateInstanceHelper(self) - def _check_servers_options(self, context, search_options): + def _remove_invalid_options(self, context, search_options): if FLAGS.allow_admin_api and context.is_admin: # Allow all options return @@ -156,7 +156,7 @@ class Controller(object): search_opts.update(req.str_GET) context = req.environ['nova.context'] - self._check_servers_options(context, search_opts) + self._remove_invalid_options(context, search_opts) # Convert recurse_zones into a boolean search_opts['recurse_zones'] = utils.bool_from_str( diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index d6e7204b4..65a1c19a1 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1206,6 +1206,8 @@ def instance_get_all_by_filters(context, filters): 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: @@ -1224,10 +1226,10 @@ def instance_get_all_by_filters(context, filters): 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[filter_name]) - # Remove this from filters, so it doesn't get tried below - del filters[filter_name] + filters.pop(filter_name)) instances = query_prefix.all() -- cgit From ca152762aa73a93583be2ada237cf8bbbcc99220 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 6 Jul 2011 05:41:47 -0700 Subject: clean up OS API servers getting --- nova/api/openstack/servers.py | 94 +++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 57 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 9a3872113..ce04a1eab 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -46,34 +46,14 @@ FLAGS = flags.FLAGS class Controller(object): """ The Server API base controller class for the OpenStack API """ - # These are a list of possible query string paramters to the - # /servers query that a user should be able to do. Specify this - # in your subclasses. When admin api is off, unknown options will - # get filtered out without error. - servers_search_options = [] - def __init__(self): self.compute_api = compute.API() self.helper = helper.CreateInstanceHelper(self) - def _remove_invalid_options(self, context, search_options): - 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 self.servers_search_options] - unk_opt_str = ", ".join(unknown_options) - log_msg = _("Stripping out options '%(unk_opt_str)s' from servers " - "query") % locals() - LOG.debug(log_msg) - for opt in unknown_options: - search_options.pop(opt, None) - def index(self, req): """ Returns a list of server names and ids for a given user """ try: - servers = self._servers_from_request(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: @@ -83,7 +63,7 @@ class Controller(object): def detail(self, req): """ Returns a list of server details for a given user """ try: - servers = self._servers_from_request(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: @@ -99,13 +79,21 @@ class Controller(object): def _action_rebuild(self, info, request, instance_id): raise NotImplementedError() - def _servers_search(self, context, req, is_detail, search_opts=None): + def _get_servers(self, req, is_detail): """Returns a list of servers, taking into account any search options specified. """ - if search_opts is None: - search_opts = {} + 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. @@ -145,26 +133,6 @@ class Controller(object): for inst in limited_list] return dict(servers=servers) - def _servers_from_request(self, req, is_detail): - """Returns a list of servers based on the request. - - Checks for search options and strips out options that should - not be available to non-admins. - """ - - search_opts = {} - search_opts.update(req.str_GET) - - context = req.environ['nova.context'] - self._remove_invalid_options(context, search_opts) - - # Convert recurse_zones into a boolean - search_opts['recurse_zones'] = utils.bool_from_str( - search_opts.get('recurse_zones', False)) - - return self._servers_search(context, req, is_detail, - search_opts=search_opts) - @scheduler_api.redirect_handler def show(self, req, id): """ Returns server details by server id """ @@ -569,12 +537,6 @@ class Controller(object): class ControllerV10(Controller): """v1.0 OpenStack API controller""" - # These are a list of possible query string paramters to the - # /servers query that a user should be able to do. When admin api - # is off, unknown options will get filtered out without error. - servers_search_options = ["reservation_id", "fixed_ip", - "name", "recurse_zones"] - @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ @@ -636,16 +598,14 @@ 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""" - # These are a list of possible query string paramters to the - # /servers query that a user should be able to do. When admin api - # is off, unknown options will get filtered out without error. - servers_search_options = ["reservation_id", "name", "recurse_zones", - "status", "image", "flavor", "changes-since"] - @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ @@ -812,6 +772,11 @@ 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): @@ -982,3 +947,18 @@ def create_resource(version='1.0'): 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) -- cgit From 1c90eb34085dbb69f37e2f63dea7496afabb06b3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 6 Jul 2011 06:20:38 -0700 Subject: clean up compute_api.get_all filter name remappings. ditch fixed_ip one-off code. fixed ec2 api call to this to compensate --- nova/api/ec2/cloud.py | 2 +- nova/compute/api.py | 75 +++++++++++++++++++++++----------------------- nova/tests/test_compute.py | 27 ++++++++++++----- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 966c3a564..db49baffa 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -242,7 +242,7 @@ class CloudController(object): search_opts=search_opts) except exception.NotFound: instance_ref = None - if instance_ref is None: + if not instance_ref: return None # This ensures that all attributes of the instance diff --git a/nova/compute/api.py b/nova/compute/api.py index 382f3c541..7a3ff0c56 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -683,44 +683,49 @@ class API(base.Base): LOG.debug(_("Searching by: %s") % str(search_opts)) # Fixups for the DB call - filters = search_opts.copy() - recurse_zones = filters.pop('recurse_zones', False) - if 'image' in filters: - filters['image_ref'] = filters['image'] - del filters['image'] - invalid_flavor = False - if 'flavor' in filters: + filters = {} + + def _remap_flavor_filter(flavor_id): instance_type = self.db.instance_type_get_by_flavor_id( - context, filters['flavor']) + context, flavor_id) filters['instance_type_id'] = instance_type['id'] - del filters['flavor'] - # 'name' means Instance.display_name - # 'instance_name' means Instance.name - if 'name' in filters: - filters['display_name'] = filters['name'] - del filters['name'] - if 'instance_name' in filters: - filters['name'] = filters['instance_name'] - del filters['instance_name'] + 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: + remap_object = filter_mapping[opt] + except KeyError: + filters[opt] = value + else: + 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 'fixed_ip' in search_opts: - # special cased for ec2. we end up ignoring all other - # search options. - try: - instance = self.db.instance_get_by_fixed_ip(context, - search_opts['fixed_ip']) - except exception.FloatingIpNotFound, e: - if not recurse_zones: - raise - if instance: - return [instance] - instances = [] - # fall through - else: - instances = self.db.instance_get_all_by_filters(context, filters) + instances = self.db.instance_get_all_by_filters(context, filters) if not recurse_zones: return instances @@ -743,12 +748,6 @@ class API(base.Base): server._info['_is_precooked'] = True instances.append(server._info) - # fixed_ip searching should return a FixedIpNotFound exception - # when an instance is not found... - fixed_ip = search_opts.get('fixed_ip', None) - if fixed_ip and not instances: - raise exception.FixedIpNotFoundForAddress(address=fixed_ip) - return instances def _cast_compute_message(self, method, context, instance_id, host=None, diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 7792f5909..18ec08597 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -949,23 +949,32 @@ class ComputeTestCase(test.TestCase): 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}) + 'instance_id': instance_id1, + 'virtual_interface_id': vif_ref1['id']}) db.fixed_ip_create(c, {'address': '1.1.2.1', - 'instance_id': instance_id2}) + 'instance_id': instance_id2, + 'virtual_interface_id': vif_ref2['id']}) # regex not allowed - self.assertRaises(exception.NotFound, - self.compute_api.get_all, - c, + instances = self.compute_api.get_all(c, search_opts={'fixed_ip': '.*'}) + self.assertEqual(len(instances), 0) - self.assertRaises(exception.NotFound, - self.compute_api.get_all, - c, + 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'}) @@ -977,6 +986,8 @@ class ComputeTestCase(test.TestCase): 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) -- cgit From b19dbcf21865aa0d1b422aecdb7ff13571ecb4e8 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 6 Jul 2011 06:37:28 -0700 Subject: fix metadata test since fixed_ip searching now goes thru filters db api call instead of the get_by_fixed_ip call --- nova/tests/test_metadata.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index b9b14d1ea..d63394ad6 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -43,16 +43,20 @@ class MetadataTestCase(test.TestCase): 'reservation_id': 'r-xxxxxxxx', 'user_data': '', 'image_ref': 7, + 'fixed_ips': [], '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, 'instance_get_by_fixed_ip', 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() -- cgit From 65fcbc8cf51cc02071d1d9cd60cf0eb59c2bcce0 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 6 Jul 2011 06:44:50 -0700 Subject: merge code i'd split from instance_get_fixed_addresses_v6 that's no longer needed to be split --- nova/db/sqlalchemy/api.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 65a1c19a1..503c526f0 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1395,33 +1395,29 @@ def instance_get_fixed_addresses(context, instance_id): return [fixed_ip.address for fixed_ip in fixed_ips] -def _ipv6_get_by_instance_ref(context, instance_ref): - # assume instance has 1 mac for each network associated with it - # get networks associated with instance - 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)] - # 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 - macs = [vif_ref['address'] for vif_ref in - sorted(vif_refs, key=lambda vif_ref: vif_ref['network_id'])] - # get project id from instance - project_id = instance_ref.project_id - # combine prefixes, macs, and project_id into (prefix,mac,p_id) tuples - prefix_mac_tuples = zip(prefixes, macs, [project_id for m in macs]) - # return list containing ipv6 address for each tuple - return [ipv6.to_global(*t) for t in prefix_mac_tuples] - - @require_context def instance_get_fixed_addresses_v6(context, instance_id): session = get_session() with session.begin(): # get instance instance_ref = instance_get(context, instance_id, session=session) - return _ipv6_get_by_instance_ref(context, instance_ref) + # assume instance has 1 mac for each network associated with it + # get networks associated with instance + 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)] + # 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 + macs = [vif_ref['address'] for vif_ref in + sorted(vif_refs, key=lambda vif_ref: vif_ref['network_id'])] + # get project id from instance + project_id = instance_ref.project_id + # combine prefixes, macs, and project_id into (prefix,mac,p_id) tuples + prefix_mac_tuples = zip(prefixes, macs, [project_id for m in macs]) + # return list containing ipv6 address for each tuple + return [ipv6.to_global(*t) for t in prefix_mac_tuples] @require_context -- cgit From ace9aa5d91d839f66998c39a977857b7a7c466a4 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Wed, 6 Jul 2011 08:25:28 -0700 Subject: wrap list comparison in test with set()s --- nova/tests/api/openstack/test_servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 2f466f561..cfb1f9382 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1170,8 +1170,8 @@ class ServersTest(test.TestCase): def fake_get_all(compute_self, context, search_opts=None): self.assertNotEqual(search_opts, None) self.assertTrue('state' in search_opts) - self.assertEqual(search_opts['state'], - [power_state.RUNNING, power_state.BLOCKED]) + 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) -- cgit From 6b83e1cd31f5e138af20fbd5c118d55da092eb35 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 7 Jul 2011 15:24:12 +0000 Subject: Added API and supporting code for rebooting or shutting down XenServer hosts. --- nova/api/openstack/contrib/hosts.py | 26 ++++++++++++++++++ nova/compute/api.py | 6 ++++ nova/compute/manager.py | 6 ++++ nova/tests/test_hosts.py | 32 +++++++++++++++++++--- nova/virt/driver.py | 4 +++ nova/virt/fake.py | 4 +++ nova/virt/hyperv.py | 4 +++ nova/virt/libvirt/connection.py | 4 +++ nova/virt/vmwareapi_conn.py | 4 +++ nova/virt/xenapi/vmops.py | 21 ++++++++++++-- nova/virt/xenapi_conn.py | 4 +++ .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 30 +++++++++++++++++++- 12 files changed, 137 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 55e57e1a4..cc71cadbd 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -78,6 +78,12 @@ class HostController(object): else: explanation = _("Invalid status: '%s'") % raw_val raise webob.exc.HTTPBadRequest(explanation=explanation) + elif key == "power_state": + if val in ("reboot", "off", "on"): + return self._set_power_state(req, id, val) + else: + explanation = _("Invalid status: '%s'") % raw_val + raise webob.exc.HTTPBadRequest(explanation=explanation) else: explanation = _("Invalid update setting: '%s'") % raw_key raise webob.exc.HTTPBadRequest(explanation=explanation) @@ -89,8 +95,28 @@ 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 _set_power_state(self, req, host, power_state): + """Turns the specified host on/off, or reboots the host.""" + context = req.environ['nova.context'] + if power_state == "on": + raise webob.exc.HTTPNotImplemented() + if power_state == "reboot": + msg = _("Rebooting host %(host)s") + else: + msg = _("Powering off host %(host)s.") + LOG.audit(msg % locals()) + result = self.compute_api.set_power_state(context, host=host, + power_state=power_state) + if result != power_state: + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) + return {"host": host, "power_state": result} + class Hosts(extensions.ExtensionDescriptor): def get_name(self): diff --git a/nova/compute/api.py b/nova/compute/api.py index b0eedcd64..71e11c5ea 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -917,6 +917,12 @@ class API(base.Base): return self._call_compute_message("set_host_enabled", context, instance_id=None, host=host, params={"enabled": enabled}) + def set_power_state(self, context, host, power_state): + """Turns the specified host on/off, or reboots the host.""" + return self._call_compute_message("set_power_state", context, + instance_id=None, host=host, + params={"power_state": power_state}) + @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for the given instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 91a604934..eb8e4df3c 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -880,6 +880,12 @@ class ComputeManager(manager.SchedulerDependentManager): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) + @exception.wrap_exception + def set_power_state(self, context, instance_id=None, host=None, + power_state=None): + """Turns the specified host on/off, or reboots the host.""" + return self.driver.set_power_state(host, power_state) + @exception.wrap_exception def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for an instance on this host.""" diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 548f81f8b..5a52e36e2 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -48,6 +48,16 @@ def stub_set_host_enabled(context, host, enabled): return status +def stub_set_power_state(context, host, power_state): + # We'll simulate success and failure by assuming + # that 'host_c1' always succeeds, and 'host_c2' + # always fails + if host == "host_c1": + return power_state + else: + return "fail" + + class FakeRequest(object): environ = {"nova.context": context.get_admin_context()} @@ -62,6 +72,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, 'set_power_state', + stub_set_power_state) def test_list_hosts(self): """Verify that the compute hosts are returned.""" @@ -87,15 +99,27 @@ class HostTestCase(test.TestCase): result_c2 = self.controller.update(self.req, "host_c2", body=en_body) self.assertEqual(result_c2["status"], "disabled") + def test_power_state(self): + en_body = {"power_state": "reboot"} + result_c1 = self.controller.update(self.req, "host_c1", body=en_body) + self.assertEqual(result_c1["power_state"], "reboot") + result_c2 = self.controller.update(self.req, "host_c2", body=en_body) + self.assertEqual(result_c2["power_state"], "fail") + + def test_bad_power_state_value(self): + bad_body = {"power_state": "bad"} + result = self.controller.update(self.req, "host_c1", body=bad_body) + self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") + def test_bad_status_value(self): bad_body = {"status": "bad"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=bad_body) + result = self.controller.update(self.req, "host_c1", body=bad_body) + self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") def test_bad_update_key(self): bad_body = {"crazy": "bad"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=bad_body) + result = self.controller.update(self.req, "host_c1", body=bad_body) + self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") def test_bad_host(self): self.assertRaises(exception.HostNotFound, self.controller.update, diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 3c4a073bf..eed32d8d6 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -253,3 +253,7 @@ class ComputeDriver(object): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" raise NotImplementedError() + + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index ea0a59f21..0596079e8 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -518,3 +518,7 @@ class FakeConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass + + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 5c1dc772d..a438ff2e8 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -503,3 +503,7 @@ class HyperVConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass + + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index b80a3daee..2a02e5a2d 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1595,3 +1595,7 @@ class LibvirtConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass + + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + pass diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index d80e14931..0136225dd 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -194,6 +194,10 @@ class VMWareESXConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" pass + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + pass + class VMWareAPISession(object): """ diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index cb96930c1..ec90ba9fe 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -936,10 +936,25 @@ class VMOps(object): 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 set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + args = {"power_state": power_state} + xenapi_resp = self._call_xenhost("set_power_state", args) + try: + resp = json.loads(xenapi_resp) + except TypeError as e: + # Already logged; return the message + return xenapi_resp.details[-1] + return resp["power_state"] + def _call_xenhost(self, method, arg_dict): """There will be several methods that will need this general handling for interacting with the xenhost plugin, so this abstracts @@ -953,7 +968,7 @@ class VMOps(object): #args={"params": arg_dict}) ret = self._session.wait_for_task(task, task_id) except self.XenAPI.Failure as e: - ret = None + ret = e LOG.error(_("The call to %(method)s returned an error: %(e)s.") % locals()) return ret diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index ec8c44c1c..0b88e0999 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -340,6 +340,10 @@ class XenAPIConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" return self._vmops.set_host_enabled(host, enabled) + def set_power_state(self, host, power_state): + """Reboots, shuts down or starts up the host.""" + return self._vmops.set_power_state(host, power_state) + class XenAPISession(object): """The session to invoke XenAPI SDK calls""" diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 292bbce12..0cf7de0ce 100644 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -103,6 +103,33 @@ def set_host_enabled(self, arg_dict): return {"status": status} +@jsonify +def set_power_state(self, arg_dict): + """Reboots or powers off this host. Ideally, we would also like to be + able to power *on* a host, but right now this is not technically + feasible. + """ + power_state = arg_dict.get("power_state") + if power_state is None: + raise pluginlib.PluginError( + _("Missing 'power_state' argument to set_power_state")) + # 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", "on": "xe host-power-on", +# "off": "xe host-shutdown"} +# result = _run_command(cmds[power_state]) +# # Should be empty string +# if result: +# raise pluginlib.PluginError(result) + return {"power_state": power_state} + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -217,4 +244,5 @@ def cleanup(dct): if __name__ == "__main__": XenAPIPlugin.dispatch( {"host_data": host_data, - "set_host_enabled": set_host_enabled}) + "set_host_enabled": set_host_enabled, + "set_power_state": set_power_state}) -- cgit From 60a9763382ccd77735a75b6047c821477eab684e Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 7 Jul 2011 15:36:39 +0000 Subject: pep8 fixes --- nova/tests/test_hosts.py | 16 ++++++++-------- nova/virt/xenapi/vmops.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 5a52e36e2..417737638 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -103,23 +103,23 @@ class HostTestCase(test.TestCase): en_body = {"power_state": "reboot"} result_c1 = self.controller.update(self.req, "host_c1", body=en_body) self.assertEqual(result_c1["power_state"], "reboot") - result_c2 = self.controller.update(self.req, "host_c2", body=en_body) - self.assertEqual(result_c2["power_state"], "fail") + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c2", body=en_body) def test_bad_power_state_value(self): bad_body = {"power_state": "bad"} - result = self.controller.update(self.req, "host_c1", body=bad_body) - self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) def test_bad_status_value(self): bad_body = {"status": "bad"} - result = self.controller.update(self.req, "host_c1", body=bad_body) - self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) def test_bad_update_key(self): bad_body = {"crazy": "bad"} - result = self.controller.update(self.req, "host_c1", body=bad_body) - self.assertEqual(str(result.wrapped_exc)[:15], "400 Bad Request") + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) def test_bad_host(self): self.assertRaises(exception.HostNotFound, self.controller.update, diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index ec90ba9fe..aec802eff 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -941,7 +941,7 @@ class VMOps(object): resp = json.loads(xenapi_resp) except TypeError as e: # Already logged; return the message - return xenapi_resp.details[-1] + return xenapi_resp.details[-1] return resp["status"] def set_power_state(self, host, power_state): @@ -952,7 +952,7 @@ class VMOps(object): resp = json.loads(xenapi_resp) except TypeError as e: # Already logged; return the message - return xenapi_resp.details[-1] + return xenapi_resp.details[-1] return resp["power_state"] def _call_xenhost(self, method, arg_dict): -- cgit From 4001ee488420589e345dc42001e6cab9c68a5e12 Mon Sep 17 00:00:00 2001 From: Kei Masumoto Date: Mon, 11 Jul 2011 16:31:31 +0900 Subject: fix comments --- nova/compute/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 7d89dde80..1fbbe0065 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1341,10 +1341,9 @@ class ComputeManager(manager.SchedulerDependentManager): 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 block_migration: block_migration - :param xml: libvirt.xml """ instance_ref = self.db.instance_get(context, instance_id) -- cgit From 40a9488b4b96fa809bd18f4a06018186a488507a Mon Sep 17 00:00:00 2001 From: Ilya Alekseyev Date: Thu, 14 Jul 2011 22:34:49 +0400 Subject: added commands --- bin/nova-manage | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index 7dfe91698..5a934b613 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -518,6 +518,27 @@ class FixedIpCommands(object): fixed_ip['address'], mac_address, hostname, host) + def reserve(self, address): + """Mark fixed ip as reserved + arguments: address""" + self._set_reserved(address, True) + + def waste(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.""" -- cgit From 718d4cf5cd4122bcecf0974c441d098f57a124b0 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Sun, 17 Jul 2011 22:49:22 +0100 Subject: Initial test case proving we have a bug of, ec2 security group name can exceed 255 chars. --- nova/tests/test_api.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20b20fcbf..63f040ffd 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -293,6 +293,26 @@ class ApiEc2TestCase(test.TestCase): self.manager.delete_project(project) self.manager.delete_user(user) + def test_group_name_valid_security_group(self): + """Test that we sanely handle invalid security group names. """ + self.expect_http() + self.mox.ReplayAll() + user = self.manager.create_user('fake', 'fake', 'fake', admin=True) + project = self.manager.create_project('fake', 'fake', 'fake') + + # At the moment, you need both of these to actually be netadmin + self.manager.add_role('fake', 'netadmin') + project.add_role('fake', 'netadmin') + + security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") + for x in range(random.randint(256, 266))) + try: + self.ec2.create_security_group(security_group_name, 'test group') + except: + pass + else: + self.fail('Exception not raised.') + def test_authorize_revoke_security_group_cidr(self): """ Test that we can add and remove CIDR based rules -- cgit From 5c6e4aa80672966ad4449007feea970cd62dee10 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Sun, 17 Jul 2011 23:52:50 +0100 Subject: Some basic validation for creating ec2 security groups. (LP: #715443) --- nova/api/ec2/__init__.py | 4 ++++ nova/api/ec2/cloud.py | 17 +++++++++++++++++ nova/exception.py | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 890d57fe7..027e35933 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -349,6 +349,10 @@ 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 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 acfd1361c..3ef64afa7 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -28,6 +28,7 @@ import os import urllib import tempfile import shutil +import re from nova import compute from nova import context @@ -602,6 +603,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_\- ]+$',group_name): + # Some validation to ensure that values match API spec. + # - Alphanumeric characters, spaces, dashes, and underscores. + # TODO(Daviey): extend beyond group_name checking, and probably + # create a param validator function 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): diff --git a/nova/exception.py b/nova/exception.py index ad6c005f8..8771328d8 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -196,6 +196,10 @@ class InvalidIpProtocol(Invalid): class InvalidContentType(Invalid): message = _("Invalid content type %(content_type)s.") +class InvalidParameterValue(Invalid): + # Cannot be templated as the error syntax varies. + # msg needs to be constructed when raised. + message = _("%(err)s") class InstanceNotRunning(Invalid): message = _("Instance %(instance_id)s is not running.") -- cgit From 64a03d48bd714672a3d68136d365bf941201affa Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Mon, 18 Jul 2011 00:06:48 +0100 Subject: Extended test to check for error specific error code and test cover for bad chars. --- nova/tests/test_api.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 63f040ffd..de399d76e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -304,12 +304,32 @@ class ApiEc2TestCase(test.TestCase): self.manager.add_role('fake', 'netadmin') project.add_role('fake', 'netadmin') + # Test block group_name of non alphanumeric characters, spaces, + # dashes, and underscores. + security_group_name = "aa #$% -=99" + + try: + self.ec2.create_security_group(security_group_name, 'test group') + except EC2ResponseError, e: + if e.code == 'InvalidParameterValue': + pass + else: + self.fail("Unexpected EC2ResponseError: %s " + "(expected InvalidParameterValue)" % e.code) + else: + self.fail('Exception not raised.') + + # Test block group_name > 255 chars security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") for x in range(random.randint(256, 266))) try: self.ec2.create_security_group(security_group_name, 'test group') - except: - pass + except EC2ResponseError, e: + if e.code == 'InvalidParameterValue': + pass + else: + self.fail("Unexpected EC2ResponseError: %s " + "(expected InvalidParameterValue)" % e.code) else: self.fail('Exception not raised.') -- cgit From 9d0b441939ab5a9227e91bb868f499d700c7c907 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Mon, 18 Jul 2011 00:16:53 +0100 Subject: pep8'd --- nova/api/ec2/cloud.py | 6 +++--- nova/exception.py | 6 ++++-- nova/tests/test_api.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 3ef64afa7..8d7aa9953 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -603,11 +603,11 @@ 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_\- ]+$',group_name): + if not re.match('^[a-zA-Z0-9_\- ]+$', group_name): # Some validation to ensure that values match API spec. # - Alphanumeric characters, spaces, dashes, and underscores. - # TODO(Daviey): extend beyond group_name checking, and probably - # create a param validator function that can be used elsewhere. + # TODO(Daviey): extend beyond group_name checking, and probably + # create a param validator function that can be used elsewhere. err = _("Value (%s) for parameter GroupName is invalid." " Content limited to Alphanumeric characters, " "spaces, dashes, and underscores.") % group_name diff --git a/nova/exception.py b/nova/exception.py index 8771328d8..8f3cf0af6 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -196,11 +196,13 @@ class InvalidIpProtocol(Invalid): class InvalidContentType(Invalid): message = _("Invalid content type %(content_type)s.") + +# Cannot be templated as the error syntax varies. +# msg needs to be constructed when raised. class InvalidParameterValue(Invalid): - # Cannot be templated as the error syntax varies. - # msg needs to be constructed when raised. message = _("%(err)s") + class InstanceNotRunning(Invalid): message = _("Instance %(instance_id)s is not running.") diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index de399d76e..6e4f2c95e 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -304,8 +304,8 @@ class ApiEc2TestCase(test.TestCase): self.manager.add_role('fake', 'netadmin') project.add_role('fake', 'netadmin') - # Test block group_name of non alphanumeric characters, spaces, - # dashes, and underscores. + # Test block group_name of non alphanumeric characters, spaces, + # dashes, and underscores. security_group_name = "aa #$% -=99" try: -- cgit From beb2337f002178b7e764f3a6dcbab4637321aa34 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Mon, 18 Jul 2011 00:22:01 +0100 Subject: nova/api/ec2/cloud.py: Rearranged imports to be alphabetical as per HACKING. --- nova/api/ec2/cloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 8d7aa9953..9de5b58f5 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -25,10 +25,10 @@ datastore. import base64 import netaddr import os -import urllib -import tempfile -import shutil import re +import shutil +import tempfile +import urllib from nova import compute from nova import context -- cgit From e68d53df98890f424e361c7c79a5b2cd62723963 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Mon, 18 Jul 2011 00:41:51 +0100 Subject: convert group_name to string, incase it's a long --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9de5b58f5..e4b008b85 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -603,7 +603,7 @@ 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_\- ]+$', group_name): + 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): extend beyond group_name checking, and probably -- cgit From 5e9e62c2382f29a55b9b0c7a2b4aefc16b9d623d Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Wed, 20 Jul 2011 20:11:47 +0100 Subject: Split tests into 2 --- nova/tests/test_api.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 48a43a46b..5759e7726 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -363,8 +363,10 @@ class ApiEc2TestCase(test.TestCase): self.manager.delete_project(project) self.manager.delete_user(user) - def test_group_name_valid_security_group(self): - """Test that we sanely handle invalid security group names. """ + 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() user = self.manager.create_user('fake', 'fake', 'fake', admin=True) @@ -376,7 +378,7 @@ class ApiEc2TestCase(test.TestCase): # Test block group_name of non alphanumeric characters, spaces, # dashes, and underscores. - security_group_name = "aa #$% -=99" + security_group_name = "aa #^% -=99" try: self.ec2.create_security_group(security_group_name, 'test group') @@ -389,6 +391,18 @@ class ApiEc2TestCase(test.TestCase): else: self.fail('Exception not raised.') + 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() + user = self.manager.create_user('fake', 'fake', 'fake', admin=True) + project = self.manager.create_project('fake', 'fake', 'fake') + + # At the moment, you need both of these to actually be netadmin + self.manager.add_role('fake', 'netadmin') + project.add_role('fake', 'netadmin') + # Test block group_name > 255 chars security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") for x in range(random.randint(256, 266))) -- cgit From 25bd75bfd2c72899bf139e671fd42fd2dc1dc0e1 Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Wed, 20 Jul 2011 20:12:19 +0100 Subject: Added LP bug num to TODO --- nova/api/ec2/cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index cd493e3e7..ecdbad895 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -695,8 +695,8 @@ class CloudController(object): 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): extend beyond group_name checking, and probably - # create a param validator function that can be used elsewhere. + # 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 -- cgit From c3cdcc1eb0c9fd37f49701d976c7ceae8df44caf Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 22 Jul 2011 22:41:29 +0200 Subject: This is me being all cocky, thinking I'll make it use ipsets... --- nova/compute/api.py | 18 ++++++++------- nova/db/sqlalchemy/models.py | 6 +++++ nova/network/linux_net.py | 30 +++++++++++++++++++++++++ nova/network/manager.py | 24 +++++++++++++++++++- nova/tests/test_iptables_network.py | 39 ++++++++++++++++++++++++++++++-- nova/virt/libvirt/firewall.py | 44 +++++++++++++++++++++++++++---------- 6 files changed, 139 insertions(+), 22 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 432658bbb..65a594d2c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -305,10 +305,6 @@ 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, @@ -464,19 +460,22 @@ 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)) + LOG.info('rules: %r', security_group_rules) # ..then we distill the security groups to which they belong.. security_groups = set() for rule in security_group_rules: @@ -485,12 +484,14 @@ class API(base.Base): rule['parent_group_id']) security_groups.add(security_group) + LOG.info('security_groups: %r', security_groups) # ..then we find the instances that are members of these groups.. instances = set() for security_group in security_groups: for instance in security_group['instances']: instances.add(instance) + LOG.info('instances: %r', instances) # ...then we find the hosts where they live... hosts = set() for instance in instances: @@ -500,6 +501,7 @@ class API(base.Base): # ...and finally we tell these nodes to refresh their view of this # particular security group. for host in hosts: + LOG.info('host: %r', host) rpc.cast(context, self.db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "refresh_security_group_members", diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index d29d3d6f1..023821dfb 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -491,6 +491,12 @@ 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/network/linux_net.py b/nova/network/linux_net.py index 283a5aca1..0e021a40f 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -96,6 +96,33 @@ class IptablesRule(object): chain = self.chain return '-A %s %s' % (chain, self.rule) +class IpSet(object): + """A class for handling large collections of IPs efficiently""" + + def __init__(self, name, execute=None): + self.name = name + self._ips = set() + if not execute: + self.execute = _execute + else: + self.execute = execute + + def __contains__(self, addr): + return addr in self._ips + + def _set_name(self): + return '%s-%s' % (binary_name, self.name) + + def add_ip(self, addr): + self._ips.add(addr) + self.execute('ipset', '-A', self._set_name(), addr) + + def remove_ip(self, addr): + self._ips.remove(addr) + self.execute('ipset', '-D', self._set_name(), addr) + + def iptables_source_match(self): + return ['-m set --match-set %s src' % (self._set_name(),)] class IptablesTable(object): """An iptables table.""" @@ -281,6 +308,9 @@ class IptablesManager(object): self.ipv4['nat'].add_chain('floating-snat') self.ipv4['nat'].add_rule('snat', '-j $floating-snat') + def ipset_supported(self): + return False + @utils.synchronized('iptables', external=True) def apply(self): """Apply the current in-memory set of iptables rules. diff --git a/nova/network/manager.py b/nova/network/manager.py index 824e8d24d..928cb09f6 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -63,6 +63,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 @@ -297,6 +298,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) @@ -350,6 +352,15 @@ class NetworkManager(manager.SchedulerDependentManager): # return so worker will only grab 1 (to help scale flatter) return self.set_network_host(context, network['id']) + def _do_trigger_security_group_members_refresh_for_instance(self, + context, + instance_id): + instance_ref = db.instance_get(context, instance_id) + groups = instance_ref.security_groups + group_ids = [group.id for group in groups] + self.compute_api.trigger_security_group_members_refresh(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 @@ -511,6 +522,9 @@ 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( + context, + instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, instance_id, network['id']) @@ -524,6 +538,12 @@ 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( + context, + instance_id) def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" @@ -825,7 +845,9 @@ 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( + context, + instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, instance_id, network['id']) diff --git a/nova/tests/test_iptables_network.py b/nova/tests/test_iptables_network.py index 918034269..d0a8c052c 100644 --- a/nova/tests/test_iptables_network.py +++ b/nova/tests/test_iptables_network.py @@ -17,11 +17,46 @@ # under the License. """Unit Tests for network code.""" -import os - from nova import test from nova.network import linux_net +class IpSetTestCase(test.TestCase): + def test_add(self): + """Adding an address""" + ipset = linux_net.IpSet('somename') + + ipset.add_ip('1.2.3.4') + self.assertTrue('1.2.3.4' in ipset) + + + def test_add_remove(self): + """Adding and then removing an address""" + + self.verify_cmd_call_count = 0 + def verify_cmd(*args): + self.assertEquals(args, self.expected_cmd) + self.verify_cmd_call_count += 1 + + self.expected_cmd = ('ipset', '-A', 'run_tests.py-somename', '1.2.3.4') + ipset = linux_net.IpSet('somename',execute=verify_cmd) + ipset.add_ip('1.2.3.4') + self.assertTrue('1.2.3.4' in ipset) + + self.expected_cmd = ('ipset', '-D', 'run_tests.py-somename', '1.2.3.4') + ipset.remove_ip('1.2.3.4') + self.assertTrue('1.2.3.4' not in ipset) + self.assertEquals(self.verify_cmd_call_count, 2) + + + def test_two_adds_one_remove(self): + """Adding the same address twice works. Removing it once removes it entirely.""" + ipset = linux_net.IpSet('somename') + + ipset.add_ip('1.2.3.4') + ipset.add_ip('1.2.3.4') + ipset.remove_ip('1.2.3.4') + self.assertTrue('1.2.3.4' not in ipset) + class IptablesManagerTestCase(test.TestCase): sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011', diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index 379197398..aa36e4184 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -663,11 +663,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: @@ -677,16 +676,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 @@ -705,9 +704,30 @@ 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: + LOG.info('Not using cidr %r', rule.cidr) + if self.iptables.ipset_supported(): + LOG.info('ipset supported %r', rule.cidr) + ipset = linux_net.IpSet('%s' % rule.group_id) + args += ipset.iptables_source_match() + fw_rules += [' '.join(args)] + else: + LOG.info('ipset unsupported %r', rule.cidr) + LOG.info('rule.grantee_group.instances: %r', rule.grantee_group.instances) + 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'] @@ -718,7 +738,9 @@ class IptablesFirewallDriver(FirewallDriver): return self.nwfilter.instance_filter_exists(instance) def refresh_security_group_members(self, security_group): - pass + if not self.iptables.ipset_supported(): + 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) -- cgit From 00fcb54769fdbe8828d7bd52a6636ffc5ad6c862 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 22 Jul 2011 22:49:16 +0200 Subject: ...and this is me snapping back into reality removing all trace of ipsets. Go me. --- nova/network/linux_net.py | 30 ---------------------------- nova/tests/test_iptables_network.py | 39 ++----------------------------------- nova/virt/libvirt/firewall.py | 30 ++++++++++------------------ 3 files changed, 12 insertions(+), 87 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 0e021a40f..283a5aca1 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -96,33 +96,6 @@ class IptablesRule(object): chain = self.chain return '-A %s %s' % (chain, self.rule) -class IpSet(object): - """A class for handling large collections of IPs efficiently""" - - def __init__(self, name, execute=None): - self.name = name - self._ips = set() - if not execute: - self.execute = _execute - else: - self.execute = execute - - def __contains__(self, addr): - return addr in self._ips - - def _set_name(self): - return '%s-%s' % (binary_name, self.name) - - def add_ip(self, addr): - self._ips.add(addr) - self.execute('ipset', '-A', self._set_name(), addr) - - def remove_ip(self, addr): - self._ips.remove(addr) - self.execute('ipset', '-D', self._set_name(), addr) - - def iptables_source_match(self): - return ['-m set --match-set %s src' % (self._set_name(),)] class IptablesTable(object): """An iptables table.""" @@ -308,9 +281,6 @@ class IptablesManager(object): self.ipv4['nat'].add_chain('floating-snat') self.ipv4['nat'].add_rule('snat', '-j $floating-snat') - def ipset_supported(self): - return False - @utils.synchronized('iptables', external=True) def apply(self): """Apply the current in-memory set of iptables rules. diff --git a/nova/tests/test_iptables_network.py b/nova/tests/test_iptables_network.py index d0a8c052c..918034269 100644 --- a/nova/tests/test_iptables_network.py +++ b/nova/tests/test_iptables_network.py @@ -17,46 +17,11 @@ # under the License. """Unit Tests for network code.""" +import os + from nova import test from nova.network import linux_net -class IpSetTestCase(test.TestCase): - def test_add(self): - """Adding an address""" - ipset = linux_net.IpSet('somename') - - ipset.add_ip('1.2.3.4') - self.assertTrue('1.2.3.4' in ipset) - - - def test_add_remove(self): - """Adding and then removing an address""" - - self.verify_cmd_call_count = 0 - def verify_cmd(*args): - self.assertEquals(args, self.expected_cmd) - self.verify_cmd_call_count += 1 - - self.expected_cmd = ('ipset', '-A', 'run_tests.py-somename', '1.2.3.4') - ipset = linux_net.IpSet('somename',execute=verify_cmd) - ipset.add_ip('1.2.3.4') - self.assertTrue('1.2.3.4' in ipset) - - self.expected_cmd = ('ipset', '-D', 'run_tests.py-somename', '1.2.3.4') - ipset.remove_ip('1.2.3.4') - self.assertTrue('1.2.3.4' not in ipset) - self.assertEquals(self.verify_cmd_call_count, 2) - - - def test_two_adds_one_remove(self): - """Adding the same address twice works. Removing it once removes it entirely.""" - ipset = linux_net.IpSet('somename') - - ipset.add_ip('1.2.3.4') - ipset.add_ip('1.2.3.4') - ipset.remove_ip('1.2.3.4') - self.assertTrue('1.2.3.4' not in ipset) - class IptablesManagerTestCase(test.TestCase): sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011', diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index aa36e4184..4d615058b 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -709,23 +709,14 @@ class IptablesFirewallDriver(FirewallDriver): args += ['-s', rule.cidr] fw_rules += [' '.join(args)] else: - LOG.info('Not using cidr %r', rule.cidr) - if self.iptables.ipset_supported(): - LOG.info('ipset supported %r', rule.cidr) - ipset = linux_net.IpSet('%s' % rule.group_id) - args += ipset.iptables_source_match() - fw_rules += [' '.join(args)] - else: - LOG.info('ipset unsupported %r', rule.cidr) - LOG.info('rule.grantee_group.instances: %r', rule.grantee_group.instances) - 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)] + 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'] @@ -738,9 +729,8 @@ class IptablesFirewallDriver(FirewallDriver): return self.nwfilter.instance_filter_exists(instance) def refresh_security_group_members(self, security_group): - if not self.iptables.ipset_supported(): - self.do_refresh_security_group_rules(security_group) - self.iptables.apply() + 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) -- cgit From 1f55e116adbf00a0a5bd990f99a680e9d6b1618e Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: ec2utils: factor generic helper function into generic place This patch moves out a helper function, properties_root_device_name(), into generic file nova/block_device.py. --- nova/api/ec2/cloud.py | 5 +++-- nova/api/ec2/ec2utils.py | 19 ------------------- nova/block_device.py | 35 +++++++++++++++++++++++++++++++++++ nova/compute/api.py | 3 ++- nova/tests/test_api.py | 7 +++++-- 5 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 nova/block_device.py diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 16ca1ed2a..b25f74f61 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -30,6 +30,7 @@ import tempfile import time import shutil +from nova import block_device from nova import compute from nova import context @@ -1240,7 +1241,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 @@ -1313,7 +1314,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 diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bae1e0ee5..14891debb 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -137,25 +137,6 @@ def dict_from_dotted_str(items): 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 - # .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: diff --git a/nova/block_device.py b/nova/block_device.py new file mode 100644 index 000000000..963dffa37 --- /dev/null +++ b/nova/block_device.py @@ -0,0 +1,35 @@ +# 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. + + +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 + # .manifest.xml + if 'root_device_name' in properties: + root_device_name = properties['root_device_name'] + + return root_device_name diff --git a/nova/compute/api.py b/nova/compute/api.py index 9994e5724..43a95aa17 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -22,6 +22,7 @@ import eventlet import re import time +from nova import block_device from nova import db from nova import exception from nova import flags @@ -218,7 +219,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 = { diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 26ac5ff24..d5f653bc6 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): -- cgit From ba6b6a20eeedb0311e06090d2f60d36964d67cf4 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: block_device: introduce helper function to check swap or ephemeral device and move generic function, mappings_prepend_dev() from ec2utils to block_device --- nova/api/ec2/cloud.py | 8 +++----- nova/api/ec2/ec2utils.py | 10 ---------- nova/block_device.py | 36 ++++++++++++++++++++++++++++++++++++ nova/tests/test_api.py | 2 +- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b25f74f61..c35194f6f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -106,7 +106,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): @@ -145,8 +145,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', [])] @@ -1447,8 +1446,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 14891debb..bcdf2ba78 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -135,13 +135,3 @@ def dict_from_dotted_str(items): args[key] = value return args - - -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/block_device.py b/nova/block_device.py index 963dffa37..8d95e0029 100644 --- a/nova/block_device.py +++ b/nova/block_device.py @@ -15,6 +15,8 @@ # 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. @@ -33,3 +35,37 @@ def properties_root_device_name(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/tests/test_api.py b/nova/tests/test_api.py index d5f653bc6..e3d2ee2fc 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -187,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) -- cgit From 9be2793c2e057a5e4f8c8c4dd2131ddcc3b11608 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: db/api: block_device_mapping_update_or_create() It is possible to have same virtual device name. So eliminate old entries whose entry has same virtual device name. --- nova/db/sqlalchemy/api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ba03cabbc..ad51f5192 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -20,6 +20,7 @@ Implementation of SQLAlchemy backend. """ import warnings +from nova import block_device from nova import db from nova import exception from nova import flags @@ -2264,6 +2265,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): -- cgit From 92ac32e148d31a957be6e8f3e90724216e10106a Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: api/ec2: implement describe_instance_attribute() This patch implements DescribeInstanceAttribute. --- nova/api/ec2/cloud.py | 131 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 14 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index c35194f6f..de75b912b 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -260,7 +260,7 @@ class CloudController(object): instance_ref['id']) security_groups = [x['name'] for x in security_groups] 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'], @@ -874,13 +874,102 @@ 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) @@ -927,6 +1016,27 @@ class CloudController(object): result['blockDeviceMapping'] = mapping result['rootDeviceType'] = root_device_type + @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, **kwargs): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls @@ -952,6 +1062,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']} @@ -980,16 +1092,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'] @@ -999,12 +1107,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) -- cgit From a840e368235938a2fda96ab1694196e551ad22cc Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: ec2/get_metadata: teach block device mapping to get_metadata() This patch teachs bout block device mapping to get_metadata() --- nova/api/ec2/cloud.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index de75b912b..65f18ddbf 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -79,6 +79,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): @@ -233,6 +237,30 @@ class CloudController(object): state = 'available' return image['properties'].get('image_state', state) + def _get_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) @@ -259,18 +287,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._get_instance_mapping(ctxt, instance_ref) 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, -- cgit From e0517aef19bb00aa88809cb3c7d650ea38a08be2 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: compute/manager, virt: pass down root device name/swap/ephemeral to virt driver This patch makes compute/manager pass down infos about root device name, swap and ephemerals to virt driver. --- nova/compute/manager.py | 33 +++++++++++++++++++++++++-------- nova/virt/driver.py | 25 ++++++++++++++++++++++++- nova/virt/fake.py | 2 +- nova/virt/hyperv.py | 2 +- nova/virt/libvirt/connection.py | 25 ++++++++++++++----------- nova/virt/xenapi_conn.py | 2 +- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5819a520a..a4d2797d6 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -44,6 +44,7 @@ import functools from eventlet import greenthread +from nova import block_device from nova import exception from nova import flags import nova.image @@ -223,6 +224,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) @@ -230,11 +233,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 @@ -270,7 +280,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.""" @@ -313,13 +323,20 @@ 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(instance, network_info, bd_mapping) + self.driver.spawn(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: " diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 178279d31..62c4f7ead 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -32,6 +32,29 @@ 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 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. @@ -61,7 +84,7 @@ class ComputeDriver(object): """Return a list of InstanceInfo for all registered VMs""" raise NotImplementedError() - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info=None, block_device_info=None): """Launch a VM for the specified instance""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index ea0a59f21..48a03dac8 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -129,7 +129,7 @@ class FakeConnection(driver.ComputeDriver): info_list.append(self._map_to_instance_info(instance)) return info_list - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, instance, network_info=None, block_device_info=None): """ Create a new instance/VM/domain on the virtualization platform. diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 5c1dc772d..f0ce71392 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -139,7 +139,7 @@ class HyperVConnection(driver.ComputeDriver): return instance_infos - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, 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: diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 342dea98f..264c88a9e 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -580,14 +580,13 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(ilyaalekseyev): Implementation like in multinics # for xenapi(tr3buchet) @exception.wrap_exception() - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info=None, block_device_info=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=block_device_info) self.firewall_driver.setup_basic_filtering(instance, network_info) self.firewall_driver.prepare_instance_filter(instance, network_info) self._create_image(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) @@ -769,8 +768,12 @@ class LibvirtConnection(driver.ComputeDriver): # TODO(vish): should we format disk by default? def _create_image(self, inst, libvirt_xml, suffix='', disk_images=None, - network_info=None, block_device_mapping=None): - block_device_mapping = block_device_mapping or [] + network_info=None, block_device_info=None): + block_device_mapping = driver.block_device_info_get_mapping( + block_device_info) + + if not network_info: + network_info = netutils.get_network_info(inst) if not suffix: suffix = '' @@ -974,8 +977,9 @@ class LibvirtConnection(driver.ComputeDriver): return False def _prepare_xml_info(self, instance, rescue=False, network_info=None, - block_device_mapping=None): - block_device_mapping = block_device_mapping or [] + block_device_info=None): + block_device_mapping = driver.block_device_info_get_mapping( + block_device_info) # TODO(adiantum) remove network_info creation code # when multinics will be completed if not network_info: @@ -1030,12 +1034,11 @@ class LibvirtConnection(driver.ComputeDriver): return xml_info def to_xml(self, instance, rescue=False, network_info=None, - block_device_mapping=None): - block_device_mapping = block_device_mapping or [] + 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) + block_device_info) xml = str(Template(self.libvirt_xml, searchList=[xml_info])) LOG.debug(_('instance %s: finished toXML method'), instance['name']) return xml diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index ec8c44c1c..4c6f9fe46 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -194,7 +194,7 @@ class XenAPIConnection(driver.ComputeDriver): def list_instances_detail(self): return self._vmops.list_instances_detail() - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, instance, network_info=None, block_device_info=None): """Create VM instance""" self._vmops.spawn(instance, network_info) -- cgit From e05b3b11e67f18a6ff4867dfbc75554fd78cad1b Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:55:25 +0900 Subject: compute/api: pass down ephemeral device info --- nova/compute/api.py | 70 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 43a95aa17..942114161 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -33,7 +33,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 @@ -251,34 +250,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} @@ -287,10 +316,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', @@ -300,8 +337,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). @@ -334,12 +371,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 @@ -454,7 +491,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) -- cgit From 3c8cc5b06f477b88d20a748a924d6afac5c5260f Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:01 +0900 Subject: virt/libvirt: teach libvirt driver root device name This patch teaches libvirt driver root device name. --- nova/virt/libvirt.xml.template | 11 +++++++---- nova/virt/libvirt/connection.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index e1a683da8..e46a75915 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -12,13 +12,15 @@ #set $disk_bus = 'uml' uml /usr/bin/linux - /dev/ubda + #set $root_device_name = $getVar('root_device_name', '/dev/ubda') + ${root_device_name} #else #if $type == 'xen' #set $disk_prefix = 'sd' #set $disk_bus = 'scsi' linux - /dev/xvda + #set $root_device_name = $getVar('root_device_name', '/dev/xvda') + ${root_device_name} #else #set $disk_prefix = 'vd' #set $disk_bus = 'virtio' @@ -33,7 +35,8 @@ #if $type == 'xen' ro #else - root=/dev/vda console=ttyS0 + #set $root_device_name = $getVar('root_device_name', '/dev/vda') + root=${root_device_name} console=ttyS0 #end if #if $getVar('ramdisk', None) ${ramdisk} @@ -71,7 +74,7 @@ - + #end if #if $getVar('local', False) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 264c88a9e..30ad3c4fb 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -54,6 +54,7 @@ from xml.etree import ElementTree from eventlet import greenthread from eventlet import tpool +from nova import block_device from nova import context from nova import db from nova import exception @@ -834,7 +835,7 @@ class LibvirtConnection(driver.ComputeDriver): size = None root_fname += "_sm" - if not self._volume_in_mapping(self.root_mount_device, + if not self._volume_in_mapping(self.default_root_device, block_device_mapping): self._cache_image(fn=self._fetch_image, target=basepath('disk'), @@ -965,13 +966,13 @@ class LibvirtConnection(driver.ComputeDriver): return result - root_mount_device = 'vda' # FIXME for now. it's hard coded. + default_root_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) + mount_device_ = block_device.strip_dev(mount_device) for vol in block_device_mapping: - vol_mount_device = _strip_dev(vol['mount_device']) + vol_mount_device = block_device.strip_dev(vol['mount_device']) if vol_mount_device == mount_device_: return True return False @@ -998,8 +999,8 @@ class LibvirtConnection(driver.ComputeDriver): driver_type = 'raw' for vol in block_device_mapping: - vol['mount_device'] = _strip_dev(vol['mount_device']) - ebs_root = self._volume_in_mapping(self.root_mount_device, + vol['mount_device'] = block_device.strip_dev(vol['mount_device']) + ebs_root = self._volume_in_mapping(self.default_root_device, block_device_mapping) if self._volume_in_mapping(self.local_mount_device, block_device_mapping): @@ -1020,6 +1021,11 @@ class LibvirtConnection(driver.ComputeDriver): 'ebs_root': ebs_root, 'volumes': block_device_mapping} + 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 + if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'): xml_info['vncserver_host'] = FLAGS.vncserver_host xml_info['vnc_keymap'] = FLAGS.vnc_keymap -- cgit From 2c1b9ac98673c0ef1ae931c6b9d84e4b0741eed9 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:03 +0900 Subject: virt/libvirt: teach libvirt driver swap/ephemeral device This patch teaches libvirt virt driver swap/ephemeral device. --- nova/virt/driver.py | 4 ++ nova/virt/libvirt.xml.template | 22 +++++-- nova/virt/libvirt/connection.py | 127 ++++++++++++++++++++++++++++++++-------- 3 files changed, 122 insertions(+), 31 deletions(-) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 62c4f7ead..b2406e306 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -43,6 +43,10 @@ def block_device_info_get_swap(block_device_info): '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 [] diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index e46a75915..f7cf306bc 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -3,12 +3,10 @@ ${memory_kb} #if $type == 'lxc' - #set $disk_prefix = '' #set $disk_bus = '' exe /sbin/init #else if $type == 'uml' - #set $disk_prefix = 'ubd' #set $disk_bus = 'uml' uml /usr/bin/linux @@ -16,13 +14,11 @@ ${root_device_name} #else #if $type == 'xen' - #set $disk_prefix = 'sd' #set $disk_bus = 'scsi' linux #set $root_device_name = $getVar('root_device_name', '/dev/xvda') ${root_device_name} #else - #set $disk_prefix = 'vd' #set $disk_bus = 'virtio' hvm #end if @@ -77,13 +73,27 @@ #end if - #if $getVar('local', False) + #if $getVar('local_device', False) - + #end if + #for $eph in $ephemerals + + + + + + #end for + #if $getVar('swap_device', False) + + + + + + #end if #for $vol in $volumes diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 30ad3c4fb..cf013df30 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -149,8 +149,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): @@ -768,6 +768,11 @@ 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, inst, libvirt_xml, suffix='', disk_images=None, network_info=None, block_device_info=None): block_device_mapping = driver.block_device_info_get_mapping( @@ -836,7 +841,7 @@ class LibvirtConnection(driver.ComputeDriver): root_fname += "_sm" if not self._volume_in_mapping(self.default_root_device, - block_device_mapping): + block_device_info): self._cache_image(fn=self._fetch_image, target=basepath('disk'), fname=root_fname, @@ -846,13 +851,38 @@ class LibvirtConnection(driver.ComputeDriver): project=project, 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=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, - local_gb=inst_type['local_gb']) + 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 @@ -966,16 +996,35 @@ class LibvirtConnection(driver.ComputeDriver): return result - default_root_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_ = block_device.strip_dev(mount_device) - for vol in block_device_mapping: - vol_mount_device = block_device.strip_dev(vol['mount_device']) - if vol_mount_device == mount_device_: - return True - return False + 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 _prepare_xml_info(self, instance, rescue=False, network_info=None, block_device_info=None): @@ -1000,13 +1049,24 @@ class LibvirtConnection(driver.ComputeDriver): for vol in block_device_mapping: vol['mount_device'] = block_device.strip_dev(vol['mount_device']) + ebs_root = self._volume_in_mapping(self.default_root_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'] + 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'], @@ -1015,16 +1075,33 @@ 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, '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(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 -- cgit From af21767505b668c882734552115decdf8a798581 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:03 +0900 Subject: test_libvirt: fix up for local_gb --- nova/tests/test_libvirt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 6e2ec7ed6..2a21d0d32 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -187,6 +187,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): -- cgit From 47e7a21d74ebd06d994ad41088adb92d615aab0c Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: test_compute: make test_compute pass --- nova/tests/test_compute.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 5d59b628a..c5ce18495 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -887,15 +887,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'}, @@ -937,19 +939,21 @@ 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) @@ -962,7 +966,8 @@ class ComputeTestCase(test.TestCase): 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'}, -- cgit From 51c0c36bc5357102d0fa564a73631f1420e253b1 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: test_metadata: make test_metadata pass --- nova/tests/test_metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index c862726ab..f81e7a00a 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -43,6 +43,7 @@ class MetadataTestCase(test.TestCase): 'reservation_id': 'r-xxxxxxxx', 'user_data': '', 'image_ref': 7, + 'root_device_name': '/dev/sda1', 'hostname': 'test'}) def instance_get(*args, **kwargs): -- cgit From 77c34f0223a21d122062b2057e9ed1584dbbf8bf Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: nova/tests/test_compute.py: make test_compute.test_update_block_device_mapping happy --- nova/tests/test_compute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index c5ce18495..8f1364532 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -959,7 +959,8 @@ class ComputeTestCase(test.TestCase): 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)] -- cgit From 4c1fd45270faef4b42504bb5e2b8bd3e49b14d8c Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: tests/test_cloud:test_modify_image: make it pass --- nova/tests/test_cloud.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 8cdc73a66..0f1dfb813 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -852,13 +852,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) -- cgit From 405df88f00ce71621d3fda3ec52e5cf1217c8e05 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: image/glance: teach glance block device mapping --- nova/image/glance.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/nova/image/glance.py b/nova/image/glance.py index 5c2dc957b..88d3cf3af 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 @@ -184,6 +186,7 @@ class GlanceImageService(service.BaseImageService): """ # 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: @@ -210,12 +213,20 @@ class GlanceImageService(service.BaseImageService): """Clears out all images.""" 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 @@ -241,3 +252,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) -- cgit From 24b6597035c4393383ed1bdc2a6e52830743a7ea Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: db/api: fix network_get_by_cidr() User of the function is only 'nova-manage network delete'. It doesn't check deleted flag which must be checked. Otherwise some it might pick up deleted column depending on query result, and tries to delete already deleted columns and results in exception. --- nova/db/sqlalchemy/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ad51f5192..abfa6a3b7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1681,7 +1681,9 @@ def network_get_by_bridge(context, bridge): def network_get_by_cidr(context, cidr): session = get_session() result = session.query(models.Network).\ - filter_by(cidr=cidr).first() + filter_by(cidr=cidr).\ + filter_by(deleted=False).\ + first() if not result: raise exception.NetworkNotFoundForCidr(cidr=cidr) -- cgit From 4960b77202aba106adb8780ea724b26d958d5c81 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:04 +0900 Subject: tests: unit tests for nova.block_device --- nova/tests/test_block_device.py | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 nova/tests/test_block_device.py 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') -- cgit From 3af916ba0d87d383a89250b3aac4cf5e5b728f69 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:05 +0900 Subject: tests: unit tests for nova.virt --- nova/tests/test_virt.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 nova/tests/test_virt.py 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})) -- cgit From ba6404f05d9fb34a729d45e1ee055c7a7156c5c4 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:05 +0900 Subject: tests/glance: unit tests for glance serializer --- nova/tests/image/test_glance.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index 223e7ae57..488b03f9c 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -232,3 +232,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) -- cgit From 5113f78ddb8d7ccecea4e4ec8cbf35765af46d40 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:05 +0900 Subject: tests: unit tests for nova.virt.libvirt.connection._volume_in_mapping() --- nova/tests/test_libvirt.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 2a21d0d32..0c198f1b5 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -786,6 +786,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): -- cgit From 142a95a223a4259bcb3b35087b6d24f8310e3fa6 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:05 +0900 Subject: tests: an unit test for nova.compute.api.API._ephemeral_size() --- nova/tests/test_compute.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 8f1364532..32f55c6e2 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -990,3 +990,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) -- cgit From 916231fd945c5e726a21decdf1b6370b2fcefe70 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Sat, 23 Jul 2011 16:57:05 +0900 Subject: tests: unit tests for describe instance attribute --- nova/tests/test_cloud.py | 144 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 0f1dfb813..507b35d22 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 @@ -1438,3 +1440,145 @@ 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._get_instance_mapping(ctxt, instance_ref0), + cloud._DEFAULT_MAPPINGS) + self.assertEqual(self.cloud._get_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'}) -- cgit From 5961aa33f01db7503beeab4fabafb8e0d9ef6a3e Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sun, 24 Jul 2011 06:29:43 -0700 Subject: Adjust and re-enable relevant unit tests. --- nova/tests/test_libvirt.py | 50 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index f99e1713d..8eec7aada 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -891,7 +891,6 @@ class IptablesFirewallTestCase(test.TestCase): 'project_id': 'fake', 'instance_type_id': 1}) - @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_static_filters(self): instance_ref = self._create_instance_ref() ip = '10.11.12.13' @@ -907,17 +906,41 @@ class IptablesFirewallTestCase(test.TestCase): fixed_ip = {'address': ip, 'network_id': network_ref['id'], 'virtual_interface_id': vif_ref['id']} + + src_instance_ref = self._create_instance_ref() + src_instance_ip = '10.11.12.14' + src_instance_vif = {'address': '56:12:12:12:12:13', + 'network_id': network_ref['id'], + 'instance_id': src_instance_ref['id']} + src_instance_vif_ref = db.virtual_interface_create(self.context, + src_instance_vif) + src_instance_fixed_ip = {'address': src_instance_ip, + 'network_id': network_ref['id'], + 'virtual_interface_id': + src_instance_vif_ref['id']} + admin_ctxt = context.get_admin_context() db.fixed_ip_create(admin_ctxt, fixed_ip) db.fixed_ip_update(admin_ctxt, ip, {'allocated': True, 'instance_id': instance_ref['id']}) + db.fixed_ip_create(admin_ctxt, src_instance_fixed_ip) + db.fixed_ip_update(admin_ctxt, src_instance_ip, + {'allocated': True, + 'instance_id': src_instance_ref['id']}) + secgroup = db.security_group_create(admin_ctxt, {'user_id': 'fake', 'project_id': 'fake', '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', @@ -939,9 +962,19 @@ 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): @@ -994,17 +1027,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_instance_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']) -- cgit From a943c01dd56169270e1986ce62ae99f16ee4abe3 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sun, 24 Jul 2011 06:30:59 -0700 Subject: Make IP allocation test work again. --- nova/tests/test_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index b09021e13..9e021feea 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -216,7 +216,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') -- cgit From 6ddc49298f87fc20c6daff994495d745dc82b6e3 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sun, 24 Jul 2011 06:33:57 -0700 Subject: Use subscript rather than attribute. --- nova/virt/libvirt/firewall.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index 4d615058b..27056255c 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -709,14 +709,15 @@ class IptablesFirewallDriver(FirewallDriver): args += ['-s', rule.cidr] fw_rules += [' '.join(args)] else: - 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)] + 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'] -- cgit From a88f7ab3b7469af70c74ed5962abf867e62d768f Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sun, 24 Jul 2011 06:35:38 -0700 Subject: Use admin context when fetching instances. --- nova/network/manager.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 928cb09f6..ac37bb885 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -353,12 +353,12 @@ class NetworkManager(manager.SchedulerDependentManager): return self.set_network_host(context, network['id']) def _do_trigger_security_group_members_refresh_for_instance(self, - context, instance_id): - instance_ref = db.instance_get(context, instance_id) - groups = instance_ref.security_groups - group_ids = [group.id for group in groups] - self.compute_api.trigger_security_group_members_refresh(context, + 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): @@ -523,7 +523,6 @@ class NetworkManager(manager.SchedulerDependentManager): network['id'], instance_id) self._do_trigger_security_group_members_refresh_for_instance( - context, instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, instance_id, @@ -542,7 +541,6 @@ class NetworkManager(manager.SchedulerDependentManager): instance_ref = fixed_ip_ref['instance'] instance_id = instance_ref['id'] self._do_trigger_security_group_members_refresh_for_instance( - context, instance_id) def lease_fixed_ip(self, context, address): @@ -846,7 +844,6 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): network['id'], instance_id) self._do_trigger_security_group_members_refresh_for_instance( - context, instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, instance_id, -- cgit -- cgit From 3841a5515807b42e2e74e3119f76cdb2ef0f5575 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 26 Jul 2011 11:04:53 -0700 Subject: Remove debugging code. --- nova/compute/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 65a594d2c..ad8886f23 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -475,7 +475,6 @@ class API(base.Base): context, group_id)) - LOG.info('rules: %r', security_group_rules) # ..then we distill the security groups to which they belong.. security_groups = set() for rule in security_group_rules: @@ -484,14 +483,12 @@ class API(base.Base): rule['parent_group_id']) security_groups.add(security_group) - LOG.info('security_groups: %r', security_groups) # ..then we find the instances that are members of these groups.. instances = set() for security_group in security_groups: for instance in security_group['instances']: instances.add(instance) - LOG.info('instances: %r', instances) # ...then we find the hosts where they live... hosts = set() for instance in instances: @@ -501,7 +498,6 @@ class API(base.Base): # ...and finally we tell these nodes to refresh their view of this # particular security group. for host in hosts: - LOG.info('host: %r', host) rpc.cast(context, self.db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "refresh_security_group_members", -- cgit From 00171c3f50d333a1771efc048b064e1fd73614b0 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 26 Jul 2011 14:10:26 -0700 Subject: pep8 --- nova/db/sqlalchemy/models.py | 3 +-- nova/tests/test_network.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 4ecf80c8f..9f1967d69 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -494,14 +494,13 @@ 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", + grantee_group = relationship("SecurityGroup", foreign_keys=group_id, primaryjoin='and_(' 'SecurityGroupIngressRule.group_id == SecurityGroup.id,' 'SecurityGroupIngressRule.deleted == False)') - class ProviderFirewallRule(BASE, NovaBase): """Represents a rule in a security group.""" __tablename__ = 'provider_fw_rules' diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index e6cd5858a..4119953f2 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -215,8 +215,8 @@ class VlanNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(db, 'instance_get') db.instance_get(mox.IgnoreArg(), - mox.IgnoreArg()).AndReturn({ 'security_groups': - [ { 'id': 0 } ] }) + mox.IgnoreArg()).AndReturn({'security_groups': + [{'id': 0}]}) db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('192.168.0.1') -- cgit From 151ecddc0227ff9e779712532971fac8a2c79c7b Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Wed, 27 Jul 2011 15:40:13 +0100 Subject: Address merge review concerns. --- nova/virt/xenapi/vm_utils.py | 2 +- nova/virt/xenapi/vmops.py | 28 +++++++++++++++++----------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index d0ffb13da..5d6fe0825 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -520,7 +520,7 @@ class VMHelper(HelperBase): # DISK restores LOG.debug(_("Fetching image %(image)s") % locals()) LOG.debug(_("Image Type: %s"), ImageType.to_string(image_type)) - sr_ref = None + if image_type == ImageType.DISK_ISO: sr_ref = safe_find_iso_sr(session) LOG.debug(_("ISO: Found sr possibly containing the ISO image")) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 1830ce18d..7ac923933 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -238,6 +238,21 @@ class VMOps(object): raise vm_create_error + # 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.xenapi_inject_image: + 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 @@ -253,7 +268,7 @@ class VMOps(object): 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 so use '2', we've used '0' + # 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) @@ -264,7 +279,7 @@ class VMOps(object): 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, we've used '0' + # userdevice 1 is reserved for rescue and we've used '0' userdevice = 2 # Attach any other disks @@ -278,15 +293,6 @@ class VMOps(object): bootable=False) userdevice += 1 - # Alter the image before VM start for, e.g. network injection - if FLAGS.xenapi_inject_image: - 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) -- cgit From 2e3b199005d16ee3e35cd6c71b8512628e3631bc Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Thu, 28 Jul 2011 21:12:03 +0100 Subject: Simplified test cases --- nova/api/ec2/cloud.py | 2 +- nova/tests/test_api.py | 27 ++++++--------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index ecdbad895..371837d19 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -695,7 +695,7 @@ class CloudController(object): 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 + # 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, " diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 5759e7726..40e62ac76 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -365,7 +365,7 @@ class ApiEc2TestCase(test.TestCase): 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, + API Spec states we should only accept alphanumeric characters, spaces, dashes, and underscores. """ self.expect_http() self.mox.ReplayAll() @@ -380,16 +380,8 @@ class ApiEc2TestCase(test.TestCase): # dashes, and underscores. security_group_name = "aa #^% -=99" - try: - self.ec2.create_security_group(security_group_name, 'test group') - except EC2ResponseError, e: - if e.code == 'InvalidParameterValue': - pass - else: - self.fail("Unexpected EC2ResponseError: %s " - "(expected InvalidParameterValue)" % e.code) - else: - self.fail('Exception not raised.') + 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. @@ -406,16 +398,9 @@ class ApiEc2TestCase(test.TestCase): # Test block group_name > 255 chars security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") for x in range(random.randint(256, 266))) - try: - self.ec2.create_security_group(security_group_name, 'test group') - except EC2ResponseError, e: - if e.code == 'InvalidParameterValue': - pass - else: - self.fail("Unexpected EC2ResponseError: %s " - "(expected InvalidParameterValue)" % e.code) - else: - self.fail('Exception not raised.') + + self.assertRaises(EC2ResponseError, self.ec2.create_security_group, + security_group_name, 'test group') def test_authorize_revoke_security_group_cidr(self): """ -- cgit From fe195087797ca031e437c34e25380354e3ba4f56 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 28 Jul 2011 21:59:02 +0000 Subject: Added methods to read/write values to a config file on the XenServer host. --- .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 48 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 292bbce12..8b85fe666 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,49 @@ 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): + conf = _get_config_dict() + key = arg_dict["key"] + ret = conf.get(key) + if ret is None: + # Can't jsonify None + return "None" + return ret + + +@jsonify +def set_config(self, arg_dict): + conf = _get_config_dict() + key = arg_dict["key"] + val = arg_dict["value"] + conf.update({key: val}) + _write_config_dict(conf) + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -217,4 +261,6 @@ def cleanup(dct): if __name__ == "__main__": XenAPIPlugin.dispatch( {"host_data": host_data, - "set_host_enabled": set_host_enabled}) + "set_host_enabled": set_host_enabled, + "get_config": get_config, + "set_config": set_config}) -- cgit From 1753da4d586f896f449828879e4361241289e376 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 28 Jul 2011 22:25:08 +0000 Subject: Added the config values to the return of the host_data method. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 8b85fe666..873d1fe63 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -159,6 +159,8 @@ 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 + ret_dict.update(_get_config_dict) return ret_dict -- cgit From a52b643b18e1bac18b642ecfd781809eb5612763 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 29 Jul 2011 10:51:50 +0900 Subject: api/ec2: rename CloudController._get_instance_mapping into _format_instance_mapping This patch renames nova.api.ec2.cloud.CouldController._get_instance_mapping to _format_instance_mapping in order to make it clear that the method is for API formatting, not for internal use. --- nova/api/ec2/cloud.py | 4 ++-- nova/tests/test_cloud.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 65f18ddbf..9b0ec2fde 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -237,7 +237,7 @@ class CloudController(object): state = 'available' return image['properties'].get('image_state', state) - def _get_instance_mapping(self, ctxt, instance_ref): + 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 @@ -287,7 +287,7 @@ 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._get_instance_mapping(ctxt, instance_ref) + mappings = self._format_instance_mapping(ctxt, instance_ref) data = { 'user-data': self._format_user_data(instance_ref), 'meta-data': { diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 507b35d22..ac959bd63 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -1505,9 +1505,11 @@ class CloudTestCase(test.TestCase): 'ephemeral1': '/dev/sdd', 'ephemeral2': '/dev/sd3'} - self.assertEqual(self.cloud._get_instance_mapping(ctxt, instance_ref0), + self.assertEqual(self.cloud._format_instance_mapping(ctxt, + instance_ref0), cloud._DEFAULT_MAPPINGS) - self.assertEqual(self.cloud._get_instance_mapping(ctxt, instance_ref1), + self.assertEqual(self.cloud._format_instance_mapping(ctxt, + instance_ref1), expected) def test_describe_instance_attribute(self): -- cgit From 45c3c01f69e1f13ced70942e6c8369098a307c48 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 29 Jul 2011 01:54:19 -0400 Subject: Added xml schema validation for extensions resources. Added corresponding xml schemas. Added lxml dep, which is needed for doing xml schema validation. --- nova/api/openstack/extensions.py | 31 +- nova/api/openstack/schemas/atom-link.rng | 141 ++++++ nova/api/openstack/schemas/atom.rng | 597 +++++++++++++++++++++++++ nova/api/openstack/schemas/v1.1/extension.rng | 11 + nova/api/openstack/schemas/v1.1/extensions.rng | 6 + nova/tests/api/openstack/test_extensions.py | 26 +- tools/pip-requires | 1 + 7 files changed, 793 insertions(+), 20 deletions(-) create mode 100644 nova/api/openstack/schemas/atom-link.rng create mode 100644 nova/api/openstack/schemas/atom.rng create mode 100644 nova/api/openstack/schemas/v1.1/extension.rng create mode 100644 nova/api/openstack/schemas/v1.1/extensions.rng diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index cc889703e..6188e274d 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') @@ -470,36 +471,38 @@ class ResourceExtension(object): 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/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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content. + + + An atom:entry must have an atom:author if its feed does not. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + .+@.+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + 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 @@ + + + + + diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index d459c694f..2bc8bbb07 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -20,16 +20,20 @@ import os.path import stubout import unittest import webob -from xml.etree import ElementTree +from lxml import etree +from StringIO import StringIO from nova import context from nova import flags +from nova import utils 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 + FLAGS = flags.FLAGS NS = "{http://docs.openstack.org/compute/api/v1.1}" ATOMNS = "{http://www.w3.org/2005/Atom}" @@ -140,7 +144,7 @@ class ExtensionControllerTest(unittest.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. @@ -156,6 +160,8 @@ class ExtensionControllerTest(unittest.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) @@ -163,9 +169,10 @@ class ExtensionControllerTest(unittest.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') @@ -175,6 +182,8 @@ class ExtensionControllerTest(unittest.TestCase): self.assertEqual(root.findtext('{0}description'.format(NS)), 'The Fox In Socks Extension') + xmlutil.validate_schema(root, 'extension') + class ResourceExtensionTest(unittest.TestCase): @@ -354,7 +363,8 @@ class ExtensionsXMLSerializerTest(unittest.TestCase): } 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']) @@ -368,6 +378,8 @@ class ExtensionsXMLSerializerTest(unittest.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 = { @@ -415,7 +427,7 @@ class ExtensionsXMLSerializerTest(unittest.TestCase): 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): @@ -431,3 +443,5 @@ class ExtensionsXMLSerializerTest(unittest.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/tools/pip-requires b/tools/pip-requires index dec93c351..8d9798357 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -9,6 +9,7 @@ boto==1.9b carrot==0.10.5 eventlet lockfile==0.8 +lxml==0.8.2 python-novaclient==2.5.7 python-daemon==1.5.5 python-gflags==1.3 -- cgit From 22b0e3948beaa2b1b3d61562e453412abb5edcbc Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 29 Jul 2011 03:42:40 -0400 Subject: Removing unnecessary imports. --- nova/tests/api/openstack/test_extensions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 2bc8bbb07..ae0dc283f 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -21,11 +21,9 @@ import stubout import unittest import webob from lxml import etree -from StringIO import StringIO from nova import context from nova import flags -from nova import utils from nova.api import openstack from nova.api.openstack import extensions from nova.api.openstack import flavors -- cgit From 79283cbb13d91e3c25e42af765f9da627813a6d8 Mon Sep 17 00:00:00 2001 From: Kei masumoto Date: Fri, 29 Jul 2011 20:03:23 +0900 Subject: merged trunk and fixed post_live_migratioin_at_destination to get nw_info --- nova/compute/manager.py | 4 +++- nova/virt/libvirt/connection.py | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index e80394fbd..8aee456fc 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1389,7 +1389,7 @@ class ComputeManager(manager.SchedulerDependentManager): # 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) + self.driver.destroy(instance_ref, network_info) LOG.info(_('Migrating %(i_name)s to %(dest)s finished successfully.') % locals()) @@ -1409,8 +1409,10 @@ class ComputeManager(manager.SchedulerDependentManager): 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) def rollback_live_migration(self, context, instance_ref, diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0eab0b109..31f7acb4d 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1620,7 +1620,9 @@ class LibvirtConnection(driver.ComputeDriver): user, project) - def post_live_migration_at_destination(self, ctxt, instance_ref, + def post_live_migration_at_destination(self, ctxt, + instance_ref, + network_info, block_migration): """Post operation of live migration at destination host. @@ -1628,6 +1630,7 @@ class LibvirtConnection(driver.ComputeDriver): :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. @@ -1639,7 +1642,7 @@ class LibvirtConnection(driver.ComputeDriver): # In case of block migration, destination does not have # libvirt.xml if not os.path.isfile(xml_path): - xml = self.to_xml(instance_ref) + 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() -- cgit From 9afa4437bf43655766e7a6f13f67ad52f27ba7b5 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 29 Jul 2011 12:14:29 -0400 Subject: Fixing lxml version requirement. --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 8d9798357..150139eb5 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -9,7 +9,7 @@ boto==1.9b carrot==0.10.5 eventlet lockfile==0.8 -lxml==0.8.2 +lxml==2.3 python-novaclient==2.5.7 python-daemon==1.5.5 python-gflags==1.3 -- cgit From 8c54cfdabad3b3c102bae05283dd8484da38c557 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Mon, 1 Aug 2011 09:30:13 +0100 Subject: Remove copy/paste error. --- nova/virt/xenapi/vmops.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index f2258bde3..5c0b3020f 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -293,15 +293,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) -- cgit From e72fafbf76ed456039426a96dd65d2c148dffa29 Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 1 Aug 2011 12:37:12 -0700 Subject: adding a function with logic to make the creation of networks validation a bit smarter: - detects if the cidr is already in use - when specifying a supernet to be split into smaller subnets via num_networks && network_size, ensures none of the returned subnets are in use by either a subnet of the same size and range, nor a SMALLER size within the same range. - detects if splitting a supernet into # of num_networks && network_size will fit - detects if the supernet/cidr specified is conflicting with a network cidr that currently exists that may be a larger supernet already encompassing the specified cidr. " --- nova/network/manager.py | 48 +++++++++++++++++++++ nova/tests/test_network.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/nova/network/manager.py b/nova/network/manager.py index 4a3791d8a..fafb75c9e 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -609,6 +609,54 @@ 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: + raise ValueError(_("network_size * num_networks exceeds cidr size")) + adjusted_cidr = netaddr.IPNetwork(req_net_ip+'/'+str(significant_bits)) + all_req_nets = [adjusted_cidr] + 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: + raise ValueError(_("requested cidr (%s) conflicts with existing supernet (%s)" % (str(adjusted_cidr), str(adjusted_cidr_supernet)))) + # split supernet into subnets + if num_networks >= 2: + next_cidr = adjusted_cidr + for used_cidr in used_cidrs: + # watch for smaller subnets conflicting + if used_cidr.size < next_cidr.size: + for ucsupernet in used_cidr.supernet(): + if ucsupernet.size == next_cidr.size: + used_cidrs.append(ucsupernet) + for index in range(1, num_networks): + while True: + next_cidr = next_cidr.next() + if next_cidr in used_cidrs: + continue + else: + all_req_nets.append(next_cidr) + break + all_req_nets = list(set(all_req_nets)) + if not used_nets: + return all_req_nets + # after splitting ensure there were enough to satisfy the num_networks + if len(all_req_nets) < num_networks: + raise ValueError(_("Not enough subnets avail to satisfy requested num_networks")) + # if one of the split subnets were already defined, remove from create list + for req_cidr in all_req_nets: + if req_cidr in used_cidrs: + all_req_nets.remove(req_cidr) + 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): diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 28f50d328..8a851cf76 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import context from nova import db from nova import exception from nova import flags @@ -249,6 +250,15 @@ 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): + return {'foo': 'bar'} + + def network_get_all(self, context): + raise exception.NoNetworksFound() + def __init__(self): self.db = self.FakeDB() self.deallocate_called = None @@ -267,3 +277,95 @@ 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', '192.168.1.0', '192.168.3.0', '192.168.4.0'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr+'/24' in cidrs) + self.assertFalse('192.168.2.0/24' in cidrs) + + def test__validate_cidrs_split_cidr_smaller_subnet_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/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', '192.168.1.0', '192.168.3.0', '192.168.4.0'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr+'/24' in cidrs) + self.assertFalse('192.168.2.0/24' in cidrs) + + 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] + print returned_cidrs + 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() + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, 'cidr': '192.168.0.0/8'}]) + self.mox.ReplayAll() + args = [None, '192.168.0.0/24', 1, 256] + # ValueError: requested cidr (192.168.0.0/24) conflicts with existing supernet (192.0.0.0/8) + self.assertRaises(ValueError, manager._validate_cidrs, *args) +# +# def test_create_networks_cidr_already_used(self): +# mockany = self.mox.CreateMockAnything() +# manager = self.FakeNetworkManager() +# self.mox.StubOutWithMock(manager.db, 'network_get_by_cidr') +# manager.db.network_get_by_cidr(mox.IgnoreArg(), '192.168.0.0/24').AndReturn(mockany) +# 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) -- cgit From 951114be20065044e7f12e37188eb30e859ff2cb Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 1 Aug 2011 12:47:41 -0700 Subject: removed redundant logic --- nova/network/manager.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index fafb75c9e..f6c7e35e0 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -646,15 +646,9 @@ class NetworkManager(manager.SchedulerDependentManager): all_req_nets.append(next_cidr) break all_req_nets = list(set(all_req_nets)) - if not used_nets: - return all_req_nets # after splitting ensure there were enough to satisfy the num_networks if len(all_req_nets) < num_networks: raise ValueError(_("Not enough subnets avail to satisfy requested num_networks")) - # if one of the split subnets were already defined, remove from create list - for req_cidr in all_req_nets: - if req_cidr in used_cidrs: - all_req_nets.remove(req_cidr) return all_req_nets def create_networks(self, context, label, cidr, multi_host, num_networks, -- cgit From 85795ff1f8b6a0ff3de634828208d6debd91692f Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 1 Aug 2011 21:06:47 +0000 Subject: Added option for rebooting or shutting down a host. --- nova/api/openstack/contrib/hosts.py | 15 +++++++++++++ nova/compute/api.py | 5 +++++ nova/compute/manager.py | 8 ++++++- nova/tests/test_hosts.py | 9 ++++++++ nova/virt/driver.py | 4 ++++ nova/virt/fake.py | 4 ++++ nova/virt/hyperv.py | 4 ++++ nova/virt/libvirt/connection.py | 4 ++++ nova/virt/vmwareapi_conn.py | 4 ++++ nova/virt/xenapi/vmops.py | 8 +++++++ nova/virt/xenapi_conn.py | 4 ++++ .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 25 +++++++++++++++++++++- 12 files changed, 92 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 55e57e1a4..b6a4bdb77 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -78,6 +78,12 @@ class HostController(object): else: explanation = _("Invalid status: '%s'") % raw_val raise webob.exc.HTTPBadRequest(explanation=explanation) + elif key == "powerstate": + if val in ("reboot", "shutdown"): + return self._set_powerstate(req, id, val) + else: + explanation = _("Invalid powerstate: '%s'") % raw_val + raise webob.exc.HTTPBadRequest(explanation=explanation) else: explanation = _("Invalid update setting: '%s'") % raw_key raise webob.exc.HTTPBadRequest(explanation=explanation) @@ -91,6 +97,15 @@ class HostController(object): enabled=enabled) return {"host": host, "status": result} + def _set_powerstate(self, req, host, state): + """Reboots or shuts down the host.""" + context = req.environ['nova.context'] + LOG.audit(_("Changing powerstate of host %(host)s to %(state)s.") + % locals()) + result = self.compute_api.set_host_powerstate(context, host=host, + state=state) + return {"host": host, "powerstate": result} + class Hosts(extensions.ExtensionDescriptor): def get_name(self): diff --git a/nova/compute/api.py b/nova/compute/api.py index 8f7b3c3ef..bd17fdf31 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -998,6 +998,11 @@ class API(base.Base): return self._call_compute_message("set_host_enabled", context, instance_id=None, host=host, params={"enabled": enabled}) + def set_host_powerstate(self, context, host, state): + """Reboots or shuts down the host.""" + return self._call_compute_message("set_host_powerstate", context, + instance_id=None, host=host, params={"state": state}) + @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for the given instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index a2d84cd76..d0f9a81f4 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1,4 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 +: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. @@ -927,6 +927,12 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, result)) + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + def set_host_powerstate(self, context, instance_id=None, host=None, + state=None): + """Reboots or shuts down the host.""" + return self.driver.set_host_powerstate(host, state) + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def set_host_enabled(self, context, instance_id=None, host=None, enabled=None): diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 548f81f8b..ad057f429 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -48,6 +48,13 @@ def stub_set_host_enabled(context, host, enabled): return status +def stub_set_host_powerstate(context, host, state): + # We'll simulate success and failure by assuming + # that 'host_c1' always succeeds, and 'host_c2' + # always fails + return state if host == "host_c1" else "running" + + class FakeRequest(object): environ = {"nova.context": context.get_admin_context()} @@ -62,6 +69,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, 'set_host_powerstate', + stub_set_host_powerstate) def test_list_hosts(self): """Verify that the compute hosts are returned.""" diff --git a/nova/virt/driver.py b/nova/virt/driver.py index b219fb2cb..bbb17480d 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -251,6 +251,10 @@ class ComputeDriver(object): """Poll for rescued instances""" raise NotImplementedError() + def set_host_powerstate(self, host, state): + """Reboots or shuts down the host.""" + raise NotImplementedError() + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 26bc421c0..6dc6552d7 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -515,6 +515,10 @@ class FakeConnection(driver.ComputeDriver): """Return fake Host Status of ram, disk, network.""" return self.host_status + def set_host_powerstate(self, host, state): + """Reboots or shuts down 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 c26fe108b..119def38b 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -498,6 +498,10 @@ class HyperVConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def set_host_powerstate(self, host, state): + """Reboots or shuts down the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 17c328a83..ae1a16d44 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1583,6 +1583,10 @@ class LibvirtConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def set_host_powerstate(self, host, state): + """Reboots or shuts down 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/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index ce57847b2..af547821f 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -190,6 +190,10 @@ class VMWareESXConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" return + def set_host_powerstate(self, host, state): + """Reboots or shuts down the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 7e02e1def..8e57042f9 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1023,6 +1023,14 @@ class VMOps(object): # TODO: implement this! return 'http://fakeajaxconsole/fake_url' + def set_host_powerstate(self, host, state): + """Reboots or shuts down the host.""" + args = {"state": json.dumps(state)} + methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"} + json_resp = self._call_xenhost(methods[state], args) + resp = json.loads(json_resp) + return resp["powerstate"] + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" args = {"enabled": json.dumps(enabled)} diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index cc18ed83c..5ac837a17 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -334,6 +334,10 @@ class XenAPIConnection(driver.ComputeDriver): True, run the update first.""" return self.HostState.get_host_stats(refresh=refresh) + def set_host_powerstate(self, host, state): + """Reboots or shuts down the host.""" + return self._vmops.set_host_powerstate(host, state) + 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/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 292bbce12..5a5122b4a 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -103,6 +103,27 @@ def set_host_enabled(self, arg_dict): return {"status": status} +def _powerstate(state): + host_uuid = _get_host_uuid() + cmd = "xe host-disable uuid=%(host_uuid)s" % locals() + _run_command(cmd) + cmd = "xe host-%(state)s uuid=%(host_uuid)s" % locals() + _run_command(cmd) + return {"powerstate": state} + + +@jsonify +def host_reboot(self, arg_dict): + """Reboots the host.""" + return _powerstate("reboot") + + +@jsonify +def host_shutdown(self, arg_dict): + """Reboots the host.""" + return _powerstate("shutdown") + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -217,4 +238,6 @@ 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}) -- cgit From ab4bfcf6c458ab6bf6ead126a91413b92aa543b8 Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 1 Aug 2011 16:27:17 -0700 Subject: pep8 fixes --- nova/network/manager.py | 12 ++++++++---- nova/tests/test_network.py | 36 +++++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index f6c7e35e0..9d0f080ba 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -615,8 +615,10 @@ class NetworkManager(manager.SchedulerDependentManager): req_net_ip = str(req_net.ip) req_size = network_size * num_networks if req_size > req_net.size: - raise ValueError(_("network_size * num_networks exceeds cidr size")) - adjusted_cidr = netaddr.IPNetwork(req_net_ip+'/'+str(significant_bits)) + 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) all_req_nets = [adjusted_cidr] try: used_nets = self.db.network_get_all(context) @@ -627,7 +629,8 @@ class NetworkManager(manager.SchedulerDependentManager): raise ValueError(_("cidr already in use")) for adjusted_cidr_supernet in adjusted_cidr.supernet(): if adjusted_cidr_supernet in used_cidrs: - raise ValueError(_("requested cidr (%s) conflicts with existing supernet (%s)" % (str(adjusted_cidr), str(adjusted_cidr_supernet)))) + msg = "requested cidr (%s) conflicts with existing supernet" + raise ValueError(_(msg % str(adjusted_cidr))) # split supernet into subnets if num_networks >= 2: next_cidr = adjusted_cidr @@ -648,7 +651,8 @@ class NetworkManager(manager.SchedulerDependentManager): all_req_nets = list(set(all_req_nets)) # after splitting ensure there were enough to satisfy the num_networks if len(all_req_nets) < num_networks: - raise ValueError(_("Not enough subnets avail to satisfy requested num_networks")) + msg = "Not enough subnets avail to satisfy requested num_networks" + raise ValueError(_(msg)) return all_req_nets def create_networks(self, context, label, cidr, multi_host, num_networks, diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 8a851cf76..1f5305b1d 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -297,28 +297,32 @@ class CommonNetworkTestCase(test.TestCase): 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'}]) + 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', '192.168.1.0', '192.168.3.0', '192.168.4.0'] + exp_cidrs = ['192.168.0.0', '192.168.1.0', '192.168.3.0', + '192.168.4.0'] for exp_cidr in exp_cidrs: - self.assertTrue(exp_cidr+'/24' in cidrs) + self.assertTrue(exp_cidr + '/24' in cidrs) self.assertFalse('192.168.2.0/24' in cidrs) - def test__validate_cidrs_split_cidr_smaller_subnet_in_use_middle_of_range(self): + def test__validate_cidrs_split_cidr_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.0/25'}]) + 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', '192.168.1.0', '192.168.3.0', '192.168.4.0'] + exp_cidrs = ['192.168.0.0', '192.168.1.0', '192.168.3.0', + '192.168.4.0'] for exp_cidr in exp_cidrs: - self.assertTrue(exp_cidr+'/24' in cidrs) + self.assertTrue(exp_cidr + '/24' in cidrs) self.assertFalse('192.168.2.0/24' in cidrs) def test__validate_cidrs_one_in_use(self): @@ -331,7 +335,8 @@ class CommonNetworkTestCase(test.TestCase): 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'}]) + 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] @@ -340,7 +345,8 @@ class CommonNetworkTestCase(test.TestCase): 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 + # ValueError: Not enough subnets avail to satisfy requested + # num_networks self.assertRaises(ValueError, manager._validate_cidrs, *args) def test__validate_cidrs_split_partial(self): @@ -355,17 +361,21 @@ class CommonNetworkTestCase(test.TestCase): 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/8'}]) + manager.db.network_get_all(ctxt).AndReturn([{'id': 1, + 'cidr': '192.168.0.0/8'}]) self.mox.ReplayAll() args = [None, '192.168.0.0/24', 1, 256] - # ValueError: requested cidr (192.168.0.0/24) conflicts with existing supernet (192.0.0.0/8) + # ValueError: requested cidr (192.168.0.0/24) conflicts + # with existing supernet self.assertRaises(ValueError, manager._validate_cidrs, *args) # # def test_create_networks_cidr_already_used(self): # mockany = self.mox.CreateMockAnything() # manager = self.FakeNetworkManager() # self.mox.StubOutWithMock(manager.db, 'network_get_by_cidr') -# manager.db.network_get_by_cidr(mox.IgnoreArg(), '192.168.0.0/24').AndReturn(mockany) +# manager.db.network_get_by_cidr(mox.IgnoreArg(), '192.168.0.0/24')\ +# .AndReturn(mockany) # self.mox.ReplayAll() -# args = [None, 'foo', '192.168.0.0/24', None, 1, 256, 'fd00::/48', None, None, None] +# args = [None, 'foo', '192.168.0.0/24', None, 1, 256, +# 'fd00::/48', None, None, None] # self.assertRaises(ValueError, manager.create_networks, *args) -- cgit From 883095f1e69d70173339ccba64f2b2a8a1d48a5f Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Mon, 1 Aug 2011 20:45:30 -0500 Subject: Added @test.skip_unless and @test.skip_if functionality. Also created nova/tests/test_skip_examples.py to show the skip cases usage. --- nova/test.py | 31 +++++++++++++++++++++++++++++++ nova/tests/test_skip_examples.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 nova/tests/test_skip_examples.py diff --git a/nova/test.py b/nova/test.py index 549aa6fcf..afc118100 100644 --- a/nova/test.py +++ b/nova/test.py @@ -67,6 +67,37 @@ class skip_test(object): _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): + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if self.condition: + raise nose.SkipTest(self.message) + _skipper.__name__ = func.__name__ + _skipper.__doc__ = func.__doc__ + return _skipper + + +class skip_unless(object): + """Decorator that skips a test if condition is false.""" + def __init__(self, condition, msg): + self.condition = condition + self.message = msg + + def __call__(self, func): + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if not self.condition: + raise nose.SkipTest(self.message) + _skipper.__name__ = func.__name__ + _skipper.__doc__ = func.__doc__ + return _skipper + def skip_if_fake(func): """Decorator that skips a test if running in fake mode.""" diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py new file mode 100644 index 000000000..dfbaac74b --- /dev/null +++ b/nova/tests/test_skip_examples.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from nova import test + + +class ExampleSkipTestCase(test.TestCase): + @test.skip_test("testing skipping") + def test_skip_test(self): + x = 1 + + @test.skip_if(os.getenv("USER"), + "Skiping -- Environment variable USER exists") + def test_skip_if_env_user_exists(self): + x = 1 + + @test.skip_unless(os.getenv("BLAH"), + "Skipping -- Environment variable BLAH does not exist") + def test_skip_unless_env_blah_exists(self): + x = 1 -- cgit From 07d89c29389fe8f2b9f3a398ab99566d151e8e92 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 02:19:31 +0000 Subject: Added host shutdown/reboot conditioning. --- .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 5a5122b4a..873b4c58d 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -105,10 +105,20 @@ def set_host_enabled(self, arg_dict): def _powerstate(state): host_uuid = _get_host_uuid() - cmd = "xe host-disable uuid=%(host_uuid)s" % locals() - _run_command(cmd) - cmd = "xe host-%(state)s uuid=%(host_uuid)s" % locals() - _run_command(cmd) + # 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[state]) + # Should be empty string + if result: + raise pluginlib.PluginError(result) return {"powerstate": state} @@ -124,6 +134,12 @@ def host_shutdown(self, arg_dict): return _powerstate("shutdown") +@jsonify +def host_start(self, arg_dict): + """Starts the host.""" + return _powerstate("startup") + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -240,4 +256,5 @@ if __name__ == "__main__": {"host_data": host_data, "set_host_enabled": set_host_enabled, "host_shutdown": host_shutdown, - "host_reboot": host_reboot}) + "host_reboot": host_reboot, + "host_start": host_start}) -- cgit From bcfd8f5e1e0c3b53a2ad4a5bb533d94dcf5ef18c Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 1 Aug 2011 21:34:43 -0700 Subject: added some tests for network create & moved the ipv6 logic back into the function --- nova/network/manager.py | 43 ++++++++++++++++++------------------------- nova/tests/test_network.py | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 9d0f080ba..8b86bc036 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -659,71 +659,64 @@ class NetworkManager(manager.SchedulerDependentManager): network_size, cidr_v6, gateway_v6, bridge, bridge_interface, dns1=None, dns2=None, **kwargs): """Create networks based on parameters.""" - fixed_net = netaddr.IPNetwork(cidr) + req_cidrs = self._validate_cidrs(context, cidr, num_networks, network_size) + if FLAGS.use_ipv6: fixed_net_v6 = netaddr.IPNetwork(cidr_v6) significant_bits_v6 = 64 network_size_v6 = 1 << 64 - for index in range(num_networks): - start = index * network_size - significant_bits = 32 - int(math.log(network_size, 2)) - cidr = '%s/%s' % (fixed_net[start], significant_bits) - project_net = netaddr.IPNetwork(cidr) + count = 0 + for req_cidr in req_cidrs: + count = count + 1 net = {} net['bridge'] = bridge net['bridge_interface'] = bridge_interface net['dns1'] = dns1 net['dns2'] = dns2 - net['cidr'] = cidr - net['multi_host'] = multi_host - net['netmask'] = str(project_net.netmask) - net['gateway'] = str(project_net[1]) - net['broadcast'] = str(project_net.broadcast) - net['dhcp_start'] = str(project_net[2]) + net['cidr'] = str(req_cidr) + net['netmask'] = str(req_cidr.netmask) + net['gateway'] = str(req_cidr[1]) + net['broadcast'] = str(req_cidr.broadcast) + net['dhcp_start'] = str(req_cidr[2]) if num_networks > 1: - net['label'] = '%s_%d' % (label, index) + net['label'] = '%s_%d' % (label, count) else: net['label'] = label if FLAGS.use_ipv6: - start_v6 = index * network_size_v6 + start_v6 = count * network_size_v6 cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], significant_bits_v6) net['cidr_v6'] = cidr_v6 - project_net_v6 = netaddr.IPNetwork(cidr_v6) - if gateway_v6: # use a pre-defined gateway if one is provided net['gateway_v6'] = str(gateway_v6) else: net['gateway_v6'] = str(project_net_v6[1]) - net['netmask_v6'] = str(project_net_v6._prefixlen) if kwargs.get('vpn', False): # this bit here is for vlan-manager del net['dns1'] del net['dns2'] - vlan = kwargs['vlan_start'] + index - net['vpn_private_address'] = str(project_net[2]) - net['dhcp_start'] = str(project_net[3]) + vlan = kwargs['vlan_start'] + count + net['vpn_private_address'] = str(req_cidr[2]) + net['dhcp_start'] = str(req_cidr[3]) net['vlan'] = vlan net['bridge'] = 'br%s' % vlan # NOTE(vish): This makes ports unique accross the cloud, a more # robust solution would be to make them uniq per ip - net['vpn_public_port'] = kwargs['vpn_start'] + index + net['vpn_public_port'] = kwargs['vpn_start'] + count - # None if network with cidr or cidr_v6 already exists network = self.db.network_create_safe(context, net) - if network: self._create_fixed_ips(context, network['id']) else: - raise ValueError(_('Network with cidr %s already exists') % - cidr) + msg = "Error creating cidr %s, see log" + raise ValueError(_(msg) % str(cidr)) @property def _bottom_reserved_ips(self): # pylint: disable=R0201 diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 1f5305b1d..424b5f098 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -361,21 +361,36 @@ class CommonNetworkTestCase(test.TestCase): 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/8'}]) + 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_cidr_already_used(self): -# mockany = self.mox.CreateMockAnything() -# manager = self.FakeNetworkManager() -# self.mox.StubOutWithMock(manager.db, 'network_get_by_cidr') -# manager.db.network_get_by_cidr(mox.IgnoreArg(), '192.168.0.0/24')\ -# .AndReturn(mockany) -# 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(self): + cidr = '192.168.0.0/24' + manager = self.FakeNetworkManager() + self.mox.StubOutWithMock(manager.db, 'network_create_safe') + ignore = mox.IgnoreArg() + fakecidr= {'id': 1, 'cidr': cidr} + manager.db.network_create_safe(ignore, ignore).AndReturn(fakecidr) + self.mox.StubOutWithMock(manager, '_create_fixed_ips') + manager._create_fixed_ips(ignore, ignore).AndReturn('foo') + self.mox.ReplayAll() + args = [None, 'foo', cidr, None, 1, 256, 'fd00::/48', None, None, + None] + # making it to the end, without err, is enough validation + 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) -- cgit From 51f0cbf9221b461eb92beae2497e871bf2a7f45f Mon Sep 17 00:00:00 2001 From: John Tran Date: Tue, 2 Aug 2011 10:06:22 -0700 Subject: refactored tests --- nova/network/manager.py | 13 ++++++------- nova/tests/test_network.py | 27 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 8b86bc036..b21667def 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -666,9 +666,8 @@ class NetworkManager(manager.SchedulerDependentManager): significant_bits_v6 = 64 network_size_v6 = 1 << 64 - count = 0 - for req_cidr in req_cidrs: - count = count + 1 + for index in range(len(req_cidrs)): + req_cidr = req_cidrs[index] net = {} net['bridge'] = bridge net['bridge_interface'] = bridge_interface @@ -680,12 +679,12 @@ class NetworkManager(manager.SchedulerDependentManager): net['broadcast'] = str(req_cidr.broadcast) net['dhcp_start'] = str(req_cidr[2]) if num_networks > 1: - net['label'] = '%s_%d' % (label, count) + net['label'] = '%s_%d' % (label, index) else: net['label'] = label if FLAGS.use_ipv6: - start_v6 = count * network_size_v6 + start_v6 = index * network_size_v6 cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], significant_bits_v6) net['cidr_v6'] = cidr_v6 @@ -701,7 +700,7 @@ class NetworkManager(manager.SchedulerDependentManager): # this bit here is for vlan-manager del net['dns1'] del net['dns2'] - vlan = kwargs['vlan_start'] + count + vlan = kwargs['vlan_start'] + index net['vpn_private_address'] = str(req_cidr[2]) net['dhcp_start'] = str(req_cidr[3]) net['vlan'] = vlan @@ -709,7 +708,7 @@ class NetworkManager(manager.SchedulerDependentManager): # NOTE(vish): This makes ports unique accross the cloud, a more # robust solution would be to make them uniq per ip - net['vpn_public_port'] = kwargs['vpn_start'] + count + net['vpn_public_port'] = kwargs['vpn_start'] + index network = self.db.network_create_safe(context, net) if network: diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 424b5f098..9c808c6db 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -254,7 +254,9 @@ class CommonNetworkTestCase(test.TestCase): raise exception.NetworkNotFoundForCidr() def network_create_safe(self, context, net): - return {'foo': 'bar'} + fakenet = {} + fakenet['id'] = 999 + return fakenet def network_get_all(self, context): raise exception.NoNetworksFound() @@ -266,6 +268,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') @@ -372,16 +377,11 @@ class CommonNetworkTestCase(test.TestCase): def test_create_networks(self): cidr = '192.168.0.0/24' manager = self.FakeNetworkManager() - self.mox.StubOutWithMock(manager.db, 'network_create_safe') - ignore = mox.IgnoreArg() - fakecidr= {'id': 1, 'cidr': cidr} - manager.db.network_create_safe(ignore, ignore).AndReturn(fakecidr) - self.mox.StubOutWithMock(manager, '_create_fixed_ips') - manager._create_fixed_ips(ignore, ignore).AndReturn('foo') - self.mox.ReplayAll() + self.stubs.Set(manager, '_create_fixed_ips', + self.fake_create_fixed_ips) args = [None, 'foo', cidr, None, 1, 256, 'fd00::/48', None, None, None] - # making it to the end, without err, is enough validation + result = manager.create_networks(*args) self.assertEqual(manager.create_networks(*args), None) def test_create_networks_cidr_already_used(self): @@ -394,3 +394,12 @@ class CommonNetworkTestCase(test.TestCase): 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) -- cgit From f06dee2b82bd658a57736d94974f431976085400 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 19:02:40 +0000 Subject: Fixed several typos --- nova/api/openstack/contrib/hosts.py | 5 ++--- nova/compute/manager.py | 2 +- nova/tests/test_hosts.py | 2 +- tools/pip-requires | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 94fba910c..2d9d494b9 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -85,7 +85,7 @@ class HostController(object): # 'startup' option to start up a host, but this is not # technically feasible now, as we run the host on the # XenServer box. - msg = _("Host startup on XenServer is not supported.")) + msg = _("Host startup on XenServer is not supported.") raise webob.exc.HTTPBadRequest(explanation=msg) elif val in ("reboot", "shutdown"): return self._set_powerstate(req, id, val) @@ -106,8 +106,7 @@ class HostController(object): return {"host": host, "status": result} def _set_powerstate(self, req, host, state): - """Reboots or shuts down the host. - """ + """Reboots or shuts down the host.""" context = req.environ['nova.context'] result = self.compute_api.set_host_powerstate(context, host=host, state=state) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d0f9a81f4..cd05f3f24 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1,4 +1,4 @@ -: tabstop=4 shiftwidth=4 softtabstop=4 +#: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index ad057f429..9f54d5ec7 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -52,7 +52,7 @@ def stub_set_host_powerstate(context, host, state): # We'll simulate success and failure by assuming # that 'host_c1' always succeeds, and 'host_c2' # always fails - return state if host == "host_c1" else "running" + return state if host == "host_c1" else "running" class FakeRequest(object): diff --git a/tools/pip-requires b/tools/pip-requires index dec93c351..1e8f7e1d2 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -33,3 +33,4 @@ coverage nosexcover GitPython paramiko +xattr -- cgit From 4887331a0d972b4d9e1fbd365c36edf1d7b076ec Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 15:10:09 -0400 Subject: fix pylint errors --- nova/tests/scheduler/test_scheduler.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 6a56a57db..a515636a3 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -51,10 +51,10 @@ FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' class TestDriver(driver.Scheduler): """Scheduler Driver for Tests""" - def schedule(context, topic, *args, **kwargs): + def schedule(self, context, topic, *args, **kwargs): return 'fallback_host' - def schedule_named_method(context, topic, num): + def schedule_named_method(self, context, topic, num): return 'named_host' @@ -301,7 +301,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 +324,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 +343,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 +366,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 +389,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_wont_sechedule_if_specified_host_is_down(self): + def test_wont_sechedule_if_specified_host_is_down_no_queue(self): compute1 = service.Service('host1', 'nova-compute', 'compute', @@ -408,7 +408,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 +423,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 +456,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 +477,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') @@ -993,7 +993,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') @@ -1081,10 +1081,10 @@ 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') -- cgit From 378571665538fb5c4667a9d068678378ddf8526f Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 15:19:00 -0400 Subject: fix pylint errors --- nova/db/sqlalchemy/api.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 36614df1f..ad935fa44 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3202,9 +3202,8 @@ def instance_metadata_update_or_create(context, instance_id, metadata): meta_ref = None for key, value in metadata.iteritems(): try: - meta_ref = instance_metadata_get_item(context, instance_id, key, - session) - except: + meta_ref = instance_metadata_get_item(context, instance_id, key) + except exception.InstanceMetadataNotFound, e: meta_ref = models.InstanceMetadata() meta_ref.update({"key": key, "value": value, "instance_id": instance_id, @@ -3322,11 +3321,9 @@ 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) - except: + spec_ref = instance_type_extra_specs_get_item( + context, instance_type_id, key) + except exception.InstanceTypeExtraSpecsNotFound, e: spec_ref = models.InstanceTypeExtraSpecs() spec_ref.update({"key": key, "value": value, "instance_type_id": instance_type_id, -- cgit From d1891d2dd18a14535ec22a0363fd8234a01dbb8c Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 15:30:27 -0400 Subject: remove unused parameter --- nova/virt/hyperv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index c26fe108b..315f635c7 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -373,7 +373,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): """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) -- cgit From 0079cc3536811baf9ed6fa0cedbd5863c602644b Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 19:50:54 +0000 Subject: Removed duplicate xattr from pip-requires --- tools/pip-requires | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index e6ef3996c..23e707034 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -33,4 +33,3 @@ coverage nosexcover GitPython paramiko -xattr -- cgit From 614895b6c93904888aab99d1507d94271d763c04 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 16:08:17 -0400 Subject: fix missing method call and add failing test --- nova/tests/test_xenapi.py | 9 +++++++++ nova/tests/xenapi/stubs.py | 4 ++++ nova/virt/xenapi_conn.py | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 6b7d5df76..9032c82ab 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -669,6 +669,14 @@ class XenAPIVMTestCase(test.TestCase): self.conn.spawn(instance, network_info) return instance + def test_revert_migration(self): + instance = self._create_instance() + stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) + stubs.stubout_loopingcall_start(self.stubs) + conn = xenapi_conn.get_connection(False) + conn.revert_migration(instance) + + class XenAPIDiffieHellmanTestCase(test.TestCase): """Unit tests for Diffie-Hellman code.""" @@ -842,6 +850,7 @@ class XenAPIMigrateInstance(test.TestCase): network_info, resize_instance=False) + class XenAPIImageTypeTestCase(test.TestCase): """Test ImageType class.""" diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 66c79d465..195e4d038 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -230,10 +230,14 @@ def stub_out_vm_methods(stubs): def fake_spawn_rescue(self, inst): inst._rescue = False + def fake_revert_migration(self, inst): + pass + stubs.Set(vmops.VMOps, "_shutdown", fake_shutdown) stubs.Set(vmops.VMOps, "_acquire_bootlock", fake_acquire_bootlock) stubs.Set(vmops.VMOps, "_release_bootlock", fake_release_bootlock) stubs.Set(vmops.VMOps, "spawn_rescue", fake_spawn_rescue) + stubs.Set(vmops.VMOps, "revert_migration", fake_revert_migration) class FakeSessionForVolumeTests(fake.SessionBase): diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index a3d0abf9f..042bf2b02 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -190,7 +190,7 @@ class XenAPIConnection(driver.ComputeDriver): def revert_migration(self, instance): """Reverts a resize, powering back on the instance""" - self._vmops.revert_resize(instance) + self._vmops.revert_migration(instance) def finish_migration(self, instance, disk_info, network_info, resize_instance=False): -- cgit From 4c07cac5b0e79f3911fbcc392c3f9e7f07333968 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 20:39:14 +0000 Subject: Fixed a missing space. --- nova/compute/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index b83c4831c..5ea7a1c01 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -995,7 +995,7 @@ class API(base.Base): return self._call_compute_message("set_host_enabled", context, instance_id=None, host=host, params={"enabled": enabled}) - def set_host_powerstate(self, context, host, state): + def set_host_powerstate(self, context, host, state): """Reboots or shuts down the host.""" return self._call_compute_message("set_host_powerstate", context, instance_id=None, host=host, params={"state": state}) -- cgit From b5a4a8d6cc68fec2dab205c5da494bce69241c33 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 17:05:10 -0400 Subject: fix pep8 complaints --- nova/tests/test_xenapi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 9032c82ab..8e3c88f8e 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -677,7 +677,6 @@ class XenAPIVMTestCase(test.TestCase): conn.revert_migration(instance) - class XenAPIDiffieHellmanTestCase(test.TestCase): """Unit tests for Diffie-Hellman code.""" def setUp(self): @@ -850,7 +849,6 @@ class XenAPIMigrateInstance(test.TestCase): network_info, resize_instance=False) - class XenAPIImageTypeTestCase(test.TestCase): """Test ImageType class.""" -- cgit From 9bac11d9ff9933460f7ddf1bd1dd77d4d3397e47 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 17:05:21 -0400 Subject: fix failing tests --- nova/db/sqlalchemy/api.py | 17 +++++++++++------ nova/tests/scheduler/test_scheduler.py | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ad935fa44..60220cd68 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3177,8 +3177,9 @@ def instance_metadata_delete_all(context, instance_id): @require_context @require_instance_exists -def instance_metadata_get_item(context, instance_id, key): - session = get_session() +def instance_metadata_get_item(context, instance_id, key, session=None): + if not session: + session = get_session() meta_result = session.query(models.InstanceMetadata).\ filter_by(instance_id=instance_id).\ @@ -3202,7 +3203,8 @@ def instance_metadata_update_or_create(context, instance_id, metadata): meta_ref = None for key, value in metadata.iteritems(): try: - meta_ref = instance_metadata_get_item(context, instance_id, key) + meta_ref = instance_metadata_get_item( + context, instance_id, key, session) except exception.InstanceMetadataNotFound, e: meta_ref = models.InstanceMetadata() meta_ref.update({"key": key, "value": value, @@ -3298,8 +3300,11 @@ def instance_type_extra_specs_delete(context, instance_type_id, key): @require_context -def instance_type_extra_specs_get_item(context, instance_type_id, key): - session = get_session() +def instance_type_extra_specs_get_item(context, instance_type_id, key, + session=None): + + if not session: + session = get_session() spec_result = session.query(models.InstanceTypeExtraSpecs).\ filter_by(instance_type_id=instance_type_id).\ @@ -3322,7 +3327,7 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id, for key, value in specs.iteritems(): try: spec_ref = instance_type_extra_specs_get_item( - context, instance_type_id, key) + 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/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index a515636a3..0d7634d0d 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -51,10 +51,10 @@ FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' class TestDriver(driver.Scheduler): """Scheduler Driver for Tests""" - def schedule(self, context, topic, *args, **kwargs): + def schedule(context, topic, *args, **kwargs): return 'fallback_host' - def schedule_named_method(self, context, topic, num): + def schedule_named_method(context, topic, num): return 'named_host' -- cgit From 1d3d1d5fb552f2dc80c39ad15d89d59bfc7f873a Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 21:11:12 +0000 Subject: Minor test fixes --- nova/api/openstack/contrib/hosts.py | 4 ++-- nova/tests/test_hosts.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index e9f82c75b..c90a889d5 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -78,7 +78,7 @@ class HostController(object): else: explanation = _("Invalid status: '%s'") % raw_val raise webob.exc.HTTPBadRequest(explanation=explanation) - elif key == "powerstate": + elif key == "power_state": if val == "startup": # The only valid values for 'state' are 'reboot' or # 'shutdown'. For completeness' sake there is the @@ -113,7 +113,7 @@ class HostController(object): context = req.environ['nova.context'] result = self.compute_api.set_host_powerstate(context, host=host, state=state) - return {"host": host, "powerstate": result} + return {"host": host, "power_state": result} class Hosts(extensions.ExtensionDescriptor): diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 1c7c21a1a..d8f90a109 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -49,10 +49,7 @@ def stub_set_host_enabled(context, host, enabled): def stub_set_host_powerstate(context, host, state): - # We'll simulate success and failure by assuming - # that 'host_c1' always succeeds, and 'host_c2' - # always fails - return state if host == "host_c1" else "running" + return state class FakeRequest(object): @@ -96,12 +93,14 @@ class HostTestCase(test.TestCase): result_c2 = self.controller.update(self.req, "host_c2", body=en_body) self.assertEqual(result_c2["status"], "disabled") - def test_power_state(self): + def test_host_power_state(self): en_body = {"power_state": "reboot"} result_c1 = self.controller.update(self.req, "host_c1", body=en_body) self.assertEqual(result_c1["power_state"], "reboot") + # Test invalid power_state + en_body = {"power_state": "invalid"} self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c2", body=en_body) + self.req, "host_c1", body=en_body) def test_bad_power_state_value(self): bad_body = {"power_state": "bad"} -- cgit From 8774c69c4a8d97ab2dac32c0f52e6963543a46f1 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 2 Aug 2011 17:11:53 -0400 Subject: raise correct error --- nova/scheduler/zone_aware_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index d99d7214c..8440421e1 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -264,7 +264,7 @@ class ZoneAwareScheduler(driver.Scheduler): """ if topic != "compute": - raise NotImplemented(_("Zone Aware Scheduler only understands " + raise NotImplementedError(_("Zone Aware Scheduler only understands " "Compute nodes (for now)")) num_instances = request_spec.get('num_instances', 1) -- cgit From 14e8257af4624fa5b056a1b0e94d1b584e080ce9 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Tue, 2 Aug 2011 22:48:47 +0000 Subject: Added check for --allow-admin-api to the host API extension code. --- nova/api/openstack/contrib/hosts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index c90a889d5..78ba66771 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -133,6 +133,11 @@ class Hosts(extensions.ExtensionDescriptor): return "2011-06-29T00:00:00+00:00" def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', HostController(), - collection_actions={'update': 'PUT'}, member_actions={})] + resources = [] + # If we are not in an admin env, don't add the resource. Regular users + # shouldn't have access to the host. + if FLAGS.allow_admin_api: + resources = [extensions.ResourceExtension('os-hosts', + HostController(), collection_actions={'update': 'PUT'}, + member_actions={})] return resources -- cgit From a0ec6a6aa5ebdde1d099c5f6c03cf1dbd28441fa Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Wed, 3 Aug 2011 00:52:15 +0000 Subject: Removed duplicate methods created by previous merge. --- nova/compute/manager.py | 8 +------ nova/virt/driver.py | 4 ---- nova/virt/fake.py | 4 ---- nova/virt/hyperv.py | 4 ---- nova/virt/libvirt/connection.py | 4 ---- nova/virt/vmwareapi_conn.py | 4 ---- nova/virt/xenapi/vmops.py | 11 --------- nova/virt/xenapi_conn.py | 4 ---- .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 27 ---------------------- 9 files changed, 1 insertion(+), 69 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 37b920074..3c89042c8 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1,4 +1,4 @@ -#: tabstop=4 shiftwidth=4 softtabstop=4 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. @@ -939,12 +939,6 @@ class ComputeManager(manager.SchedulerDependentManager): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def set_power_state(self, context, instance_id=None, host=None, - power_state=None): - """Turns the specified host on/off, or reboots the host.""" - return self.driver.set_power_state(host, power_state) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for an instance on this host.""" diff --git a/nova/virt/driver.py b/nova/virt/driver.py index b32ed7c54..bbb17480d 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -259,10 +259,6 @@ class ComputeDriver(object): """Sets the specified host's ability to accept new instances.""" raise NotImplementedError() - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - raise NotImplementedError() - def plug_vifs(self, instance, network_info): """Plugs in VIFs to networks.""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 135b88ab8..888dca220 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -518,7 +518,3 @@ class FakeConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 553f85265..119def38b 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -505,7 +505,3 @@ class HyperVConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 150478edf..46f3eef4b 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1586,7 +1586,3 @@ class LibvirtConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 6f68fd726..af547821f 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -198,10 +198,6 @@ class VMWareESXConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" pass - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass - def plug_vifs(self, instance, network_info): """Plugs in VIFs to networks.""" self._vmops.plug_vifs(instance, network_info) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 194c33d54..a90ae0128 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1042,17 +1042,6 @@ class VMOps(object): return xenapi_resp.details[-1] return resp["status"] - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - args = {"power_state": power_state} - xenapi_resp = self._call_xenhost("set_power_state", args) - try: - resp = json.loads(xenapi_resp) - except TypeError as e: - # Already logged; return the message - return xenapi_resp.details[-1] - return resp["power_state"] - def _call_xenhost(self, method, arg_dict): """There will be several methods that will need this general handling for interacting with the xenhost plugin, so this abstracts diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 468e12696..5962b4ff8 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -339,10 +339,6 @@ class XenAPIConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" return self._vmops.set_host_enabled(host, enabled) - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - return self._vmops.set_power_state(host, power_state) - class XenAPISession(object): """The session to invoke XenAPI SDK calls""" diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 5169aeb12..c29d57717 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -142,33 +142,6 @@ def host_start(self, arg_dict): return _powerstate("startup") -@jsonify -def set_power_state(self, arg_dict): - """Reboots or powers off this host. Ideally, we would also like to be - able to power *on* a host, but right now this is not technically - feasible. - """ - power_state = arg_dict.get("power_state") - if power_state is None: - raise pluginlib.PluginError( - _("Missing 'power_state' argument to set_power_state")) - # 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", "on": "xe host-power-on", -# "off": "xe host-shutdown"} -# result = _run_command(cmds[power_state]) -# # Should be empty string -# if result: -# raise pluginlib.PluginError(result) - return {"power_state": power_state} - - @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status -- cgit From 7b69ef4fe1e4aabcf44789455b96492b168ad6f5 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Wed, 3 Aug 2011 01:32:08 +0000 Subject: Removed trailing whitespace that somehow made it into trunk. --- nova/api/openstack/create_instance_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index a2d18d37e..333994fcc 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -92,7 +92,7 @@ class CreateInstanceHelper(object): image_href = self.controller._image_ref_from_req_data(body) # If the image href was generated by nova api, strip image_href # down to an id and use the default glance connection params - + if str(image_href).startswith(req.application_url): image_href = image_href.split('/').pop() try: -- cgit From 44964f41ed591c8ece72dbc36f5d668d2e279274 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Tue, 2 Aug 2011 22:22:34 -0500 Subject: renaming test_skip_unless_env_foo_exists() --- nova/tests/test_skip_examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index dfbaac74b..b7979d34b 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -31,7 +31,7 @@ class ExampleSkipTestCase(test.TestCase): def test_skip_if_env_user_exists(self): x = 1 - @test.skip_unless(os.getenv("BLAH"), - "Skipping -- Environment variable BLAH does not exist") - def test_skip_unless_env_blah_exists(self): + @test.skip_unless(os.getenv("FOO"), + "Skipping -- Environment variable FOO does not exist") + def test_skip_unless_env_foo_exists(self): x = 1 -- cgit From 836c394def93a8589bcb7387862390b24b8fdab8 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Tue, 2 Aug 2011 22:25:23 -0500 Subject: Resolved pep8 errors --- nova/api/openstack/create_instance_helper.py | 2 +- nova/test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index a2d18d37e..333994fcc 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -92,7 +92,7 @@ class CreateInstanceHelper(object): image_href = self.controller._image_ref_from_req_data(body) # If the image href was generated by nova api, strip image_href # down to an id and use the default glance connection params - + if str(image_href).startswith(req.application_url): image_href = image_href.split('/').pop() try: diff --git a/nova/test.py b/nova/test.py index afc118100..f1b4050f9 100644 --- a/nova/test.py +++ b/nova/test.py @@ -67,6 +67,7 @@ class skip_test(object): _skipper.__doc__ = func.__doc__ return _skipper + class skip_if(object): """Decorator that skips a test if contition is true.""" def __init__(self, condition, msg): @@ -84,7 +85,7 @@ class skip_if(object): class skip_unless(object): - """Decorator that skips a test if condition is false.""" + """Decorator that skips a test if condition is not true.""" def __init__(self, condition, msg): self.condition = condition self.message = msg -- cgit From d543b9914506b39defbf94ebc63eb95f0d58a8cd Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Wed, 3 Aug 2011 09:32:24 -0500 Subject: Removed dependancy on os.getenv. Test cases now raise Exception if they are not properly skipped. --- nova/tests/test_skip_examples.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index b7979d34b..e41cd848d 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -16,22 +16,18 @@ # License for the specific language governing permissions and limitations # under the License. -import os - from nova import test class ExampleSkipTestCase(test.TestCase): - @test.skip_test("testing skipping") + @test.skip_test("Example usage of @test.skip_test()") def test_skip_test(self): - x = 1 + raise Exception("skip_test failed to work properly.") - @test.skip_if(os.getenv("USER"), - "Skiping -- Environment variable USER exists") + @test.skip_if(True, "Example usage of @test.skip_if()") def test_skip_if_env_user_exists(self): - x = 1 + raise Exception("skip_if failed to work properly.") - @test.skip_unless(os.getenv("FOO"), - "Skipping -- Environment variable FOO does not exist") + @test.skip_unless(False, "Example usage of @test.skip_unless()") def test_skip_unless_env_foo_exists(self): - x = 1 + raise Exception("skip_unless failed to work properly.") -- cgit From c237b762381856b1041be06debf0deed4ae4e2a8 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Wed, 3 Aug 2011 10:44:09 -0500 Subject: replaced raise Exception with self.fail() --- nova/tests/test_skip_examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index e41cd848d..41c3b1b5f 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -22,12 +22,12 @@ from nova import test class ExampleSkipTestCase(test.TestCase): @test.skip_test("Example usage of @test.skip_test()") def test_skip_test(self): - raise Exception("skip_test failed to work properly.") + self.fail("skip_test failed to work properly.") @test.skip_if(True, "Example usage of @test.skip_if()") def test_skip_if_env_user_exists(self): - raise Exception("skip_if failed to work properly.") + self.fail("skip_if failed to work properly.") @test.skip_unless(False, "Example usage of @test.skip_unless()") def test_skip_unless_env_foo_exists(self): - raise Exception("skip_unless failed to work properly.") + self.fail("skip_unless failed to work properly.") -- cgit From e34a4c2feb7b2abef806ed720e1533e2e0fb94ef Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:05:58 -0400 Subject: remove bit-rotted code. fixes #820062 --- bin/nova-import-canonical-imagestore | 110 ----------------------------------- 1 file changed, 110 deletions(-) delete mode 100755 bin/nova-import-canonical-imagestore 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() -- cgit From 916e0ce0997bdf3135684865eff6fadcda95752b Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:13:53 -0400 Subject: remove unused imports --- nova/api/openstack/create_instance_helper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 53e814cd5..e4b897cd6 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 -- cgit From 91e16e057f083d1a0b8dffaa00b5979c12c23edc Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:30:45 -0400 Subject: fix potential runtime exception The exception could occur if a client were to create an APIRouter object. The fix relies on more established OOP patterns. --- nova/api/openstack/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 868b98a31..8d7f7a405 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -81,7 +81,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 @@ -147,7 +150,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'}, @@ -163,7 +166,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() mapper.resource("image_meta", "metadata", controller=image_metadata_controller, -- cgit From a049e682b54525d2814f20b08ca64320040675b8 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:46:02 -0400 Subject: fix undefined variable error --- nova/scheduler/least_cost.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) -- cgit From 933587d821797037bb765e9d0bd5ea063c07785b Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:47:35 -0400 Subject: fix duplicate function name --- nova/tests/test_instance_types_extra_specs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) -- cgit From 98307324d7b1cce2f7312d1195c9674f0e0323b6 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:49:14 -0400 Subject: use correct exception name --- nova/virt/vmwareapi/vif.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" -- cgit From 4aada43cc0d1a3cb32094d6d6be1eb662f01d063 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 14:51:47 -0400 Subject: use an existing exception --- nova/virt/xenapi/vm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 60ef0df43..8a715af35 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -794,7 +794,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()) -- cgit From 6203ae5d5b285d62bd2c1bf1f2f11d3b64b53511 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 15:05:57 -0400 Subject: pep8 fixes --- nova/scheduler/zone_aware_scheduler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 8440421e1..297922570 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -264,8 +264,8 @@ class ZoneAwareScheduler(driver.Scheduler): """ if topic != "compute": - raise NotImplementedError(_("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'] -- cgit From a5add8b30c96ad1d83eebcd9756b0f358c9cb725 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 15:16:38 -0400 Subject: follow convention when raising exceptions --- nova/api/openstack/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 8d7f7a405..0af39c2df 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -82,7 +82,7 @@ class APIRouter(base_wsgi.Router): super(APIRouter, self).__init__(mapper) def _setup_routes(self, mapper): - raise NotImplementedError("you must implement _setup_routes") + raise NotImplementedError(_("You must implement _setup_routes.")) def _setup_base_routes(self, mapper, version): """Routes common to all versions.""" -- cgit From e7cb0c70384fc247a7f330ed7c4db5a3b0de814c Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 16:45:46 -0400 Subject: align multi-line string --- nova/scheduler/zone_aware_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index 297922570..9ac0f94ed 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -265,7 +265,7 @@ class ZoneAwareScheduler(driver.Scheduler): if topic != "compute": raise NotImplementedError(_("Zone Aware Scheduler only understands" - " Compute nodes (for now)")) + " Compute nodes (for now)")) num_instances = request_spec.get('num_instances', 1) instance_type = request_spec['instance_type'] -- cgit From 8b0ebd90edb47be344b355334ae16d6b7715087d Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 17:02:45 -0400 Subject: all subclasses of ComputeDriver should fully implement the interface of the destroy method. --- nova/virt/fake.py | 2 +- nova/virt/hyperv.py | 2 +- nova/virt/vmwareapi_conn.py | 2 +- nova/virt/xenapi_conn.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 2898f23a4..b7396d61c 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -293,7 +293,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] diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 315f635c7..1e4cce10c 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -373,7 +373,7 @@ class HyperVConnection(driver.ComputeDriver): raise exception.InstanceNotFound(instance_id=instance.id) self._set_vm_state(instance.name, 'Reboot') - def destroy(self, instance): + 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) diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index ce57847b2..10d80c0a0 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -136,7 +136,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) diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 042bf2b02..571638496 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -216,7 +216,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) -- cgit From 634fe881223a7ea8e04b3054b39724207153be5b Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Wed, 3 Aug 2011 15:03:34 -0700 Subject: Initial version --- nova/api/openstack/contrib/security_groups.py | 489 +++++++++++++ nova/api/openstack/extensions.py | 11 +- nova/db/api.py | 5 + nova/exception.py | 4 + .../api/openstack/contrib/test_security_groups.py | 761 +++++++++++++++++++++ nova/tests/api/openstack/test_extensions.py | 6 +- 6 files changed, 1271 insertions(+), 5 deletions(-) create mode 100644 nova/api/openstack/contrib/security_groups.py create mode 100644 nova/tests/api/openstack/contrib/test_security_groups.py diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py new file mode 100644 index 000000000..39f1959e0 --- /dev/null +++ b/nova/api/openstack/contrib/security_groups.py @@ -0,0 +1,489 @@ +# 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): + r = {} + r['id'] = rule.id + r['parent_group_id'] = rule.parent_group_id + r['ip_protocol'] = rule.protocol + r['from_port'] = rule.from_port + r['to_port'] = rule.to_port + r['group'] = {} + r['ip_range'] = {} + if rule.group_id: + source_group = db.security_group_get(context, rule.group_id) + r['group'] = {'name': source_group.name, + 'tenant_id': source_group.project_id} + else: + r['ip_range'] = {'cidr': rule.cidr} + return r + + def _format_security_group(self, context, group): + g = {} + g['id'] = group.id + g['description'] = group.description + g['name'] = group.name + g['tenant_id'] = group.project_id + g['rules'] = [] + for rule in group.rules: + r = self._format_security_group_rule(context, rule) + g['rules'] += [r] + return g + + 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.NotFound 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) + if context.is_admin: + groups = db.security_group_get_all(context) + else: + 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_name(group_name) + self._validate_security_group_description(group_description) + + 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_name(self, value): + if value is None: + msg = _("Security group name is mandatory") + raise exc.HTTPBadRequest(explanation=msg) + + if not isinstance(value, basestring): + msg = _("Security group name is not a string or unicode") + raise exc.HTTPBadRequest(explanation=msg) + + if value.strip() == '': + msg = _("Security group name is an empty string") + raise exc.HTTPBadRequest(explanation=msg) + + if len(value.strip()) > 255: + msg = _("Security group name should not be greater " + "than 255 characters") + raise exc.HTTPBadRequest(explanation=msg) + + def _validate_security_group_description(self, value): + if value is None: + msg = _("Security group description is mandatory") + raise exc.HTTPBadRequest(explanation=msg) + + if not isinstance(value, basestring): + msg = _("Security group description is not a string or unicode") + raise exc.HTTPBadRequest(explanation=msg) + + if value.strip() == '': + msg = _("Security group description is an empty string") + raise exc.HTTPBadRequest(explanation=msg) + + if len(value.strip()) > 255: + msg = _("Security group description should not be " + "greater than 255 characters") + 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) + if not security_group: + raise exception.SecurityGroupNotFound(security_group_id=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('security_groups', + controller=SecurityGroupController(), + deserializer=deserializer, + serializer=serializer) + + resources.append(res) + + body_deserializers = { + 'application/xml': SecurityGroupRulesXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('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/extensions.py b/nova/api/openstack/extensions.py index cc889703e..15b3cfae4 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -265,9 +265,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,12 +464,15 @@ 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={}, member_actions={}, + deserializer=None, serializer=None): 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): diff --git a/nova/db/api.py b/nova/db/api.py index 47308bdba..e189dfc16 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1096,6 +1096,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) + + ################### diff --git a/nova/exception.py b/nova/exception.py index 68e6ac937..535410bd6 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -198,6 +198,10 @@ class InvalidContentType(Invalid): message = _("Invalid content type %(content_type)s.") +class InvalidCidr(Invalid): + message = _("Invalid cidr %(cidr)s.") + + class InstanceNotRunning(Invalid): message = _("Instance %(instance_id)s is not running.") 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..eaef413a7 --- /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/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([ + '', + '' % (sg['name'])]) + if 'description' in sg: + body_parts.append('%s' + % sg['description']) + body_parts.append('') + return ''.join(body_parts) + + def _get_create_request_xml(self, body_dict): + req = webob.Request.blank('/v1.1/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/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/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/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/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/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/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/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/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/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 = """ + + 12 + 22 + 22 + + tcp + 10.0.0.0/24 +""" + 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 = """ + + 12 + 22 + 22 + + 10.0.0.0/24 +""" + 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 = """ + + test +""" + 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 = """ + +""" + 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 = """ + +test +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "security_group": { + "description": "test", + }, + } + self.assertEquals(request['body'], expected) diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 47c37225c..98a3a72d4 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -98,7 +98,7 @@ 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", "Multinic", "SecurityGroups", "Volumes"]) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [ @@ -109,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': [] }, ) @@ -145,7 +145,7 @@ class ExtensionControllerTest(test.TestCase): # Make sure we have all the extensions. exts = root.findall('{0}extension'.format(NS)) - self.assertEqual(len(exts), 6) + self.assertEqual(len(exts), 7) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [x for x in exts if x.get('alias') == 'FOXNSOX'] -- cgit From b94eb7bf4fd71a23cacc20def2b5a47dad053b56 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Wed, 3 Aug 2011 16:42:23 -0700 Subject: Remove whitespaces from name and description before creating security group --- nova/api/openstack/contrib/security_groups.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index 39f1959e0..7da046b8f 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -140,6 +140,8 @@ class SecurityGroupController(object): self._validate_security_group_name(group_name) self._validate_security_group_description(group_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) -- cgit From 23f6ec84a93638e880f60bec00db8587b9e3e8d8 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Wed, 3 Aug 2011 20:26:37 -0400 Subject: fix pylint W0102 errors. --- nova/api/openstack/extensions.py | 6 +++++- nova/compute/api.py | 19 +++++++++++++++---- nova/objectstore/s3server.py | 5 ++++- nova/scheduler/manager.py | 4 +++- nova/tests/api/openstack/test_limits.py | 5 ++++- nova/tests/test_auth.py | 7 ++++++- nova/tests/test_compute.py | 10 ++++++++-- nova/tests/test_libvirt.py | 5 ++++- nova/virt/vmwareapi/io_util.py | 5 ++++- nova/virt/vmwareapi/vim_util.py | 10 ++++++++-- nova/virt/vmwareapi/vmware_images.py | 6 +++++- 11 files changed, 66 insertions(+), 16 deletions(-) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index cc889703e..b070dd61a 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -460,7 +460,11 @@ 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): + if not collection_actions: + collection_actions = {} + if not member_actions: + member_actions = {} self.collection = collection self.controller = controller self.parent = parent diff --git a/nova/compute/api.py b/nova/compute/api.py index aae16d1da..fe62b50ce 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -121,8 +121,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 +150,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 +162,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) @@ -395,12 +399,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, @@ -424,7 +432,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 +447,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, 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('\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/scheduler/manager.py b/nova/scheduler/manager.py index 749d66cad..c8b16b622 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -69,8 +69,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 capability: + capability = {} self.zone_manager.update_service_capabilities(service_name, host, capabilities) diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 6c3d531e3..1dc3c3a17 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 diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py index 7c0f783bb..0fb2fdcc6 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_compute.py b/nova/tests/test_compute.py index 879e4b9cb..9be349d14 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -73,8 +73,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 +90,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' diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index c5c3151dc..aa77258cb 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -1162,8 +1162,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' 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/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) -- cgit From 27392e9412c7042b67947dbc103704d121d60e12 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Wed, 3 Aug 2011 20:48:17 -0500 Subject: Conditionals were not actually runing the tests when they were supposed to. Renamed example testcases --- nova/test.py | 2 ++ nova/tests/test_skip_examples.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nova/test.py b/nova/test.py index f1b4050f9..97cd3e0d0 100644 --- a/nova/test.py +++ b/nova/test.py @@ -79,6 +79,7 @@ class skip_if(object): """Wrapped skipper function.""" if self.condition: raise nose.SkipTest(self.message) + func(*args, **kw) _skipper.__name__ = func.__name__ _skipper.__doc__ = func.__doc__ return _skipper @@ -95,6 +96,7 @@ class skip_unless(object): """Wrapped skipper function.""" if not self.condition: raise nose.SkipTest(self.message) + func(*args, **kw) _skipper.__name__ = func.__name__ _skipper.__doc__ = func.__doc__ return _skipper diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index 41c3b1b5f..0ae3576f7 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -21,13 +21,13 @@ from nova import test class ExampleSkipTestCase(test.TestCase): @test.skip_test("Example usage of @test.skip_test()") - def test_skip_test(self): + 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_env_user_exists(self): + 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_env_foo_exists(self): + def test_skip_unless_example(self): self.fail("skip_unless failed to work properly.") -- cgit From 144720ce87b3f0f469ed748780af55e32babc821 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Thu, 4 Aug 2011 09:29:43 -0500 Subject: Added in tests that verify tests are skipped appropriately. --- nova/tests/test_skip_examples.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index 0ae3576f7..033f1ea2e 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -20,6 +20,8 @@ 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.") @@ -31,3 +33,15 @@ class ExampleSkipTestCase(test.TestCase): @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") -- cgit From 194f0e4909490c4b626bd211c46121ae37db20dd Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 4 Aug 2011 10:43:42 -0700 Subject: uses 2.6.0 novaclient (OS API 1.1 support) --- nova/compute/manager.py | 5 +++-- nova/scheduler/api.py | 13 +++++++------ nova/scheduler/zone_aware_scheduler.py | 9 +++++---- nova/scheduler/zone_manager.py | 7 ++++--- nova/tests/scheduler/test_scheduler.py | 12 +++++++----- nova/tests/test_zones.py | 1 - tools/pip-requires | 2 +- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 94bac9be4..843f6d490 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -167,10 +167,11 @@ class ComputeManager(manager.SchedulerDependentManager): 'nova-compute restart.'), locals()) self.reboot_instance(context, instance['id']) elif drv_state == power_state.RUNNING: - try: # Hyper-V and VMWareAPI drivers will raise and exception + try: # Hyper-V and VMWareAPI drivers will raise and exception self.driver.ensure_filtering_rules_for_instance(instance) except NotImplementedError: - LOG.warning(_('Hypervisor driver does not support firewall rules')) + LOG.warning( + _('Hypervisor driver does not support firewall rules')) def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 137b671c0..55cea5f8f 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -17,7 +17,8 @@ Handles all requests relating to schedulers. """ -import novaclient +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions from nova import db from nova import exception @@ -112,7 +113,7 @@ def _wrap_method(function, self): def _process(func, zone): """Worker stub for green thread pool. Give the worker an authenticated nova client and zone info.""" - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() return func(nova, zone) @@ -132,10 +133,10 @@ def call_zone_method(context, method_name, errors_to_ignore=None, zones = db.zone_get_all(context) for zone in zones: try: - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: url = zone.api_url LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s") % locals()) @@ -188,7 +189,7 @@ def _issue_novaclient_command(nova, zone, collection, if method_name in ['find', 'findall']: try: return getattr(manager, method_name)(**kwargs) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s.%(method_name)s didn't find " "anything matching '%(kwargs)s' on '%(url)s'" % @@ -200,7 +201,7 @@ def _issue_novaclient_command(nova, zone, collection, item = args.pop(0) try: result = manager.get(item) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" % locals())) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index d99d7214c..7e813af7e 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 @@ -117,10 +119,9 @@ class ZoneAwareScheduler(driver.Scheduler): % locals()) nova = None try: - nova = novaclient.OpenStack(zone.username, zone.password, None, - url) + nova = novaclient.Client(zone.username, zone.password, None, url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: raise exception.NotAuthorized(_("Bad credentials attempting " "to talk to zone at %(url)s.") % locals()) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index efdac06e1..97bdf3d44 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -18,10 +18,11 @@ ZoneManager oversees all communications with child Zones. """ import datetime -import novaclient import thread import traceback +from novaclient import v1_1 as novaclient + from eventlet import greenpool from nova import db @@ -89,8 +90,8 @@ class ZoneState(object): def _call_novaclient(zone): """Call novaclient. Broken out for testing purposes.""" - client = novaclient.OpenStack(zone.username, zone.password, None, - zone.api_url) + client = novaclient.Client(zone.username, zone.password, None, + zone.api_url) return client.zones.info()._info diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 6a56a57db..bd2394adf 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 @@ -1039,10 +1041,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): @@ -1088,7 +1090,7 @@ class FakeZonesProxy(object): raise Exception('testing') -class FakeNovaClientOpenStack(object): +class FakeNovaClientZones(object): def __init__(self, *args, **kwargs): self.zones = FakeZonesProxy() @@ -1101,7 +1103,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/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/tools/pip-requires b/tools/pip-requires index 23e707034..fd0ca639d 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -9,7 +9,7 @@ boto==1.9b carrot==0.10.5 eventlet lockfile==0.8 -python-novaclient==2.5.9 +python-novaclient==2.6.0 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 -- cgit From 0c19e26cddb50bf6808670d550d71ab435df37c5 Mon Sep 17 00:00:00 2001 From: Donal Lafferty Date: Thu, 4 Aug 2011 18:48:31 +0100 Subject: Fix comments. --- nova/virt/xenapi/vm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index a723c5e22..666b84b76 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -644,7 +644,7 @@ class VMHelper(HelperBase): # 3. Disk is_pv = True elif disk_image_type == ImageType.DISK_ISO: - # 5. ISO + # 4. ISO is_pv = False else: raise exception.Error(_("Unknown image format %(disk_image_type)s") -- cgit From 7381c55a3549ead494c6bd13dece17f293442940 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 4 Aug 2011 11:40:07 -0700 Subject: added NOVA_VERSION to novarc --- nova/auth/novarc.template | 1 + 1 file changed, 1 insertion(+) 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" -- cgit From bdbf3efcadeda46e66787edee344def84dccef73 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 4 Aug 2011 11:50:20 -0700 Subject: OS v1.1 is now the default into novarc --- nova/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index 12c6d1356..eb6366ed9 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -317,7 +317,7 @@ DEFINE_string('osapi_extensions_path', '/var/lib/nova/extensions', DEFINE_string('osapi_host', '$my_ip', 'ip of api server') DEFINE_string('osapi_scheme', 'http', 'prefix for openstack') DEFINE_integer('osapi_port', 8774, 'OpenStack API port') -DEFINE_string('osapi_path', '/v1.0/', 'suffix for openstack') +DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack') DEFINE_integer('osapi_max_limit', 1000, 'max number of items returned in a collection response') -- cgit From f22c19c6f78451074c33fe8da855755574cb6b49 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 4 Aug 2011 15:51:41 -0400 Subject: Split serverXMLDeserializers into v1.0 and v1.1 --- nova/api/openstack/create_instance_helper.py | 48 +++++++++++++++++++++++++ nova/api/openstack/servers.py | 7 +++- nova/tests/api/openstack/test_server_actions.py | 4 +-- nova/tests/api/openstack/test_servers.py | 2 +- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 2a8e7fd7e..832c890b6 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -304,6 +304,54 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): metadata_deserializer = common.MetadataXMLDeserializer() + def create(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" + server = {} + server_node = self.find_first_child_named(node, 'server') + + attributes = ["name", "imageId", "flavorId", "adminPass"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + server["metadata"] = self.metadata_deserializer.extract_metadata( + metadata_node) + + server["personality"] = self._extract_personality(server_node) + + return server + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + node = self.find_first_child_named(server_node, "personality") + personality = [] + if node is not None: + for file_node in self.find_children_named(node, "file"): + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self.extract_text(file_node) + personality.append(item) + return personality + + +class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + metadata_deserializer = common.MetadataXMLDeserializer() + def action(self, string): dom = minidom.parseString(string) action_node = dom.childNodes[0] diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index d17714371..3acc4510e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -891,8 +891,13 @@ def create_resource(version='1.0'): 'application/xml': xml_serializer, } + xml_deserializer = { + '1.0': helper.ServerXMLDeserializer(), + '1.1': helper.ServerXMLDeserializerV11(), + }[version] + body_deserializers = { - 'application/xml': helper.ServerXMLDeserializer(), + 'application/xml': xml_deserializer, } serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index fc08a2d53..8f4cfacf8 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -734,10 +734,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 diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 1349b289d..748573cce 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2181,7 +2181,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 = """ -- cgit From 346454c7fb1156d8dff075042f1c45dbb22cded1 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 4 Aug 2011 16:02:28 -0400 Subject: Nuke hostname. We don't use it. --- bin/nova-dhcpbridge | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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) -- cgit From 9ce80fc74b3ea4513369b795d1e6891d6dfa8e03 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 4 Aug 2011 16:20:37 -0400 Subject: Updated create image server action to respect 1.1 --- nova/api/openstack/create_instance_helper.py | 23 ++++++++++++++--------- nova/tests/api/openstack/test_server_actions.py | 1 - nova/tests/api/openstack/test_servers.py | 10 ---------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 832c890b6..eec861428 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -380,8 +380,10 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): 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): @@ -395,29 +397,32 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): 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/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 8f4cfacf8..fe627987f 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -750,7 +750,6 @@ class TestServerActionXMLDeserializerV11(test.TestCase): expected = { "createImage": { "name": "new-server-test", - "metadata": {}, }, } self.assertEquals(request['body'], expected) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 748573cce..235554afb 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2195,8 +2195,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2215,8 +2213,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "adminPass": "1234", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2233,8 +2229,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) @@ -2251,8 +2245,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) @@ -2296,7 +2288,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "metadata": {"one": "two", "open": "snack"}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2318,7 +2309,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=="}, -- cgit From 0f515d6d31b2c95ed7f1e3ca8d9d67f98fda9fbe Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 4 Aug 2011 16:26:31 -0400 Subject: Added XML serialization for server actions --- nova/api/openstack/create_instance_helper.py | 46 ++++++++ nova/tests/api/openstack/test_server_actions.py | 144 ++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index eec861428..894d47beb 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -360,6 +360,12 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): 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) @@ -373,6 +379,46 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): 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: diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index fe627987f..b175a7ff2 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -770,3 +770,147 @@ class TestServerActionXMLDeserializerV11(test.TestCase): }, } self.assertEquals(request['body'], expected) + + def test_change_pass(self): + serial_request = """ + """ + 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 = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_reboot(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "reboot": { + "type": "HARD", + }, + } + self.assertEquals(request['body'], expected) + + def test_reboot_no_type(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_resize(self): + serial_request = """ + """ + 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 = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_confirm_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "confirmResize": None, + } + self.assertEquals(request['body'], expected) + + def test_revert_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "revertResize": None, + } + self.assertEquals(request['body'], expected) + + def test_rebuild(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + 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.assertDictEqual(request['body'], expected) + + def test_rebuild_minimum(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + }, + } + self.assertDictEqual(request['body'], expected) + + def test_rebuild_no_imageRef(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') -- cgit From 5b463f5d14c62f66250d5edc3edbd2ded704e0da Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 4 Aug 2011 16:38:55 -0400 Subject: Added missing tests for server actions Updated reboot to verify the reboot type is HARD or SOFT Fixed case of having an empty flavorref on resize --- nova/api/openstack/servers.py | 9 +- nova/tests/api/openstack/test_server_actions.py | 107 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3acc4510e..fa4d11e24 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -268,9 +268,13 @@ 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'] + if (not reboot_type == 'HARD') and (not reboot_type == 'SOFT'): + msg = _("Argument 'type' for reboot is not HARD or SOFT") + LOG.exception(msg) + raise exc.HTTPBadRequest() else: LOG.exception(_("Missing argument 'type' for reboot")) - raise exc.HTTPUnprocessableEntity() + raise exc.HTTPBadRequest() try: # TODO(gundlach): pass reboot_type, support soft reboot in # virt driver @@ -646,6 +650,9 @@ class ControllerV11(Controller): """ Resizes a given instance to the flavor size requested """ try: flavor_ref = input_dict["resize"]["flavorRef"] + if not flavor_ref: + msg = _("Resize request has invalid 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) except (KeyError, TypeError): msg = _("Resize requests require 'flavorRef' attribute.") raise exc.HTTPBadRequest(explanation=msg) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index b175a7ff2..b5e4246e9 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -479,6 +479,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 = """ + """ + 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') @@ -515,6 +530,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": { @@ -657,6 +708,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': { -- cgit From 75b110aa451382cce94f10a392597b40df97839c Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 4 Aug 2011 20:49:21 +0000 Subject: Changed all references to 'power state' to 'power action' as requested by review. --- nova/api/openstack/contrib/hosts.py | 43 ++++++++++++---------- nova/compute/api.py | 6 +-- nova/compute/manager.py | 6 +-- nova/tests/test_hosts.py | 31 ++++++++-------- nova/virt/driver.py | 2 +- nova/virt/fake.py | 2 +- nova/virt/hyperv.py | 2 +- nova/virt/libvirt/connection.py | 2 +- nova/virt/vmwareapi_conn.py | 2 +- nova/virt/xenapi/vmops.py | 8 ++-- nova/virt/xenapi_conn.py | 4 +- .../xenserver/xenapi/etc/xapi.d/plugins/xenhost | 12 +++--- 12 files changed, 61 insertions(+), 59 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 78ba66771..09adbe2f4 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -70,7 +70,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, @@ -78,20 +78,6 @@ class HostController(object): else: explanation = _("Invalid status: '%s'") % raw_val raise webob.exc.HTTPBadRequest(explanation=explanation) - elif key == "power_state": - if val == "startup": - # The only valid values for 'state' are 'reboot' or - # 'shutdown'. For completeness' sake there is the - # 'startup' option to start up a host, but this is not - # technically feasible now, as we run the host on the - # XenServer box. - msg = _("Host startup on XenServer is not supported.") - raise webob.exc.HTTPBadRequest(explanation=msg) - elif val in ("reboot", "shutdown"): - return self._set_powerstate(req, id, val) - else: - explanation = _("Invalid powerstate: '%s'") % raw_val - raise webob.exc.HTTPBadRequest(explanation=explanation) else: explanation = _("Invalid update setting: '%s'") % raw_key raise webob.exc.HTTPBadRequest(explanation=explanation) @@ -108,12 +94,28 @@ class HostController(object): raise webob.exc.HTTPBadRequest(explanation=result) return {"host": host, "status": result} - def _set_powerstate(self, req, host, state): + def _host_power_action(self, req, host, action): """Reboots or shuts down the host.""" context = req.environ['nova.context'] - result = self.compute_api.set_host_powerstate(context, host=host, - state=state) - return {"host": host, "power_state": result} + result = self.compute_api.host_power_action(context, host=host, + action=action) + return {"host": host, "power_action": result} + + def startup(self, req, id): + """The only valid values for 'action' are 'reboot' or + 'shutdown'. For completeness' sake there is the + 'startup' option to start up a host, but this is not + technically feasible now, as we run the host on the + XenServer box. + """ + msg = _("Host startup on XenServer is not supported.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + 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): @@ -139,5 +141,6 @@ class Hosts(extensions.ExtensionDescriptor): if FLAGS.allow_admin_api: resources = [extensions.ResourceExtension('os-hosts', HostController(), collection_actions={'update': 'PUT'}, - member_actions={})] + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] return resources diff --git a/nova/compute/api.py b/nova/compute/api.py index 3e0cd7cfa..5f85ca908 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -995,10 +995,10 @@ class API(base.Base): return self._call_compute_message("set_host_enabled", context, instance_id=None, host=host, params={"enabled": enabled}) - def set_host_powerstate(self, context, host, state): + def host_power_action(self, context, host, action): """Reboots or shuts down the host.""" - return self._call_compute_message("set_host_powerstate", context, - instance_id=None, host=host, params={"state": state}) + return self._call_compute_message("host_power_action", context, + instance_id=None, host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 220b09554..c2c12a9a2 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -955,10 +955,10 @@ class ComputeManager(manager.SchedulerDependentManager): result)) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def set_host_powerstate(self, context, instance_id=None, host=None, - state=None): + def host_power_action(self, context, instance_id=None, host=None, + action=None): """Reboots or shuts down the host.""" - return self.driver.set_host_powerstate(host, state) + return self.driver.host_power_action(host, action) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def set_host_enabled(self, context, instance_id=None, host=None, diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index d8f90a109..cd22571e6 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -48,8 +48,8 @@ def stub_set_host_enabled(context, host, enabled): return status -def stub_set_host_powerstate(context, host, state): - return state +def stub_host_power_action(context, host, action): + return action class FakeRequest(object): @@ -66,8 +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, 'set_host_powerstate', - stub_set_host_powerstate) + 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.""" @@ -93,19 +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_power_state(self): - en_body = {"power_state": "reboot"} - result_c1 = self.controller.update(self.req, "host_c1", body=en_body) - self.assertEqual(result_c1["power_state"], "reboot") - # Test invalid power_state - en_body = {"power_state": "invalid"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=en_body) + def test_host_startup(self): + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.startup, + self.req, "host_c1") - def test_bad_power_state_value(self): - bad_body = {"power_state": "bad"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=bad_body) + def test_host_shutdown(self): + result = self.controller.shutdown(self.req, "host_c1") + print "RES", result + 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"} diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 30f14459a..052c6607e 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -282,7 +282,7 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 7b13f4fbe..db51c258b 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -512,7 +512,7 @@ class FakeConnection(driver.ComputeDriver): """Return fake Host Status of ram, disk, network.""" return self.host_status - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 00c4d360b..f0efeb581 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -499,7 +499,7 @@ class HyperVConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index f0b2014ac..14e02c7c4 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1558,7 +1558,7 @@ class LibvirtConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" pass diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 90d083f10..5937d9585 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -191,7 +191,7 @@ class VMWareESXConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" return - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" pass diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index dfd85dd2c..509abd767 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1024,13 +1024,13 @@ class VMOps(object): # TODO: implement this! return 'http://fakeajaxconsole/fake_url' - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" - args = {"state": json.dumps(state)} + args = {"action": json.dumps(action)} methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"} - json_resp = self._call_xenhost(methods[state], args) + json_resp = self._call_xenhost(methods[action], args) resp = json.loads(json_resp) - return resp["powerstate"] + return resp["power_action"] def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index fa5b78a2a..3452343c6 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -332,9 +332,9 @@ class XenAPIConnection(driver.ComputeDriver): True, run the update first.""" return self.HostState.get_host_stats(refresh=refresh) - def set_host_powerstate(self, host, state): + def host_power_action(self, host, action): """Reboots or shuts down the host.""" - return self._vmops.set_host_powerstate(host, state) + return self._vmops.host_power_action(host, action) def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index c29d57717..f6a9ac8d8 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -103,7 +103,7 @@ def set_host_enabled(self, arg_dict): return {"status": status} -def _powerstate(state): +def _power_action(action): host_uuid = _get_host_uuid() # Host must be disabled first result = _run_command("xe host-disable") @@ -115,23 +115,23 @@ def _powerstate(state): raise pluginlib.PluginError(result) cmds = {"reboot": "xe host-reboot", "startup": "xe host-power-on", "shutdown": "xe host-shutdown"} - result = _run_command(cmds[state]) + result = _run_command(cmds[action]) # Should be empty string if result: raise pluginlib.PluginError(result) - return {"powerstate": state} + return {"power_action": action} @jsonify def host_reboot(self, arg_dict): """Reboots the host.""" - return _powerstate("reboot") + return _power_action("reboot") @jsonify def host_shutdown(self, arg_dict): """Reboots the host.""" - return _powerstate("shutdown") + return _power_action("shutdown") @jsonify @@ -139,7 +139,7 @@ def host_start(self, arg_dict): """Starts the host. NOTE: Currently not feasible, since the host runs on the same machine as Xen. """ - return _powerstate("startup") + return _power_action("startup") @jsonify -- cgit From dcac4bc6c7be9832704e37cca7ce815e083974f5 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 4 Aug 2011 20:55:56 +0000 Subject: Added admin-only decorator --- nova/api/openstack/contrib/admin_only.py | 30 ++++++++++++++++++++++++++++++ nova/api/openstack/contrib/hosts.py | 14 ++++++-------- 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 nova/api/openstack/contrib/admin_only.py diff --git a/nova/api/openstack/contrib/admin_only.py b/nova/api/openstack/contrib/admin_only.py new file mode 100644 index 000000000..e821c9e1f --- /dev/null +++ b/nova/api/openstack/contrib/admin_only.py @@ -0,0 +1,30 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Decorator for limiting extensions that should be admin-only.""" + +from functools import wraps +from nova import flags +FLAGS = flags.FLAGS + + +def admin_only(fnc): + @wraps(fnc) + def _wrapped(self, *args, **kwargs): + if FLAGS.allow_admin_api: + return fnc(self, *args, **kwargs) + return [] + _wrapped.func_name = fnc.func_name + return _wrapped diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 09adbe2f4..cdf8760d5 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 @@ -134,13 +135,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 = [] - # If we are not in an admin env, don't add the resource. Regular users - # shouldn't have access to the host. - if FLAGS.allow_admin_api: - resources = [extensions.ResourceExtension('os-hosts', - HostController(), collection_actions={'update': 'PUT'}, - member_actions={"startup": "GET", "shutdown": "GET", - "reboot": "GET"})] + resources = [extensions.ResourceExtension('os-hosts', + HostController(), collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] return resources -- cgit From b5ff9bc2add98444773a26ce37e1ceb82e9531ae Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 4 Aug 2011 21:10:22 +0000 Subject: Removed test show() method --- nova/api/openstack/contrib/hosts.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index ddb611905..7290360ad 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -65,13 +65,6 @@ class HostController(object): def index(self, req): return {'hosts': _list_hosts(req)} - def show(self, req, id): - """Check the query vars for values to be returned from the host config - settings. Return a dict with the query var as the key and the config - setting as the value. - """ - return {"PARAMS": req.params.keys()} - @check_host def update(self, req, id, body): for raw_key, raw_val in body.iteritems(): -- cgit From dae356ad06bb4926ca91fd9a5182271d93c4b7be Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Thu, 4 Aug 2011 21:18:55 +0000 Subject: Forgot the instance_id parameter in the finish call --- nova/compute/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 94bac9be4..a062cbe91 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -746,7 +746,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()) -- cgit From 77a1a63c30a9797f2f729c7b815d6660bed880d5 Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Thu, 4 Aug 2011 21:26:21 +0000 Subject: Bad method call --- nova/virt/xenapi_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 91d9d8530..166141e5e 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -191,7 +191,7 @@ class XenAPIConnection(driver.ComputeDriver): def revert_migration(self, instance): """Reverts a resize, powering back on the instance""" - self._vmops.revert_resize(instance) + self._vmops.revert_migration(instance) def finish_migration(self, context, instance, disk_info, network_info, resize_instance=False): -- cgit From 5fe92058d0ee11a7e9ea1c8f56b7e9350cf703e4 Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Thu, 4 Aug 2011 22:18:52 +0000 Subject: Revert migration now finishes --- nova/compute/manager.py | 4 ++-- nova/tests/test_xenapi.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index a062cbe91..0fd9c3c13 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -746,8 +746,8 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref['host']) rpc.cast(context, topic, {'method': 'finish_revert_resize', - 'args': { 'instance_id': instance_ref['uuid'], - '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()) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 5b29cf537..d269408b9 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -767,6 +767,53 @@ 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 -- cgit From 8c7b71f65e54d67615e52927591e12a43b8b3991 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 16:05:08 -0700 Subject: re-integrated my changes after merging trunk. fixed some pep8 issues. sorting the list of cidrs to create, so that it will create x.x.0.0 with a lower 'id' than x.x.1.0 (as an example). <- was causing libvirtd test to fail --- nova/network/manager.py | 54 +++++++++++++++++++++++++++++++++++++++++----- nova/tests/test_network.py | 6 +++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 8fc6a295f..3c29417d7 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -614,6 +614,52 @@ 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) + all_req_nets = [adjusted_cidr] + 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))) + # split supernet into subnets + if num_networks >= 2: + next_cidr = adjusted_cidr + for used_cidr in used_cidrs: + # watch for smaller subnets conflicting + if used_cidr.size < next_cidr.size: + for ucsupernet in used_cidr.supernet(): + if ucsupernet.size == next_cidr.size: + used_cidrs.append(ucsupernet) + for index in range(1, num_networks): + while True: + next_cidr = next_cidr.next() + if next_cidr in used_cidrs: + continue + else: + all_req_nets.append(next_cidr) + break + all_req_nets = sorted(list(set(all_req_nets))) + # after splitting ensure there were enough to satisfy the num_networks + if len(all_req_nets) < num_networks: + msg = "Not enough subnets avail to satisfy requested num_networks" + raise ValueError(_(msg)) + 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 +670,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 +681,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) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 9c808c6db..ceb12e890 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -269,7 +269,7 @@ class CommonNetworkTestCase(test.TestCase): self.deallocate_called = address def fake_create_fixed_ips(self, context, network_id): - return None + return None def test_remove_fixed_ip_from_instance(self): manager = self.FakeNetworkManager() @@ -366,7 +366,7 @@ class CommonNetworkTestCase(test.TestCase): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() - fakecidr= [{'id': 1, 'cidr': '192.168.0.0/8'}] + 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] @@ -388,7 +388,7 @@ class CommonNetworkTestCase(test.TestCase): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() - fakecidr= [{'id': 1, 'cidr': '192.168.0.0/24'}] + 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, -- cgit From f58d441b55e143de35aefd039b80e0b27dad9ce2 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 16:27:55 -0700 Subject: removed unnecessary context from test I had left there from prior --- nova/tests/test_network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index ceb12e890..1ff3f0c01 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -from nova import context from nova import db from nova import exception from nova import flags -- cgit From d470f82ea09214b6f535995aa551a23eb4ccc4ed Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Thu, 4 Aug 2011 17:43:37 -0700 Subject: Properly format mapping['dns'] before handing off to template for injection (Fixes LP Bug #821203) --- nova/virt/libvirt/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index f7aae4112..0f0f62588 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -880,7 +880,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} -- cgit From 89ec28c70d7795d427ecd4242cb1856eabdca104 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 18:01:07 -0700 Subject: fixed bug, wasn't detecting smaller subnet conflict properly added test for it --- nova/network/manager.py | 35 ++++++++++++++++++++++++----------- nova/tests/test_network.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 3c29417d7..873fcadf5 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -624,7 +624,6 @@ class NetworkManager(manager.SchedulerDependentManager): raise ValueError(_(msg)) adjusted_cidr_str = req_net_ip + '/' + str(significant_bits) adjusted_cidr = netaddr.IPNetwork(adjusted_cidr_str) - all_req_nets = [adjusted_cidr] try: used_nets = self.db.network_get_all(context) except exception.NoNetworksFound: @@ -636,22 +635,36 @@ class NetworkManager(manager.SchedulerDependentManager): if adjusted_cidr_supernet in used_cidrs: msg = "requested cidr (%s) conflicts with existing supernet" raise ValueError(_(msg % str(adjusted_cidr))) - # split supernet into subnets - if num_networks >= 2: + # 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 used_cidr in used_cidrs: - # watch for smaller subnets conflicting - if used_cidr.size < next_cidr.size: - for ucsupernet in used_cidr.supernet(): - if ucsupernet.size == next_cidr.size: - used_cidrs.append(ucsupernet) - for index in range(1, num_networks): + for index in range(num_networks): while True: - next_cidr = next_cidr.next() if next_cidr in used_cidrs: + next_cidr = next_cidr.next() + continue + elif next_cidr in used_supernets: + next_cidr = next_cidr.next() continue else: all_req_nets.append(next_cidr) + next_cidr = next_cidr.next() break all_req_nets = sorted(list(set(all_req_nets))) # after splitting ensure there were enough to satisfy the num_networks diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 1ff3f0c01..e3a677c97 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -313,7 +313,19 @@ class CommonNetworkTestCase(test.TestCase): self.assertTrue(exp_cidr + '/24' in cidrs) self.assertFalse('192.168.2.0/24' in cidrs) - def test__validate_cidrs_split_cidr_smaller_subnet_in_use(self): + 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() @@ -329,6 +341,22 @@ class CommonNetworkTestCase(test.TestCase): self.assertTrue(exp_cidr + '/24' 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] + print cidrs + exp_cidrs = ['192.168.2.32', '192.168.2.64', '192.168.2.96'] + for exp_cidr in exp_cidrs: + self.assertTrue(exp_cidr + '/27' in cidrs) + self.assertFalse('192.168.2.0/27' in cidrs) + def test__validate_cidrs_one_in_use(self): manager = self.FakeNetworkManager() args = [None, '192.168.0.0/24', 2, 256] -- cgit From 79e51d7b138948eddd307747c517be9ad1aa67d1 Mon Sep 17 00:00:00 2001 From: Gabe Westmaas Date: Fri, 5 Aug 2011 01:07:53 +0000 Subject: Adding missing module xmlutil --- nova/api/openstack/xmlutil.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 nova/api/openstack/xmlutil.py 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) -- cgit From 38eb72be5f15731ba34a7dc0f8a28aa0fb63ea90 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 18:37:36 -0700 Subject: fixed bug , when logic searched for next avail cidr it would return cidrs that were out of range of original requested cidr block. added test for it --- nova/network/manager.py | 4 ++++ nova/tests/test_network.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 873fcadf5..0bb107268 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -655,6 +655,10 @@ class NetworkManager(manager.SchedulerDependentManager): # 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: if next_cidr in used_cidrs: next_cidr = next_cidr.next() diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index e3a677c97..b4c5a7584 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -351,12 +351,26 @@ class CommonNetworkTestCase(test.TestCase): nets = manager._validate_cidrs(None, '192.168.2.0/24', 3, 32) self.assertEqual(3, len(nets)) cidrs = [str(net) for net in nets] - print cidrs exp_cidrs = ['192.168.2.32', '192.168.2.64', '192.168.2.96'] for exp_cidr in exp_cidrs: self.assertTrue(exp_cidr + '/27' 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] @@ -385,7 +399,6 @@ class CommonNetworkTestCase(test.TestCase): manager = self.FakeNetworkManager() nets = manager._validate_cidrs(None, '192.168.0.0/16', 2, 256) returned_cidrs = [str(net) for net in nets] - print returned_cidrs self.assertTrue('192.168.0.0/24' in returned_cidrs) self.assertTrue('192.168.1.0/24' in returned_cidrs) -- cgit From b557b6366b21a0d3795369785037ee29c8cef377 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 18:52:15 -0700 Subject: fix pep8 issues --- nova/network/manager.py | 14 +++++++++----- nova/tests/test_network.py | 5 +++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 0bb107268..1b92effb2 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -647,7 +647,8 @@ class NetworkManager(manager.SchedulerDependentManager): all_req_nets = [] if num_networks == 1: if adjusted_cidr in used_supernets: - msg = "requested cidr (%s) conflicts with existing smaller cidr" + msg = "requested cidr (%s) conflicts with existing smaller" \ + " cidr" raise ValueError(_(msg % str(adjusted_cidr))) else: all_req_nets.append(adjusted_cidr) @@ -656,8 +657,9 @@ class NetworkManager(manager.SchedulerDependentManager): 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" + msg = "Not enough subnets avail to satisfy requested " \ + "num_net works - some subnets in requested range" \ + " already in use" raise ValueError(_(msg)) while True: if next_cidr in used_cidrs: @@ -671,9 +673,11 @@ class NetworkManager(manager.SchedulerDependentManager): next_cidr = next_cidr.next() break all_req_nets = sorted(list(set(all_req_nets))) - # after splitting ensure there were enough to satisfy the num_networks + # after splitting ensure there were enough to satisfy the + # num_networks if len(all_req_nets) < num_networks: - msg = "Not enough subnets avail to satisfy requested num_networks" + msg = "Not enough subnets avail to satisfy requested " \ + "num_networks" raise ValueError(_(msg)) return all_req_nets diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index b4c5a7584..8c84ea938 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -367,8 +367,9 @@ class CommonNetworkTestCase(test.TestCase): 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 + # 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): -- cgit From 637dfc0f44cbd5bf0c76d80d708a241e562403ac Mon Sep 17 00:00:00 2001 From: Gabe Westmaas Date: Fri, 5 Aug 2011 01:55:53 +0000 Subject: Added explanations to exceptions and cleaned up reboot types --- nova/api/openstack/servers.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fa4d11e24..9a55af8ea 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -267,14 +267,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'] - if (not reboot_type == 'HARD') and (not reboot_type == 'SOFT'): + 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() + raise exc.HTTPBadRequest(explanation=msg) else: - LOG.exception(_("Missing argument 'type' for reboot")) - raise exc.HTTPBadRequest() + 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 -- cgit From b7167b21d615f8617d588a1656aa341fd226ded9 Mon Sep 17 00:00:00 2001 From: John Tran Date: Thu, 4 Aug 2011 20:13:23 -0700 Subject: removed redundant logic --- nova/network/manager.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 1b92effb2..3d915423f 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -673,12 +673,6 @@ class NetworkManager(manager.SchedulerDependentManager): next_cidr = next_cidr.next() break all_req_nets = sorted(list(set(all_req_nets))) - # after splitting ensure there were enough to satisfy the - # num_networks - if len(all_req_nets) < num_networks: - msg = "Not enough subnets avail to satisfy requested " \ - "num_networks" - raise ValueError(_(msg)) return all_req_nets def create_networks(self, context, label, cidr, multi_host, num_networks, -- cgit From b068e1edd79a5382ef1237d0b47e60dc141bae35 Mon Sep 17 00:00:00 2001 From: Ilya Alekseyev Date: Fri, 5 Aug 2011 07:55:22 +0400 Subject: added missing tests --- nova/tests/test_nova_manage.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 nova/tests/test_nova_manage.py diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py new file mode 100644 index 000000000..ca9ffa222 --- /dev/null +++ b/nova/tests/test_nova_manage.py @@ -0,0 +1,79 @@ +# 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 + +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') + +import imp +nova_manage = imp.load_source('nova_manage', NOVA_MANAGE_PATH) + +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_waste(self): + db.fixed_ip_update(context.get_admin_context(), '10.0.0.100', + {'reserved': True}) + self.commands.waste('10.0.0.100') + address = db.fixed_ip_get_by_address(context.get_admin_context(), + '10.0.0.100') + self.assertEqual(address['reserved'], False) -- cgit From 625330bbc70b7be0d007c2a5ce1fba4dfcc29bf8 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Thu, 4 Aug 2011 22:59:36 -0500 Subject: utilized functools.wraps --- nova/test.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nova/test.py b/nova/test.py index 97cd3e0d0..d3b9d3ee2 100644 --- a/nova/test.py +++ b/nova/test.py @@ -60,11 +60,10 @@ 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 @@ -75,13 +74,12 @@ class skip_if(object): 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) - _skipper.__name__ = func.__name__ - _skipper.__doc__ = func.__doc__ return _skipper @@ -92,13 +90,12 @@ class skip_unless(object): 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) - _skipper.__name__ = func.__name__ - _skipper.__doc__ = func.__doc__ return _skipper -- cgit From 13fb04effa25e860eaa9e4ebaca2b4d906a140b0 Mon Sep 17 00:00:00 2001 From: Justin Shepherd Date: Thu, 4 Aug 2011 23:01:22 -0500 Subject: fixed pep8 issue --- nova/tests/test_skip_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py index 033f1ea2e..8ca203442 100644 --- a/nova/tests/test_skip_examples.py +++ b/nova/tests/test_skip_examples.py @@ -37,7 +37,7 @@ class ExampleSkipTestCase(test.TestCase): @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 -- cgit From fe343a30ad5317ac5635667e72f56be775284658 Mon Sep 17 00:00:00 2001 From: Isaku Yamahata Date: Fri, 5 Aug 2011 15:23:54 +0900 Subject: fix mismerge --- nova/virt/libvirt/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index c49f6da5d..7f5bdcf10 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1052,7 +1052,8 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(yamahata): # for nova.api.ec2.cloud.CloudController.get_metadata() xml_info['root_device'] = self.default_root_device - db.instance_update(context.get_admin_context(), instance['id'], + 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) -- cgit From 2a89883297f6b5398abb7486e9e26c12ab0fc0ec Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Fri, 5 Aug 2011 14:02:55 +0200 Subject: Remove spurious direct use of subprocess --- nova/console/xvp.py | 1 - nova/utils.py | 4 ++++ nova/virt/libvirt/connection.py | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) 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/utils.py b/nova/utils.py index 1e2dbebb1..3a2049464 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -137,6 +137,8 @@ def execute(*cmd, **kwargs): :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. + :shell True | False. Defaults to False. If set to True, + Popen command is called shell=True. :raises exception.Error on receiving unknown arguments :raises exception.ProcessExecutionError @@ -147,6 +149,7 @@ def execute(*cmd, **kwargs): check_exit_code = kwargs.pop('check_exit_code', 0) delay_on_retry = kwargs.pop('delay_on_retry', True) attempts = kwargs.pop('attempts', 1) + shell = kwargs.pop('shell', False) if len(kwargs): raise exception.Error(_('Got unknown keyword args ' 'to utils.execute: %r') % kwargs) @@ -164,6 +167,7 @@ def execute(*cmd, **kwargs): stdin=_PIPE, stdout=_PIPE, stderr=_PIPE, + shell=shell, env=env) result = None if process_input is not None: diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0b93a8399..ec202a4ef 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -43,7 +43,6 @@ import os import random import re import shutil -import subprocess import sys import tempfile import time @@ -684,7 +683,7 @@ class LibvirtConnection(driver.ComputeDriver): cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \ % (utils.novadir(), ajaxterm_cmd, token, port) - subprocess.Popen(cmd, shell=True) + utils.execute(cmd, shell=True) return {'token': token, 'host': host, 'port': port} def get_host_ip_addr(self): -- cgit From ecdb1c87fb3904080d48f3fd3b3619f6c4a40df9 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Fri, 5 Aug 2011 14:33:12 +0200 Subject: Add run_as_root parameter to utils.execute, uses new sudo_helper FLAG to prefix command --- nova/flags.py | 3 +++ nova/utils.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/nova/flags.py b/nova/flags.py index 12c6d1356..e30c0d6be 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -392,3 +392,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('sudo_helper', 'sudo', + 'Command prefix to use for running commands as root') diff --git a/nova/utils.py b/nova/utils.py index 3a2049464..0820a2bde 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 @@ -139,6 +140,9 @@ def execute(*cmd, **kwargs): :attempts How many times to retry cmd. :shell True | False. Defaults to False. If set to True, Popen command is called shell=True. + :run_as_root True | False. Defaults to False. If set to True, + the command is prefixed by the command specified + in the sudo_helper FLAG. :raises exception.Error on receiving unknown arguments :raises exception.ProcessExecutionError @@ -150,9 +154,13 @@ def execute(*cmd, **kwargs): delay_on_retry = kwargs.pop('delay_on_retry', True) attempts = kwargs.pop('attempts', 1) shell = kwargs.pop('shell', False) + 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.sudo_helper) + cmd cmd = map(str, cmd) while attempts > 0: -- cgit From 9742ff588d3ce90b1420eaf95e311f02a4e196da Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 5 Aug 2011 14:54:56 +0200 Subject: Add generic image service tests. --- nova/image/fake.py | 41 ++++++++++---- nova/tests/test_image.py | 135 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 nova/tests/test_image.py diff --git a/nova/image/fake.py b/nova/image/fake.py index 28e912534..b29ec6517 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/tests/test_image.py b/nova/tests/test_image.py new file mode 100644 index 000000000..ba5b93f9b --- /dev/null +++ b/nova/tests/test_image.py @@ -0,0 +1,135 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Linux2Go +# 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() + -- cgit From ab1ba7cbcffc92c2c82c468fb0a2a81f93db3f85 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 5 Aug 2011 06:01:55 -0700 Subject: fixed up zones controller to properly work with 1.1 --- nova/api/openstack/zones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): -- cgit From 1e366d9de180347b47675bf7f811ffd5a293ef10 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 5 Aug 2011 15:37:36 +0200 Subject: pep8 --- nova/image/fake.py | 2 +- nova/tests/test_image.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/nova/image/fake.py b/nova/image/fake.py index b29ec6517..97af81711 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -118,7 +118,7 @@ class _FakeImageService(service.BaseImageService): """Returns list of images.""" retval = [] for img in self.images.values(): - retval += [dict([(k, v) for k, v in img.iteritems() + retval += [dict([(k, v) for k, v in img.iteritems() if k in ['id', 'name']])] return retval diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py index ba5b93f9b..5ec8812f3 100644 --- a/nova/tests/test_image.py +++ b/nova/tests/test_image.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Linux2Go +# Copyright 2011 OpenStack LLC # Author: Soren Hansen # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -82,16 +82,14 @@ class _ImageTestCase(test.TestCase): self.image_service.create(self.context, {}) index = self.image_service.index(self.context) - self.assertEquals(len(index), image_count+1) + 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, @@ -132,4 +130,3 @@ class FakeImageTestCase(_ImageTestCase): def setUp(self): super(FakeImageTestCase, self).setUp() self.image_service = nova.image.fake.FakeImageService() - -- cgit From 070d832dad062dfb18439e77b4460e6ebe75bdb0 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 5 Aug 2011 15:38:25 +0200 Subject: Make disk_format and container_format optional for libvirt's snapshot implementation. --- nova/virt/libvirt/connection.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 0b93a8399..3663337e9 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -392,9 +392,7 @@ class LibvirtConnection(driver.ComputeDriver): nova.image.get_image_service(image_href) snapshot = snapshot_image_service.show(context, snapshot_image_id) - metadata = {'disk_format': base['disk_format'], - 'container_format': base['container_format'], - 'is_public': False, + metadata = {'is_public': False, 'status': 'active', 'name': snapshot['name'], 'properties': { @@ -409,6 +407,12 @@ class LibvirtConnection(driver.ComputeDriver): arch = base['properties']['architecture'] metadata['properties']['architecture'] = arch + if 'disk_format' in base: + metadata['disk_format'] = base['disk_format'] + + if 'container_format' in base: + metadata['container_format'] = base['container_format'] + # Make the snapshot snapshot_name = uuid.uuid4().hex snapshot_xml = """ -- cgit From e3433605d77492a58916d2e131eb0701baf849fa Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 5 Aug 2011 07:19:35 -0700 Subject: pep8 violations sneaking into trunk? --- nova/api/direct.py | 1 + nova/api/openstack/common.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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/openstack/common.py b/nova/api/openstack/common.py index 715b9e4a4..75e862630 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -154,7 +154,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 -- cgit From 19e50320e36f02ce11a6aaae8f88a6ddbc132859 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 5 Aug 2011 07:21:55 -0700 Subject: pep8 violations sneaking into trunk? --- nova/db/sqlalchemy/api.py | 3 ++- nova/tests/test_xenapi.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ce12ba4e0..8cfa94ed7 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3301,7 +3301,8 @@ 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() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 8048e5341..dfc1eeb0a 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -767,7 +767,6 @@ 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 -- cgit From 38756955417e5c2fad7c8848252c5a2334912e02 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Fri, 5 Aug 2011 16:23:48 +0200 Subject: Removed most direct sudo calls, make them use run_as_root=True instead --- nova/network/linux_net.py | 78 ++++++++++++++++-------------- nova/tests/test_libvirt.py | 10 ++-- nova/tests/test_volume.py | 10 ++-- nova/tests/test_xenapi.py | 10 ++-- nova/virt/disk.py | 47 +++++++++--------- nova/virt/libvirt/connection.py | 7 +-- nova/virt/libvirt/vif.py | 16 ++++--- nova/virt/xenapi/vm_utils.py | 15 +++--- nova/virt/xenapi/volume_utils.py | 4 +- nova/volume/driver.py | 101 +++++++++++++++++++++------------------ 10 files changed, 161 insertions(+), 137 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 8ace07884..3c62c75cd 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,63 @@ 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('brctl', 'addbr', bridge, run_as_root=True) + _execute('brctl', 'setfd', bridge, 0, run_as_root=True) # _execute('sudo brctl setageing %s 10' % bridge) - _execute('sudo', 'brctl', 'stp', bridge, 'off') - _execute('sudo', 'ip', 'link', 'set', bridge, 'up') + _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,7 +610,7 @@ 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) @@ -646,7 +654,7 @@ 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: @@ -732,7 +740,7 @@ def _stop_dnsmasq(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 +796,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/tests/test_libvirt.py b/nova/tests/test_libvirt.py index f8b866985..1baf373bf 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -884,18 +884,18 @@ class IptablesFirewallTestCase(test.TestCase): # 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 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..84480d26f 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -548,8 +548,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 +592,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 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/libvirt/connection.py b/nova/virt/libvirt/connection.py index ec202a4ef..5053daf8f 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -607,9 +607,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: @@ -632,7 +633,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 @@ -914,7 +915,7 @@ 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')) + utils.execute('chown', 'root', basepath('disk'), run_as_root=True) root_mount_device = 'vda' # FIXME for now. it's hard coded. local_mount_device = 'vdb' # FIXME for now. it's hard coded. diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 711b05bae..e243d4fa0 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -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/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 6d2340ccd..7b79e129c 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -967,7 +967,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 +986,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 +1003,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 +1057,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/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/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 -- cgit From 336efaf814c7796b9426d045f82f2d1e30d8db72 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 5 Aug 2011 11:17:05 -0400 Subject: Add exception logging for instance IDs in the __public_instance_is_accessible smoke test function. This should help troubleshoot an intermittent failure. --- smoketests/test_netadmin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smoketests/test_netadmin.py b/smoketests/test_netadmin.py index 60086f065..de69c98a2 100644 --- a/smoketests/test_netadmin.py +++ b/smoketests/test_netadmin.py @@ -115,7 +115,8 @@ class SecurityGroupTests(base.UserSmokeTestCase): if not instance_id: return False if instance_id != self.data['instance'].id: - raise Exception("Wrong instance id") + raise Exception("Wrong instance id. Expected: %s, Got: %s" % + (self.data['instance'].id, instance_id)) return True def test_001_can_create_security_group(self): -- cgit From a77f10ee052fdafeb9d52407695719397c52e68d Mon Sep 17 00:00:00 2001 From: Ilya Alekseyev Date: Fri, 5 Aug 2011 19:45:17 +0400 Subject: methods renamed --- bin/nova-manage | 2 +- nova/tests/test_nova_manage.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index ba08f136b..6f71bc403 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -601,7 +601,7 @@ class FixedIpCommands(object): @args('--address', dest="address", metavar='', help='IP address') - def waste(self, address): + def unreserve(self, address): """Mark fixed ip as free to use arguments: address""" self._set_reserved(address, False) diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py index ca9ffa222..9c6563f14 100644 --- a/nova/tests/test_nova_manage.py +++ b/nova/tests/test_nova_manage.py @@ -16,6 +16,7 @@ # under the License. import os +import sys TOPDIR = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -23,8 +24,10 @@ TOPDIR = os.path.normpath(os.path.join( 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', NOVA_MANAGE_PATH) +nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH) +sys.dont_write_bytecode = False import netaddr from nova import context @@ -70,10 +73,10 @@ class FixedIpCommandsTestCase(test.TestCase): '10.0.0.100') self.assertEqual(address['reserved'], True) - def test_waste(self): + def test_unreserve(self): db.fixed_ip_update(context.get_admin_context(), '10.0.0.100', {'reserved': True}) - self.commands.waste('10.0.0.100') + 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) -- cgit From 9602a558b6be6e6812626b986c0f9557a3862fe6 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 5 Aug 2011 11:59:14 -0400 Subject: glance image service pagination --- nova/api/openstack/images.py | 4 +-- nova/exception.py | 4 +++ nova/image/glance.py | 82 +++++++++++++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index c76738d30..b9bc83fde 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -143,7 +143,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 +156,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/exception.py b/nova/exception.py index 68e6ac937..792e306c1 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -150,6 +150,10 @@ class NovaException(Exception): return self._error_string +class ImagePaginationFailed(NovaException): + message = _("Failed to paginate through images from image service") + + class VirtualInterfaceCreateException(NovaException): message = _("Virtual Interface creation failed") diff --git a/nova/image/glance.py b/nova/image/glance.py index 44a3c6f83..ccb1b7ec0 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -87,42 +87,70 @@ 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: + _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['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.""" -- cgit -- cgit From 149153085027237c343cc325e163979f1cd31a21 Mon Sep 17 00:00:00 2001 From: Gabe Westmaas Date: Fri, 5 Aug 2011 16:22:21 +0000 Subject: Moving from assertDictEqual to assertDictMatch --- nova/tests/api/openstack/test_server_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 005923f4e..7e24d24fd 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -986,7 +986,7 @@ class TestServerActionXMLDeserializerV11(test.TestCase): ], }, } - self.assertDictEqual(request['body'], expected) + self.assertDictMatch(request['body'], expected) def test_rebuild_minimum(self): serial_request = """ @@ -999,7 +999,7 @@ class TestServerActionXMLDeserializerV11(test.TestCase): "imageRef": "http://localhost/images/1", }, } - self.assertDictEqual(request['body'], expected) + self.assertDictMatch(request['body'], expected) def test_rebuild_no_imageRef(self): serial_request = """ -- cgit From 2935bebd718e770d0f2c9d1ab5dca76cc7d5f76a Mon Sep 17 00:00:00 2001 From: John Tran Date: Fri, 5 Aug 2011 09:50:11 -0700 Subject: fix typo --- nova/tests/test_network.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 40cc96147..1f27643e5 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -360,8 +360,7 @@ class CommonNetworkTestCase(test.TestCase): 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'} - ] + {'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] -- cgit From 681d3e2bfac2aa8e19cb393591b99efcbcdd8230 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 5 Aug 2011 14:15:53 -0400 Subject: re-enabling sort_key/sort_dir and fixing filters line --- nova/image/glance.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index ccb1b7ec0..23749e560 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -116,10 +116,11 @@ class GlanceImageService(service.BaseImageService): def _extract_query_params(self, params): _params = {} - accepted_params = ('filters', 'marker', 'limit') - #'sort_key', 'sort_dir') + accepted_params = ('filters', 'marker', 'limit', + 'sort_key', 'sort_dir') for param in accepted_params: - _params[param] = params.get(param) + if param in params: + _params[param] = params.get(param) return _params @@ -128,7 +129,7 @@ class GlanceImageService(service.BaseImageService): self._set_client_context(context) # ensure filters is a dict - kwargs['filters'] = kwargs['filters'] or {} + kwargs['filters'] = kwargs.get('filters', {}) # NOTE(vish): don't filter out private images kwargs['filters'].setdefault('is_public', 'none') -- cgit From 3812d22b7a6f5d74418a7a99dc69c68a5b9f9046 Mon Sep 17 00:00:00 2001 From: John Tran Date: Fri, 5 Aug 2011 11:36:00 -0700 Subject: fixed per peer review --- nova/network/manager.py | 27 ++++++++++++--------------- nova/tests/test_network.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 3d915423f..5677df670 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -620,8 +620,8 @@ class NetworkManager(manager.SchedulerDependentManager): 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)) + 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: @@ -633,8 +633,8 @@ class NetworkManager(manager.SchedulerDependentManager): 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))) + 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: @@ -647,9 +647,9 @@ class NetworkManager(manager.SchedulerDependentManager): 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))) + 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: @@ -657,17 +657,14 @@ class NetworkManager(manager.SchedulerDependentManager): 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 " \ + msg = ("Not enough subnets avail to satisfy requested " \ "num_net works - some subnets in requested range" \ - " already in use" - raise ValueError(_(msg)) + " already in use") + raise ValueError(msg) while True: - if next_cidr in used_cidrs: + used_values = used_cidrs + used_supernets + if next_cidr in used_values: next_cidr = next_cidr.next() - continue - elif next_cidr in used_supernets: - next_cidr = next_cidr.next() - continue else: all_req_nets.append(next_cidr) next_cidr = next_cidr.next() diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 1f27643e5..547a7a1fa 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -280,14 +280,14 @@ class CommonNetworkTestCase(test.TestCase): manager.remove_fixed_ip_from_instance, None, 99, 'bad input') - def test__validate_cidrs(self): + 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): + 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)) @@ -295,7 +295,7 @@ class CommonNetworkTestCase(test.TestCase): 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): + 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() @@ -305,13 +305,13 @@ class CommonNetworkTestCase(test.TestCase): 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', '192.168.1.0', '192.168.3.0', - '192.168.4.0'] + 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 + '/24' in 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): + def test_validate_cidrs_smaller_subnet_in_use(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() @@ -323,7 +323,7 @@ class CommonNetworkTestCase(test.TestCase): 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): + def test_validate_cidrs_split_smaller_cidr_in_use(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() @@ -333,13 +333,13 @@ class CommonNetworkTestCase(test.TestCase): 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', '192.168.1.0', '192.168.3.0', - '192.168.4.0'] + 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 + '/24' in 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): + def test_validate_cidrs_split_smaller_cidr_in_use2(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() @@ -349,12 +349,12 @@ class CommonNetworkTestCase(test.TestCase): 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', '192.168.2.64', '192.168.2.96'] + 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 + '/27' in 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): + def test_validate_cidrs_split_all_in_use(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() @@ -369,13 +369,13 @@ class CommonNetworkTestCase(test.TestCase): # in use self.assertRaises(ValueError, manager._validate_cidrs, *args) - def test__validate_cidrs_one_in_use(self): + 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): + def test_validate_cidrs_already_used(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() @@ -386,21 +386,21 @@ class CommonNetworkTestCase(test.TestCase): args = [None, '192.168.0.0/24', 1, 256] self.assertRaises(ValueError, manager._validate_cidrs, *args) - def test__validate_cidrs_too_many(self): + 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): + 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): + def test_validate_cidrs_conflict_existing_supernet(self): manager = self.FakeNetworkManager() self.mox.StubOutWithMock(manager.db, 'network_get_all') ctxt = mox.IgnoreArg() -- cgit From 353fa4871069cf0b926f09aa00496002f65584cb Mon Sep 17 00:00:00 2001 From: John Tran Date: Fri, 5 Aug 2011 11:48:06 -0700 Subject: fixed per peer review --- nova/network/manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 5677df670..402049d44 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -647,8 +647,8 @@ class NetworkManager(manager.SchedulerDependentManager): all_req_nets = [] if num_networks == 1: if adjusted_cidr in used_supernets: - msg = _("requested cidr (%s) conflicts with existing smaller" \ - " cidr") + msg = _("requested cidr (%s) conflicts with existing smaller" + " cidr") raise ValueError(msg % str(adjusted_cidr)) else: all_req_nets.append(adjusted_cidr) @@ -657,9 +657,9 @@ class NetworkManager(manager.SchedulerDependentManager): 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") + 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 -- cgit From a3e618bdc64f5e2cbb62fcf4a5b2df0e437c4fd9 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 5 Aug 2011 15:02:18 -0400 Subject: fixing filters get --- nova/image/glance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index 23749e560..da93f0d1c 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -129,7 +129,7 @@ class GlanceImageService(service.BaseImageService): self._set_client_context(context) # ensure filters is a dict - kwargs['filters'] = kwargs.get('filters', {}) + kwargs['filters'] = kwargs.get('filters') or {} # NOTE(vish): don't filter out private images kwargs['filters'].setdefault('is_public', 'none') -- cgit From 2fe0c5fe95487df8827db10f38065e3c8ac3800f Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Fri, 5 Aug 2011 12:09:46 -0700 Subject: Fixed review comments --- nova/api/openstack/contrib/security_groups.py | 101 ++++++++------------- .../api/openstack/contrib/test_security_groups.py | 8 +- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index 7da046b8f..e2fed7965 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -45,33 +45,33 @@ class SecurityGroupController(object): super(SecurityGroupController, self).__init__() def _format_security_group_rule(self, context, rule): - r = {} - r['id'] = rule.id - r['parent_group_id'] = rule.parent_group_id - r['ip_protocol'] = rule.protocol - r['from_port'] = rule.from_port - r['to_port'] = rule.to_port - r['group'] = {} - r['ip_range'] = {} + 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) - r['group'] = {'name': source_group.name, + sg_rule['group'] = {'name': source_group.name, 'tenant_id': source_group.project_id} else: - r['ip_range'] = {'cidr': rule.cidr} - return r + sg_rule['ip_range'] = {'cidr': rule.cidr} + return sg_rule def _format_security_group(self, context, group): - g = {} - g['id'] = group.id - g['description'] = group.description - g['name'] = group.name - g['tenant_id'] = group.project_id - g['rules'] = [] + 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: - r = self._format_security_group_rule(context, rule) - g['rules'] += [r] - return g + 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.""" @@ -97,7 +97,7 @@ class SecurityGroupController(object): except ValueError: msg = _("Security group id is not integer") return exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: + except exception.SecurityGroupNotFound as exp: return exc.HTTPNotFound(explanation=unicode(exp)) LOG.audit(_("Delete security group %s"), id, context=context) @@ -138,8 +138,9 @@ class SecurityGroupController(object): group_name = security_group.get('name', None) group_description = security_group.get('description', None) - self._validate_security_group_name(group_name) - self._validate_security_group_description(group_description) + 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() @@ -158,40 +159,21 @@ class SecurityGroupController(object): return {'security_group': self._format_security_group(context, group_ref)} - def _validate_security_group_name(self, value): - if value is None: - msg = _("Security group name is mandatory") - raise exc.HTTPBadRequest(explanation=msg) - - if not isinstance(value, basestring): - msg = _("Security group name is not a string or unicode") - raise exc.HTTPBadRequest(explanation=msg) - - if value.strip() == '': - msg = _("Security group name is an empty string") - raise exc.HTTPBadRequest(explanation=msg) - - if len(value.strip()) > 255: - msg = _("Security group name should not be greater " - "than 255 characters") - raise exc.HTTPBadRequest(explanation=msg) - - def _validate_security_group_description(self, value): - if value is None: - msg = _("Security group description is mandatory") - raise exc.HTTPBadRequest(explanation=msg) - - if not isinstance(value, basestring): - msg = _("Security group description is not a string or unicode") + 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 value.strip() == '': - msg = _("Security group description is an empty string") + if not val: + msg = _("Security group %s cannot be empty.") % typ raise exc.HTTPBadRequest(explanation=msg) - - if len(value.strip()) > 255: - msg = _("Security group description should not be " - "greater than 255 characters") + if len(val) > 255: + msg = _("Security group %s should not be greater " + "than 255 characters.") % typ raise exc.HTTPBadRequest(explanation=msg) @@ -220,7 +202,7 @@ class SecurityGroupRulesController(SecurityGroupController): msg = _("Security group (%s) not found") % parent_group_id return exc.HTTPNotFound(explanation=msg) - msg = "Authorize security group ingress %s" + msg = _("Authorize security group ingress %s") LOG.audit(_(msg), security_group['name'], context=context) try: @@ -315,7 +297,7 @@ class SecurityGroupRulesController(SecurityGroupController): 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)): + (max(from_port, to_port) > 65535)): raise exception.InvalidPortRange(from_port=from_port, to_port=to_port) @@ -345,17 +327,14 @@ class SecurityGroupRulesController(SecurityGroupController): group_id = rule.parent_group_id self.compute_api.ensure_default_security_group(context) - security_group = db.security_group_get(context, group_id) - if not security_group: - raise exception.SecurityGroupNotFound(security_group_id=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']) + security_group_id=security_group['id']) return exc.HTTPAccepted() diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py index eaef413a7..9a86a60f8 100644 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -73,10 +73,10 @@ class TestSecurityGroups(test.TestCase): sg = body_dict['security_group'] body_parts = [] body_parts.extend([ - '', - '' % (sg['name'])]) + '', + '' % (sg['name'])]) if 'description' in sg: body_parts.append('%s' % sg['description']) -- cgit From f03c926a7d28ee35789048ea53c36cd452ed3571 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 5 Aug 2011 14:28:22 -0500 Subject: Allow actions queries by UUID and PEP8 fixes. --- nova/api/direct.py | 1 + nova/api/openstack/common.py | 3 ++- nova/db/sqlalchemy/api.py | 10 ++++++++-- nova/tests/test_image.py | 2 ++ nova/tests/test_xenapi.py | 1 - 5 files changed, 13 insertions(+), 4 deletions(-) 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/openstack/common.py b/nova/api/openstack/common.py index 715b9e4a4..4548c2c75 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -154,7 +154,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 diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ce12ba4e0..f469dc0e5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1426,9 +1426,14 @@ def instance_action_create(context, values): def instance_get_actions(context, instance_id): """Return the actions associated to the given instance id""" session = get_session() + + if utils.is_uuid_like(instance_id): + instance = instance_get_by_uuid(context, instance_id, session) + instance_id = instance.id + return session.query(models.InstanceActions).\ filter_by(instance_id=instance_id).\ - all() + all() ################### @@ -3301,7 +3306,8 @@ 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() diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py index 5ec8812f3..9680d6f2b 100644 --- a/nova/tests/test_image.py +++ b/nova/tests/test_image.py @@ -22,6 +22,7 @@ from nova import exception from nova import test import nova.image + class _ImageTestCase(test.TestCase): def setUp(self): super(_ImageTestCase, self).setUp() @@ -126,6 +127,7 @@ class _ImageTestCase(test.TestCase): index = self.image_service.index(self.context) self.assertEquals(len(index), 0) + class FakeImageTestCase(_ImageTestCase): def setUp(self): super(FakeImageTestCase, self).setUp() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 8048e5341..dfc1eeb0a 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -767,7 +767,6 @@ 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 -- cgit From 2a329ff0734bc4413723322e289a0ac486ed7e2f Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Fri, 5 Aug 2011 12:43:27 -0700 Subject: Fixed localization review comment --- nova/api/openstack/contrib/security_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index e2fed7965..d3a8e21b8 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -203,7 +203,7 @@ class SecurityGroupRulesController(SecurityGroupController): return exc.HTTPNotFound(explanation=msg) msg = _("Authorize security group ingress %s") - LOG.audit(_(msg), security_group['name'], context=context) + LOG.audit(msg, security_group['name'], context=context) try: values = self._rule_args_to_dict(context, @@ -330,7 +330,7 @@ class SecurityGroupRulesController(SecurityGroupController): security_group = db.security_group_get(context, group_id) msg = _("Revoke security group ingress %s") - LOG.audit(_(msg), security_group['name'], context=context) + 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, -- cgit From 09772f5bf3140a6f4cbaace50ead8d25a874cbb0 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 5 Aug 2011 14:37:44 -0700 Subject: If ip is deallocated from project, but attached to a fixed ip, it is now detached --- nova/api/openstack/contrib/floating_ips.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 3d8049324..616388e80 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -97,8 +97,13 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] - ip = self.network_api.get_floating_ip(context, id) + try: + if 'fixed_ip' in ip: + self.disassociate(req, id, '') + except: + pass + self.network_api.release_floating_ip(context, address=ip) return {'released': { -- cgit From 9633e9877c7836c18c30b51c8494abfb025e64ca Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 5 Aug 2011 22:14:15 +0000 Subject: Adding flag around image-create for v1.0 --- nova/api/openstack/__init__.py | 3 +++ nova/api/openstack/images.py | 6 ++++++ nova/tests/api/openstack/test_images.py | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index d6a98c2cd..4d49df2ad 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -50,6 +50,9 @@ FLAGS = flags.FLAGS flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') class FaultWrapper(base_wsgi.Middleware): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index b9bc83fde..7b738e1f3 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -108,6 +108,12 @@ class ControllerV10(Controller): def create(self, req, body): """Snapshot a server instance and save the image.""" + 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) + try: image = body["image"] except (KeyError, TypeError): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 8e2e3f390..38495bbe7 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -1049,6 +1049,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 -- cgit From 8a87a3ecc0bc9fe524f42950ec0d50124bb5b2b6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 5 Aug 2011 15:25:12 -0700 Subject: Pass a real context object into image service calls --- bin/nova-manage | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index f272351c2..7b4923021 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1235,11 +1235,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): -- cgit From ccea6c91b2314311587466d67d20f1583ddba1ee Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 5 Aug 2011 15:28:10 -0700 Subject: adding logging to exception in delete method --- nova/api/openstack/contrib/floating_ips.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 616388e80..49ab88bb6 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -24,6 +24,9 @@ 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']} @@ -98,11 +101,12 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] ip = self.network_api.get_floating_ip(context, id) + try: if 'fixed_ip' in ip: self.disassociate(req, id, '') - except: - pass + except Exception, e: + LOG.exception(_("Error disassociating fixed_ip %s"), e) self.network_api.release_floating_ip(context, address=ip) -- cgit From c49e99a7fc590c2dde6125843d904895ca8861a3 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 5 Aug 2011 22:29:28 +0000 Subject: Disable flag for V1 Openstack API --- nova/api/openstack/servers.py | 6 ++++++ nova/tests/api/openstack/test_server_actions.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 1051ba571..391c7d644 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -691,6 +691,12 @@ class ControllerV11(Controller): def _action_create_image(self, input_dict, req, instance_id): """Snapshot a server instance.""" + 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) + entity = input_dict.get("createImage", {}) try: diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 7e24d24fd..bf18bc1b0 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -458,6 +458,7 @@ class ServerActionsTestV11(test.TestCase): self.service.delete_all() self.sent_to_glance = {} fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) + self.flags(allow_instance_snapshots=True) def tearDown(self): self.stubs.UnsetAll() @@ -775,6 +776,23 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) + def test_create_image_snapshots_disabled(self): + """Don't permit a snapshot if the allow_instance_snapshots flag is + False + """ + self.flags(allow_instance_snapshots=False) + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_with_metadata(self): body = { 'createImage': { -- cgit From bdabdd50845279cbca11f510dd5da6a5aa110528 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 5 Aug 2011 22:56:08 +0000 Subject: Using decorator for snapshots enabled check --- nova/api/openstack/common.py | 14 ++++++++++++++ nova/api/openstack/images.py | 7 +------ nova/api/openstack/servers.py | 7 +------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 4548c2c75..ec9368140 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 @@ -280,3 +281,16 @@ 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/images.py b/nova/api/openstack/images.py index 7b738e1f3..0aabb9e56 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -106,14 +106,9 @@ 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.""" - 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) - try: image = body["image"] except (KeyError, TypeError): diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 391c7d644..4d6518598 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -689,14 +689,9 @@ 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.""" - 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) - entity = input_dict.get("createImage", {}) try: -- cgit From fe7f229c8ad91b1ae9187b8c541fdefd535eed9b Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 5 Aug 2011 16:20:53 -0700 Subject: moving try/except block, and changing syntax of except statement --- nova/api/openstack/contrib/floating_ips.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 49ab88bb6..996a42abe 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -101,12 +101,12 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] ip = self.network_api.get_floating_ip(context, id) - - try: - if 'fixed_ip' in ip: + + if 'fixed_ip' in ip: + try: self.disassociate(req, id, '') - except Exception, e: - LOG.exception(_("Error disassociating fixed_ip %s"), e) + except Exception as e: + LOG.exception(_("Error disassociating fixed_ip %s"), e) self.network_api.release_floating_ip(context, address=ip) -- cgit From b15535a20b7f717aa23f5bc6d695e574bb86c407 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 5 Aug 2011 23:26:08 +0000 Subject: Adding check to stub method --- nova/api/openstack/common.py | 2 +- nova/api/openstack/servers.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index ec9368140..375304930 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -289,7 +289,7 @@ def check_snapshots_enabled(f): if not FLAGS.allow_instance_snapshots: LOG.warn(_('Rejecting snapshot request, snapshots currently' ' disabled')) - msg = _("Instance Snapshots are not permitted at this time.") + 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/servers.py b/nova/api/openstack/servers.py index 4d6518598..f1a27a98c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -240,6 +240,7 @@ class Controller(object): resp.headers['Location'] = image_ref return resp + @common.check_snapshots_enabled def _action_create_image(self, input_dict, req, id): return exc.HTTPNotImplemented() -- cgit From 8c6ccfd51698bffe0d56193bc4137ad80708e6d3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 5 Aug 2011 16:41:03 -0700 Subject: zone_aware_scheduler classes couldn't build instances due to a change to compute api's create_db_entry_for_new_instance call. now passing image argument down to the scheduler and through to the call. updated a existing test to cover this --- nova/compute/api.py | 4 ++++ nova/scheduler/zone_aware_scheduler.py | 3 ++- nova/tests/scheduler/test_zone_aware_scheduler.py | 27 ++++++++++++++++++----- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 80d54d029..752f29384 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -360,6 +360,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=None, num_instances=1): """Send the run_instance request to the schedulers for processing.""" pid = context.project_id @@ -373,6 +374,7 @@ class API(base.Base): filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter' request_spec = { + 'image': image, 'instance_properties': base_options, 'instance_type': instance_type, 'filter': filter_class, @@ -415,6 +417,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, num_instances=num_instances) return base_options['reservation_id'] @@ -463,6 +466,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=instance_id) return [dict(x.iteritems()) for x in instances] diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index d99d7214c..a47bf7fe7 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -58,12 +58,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 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) -- cgit From 7a5bb39ef11d630df26f2fcfbf249f0c34e9fa55 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 5 Aug 2011 18:51:17 -0500 Subject: Pep8 fix --- nova/api/openstack/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 375304930..5226cdf9a 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -293,4 +293,3 @@ def check_snapshots_enabled(f): raise webob.exc.HTTPBadRequest(explanation=msg) return f(*args, **kwargs) return inner - -- cgit From a30856cd5a7358772d47c3877dd01d1078ffe472 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 5 Aug 2011 20:05:33 -0400 Subject: Update the OSAPI v1.1 server 'createImage' and 'createBackup' actions to limit the number of image metadata items based on the configured quota.allowed_metadata_items that is set. --- nova/api/openstack/common.py | 14 ++++++++- nova/api/openstack/image_metadata.py | 16 ++-------- nova/api/openstack/servers.py | 6 ++-- nova/tests/api/openstack/test_server_actions.py | 40 +++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 715b9e4a4..a4cdbda76 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -24,6 +24,7 @@ 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 @@ -154,7 +155,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 +193,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): 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/servers.py b/nova/api/openstack/servers.py index 3930982dc..6936864df 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -218,13 +218,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, @@ -702,13 +703,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, diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 562cefe90..021b105d4 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() @@ -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) @@ -684,6 +708,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': {}, -- cgit From 82eb299fd0fa6601d4704836ed7e76369f086ffc Mon Sep 17 00:00:00 2001 From: "Dave Walker (Daviey)" Date: Sat, 6 Aug 2011 20:18:35 +0100 Subject: simplified test cases further, thanks to trunk changes --- nova/tests/test_api.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 3af1563fa..533447362 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -342,12 +342,6 @@ class ApiEc2TestCase(test.TestCase): spaces, dashes, and underscores. """ self.expect_http() self.mox.ReplayAll() - user = self.manager.create_user('fake', 'fake', 'fake', admin=True) - project = self.manager.create_project('fake', 'fake', 'fake') - - # At the moment, you need both of these to actually be netadmin - self.manager.add_role('fake', 'netadmin') - project.add_role('fake', 'netadmin') # Test block group_name of non alphanumeric characters, spaces, # dashes, and underscores. @@ -361,12 +355,6 @@ class ApiEc2TestCase(test.TestCase): API Spec states that the length should not exceed 255 chars """ self.expect_http() self.mox.ReplayAll() - user = self.manager.create_user('fake', 'fake', 'fake', admin=True) - project = self.manager.create_project('fake', 'fake', 'fake') - - # At the moment, you need both of these to actually be netadmin - self.manager.add_role('fake', 'netadmin') - project.add_role('fake', 'netadmin') # Test block group_name > 255 chars security_group_name = "".join(random.choice("poiuytrewqasdfghjklmnbvc") -- cgit From 458932a4f069225ff1a2fad0df81242f058e7af9 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 6 Aug 2011 22:59:09 -0400 Subject: Update the curl command in the __public_instance_is_accessible function of test_netadmin to return an error code which we can then check for and handle properly. This should allow calling functions to properly retry and timout if an actual test failure happens. --- smoketests/test_netadmin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/smoketests/test_netadmin.py b/smoketests/test_netadmin.py index de69c98a2..8c8fa35b8 100644 --- a/smoketests/test_netadmin.py +++ b/smoketests/test_netadmin.py @@ -109,9 +109,12 @@ 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: -- cgit From 8c75de3188fdbec6456fcf7071b6b08b9d1a0d40 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sun, 7 Aug 2011 16:40:07 -0400 Subject: Set image progress to 100 if the image is active. --- nova/api/openstack/views/images.py | 4 +++- nova/tests/api/openstack/test_images.py | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 873ce212a..8539fbcbf 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"] == "ACTIVE": + image["progress"] = 100 + else: image["progress"] = 0 return image diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 8e2e3f390..498e8b14c 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): -- cgit From 309e49873fd2535fa64b242aea254b72b5cbb4a9 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Sun, 7 Aug 2011 22:05:01 -0400 Subject: Adding __init__.py files --- nova/api/openstack/schemas/__init__.py | 0 nova/api/openstack/schemas/v1.1/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 nova/api/openstack/schemas/__init__.py create mode 100644 nova/api/openstack/schemas/v1.1/__init__.py diff --git a/nova/api/openstack/schemas/__init__.py b/nova/api/openstack/schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/schemas/v1.1/__init__.py b/nova/api/openstack/schemas/v1.1/__init__.py new file mode 100644 index 000000000..e69de29bb -- cgit From 27a77fbc2651381d9663064a363105f803781924 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Mon, 8 Aug 2011 09:30:56 +0000 Subject: Save exception and re-raise that instead of depending on thread local exception that may have been clobbered by intermediate processing --- nova/exception.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nova/exception.py b/nova/exception.py index 792e306c1..b017c8d87 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -96,6 +96,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 +126,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 -- cgit From e4ee8b54d0e840050357902b78f7e48013be9096 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 8 Aug 2011 10:12:01 -0400 Subject: upper() is even better. --- nova/api/openstack/views/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 8539fbcbf..912303d14 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -77,7 +77,7 @@ class ViewBuilder(object): "status": image_obj.get("status"), }) - if image["status"] == "ACTIVE": + if image["status"].upper() == "ACTIVE": image["progress"] = 100 else: image["progress"] = 0 -- cgit From b1a503053cb8cbeb1a4ab18e650b49cc4da15e23 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 14:19:53 +0000 Subject: Moved the restriction on host startup to the xenapi layer.: --- nova/api/openstack/contrib/hosts.py | 18 +++++++----------- nova/virt/xenapi/vmops.py | 2 +- nova/virt/xenapi_conn.py | 13 +++++++++++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index cdf8760d5..d5bd3166b 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -96,21 +96,17 @@ class HostController(object): return {"host": host, "status": result} def _host_power_action(self, req, host, action): - """Reboots or shuts down the host.""" + """Reboots, shuts down or powers up the host.""" context = req.environ['nova.context'] - result = self.compute_api.host_power_action(context, host=host, - action=action) + 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): - """The only valid values for 'action' are 'reboot' or - 'shutdown'. For completeness' sake there is the - 'startup' option to start up a host, but this is not - technically feasible now, as we run the host on the - XenServer box. - """ - msg = _("Host startup on XenServer is not supported.") - raise webob.exc.HTTPBadRequest(explanation=msg) + 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") diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index df90b62c4..2a5cf8b5e 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1032,7 +1032,7 @@ class VMOps(object): return 'http://fakeajaxconsole/fake_url' def host_power_action(self, host, action): - """Reboots, shuts down or powers up the host.""" + """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) diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 720c9fd58..2a6a97faf 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -333,8 +333,17 @@ class XenAPIConnection(driver.ComputeDriver): return self.HostState.get_host_stats(refresh=refresh) def host_power_action(self, host, action): - """Reboots, shuts down or powers up the host.""" - return self._vmops.host_power_action(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.""" -- cgit From 973032959ea4b1300cb68f767885dbd3226bebd9 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 14:42:18 +0000 Subject: Fixed some typos from the last refactoring --- nova/api/openstack/contrib/hosts.py | 2 +- nova/tests/test_hosts.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index d5bd3166b..ecaa365b7 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -133,7 +133,7 @@ class Hosts(extensions.ExtensionDescriptor): @admin_only.admin_only def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', + resources = [extensions.ResourceExtension('os-hosts', HostController(), collection_actions={'update': 'PUT'}, member_actions={"startup": "GET", "shutdown": "GET", "reboot": "GET"})] diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index cd22571e6..a724db9da 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -94,12 +94,11 @@ class HostTestCase(test.TestCase): self.assertEqual(result_c2["status"], "disabled") def test_host_startup(self): - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.startup, - self.req, "host_c1") + 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") - print "RES", result self.assertEqual(result["power_action"], "shutdown") def test_host_reboot(self): -- cgit From 586359f792cb32210f83046e46a0cdb85b319fcd Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 14:51:42 +0000 Subject: Cleaned up some old code added by the last merge --- nova/compute/manager.py | 8 +------- nova/virt/fake.py | 4 ---- nova/virt/hyperv.py | 4 ---- nova/virt/libvirt/connection.py | 4 ---- nova/virt/vmwareapi_conn.py | 4 ---- nova/virt/xenapi/vmops.py | 11 ----------- nova/virt/xenapi_conn.py | 4 ---- 7 files changed, 1 insertion(+), 38 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 4908cba97..ecfbd3908 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1,4 +1,4 @@ -#: tabstop=4 shiftwidth=4 softtabstop=4 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. @@ -969,12 +969,6 @@ class ComputeManager(manager.SchedulerDependentManager): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def set_power_state(self, context, instance_id=None, host=None, - power_state=None): - """Turns the specified host on/off, or reboots the host.""" - return self.driver.set_power_state(host, power_state) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for an instance on this host.""" diff --git a/nova/virt/fake.py b/nova/virt/fake.py index a811edf7a..89ad20494 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -519,7 +519,3 @@ class FakeConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 6524e1739..ae30c62f0 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -506,7 +506,3 @@ class HyperVConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index af5221f55..7655bf386 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1569,7 +1569,3 @@ class LibvirtConnection(driver.ComputeDriver): def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass - - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index cfa4cb418..aaa384374 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -199,10 +199,6 @@ class VMWareESXConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" pass - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - pass - def plug_vifs(self, instance, network_info): """Plugs in VIFs to networks.""" self._vmops.plug_vifs(instance, network_info) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 2a5cf8b5e..b549b33d1 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1050,17 +1050,6 @@ class VMOps(object): return xenapi_resp.details[-1] return resp["status"] - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - args = {"power_state": power_state} - xenapi_resp = self._call_xenhost("set_power_state", args) - try: - resp = json.loads(xenapi_resp) - except TypeError as e: - # Already logged; return the message - return xenapi_resp.details[-1] - return resp["power_state"] - def _call_xenhost(self, method, arg_dict): """There will be several methods that will need this general handling for interacting with the xenhost plugin, so this abstracts diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 2a6a97faf..a1c9a1e30 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -349,10 +349,6 @@ class XenAPIConnection(driver.ComputeDriver): """Sets the specified host's ability to accept new instances.""" return self._vmops.set_host_enabled(host, enabled) - def set_power_state(self, host, power_state): - """Reboots, shuts down or starts up the host.""" - return self._vmops.set_power_state(host, power_state) - class XenAPISession(object): """The session to invoke XenAPI SDK calls""" -- cgit From b23387ef7a0024ac11e0970e3b76fa3441e30a9c Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 17:34:42 +0000 Subject: Review fixes --- nova/compute/api.py | 4 ++-- nova/compute/manager.py | 6 ++---- nova/virt/xenapi/vmops.py | 2 +- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index d5d66fa57..d2c08678b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -997,12 +997,12 @@ class API(base.Base): def set_host_enabled(self, context, host, enabled): """Sets the specified host's ability to accept new instances.""" return self._call_compute_message("set_host_enabled", context, - instance_id=None, host=host, params={"enabled": enabled}) + host=host, params={"enabled": enabled}) def host_power_action(self, context, host, action): """Reboots, shuts down or powers up the host.""" return self._call_compute_message("host_power_action", context, - instance_id=None, host=host, params={"action": action}) + host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ecfbd3908..cb6617c33 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -958,14 +958,12 @@ class ComputeManager(manager.SchedulerDependentManager): result)) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def host_power_action(self, context, instance_id=None, host=None, - action=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, instance_id=None, host=None, - enabled=None): + def set_host_enabled(self, context, host=None, enabled=None): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index b549b33d1..b913e764e 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1045,7 +1045,7 @@ class VMOps(object): xenapi_resp = self._call_xenhost("set_host_enabled", args) try: resp = json.loads(xenapi_resp) - except TypeError as e: + except TypeError as e: # Already logged; return the message return xenapi_resp.details[-1] return resp["status"] diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index f6a9ac8d8..7bf507d0f 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -136,7 +136,7 @@ def host_shutdown(self, arg_dict): @jsonify def host_start(self, arg_dict): - """Starts the host. NOTE: Currently not feasible, since the host + """Starts the host. Currently not feasible, since the host runs on the same machine as Xen. """ return _power_action("startup") -- cgit From c600b2cf3697fc3587fe5519fda8dd4b82d67234 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Mon, 8 Aug 2011 12:10:14 -0700 Subject: adding forgotten import for logging --- nova/api/openstack/contrib/floating_ips.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 996a42abe..52c9c6cf9 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -18,6 +18,7 @@ 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 -- cgit From 9788cddbf7833a82fc5589dd5f2869a309d1f657 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Mon, 8 Aug 2011 19:28:42 +0000 Subject: Import sys as well --- nova/exception.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/exception.py b/nova/exception.py index b017c8d87..a87728fff 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -25,6 +25,7 @@ SHOULD include dedicated exception logging. """ from functools import wraps +import sys from nova import log as logging -- cgit From 4c7a33ee907ae7abcff020c75dde8ed320fe7aeb Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Mon, 8 Aug 2011 15:30:44 -0400 Subject: assert that vmops.revert_migration is called --- nova/tests/test_xenapi.py | 25 ++++++++++++++++++------- nova/tests/xenapi/stubs.py | 4 ---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 8e3c88f8e..34aba140b 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -637,6 +637,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) @@ -669,13 +687,6 @@ class XenAPIVMTestCase(test.TestCase): self.conn.spawn(instance, network_info) return instance - def test_revert_migration(self): - instance = self._create_instance() - stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) - stubs.stubout_loopingcall_start(self.stubs) - conn = xenapi_conn.get_connection(False) - conn.revert_migration(instance) - class XenAPIDiffieHellmanTestCase(test.TestCase): """Unit tests for Diffie-Hellman code.""" diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 195e4d038..66c79d465 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -230,14 +230,10 @@ def stub_out_vm_methods(stubs): def fake_spawn_rescue(self, inst): inst._rescue = False - def fake_revert_migration(self, inst): - pass - stubs.Set(vmops.VMOps, "_shutdown", fake_shutdown) stubs.Set(vmops.VMOps, "_acquire_bootlock", fake_acquire_bootlock) stubs.Set(vmops.VMOps, "_release_bootlock", fake_release_bootlock) stubs.Set(vmops.VMOps, "spawn_rescue", fake_spawn_rescue) - stubs.Set(vmops.VMOps, "revert_migration", fake_revert_migration) class FakeSessionForVolumeTests(fake.SessionBase): -- cgit From 1fc7164ecaa42c33ff510ec8ef3ca92f183c96e3 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Mon, 8 Aug 2011 15:37:23 -0400 Subject: remove obsolete script from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 8121e250b..118ac1abd 100644 --- a/setup.py +++ b/setup.py @@ -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', -- cgit From de23e5ad63f6293060835e496363c935044480d6 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 20:03:14 +0000 Subject: cleaned up unneeded line --- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index a28f5abfb..4028fdc7a 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -104,7 +104,6 @@ def set_host_enabled(self, arg_dict): return {"status": status} -<<<<<<< TREE def _write_config_dict(dct): conf_file = file(config_file_path, "w") json.dump(dct, conf_file) -- cgit From 3f23c79bbb556cf05f7cf8c839edb6398464e051 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 20:12:35 +0000 Subject: Cleaned up merge messes. --- nova/api/openstack/contrib/hosts.py | 15 +-------------- nova/compute/api.py | 5 ----- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 63e5f545f..ecaa365b7 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -79,20 +79,6 @@ class HostController(object): else: explanation = _("Invalid status: '%s'") % raw_val raise webob.exc.HTTPBadRequest(explanation=explanation) - elif key == "power_state": - if val == "startup": - # The only valid values for 'state' are 'reboot' or - # 'shutdown'. For completeness' sake there is the - # 'startup' option to start up a host, but this is not - # technically feasible now, as we run the host on the - # XenServer box. - msg = _("Host startup on XenServer is not supported.") - raise webob.exc.HTTPBadRequest(explanation=msg) - elif val in ("reboot", "shutdown"): - return self._set_powerstate(req, id, val) - else: - explanation = _("Invalid powerstate: '%s'") % raw_val - raise webob.exc.HTTPBadRequest(explanation=explanation) else: explanation = _("Invalid update setting: '%s'") % raw_key raise webob.exc.HTTPBadRequest(explanation=explanation) @@ -117,6 +103,7 @@ class HostController(object): 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") diff --git a/nova/compute/api.py b/nova/compute/api.py index ccff421fe..d2c08678b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1004,11 +1004,6 @@ class API(base.Base): return self._call_compute_message("host_power_action", context, host=host, params={"action": action}) - def host_power_action(self, context, host, action): - """Reboots or shuts down the host.""" - return self._call_compute_message("host_power_action", context, - instance_id=None, host=host, params={"action": action}) - @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for the given instance.""" -- cgit From 61cf3721ce94d7f2458e4e469cbee3333f954588 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 8 Aug 2011 16:38:14 -0400 Subject: cleaning up instance metadata api code --- nova/api/openstack/server_metadata.py | 28 +++++++---------- nova/compute/api.py | 25 ++++++++++----- nova/db/api.py | 6 ++-- nova/db/sqlalchemy/api.py | 39 +++++++++++++++++------- nova/tests/api/openstack/test_server_metadata.py | 32 +++++++++---------- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index b0b014f86..969769729 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -57,16 +57,7 @@ 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) - - except quota.QuotaError as error: - self._handle_quota_error(error) + self._update_instance_metadata(context, server_id, metadata, False) return body @@ -88,7 +79,7 @@ class Controller(object): 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, False) return {'meta': {id: meta_value}} @@ -100,20 +91,23 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._set_instance_metadata(context, server_id, metadata) + self._update_instance_metadata(context, server_id, metadata, True) return {'metadata': 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) + 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) diff --git a/nova/compute/api.py b/nova/compute/api.py index d2c08678b..646290e57 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1175,11 +1175,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 not delete: + _metadata = self.get_instance_metadata(context, instance_id) + _metadata.update(metadata) + else: + _metadata = metadata + + self._check_metadata_properties_quota(context, _metadata) + + self.db.instance_metadata_update(context, instance_id, _metadata, True) diff --git a/nova/db/api.py b/nova/db/api.py index 47308bdba..90003cf86 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1381,9 +1381,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) #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 003eb8bcb..5ae14261c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1345,9 +1345,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, @@ -3201,21 +3202,37 @@ 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 all metadata that isn't passed in if delete kwarg is True + if delete: + original_metadata = instance_metadata_get(context, instance_id) + for meta in original_metadata.iteritems(): + if meta.key not in metadata: + meta.update({"deleted": True}) + meta.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} + + # if the metadata item exists, make sure it is not delete try: - meta_ref = instance_metadata_get_item(context, instance_id, key, - session) + meta_ref = instance_metadata_get_item(context, instance_id, + meta_key, session) + item.update({"deleted": False}) + + # if the item doesn't exist, we also need to set key and instance_id 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 diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index 08a6a062a..a90d572c8 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,7 +202,7 @@ 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_update', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' @@ -216,7 +216,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(expected, 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_update", return_create_instance_metadata) req = webob.Request.blank("/v1.1/servers/1/metadata") req.method = "POST" @@ -240,7 +240,7 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(request_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 +258,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 +276,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 +289,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 +300,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 +320,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 +334,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 +361,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 +370,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 +380,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 +390,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 +404,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' -- cgit From 4de24a4d44040ba38a474cd789b95a2b59d494ff Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 8 Aug 2011 17:33:03 -0400 Subject: making server metadata work functionally --- nova/api/openstack/server_metadata.py | 18 +++++++++++------- nova/db/api.py | 2 +- nova/db/sqlalchemy/api.py | 12 ++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 969769729..ed90be0c9 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -23,6 +23,10 @@ from nova.api.openstack import wsgi from nova import exception from nova import quota +from nova import log as logging + +LOG = logging.getLogger("nova.api.openstack.server_metadata") + class Controller(object): """ The server metadata API controller for the Openstack API """ @@ -69,19 +73,19 @@ 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._update_instance_metadata(context, server_id, meta_item, False) - return {'meta': {id: meta_value}} + return {'meta': meta_item} def update_all(self, req, server_id, body): try: @@ -107,8 +111,8 @@ class Controller(object): msg = _('Server does not exist') raise exc.HTTPNotFound(explanation=msg) - except (ValueError, AttributeError): - msg = _("Malformed request body") + except (ValueError, AttributeError), ex: + msg = _("Malformed request body: %s") % (str(ex),) raise exc.HTTPBadRequest(explanation=msg) except quota.QuotaError as error: @@ -132,12 +136,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/db/api.py b/nova/db/api.py index 90003cf86..0516c683f 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1383,7 +1383,7 @@ def instance_metadata_delete(context, instance_id, key): 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) + IMPL.instance_metadata_update(context, instance_id, metadata, delete) #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 5ae14261c..2e29f34ac 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3208,10 +3208,12 @@ def instance_metadata_update(context, instance_id, metadata, delete): # Set all metadata that isn't passed in if delete kwarg is True if delete: original_metadata = instance_metadata_get(context, instance_id) - for meta in original_metadata.iteritems(): - if meta.key not in metadata: - meta.update({"deleted": True}) - meta.save(session=session) + 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 @@ -3221,11 +3223,9 @@ def instance_metadata_update(context, instance_id, metadata, delete): # update the value whether it exists or not item = {"value": meta_value} - # if the metadata item exists, make sure it is not delete try: meta_ref = instance_metadata_get_item(context, instance_id, meta_key, session) - item.update({"deleted": False}) # if the item doesn't exist, we also need to set key and instance_id except exception.InstanceMetadataNotFound, e: -- cgit From 450a9ff6d7082c2c12ad933be0743c010103ffa9 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Mon, 8 Aug 2011 14:43:52 -0700 Subject: added --purge optparse for flavor delete --- bin/nova-manage | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 40f22c19c..e010bbe4b 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1120,10 +1120,12 @@ class InstanceTypeCommands(object): @args('--name', dest='name', metavar='', 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 == True: instance_types.purge(name) verb = "purged" else: -- cgit From 607d919420913969c90cedfba8857f07fc355c5e Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 8 Aug 2011 17:44:58 -0400 Subject: removing log lines --- nova/api/openstack/server_metadata.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index ed90be0c9..a10c48156 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -23,10 +23,6 @@ from nova.api.openstack import wsgi from nova import exception from nova import quota -from nova import log as logging - -LOG = logging.getLogger("nova.api.openstack.server_metadata") - class Controller(object): """ The server metadata API controller for the Openstack API """ @@ -112,7 +108,7 @@ class Controller(object): raise exc.HTTPNotFound(explanation=msg) except (ValueError, AttributeError), ex: - msg = _("Malformed request body: %s") % (str(ex),) + msg = _("Malformed request body") raise exc.HTTPBadRequest(explanation=msg) except quota.QuotaError as error: -- cgit From fee2812193258a1a4ade3116282d3f5c1cf1f58c Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 8 Aug 2011 21:46:33 +0000 Subject: Fixed typo found in review --- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 8bd376264..cd9694ce1 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -215,7 +215,8 @@ def host_data(self, arg_dict): # and convert the data types as needed. ret_dict = cleanup(parsed_data) # Add any config settings - ret_dict.update(_get_config_dict) + config = _get_config_dict() + ret_dict.update(config) return ret_dict -- cgit From 9a52a79f45bb526f5ff15d8bb136bb947a114824 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Mon, 8 Aug 2011 15:33:17 -0700 Subject: fixed conditional because jk0 is very picky :) --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index e010bbe4b..a8a872c28 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1125,7 +1125,7 @@ class InstanceTypeCommands(object): def delete(self, name, purge): """Marks instance types / flavors as deleted""" try: - if purge == True: + if purge: instance_types.purge(name) verb = "purged" else: -- cgit From 543a783cefc3b34fa4a5d4ae5b9034090666d182 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 8 Aug 2011 20:23:15 -0400 Subject: Fixing a bug in nova.utils.novadir() --- nova/api/openstack/schemas/__init__.py | 0 nova/api/openstack/schemas/v1.1/__init__.py | 0 nova/utils.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 nova/api/openstack/schemas/__init__.py delete mode 100644 nova/api/openstack/schemas/v1.1/__init__.py diff --git a/nova/api/openstack/schemas/__init__.py b/nova/api/openstack/schemas/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/schemas/v1.1/__init__.py b/nova/api/openstack/schemas/v1.1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/utils.py b/nova/utils.py index 4ea623cc1..da8826f1f 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -223,7 +223,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): -- cgit From d7880c2a0ba1d4285edb33208e8a94a8e9f15a21 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 9 Aug 2011 00:27:28 -0400 Subject: changing server create response to 202 --- nova/api/openstack/servers.py | 3 +++ nova/tests/api/openstack/test_servers.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f1a27a98c..63e71b3eb 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -743,6 +743,9 @@ class ControllerV11(Controller): class HeadersSerializer(wsgi.ResponseHeadersSerializer): + def create(self, response, data): + response.status_int = 202 + def delete(self, response, data): response.status_int = 204 diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index fd06b2e64..ca8dfc5b9 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1356,7 +1356,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']) @@ -1451,7 +1451,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 +1496,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']) -- cgit From 44fc059d8bf8e57a808d69ba3b5c9a4235707d34 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 9 Aug 2011 00:47:16 -0400 Subject: updating more test cases --- nova/tests/api/openstack/test_servers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index ca8dfc5b9..b411a5e4f 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1159,7 +1159,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']) @@ -2513,13 +2513,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): @@ -2529,7 +2529,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): @@ -2539,7 +2539,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): @@ -2602,7 +2602,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 = [ @@ -2615,7 +2615,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): @@ -2624,13 +2624,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'])) @@ -2638,7 +2638,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') -- cgit From 2a8cff40af58d6d2b2fc3a818816eb2a913cccfb Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 11:22:32 +0100 Subject: Fix ajaxterm's use of shell=True, prevent vmops.py from running its own version of utils.execute --- nova/utils.py | 4 ---- nova/virt/libvirt/connection.py | 6 +++--- nova/virt/xenapi/vmops.py | 26 ++++++++------------------ 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/nova/utils.py b/nova/utils.py index 0820a2bde..7f27c5fd5 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -138,8 +138,6 @@ def execute(*cmd, **kwargs): :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. - :shell True | False. Defaults to False. If set to True, - Popen command is called shell=True. :run_as_root True | False. Defaults to False. If set to True, the command is prefixed by the command specified in the sudo_helper FLAG. @@ -153,7 +151,6 @@ def execute(*cmd, **kwargs): check_exit_code = kwargs.pop('check_exit_code', 0) delay_on_retry = kwargs.pop('delay_on_retry', True) attempts = kwargs.pop('attempts', 1) - shell = kwargs.pop('shell', False) run_as_root = kwargs.pop('run_as_root', False) if len(kwargs): raise exception.Error(_('Got unknown keyword args ' @@ -175,7 +172,6 @@ def execute(*cmd, **kwargs): stdin=_PIPE, stdout=_PIPE, stderr=_PIPE, - shell=shell, env=env) result = None if process_input is not None: diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 5053daf8f..a01fc3317 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -681,10 +681,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] - utils.execute(cmd, shell=True) + utils.execute(cmd) return {'token': token, 'host': host, 'port': port} def get_host_ip_addr(self): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index a78413370..230ecd560 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1320,12 +1320,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 +1376,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) -- cgit From ed9ea0848e4c8e8220a7f3bc175d7855c88c84d0 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 12:05:05 +0100 Subject: Use close_fds by default since it's good for you --- nova/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/utils.py b/nova/utils.py index 7f27c5fd5..d691481f8 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -172,6 +172,7 @@ def execute(*cmd, **kwargs): stdin=_PIPE, stdout=_PIPE, stderr=_PIPE, + close_fds=True, env=env) result = None if process_input is not None: -- cgit From 7e810e1f266daaa63167ea8412dc0416e88f688f Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 12:21:28 +0100 Subject: Fix usage of sudo -E and addl_env in dnsmasq/radvd calls, remove addl_env support, fix fake_execute allowed kwargs --- nova/network/linux_net.py | 63 +++++++++++++++++++---------------------------- nova/tests/fake_utils.py | 12 ++++++--- nova/utils.py | 8 +----- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 3c62c75cd..a8af4af57 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -617,11 +617,26 @@ def update_dhcp(context, network_ref): 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') @@ -659,8 +674,12 @@ interface %s 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): @@ -704,36 +723,6 @@ 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) diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py index be59970c9..72f79921e 100644 --- a/nova/tests/fake_utils.py +++ b/nova/tests/fake_utils.py @@ -63,9 +63,11 @@ 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) + process_input = kwargs.pop('process_input', 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) 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/utils.py b/nova/utils.py index d691481f8..dc81abdb9 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -132,7 +132,6 @@ 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 @@ -147,7 +146,6 @@ def execute(*cmd, **kwargs): """ 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) @@ -164,16 +162,12 @@ def execute(*cmd, **kwargs): 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, - close_fds=True, - env=env) + close_fds=True) result = None if process_input is not None: result = obj.communicate(process_input) -- cgit From 446fb79eb0025ce50cac9fc5496d4e4840134bce Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 13:30:06 +0100 Subject: Command args can be a tuple, convert them to list --- nova/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/utils.py b/nova/utils.py index dc81abdb9..80044e37d 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -155,7 +155,7 @@ def execute(*cmd, **kwargs): 'to utils.execute: %r') % kwargs) if run_as_root: - cmd = shlex.split(FLAGS.sudo_helper) + cmd + cmd = shlex.split(FLAGS.sudo_helper) + list(cmd) cmd = map(str, cmd) while attempts > 0: -- cgit From 142f072cc3416f88dd0af757b17d7ddd473c5e20 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 13:34:54 +0100 Subject: Remove old commented line --- nova/network/linux_net.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index a8af4af57..8b7b59f55 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -492,7 +492,6 @@ def ensure_bridge(bridge, interface, net_attrs=None): LOG.debug(_('Starting Bridge interface for %s'), interface) _execute('brctl', 'addbr', bridge, run_as_root=True) _execute('brctl', 'setfd', bridge, 0, run_as_root=True) - # _execute('sudo brctl setageing %s 10' % bridge) _execute('brctl', 'stp', bridge, 'off', run_as_root=True) _execute('ip', 'link', 'set', bridge, 'up', run_as_root=True) if net_attrs: -- cgit From 238122f55fe70eedcd670714d80e6d2c10c4f3ab Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 13:47:55 +0100 Subject: Minor fix to reduce diff --- nova/tests/fake_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py index 72f79921e..84ab641ea 100644 --- a/nova/tests/fake_utils.py +++ b/nova/tests/fake_utils.py @@ -63,11 +63,11 @@ def fake_execute(*cmd_parts, **kwargs): """ global _fake_execute_repliers - process_input = kwargs.pop('process_input', 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) + process_input = kwargs.get('process_input', 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) -- cgit From c1ac67222e18c5fe74a52aa864231d5311374816 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 14:11:15 +0100 Subject: Rename sudo_helper FLAG into root_helper --- nova/flags.py | 2 +- nova/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/flags.py b/nova/flags.py index e30c0d6be..35209aa24 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -393,5 +393,5 @@ DEFINE_bool('start_guests_on_host_boot', False, DEFINE_bool('resume_guests_state_on_host_boot', False, 'Whether to start guests, that was running before the host reboot') -DEFINE_string('sudo_helper', 'sudo', +DEFINE_string('root_helper', 'sudo', 'Command prefix to use for running commands as root') diff --git a/nova/utils.py b/nova/utils.py index 80044e37d..46c329c85 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -139,7 +139,7 @@ def execute(*cmd, **kwargs): :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 sudo_helper FLAG. + in the root_helper FLAG. :raises exception.Error on receiving unknown arguments :raises exception.ProcessExecutionError @@ -155,7 +155,7 @@ def execute(*cmd, **kwargs): 'to utils.execute: %r') % kwargs) if run_as_root: - cmd = shlex.split(FLAGS.sudo_helper) + list(cmd) + cmd = shlex.split(FLAGS.root_helper) + list(cmd) cmd = map(str, cmd) while attempts > 0: -- cgit From 56ae11d27bb3a2ee00b0151983fd2a0f14667a0d Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 14:25:52 +0100 Subject: Include missing nova/api/openstack/schemas --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) 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 -- cgit From ed4a3b33647d3cbf5b1733596c1e180078e23cb0 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 9 Aug 2011 10:29:07 -0400 Subject: updating tests; fixing create output; review fixes --- nova/api/openstack/server_metadata.py | 24 ++++++++++++------- nova/compute/api.py | 2 ++ nova/db/sqlalchemy/api.py | 2 +- nova/tests/api/openstack/test_server_metadata.py | 30 ++++++++++++++++++------ 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index a10c48156..97a43fccf 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -57,9 +57,12 @@ class Controller(object): context = req.environ['nova.context'] - self._update_instance_metadata(context, server_id, metadata, False) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + False) - return body + return {'metadata': new_metadata} def update(self, req, server_id, id, body): try: @@ -91,23 +94,26 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._update_instance_metadata(context, server_id, metadata, True) + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + True) - return {'metadata': metadata} + return {'metadata': new_metadata} def _update_instance_metadata(self, context, server_id, metadata, delete=False): try: - self.compute_api.update_instance_metadata(context, - server_id, - metadata, - delete) + 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, AttributeError), ex: + except (ValueError, AttributeError): msg = _("Malformed request body") raise exc.HTTPBadRequest(explanation=msg) diff --git a/nova/compute/api.py b/nova/compute/api.py index 646290e57..867f6ce99 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1192,3 +1192,5 @@ class API(base.Base): self._check_metadata_properties_quota(context, _metadata) self.db.instance_metadata_update(context, instance_id, _metadata, True) + + return _metadata diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2e29f34ac..24ea23611 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3205,7 +3205,7 @@ def instance_metadata_get_item(context, instance_id, key, session=None): def instance_metadata_update(context, instance_id, metadata, delete): session = get_session() - # Set all metadata that isn't passed in if delete kwarg is True + # 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(): diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index a90d572c8..cb43c58f6 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -202,20 +202,29 @@ class ServerMetaDataTest(test.TestCase): self.assertEqual(404, res.status_int) def test_create(self): + 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_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") @@ -225,19 +234,26 @@ class ServerMetaDataTest(test.TestCase): request_metadata = minidom.parseString(""" - value3 - value2 - value1 + value5 """.replace(" ", "").replace("\n", "")) req.body = str(request_metadata.toxml()) response = req.get_response(fakes.wsgi_app()) + expected_metadata = minidom.parseString(""" + + value3 + value2 + value1 + value5 + + """.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', -- cgit From f80ac0c7404882fa0f3e640d1330ab37e6da797a Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Tue, 9 Aug 2011 15:44:43 +0100 Subject: Fix remaining two pep8 violations --- bin/nova-manage | 3 +-- nova/tests/api/openstack/test_server_actions.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 40f22c19c..550454686 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -726,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 diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index bf18bc1b0..311db3f42 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -943,9 +943,7 @@ class TestServerActionXMLDeserializerV11(test.TestCase): flavorRef="http://localhost/flavors/3"/>""" request = self.deserializer.deserialize(serial_request, 'action') expected = { - "resize": { - "flavorRef": "http://localhost/flavors/3" - }, + "resize": {"flavorRef": "http://localhost/flavors/3"}, } self.assertEquals(request['body'], expected) -- cgit From d72e36d63b1aefe7731d5c832c2b2fa52227407c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 9 Aug 2011 11:10:14 -0400 Subject: making usage of 'delete' argument more clear --- nova/api/openstack/server_metadata.py | 9 ++++++--- nova/compute/api.py | 8 +++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 97a43fccf..2b235f79a 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -60,7 +60,7 @@ class Controller(object): new_metadata = self._update_instance_metadata(context, server_id, metadata, - False) + delete=False) return {'metadata': new_metadata} @@ -82,7 +82,10 @@ class Controller(object): raise exc.HTTPBadRequest(explanation=expl) context = req.environ['nova.context'] - self._update_instance_metadata(context, server_id, meta_item, False) + self._update_instance_metadata(context, + server_id, + meta_item, + delete=False) return {'meta': meta_item} @@ -97,7 +100,7 @@ class Controller(object): new_metadata = self._update_instance_metadata(context, server_id, metadata, - True) + delete=True) return {'metadata': new_metadata} diff --git a/nova/compute/api.py b/nova/compute/api.py index 867f6ce99..aaff8b370 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1183,14 +1183,12 @@ class API(base.Base): `metadata` argument will be deleted. """ - if not delete: + if delete: + _metadata = metadata + else: _metadata = self.get_instance_metadata(context, instance_id) _metadata.update(metadata) - else: - _metadata = metadata self._check_metadata_properties_quota(context, _metadata) - self.db.instance_metadata_update(context, instance_id, _metadata, True) - return _metadata -- cgit From 73a26895d850d717d5bd5f106edc6c9ae09218a4 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 9 Aug 2011 11:47:46 -0400 Subject: fixing one pep8 failure --- nova/tests/api/openstack/test_server_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index cb43c58f6..ec446f0f0 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -218,7 +218,7 @@ class ServerMetaDataTest(test.TestCase): input['metadata'].update({ "key1": "value1", "key2": "value2", - "key3":"value3", + "key3": "value3", }) self.assertEqual(input, res_dict) -- cgit From 47229cb10c7a322755d36229649c9d3e5712592d Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Tue, 9 Aug 2011 17:32:39 +0000 Subject: Be more tolerant of agent failures. The instance still booted (most likely) so don't treat it like it didn't --- nova/virt/xenapi/vmops.py | 90 ++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index b913e764e..50aa0d3b2 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -282,6 +282,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 +293,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 +327,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 +345,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 +601,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 +638,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 +660,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 +675,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 +705,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.""" @@ -1178,8 +1169,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): @@ -1197,20 +1199,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): -- cgit From d5bcc6764b418d7aa16b53b50173261385445ee8 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 9 Aug 2011 11:31:48 -0700 Subject: initial port --- nova/api/openstack/contrib/keypairs.py | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 nova/api/openstack/contrib/keypairs.py diff --git a/nova/api/openstack/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py new file mode 100644 index 000000000..879bd9712 --- /dev/null +++ b/nova/api/openstack/contrib/keypairs.py @@ -0,0 +1,137 @@ +# 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""" + +from webob import exc + +from nova import db +from nova import crypto +from nova import exception +from nova.api.openstack import extensions + + +class KeypairController(object): + """ Keypair API controller for the Openstack API """ + + # TODO(ja): the keypair crud logic should be in nova.compute.API? + + def _gen_key(self): + """ + Generate a key + """ + # TODO(ja): crypto.generate_key_pair is currently a slow method + # and should probably be moved to a process pool? + 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 key_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: + key_name (required) - string + fingerprint (optional) - string + public_key (optional) - string + """ + + context = req.environ['nova.context'] + params = body['keypair'] + key_name = params['key_name'] + + # NOTE(ja): generation is slow, so shortcut invalid key_name exception + try: + db.key_pair_get(context, context.user_id, key_name) + raise exception.KeyPairExists(key_name=key_name) + except exception.NotFound: + pass + + keypair = {'user_id': context.user_id, + 'name': key_name} + + # import if public_key is sent + if 'public_key' in params: + keypair['public_key'] = params['public_key'] + keypair['fingerprint'] = params.get('fingerprint', None) + 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, name): + """ + Delete a keypair with a given name + """ + context = req.environ['nova.context'] + db.key_pair_destroy(context, context.user_id, name) + 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({ + 'name': key_pair['name'], + 'key_name': key_pair['name'], + '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 -- cgit From 057b6ad650013a952f88f6e02f3e3db0164084d1 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 9 Aug 2011 13:00:13 -0700 Subject: added tests - list doesn't pass due to unicode issues --- nova/api/openstack/contrib/keypairs.py | 26 +++--- nova/crypto.py | 10 ++- nova/tests/api/openstack/contrib/test_keypairs.py | 99 +++++++++++++++++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 nova/tests/api/openstack/contrib/test_keypairs.py diff --git a/nova/api/openstack/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py index 879bd9712..8435494e1 100644 --- a/nova/api/openstack/contrib/keypairs.py +++ b/nova/api/openstack/contrib/keypairs.py @@ -23,19 +23,20 @@ from nova import db from nova import crypto from nova import exception from nova.api.openstack import extensions - +import os +import shutil +import tempfile class KeypairController(object): """ Keypair API controller for the Openstack API """ - # TODO(ja): the keypair crud logic should be in nova.compute.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 """ - # TODO(ja): crypto.generate_key_pair is currently a slow method - # and should probably be moved to a process pool? private_key, public_key, fingerprint = crypto.generate_key_pair() return {'private_key': private_key, 'public_key': public_key, @@ -52,7 +53,6 @@ class KeypairController(object): params: keypair object with: key_name (required) - string - fingerprint (optional) - string public_key (optional) - string """ @@ -72,8 +72,14 @@ class KeypairController(object): # 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'] = params.get('fingerprint', None) + keypair['fingerprint'] = fingerprint else: generated_key = self._gen_key() keypair['private_key'] = generated_key['private_key'] @@ -83,12 +89,12 @@ class KeypairController(object): db.key_pair_create(context, keypair) return {'keypair': keypair} - def delete(self, req, name): + 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, name) + db.key_pair_destroy(context, context.user_id, id) return exc.HTTPAccepted() def index(self, req): @@ -99,11 +105,11 @@ class KeypairController(object): key_pairs = db.key_pair_get_all_by_user(context, context.user_id) rval = [] for key_pair in key_pairs: - rval.append({ + rval.append({'keypair': { 'name': key_pair['name'], 'key_name': key_pair['name'], 'fingerprint': key_pair['fingerprint'], - }) + }}) return {'keypairs': rval} 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/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py new file mode 100644 index 000000000..1f6028e08 --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -0,0 +1,99 @@ +# 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, key_name): + if not (user_id and key_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': {'key_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': {'key_name': 'create_test', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznAx9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6YQj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymiMZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7ljj5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGcj7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwBbHkXa6OciiJDvkRzJXzf'}} + 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) + -- cgit From c95954ca1a704b6f6e53e7b37f797ad51cb5efa9 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Tue, 9 Aug 2011 14:17:56 -0700 Subject: adding myself to authors --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index b216873df..e639cbf76 100644 --- a/Authors +++ b/Authors @@ -37,6 +37,7 @@ Hisaharu Ishii Hisaki Ohara Ilya Alekseyev Isaku Yamahata +Jake Dahn Jason Cannavale Jason Koelker Jay Pipes -- cgit From 568188c158db3caddfcd9ecb384189f93b076dd9 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 9 Aug 2011 14:23:30 -0700 Subject: add Keypairs to test_extensions --- nova/tests/api/openstack/test_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 409fa0e71..eb9afbdb1 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -96,7 +96,7 @@ 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", "Volumes"]) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [ @@ -143,7 +143,7 @@ class ExtensionControllerTest(test.TestCase): # Make sure we have all the extensions. exts = root.findall('{0}extension'.format(NS)) - self.assertEqual(len(exts), 6) + self.assertEqual(len(exts), 7) # Make sure that at least Fox in Sox is correct. (fox_ext,) = [x for x in exts if x.get('alias') == 'FOXNSOX'] -- cgit From cfa2303fcb0b59e64504d079256e4356fa3bf01f Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Tue, 9 Aug 2011 14:45:31 -0700 Subject: adding other emails to mailmap --- .mailmap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mailmap b/.mailmap index 76e7bc669..5c8df80e0 100644 --- a/.mailmap +++ b/.mailmap @@ -18,6 +18,8 @@ + + -- cgit From 131b2185e23567895e3f87cdfe9c2822d18910b2 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 9 Aug 2011 16:12:59 -0700 Subject: tests pass --- nova/api/openstack/contrib/keypairs.py | 16 ++++++++-------- nova/tests/api/openstack/contrib/test_keypairs.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py index 8435494e1..d718846f1 100644 --- a/nova/api/openstack/contrib/keypairs.py +++ b/nova/api/openstack/contrib/keypairs.py @@ -46,29 +46,29 @@ class KeypairController(object): """ Create or import keypair. - Sending key_name will generate a key and return private_key + 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: - key_name (required) - string + name (required) - string public_key (optional) - string """ context = req.environ['nova.context'] params = body['keypair'] - key_name = params['key_name'] + name = params['name'] - # NOTE(ja): generation is slow, so shortcut invalid key_name exception + # NOTE(ja): generation is slow, so shortcut invalid name exception try: - db.key_pair_get(context, context.user_id, key_name) - raise exception.KeyPairExists(key_name=key_name) + 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': key_name} + 'name': name} # import if public_key is sent if 'public_key' in params: @@ -107,7 +107,7 @@ class KeypairController(object): for key_pair in key_pairs: rval.append({'keypair': { 'name': key_pair['name'], - 'key_name': key_pair['name'], + 'public_key': key_pair['public_key'], 'fingerprint': key_pair['fingerprint'], }}) diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py index 1f6028e08..c9dc34d65 100644 --- a/nova/tests/api/openstack/contrib/test_keypairs.py +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -36,8 +36,8 @@ def db_key_pair_create(self, keypair): pass -def db_key_pair_destroy(context, user_id, key_name): - if not (user_id and key_name): +def db_key_pair_destroy(context, user_id, name): + if not (user_id and name): raise Exception() @@ -65,7 +65,7 @@ class KeypairsTest(test.TestCase): self.assertEqual(res_dict, response) def test_keypair_create(self): - body = {'keypair': {'key_name': 'create_test'}} + body = {'keypair': {'name': 'create_test'}} req = webob.Request.blank('/v1.1/os-keypairs') req.method = 'POST' req.body = json.dumps(body) @@ -77,7 +77,7 @@ class KeypairsTest(test.TestCase): self.assertTrue(len(res_dict['keypair']['private_key']) > 0) def test_keypair_import(self): - body = {'keypair': {'key_name': 'create_test', + body = {'keypair': {'name': 'create_test', 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznAx9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6YQj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymiMZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7ljj5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGcj7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwBbHkXa6OciiJDvkRzJXzf'}} req = webob.Request.blank('/v1.1/os-keypairs') req.method = 'POST' -- cgit From 8c6ebaf9861e3b652663c0ff60ad545864f72677 Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 9 Aug 2011 19:34:51 -0400 Subject: use correct variable name --- nova/db/sqlalchemy/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 1da54b3da..8119cdfb8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3346,8 +3346,8 @@ def instance_metadata_update(context, instance_id, metadata, delete): 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() item.update({"key": meta_key, "instance_id": instance_id}) -- cgit From 83ed9faa488b3ec0f1cb16e7147293c912e2fc2b Mon Sep 17 00:00:00 2001 From: Matthew Hooker Date: Tue, 9 Aug 2011 19:35:40 -0400 Subject: pep8 fix --- nova/api/openstack/contrib/floating_ips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 52c9c6cf9..2aba1068a 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -102,7 +102,7 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] ip = self.network_api.get_floating_ip(context, id) - + if 'fixed_ip' in ip: try: self.disassociate(req, id, '') -- cgit From f73b6dc8e90b763da1fe86496fc6fd6a80b99f0a Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Tue, 9 Aug 2011 17:03:24 -0700 Subject: List security groups project wise for admin users same as other users --- nova/api/openstack/contrib/security_groups.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index d3a8e21b8..1d70956dc 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -110,12 +110,8 @@ class SecurityGroupController(object): context = req.environ['nova.context'] self.compute_api.ensure_default_security_group(context) - if context.is_admin: - groups = db.security_group_get_all(context) - else: - groups = db.security_group_get_by_project(context, - context.project_id) - + 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] -- cgit From 0719b038ba1548f98668991507c001eb18c82981 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 9 Aug 2011 17:22:01 -0700 Subject: import formatting - thx --- nova/api/openstack/contrib/keypairs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py index d718846f1..201648ab5 100644 --- a/nova/api/openstack/contrib/keypairs.py +++ b/nova/api/openstack/contrib/keypairs.py @@ -17,15 +17,17 @@ """ Keypair management extension""" +import os +import shutil +import tempfile + from webob import exc -from nova import db from nova import crypto +from nova import db from nova import exception from nova.api.openstack import extensions -import os -import shutil -import tempfile + class KeypairController(object): """ Keypair API controller for the Openstack API """ -- cgit From 6548ce754984f2eb5e72612392a8a3392c2a21a2 Mon Sep 17 00:00:00 2001 From: John Tran Date: Tue, 9 Aug 2011 18:43:18 -0700 Subject: fix so that the exception shows up in euca2ools instead of UnknownError --- nova/api/ec2/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 8b6e47cfb..ffd5382bf 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -358,6 +358,10 @@ class Executor(wsgi.Application): 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), -- cgit From 5ad921d7ae360b60c14e1e85b237af7f02b14fdc Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Wed, 10 Aug 2011 10:09:50 +0100 Subject: Pass py_modules=[] to setup to avoid installing run_tests.py as a top-level module. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8121e250b..b0e574b20 100644 --- a/setup.py +++ b/setup.py @@ -133,4 +133,5 @@ setup(name='nova', 'bin/stack', 'bin/nova-volume', 'bin/nova-vncproxy', - 'tools/nova-debug']) + 'tools/nova-debug'], + py_modules=[]) -- cgit -- cgit From b3b13df03f0e843300f63bded410ffc4e0bd4e9f Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Wed, 10 Aug 2011 11:25:38 -0500 Subject: Removed verbose debugging output when capabilities are reported. --- nova/scheduler/zone_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index 97bdf3d44..9d05ea42e 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -198,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 -- cgit From a46964ad11a85effa833decb81384b478a0cf75d Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 10 Aug 2011 13:21:36 -0400 Subject: Allows multiple MySQL connections to be maintained using eventlet's db_pool. --- nova/db/sqlalchemy/session.py | 92 ++++++++++++++++++++++++++++++++----------- nova/flags.py | 6 +++ 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 4a9a28f43..726a801af 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -19,37 +19,81 @@ 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: + LOG.debug(_("Unable to load MySQLdb module.")) -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, + } + + LOG.info(_("SQL connection: %s") % FLAGS.sql_connection) + + if "sqlite" in connection_dict.drivername: + engine_args["poolclass"] = sqlalchemy.pool.NullPool + + if "mysql" in connection_dict.drivername: + LOG.info(_("Using MySQL/eventlet DB connection pool.")) + pool_args = { + "db": connection_dict.database, + "user": connection_dict.username, + "passwd": connection_dict.password, + "host": connection_dict.host, + "min_size": FLAGS.sql_min_pool_size, + "max_size": FLAGS.sql_max_pool_size, + "max_idle": FLAGS.sql_idle_timeout, + "max_age": 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/flags.py b/nova/flags.py index eb6366ed9..fa5528c8c 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -345,6 +345,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') -- cgit From 93c4a691c28668d62103b2ae2f90b284950cd95f Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 10 Aug 2011 14:01:18 -0400 Subject: Make sure to not use MySQLdb if you don't have it. --- nova/db/sqlalchemy/session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 726a801af..6ef2d7b2c 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -38,7 +38,7 @@ LOG = nova.log.getLogger("nova.db.sqlalchemy") try: import MySQLdb except ImportError: - LOG.debug(_("Unable to load MySQLdb module.")) + MySQLdb = None _ENGINE = None @@ -72,8 +72,8 @@ def get_engine(): if "sqlite" in connection_dict.drivername: engine_args["poolclass"] = sqlalchemy.pool.NullPool - if "mysql" in connection_dict.drivername: - LOG.info(_("Using MySQL/eventlet DB connection pool.")) + if MySQLdb and "mysql" in connection_dict.drivername: + LOG.info(_("Using MySQLdb/eventlet DB connection pool.")) pool_args = { "db": connection_dict.database, "user": connection_dict.username, -- cgit From ad3ccef3e86220b480a114bb70eaa9def2abd430 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 10 Aug 2011 14:03:16 -0400 Subject: Removed un-needed log line. --- nova/db/sqlalchemy/session.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 6ef2d7b2c..7ebe000e8 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -67,8 +67,6 @@ def get_engine(): "echo": False, } - LOG.info(_("SQL connection: %s") % FLAGS.sql_connection) - if "sqlite" in connection_dict.drivername: engine_args["poolclass"] = sqlalchemy.pool.NullPool -- cgit From 8331fee7695a40824ae1bd24c52b22987b5f3507 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 10 Aug 2011 15:06:04 -0400 Subject: elif and FLAG feedback --- nova/db/sqlalchemy/session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 7ebe000e8..81cb9fad0 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -70,7 +70,7 @@ def get_engine(): if "sqlite" in connection_dict.drivername: engine_args["poolclass"] = sqlalchemy.pool.NullPool - if MySQLdb and "mysql" in connection_dict.drivername: + elif MySQLdb and "mysql" in connection_dict.drivername: LOG.info(_("Using MySQLdb/eventlet DB connection pool.")) pool_args = { "db": connection_dict.database, @@ -80,7 +80,6 @@ def get_engine(): "min_size": FLAGS.sql_min_pool_size, "max_size": FLAGS.sql_max_pool_size, "max_idle": FLAGS.sql_idle_timeout, - "max_age": FLAGS.sql_idle_timeout, } creator = eventlet.db_pool.ConnectionPool(MySQLdb, **pool_args) engine_args["pool_size"] = FLAGS.sql_max_pool_size -- cgit From 4d2d064e9da37ce72010408bc1aad8ca67708462 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 10 Aug 2011 16:20:31 -0400 Subject: Support for postgresql. --- nova/db/sqlalchemy/session.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 81cb9fad0..07ca27bab 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -41,6 +41,12 @@ except ImportError: MySQLdb = None +try: + import psycopg2 +except ImportError: + psycopg2 = None + + _ENGINE = None _MAKER = None @@ -62,28 +68,35 @@ def get_session(autocommit=True, expire_on_commit=False): 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, + "pool_size": FLAGS.sql_max_pool_size, + "pool_timeout": FLAGS.sql_pool_timeout, "echo": False, } + pool_args = { + "db": connection_dict.database, + "user": connection_dict.username, + "passwd": connection_dict.password, + "host": connection_dict.host, + "min_size": FLAGS.sql_min_pool_size, + "max_size": FLAGS.sql_max_pool_size, + "max_idle": FLAGS.sql_idle_timeout, + } + if "sqlite" in connection_dict.drivername: + del engine_args["pool_size"] + del engine_args["pool_timeout"] engine_args["poolclass"] = sqlalchemy.pool.NullPool elif MySQLdb and "mysql" in connection_dict.drivername: - LOG.info(_("Using MySQLdb/eventlet DB connection pool.")) - pool_args = { - "db": connection_dict.database, - "user": connection_dict.username, - "passwd": connection_dict.password, - "host": connection_dict.host, - "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 + + elif psycopg2 and "postgresql" in connection_dict.drivername: + creator = eventlet.db_pool.ConnectionPool(psycopg2, **pool_args) engine_args["creator"] = creator.create return sqlalchemy.create_engine(FLAGS.sql_connection, **engine_args) -- cgit From 7b72972cbc9fbd267160d8d3282e1d0ec888de98 Mon Sep 17 00:00:00 2001 From: John Tran Date: Wed, 10 Aug 2011 16:19:21 -0700 Subject: removed typos, end of line chars --- nova/api/ec2/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index ffd5382bf..96df97393 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -358,10 +358,10 @@ class Executor(wsgi.Application): 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.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), -- cgit From 0a543d4f8ff31733c32cbd9063e461ca41a0b076 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 10 Aug 2011 21:27:40 -0400 Subject: Changed bad server actions requests to raise an HTTP 400 --- nova/api/openstack/servers.py | 10 +++++++--- nova/tests/api/openstack/test_extensions.py | 2 +- nova/tests/api/openstack/test_server_actions.py | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 736fdf6ce..c7d17a5bc 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -210,11 +210,15 @@ class Controller(object): } self.actions.update(admin_actions) - for key in self.actions.keys(): - if key in body: + for key in body.keys(): + 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. diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 8b7e11a5b..12eee3367 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -280,7 +280,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")) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 717e11c00..687a19390 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -352,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) @@ -487,6 +487,24 @@ class ServerActionsTestV11(test.TestCase): 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) -- cgit From 7d9ff45ec1acc3cba0d58341f3cce011df140f8e Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Thu, 11 Aug 2011 12:37:29 +0100 Subject: Remove doublequotes from env variable setting since they are literally passed --- nova/network/linux_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 8b7b59f55..4e1e1f85a 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -616,8 +616,8 @@ def update_dhcp(context, network_ref): else: LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), pid) - cmd = ['FLAGFILE="%s"' % FLAGS.dhcpbridge_flagfile, - 'DNSMASQ_INTERFACE="%s"' % network_ref['bridge'], + cmd = ['FLAGFILE=%s' % FLAGS.dhcpbridge_flagfile, + 'DNSMASQ_INTERFACE=%s' % network_ref['bridge'], 'dnsmasq', '--strict-order', '--bind-interfaces', -- cgit From 8517d9563191b635669032e8364d8fa64876b977 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 11 Aug 2011 10:53:40 -0400 Subject: Fixed per HACKING --- nova/api/openstack/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c7d17a5bc..c3bb23adb 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -214,10 +214,10 @@ class Controller(object): if key in self.actions: return self.actions[key](body, req, id) else: - msg = _('There is no such server action: %s' % key) + msg = _("There is no such server action: %s") % (key,)) raise exc.HTTPBadRequest(explanation=msg) - msg = _('Invalid request body') + msg = _("Invalid request body") raise exc.HTTPBadRequest(explanation=msg) def _action_create_backup(self, input_dict, req, instance_id): -- cgit From b121cd266d3d5e1719e644d6bd82d6402f13d2e2 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 11 Aug 2011 11:55:02 -0400 Subject: Logging for SQLAlchemy type. --- nova/db/sqlalchemy/session.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 07ca27bab..073e4ae49 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -77,10 +77,8 @@ def get_engine(): } pool_args = { - "db": connection_dict.database, - "user": connection_dict.username, - "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, @@ -92,10 +90,20 @@ def get_engine(): engine_args["poolclass"] = sqlalchemy.pool.NullPool elif MySQLdb and "mysql" in connection_dict.drivername: + LOG.info(_("Using mysql/eventlet db_pool.")) + pool_args.update({ + "db": connection_dict.database, + "passwd": connection_dict.password, + }) creator = eventlet.db_pool.ConnectionPool(MySQLdb, **pool_args) engine_args["creator"] = creator.create elif psycopg2 and "postgresql" in connection_dict.drivername: + LOG.info(_("Using postgresql/eventlet db_pool.")) + pool_args.update({ + "database": connection_dict.database, + "password": connection_dict.password, + }) creator = eventlet.db_pool.ConnectionPool(psycopg2, **pool_args) engine_args["creator"] = creator.create -- cgit From 7ae64a0b7e1db7e46d183bfa8a2fe1be5d47f1cc Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 11 Aug 2011 11:57:16 -0400 Subject: removed extra paren --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c3bb23adb..4c56539a4 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -214,7 +214,7 @@ class Controller(object): if key in self.actions: return self.actions[key](body, req, id) else: - msg = _("There is no such server action: %s") % (key,)) + msg = _("There is no such server action: %s") % (key,) raise exc.HTTPBadRequest(explanation=msg) msg = _("Invalid request body") -- cgit From 253d75e71beb1ce9c65e84233a3178f95f82d77d Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 11 Aug 2011 12:02:05 -0400 Subject: More logging. --- nova/db/sqlalchemy/session.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 073e4ae49..141b7bf37 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -107,6 +107,9 @@ def get_engine(): creator = eventlet.db_pool.ConnectionPool(psycopg2, **pool_args) engine_args["creator"] = creator.create + LOG.debug(_("SQLAlchemy Engine Arguments: %(engine_args)s") % locals()) + LOG.debug(_("Eventlet Pool Arguments: %(pool_args)s") % locals()) + return sqlalchemy.create_engine(FLAGS.sql_connection, **engine_args) -- cgit From 24869338aad2dfd36db9d466820325d1a3ed1adb Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Thu, 11 Aug 2011 18:01:37 +0000 Subject: Make PUT /servers/ follow the API specs and return a 200 status --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 736fdf6ce..0f8e8e461 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -185,7 +185,7 @@ class Controller(object): except exception.NotFound: raise exc.HTTPNotFound() - return exc.HTTPNoContent() + return webob.Response() # 200 response def _parse_update(self, context, id, inst_dict, update_dict): pass -- cgit From 2ccec88a5a5c85ce7776b4b70d490189d63d3098 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Thu, 11 Aug 2011 11:15:14 -0700 Subject: Added availability zone support to the Create Server API --- bin/nova-manage | 14 +++++++++++--- nova/api/openstack/create_instance_helper.py | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 077a89d6f..02591a49c 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -882,6 +882,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) @@ -889,9 +897,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='', help='Host') @args('--service', dest='service', metavar='', diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 1425521a9..4e1da549e 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -122,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() @@ -161,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: -- cgit From 5dd39df596f7038cffde5079822ae4b747b92b72 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 11 Aug 2011 14:20:18 -0400 Subject: minor cleanup --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 4c56539a4..c22a5a2a6 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -210,7 +210,7 @@ class Controller(object): } self.actions.update(admin_actions) - for key in body.keys(): + for key in body: if key in self.actions: return self.actions[key](body, req, id) else: -- cgit From 49da55f7952f8daecf6df9498769b336af95ce6d Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 11 Aug 2011 14:27:14 -0400 Subject: Removed postgres, bug in current ubuntu package which won't allow it to work easily. Will add a bug in LP. --- nova/db/sqlalchemy/session.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 141b7bf37..ffa3c747c 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -41,12 +41,6 @@ except ImportError: MySQLdb = None -try: - import psycopg2 -except ImportError: - psycopg2 = None - - _ENGINE = None _MAKER = None @@ -76,14 +70,6 @@ def get_engine(): "echo": False, } - pool_args = { - "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, - } - if "sqlite" in connection_dict.drivername: del engine_args["pool_size"] del engine_args["pool_timeout"] @@ -94,22 +80,15 @@ def get_engine(): pool_args.update({ "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["creator"] = creator.create - elif psycopg2 and "postgresql" in connection_dict.drivername: - LOG.info(_("Using postgresql/eventlet db_pool.")) - pool_args.update({ - "database": connection_dict.database, - "password": connection_dict.password, - }) - creator = eventlet.db_pool.ConnectionPool(psycopg2, **pool_args) - engine_args["creator"] = creator.create - - LOG.debug(_("SQLAlchemy Engine Arguments: %(engine_args)s") % locals()) - LOG.debug(_("Eventlet Pool Arguments: %(pool_args)s") % locals()) - return sqlalchemy.create_engine(FLAGS.sql_connection, **engine_args) -- cgit From 3bfaf0a0720fc8713fb77fddd8f1b2dffa0eabfc Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Thu, 11 Aug 2011 18:28:15 +0000 Subject: v1.0 and v1.1 API differs for PUT, so split them out Update tests to match API --- nova/api/openstack/servers.py | 72 ++++++++++++++++++++------------ nova/tests/api/openstack/test_servers.py | 4 +- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 0f8e8e461..90b6e684b 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -161,32 +161,6 @@ class Controller(object): server['server']['adminPass'] = extra_values['password'] return server - @scheduler_api.redirect_handler - def update(self, req, id, body): - """ Updates the server name or password """ - if len(req.body) == 0: - raise exc.HTTPUnprocessableEntity() - - if not body: - raise exc.HTTPUnprocessableEntity() - - ctxt = req.environ['nova.context'] - update_dict = {} - - if 'name' in body['server']: - name = body['server']['name'] - 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 webob.Response() # 200 response - def _parse_update(self, context, id, inst_dict, update_dict): pass @@ -545,6 +519,29 @@ class Controller(object): class ControllerV10(Controller): """v1.0 OpenStack API controller""" + @scheduler_api.redirect_handler + def update(self, req, id, body): + """ Updates the server name or password """ + if len(req.body) == 0 or not body: + raise exc.HTTPUnprocessableEntity() + + ctxt = req.environ['nova.context'] + update_dict = {} + + if 'name' in body['server']: + name = body['server']['name'] + 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() + @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ @@ -614,6 +611,29 @@ class ControllerV10(Controller): class ControllerV11(Controller): """v1.1 OpenStack API controller""" + @scheduler_api.redirect_handler + def update(self, req, id, body): + """ Updates the server name or password """ + if len(req.body) == 0 or not body: + raise exc.HTTPUnprocessableEntity() + + ctxt = req.environ['nova.context'] + update_dict = {} + + if 'name' in body['server']: + name = body['server']['name'] + 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() + + # v1.1 API returns 200, which differs from v1.0 + @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b6342ae2f..b732d2972 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1869,7 +1869,7 @@ class ServersTest(test.TestCase): req.content_type = 'application/json' req.body = json.dumps({'server': {'name': 'new-name'}}) res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 204) + self.assertEqual(res.status_int, 200) self.assertEqual(res.body, '') def test_update_server_adminPass_ignored_v1_1(self): @@ -1889,7 +1889,7 @@ class ServersTest(test.TestCase): 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.status_int, 200) self.assertEqual(res.body, '') def test_create_backup_schedules(self): -- cgit From 3017d3a7cd9cd4928a5e5247054b877e63fac095 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 11 Aug 2011 14:36:29 -0400 Subject: Silly fixes. --- nova/db/sqlalchemy/session.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index ffa3c747c..07f281938 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -65,19 +65,15 @@ def get_engine(): engine_args = { "pool_recycle": FLAGS.sql_idle_timeout, - "pool_size": FLAGS.sql_max_pool_size, - "pool_timeout": FLAGS.sql_pool_timeout, "echo": False, } if "sqlite" in connection_dict.drivername: - del engine_args["pool_size"] - del engine_args["pool_timeout"] engine_args["poolclass"] = sqlalchemy.pool.NullPool elif MySQLdb and "mysql" in connection_dict.drivername: LOG.info(_("Using mysql/eventlet db_pool.")) - pool_args.update({ + pool_args = { "db": connection_dict.database, "passwd": connection_dict.password, "host": connection_dict.host, @@ -85,8 +81,10 @@ def get_engine(): "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) -- cgit From 45d6ab8ffec6ff4b26500df7049ce4092b15f00c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 11 Aug 2011 15:30:43 -0400 Subject: fixing id parsing --- nova/api/openstack/common.py | 9 ++++++--- nova/tests/api/openstack/test_common.py | 4 ++++ nova/tests/api/openstack/test_servers.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index dfdd62201..23614d598 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -169,10 +169,13 @@ def get_id_from_href(href): Returns: 123 """ - if re.match(r'\d+$', str(href)): - return int(href) try: - return int(urlparse.urlsplit(href).path.split('/')[-1]) + href = str(href) + + if re.match(r'\d+$', href): + return int(href) + else: + 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')) 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_servers.py b/nova/tests/api/openstack/test_servers.py index b6342ae2f..290f6e990 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1653,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() -- cgit From 8f3ad17b4ee25fedeee98132f22cf1eeb5974a2c Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 11 Aug 2011 16:04:12 -0400 Subject: fixed pep8 issue --- nova/tests/api/openstack/contrib/test_keypairs.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py index c9dc34d65..23f19157e 100644 --- a/nova/tests/api/openstack/contrib/test_keypairs.py +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -77,8 +77,21 @@ class KeypairsTest(test.TestCase): self.assertTrue(len(res_dict['keypair']['private_key']) > 0) def test_keypair_import(self): - body = {'keypair': {'name': 'create_test', - 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznAx9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6YQj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymiMZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7ljj5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGcj7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwBbHkXa6OciiJDvkRzJXzf'}} + 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) -- cgit From 03cf6551feae597dd71fbf7b52b41415863d1241 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 11 Aug 2011 16:05:33 -0400 Subject: spacing fixes --- nova/tests/api/openstack/contrib/test_keypairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py index 23f19157e..eb3bc7af0 100644 --- a/nova/tests/api/openstack/contrib/test_keypairs.py +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -28,6 +28,7 @@ def fake_keypair(name): 'fingerprint': 'FAKE_FINGERPRINT', 'name': name} + def db_key_pair_get_all_by_user(self, user_id): return [fake_keypair('FAKE')] @@ -109,4 +110,3 @@ class KeypairsTest(test.TestCase): req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) - -- cgit From 684c41e7a4aa5fdab78f2e1aac1d309c3bb16412 Mon Sep 17 00:00:00 2001 From: "vladimir.p" Date: Thu, 11 Aug 2011 13:18:30 -0700 Subject: lp824780: fixed typo in update_service_capabilities --- nova/scheduler/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index c8b16b622..294de62e4 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -71,8 +71,8 @@ class SchedulerManager(manager.Manager): def update_service_capabilities(self, context=None, service_name=None, host=None, capabilities=None): """Process a capability update from a service node.""" - if not capability: - capability = {} + if not capabilities: + capabilities = {} self.zone_manager.update_service_capabilities(service_name, host, capabilities) -- cgit From 26d96b80fdc07d8bb9453112cd33ee12143c6f46 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Thu, 11 Aug 2011 20:48:16 +0000 Subject: v1.1 API also requires the server be returned in the body --- nova/api/openstack/servers.py | 83 +++++++++++++------------------- nova/tests/api/openstack/test_servers.py | 6 ++- 2 files changed, 37 insertions(+), 52 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 90b6e684b..f19befd6f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -161,8 +161,32 @@ class Controller(object): server['server']['adminPass'] = extra_values['password'] return server - def _parse_update(self, context, id, inst_dict, update_dict): - pass + @scheduler_api.redirect_handler + def update(self, req, id, body): + """ Updates the server name or password """ + if len(req.body) == 0: + raise exc.HTTPUnprocessableEntity() + + if not body: + raise exc.HTTPUnprocessableEntity() + + ctxt = req.environ['nova.context'] + update_dict = {} + + if 'name' in body['server']: + name = body['server']['name'] + self.helper._validate_server_name(name) + update_dict['display_name'] = name.strip() + + try: + self.compute_api.update(ctxt, id, **update_dict) + except exception.NotFound: + raise exc.HTTPNotFound() + + return self._update(ctxt, req, id, body) + + def _update(self, context, req, id, inst_dict): + return exc.HTTPNotImplemented() @scheduler_api.redirect_handler def action(self, req, id, body): @@ -519,29 +543,6 @@ class Controller(object): class ControllerV10(Controller): """v1.0 OpenStack API controller""" - @scheduler_api.redirect_handler - def update(self, req, id, body): - """ Updates the server name or password """ - if len(req.body) == 0 or not body: - raise exc.HTTPUnprocessableEntity() - - ctxt = req.environ['nova.context'] - update_dict = {} - - if 'name' in body['server']: - name = body['server']['name'] - 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() - @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ @@ -565,10 +566,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 """ @@ -611,29 +613,6 @@ class ControllerV10(Controller): class ControllerV11(Controller): """v1.1 OpenStack API controller""" - @scheduler_api.redirect_handler - def update(self, req, id, body): - """ Updates the server name or password """ - if len(req.body) == 0 or not body: - raise exc.HTTPUnprocessableEntity() - - ctxt = req.environ['nova.context'] - update_dict = {} - - if 'name' in body['server']: - name = body['server']['name'] - 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() - - # v1.1 API returns 200, which differs from v1.0 - @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ @@ -713,6 +692,10 @@ 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: diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b732d2972..457ce5aec 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1870,7 +1870,8 @@ class ServersTest(test.TestCase): req.body = json.dumps({'server': {'name': 'new-name'}}) res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - self.assertEqual(res.body, '') + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) def test_update_server_adminPass_ignored_v1_1(self): inst_dict = dict(name='server_test', adminPass='bacon') @@ -1890,7 +1891,8 @@ class ServersTest(test.TestCase): req.body = self.body res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - self.assertEqual(res.body, '') + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedule') -- cgit From 500bd3218906e2575467467700f80f5b31e0a87e Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 11 Aug 2011 16:26:26 -0700 Subject: allow scheduling topics to multiple drivers --- nova/scheduler/manager.py | 5 ++- nova/scheduler/multi.py | 73 ++++++++++++++++++++++++++++++++++ nova/tests/scheduler/test_scheduler.py | 24 ++++++++++- 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 nova/scheduler/multi.py diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index c8b16b622..13c0bf22a 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: 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/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 7a26fd1bb..d70a6779f 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -36,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 @@ -391,7 +392,7 @@ class SimpleDriverTestCase(test.TestCase): compute1.kill() compute2.kill() - def test_wont_sechedule_if_specified_host_is_down_no_queue(self): + def test_wont_schedule_if_specified_host_is_down_no_queue(self): compute1 = service.Service('host1', 'nova-compute', 'compute', @@ -903,6 +904,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 -- cgit From 4ce5c65e4002f1c3cca02bb06d892a6d270a0149 Mon Sep 17 00:00:00 2001 From: "vladimir.p" Date: Thu, 11 Aug 2011 16:32:51 -0700 Subject: Author added --- Authors | 1 + 1 file changed, 1 insertion(+) diff --git a/Authors b/Authors index e639cbf76..ccd70baaf 100644 --- a/Authors +++ b/Authors @@ -103,6 +103,7 @@ Tushar Patil Vasiliy Shlykov Vishvananda Ishaya Vivek Y S +Vladimir Popovski William Wolf Yoshiaki Tamura Youcef Laribi -- cgit From 68161e3e224ff77e4e93d02e5fabbd9ea17b0d48 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Thu, 11 Aug 2011 17:04:33 -0700 Subject: prefixed with os- for the newly added extensions --- nova/api/openstack/contrib/security_groups.py | 4 ++-- .../api/openstack/contrib/test_security_groups.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index 1d70956dc..6c57fbb51 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -366,7 +366,7 @@ class Security_groups(extensions.ExtensionDescriptor): } deserializer = wsgi.RequestDeserializer(body_deserializers) - res = extensions.ResourceExtension('security_groups', + res = extensions.ResourceExtension('os-security-groups', controller=SecurityGroupController(), deserializer=deserializer, serializer=serializer) @@ -378,7 +378,7 @@ class Security_groups(extensions.ExtensionDescriptor): } deserializer = wsgi.RequestDeserializer(body_deserializers) - res = extensions.ResourceExtension('security_group_rules', + res = extensions.ResourceExtension('os-security-group-rules', controller=SecurityGroupRulesController(), deserializer=deserializer, serializer=serializer) diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py index 9a86a60f8..4317880ca 100644 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -25,7 +25,7 @@ from nova.tests.api.openstack import fakes def _get_create_request_json(body_dict): - req = webob.Request.blank('/v1.1/security_groups') + req = webob.Request.blank('/v1.1/os-security-groups') req.headers['Content-Type'] = 'application/json' req.method = 'POST' req.body = json.dumps(body_dict) @@ -84,7 +84,7 @@ class TestSecurityGroups(test.TestCase): return ''.join(body_parts) def _get_create_request_xml(self, body_dict): - req = webob.Request.blank('/v1.1/security_groups') + req = webob.Request.blank('/v1.1/os-security-groups') req.headers['Content-Type'] = 'application/xml' req.content_type = 'application/xml' req.accept = 'application/xml' @@ -99,7 +99,7 @@ class TestSecurityGroups(test.TestCase): return response def _delete_security_group(self, id): - request = webob.Request.blank('/v1.1/security_groups/%s' + request = webob.Request.blank('/v1.1/os-security-groups/%s' % id) request.method = 'DELETE' response = request.get_response(fakes.wsgi_app()) @@ -238,7 +238,7 @@ class TestSecurityGroups(test.TestCase): security_group['description'] = "group-description" response = _create_security_group_json(security_group) - req = webob.Request.blank('/v1.1/security_groups') + 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()) @@ -272,7 +272,7 @@ class TestSecurityGroups(test.TestCase): response = _create_security_group_json(security_group) res_dict = json.loads(response.body) - req = webob.Request.blank('/v1.1/security_groups/%s' % + req = webob.Request.blank('/v1.1/os-security-groups/%s' % res_dict['security_group']['id']) req.headers['Content-Type'] = 'application/json' req.method = 'GET' @@ -292,14 +292,14 @@ class TestSecurityGroups(test.TestCase): self.assertEquals(res_dict, expected) def test_get_security_group_by_invalid_id(self): - req = webob.Request.blank('/v1.1/security_groups/invalid') + 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/security_groups/111111111') + 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()) @@ -354,7 +354,7 @@ class TestSecurityGroupRules(test.TestCase): super(TestSecurityGroupRules, self).tearDown() def _create_security_group_rule_json(self, rules): - request = webob.Request.blank('/v1.1/security_group_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) @@ -362,7 +362,7 @@ class TestSecurityGroupRules(test.TestCase): return response def _delete_security_group_rule(self, id): - request = webob.Request.blank('/v1.1/security_group_rules/%s' + request = webob.Request.blank('/v1.1/os-security-group-rules/%s' % id) request.method = 'DELETE' response = request.get_response(fakes.wsgi_app()) @@ -420,7 +420,7 @@ class TestSecurityGroupRules(test.TestCase): self.assertEquals(response.status_int, 400) def test_create_with_no_body_json(self): - request = webob.Request.blank('/v1.1/security_group_rules') + request = webob.Request.blank('/v1.1/os-security-group-rules') request.headers['Content-Type'] = 'application/json' request.method = 'POST' request.body = json.dumps(None) @@ -428,7 +428,7 @@ class TestSecurityGroupRules(test.TestCase): self.assertEquals(response.status_int, 422) def test_create_with_no_security_group_rule_in_body_json(self): - request = webob.Request.blank('/v1.1/security_group_rules') + request = webob.Request.blank('/v1.1/os-security-group-rules') request.headers['Content-Type'] = 'application/json' request.method = 'POST' body_dict = {'test': "test"} -- cgit From c398d6cf85dcb30b8cd499f410618b88a0b5c8c9 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 11 Aug 2011 22:02:21 -0700 Subject: join virtual_interfaces.instance for DB queries for instances. updates instance_get_all to match instance_get_all_by_filters. --- nova/db/sqlalchemy/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 8119cdfb8..e5d35a20b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1139,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')).\ @@ -1202,6 +1205,7 @@ def instance_get_all_by_filters(context, filters): 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')).\ -- cgit From f95e0118d91a8f77345e4d78980e2523cb4dba56 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 12 Aug 2011 10:59:10 -0400 Subject: Fixes to the OSAPI floating API extension DELETE. Updated to use correct args for self.disassociate (don't sweep exceptions which should cause test cases to fail under the rug). Additionally updated to pass network_api.release_floating_ip the address instead of a dict. --- nova/api/openstack/contrib/floating_ips.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 2aba1068a..c07bfdf09 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -104,12 +104,9 @@ class FloatingIPController(object): ip = self.network_api.get_floating_ip(context, id) if 'fixed_ip' in ip: - try: - self.disassociate(req, id, '') - except Exception as e: - LOG.exception(_("Error disassociating fixed_ip %s"), e) + self.disassociate(req, id) - self.network_api.release_floating_ip(context, address=ip) + self.network_api.release_floating_ip(context, address=ip['address']) return {'released': { "id": ip['id'], -- cgit From 15271a08de44da1813bfb2a2b68a2f28ef887c21 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Fri, 12 Aug 2011 08:43:42 -0700 Subject: fix typo that causes ami instances to launch with a kernal as ramdisk --- nova/virt/xenapi/vmops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index b9cd59946..b1522729a 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', -- cgit From 954e8e24c6b8ceb541c539ce7c26da4b35b5f0b1 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 12 Aug 2011 11:44:49 -0400 Subject: rewriting parsing --- nova/api/openstack/common.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 23614d598..b2a675653 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -169,16 +169,20 @@ def get_id_from_href(href): Returns: 123 """ + 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: - href = str(href) - - if re.match(r'\d+$', href): - return int(href) - else: - 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')) + return int(urlparse.urlsplit(href).path.split('/')[-1]) + except ValueError as error: + LOG.debug(_("Failed to parse ID from %(href)s: %(error)s") % locals()) + raise def remove_version_from_href(href): -- cgit From 258e169a60d3551e789022ec23d6ae040c1f981e Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Fri, 12 Aug 2011 20:18:47 +0000 Subject: Stub out instance_get as well so we can show the results of the name change --- nova/api/openstack/servers.py | 2 +- nova/tests/api/openstack/test_servers.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f19befd6f..42e46a94a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -163,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() diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index d43a40070..1a4288ae7 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, ) @@ -1880,14 +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, 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') @@ -1898,8 +1901,9 @@ 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' @@ -1909,6 +1913,7 @@ class ServersTest(test.TestCase): 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') -- cgit From e7858fabb433a0ee587a9444f749381bf36d5d92 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Fri, 12 Aug 2011 23:58:13 +0000 Subject: Added durable option for nova rabbit queues added queueu delete script for admin/debug purposes --- bin/clear_rabbit_queues | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ nova/flags.py | 1 + nova/rpc/amqp.py | 8 ++++-- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100755 bin/clear_rabbit_queues 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/nova/flags.py b/nova/flags.py index eb6366ed9..db3906374 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') 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) -- cgit From a1e60edee71d2cb24739f2f44ba13fbf28e72c95 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 12 Aug 2011 21:03:11 -0700 Subject: get rid of network_info hack and pass it everywhere --- nova/virt/libvirt/connection.py | 22 ++++++-------- nova/virt/libvirt/firewall.py | 64 +++++++++++++++------------------------ nova/virt/libvirt/netutils.py | 67 ----------------------------------------- 3 files changed, 33 insertions(+), 120 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 6d043577a..bc317d660 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -471,10 +471,10 @@ class LibvirtConnection(driver.ComputeDriver): # 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.""" @@ -531,7 +531,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} @@ -574,9 +574,9 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(ilyaalekseyev): Implementation like in multinics # for xenapi(tr3buchet) @exception.wrap_exception() - def spawn(self, context, instance, - network_info=None, block_device_info=None): - xml = self.to_xml(instance, False, network_info=network_info, + def spawn(self, context, instance, network_info, + 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) @@ -584,7 +584,7 @@ class LibvirtConnection(driver.ComputeDriver): 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.""" @@ -992,10 +992,6 @@ class LibvirtConnection(driver.ComputeDriver): block_device_info=None): block_device_mapping = driver.block_device_info_get_mapping( block_device_info) - # TODO(adiantum) remove network_info creation code - # when multinics will be completed - if not network_info: - network_info = netutils.get_network_info(instance) nics = [] for (network, mapping) in network_info: @@ -1082,7 +1078,7 @@ class LibvirtConnection(driver.ComputeDriver): xml_info['disk'] = xml_info['basepath'] + "/disk" return xml_info - def to_xml(self, instance, rescue=False, network_info=None, + 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']) diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index 9ce57b6c9..fa29b99c3 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 @@ -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): ''' - 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. @@ -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() @@ -500,9 +490,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 +510,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 +519,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 +542,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 +572,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 +591,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 = [] @@ -726,14 +714,10 @@ class IptablesFirewallDriver(FirewallDriver): 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 -- cgit From 87e97a868b4b7361937bac8f637ec014276aaf5c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 12 Aug 2011 21:03:53 -0700 Subject: use dhcp server instead of gateway for filter exception --- nova/virt/libvirt/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index fa29b99c3..a7a67dacb 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -609,7 +609,7 @@ 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 ' -- cgit From 87ff404bf2bffe690292f7d3922c1ca2529f852b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 12 Aug 2011 21:17:20 -0700 Subject: rename project_net to same_net --- nova/virt/libvirt/connection.py | 6 +++--- nova/virt/libvirt/firewall.py | 8 ++++---- nova/virt/libvirt/vif.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index bc317d660..f905ce92b 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 """ @@ -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') diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index a7a67dacb..11e3906b8 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -232,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) @@ -378,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') @@ -616,7 +616,7 @@ class IptablesFirewallDriver(FirewallDriver): '-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,)) @@ -633,7 +633,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] diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index e243d4fa0..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 = "\n" net, mask = netutils.get_net_and_mask(network['cidr']) values = [("PROJNET", net), ("PROJMASK", mask)] -- cgit From f7d1270c94d884e661a79d74fb2b2f88f6eb619f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 12 Aug 2011 22:05:34 -0700 Subject: fix all of the tests --- nova/tests/test_libvirt.py | 84 ++++++++++++++++++++++------------------- nova/virt/libvirt/connection.py | 8 ++-- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 2180cf4f0..df291ee68 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -49,18 +49,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}]} @@ -273,15 +274,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): @@ -408,16 +408,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, @@ -431,7 +431,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 = [ @@ -528,17 +529,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 @@ -942,8 +946,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) @@ -1008,7 +1013,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 @@ -1023,7 +1028,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") @@ -1043,11 +1048,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) @@ -1057,14 +1063,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'] @@ -1094,8 +1100,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] @@ -1247,7 +1253,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', @@ -1263,9 +1269,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']) @@ -1296,11 +1303,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/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index f905ce92b..5945a725d 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -463,8 +463,8 @@ 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 @@ -988,7 +988,7 @@ class LibvirtConnection(driver.ComputeDriver): else: raise exception.InvalidDevicePath(path=device_path) - def _prepare_xml_info(self, instance, rescue=False, network_info=None, + 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) @@ -1082,7 +1082,7 @@ class LibvirtConnection(driver.ComputeDriver): 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, + 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']) -- cgit From c533e6ed3d2df8725dbcb48e7e546eb853b7ad41 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 12 Aug 2011 22:36:10 -0700 Subject: make sure security groups come back on restart of nova-compute --- nova/compute/manager.py | 6 ++++-- nova/tests/test_compute.py | 4 ++-- nova/tests/test_libvirt.py | 2 ++ nova/virt/driver.py | 2 +- nova/virt/fake.py | 4 ++-- nova/virt/libvirt/connection.py | 9 +++++---- nova/virt/libvirt/firewall.py | 14 ++++++-------- nova/virt/xenapi_conn.py | 2 +- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d38213083..5b98e9ec1 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -170,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')) @@ -1308,7 +1310,7 @@ 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): """Executing live migration. diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 73c9bd78d..9d6e5aee5 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -632,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 @@ -657,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 diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index df291ee68..7f4a3b09a 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -644,6 +644,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() @@ -653,6 +654,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')) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index df4a66ac2..20af2666d 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -252,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 diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 880702af1..2ffa33d40 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -487,7 +487,7 @@ 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.') @@ -496,7 +496,7 @@ class FakeConnection(driver.ComputeDriver): """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.') diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 5945a725d..71516011a 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1502,7 +1502,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. @@ -1532,14 +1532,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: diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index 11e3906b8..55fc58458 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -92,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() @@ -391,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)) @@ -702,15 +700,15 @@ class IptablesFirewallDriver(FirewallDriver): 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 - 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) diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 76b6c57fc..0ec957cf3 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -309,7 +309,7 @@ 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 -- cgit From b46320a4175adc4012e60d4eae793a42f3a8186b Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Mon, 15 Aug 2011 02:55:22 -0700 Subject: make list response for floating ip match other apis --- nova/api/openstack/contrib/floating_ips.py | 4 ++-- nova/tests/api/openstack/contrib/test_floating_ips.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index c07bfdf09..f6824a601 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -43,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): diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index ab7ae2e54..7996ebbb9 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): -- cgit From c53d0567e4526b1e4a2ee5665ac81170a1771d17 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 15 Aug 2011 11:10:44 -0700 Subject: Fix the tests when libvirt actually exists --- nova/tests/test_libvirt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 7f4a3b09a..f3f7e8057 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -627,7 +627,7 @@ class LibvirtConnTestCase(test.TestCase): return # Preparing mocks - def fake_none(self): + def fake_none(self, *args): return def fake_raise(self): @@ -1050,7 +1050,7 @@ class IptablesFirewallTestCase(test.TestCase): instance_ref = self._create_instance_ref() _setup_networking(instance_ref['id'], self.test_ip) - network_info = _create_network_info + 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) -- cgit From 055c7692fc91d1d7d709c975fd223cc67f18ef8f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 15 Aug 2011 11:31:44 -0700 Subject: remove openwrt image --- smoketests/openwrt-x86-ext2.image | Bin 4612608 -> 0 bytes smoketests/openwrt-x86-vmlinuz | Bin 1169948 -> 0 bytes smoketests/random.image | Bin 0 -> 65536 bytes smoketests/random.kernel | Bin 0 -> 16384 bytes smoketests/test_sysadmin.py | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 smoketests/openwrt-x86-ext2.image delete mode 100644 smoketests/openwrt-x86-vmlinuz create mode 100644 smoketests/random.image create mode 100644 smoketests/random.kernel diff --git a/smoketests/openwrt-x86-ext2.image b/smoketests/openwrt-x86-ext2.image deleted file mode 100644 index cd2dfa426..000000000 Binary files a/smoketests/openwrt-x86-ext2.image and /dev/null differ diff --git a/smoketests/openwrt-x86-vmlinuz b/smoketests/openwrt-x86-vmlinuz deleted file mode 100644 index 59cc9bb1f..000000000 Binary files a/smoketests/openwrt-x86-vmlinuz and /dev/null differ diff --git a/smoketests/random.image b/smoketests/random.image new file mode 100644 index 000000000..f2c0c30bb Binary files /dev/null and b/smoketests/random.image differ diff --git a/smoketests/random.kernel b/smoketests/random.kernel new file mode 100644 index 000000000..01a6284dd Binary files /dev/null and b/smoketests/random.kernel differ 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) -- cgit From bc7892f698fbfc21f8d242f52e012d9165e46de7 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Mon, 15 Aug 2011 11:55:53 -0700 Subject: Adding standard inclusion of a body param which most http clients will send along with a POST request. --- nova/api/openstack/contrib/floating_ips.py | 2 +- nova/tests/api/openstack/contrib/test_floating_ips.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index c07bfdf09..121a1d4a0 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -131,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): """ POST /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index ab7ae2e54..dd58a1b22 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -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) -- cgit From fdb8c92739026e96ac52fc165d70c8f8c7594177 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Mon, 15 Aug 2011 12:04:51 -0700 Subject: making body default to none --- nova/api/openstack/contrib/floating_ips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 121a1d4a0..768b9deb1 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -131,7 +131,7 @@ class FloatingIPController(object): "floating_ip": floating_ip, "fixed_ip": fixed_ip}} - def disassociate(self, req, id, body): + 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) -- cgit