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 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 8d97118be776fcaad3053d1f93f61d339685a4ae Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Fri, 22 Jul 2011 17:26:11 +0400 Subject: Moved restaring instances from livbirt driver to ComputeManager. --- nova/compute/manager.py | 19 +++++++++++++++++++ nova/flags.py | 3 +++ nova/virt/libvirt/connection.py | 25 ++----------------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5819a520a..c7d3004a5 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -146,7 +146,26 @@ class ComputeManager(manager.SchedulerDependentManager): def init_host(self): """Initialization for a standalone compute service.""" + # NOTE(nsokolov): based on itoumsn's implementation from libvirt driver + from nova import context self.driver.init_host(host=self.host) + admin_context = context.get_admin_context() + for instance in self.db.instance_get_all_by_host(admin_context, self.host): + try: + LOG.debug(_('Checking state of %s'), instance['name']) + state = self.driver.get_info(instance['name'])['state'] + except exception.NotFound: + state = power_state.SHUTOFF + + LOG.debug(_('Current state of %(name)s is %(state)s, state in DB is %(db_state)s.'), + {'name': instance['name'], 'state': state, 'db_state': instance['state']}) + + if instance['state'] == power_state.RUNNING and state != power_state.RUNNING \ + and FLAGS.start_guests_on_host_boot: + LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.')) + self.reboot_instance(admin_context, instance[id]) + else: + self.db.instance_set_state(ctxt, instance['id'], state) def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" diff --git a/nova/flags.py b/nova/flags.py index 49355b436..23ca38b17 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -387,3 +387,6 @@ DEFINE_list('zone_capabilities', 'Key/Multi-value list representng capabilities of this zone') DEFINE_string('build_plan_encryption_key', None, '128bit (hex) encryption key for scheduler build plans.') + +DEFINE_bool('start_guests_on_host_boot', False, + 'Whether to restart guests when the host reboots') diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 342dea98f..d85b91ee2 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -121,8 +121,6 @@ flags.DEFINE_integer('live_migration_bandwidth', 0, 'Define live migration behavior') flags.DEFINE_string('qemu_img', 'qemu-img', 'binary to use for qemu-img commands') -flags.DEFINE_bool('start_guests_on_host_boot', False, - 'Whether to restart guests when the host reboots') def get_connection(read_only): @@ -167,27 +165,8 @@ class LibvirtConnection(driver.ComputeDriver): self.firewall_driver = fw_class(get_connection=self._get_connection) def init_host(self, host): - # Adopt existing VM's running here - ctxt = context.get_admin_context() - for instance in db.instance_get_all_by_host(ctxt, host): - try: - LOG.debug(_('Checking state of %s'), instance['name']) - state = self.get_info(instance['name'])['state'] - except exception.NotFound: - state = power_state.SHUTOFF - - LOG.debug(_('Current state of %(name)s was %(state)s.'), - {'name': instance['name'], 'state': state}) - db.instance_set_state(ctxt, instance['id'], state) - - # NOTE(justinsb): We no longer delete SHUTOFF instances, - # the user may want to power them back on - - if state != power_state.RUNNING: - continue - self.firewall_driver.setup_basic_filtering(instance) - self.firewall_driver.prepare_instance_filter(instance) - self.firewall_driver.apply_instance_filter(instance) + # NOTE(nsokolov): moved instance restarting to ComputeManager + pass def _get_connection(self): if not self._wrapped_conn or not self._test_connection(): -- cgit From 0e2726f452fe6991797728bca1e514943725e7a2 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Fri, 22 Jul 2011 10:34:01 -0400 Subject: initial test for v1.1 detail request --- nova/tests/api/openstack/test_versions.py | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 0eae18a62..aaa1f4976 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -16,6 +16,7 @@ # under the License. import json +import stubout import webob from nova import context @@ -29,6 +30,9 @@ class VersionsTest(test.TestCase): def setUp(self): super(VersionsTest, self).setUp() self.context = context.get_admin_context() + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_auth(self.stubs) + def tearDown(self): super(VersionsTest, self).tearDown() @@ -64,6 +68,55 @@ class VersionsTest(test.TestCase): ] self.assertEqual(versions, expected) + def test_get_version_1_1_detail(self): + req = webob.Request.blank('/v1.1/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + print res.body + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + versions = json.loads(res.body)["versions"] + expected = [ + { + "version" : { + "id" : "v1.1", + "status" : "CURRENT", + "updated" : "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel" : "self", + "href" : "http://servers.api.openstack.org/v1.0/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/" + "vnd.openstack.compute-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/" + "vnd.openstack.compute-v1.1+json" + } + ] + } + } + ] + self.assertEqual(versions, expected) + def test_get_version_list_xml(self): req = webob.Request.blank('/') req.accept = "application/xml" -- 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 4937c2f2c757776eacba20a6446c059c4938d6b8 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 11:06:06 +0400 Subject: Removed driver-specific autostart code. --- nova/virt/libvirt/connection.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index d85b91ee2..65a6b6393 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -571,11 +571,6 @@ class LibvirtConnection(driver.ComputeDriver): LOG.debug(_("instance %s: is running"), instance['name']) self.firewall_driver.apply_instance_filter(instance) - if FLAGS.start_guests_on_host_boot: - LOG.debug(_("instance %s: setting autostart ON") % - instance['name']) - domain.setAutostart(1) - def _wait_for_boot(): """Called at an interval until the VM is running.""" instance_name = instance['name'] -- cgit From 708b0cb65a672e9f6b8bab4817061be4fa2a8928 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 14:22:38 +0400 Subject: Fixed init_host context name. --- 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 c7d3004a5..b876e87d0 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -165,7 +165,7 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.')) self.reboot_instance(admin_context, instance[id]) else: - self.db.instance_set_state(ctxt, instance['id'], state) + self.db.instance_set_state(admin_context, instance['id'], state) def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" -- cgit From d8f4d773b08a94b171ff2643d48daa5b2709118a Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 14:33:45 +0400 Subject: Fixed id. --- 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 b876e87d0..b7e54d903 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -163,7 +163,7 @@ class ComputeManager(manager.SchedulerDependentManager): if instance['state'] == power_state.RUNNING and state != power_state.RUNNING \ and FLAGS.start_guests_on_host_boot: LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.')) - self.reboot_instance(admin_context, instance[id]) + self.reboot_instance(admin_context, instance['id']) else: self.db.instance_set_state(admin_context, instance['id'], state) -- cgit From 19379c78e6efd4637d876c91b022e6e7dbd38836 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 14:37:21 +0400 Subject: Fixed logging. --- 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 b7e54d903..071eabcfe 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -162,7 +162,7 @@ class ComputeManager(manager.SchedulerDependentManager): if instance['state'] == power_state.RUNNING and state != power_state.RUNNING \ and FLAGS.start_guests_on_host_boot: - LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.')) + LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.'), {'name': instance['name']}) self.reboot_instance(admin_context, instance['id']) else: self.db.instance_set_state(admin_context, instance['id'], state) -- cgit From cf4aea379eb337b16a9816d45c50c0553c500d0d Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 15:03:02 +0400 Subject: pep8 --- nova/compute/manager.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 071eabcfe..ebd91177f 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -150,22 +150,29 @@ class ComputeManager(manager.SchedulerDependentManager): from nova import context self.driver.init_host(host=self.host) admin_context = context.get_admin_context() - for instance in self.db.instance_get_all_by_host(admin_context, self.host): + for instance in self.db.instance_get_all_by_host(admin_context, + self.host): try: LOG.debug(_('Checking state of %s'), instance['name']) state = self.driver.get_info(instance['name'])['state'] except exception.NotFound: state = power_state.SHUTOFF - LOG.debug(_('Current state of %(name)s is %(state)s, state in DB is %(db_state)s.'), - {'name': instance['name'], 'state': state, 'db_state': instance['state']}) - - if instance['state'] == power_state.RUNNING and state != power_state.RUNNING \ - and FLAGS.start_guests_on_host_boot: - LOG.debug(_('Rebooting instance %(name)s after nova-compute restart.'), {'name': instance['name']}) + LOG.debug(_('Current state of %(name)s is %(state)s, state in ' + 'DB is %(db_state)s.'), {'name': instance['name'], + 'state': state, + 'db_state': + instance['state']}) + + if instance['state'] == power_state.RUNNING \ + and state != power_state.RUNNING \ + and FLAGS.start_guests_on_host_boot: + LOG.debug(_('Rebooting instance %(name)s after nova-compute ' + ' restart.'), {'name': instance['name']}) self.reboot_instance(admin_context, instance['id']) else: - self.db.instance_set_state(admin_context, instance['id'], state) + self.db.instance_set_state(admin_context, instance['id'], + state) def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" -- cgit From d85a43c4cdb1bfd28355ded486af2ded8f43d6b0 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 19:54:23 +0400 Subject: Some estetic refactoring. --- nova/compute/manager.py | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ebd91177f..950fe0d53 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -44,6 +44,7 @@ import functools from eventlet import greenthread +import nova.context from nova import exception from nova import flags import nova.image @@ -145,34 +146,26 @@ class ComputeManager(manager.SchedulerDependentManager): *args, **kwargs) def init_host(self): - """Initialization for a standalone compute service.""" - # NOTE(nsokolov): based on itoumsn's implementation from libvirt driver - from nova import context + """Initialization for a standalone compute service. + + Reboots instances marked as running in DB if they is not running.""" self.driver.init_host(host=self.host) - admin_context = context.get_admin_context() - for instance in self.db.instance_get_all_by_host(admin_context, - self.host): - try: - LOG.debug(_('Checking state of %s'), instance['name']) - state = self.driver.get_info(instance['name'])['state'] - except exception.NotFound: - state = power_state.SHUTOFF - - LOG.debug(_('Current state of %(name)s is %(state)s, state in ' - 'DB is %(db_state)s.'), {'name': instance['name'], - 'state': state, - 'db_state': - instance['state']}) - - if instance['state'] == power_state.RUNNING \ - and state != power_state.RUNNING \ - and FLAGS.start_guests_on_host_boot: - LOG.debug(_('Rebooting instance %(name)s after nova-compute ' - ' restart.'), {'name': instance['name']}) - self.reboot_instance(admin_context, instance['id']) - else: - self.db.instance_set_state(admin_context, instance['id'], - state) + context = nova.context.get_admin_context() + instances = self.db.instance_get_all_by_host(context, self.host) + for instance in instances: + inst_name = instance['name'] + db_state = instance['state'] + drv_state = self._update_state(context, instance['id']) + + expect_running = db_state == power_state.RUNNING != drv_state + + LOG.debug(_('Current state of %(inst_name)s is %(drv_state)s, ' + 'state in DB is %(db_state)s.'), locals()) + + if expect_running and FLAGS.start_guests_on_host_boot: + LOG.info(_('Rebooting instance %(inst_name)s after ' + 'nova-compute restart.'), locals()) + self.reboot_instance(context, instance['id']) def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" @@ -180,6 +173,7 @@ class ComputeManager(manager.SchedulerDependentManager): if state is None: try: + LOG.debug(_('Checking state of %s'), instance_ref['name']) info = self.driver.get_info(instance_ref['name']) except exception.NotFound: info = None -- cgit From 9c88bbee56dd05703af8f7c0df839a4da73f491a Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Mon, 25 Jul 2011 19:59:00 +0400 Subject: Hotfix. --- nova/compute/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 950fe0d53..79e9b16d3 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -184,6 +184,7 @@ class ComputeManager(manager.SchedulerDependentManager): state = power_state.FAILED self.db.instance_set_state(context, instance_id, state) + return state def _update_launched_at(self, context, instance_id, launched_at=None): """Update the launched_at parameter of the given instance.""" -- cgit From 99bc14f16bce9f125715fbe436b7fc0969b62420 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Mon, 25 Jul 2011 14:10:08 -0400 Subject: added 1.0 detail test, added VersionRequestDeserializer to support Versions actions properly, started 300/multiple choice work --- nova/api/openstack/__init__.py | 6 ++ nova/api/openstack/versions.py | 92 ++++++++++++++++++++++++++++++- nova/tests/api/openstack/test_versions.py | 51 ++++++++++++++++- 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e87d7c754..fb6f5515e 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -40,6 +40,7 @@ from nova.api.openstack import servers from nova.api.openstack import server_metadata from nova.api.openstack import shared_ip_groups from nova.api.openstack import users +from nova.api.openstack import versions from nova.api.openstack import wsgi from nova.api.openstack import zones @@ -115,6 +116,10 @@ class APIRouter(base_wsgi.Router): 'select': 'POST', 'boot': 'POST'}) + mapper.connect("versions", "/", + controller=versions.create_resource(version), + action="index") + mapper.resource("console", "consoles", controller=consoles.create_resource(), parent_resource=dict(member_name='server', @@ -164,6 +169,7 @@ class APIRouterV11(APIRouter): def _setup_routes(self, mapper): super(APIRouterV11, self)._setup_routes(mapper, '1.1') + mapper.resource("image_meta", "meta", controller=image_metadata.create_resource(), parent_resource=dict(member_name='image', diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index df7a94b7e..445a14372 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -28,6 +28,13 @@ ATOM_XMLNS = "http://www.w3.org/2005/Atom" class Versions(wsgi.Resource): + @classmethod + def factory(cls, global_config, **local_config): + """Paste factory.""" + def _factory(app): + return cls(app, **local_config) + return _factory + def __init__(self): metadata = { "attributes": { @@ -45,7 +52,7 @@ class Versions(wsgi.Resource): supported_content_types = ('application/json', 'application/xml', 'application/atom+xml') - deserializer = wsgi.RequestDeserializer( + deserializer = VersionsRequestDeserializer( supported_content_types=supported_content_types) wsgi.Resource.__init__(self, None, serializer=serializer, @@ -53,6 +60,14 @@ class Versions(wsgi.Resource): def dispatch(self, request, *args): """Respond to a request for all OpenStack API versions.""" + if request.path == '/': + # List Versions + return self._versions_list(request) + else: + # Versions Multiple Choice + return self._versions_multi_choice(request) + + def _versions_list(self, request): version_objs = [ { "id": "v1.1", @@ -72,6 +87,47 @@ class Versions(wsgi.Resource): versions = [builder.build(version) for version in version_objs] return dict(versions=versions) + def _versions_multi_choice(self, request): + version_objs = [ + { + "id": "v1.1", + "status": "CURRENT", + #TODO(wwolf) get correct value for these + "updated": "2011-07-18T11:30:00Z", + }, + { + "id": "v1.0", + "status": "DEPRECATED", + #TODO(wwolf) get correct value for these + "updated": "2010-10-09T11:30:00Z", + }, + ] + + builder = nova.api.openstack.views.versions.get_view_builder(request) + versions = [builder.build(version) for version in version_objs] + return dict(versions=versions) + + +class VersionV10(object): + def index(self, req): + return "test index 1.0" + + +class VersionV11(object): + def index(self, req): + return "test index 1.1" + +class VersionsRequestDeserializer(wsgi.RequestDeserializer): + def get_action_args(self, request_environment): + """Parse dictionary created by routes library.""" + + args = {} + if request_environment['PATH_INFO'] == '/': + args['action'] = 'index' + else: + args['action'] = 'multi' + + return args class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _versions_to_xml(self, versions): @@ -96,7 +152,13 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return version_node - def default(self, data): + def index(self, data): + self._xml_doc = minidom.Document() + node = self._versions_to_xml(data['versions']) + + return self.to_xml_string(node) + + def multi(self, data): self._xml_doc = minidom.Document() node = self._versions_to_xml(data['versions']) @@ -190,10 +252,34 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): entry.appendChild(content) root.appendChild(entry) - def default(self, data): + def index(self, data): self._xml_doc = minidom.Document() node = self._xml_doc.createElementNS(self.xmlns, 'feed') self._create_meta(node, data['versions']) self._create_version_entries(node, data['versions']) return self.to_xml_string(node) + + def multi(self, data): + self._xml_doc = minidom.Document() + node = self._xml_doc.createElementNS(self.xmlns, 'feed') + self._create_meta(node, data['versions']) + self._create_version_entries(node, data['versions']) + + return self.to_xml_string(node) + + +def create_resource(version='1.0'): + controller = { + '1.0': VersionV10, + '1.1': VersionV11, + }[version]() + + body_serializers = { + 'application/xml': VersionsXMLSerializer(), + 'application/atom+xml': VersionsAtomSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(controller, serializer=serializer) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index aaa1f4976..b2896a780 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -68,6 +68,55 @@ class VersionsTest(test.TestCase): ] self.assertEqual(versions, expected) + def test_get_version_1_0_detail(self): + req = webob.Request.blank('/v1.0/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + print res.body + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + versions = json.loads(res.body)["versions"] + expected = [ + { + "version" : { + "id" : "v1.0", + "status" : "CURRENT", + "updated" : "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel" : "self", + "href" : "http://servers.api.openstack.org/v1.0/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/" + "vnd.openstack.compute-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/" + "vnd.openstack.compute-v1.0+json" + } + ] + } + } + ] + self.assertEqual(versions, expected) + def test_get_version_1_1_detail(self): req = webob.Request.blank('/v1.1/') req.accept = "application/json" @@ -85,7 +134,7 @@ class VersionsTest(test.TestCase): "links": [ { "rel" : "self", - "href" : "http://servers.api.openstack.org/v1.0/" + "href" : "http://servers.api.openstack.org/v1.1/" }, { "rel" : "describedby", -- cgit From 810d4b89cbbfa9388fb61f9069ea0104a7d77752 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Mon, 25 Jul 2011 15:28:06 -0400 Subject: removed prints, got versions detail tests passing, still need to do xml/atom --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/versions.py | 96 ++++++++-- nova/tests/api/openstack/test_versions.py | 295 +++++++++++++++++++++--------- 3 files changed, 292 insertions(+), 101 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index fb6f5515e..a6a8b4595 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -118,7 +118,7 @@ class APIRouter(base_wsgi.Router): mapper.connect("versions", "/", controller=versions.create_resource(version), - action="index") + action="detail") mapper.resource("console", "consoles", controller=consoles.create_resource(), diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 445a14372..5655edcfc 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -88,6 +88,7 @@ class Versions(wsgi.Resource): return dict(versions=versions) def _versions_multi_choice(self, request): + #TODO version_objs = [ { "id": "v1.1", @@ -109,18 +110,87 @@ class Versions(wsgi.Resource): class VersionV10(object): - def index(self, req): - return "test index 1.0" + def detail(self, req): + #TODO + return { + "version" : { + "id": "v1.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.0/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.0+json" + } + ], + }, + } class VersionV11(object): - def index(self, req): - return "test index 1.1" + def detail(self, req): + return { + "version" : { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.1/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.1+json" + } + ], + }, + } + class VersionsRequestDeserializer(wsgi.RequestDeserializer): def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" - args = {} if request_environment['PATH_INFO'] == '/': args['action'] = 'index' @@ -129,6 +199,7 @@ class VersionsRequestDeserializer(wsgi.RequestDeserializer): return args + class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _versions_to_xml(self, versions): root = self._xml_doc.createElement('versions') @@ -158,6 +229,9 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) + def detail(self,data): + return "" + def multi(self, data): self._xml_doc = minidom.Document() node = self._versions_to_xml(data['versions']) @@ -167,6 +241,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): class VersionsAtomSerializer(wsgi.XMLDictSerializer): def __init__(self, metadata=None, xmlns=None): + self.metadata = metadata or {} if not xmlns: self.xmlns = ATOM_XMLNS else: @@ -260,14 +335,9 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) - def multi(self, data): - self._xml_doc = minidom.Document() - node = self._xml_doc.createElementNS(self.xmlns, 'feed') - self._create_meta(node, data['versions']) - self._create_version_entries(node, data['versions']) - - return self.to_xml_string(node) - + def detail(self, data): + #TODO + pass def create_resource(version='1.0'): controller = { diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index b2896a780..9c4af0787 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -72,99 +72,132 @@ class VersionsTest(test.TestCase): req = webob.Request.blank('/v1.0/') req.accept = "application/json" res = req.get_response(fakes.wsgi_app()) - print res.body self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/json") - versions = json.loads(res.body)["versions"] - expected = [ - { - "version" : { - "id" : "v1.0", - "status" : "CURRENT", - "updated" : "2011-01-21T11:33:21-06:00", - "links": [ - { - "rel" : "self", - "href" : "http://servers.api.openstack.org/v1.0/" - }, - { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" - }, - { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" - } - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/" - "vnd.openstack.compute-v1.0+xml" - }, - { - "base" : "application/json", - "type" : "application/" - "vnd.openstack.compute-v1.0+json" - } - ] - } + version = json.loads(res.body) + expected = { + "version" : { + "id" : "v1.0", + "status" : "CURRENT", + "updated" : "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel" : "self", + "href" : "http://servers.api.openstack.org/v1.0/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/" + "vnd.openstack.compute-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/" + "vnd.openstack.compute-v1.0+json" + } + ] } - ] - self.assertEqual(versions, expected) + } + self.assertEqual(expected, version) def test_get_version_1_1_detail(self): req = webob.Request.blank('/v1.1/') req.accept = "application/json" res = req.get_response(fakes.wsgi_app()) - print res.body self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/json") - versions = json.loads(res.body)["versions"] - expected = [ - { - "version" : { - "id" : "v1.1", - "status" : "CURRENT", - "updated" : "2011-01-21T11:33:21-06:00", - "links": [ - { - "rel" : "self", - "href" : "http://servers.api.openstack.org/v1.1/" - }, - { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" - }, - { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" - } - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/" - "vnd.openstack.compute-v1.1+xml" - }, - { - "base" : "application/json", - "type" : "application/" - "vnd.openstack.compute-v1.1+json" - } - ] - } + version = json.loads(res.body) + expected = { + "version" : { + "id" : "v1.1", + "status" : "CURRENT", + "updated" : "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel" : "self", + "href" : "http://servers.api.openstack.org/v1.1/" + }, + { + "rel" : "describedby", + "type" : "application/pdf", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel" : "describedby", + "type" : "application/vnd.sun.wadl+xml", + "href" : "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + } + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/" + "vnd.openstack.compute-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/" + "vnd.openstack.compute-v1.1+json" + } + ] } - ] - self.assertEqual(versions, expected) + } + self.assertEqual(expected, version) + + def test_get_version_1_0_detail_xml(self): + pass + + def test_get_version_1_1_detail_xml(self): + req = webob.Request.blank('/v1.1/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + expected = """ + + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + self.assertEqual(expected, actual) def test_get_version_list_xml(self): req = webob.Request.blank('/') @@ -265,7 +298,37 @@ class VersionsTest(test.TestCase): self.assertEqual(actual, expected) - def test_xml_serializer(self): + def test_versions_list_xml_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.7.1", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "links": [ + { + "rel": "self", + "href": "http://test/2.7.1", + } + ], + }, + ] + } + + expected = """ + + + + + """.replace(" ", "").replace("\n", "") + + serializer = versions.VersionsXMLSerializer() + response = serializer.index(versions_data) + response = response.replace(" ", "").replace("\n", "") + self.assertEqual(expected, response) + + def test_versions_detail_xml_serializer(self): versions_data = { 'versions': [ { @@ -291,11 +354,70 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") serializer = versions.VersionsXMLSerializer() - response = serializer.default(versions_data) + response = serializer.index(versions_data) + response = response.replace(" ", "").replace("\n", "") + self.assertEqual(expected, response) + + def test_versions_list_atom_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.9.8", + "updated": "2011-07-20T11:40:00Z", + "status": "CURRENT", + "links": [ + { + "rel": "self", + "href": "http://test/2.9.8", + } + ], + }, + ] + } + + expected = """ + + + Available API Versions + + + 2011-07-20T11:40:00Z + + + http://test/ + + + + Rackspace + + + http://www.rackspace.com/ + + + + + + http://test/2.9.8 + + + Version 2.9.8 + + + 2011-07-20T11:40:00Z + + + + Version 2.9.8 CURRENT (2011-07-20T11:40:00Z) + + + """.replace(" ", "").replace("\n", "") + + serializer = versions.VersionsAtomSerializer() + response = serializer.index(versions_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) - def test_atom_serializer(self): + def test_version_detail_atom_serializer(self): versions_data = { 'versions': [ { @@ -350,7 +472,6 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") serializer = versions.VersionsAtomSerializer() - response = serializer.default(versions_data) - print response + response = serializer.index(versions_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) -- cgit From 71a103822b41df3d90a1e958baffda55a9cb8730 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Mon, 25 Jul 2011 16:25:19 -0400 Subject: xml version detail working with tests --- nova/api/openstack/versions.py | 30 +++++- nova/tests/api/openstack/test_versions.py | 165 +++++++++++++++++++++--------- 2 files changed, 147 insertions(+), 48 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 5655edcfc..00fc8d98f 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -209,16 +209,39 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return root - def _create_version_node(self, version): + def _create_media_types(self, media_types): + base = self._xml_doc.createElement('media-types') + for type in media_types: + node = self._xml_doc.createElement('media-type') + node.setAttribute('base', type['base']) + node.setAttribute('type', type['type']) + base.appendChild(node) + + return base + + def _create_version_node(self, version, create_ns=False): version_node = self._xml_doc.createElement('version') + if create_ns: + xmlns = "http://docs.openstack.org/common/api/%s" % version['id'] + xmlns_atom = "http://www.w3.org/2005/Atom" + version_node.setAttribute('xmlns', xmlns) + version_node.setAttribute('xmlns:atom', xmlns_atom) + version_node.setAttribute('id', version['id']) version_node.setAttribute('status', version['status']) version_node.setAttribute('updated', version['updated']) + if 'media-types' in version: + media_types = self._create_media_types(version['media-types']) + version_node.appendChild(media_types) + for link in version['links']: link_node = self._xml_doc.createElement('atom:link') link_node.setAttribute('rel', link['rel']) link_node.setAttribute('href', link['href']) + if 'type' in link: + link_node.setAttribute('type', link['type']) + version_node.appendChild(link_node) return version_node @@ -230,7 +253,10 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) def detail(self,data): - return "" + self._xml_doc = minidom.Document() + node = self._create_version_node(data['version'], True) + + return self.to_xml_string(node) def multi(self, data): self._xml_doc = minidom.Document() diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 9c4af0787..632d388fa 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -161,7 +161,40 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, version) def test_get_version_1_0_detail_xml(self): - pass + req = webob.Request.blank('/v1.0/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + expected = """ + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + self.assertEqual(expected, actual) def test_get_version_1_1_detail_xml(self): req = webob.Request.blank('/v1.1/') @@ -170,31 +203,31 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") expected = """ - - - - - - - - - - - - - - """.replace(" ", "").replace("\n", "") + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -330,31 +363,70 @@ class VersionsTest(test.TestCase): def test_versions_detail_xml_serializer(self): versions_data = { - 'versions': [ - { - "id": "2.7.1", - "updated": "2011-07-18T11:30:00Z", - "status": "DEPRECATED", - "links": [ - { - "rel": "self", - "href": "http://test/2.7.1", - } - ], - }, - ] + "version" : { + "id": "v1.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21-06:00", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.0/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.0+json" + } + ], + }, } expected = """ - - - - - """.replace(" ", "").replace("\n", "") + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") serializer = versions.VersionsXMLSerializer() - response = serializer.index(versions_data) + response = serializer.detail(versions_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) @@ -418,6 +490,7 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, response) def test_version_detail_atom_serializer(self): + #TODO versions_data = { 'versions': [ { -- cgit From ba4946d0d3c73e5d9f67f42203d103bf98563458 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Tue, 26 Jul 2011 00:31:42 +0400 Subject: Fixed old libvirt semantics, added resume_guests_state_on_host_boot flag. --- nova/compute/manager.py | 9 ++++----- nova/flags.py | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 79e9b16d3..bfac3df28 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -146,9 +146,7 @@ class ComputeManager(manager.SchedulerDependentManager): *args, **kwargs) def init_host(self): - """Initialization for a standalone compute service. - - Reboots instances marked as running in DB if they is not running.""" + """Initialization for a standalone compute service.""" self.driver.init_host(host=self.host) context = nova.context.get_admin_context() instances = self.db.instance_get_all_by_host(context, self.host) @@ -157,12 +155,13 @@ class ComputeManager(manager.SchedulerDependentManager): db_state = instance['state'] drv_state = self._update_state(context, instance['id']) - expect_running = db_state == power_state.RUNNING != drv_state + expect_running = (db_state == power_state.RUNNING != drv_state) LOG.debug(_('Current state of %(inst_name)s is %(drv_state)s, ' 'state in DB is %(db_state)s.'), locals()) - if expect_running and FLAGS.start_guests_on_host_boot: + if (expect_running and FLAGS.resume_guests_state_on_host_boot)\ + or FLAGS.start_guests_on_host_boot: LOG.info(_('Rebooting instance %(inst_name)s after ' 'nova-compute restart.'), locals()) self.reboot_instance(context, instance['id']) diff --git a/nova/flags.py b/nova/flags.py index 23ca38b17..6c7e448ad 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -390,3 +390,5 @@ DEFINE_string('build_plan_encryption_key', None, 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') -- cgit From 879e49c67b78d3336a24cf1af12f21258c2225fa Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Tue, 26 Jul 2011 01:46:40 +0400 Subject: Estetic fix. --- 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 bfac3df28..a6eb1efb2 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -155,7 +155,8 @@ class ComputeManager(manager.SchedulerDependentManager): db_state = instance['state'] drv_state = self._update_state(context, instance['id']) - expect_running = (db_state == power_state.RUNNING != drv_state) + expect_running = db_state == power_state.RUNNING \ + and drv_state != db_state LOG.debug(_('Current state of %(inst_name)s is %(drv_state)s, ' 'state in DB is %(db_state)s.'), locals()) -- cgit From 7be2b2482fde20be8802cfe6a200590933a73d7e Mon Sep 17 00:00:00 2001 From: William Wolf Date: Mon, 25 Jul 2011 18:28:43 -0400 Subject: atom and xml_detail working, with tests --- nova/api/openstack/versions.py | 49 ++++++-- nova/tests/api/openstack/test_versions.py | 200 +++++++++++++++++++++--------- 2 files changed, 183 insertions(+), 66 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 00fc8d98f..6fba4bbe0 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -116,7 +116,7 @@ class VersionV10(object): "version" : { "id": "v1.0", "status": "CURRENT", - "updated": "2011-01-21T11:33:21-06:00", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -155,7 +155,7 @@ class VersionV11(object): "version" : { "id": "v1.1", "status": "CURRENT", - "updated": "2011-01-21T11:33:21-06:00", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -298,8 +298,33 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): link_href = link_href.rstrip('/') return link_href.rsplit('/', 1)[0] + '/' - def _create_meta(self, root, versions): - title = self._create_text_elem('title', 'Available API Versions', + def _create_detail_meta(self, root, version): + title = self._create_text_elem('title', "About This Version", + type='text') + + updated = self._create_text_elem('updated', version['updated']) + + uri = version['links'][0]['href'] + id = self._create_text_elem('id', uri) + + link = self._xml_doc.createElement('link') + link.setAttribute('rel', 'self') + link.setAttribute('href', uri) + + author = self._xml_doc.createElement('author') + author_name = self._create_text_elem('name', 'Rackspace') + author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/') + author.appendChild(author_name) + author.appendChild(author_uri) + + root.appendChild(title) + root.appendChild(updated) + root.appendChild(id) + root.appendChild(author) + root.appendChild(link) + + def _create_list_meta(self, root, versions): + title = self._create_text_elem('title', "Available API Versions", type='text') # Set this updated to the most recently updated version recent = self._get_most_recent_update(versions) @@ -307,6 +332,7 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): base_url = self._get_base_url(versions[0]['links'][0]['href']) id = self._create_text_elem('id', base_url) + link = self._xml_doc.createElement('link') link.setAttribute('rel', 'self') link.setAttribute('href', base_url) @@ -341,7 +367,10 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): link_node = self._xml_doc.createElement('link') link_node.setAttribute('rel', link['rel']) link_node.setAttribute('href', link['href']) - entry.appendChild(link_node) + if 'type' in link: + link_node.setAttribute('type', link['type']) + + entry.appendChild(link_node) content = self._create_text_elem('content', 'Version %s %s (%s)' % @@ -356,14 +385,18 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): def index(self, data): self._xml_doc = minidom.Document() node = self._xml_doc.createElementNS(self.xmlns, 'feed') - self._create_meta(node, data['versions']) + self._create_list_meta(node, data['versions']) self._create_version_entries(node, data['versions']) return self.to_xml_string(node) def detail(self, data): - #TODO - pass + self._xml_doc = minidom.Document() + node = self._xml_doc.createElementNS(self.xmlns, 'feed') + self._create_detail_meta(node, data['version']) + self._create_version_entries(node, [data['version']]) + + return self.to_xml_string(node) def create_resource(version='1.0'): controller = { diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 632d388fa..11b69ec83 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -79,7 +79,7 @@ class VersionsTest(test.TestCase): "version" : { "id" : "v1.0", "status" : "CURRENT", - "updated" : "2011-01-21T11:33:21-06:00", + "updated" : "2011-01-21T11:33:21Z", "links": [ { "rel" : "self", @@ -125,7 +125,7 @@ class VersionsTest(test.TestCase): "version" : { "id" : "v1.1", "status" : "CURRENT", - "updated" : "2011-01-21T11:33:21-06:00", + "updated" : "2011-01-21T11:33:21Z", "links": [ { "rel" : "self", @@ -168,7 +168,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") expected = """ @@ -204,7 +204,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") expected = """ @@ -253,6 +253,80 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, actual) + def test_get_version_1_0_detail_atom(self): + #TODO + req = webob.Request.blank('/v1.0/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + expected = """ + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + self.assertEqual(expected, actual) + + def test_get_version_1_1_detail_atom(self): + #TODO + req = webob.Request.blank('/v1.1/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + expected = """ + + + + + + + + + + + + + """.replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + self.assertEqual(expected, actual) + def test_get_version_list_atom(self): req = webob.Request.blank('/') req.accept = "application/atom+xml" @@ -361,12 +435,12 @@ class VersionsTest(test.TestCase): response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) - def test_versions_detail_xml_serializer(self): - versions_data = { + def test_version_detail_xml_serializer(self): + version_data = { "version" : { "id": "v1.0", "status": "CURRENT", - "updated": "2011-01-21T11:33:21-06:00", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -400,7 +474,7 @@ class VersionsTest(test.TestCase): expected = """ @@ -426,7 +500,7 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") serializer = versions.VersionsXMLSerializer() - response = serializer.detail(versions_data) + response = serializer.detail(version_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) @@ -492,59 +566,69 @@ class VersionsTest(test.TestCase): def test_version_detail_atom_serializer(self): #TODO versions_data = { - 'versions': [ - { - "id": "2.9.8", - "updated": "2011-07-20T11:40:00Z", - "status": "CURRENT", - "links": [ - { - "rel": "self", - "href": "http://test/2.9.8", - } - ], - }, - ] + "version" : { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.1/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.1+json" + } + ], + }, } expected = """ - - - Available API Versions - - - 2011-07-20T11:40:00Z - - - http://test/ - - - - Rackspace - - - http://www.rackspace.com/ - - - - - - http://test/2.9.8 - - - Version 2.9.8 - - - 2011-07-20T11:40:00Z - - - - Version 2.9.8 CURRENT (2011-07-20T11:40:00Z) - - - """.replace(" ", "").replace("\n", "") + + About This Version + 2011-01-21T11:33:21Z + http://servers.api.openstack.org/v1.1/ + + Rackspace + http://www.rackspace.com/ + + + + http://servers.api.openstack.org/v1.1/ + Version v1.1 + 2011-01-21T11:33:21Z + + + + + Version v1.1 CURRENT (2011-01-21T11:33:21Z) + + + """.replace(" ", "").replace("\n", "") serializer = versions.VersionsAtomSerializer() - response = serializer.index(versions_data) + response = serializer.detail(versions_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) -- cgit From d4842ac958bda3b446d14c7348692acc231e0041 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Tue, 26 Jul 2011 02:30:20 +0400 Subject: Added ensuring filter rules for all VMs. --- nova/compute/manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index a6eb1efb2..53390098a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -166,6 +166,11 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.info(_('Rebooting instance %(inst_name)s after ' '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 + self.driver.ensure_filtering_rules_for_instance(instance) + except NotImplementedError: + pass def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" -- cgit From 8f5a6d15e671c95c6e38147ca15fb49fd672e788 Mon Sep 17 00:00:00 2001 From: Nikolay Sokolov Date: Tue, 26 Jul 2011 02:33:39 +0400 Subject: Warn user instead of ignoring --- 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 53390098a..1f207e6f0 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -170,7 +170,7 @@ class ComputeManager(manager.SchedulerDependentManager): try: # Hyper-V and VMWareAPI drivers will raise and exception self.driver.ensure_filtering_rules_for_instance(instance) except NotImplementedError: - pass + 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.""" -- cgit From 4236f438a81e361beb1b05edd87154b4d5e1ce85 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Mon, 25 Jul 2011 22:38:59 +0000 Subject: Pass on auth_token --- nova/context.py | 4 +++- nova/image/glance.py | 11 +++++++++++ nova/tests/glance/stubs.py | 5 ++++- nova/tests/image/test_glance.py | 3 +++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/nova/context.py b/nova/context.py index 99085ed75..a765d1695 100644 --- a/nova/context.py +++ b/nova/context.py @@ -32,7 +32,8 @@ class RequestContext(object): """ def __init__(self, user, project, is_admin=None, read_deleted=False, - remote_address=None, timestamp=None, request_id=None): + remote_address=None, timestamp=None, request_id=None, + auth_token=None): if hasattr(user, 'id'): self._user = user self.user_id = user.id @@ -63,6 +64,7 @@ class RequestContext(object): chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-' request_id = ''.join([random.choice(chars) for x in xrange(20)]) self.request_id = request_id + self.auth_token = auth_token @property def user(self): diff --git a/nova/image/glance.py b/nova/image/glance.py index 5c2dc957b..44a3c6f83 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -83,11 +83,16 @@ class GlanceImageService(service.BaseImageService): client = property(_get_client, _set_client) + def _set_client_context(self, context): + """Sets the client's auth token.""" + self.client.set_auth_token(context.auth_token) + def index(self, context, filters=None, marker=None, limit=None): """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: @@ -104,6 +109,7 @@ class GlanceImageService(service.BaseImageService): def detail(self, context, filters=None, marker=None, limit=None): """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: @@ -120,6 +126,7 @@ class GlanceImageService(service.BaseImageService): def show(self, context, image_id): """Returns a dict with image data for the given opaque image id.""" + self._set_client_context(context) try: image_meta = self.client.get_image_meta(image_id) except glance_exception.NotFound: @@ -143,6 +150,7 @@ class GlanceImageService(service.BaseImageService): def get(self, context, image_id, data): """Calls out to Glance for metadata and data and writes data.""" + self._set_client_context(context) try: image_meta, image_chunks = self.client.get_image(image_id) except glance_exception.NotFound: @@ -160,6 +168,7 @@ class GlanceImageService(service.BaseImageService): :raises: AlreadyExists if the image already exist. """ + self._set_client_context(context) # Translate Base -> Service LOG.debug(_('Creating image in Glance. Metadata passed in %s'), image_meta) @@ -182,6 +191,7 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ + self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) try: @@ -198,6 +208,7 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ + self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) try: diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index aac3ff330..d51b19ccd 100644 --- a/nova/tests/glance/stubs.py +++ b/nova/tests/glance/stubs.py @@ -60,7 +60,10 @@ class FakeGlance(object): 'container_format': 'ovf'}, 'image_data': StringIO.StringIO('')}} - def __init__(self, host, port=None, use_ssl=False): + def __init__(self, host, port=None, use_ssl=False, auth_tok=None): + pass + + def set_auth_token(self, auth_tok): pass def get_image_meta(self, image_id): diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index 223e7ae57..5a40f578f 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -31,6 +31,9 @@ class StubGlanceClient(object): self.add_response = add_response self.update_response = update_response + def set_auth_token(self, auth_tok): + pass + def get_image_meta(self, image_id): return self.images[image_id] -- cgit From eba09454a21ce49afa821ec63ed801883354ff7e Mon Sep 17 00:00:00 2001 From: William Wolf Date: Mon, 25 Jul 2011 20:34:41 -0400 Subject: initial stuff to get away from string comparisons for XML, and use ElementTree --- nova/api/openstack/versions.py | 7 ++ nova/tests/api/openstack/test_versions.py | 109 +++++++++++++++++++++++------- 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 6fba4bbe0..c250dac8c 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -201,6 +201,13 @@ class VersionsRequestDeserializer(wsgi.RequestDeserializer): class VersionsXMLSerializer(wsgi.XMLDictSerializer): + #TODO(wwolf): this is temporary until we get rid of toprettyxml + # in the base class (XMLDictSerializer), which I plan to do in + # another branch + def to_xml_string(self, node, has_atom=False): + self._add_xmlns(node, has_atom) + return node.toxml(encoding='UTF-8') + def _versions_to_xml(self, versions): root = self._xml_doc.createElement('versions') diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 11b69ec83..31b201214 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -18,6 +18,8 @@ import json import stubout import webob +import xml.etree.ElementTree + from nova import context from nova import test @@ -166,6 +168,21 @@ class VersionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") + root = xml.etree.ElementTree.XML(res.body) + self.assertEqual(root.tag.split('}')[1], "version") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "http://docs.openstack.org/common/api/v1.0") + + children = list(root) + media_types = children[0] + media_type_nodes = list(media_types) + links = (children[1], children[2], children[3]) + + self.assertEqual(media_types.tag.split('}')[1], 'media-types') + for media_node in media_type_nodes: + self.assertEqual(media_node.tag.split('}')[1], 'media-type') + + expected = """ + serializer = versions.VersionsXMLSerializer() + response = serializer.detail(version_data) - - - - + root = xml.etree.ElementTree.XML(response) + self.assertEqual(root.tag.split('}')[1], "version") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "http://docs.openstack.org/common/api/v1.0") - + children = list(root) + media_types = children[0] + media_type_nodes = list(media_types) + links = (children[1], children[2], children[3]) - + self.assertEqual(media_types.tag.split('}')[1], 'media-types') + for i, media_node in enumerate(media_type_nodes): + self.assertEqual(media_node.tag.split('}')[1], 'media-type') + for key, val in version_data['version']['media-types'][i].items(): + self.assertEqual(val, media_node.get(key)) - - """.replace(" ", "").replace("\n", "") + for i, link in enumerate(links): + self.assertEqual(link.tag.split('}')[0].strip('{'), + 'http://www.w3.org/2005/Atom') + self.assertEqual(link.tag.split('}')[1], 'link') + for key, val in version_data['version']['links'][i].items(): + self.assertEqual(val, link.get(key)) - serializer = versions.VersionsXMLSerializer() - response = serializer.detail(version_data) - response = response.replace(" ", "").replace("\n", "") - self.assertEqual(expected, response) def test_versions_list_atom_serializer(self): versions_data = { @@ -563,6 +575,51 @@ class VersionsTest(test.TestCase): response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) + root = xml.etree.ElementTree.XML(response) + self.assertEqual(root.tag.split('}')[1], "feed") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "http://www.w3.org/2005/Atom") + + children = list(root) + title = children[0] + updated = children[1] + id = children[2] + author = children[3] + link = children[4] + entry = children[5] + + self.assertEqual(title.tag.split('}')[1], 'title') + self.assertEqual(title.text, 'Available API Versions') + self.assertEqual(updated.tag.split('}')[1], 'updated') + self.assertEqual(updated.text, '2011-07-20T11:40:00Z') + self.assertEqual(id.tag.split('}')[1], 'id') + self.assertEqual(id.text, 'http://test/') + + self.assertEqual(author.tag.split('}')[1], 'author') + author_name = list(author)[0] + author_uri = list(author)[1] + self.assertEqual(author_name.tag.split('}')[1], 'name') + self.assertEqual(author_name.text, 'Rackspace') + self.assertEqual(author_uri.tag.split('}')[1], 'uri') + self.assertEqual(author_uri.text, 'http://www.rackspace.com/') + + self.assertEqual(link.get('href'), 'http://test/') + self.assertEqual(link.get('rel'), 'self') + + self.assertEqual(entry.tag.split('}')[1], 'entry') + entry_children = list(entry) + entry_id = entry_children[0] + entry_title = entry_children[1] + entry_updated = entry_children[1] + + + + #self.assertEqual(media_types.tag.split('}')[1], 'media-types') + #for i, media_node in enumerate(media_type_nodes): + #self.assertEqual(media_node.tag.split('}')[1], 'media-type') + #for key, val in version_data['version']['media-types'][i].items(): + #self.assertEqual(val, media_node.get(key)) + def test_version_detail_atom_serializer(self): #TODO versions_data = { -- cgit From 696dc56b74a08d224beccdfd644536ec4217321d Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:15:44 -0400 Subject: updated atom tests --- nova/api/openstack/versions.py | 14 +++- nova/tests/api/openstack/test_versions.py | 111 +++++++++++++++--------------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index c250dac8c..03b99f342 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -115,7 +115,7 @@ class VersionV10(object): return { "version" : { "id": "v1.0", - "status": "CURRENT", + "status": "DEPRECATED", "updated": "2011-01-21T11:33:21Z", "links": [ { @@ -415,7 +415,15 @@ def create_resource(version='1.0'): 'application/xml': VersionsXMLSerializer(), 'application/atom+xml': VersionsAtomSerializer(), } - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(controller, serializer=serializer) + supported_content_types = ('application/json', + 'application/xml', + 'application/atom+xml') + deserializer = wsgi.RequestDeserializer( + supported_content_types=supported_content_types) + + + + return wsgi.Resource(controller, serializer=serializer, + deserializer=deserializer) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index c48f397a1..88fe71041 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -271,75 +271,73 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, actual) def test_get_version_1_0_detail_atom(self): - #TODO req = webob.Request.blank('/v1.0/') - req.accept = "application/xml" + req.accept = "application/atom+xml" res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/xml") + self.assertEqual("application/atom+xml", res.content_type) expected = """ - - - - - - - - - - - - - """.replace(" ", "").replace("\n", "") + + About This Version + 2011-01-21T11:33:21Z + http://servers.api.openstack.org/v1.0/ + + Rackspace + http://www.rackspace.com/ + + + + http://servers.api.openstack.org/v1.0/ + Version v1.0 + 2011-01-21T11:33:21Z + + + + + Version v1.0 DEPRECATED (2011-01-21T11:33:21Z) + + + """.replace(" ", "").replace("\n", "") actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) def test_get_version_1_1_detail_atom(self): - #TODO req = webob.Request.blank('/v1.1/') - req.accept = "application/xml" + req.accept = "application/atom+xml" res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/xml") + self.assertEqual("application/atom+xml", res.content_type) expected = """ - - - - - - - - - - - - - """.replace(" ", "").replace("\n", "") + + About This Version + 2011-01-21T11:33:21Z + http://servers.api.openstack.org/v1.1/ + + Rackspace + http://www.rackspace.com/ + + + + http://servers.api.openstack.org/v1.1/ + Version v1.1 + 2011-01-21T11:33:21Z + + + + + Version v1.1 CURRENT (2011-01-21T11:33:21Z) + + + """.replace(" ", "").replace("\n", "") actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -621,7 +619,6 @@ class VersionsTest(test.TestCase): #self.assertEqual(val, media_node.get(key)) def test_version_detail_atom_serializer(self): - #TODO versions_data = { "version" : { "id": "v1.1", -- cgit From cdcc860cd5d513638c9d85b692f4b46b5e2832ef Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:18:08 -0400 Subject: fixed detail xml and json tests that got broken --- nova/tests/api/openstack/test_versions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 88fe71041..448a69c07 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -80,7 +80,7 @@ class VersionsTest(test.TestCase): expected = { "version" : { "id" : "v1.0", - "status" : "CURRENT", + "status" : "DEPRECATED", "updated" : "2011-01-21T11:33:21Z", "links": [ { @@ -184,7 +184,7 @@ class VersionsTest(test.TestCase): expected = """ - @@ -609,6 +609,9 @@ class VersionsTest(test.TestCase): entry_id = entry_children[0] entry_title = entry_children[1] entry_updated = entry_children[1] + entry_link = entry_children[1] + entry_content = entry_children[1] + self.assertEqual(entry_id.text, "http://test/2.9.8") -- cgit From bde063a98dad2ce75be1016b39a2c3f08759d4f6 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:30:58 -0400 Subject: got rid of string comparisons in serializer tests --- nova/api/openstack/versions.py | 9 +++++ nova/tests/api/openstack/test_versions.py | 67 ++++++++----------------------- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 03b99f342..40c187607 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -205,6 +205,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): # in the base class (XMLDictSerializer), which I plan to do in # another branch def to_xml_string(self, node, has_atom=False): + print "TOXML" self._add_xmlns(node, has_atom) return node.toxml(encoding='UTF-8') @@ -273,6 +274,14 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): class VersionsAtomSerializer(wsgi.XMLDictSerializer): + #TODO(wwolf): this is temporary until we get rid of toprettyxml + # in the base class (XMLDictSerializer), which I plan to do in + # another branch + def to_xml_string(self, node, has_atom=False): + print "TOXML" + self._add_xmlns(node, has_atom) + return node.toxml(encoding='UTF-8') + def __init__(self, metadata=None, xmlns=None): self.metadata = metadata or {} if not xmlns: diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 448a69c07..b8c985e4d 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -531,47 +531,9 @@ class VersionsTest(test.TestCase): ] } - expected = """ - - - Available API Versions - - - 2011-07-20T11:40:00Z - - - http://test/ - - - - Rackspace - - - http://www.rackspace.com/ - - - - - - http://test/2.9.8 - - - Version 2.9.8 - - - 2011-07-20T11:40:00Z - - - - Version 2.9.8 CURRENT (2011-07-20T11:40:00Z) - - - """.replace(" ", "").replace("\n", "") - serializer = versions.VersionsAtomSerializer() response = serializer.index(versions_data) - response = response.replace(" ", "").replace("\n", "") - self.assertEqual(expected, response) + print response root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "feed") @@ -608,18 +570,23 @@ class VersionsTest(test.TestCase): entry_children = list(entry) entry_id = entry_children[0] entry_title = entry_children[1] - entry_updated = entry_children[1] - entry_link = entry_children[1] - entry_content = entry_children[1] + entry_updated = entry_children[2] + entry_link = entry_children[3] + entry_content = entry_children[4] + self.assertEqual(entry_id.tag.split('}')[1], "id") self.assertEqual(entry_id.text, "http://test/2.9.8") - - - - #self.assertEqual(media_types.tag.split('}')[1], 'media-types') - #for i, media_node in enumerate(media_type_nodes): - #self.assertEqual(media_node.tag.split('}')[1], 'media-type') - #for key, val in version_data['version']['media-types'][i].items(): - #self.assertEqual(val, media_node.get(key)) + self.assertEqual(entry_title.tag.split('}')[1], "title") + self.assertEqual(entry_title.get('type'), "text") + self.assertEqual(entry_title.text, "Version 2.9.8") + self.assertEqual(entry_updated.tag.split('}')[1], "updated") + self.assertEqual(entry_updated.text, "2011-07-20T11:40:00Z") + self.assertEqual(entry_link.tag.split('}')[1], "link") + self.assertEqual(entry_link.get('href'), "http://test/2.9.8") + self.assertEqual(entry_link.get('rel'), "self") + self.assertEqual(entry_content.tag.split('}')[1], "content") + self.assertEqual(entry_content.get('type'), "text") + self.assertEqual(entry_content.text, + "Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)") def test_version_detail_atom_serializer(self): versions_data = { -- cgit From 26f980c955e357df3685bcccda005a3008f86afb Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:31:16 -0400 Subject: got rid of some prints --- nova/api/openstack/versions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 40c187607..f389933b9 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -205,7 +205,6 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): # in the base class (XMLDictSerializer), which I plan to do in # another branch def to_xml_string(self, node, has_atom=False): - print "TOXML" self._add_xmlns(node, has_atom) return node.toxml(encoding='UTF-8') @@ -278,7 +277,6 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): # in the base class (XMLDictSerializer), which I plan to do in # another branch def to_xml_string(self, node, has_atom=False): - print "TOXML" self._add_xmlns(node, has_atom) return node.toxml(encoding='UTF-8') -- cgit From d3e557ae0d49ea8d4a1cd50abbada6e8c1c4a7fe Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:34:19 -0400 Subject: atom test updates --- nova/tests/api/openstack/test_versions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index b8c985e4d..59dc9fb92 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -548,6 +548,7 @@ class VersionsTest(test.TestCase): link = children[4] entry = children[5] + self.assertEqual(root.tag.split('}')[1], 'feed') self.assertEqual(title.tag.split('}')[1], 'title') self.assertEqual(title.text, 'Available API Versions') self.assertEqual(updated.tag.split('}')[1], 'updated') -- cgit From 3b6208f44c079323efa290dfeb68a4afbdfb3349 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:43:57 -0400 Subject: got rid of more xml string comparisons --- nova/tests/api/openstack/test_versions.py | 90 ++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 59dc9fb92..992efbf1c 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -548,7 +548,6 @@ class VersionsTest(test.TestCase): link = children[4] entry = children[5] - self.assertEqual(root.tag.split('}')[1], 'feed') self.assertEqual(title.tag.split('}')[1], 'title') self.assertEqual(title.text, 'Available API Versions') self.assertEqual(updated.tag.split('}')[1], 'updated') @@ -626,34 +625,65 @@ class VersionsTest(test.TestCase): }, } - expected = """ - - About This Version - 2011-01-21T11:33:21Z - http://servers.api.openstack.org/v1.1/ - - Rackspace - http://www.rackspace.com/ - - - - http://servers.api.openstack.org/v1.1/ - Version v1.1 - 2011-01-21T11:33:21Z - - - - - Version v1.1 CURRENT (2011-01-21T11:33:21Z) - - - """.replace(" ", "").replace("\n", "") - serializer = versions.VersionsAtomSerializer() response = serializer.detail(versions_data) - response = response.replace(" ", "").replace("\n", "") - self.assertEqual(expected, response) + + root = xml.etree.ElementTree.XML(response) + self.assertEqual(root.tag.split('}')[1], "feed") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "http://www.w3.org/2005/Atom") + + children = list(root) + title = children[0] + updated = children[1] + id = children[2] + author = children[3] + link = children[4] + entry = children[5] + + self.assertEqual(root.tag.split('}')[1], 'feed') + self.assertEqual(title.tag.split('}')[1], 'title') + self.assertEqual(title.text, 'About This Version') + self.assertEqual(updated.tag.split('}')[1], 'updated') + self.assertEqual(updated.text, '2011-01-21T11:33:21Z') + self.assertEqual(id.tag.split('}')[1], 'id') + self.assertEqual(id.text, 'http://servers.api.openstack.org/v1.1/') + + self.assertEqual(author.tag.split('}')[1], 'author') + author_name = list(author)[0] + author_uri = list(author)[1] + self.assertEqual(author_name.tag.split('}')[1], 'name') + self.assertEqual(author_name.text, 'Rackspace') + self.assertEqual(author_uri.tag.split('}')[1], 'uri') + self.assertEqual(author_uri.text, 'http://www.rackspace.com/') + + self.assertEqual(link.get('href'), + 'http://servers.api.openstack.org/v1.1/') + self.assertEqual(link.get('rel'), 'self') + + self.assertEqual(entry.tag.split('}')[1], 'entry') + entry_children = list(entry) + entry_id = entry_children[0] + entry_title = entry_children[1] + entry_updated = entry_children[2] + entry_links = (entry_children[3], entry_children[4], entry_children[5]) + entry_content = entry_children[6] + + self.assertEqual(entry_id.tag.split('}')[1], "id") + self.assertEqual(entry_id.text, + "http://servers.api.openstack.org/v1.1/") + self.assertEqual(entry_title.tag.split('}')[1], "title") + self.assertEqual(entry_title.get('type'), "text") + self.assertEqual(entry_title.text, "Version v1.1") + self.assertEqual(entry_updated.tag.split('}')[1], "updated") + self.assertEqual(entry_updated.text, "2011-01-21T11:33:21Z") + + for i, link in enumerate(versions_data["version"]["links"]): + self.assertEqual(entry_links[i].tag.split('}')[1], "link") + for key, val in versions_data["version"]["links"][i].items(): + self.assertEqual(entry_links[i].get(key), val) + + self.assertEqual(entry_content.tag.split('}')[1], "content") + self.assertEqual(entry_content.get('type'), "text") + self.assertEqual(entry_content.text, + "Version v1.1 CURRENT (2011-01-21T11:33:21Z)") -- cgit From 58eef7695eb5539f75e358b2f55b50063551a44d Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 11:44:19 -0400 Subject: got rid of print --- nova/tests/api/openstack/test_versions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 992efbf1c..d1ec0b84a 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -533,7 +533,6 @@ class VersionsTest(test.TestCase): serializer = versions.VersionsAtomSerializer() response = serializer.index(versions_data) - print response root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "feed") -- cgit From 6fb2fe901bc4f4479e6a2bb087870927be7318a2 Mon Sep 17 00:00:00 2001 From: John Tran Date: Tue, 26 Jul 2011 10:03:16 -0700 Subject: added warning when size of subnet(s) being created are larger than FLAG.network_size in attempt to alleviate confusion. For example, currently when 'nova-manage network create foo 192.168.0.0/16', the result is that it creates a 192.168.0.0/24 instead without any indication to why. --- bin/nova-manage | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index b63bd326f..da9538e39 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -56,6 +56,7 @@ import gettext import glob import json +import math import netaddr import os import sys @@ -669,6 +670,14 @@ class NetworkCommands(object): num_networks = FLAGS.num_networks if not network_size: network_size = FLAGS.network_size + fixnet = netaddr.IPNetwork(fixed_range) + each_subnet_size = fixnet.size / int(num_networks) + if each_subnet_size > 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) + print oversize_msg if not multi_host: multi_host = FLAGS.multi_host else: -- cgit From e239dc589982a0d90eb8a50967af05a10d5e4d5b Mon Sep 17 00:00:00 2001 From: John Tran Date: Tue, 26 Jul 2011 13:12:34 -0700 Subject: fixed per peer review --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index da9538e39..ca60d28d6 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -676,7 +676,7 @@ class NetworkCommands(object): subnet = 32 - int(math.log(network_size, 2)) oversize_msg = _('Subnet(s) too large, defaulting to /%s.' ' To override, specify network_size flag.' - % subnet) + ) % subnet print oversize_msg if not multi_host: multi_host = FLAGS.multi_host -- cgit From b18754473785611112ae54523677da83dff24075 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Tue, 26 Jul 2011 20:58:33 +0000 Subject: First pass at converting this stuff--pass context down into vmops. Still need to fix unit tests and actually use auth_token from the context... --- nova/compute/manager.py | 9 +++++---- nova/virt/xenapi/vm_utils.py | 22 +++++++++++----------- nova/virt/xenapi/vmops.py | 29 +++++++++++++++-------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 31627fe3b..667d231ae 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -326,7 +326,7 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_state(context, instance_id, power_state.BUILDING) try: - self.driver.spawn(instance, network_info, bd_mapping) + self.driver.spawn(context, instance, network_info, bd_mapping) except Exception as ex: # pylint: disable=W0702 msg = _("Instance '%(instance_id)s' failed to spawn. Is " "virtualization enabled in the BIOS? Details: " @@ -430,7 +430,7 @@ class ComputeManager(manager.SchedulerDependentManager): image_ref = kwargs.get('image_ref') instance_ref.image_ref = image_ref instance_ref.injected_files = kwargs.get('injected_files', []) - self.driver.spawn(instance_ref, network_info) + self.driver.spawn(context, instance_ref, network_info) self._update_image_ref(context, instance_id, image_ref) self._update_launched_at(context, instance_id) @@ -498,7 +498,7 @@ class ComputeManager(manager.SchedulerDependentManager): 'instance: %(instance_id)s (state: %(state)s ' 'expected: %(running)s)') % locals()) - self.driver.snapshot(instance_ref, image_id) + self.driver.snapshot(context, instance_ref, image_id) if image_type == 'snapshot': if rotation: @@ -855,7 +855,8 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref = self.db.instance_get_by_uuid(context, instance_ref.uuid) network_info = self._get_instance_nw_info(context, instance_ref) - self.driver.finish_resize(instance_ref, disk_info, network_info) + self.driver.finish_resize(context, instance_ref, disk_info, + network_info) self.db.migration_update(context, migration_id, {'status': 'finished', }) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 62863c6d8..aa0e4c2df 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -359,7 +359,7 @@ class VMHelper(HelperBase): return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid) @classmethod - def upload_image(cls, session, instance, vdi_uuids, image_id): + def upload_image(cls, context, session, instance, vdi_uuids, image_id): """ Requests that the Glance plugin bundle the specified VDIs and push them into Glance using the specified human-friendly name. """ @@ -384,7 +384,7 @@ class VMHelper(HelperBase): session.wait_for_task(task, instance.id) @classmethod - def fetch_image(cls, session, instance_id, image, user, project, + def fetch_image(cls, context, session, instance_id, image, user, project, image_type): """ image_type is interpreted as an ImageType instance @@ -399,16 +399,16 @@ class VMHelper(HelperBase): access = AuthManager().get_access_key(user, project) if FLAGS.xenapi_image_service == 'glance': - return cls._fetch_image_glance(session, instance_id, image, - access, image_type) + return cls._fetch_image_glance(context, session, instance_id, + image, access, image_type) else: return cls._fetch_image_objectstore(session, instance_id, image, access, user.secret, image_type) @classmethod - def _fetch_image_glance_vhd(cls, session, instance_id, image, access, - image_type): + def _fetch_image_glance_vhd(cls, context, session, instance_id, image, + access, image_type): """Tell glance to download an image and put the VHDs into the SR Returns: A list of dictionaries that describe VDIs @@ -455,8 +455,8 @@ class VMHelper(HelperBase): return vdis @classmethod - def _fetch_image_glance_disk(cls, session, instance_id, image, access, - image_type): + def _fetch_image_glance_disk(cls, context, session, instance_id, image, + access, image_type): """Fetch the image from Glance NOTE: @@ -589,7 +589,7 @@ class VMHelper(HelperBase): return image_type @classmethod - def _fetch_image_glance(cls, session, instance_id, image, access, + def _fetch_image_glance(cls, context, session, instance_id, image, access, image_type): """Fetch image from glance based on image type. @@ -597,10 +597,10 @@ class VMHelper(HelperBase): A list of dictionaries that describe VDIs, otherwise """ if image_type == ImageType.DISK_VHD: - return cls._fetch_image_glance_vhd( + return cls._fetch_image_glance_vhd(context, session, instance_id, image, access, image_type) else: - return cls._fetch_image_glance_disk( + return cls._fetch_image_glance_disk(context, session, instance_id, image, access, image_type) @classmethod diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 0473abb97..1c6604836 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -114,10 +114,10 @@ class VMOps(object): vm_ref = VMHelper.lookup(self._session, instance.name) self._start(instance, vm_ref) - def finish_resize(self, instance, disk_info, network_info): + def finish_resize(self, context, instance, disk_info, network_info): vdi_uuid = self.link_disks(instance, disk_info['base_copy'], disk_info['cow']) - vm_ref = self._create_vm(instance, + vm_ref = self._create_vm(context, instance, [dict(vdi_type='os', vdi_uuid=vdi_uuid)], network_info) self.resize_instance(instance, vdi_uuid) @@ -133,20 +133,20 @@ class VMOps(object): LOG.debug(_("Starting instance %s"), instance.name) self._session.call_xenapi('VM.start', vm_ref, False, False) - def _create_disks(self, instance): + def _create_disks(self, context, instance): user = AuthManager().get_user(instance.user_id) project = AuthManager().get_project(instance.project_id) disk_image_type = VMHelper.determine_disk_image_type(instance) - vdis = VMHelper.fetch_image(self._session, + vdis = VMHelper.fetch_image(context, self._session, instance.id, instance.image_ref, user, project, disk_image_type) return vdis - def spawn(self, instance, network_info): + def spawn(self, context, instance, network_info): vdis = None try: - vdis = self._create_disks(instance) - vm_ref = self._create_vm(instance, vdis, network_info) + vdis = self._create_disks(context, instance) + vm_ref = self._create_vm(context, instance, vdis, network_info) self._spawn(instance, vm_ref) except (self.XenAPI.Failure, OSError, IOError) as spawn_error: LOG.exception(_("instance %s: Failed to spawn"), @@ -160,7 +160,7 @@ class VMOps(object): """Spawn a rescue instance.""" self.spawn(instance) - def _create_vm(self, instance, vdis, network_info): + def _create_vm(self, context, instance, vdis, network_info): """Create VM instance.""" instance_name = instance.name vm_ref = VMHelper.lookup(self._session, instance_name) @@ -184,12 +184,12 @@ class VMOps(object): ramdisk = None try: if instance.kernel_id: - kernel = VMHelper.fetch_image(self._session, instance.id, - instance.kernel_id, user, project, + kernel = VMHelper.fetch_image(context, self._session, + instance.id, instance.kernel_id, user, project, ImageType.KERNEL)[0] if instance.ramdisk_id: - ramdisk = VMHelper.fetch_image(self._session, instance.id, - instance.ramdisk_id, user, project, + ramdisk = VMHelper.fetch_image(context, self._session, + instance.id, instance.ramdisk_id, user, project, ImageType.RAMDISK)[0] # Create the VM ref and attach the first disk first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', @@ -440,9 +440,10 @@ class VMOps(object): vm, "start") - def snapshot(self, instance, image_id): + def snapshot(self, context, instance, image_id): """Create snapshot from a running VM instance. + :param context: request context :param instance: instance to be snapshotted :param image_id: id of image to upload to @@ -467,7 +468,7 @@ class VMOps(object): try: template_vm_ref, template_vdi_uuids = self._get_snapshot(instance) # call plugin to ship snapshot off to glance - VMHelper.upload_image( + VMHelper.upload_image(context, self._session, instance, template_vdi_uuids, image_id) finally: if template_vm_ref: -- cgit From e14754bbdbacaf6943c4061e3488f2580acd26ad Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 17:51:46 -0400 Subject: initial working 300 multiple choice stuff --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/versions.py | 191 +++++++++++++++++------------- nova/api/openstack/views/versions.py | 8 ++ nova/tests/api/openstack/test_versions.py | 59 ++++++++- 4 files changed, 174 insertions(+), 86 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index c10937bd6..0dfad0ef6 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -118,7 +118,7 @@ class APIRouter(base_wsgi.Router): mapper.connect("versions", "/", controller=versions.create_resource(version), - action="detail") + action='show') mapper.resource("console", "consoles", controller=consoles.create_resource(), diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index f389933b9..4b567957d 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -25,6 +25,80 @@ from nova.api.openstack import wsgi ATOM_XMLNS = "http://www.w3.org/2005/Atom" +VERSIONS = { + "v1.0": { + "version" : { + "id": "v1.0", + "status": "DEPRECATED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.0/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.0+json" + } + ], + }, + }, + "v1.1": { + "version" : { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.1/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base" : "application/xml", + "type" : "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base" : "application/json", + "type" : "application/vnd.openstack.compute-v1.1+json" + } + ], + }, + }, +} + + class Versions(wsgi.Resource): @@ -43,11 +117,15 @@ class Versions(wsgi.Resource): } } + headers_serializer = VersionsHeadersSerializer() + body_serializers = { 'application/atom+xml': VersionsAtomSerializer(metadata=metadata), 'application/xml': VersionsXMLSerializer(metadata=metadata), } - serializer = wsgi.ResponseSerializer(body_serializers) + serializer = wsgi.ResponseSerializer( + body_serializers=body_serializers, + headers_serializer=headers_serializer) supported_content_types = ('application/json', 'application/xml', @@ -93,100 +171,41 @@ class Versions(wsgi.Resource): { "id": "v1.1", "status": "CURRENT", - #TODO(wwolf) get correct value for these - "updated": "2011-07-18T11:30:00Z", + "links": [ + { + "rel": "self", + } + ], + "media-types": VERSIONS['v1.1']['version']['media-types'], }, { "id": "v1.0", "status": "DEPRECATED", - #TODO(wwolf) get correct value for these - "updated": "2010-10-09T11:30:00Z", + "links": [ + { + "rel": "self", + } + ], + "media-types": VERSIONS['v1.0']['version']['media-types'], }, ] builder = nova.api.openstack.views.versions.get_view_builder(request) - versions = [builder.build(version) for version in version_objs] - return dict(versions=versions) + choices = [ + builder.build_choices(version, request) + for version in version_objs] + + return dict(choices=choices) class VersionV10(object): - def detail(self, req): - #TODO - return { - "version" : { - "id": "v1.0", - "status": "DEPRECATED", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.0/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" - }, - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.0+xml" - }, - { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.0+json" - } - ], - }, - } + def show(self, req): + return VERSIONS['v1.0'] class VersionV11(object): - def detail(self, req): - return { - "version" : { - "id": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.1/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" - }, - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.1+xml" - }, - { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.1+json" - } - ], - }, - } - + def show(self, req): + return VERSIONS['v1.1'] class VersionsRequestDeserializer(wsgi.RequestDeserializer): def get_action_args(self, request_environment): @@ -259,7 +278,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) - def detail(self,data): + def show(self,data): self._xml_doc = minidom.Document() node = self._create_version_node(data['version'], True) @@ -404,7 +423,7 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) - def detail(self, data): + def show(self, data): self._xml_doc = minidom.Document() node = self._xml_doc.createElementNS(self.xmlns, 'feed') self._create_detail_meta(node, data['version']) @@ -412,6 +431,12 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) + +class VersionsHeadersSerializer(wsgi.ResponseHeadersSerializer): + def multi(self, response, data): + response.status_int = 300 + + def create_resource(version='1.0'): controller = { '1.0': VersionV10, diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 9fa8f49dc..97e35c983 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -31,6 +31,11 @@ class ViewBuilder(object): """ self.base_url = base_url + def build_choices(self, version_data, request): + version_data['links'][0]['href'] = self._build_versioned_link(request, + version_data['id']) + return version_data + def build(self, version_data): """Generic method used to generate a version entity.""" version = { @@ -42,6 +47,9 @@ class ViewBuilder(object): return version + def _build_versioned_link(self, req, version): + return '%s://%s/%s%s' % (req.scheme, req.host, version, req.path) + def _build_links(self, version_data): """Generate a container of links that refer to the provided version.""" href = self.generate_href(version_data["id"]) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index d1ec0b84a..f33ef5e6a 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -384,6 +384,61 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, actual) + def test_multi_choice_servers_list(self): + req = webob.Request.blank('/servers/2') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + print res.body + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") + + expected = { + "choices": [ + { + "id": "v1.1", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost:80/v1.1/servers/2", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" + }, + ], + }, + { + "id": "v1.0", + "status": "DEPRECATED", + "links": [ + { + "href": "http://localhost:80/v1.0/servers/2", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" + }, + ], + }, + ],} + + self.assertDictMatch(expected, json.loads(res.body)) + + def test_view_builder(self): base_url = "http://example.org/" @@ -488,7 +543,7 @@ class VersionsTest(test.TestCase): } serializer = versions.VersionsXMLSerializer() - response = serializer.detail(version_data) + response = serializer.show(version_data) root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "version") @@ -625,7 +680,7 @@ class VersionsTest(test.TestCase): } serializer = versions.VersionsAtomSerializer() - response = serializer.detail(versions_data) + response = serializer.show(versions_data) root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "feed") -- cgit From 6dbd7583f4f1ca4be59e163c4c568423a91cd29e Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 18:10:36 -0400 Subject: pep8 fixes --- nova/api/openstack/__init__.py | 4 +-- nova/api/openstack/versions.py | 31 +++++++++--------- nova/tests/api/openstack/test_versions.py | 52 +++++++++++++++---------------- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0dfad0ef6..96a2f20e0 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -116,8 +116,8 @@ class APIRouter(base_wsgi.Router): 'select': 'POST', 'boot': 'POST'}) - mapper.connect("versions", "/", - controller=versions.create_resource(version), + mapper.connect("versions", "/", + controller=versions.create_resource(version), action='show') mapper.resource("console", "consoles", diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 4b567957d..58c767d5c 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -25,12 +25,12 @@ from nova.api.openstack import wsgi ATOM_XMLNS = "http://www.w3.org/2005/Atom" -VERSIONS = { +VERSIONS = { "v1.0": { - "version" : { + "version" : { "id": "v1.0", "status": "DEPRECATED", - "updated": "2011-01-21T11:33:21Z", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -41,7 +41,7 @@ VERSIONS = { "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" "servers/api/v1.0/cs-devguide-20110125.pdf" - }, + }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", @@ -62,7 +62,7 @@ VERSIONS = { }, }, "v1.1": { - "version" : { + "version" : { "id": "v1.1", "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", @@ -76,7 +76,7 @@ VERSIONS = { "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" "servers/api/v1.1/cs-devguide-20110125.pdf" - }, + }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", @@ -99,8 +99,6 @@ VERSIONS = { } - - class Versions(wsgi.Resource): @classmethod def factory(cls, global_config, **local_config): @@ -192,7 +190,7 @@ class Versions(wsgi.Resource): builder = nova.api.openstack.views.versions.get_view_builder(request) choices = [ - builder.build_choices(version, request) + builder.build_choices(version, request) for version in version_objs] return dict(choices=choices) @@ -207,6 +205,7 @@ class VersionV11(object): def show(self, req): return VERSIONS['v1.1'] + class VersionsRequestDeserializer(wsgi.RequestDeserializer): def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" @@ -220,8 +219,8 @@ class VersionsRequestDeserializer(wsgi.RequestDeserializer): class VersionsXMLSerializer(wsgi.XMLDictSerializer): - #TODO(wwolf): this is temporary until we get rid of toprettyxml - # in the base class (XMLDictSerializer), which I plan to do in + #TODO(wwolf): this is temporary until we get rid of toprettyxml + # in the base class (XMLDictSerializer), which I plan to do in # another branch def to_xml_string(self, node, has_atom=False): self._add_xmlns(node, has_atom) @@ -244,7 +243,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): base.appendChild(node) return base - + def _create_version_node(self, version, create_ns=False): version_node = self._xml_doc.createElement('version') if create_ns: @@ -278,7 +277,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): return self.to_xml_string(node) - def show(self,data): + def show(self, data): self._xml_doc = minidom.Document() node = self._create_version_node(data['version'], True) @@ -292,8 +291,8 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): class VersionsAtomSerializer(wsgi.XMLDictSerializer): - #TODO(wwolf): this is temporary until we get rid of toprettyxml - # in the base class (XMLDictSerializer), which I plan to do in + #TODO(wwolf): this is temporary until we get rid of toprettyxml + # in the base class (XMLDictSerializer), which I plan to do in # another branch def to_xml_string(self, node, has_atom=False): self._add_xmlns(node, has_atom) @@ -455,7 +454,5 @@ def create_resource(version='1.0'): deserializer = wsgi.RequestDeserializer( supported_content_types=supported_content_types) - - return wsgi.Resource(controller, serializer=serializer, deserializer=deserializer) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index f33ef5e6a..587b085c3 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -35,7 +35,6 @@ class VersionsTest(test.TestCase): self.stubs = stubout.StubOutForTesting() fakes.stub_out_auth(self.stubs) - def tearDown(self): super(VersionsTest, self).tearDown() @@ -182,12 +181,11 @@ class VersionsTest(test.TestCase): for media_node in media_type_nodes: self.assertEqual(media_node.tag.split('}')[1], 'media-type') - expected = """ - + + rel="describedby" + type="application/vnd.sun.wadl+xml"/> """.replace(" ", "").replace("\n", "") actual = res.body.replace(" ", "").replace("\n", "") @@ -220,10 +218,10 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") expected = """ - + + rel="describedby" + type="application/vnd.sun.wadl+xml"/> """.replace(" ", "").replace("\n", "") actual = res.body.replace(" ", "").replace("\n", "") @@ -290,7 +288,8 @@ class VersionsTest(test.TestCase): http://servers.api.openstack.org/v1.0/ Version v1.0 2011-01-21T11:33:21Z - + @@ -326,7 +325,8 @@ class VersionsTest(test.TestCase): http://servers.api.openstack.org/v1.1/ Version v1.1 2011-01-21T11:33:21Z - + @@ -407,13 +407,13 @@ class VersionsTest(test.TestCase): { "base": "application/xml", "type": "application/vnd.openstack.compute-v1.1+xml" - }, + }, { "base": "application/json", "type": "application/vnd.openstack.compute-v1.1+json" }, ], - }, + }, { "id": "v1.0", "status": "DEPRECATED", @@ -432,13 +432,12 @@ class VersionsTest(test.TestCase): "base": "application/json", "type": "application/vnd.openstack.compute-v1.0+json" }, - ], + ], }, - ],} + ], } self.assertDictMatch(expected, json.loads(res.body)) - def test_view_builder(self): base_url = "http://example.org/" @@ -507,10 +506,10 @@ class VersionsTest(test.TestCase): def test_version_detail_xml_serializer(self): version_data = { - "version" : { + "version" : { "id": "v1.0", "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -521,7 +520,7 @@ class VersionsTest(test.TestCase): "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" "servers/api/v1.0/cs-devguide-20110125.pdf" - }, + }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", @@ -568,7 +567,6 @@ class VersionsTest(test.TestCase): for key, val in version_data['version']['links'][i].items(): self.assertEqual(val, link.get(key)) - def test_versions_list_atom_serializer(self): versions_data = { 'versions': [ @@ -644,7 +642,7 @@ class VersionsTest(test.TestCase): def test_version_detail_atom_serializer(self): versions_data = { - "version" : { + "version" : { "id": "v1.1", "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", @@ -658,7 +656,7 @@ class VersionsTest(test.TestCase): "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" "servers/api/v1.1/cs-devguide-20110125.pdf" - }, + }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", -- cgit From 15068c4038d93db77278ea3306d992b424168c24 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Tue, 26 Jul 2011 18:16:28 -0400 Subject: added multi_choice test just to hit another resource --- nova/tests/api/openstack/test_versions.py | 56 +++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 587b085c3..d8d6cebe2 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -384,11 +384,63 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, actual) - def test_multi_choice_servers_list(self): + def test_multi_choice_image(self): + req = webob.Request.blank('/images/1') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") + + expected = { + "choices": [ + { + "id": "v1.1", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost:80/v1.1/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" + }, + ], + }, + { + "id": "v1.0", + "status": "DEPRECATED", + "links": [ + { + "href": "http://localhost:80/v1.0/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" + }, + ], + }, + ], } + + self.assertDictMatch(expected, json.loads(res.body)) + + def test_multi_choice_server(self): req = webob.Request.blank('/servers/2') req.accept = "application/json" res = req.get_response(fakes.wsgi_app()) - print res.body self.assertEqual(res.status_int, 300) self.assertEqual(res.content_type, "application/json") -- cgit From c95ee1625a6a88afdb77d305077d1ee7eeaae854 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 26 Jul 2011 16:20:31 -0700 Subject: fix for lp816713: In instance creation, when nova-api is passed imageRefs generated by itself, strip the url down to an id so that default glance connection params are used --- nova/api/openstack/create_instance_helper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index f8317565e..0e6c0a87c 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -87,6 +87,10 @@ class CreateInstanceHelper(object): key_data = key_pair['public_key'] 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 image_href.startswith(req.application_url): + image_href = image_href.split('/').pop() try: image_service, image_id = nova.image.get_image_service(image_href) kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( -- cgit From 74e3218d2b6045457019c4de518ca4a869e37807 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 09:11:41 -0400 Subject: multi choice XML responses with tests --- nova/api/openstack/versions.py | 15 ++++-- nova/tests/api/openstack/test_versions.py | 87 +++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 58c767d5c..9909d8d0d 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -25,6 +25,7 @@ from nova.api.openstack import wsgi ATOM_XMLNS = "http://www.w3.org/2005/Atom" +OS_XMLNS_BASE = "http://docs.openstack.org/common/api/" VERSIONS = { "v1.0": { "version" : { @@ -226,8 +227,10 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): self._add_xmlns(node, has_atom) return node.toxml(encoding='UTF-8') - def _versions_to_xml(self, versions): - root = self._xml_doc.createElement('versions') + def _versions_to_xml(self, versions, name="versions", xmlns=None): + root = self._xml_doc.createElement(name) + root.setAttribute("xmlns", "%sv1.0" % OS_XMLNS_BASE) + root.setAttribute("xmlns:atom", ATOM_XMLNS) for version in versions: root.appendChild(self._create_version_node(version)) @@ -247,14 +250,15 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _create_version_node(self, version, create_ns=False): version_node = self._xml_doc.createElement('version') if create_ns: - xmlns = "http://docs.openstack.org/common/api/%s" % version['id'] + xmlns = "%s%s" % (OS_XMLNS_BASE, version['id']) xmlns_atom = "http://www.w3.org/2005/Atom" version_node.setAttribute('xmlns', xmlns) version_node.setAttribute('xmlns:atom', xmlns_atom) version_node.setAttribute('id', version['id']) version_node.setAttribute('status', version['status']) - version_node.setAttribute('updated', version['updated']) + if 'updated' in version: + version_node.setAttribute('updated', version['updated']) if 'media-types' in version: media_types = self._create_media_types(version['media-types']) @@ -285,7 +289,8 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def multi(self, data): self._xml_doc = minidom.Document() - node = self._versions_to_xml(data['versions']) + node = self._versions_to_xml(data['choices'], 'choices', + xmlns="%sv1.0" % OS_XMLNS_BASE) return self.to_xml_string(node) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index d8d6cebe2..dce8e38f1 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -27,6 +27,9 @@ from nova.tests.api.openstack import fakes from nova.api.openstack import versions from nova.api.openstack import views +ATOM_XMLNS = versions.ATOM_XMLNS +OS_XMLNS_BASE = versions.OS_XMLNS_BASE + class VersionsTest(test.TestCase): def setUp(self): @@ -170,7 +173,7 @@ class VersionsTest(test.TestCase): root = xml.etree.ElementTree.XML(res.body) self.assertEqual(root.tag.split('}')[1], "version") self.assertEqual(root.tag.split('}')[0].strip('{'), - "http://docs.openstack.org/common/api/v1.0") + "%sv1.0" % OS_XMLNS_BASE) children = list(root) media_types = children[0] @@ -184,7 +187,7 @@ class VersionsTest(test.TestCase): expected = """ @@ -206,7 +209,7 @@ class VersionsTest(test.TestCase): api/v1.0/application.wadl" rel="describedby" type="application/vnd.sun.wadl+xml"/> - """.replace(" ", "").replace("\n", "") + """.replace(" ", "").replace("\n", "") % OS_XMLNS_BASE actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -220,7 +223,7 @@ class VersionsTest(test.TestCase): expected = """ @@ -242,7 +245,8 @@ class VersionsTest(test.TestCase): api/v1.1/application.wadl" rel="describedby" type="application/vnd.sun.wadl+xml"/> - """.replace(" ", "").replace("\n", "") + """.replace(" ", "").replace("\n", "") % OS_XMLNS_BASE + actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -254,7 +258,8 @@ class VersionsTest(test.TestCase): self.assertEqual(res.status_int, 200) self.assertEqual(res.content_type, "application/xml") - expected = """ + expected = """ + @@ -262,7 +267,8 @@ class VersionsTest(test.TestCase): updated="2010-10-09T11:30:00Z"> - """.replace(" ", "").replace("\n", "") + """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, + ATOM_XMLNS) actual = res.body.replace(" ", "").replace("\n", "") @@ -437,6 +443,36 @@ class VersionsTest(test.TestCase): self.assertDictMatch(expected, json.loads(res.body)) + def test_multi_choice_image_xml(self): + req = webob.Request.blank('/images/1') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/xml") + + expected = """ + + + + + + + + + + + + + + + + """.replace(" ", "").replace("\n","") % (OS_XMLNS_BASE, + ATOM_XMLNS) + def test_multi_choice_server(self): req = webob.Request.blank('/servers/2') req.accept = "application/json" @@ -544,18 +580,51 @@ class VersionsTest(test.TestCase): } expected = """ - + - """.replace(" ", "").replace("\n", "") + """.replace(" ", "").replace("\n", "") % ( + OS_XMLNS_BASE,ATOM_XMLNS) serializer = versions.VersionsXMLSerializer() response = serializer.index(versions_data) response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) + def test_versions_multi_xml_serializer(self): + versions_data = { + 'choices': [ + { + "id": "2.7.1", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "links": [ + { + "rel": "self", + "href": "http://test/2.7.1/images", + }, + ], + }, + ] + } + + expected = """ + + + + + """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, + ATOM_XMLNS) + + serializer = versions.VersionsXMLSerializer() + response = serializer.multi(versions_data) + response = response.replace(" ", "").replace("\n", "") + self.assertEqual(expected, response) + + def test_version_detail_xml_serializer(self): version_data = { "version" : { -- cgit From 8389d214a917f1c4f2d8ddb471f94c6087ec9ea9 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 09:13:48 -0400 Subject: pep8 cleanup --- nova/tests/api/openstack/test_versions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index dce8e38f1..b2fb0efb6 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -247,7 +247,6 @@ class VersionsTest(test.TestCase): type="application/vnd.sun.wadl+xml"/> """.replace(" ", "").replace("\n", "") % OS_XMLNS_BASE - actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -470,7 +469,7 @@ class VersionsTest(test.TestCase): - """.replace(" ", "").replace("\n","") % (OS_XMLNS_BASE, + """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, ATOM_XMLNS) def test_multi_choice_server(self): @@ -586,7 +585,7 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") % ( - OS_XMLNS_BASE,ATOM_XMLNS) + OS_XMLNS_BASE, ATOM_XMLNS) serializer = versions.VersionsXMLSerializer() response = serializer.index(versions_data) @@ -624,7 +623,6 @@ class VersionsTest(test.TestCase): response = response.replace(" ", "").replace("\n", "") self.assertEqual(expected, response) - def test_version_detail_xml_serializer(self): version_data = { "version" : { -- cgit From c5c3a5696d11320e7fe0bfbe942610e93fbd1ab4 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 09:30:15 -0400 Subject: updated serializer tests for multi choice --- nova/tests/api/openstack/test_versions.py | 71 +++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index b2fb0efb6..9460e1004 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -29,6 +29,7 @@ from nova.api.openstack import views ATOM_XMLNS = versions.ATOM_XMLNS OS_XMLNS_BASE = versions.OS_XMLNS_BASE +VERSIONS = versions.VERSIONS class VersionsTest(test.TestCase): @@ -578,19 +579,26 @@ class VersionsTest(test.TestCase): ] } - expected = """ - - - - - """.replace(" ", "").replace("\n", "") % ( - OS_XMLNS_BASE, ATOM_XMLNS) - serializer = versions.VersionsXMLSerializer() response = serializer.index(versions_data) - response = response.replace(" ", "").replace("\n", "") - self.assertEqual(expected, response) + + root = xml.etree.ElementTree.XML(response) + self.assertEqual(root.tag.split('}')[1], "versions") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "%sv1.0" % OS_XMLNS_BASE) + version = list(root)[0] + self.assertEqual(version.tag.split('}')[1], "version") + self.assertEqual(version.get('id'), + versions_data['versions'][0]['id']) + self.assertEqual(version.get('status'), + versions_data['versions'][0]['status']) + + link = list(version)[0] + + self.assertEqual(link.tag.split('}')[1], "link") + self.assertEqual(link.tag.split('}')[0].strip('{'), ATOM_XMLNS) + for key, val in versions_data['versions'][0]['links'][0].items(): + self.assertEqual(link.get(key), val) def test_versions_multi_xml_serializer(self): versions_data = { @@ -599,6 +607,7 @@ class VersionsTest(test.TestCase): "id": "2.7.1", "updated": "2011-07-18T11:30:00Z", "status": "DEPRECATED", + "media-types": VERSIONS['v1.1']['version']['media-types'], "links": [ { "rel": "self", @@ -609,19 +618,37 @@ class VersionsTest(test.TestCase): ] } - expected = """ - - - - - """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, - ATOM_XMLNS) - serializer = versions.VersionsXMLSerializer() response = serializer.multi(versions_data) - response = response.replace(" ", "").replace("\n", "") - self.assertEqual(expected, response) + + root = xml.etree.ElementTree.XML(response) + self.assertEqual(root.tag.split('}')[1], "choices") + self.assertEqual(root.tag.split('}')[0].strip('{'), + "%sv1.0" % OS_XMLNS_BASE) + version = list(root)[0] + self.assertEqual(version.tag.split('}')[1], "version") + self.assertEqual(version.get('id'), versions_data['choices'][0]['id']) + self.assertEqual(version.get('status'), + versions_data['choices'][0]['status']) + + media_types = list(version)[0] + media_type_nodes = list(media_types) + self.assertEqual(media_types.tag.split('}')[1], "media-types") + + set_types = versions_data['choices'][0]['media-types'] + for i, type in enumerate(set_types): + node = media_type_nodes[i] + self.assertEqual(node.tag.split('}')[1], "media-type") + for key, val in set_types[i].items(): + self.assertEqual(node.get(key), val) + + link = list(version)[1] + + self.assertEqual(link.tag.split('}')[1], "link") + self.assertEqual(link.tag.split('}')[0].strip('{'), ATOM_XMLNS) + for key, val in versions_data['choices'][0]['links'][0].items(): + self.assertEqual(link.get(key), val) + def test_version_detail_xml_serializer(self): version_data = { -- cgit From d5b76d89d7cfc2581e2de618d33331f9267126d4 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 27 Jul 2011 09:33:20 -0400 Subject: updating tests --- nova/tests/api/openstack/test_server_metadata.py | 55 ++++++++---------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index 0431e68d2..12fc7665b 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -94,8 +94,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_index(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -104,16 +103,14 @@ class ServerMetaDataTest(unittest.TestCase): def test_index_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_index_no_data(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_empty_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -123,8 +120,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_show(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key5') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key5') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -133,32 +129,28 @@ class ServerMetaDataTest(unittest.TestCase): def test_show_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/meta/key5') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key5') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_show_meta_not_found(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_empty_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key6') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key6') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_delete(self): self.stubs.Set(nova.db.api, 'instance_metadata_delete', delete_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key5') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key5') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) def test_delete_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/meta/key5') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key5') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -166,8 +158,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_create(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' req.body = '{"metadata": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -180,8 +171,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_create_empty_body(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -189,8 +179,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_create_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/100/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/100/metadata') req.method = 'POST' req.body = '{"metadata": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -200,8 +189,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"key1": "value1"}' req.headers["content-type"] = "application/json" @@ -213,8 +201,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/asdf/100/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1') req.method = 'PUT' req.body = '{"key1": "value1"}' req.headers["content-type"] = "application/json" @@ -224,9 +211,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_empty_body(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key1') - req.environ['api.version'] = '1.1' - req.method = 'PUT' + req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) @@ -234,8 +219,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_too_many_keys(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"key1": "value1", "key2": "value2"}' req.headers["content-type"] = "application/json" @@ -245,8 +229,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_body_uri_mismatch(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/meta/bad') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/bad') req.method = 'PUT' req.body = '{"key1": "value1"}' req.headers["content-type"] = "application/json" @@ -260,8 +243,7 @@ class ServerMetaDataTest(unittest.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/servers/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -271,8 +253,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_to_many_metadata_items_on_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata_max) - req = webob.Request.blank('/v1.1/servers/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"a new key": "a new value"}' req.headers["content-type"] = "application/json" -- cgit From 0760948609ac89a43a590b36e79d691a6c79b4c3 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 27 Jul 2011 11:32:18 -0400 Subject: updating servers metadata resource --- nova/api/openstack/__init__.py | 11 +- nova/api/openstack/common.py | 4 +- nova/api/openstack/server_metadata.py | 113 ++++++++---- nova/tests/api/openstack/test_server_metadata.py | 220 ++++++++++++++++++++--- 4 files changed, 279 insertions(+), 69 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 1d14474a8..6585f1751 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -164,7 +164,9 @@ class APIRouterV11(APIRouter): def _setup_routes(self, mapper): super(APIRouterV11, self)._setup_routes(mapper, '1.1') + image_metadata_controller = image_metadata.create_resource() + mapper.resource("image_meta", "metadata", controller=image_metadata_controller, parent_resource=dict(member_name='image', @@ -175,7 +177,14 @@ class APIRouterV11(APIRouter): action='update_all', conditions={"method": ['PUT']}) + server_metadata_controller = server_metadata.create_resource() + mapper.resource("server_meta", "metadata", - controller=server_metadata.create_resource(), + controller=server_metadata_controller, parent_resource=dict(member_name='server', collection_name='servers')) + + mapper.connect("metadata", "/servers/{server_id}/metadata", + controller=server_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index efa4ab385..24c82035a 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -247,7 +247,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer): container_node = self.meta_list_to_xml(xml_doc, items) xml_doc.appendChild(container_node) self._add_xmlns(container_node) - return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') + return xml_doc.toxml('UTF-8') def index(self, metadata_dict): return self._meta_list_to_xml_string(metadata_dict) @@ -264,7 +264,7 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer): item_node = self._meta_item_to_xml(xml_doc, item_key, item_value) xml_doc.appendChild(item_node) self._add_xmlns(item_node) - return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') + return xml_doc.toxml('UTF-8') def show(self, meta_item_dict): return self._meta_item_to_xml_string(meta_item_dict['meta']) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index d4f42bbf5..d8f9cdbac 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -18,6 +18,7 @@ from webob import exc from nova import compute +from nova.api.openstack import common from nova.api.openstack import wsgi from nova import exception from nova import quota @@ -31,36 +32,37 @@ class Controller(object): super(Controller, self).__init__() def _get_metadata(self, context, server_id): - metadata = self.compute_api.get_instance_metadata(context, server_id) + try: + meta = self.compute_api.get_instance_metadata(context, server_id) + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + meta_dict = {} - for key, value in metadata.iteritems(): + for key, value in meta.iteritems(): meta_dict[key] = value - return dict(metadata=meta_dict) - - def _check_body(self, body): - if body == None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) + return meta_dict def index(self, req, server_id): """ Returns the list of metadata for a given instance """ context = req.environ['nova.context'] - try: - return self._get_metadata(context, server_id) - except exception.InstanceNotFound: - msg = _('Server %(server_id)s does not exist') % locals() - raise exc.HTTPNotFound(explanation=msg) + return {'metadata': self._get_metadata(context, server_id)} def create(self, req, server_id, body): - self._check_body(body) + try: + metadata = body['metadata'] + except (KeyError, TypeError): + msg = _("Malformed request body") + raise exc.HTTPBadRequest(esplanation=msg) + context = req.environ['nova.context'] - metadata = body.get('metadata') + try: self.compute_api.update_or_create_instance_metadata(context, server_id, metadata) except exception.InstanceNotFound: - msg = _('Server %(server_id)s does not exist') % locals() + msg = _('Server does not exist') raise exc.HTTPNotFound(explanation=msg) except quota.QuotaError as error: @@ -69,51 +71,80 @@ class Controller(object): return body def update(self, req, server_id, id, body): - self._check_body(body) - context = req.environ['nova.context'] - if not id in body: + try: + meta_item = body['meta'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + try: + meta_value = meta_item.pop(id) + except (AttributeError, KeyError): expl = _('Request body and URI mismatch') raise exc.HTTPBadRequest(explanation=expl) - if len(body) > 1: + + if len(meta_item) > 0: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + self._set_instance_metadata(context, server_id, meta_item) + + return {'meta': {id: meta_value}} + + def update_all(self, req, server_id, body): + try: + metadata = body['metadata'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + self._set_instance_metadata(context, server_id, metadata) + + return {'metadata': metadata} + + def _set_instance_metadata(self, context, server_id, metadata): try: self.compute_api.update_or_create_instance_metadata(context, server_id, - body) + metadata) except exception.InstanceNotFound: - msg = _('Server %(server_id)s does not exist') % locals() + msg = _('Server does not exist') raise exc.HTTPNotFound(explanation=msg) + except ValueError: + msg = _("Malformed request body") + raise exc.HTTPBadRequest(explanation=msg) + except quota.QuotaError as error: self._handle_quota_error(error) - return body - def show(self, req, server_id, id): """ Return a single metadata item """ context = req.environ['nova.context'] - try: - data = self._get_metadata(context, server_id) - except exception.InstanceNotFound: - msg = _('Server %(server_id)s does not exist') % locals() - raise exc.HTTPNotFound(explanation=msg) + data = self._get_metadata(context, server_id) try: - return {id: data['metadata'][id]} + return {'meta': {id: data[id]}} except KeyError: - msg = _("metadata item %s was not found" % (id)) + msg = _("Metadata item was not found") raise exc.HTTPNotFound(explanation=msg) def delete(self, req, server_id, id): """ Deletes an existing metadata """ context = req.environ['nova.context'] + + metadata = self._get_metadata(context, server_id) + try: - self.compute_api.delete_instance_metadata(context, server_id, id) - except exception.InstanceNotFound: - msg = _('Server %(server_id)s does not exist') % locals() + meta_key = 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) + def _handle_quota_error(self, error): """Reraise quota errors as api-specific http exceptions.""" if error.code == "MetadataLimitExceeded": @@ -122,10 +153,16 @@ class Controller(object): def create_resource(): - body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11), + headers_serializer = common.MetadataHeadersSerializer() + + body_deserializers = { + 'application/xml': common.MetadataXMLDeserializer(), } - serializer = wsgi.ResponseSerializer(body_serializers) + body_serializers = { + 'application/xml': common.MetadataXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(Controller(), serializer=serializer) + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index 12fc7665b..ded30a950 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -19,7 +19,7 @@ import json import stubout import unittest import webob - +from xml.dom import minidom from nova import exception from nova import flags @@ -53,11 +53,10 @@ def delete_server_metadata(context, server_id, key): def stub_server_metadata(): metadata = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} + "key1": "value1", + "key2": "value2", + "key3": "value3", + } return metadata @@ -96,10 +95,38 @@ class ServerMetaDataTest(unittest.TestCase): return_server_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) + res_dict = json.loads(res.body) self.assertEqual('application/json', res.headers['Content-Type']) - self.assertEqual('value1', res_dict['metadata']['key1']) + expected = { + 'metadata': { + 'key1':'value1', + 'key2':'value2', + 'key3':'value3', + }, + } + self.assertEqual(expected, res_dict) + + def test_index_xml(self): + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_server_metadata) + request = webob.Request.blank("/v1.1/servers/1/metadata") + request.accept = "application/xml" + response = request.get_response(fakes.wsgi_app()) + self.assertEqual(200, response.status_int) + self.assertEqual("application/xml", response.content_type) + + actual_metadata = minidom.parseString(response.body.replace(" ", "")) + + expected_metadata = minidom.parseString(""" + + value3 + value2 + value1 + + """.replace(" ", "").replace("\n","")) + + self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml()) def test_index_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) @@ -112,24 +139,42 @@ class ServerMetaDataTest(unittest.TestCase): return_empty_server_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) - self.assertEqual('application/json', res.headers['Content-Type']) - self.assertEqual(0, len(res_dict['metadata'])) + res_dict = json.loads(res.body) + expected = {'metadata': {}} + self.assertEqual(expected, res_dict) def test_show(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key5') + req = webob.Request.blank('/v1.1/servers/1/metadata/key2') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) - self.assertEqual('application/json', res.headers['Content-Type']) - self.assertEqual('value5', res_dict['key5']) + expected = {'meta': {'key2': 'value2'}} + self.assertEqual(expected, res_dict) + + def test_show_xml(self): + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_server_metadata) + request = webob.Request.blank("/v1.1/servers/1/metadata/key2") + request.accept = "application/xml" + response = request.get_response(fakes.wsgi_app()) + self.assertEqual(200, response.status_int) + self.assertEqual("application/xml", response.content_type) + + actual_metadata = minidom.parseString(response.body.replace(" ", "")) + + expected_metadata = minidom.parseString(""" + value2 + """.replace(" ", "").replace("\n","")) + + self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml()) def test_show_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/metadata/key5') + req = webob.Request.blank('/v1.1/servers/1/metadata/key2') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -141,16 +186,27 @@ class ServerMetaDataTest(unittest.TestCase): self.assertEqual(404, res.status_int) def test_delete(self): + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_server_metadata) self.stubs.Set(nova.db.api, 'instance_metadata_delete', delete_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key5') + req = webob.Request.blank('/v1.1/servers/1/metadata/key2') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) - self.assertEqual(200, res.status_int) + self.assertEqual(204, res.status_int) + self.assertEqual('', res.body) def test_delete_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/metadata/key5') + req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req.method = 'DELETE' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_delete_meta_not_found(self): + self.stubs.Set(nova.db.api, 'instance_metadata_get', + return_empty_server_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata/key6') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -160,13 +216,38 @@ class ServerMetaDataTest(unittest.TestCase): return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata') req.method = 'POST' - req.body = '{"metadata": {"key1": "value1"}}' - req.headers["content-type"] = "application/json" + req.content_type = "application/json" + expected = {"metadata": {"key1": "value1"}} + req.body = json.dumps(expected) res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) - self.assertEqual('application/json', res.headers['Content-Type']) - self.assertEqual('value1', res_dict['metadata']['key1']) + self.assertEqual(expected, res_dict) + + def test_create_xml(self): + self.stubs.Set(nova.db.api, "instance_metadata_update_or_create", + return_create_instance_metadata) + req = webob.Request.blank("/v1.1/servers/1/metadata") + req.method = "POST" + req.content_type = "application/xml" + req.accept = "application/xml" + + request_metadata = minidom.parseString(""" + + value3 + value2 + value1 + + """.replace(" ","").replace("\n","")) + + req.body = str(request_metadata.toxml()) + response = req.get_response(fakes.wsgi_app()) + + self.assertEqual(200, response.status_int) + actual_metadata = minidom.parseString(response.body) + + 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', @@ -186,24 +267,105 @@ class ServerMetaDataTest(unittest.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) + def test_update_all(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata') + req.method = 'PUT' + req.content_type = "application/json" + expected = { + 'metadata': { + 'key10': 'value10', + 'key99': 'value99', + }, + } + req.body = json.dumps(expected) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) + res_dict = json.loads(res.body) + self.assertEqual(expected, res_dict) + + def test_update_all_empty_container(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata') + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': {}} + req.body = json.dumps(expected) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) + res_dict = json.loads(res.body) + self.assertEqual(expected, res_dict) + + def test_update_all_malformed_container(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata') + req.method = 'PUT' + req.content_type = "application/json" + expected = {'meta': {}} + req.body = json.dumps(expected) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, res.status_int) + + def test_update_all_malformed_data(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata') + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': ['asdf']} + req.body = json.dumps(expected) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, res.status_int) + + def test_update_all_nonexistant_server(self): + self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) + req = webob.Request.blank('/v1.1/servers/100/metadata') + req.method = 'PUT' + req.content_type = "application/json" + req.body = json.dumps({'metadata': {'key10': 'value10'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + def test_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' - req.body = '{"key1": "value1"}' + req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) self.assertEqual('application/json', res.headers['Content-Type']) res_dict = json.loads(res.body) - self.assertEqual('value1', res_dict['key1']) + expected = {'meta': {'key1': 'value1'}} + self.assertEqual(expected, res_dict) + + def test_update_item_xml(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + req = webob.Request.blank('/v1.1/servers/1/metadata/key9') + req.method = 'PUT' + req.accept = "application/json" + req.content_type = "application/xml" + req.body = """ + value9 + """ + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) + self.assertEqual('application/json', res.headers['Content-Type']) + res_dict = json.loads(res.body) + expected = {'meta': {'key9': 'value9'}} + self.assertEqual(expected, res_dict) def test_update_item_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1') req.method = 'PUT' - req.body = '{"key1": "value1"}' + req.body = '{"meta":{"key1": "value1"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -212,6 +374,7 @@ class ServerMetaDataTest(unittest.TestCase): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req.method = 'PUT' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) @@ -221,7 +384,7 @@ class ServerMetaDataTest(unittest.TestCase): return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' - req.body = '{"key1": "value1", "key2": "value2"}' + req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) @@ -231,7 +394,7 @@ class ServerMetaDataTest(unittest.TestCase): return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/metadata/bad') req.method = 'PUT' - req.body = '{"key1": "value1"}' + req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) @@ -255,7 +418,8 @@ class ServerMetaDataTest(unittest.TestCase): return_create_instance_metadata_max) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') req.method = 'PUT' - req.body = '{"a new key": "a new value"}' + req.body = '{"meta": {"a new key": "a new value"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) + -- cgit From 4add644dbd1650a3e83e715c6d6a38c4114e4d06 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 27 Jul 2011 11:39:34 -0400 Subject: Moved server/actions tests to test_server_actions.py --- nova/tests/api/openstack/test_server_actions.py | 628 ++++++++++++++++++++++++ nova/tests/api/openstack/test_servers.py | 492 ------------------- 2 files changed, 628 insertions(+), 492 deletions(-) create mode 100644 nova/tests/api/openstack/test_server_actions.py diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py new file mode 100644 index 000000000..97c108068 --- /dev/null +++ b/nova/tests/api/openstack/test_server_actions.py @@ -0,0 +1,628 @@ +import base64 +import json +import unittest +from xml.dom import minidom + +import stubout +import webob + +from nova import context +from nova import db +from nova import utils +from nova.api.openstack import create_instance_helper +from nova.compute import instance_types +from nova.compute import power_state +import nova.db.api +from nova import test +from nova.tests.api.openstack import common +from nova.tests.api.openstack import fakes + + +def return_server_by_id(context, id): + return _get_instance() + + +def instance_update(context, instance_id, kwargs): + return _get_instance() + + +def return_server_with_power_state(power_state): + def _return_server(context, id): + instance = _get_instance() + instance['state'] = power_state + return instance + return _return_server + + +def return_server_with_uuid_and_power_state(power_state): + def _return_server(context, id): + return return_server_with_power_state(power_state) + return _return_server + + +class MockSetAdminPassword(object): + def __init__(self): + self.instance_id = None + self.password = None + + def __call__(self, context, instance_id, password): + self.instance_id = instance_id + self.password = password + + +def _get_instance(): + instance = { + "id": 1, + "created_at": "2010-10-10T12:00:00Z", + "updated_at": "2010-11-11T11:00:00Z", + "admin_pass": "", + "user_id": "", + "project_id": "", + "image_ref": "5", + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": "", + "key_data": "", + "state": 0, + "state_description": "", + "memory_mb": 0, + "vcpus": 0, + "local_gb": 0, + "hostname": "", + "host": "", + "instance_type": { + "flavorid": 1, + }, + "user_data": "", + "reservation_id": "", + "mac_address": "", + "scheduled_at": utils.utcnow(), + "launched_at": utils.utcnow(), + "terminated_at": utils.utcnow(), + "availability_zone": "", + "display_name": "test_server", + "display_description": "", + "locked": False, + "metadata": [], + #"address": , + #"floating_ips": [{"address":ip} for ip in public_addresses]} + "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"} + + return instance + + +class ServerActionsTest(test.TestCase): + + def setUp(self): + self.maxDiff = None + super(ServerActionsTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_auth(self.stubs) + self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) + self.stubs.Set(nova.db.api, 'instance_update', instance_update) + + self.webreq = common.webob_factory('/v1.0/servers') + + def tearDown(self): + self.stubs.UnsetAll() + + def test_server_actions(self): + req = webob.Request.blank("/v1.0/servers/1/actions") + req.method = "GET" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) + + def test_server_change_password(self): + body = {'changePassword': {'adminPass': '1234pass'}} + req = webob.Request.blank('/v1.0/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, 501) + + def test_server_change_password_xml(self): + req = webob.Request.blank('/v1.0/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, 501) + + def test_server_change_password_not_a_string_v1_1(self): + body = {'changePassword': {'adminPass': 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_reboot(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality={})) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + + def test_server_rebuild_accepted(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + req = webob.Request.blank('/v1.0/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) + self.assertEqual(res.body, "") + + def test_server_rebuild_rejected_when_building(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_with_uuid_and_power_state(state)) + + req = webob.Request.blank('/v1.0/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, 409) + + def test_server_rebuild_bad_entity(self): + body = { + "rebuild": { + }, + } + + req = webob.Request.blank('/v1.0/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_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_bad_flavor_fails(self): + req = self.webreq('/1/action', 'POST', dict(resize=dict(derp=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, 422) + self.assertEqual(self.resize_called, False) + + def test_resize_raises_fails(self): + req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) + + def resize_mock(*args): + raise Exception('hurr durr') + + 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()) + self.assertEqual(res.status_int, 200) + 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('hurr durr') + + 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('hurr durr') + + 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/action', 'POST', dict(migrate=None)) + + 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) + + +class ServerActionsTestV11(test.TestCase): + + def setUp(self): + self.maxDiff = None + super(ServerActionsTestV11, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_auth(self.stubs) + self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) + self.stubs.Set(nova.db.api, 'instance_update', instance_update) + + fakes.stub_out_glance(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) + service_class = 'nova.image.glance.GlanceImageService' + self.service = utils.import_object(service_class) + self.context = context.RequestContext(1, None) + self.service.delete_all() + self.sent_to_glance = {} + fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) + + def tearDown(self): + self.stubs.UnsetAll() + + def test_server_change_password_v1_1(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': '1234pass'}} + 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) + self.assertEqual(mock_method.instance_id, '1') + self.assertEqual(mock_method.password, '1234pass') + + def test_server_change_password_bad_request_v1_1(self): + body = {'changePassword': {'pass': '12345'}} + 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_empty_string_v1_1(self): + body = {'changePassword': {'adminPass': ''}} + 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_none_v1_1(self): + body = {'changePassword': {'adminPass': None}} + 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_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + 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_rebuild_rejected_when_building_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_with_uuid_and_power_state(state)) + + 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, 409) + + def test_server_rebuild_accepted_with_metadata_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": { + "new": "metadata", + }, + }, + } + + 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_rebuild_accepted_with_bad_metadata_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": "stack", + }, + } + + 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_bad_entity_v1_1(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + 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_bad_personality_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + 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_personality_v1_1(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + 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_resize_server_v11(self): + + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(resize=dict(flavorRef="http://localhost/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_create_image_v1_1(self): + 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(202, response.status_int) + location = response.headers['Location'] + self.assertEqual('http://localhost/v1.1/images/123', location) + + def test_create_image_v1_1_with_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {'key': 'asdf'}, + }, + } + 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(202, response.status_int) + location = response.headers['Location'] + self.assertEqual('http://localhost/v1.1/images/123', location) + + def test_create_image_v1_1_no_name(self): + body = { + 'createImage': {}, + } + 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_v1_1_bad_metadata(self): + body = { + 'createImage': { + 'name': 'geoff', + 'metadata': 'henry', + }, + } + 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) + + +class TestServerActionXMLDeserializer(test.TestCase): + + def setUp(self): + self.deserializer = create_instance_helper.ServerXMLDeserializer() + + def tearDown(self): + pass + + def test_create_image(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + "metadata": {}, + }, + } + self.assertEquals(request['body'], expected) + + def test_create_image_with_metadata(self): + serial_request = """ + + + value1 + +""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + "metadata": {"key1": "value1"}, + }, + } + self.assertEquals(request['body'], expected) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 9d2c7b73f..b68183429 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1766,262 +1766,6 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) - def test_server_actions(self): - req = webob.Request.blank("/v1.0/servers/1/actions") - req.method = "GET" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - - def test_server_change_password(self): - body = {'changePassword': {'adminPass': '1234pass'}} - req = webob.Request.blank('/v1.0/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, 501) - - def test_server_change_password_xml(self): - req = webob.Request.blank('/v1.0/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, 501) - - def test_server_change_password_v1_1(self): - mock_method = MockSetAdminPassword() - self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) - body = {'changePassword': {'adminPass': '1234pass'}} - 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) - self.assertEqual(mock_method.instance_id, '1') - self.assertEqual(mock_method.password, '1234pass') - - def test_server_change_password_bad_request_v1_1(self): - body = {'changePassword': {'pass': '12345'}} - 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_empty_string_v1_1(self): - body = {'changePassword': {'adminPass': ''}} - 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_none_v1_1(self): - body = {'changePassword': {'adminPass': None}} - 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_not_a_string_v1_1(self): - body = {'changePassword': {'adminPass': 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_reboot(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality={})) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type = 'application/json' - req.body = json.dumps(body) - res = req.get_response(fakes.wsgi_app()) - - def test_server_rebuild_accepted(self): - body = { - "rebuild": { - "imageId": 2, - }, - } - - req = webob.Request.blank('/v1.0/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) - self.assertEqual(res.body, "") - - def test_server_rebuild_rejected_when_building(self): - body = { - "rebuild": { - "imageId": 2, - }, - } - - state = power_state.BUILDING - new_return_server = return_server_with_power_state(state) - self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - self.stubs.Set(nova.db, 'instance_get_by_uuid', - return_server_with_uuid_and_power_state(state)) - - req = webob.Request.blank('/v1.0/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, 409) - - def test_server_rebuild_bad_entity(self): - body = { - "rebuild": { - }, - } - - req = webob.Request.blank('/v1.0/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_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - 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_rebuild_rejected_when_building_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - state = power_state.BUILDING - new_return_server = return_server_with_power_state(state) - self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - self.stubs.Set(nova.db, 'instance_get_by_uuid', - return_server_with_uuid_and_power_state(state)) - - 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, 409) - - def test_server_rebuild_accepted_with_metadata_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "metadata": { - "new": "metadata", - }, - }, - } - - 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_rebuild_accepted_with_bad_metadata_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "metadata": "stack", - }, - } - - 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_bad_entity_v1_1(self): - body = { - "rebuild": { - "imageId": 2, - }, - } - - 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_bad_personality_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "personality": [{ - "path": "/path/to/file", - "contents": "INVALID b64", - }] - }, - } - - 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_personality_v1_1(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "personality": [{ - "path": "/path/to/file", - "contents": base64.b64encode("Test String"), - }] - }, - } - - 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_delete_server_instance(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'DELETE' @@ -2089,147 +1833,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_v11(self): - - req = webob.Request.blank('/v1.1/servers/1/action') - req.content_type = 'application/json' - req.method = 'POST' - body_dict = dict(resize=dict(flavorRef="http://localhost/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_fails(self): - req = self.webreq('/1/action', 'POST', dict(resize=dict(derp=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, 422) - self.assertEqual(self.resize_called, False) - - def test_resize_raises_fails(self): - req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) - - def resize_mock(*args): - raise Exception('hurr durr') - - 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('hurr durr') - - 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('hurr durr') - - 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/action', 'POST', dict(migrate=None)) - - 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_shutdown_status(self): new_server = return_server_with_power_state(power_state.SHUTDOWN) self.stubs.Set(nova.db.api, 'instance_get', new_server) @@ -2248,101 +1851,6 @@ class ServersTest(test.TestCase): res_dict = json.loads(res.body) self.assertEqual(res_dict['server']['status'], 'SHUTOFF') - def test_create_image_v1_1(self): - 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(202, response.status_int) - location = response.headers['Location'] - self.assertEqual('http://localhost/v1.1/images/123', location) - - def test_create_image_v1_1_with_metadata(self): - body = { - 'createImage': { - 'name': 'Snapshot 1', - 'metadata': {'key': 'asdf'}, - }, - } - 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(202, response.status_int) - location = response.headers['Location'] - self.assertEqual('http://localhost/v1.1/images/123', location) - - def test_create_image_v1_1_no_name(self): - body = { - 'createImage': {}, - } - 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_v1_1_bad_metadata(self): - body = { - 'createImage': { - 'name': 'geoff', - 'metadata': 'henry', - }, - } - 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) - - -class TestServerActionXMLDeserializer(test.TestCase): - - def setUp(self): - self.deserializer = create_instance_helper.ServerXMLDeserializer() - - def tearDown(self): - pass - - def test_create_image(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "createImage": { - "name": "new-server-test", - "metadata": {}, - }, - } - self.assertEquals(request['body'], expected) - - def test_create_image_with_metadata(self): - serial_request = """ - - - value1 - -""" - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "createImage": { - "name": "new-server-test", - "metadata": {"key1": "value1"}, - }, - } - self.assertEquals(request['body'], expected) - class TestServerCreateRequestXMLDeserializerV10(unittest.TestCase): -- cgit From 154129acf1ecbdd97e5bb8558598a9b24eb8559f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 27 Jul 2011 11:43:20 -0400 Subject: Cleaned up test_servers --- nova/tests/api/openstack/test_servers.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b68183429..c33d6b367 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -97,12 +97,6 @@ def return_server_with_power_state(power_state): return _return_server -def return_server_with_uuid_and_power_state(power_state): - def _return_server(context, id): - return stub_instance(id, uuid=FAKE_UUID, power_state=power_state) - return _return_server - - def return_servers(context, user_id=1): return [stub_instance(i, user_id) for i in xrange(5)] @@ -264,19 +258,8 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api) self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api) - fakes.stub_out_glance(self.stubs) - fakes.stub_out_compute_api_snapshot(self.stubs) - service_class = 'nova.image.glance.GlanceImageService' - self.service = utils.import_object(service_class) - self.context = context.RequestContext(1, None) - self.service.delete_all() - self.sent_to_glance = {} - fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) - self.allow_admin = FLAGS.allow_admin_api - self.webreq = common.webob_factory('/v1.0/servers') - def tearDown(self): self.stubs.UnsetAll() FLAGS.allow_admin_api = self.allow_admin -- cgit From b6ee05e9575769039aca2c65c2a761c14562e7e0 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 27 Jul 2011 11:45:22 -0400 Subject: updating common metadata xml serializer tests --- nova/tests/api/openstack/test_common.py | 60 +++++++++++---------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 0b76841f0..ee96fea7e 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -323,14 +323,10 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" - - four - - - two - + four + two - """.replace(" ", "")) + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -346,11 +342,9 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" - - None - + None - """.replace(" ", "")) + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -366,11 +360,9 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(u""" - - Jos\xe9 - + Jos\xe9 - """.encode("UTF-8").replace(" ", "")) + """.encode("UTF-8").replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -385,10 +377,9 @@ class MetadataXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) expected = minidom.parseString(""" - - two - - """.replace(" ", "")) + two + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -405,14 +396,10 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" - - value6 - - - value4 - + value6 + value4 - """.replace(" ", "")) + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -427,10 +414,9 @@ class MetadataXMLSerializationTest(test.TestCase): actual = minidom.parseString(output.replace(" ", "")) expected = minidom.parseString(""" - - two - - """.replace(" ", "")) + two + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -448,17 +434,11 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" - - value2 - - - value9 - - - value1 - + value2 + value9 + value1 - """.replace(" ", "")) + """.replace(" ", "").replace("\n","")) self.assertEqual(expected.toxml(), actual.toxml()) -- cgit From 572847f9eb43ce23190566439118547ae6d6a992 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 27 Jul 2011 11:53:09 -0400 Subject: pep8 --- nova/api/openstack/server_metadata.py | 6 +++--- nova/tests/api/openstack/test_common.py | 14 +++++++------- nova/tests/api/openstack/test_server_metadata.py | 13 ++++++------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index d8f9cdbac..f25d36535 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -72,13 +72,13 @@ class Controller(object): def update(self, req, server_id, id, body): try: - meta_item = body['meta'] + meta_item = body['meta'] except (TypeError, KeyError): expl = _('Malformed request body') raise exc.HTTPBadRequest(explanation=expl) try: - meta_value = meta_item.pop(id) + meta_value = meta_item.pop(id) except (AttributeError, KeyError): expl = _('Request body and URI mismatch') raise exc.HTTPBadRequest(explanation=expl) @@ -94,7 +94,7 @@ class Controller(object): def update_all(self, req, server_id, body): try: - metadata = body['metadata'] + metadata = body['metadata'] except (TypeError, KeyError): expl = _('Malformed request body') raise exc.HTTPBadRequest(explanation=expl) diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index ee96fea7e..5a6e43579 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -326,7 +326,7 @@ class MetadataXMLSerializationTest(test.TestCase): four two - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -344,7 +344,7 @@ class MetadataXMLSerializationTest(test.TestCase): None - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -362,7 +362,7 @@ class MetadataXMLSerializationTest(test.TestCase): Jos\xe9 - """.encode("UTF-8").replace(" ", "").replace("\n","")) + """.encode("UTF-8").replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -379,7 +379,7 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" two - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -399,7 +399,7 @@ class MetadataXMLSerializationTest(test.TestCase): value6 value4 - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -416,7 +416,7 @@ class MetadataXMLSerializationTest(test.TestCase): expected = minidom.parseString(""" two - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) @@ -438,7 +438,7 @@ class MetadataXMLSerializationTest(test.TestCase): value9 value1 - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index ded30a950..2f432433d 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -100,9 +100,9 @@ class ServerMetaDataTest(unittest.TestCase): self.assertEqual('application/json', res.headers['Content-Type']) expected = { 'metadata': { - 'key1':'value1', - 'key2':'value2', - 'key3':'value3', + 'key1': 'value1', + 'key2': 'value2', + 'key3': 'value3', }, } self.assertEqual(expected, res_dict) @@ -124,7 +124,7 @@ class ServerMetaDataTest(unittest.TestCase): value2 value1 - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml()) @@ -168,7 +168,7 @@ class ServerMetaDataTest(unittest.TestCase): expected_metadata = minidom.parseString(""" value2 - """.replace(" ", "").replace("\n","")) + """.replace(" ", "").replace("\n", "")) self.assertEqual(expected_metadata.toxml(), actual_metadata.toxml()) @@ -239,7 +239,7 @@ class ServerMetaDataTest(unittest.TestCase): value2 value1 - """.replace(" ","").replace("\n","")) + """.replace(" ", "").replace("\n", "")) req.body = str(request_metadata.toxml()) response = req.get_response(fakes.wsgi_app()) @@ -422,4 +422,3 @@ class ServerMetaDataTest(unittest.TestCase): req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) - -- cgit From 6dead0b1706f3b2279504437aca63a3291dc2347 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 12:06:12 -0400 Subject: pep8 --- nova/tests/api/openstack/test_versions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 9460e1004..cbc98a4b4 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -649,7 +649,6 @@ class VersionsTest(test.TestCase): for key, val in versions_data['choices'][0]['links'][0].items(): self.assertEqual(link.get(key), val) - def test_version_detail_xml_serializer(self): version_data = { "version" : { -- cgit From 5ca4d3a88f1dd758c4ab6133e26cf2f8b05a8339 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 12:50:52 -0400 Subject: pep8 --- nova/api/openstack/versions.py | 20 ++++---- nova/tests/api/openstack/test_versions.py | 84 +++++++++++++++---------------- run_tests.sh | 4 +- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 9909d8d0d..4d40274b3 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -28,7 +28,7 @@ ATOM_XMLNS = "http://www.w3.org/2005/Atom" OS_XMLNS_BASE = "http://docs.openstack.org/common/api/" VERSIONS = { "v1.0": { - "version" : { + "version": { "id": "v1.0", "status": "DEPRECATED", "updated": "2011-01-21T11:33:21Z", @@ -52,18 +52,18 @@ VERSIONS = { ], "media-types": [ { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.0+xml" + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" }, { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.0+json" + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" } ], }, }, "v1.1": { - "version" : { + "version": { "id": "v1.1", "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", @@ -87,12 +87,12 @@ VERSIONS = { ], "media-types": [ { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.1+xml" + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" }, { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.1+json" + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" } ], }, diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index cbc98a4b4..3f7867ff4 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -81,37 +81,37 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/json") version = json.loads(res.body) expected = { - "version" : { - "id" : "v1.0", - "status" : "DEPRECATED", - "updated" : "2011-01-21T11:33:21Z", + "version": { + "id": "v1.0", + "status": "DEPRECATED", + "updated": "2011-01-21T11:33:21Z", "links": [ { - "rel" : "self", - "href" : "http://servers.api.openstack.org/v1.0/" + "rel": "self", + "href": "http://servers.api.openstack.org/v1.0/" }, { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.rackspacecloud.com/" + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" "servers/api/v1.0/cs-devguide-20110125.pdf" }, { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://docs.rackspacecloud.com/" + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" "servers/api/v1.0/application.wadl" } ], "media-types": [ { - "base" : "application/xml", - "type" : "application/" + "base": "application/xml", + "type": "application/" "vnd.openstack.compute-v1.0+xml" }, { - "base" : "application/json", - "type" : "application/" + "base": "application/json", + "type": "application/" "vnd.openstack.compute-v1.0+json" } ] @@ -127,37 +127,37 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/json") version = json.loads(res.body) expected = { - "version" : { - "id" : "v1.1", - "status" : "CURRENT", - "updated" : "2011-01-21T11:33:21Z", + "version": { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", "links": [ { - "rel" : "self", - "href" : "http://servers.api.openstack.org/v1.1/" + "rel": "self", + "href": "http://servers.api.openstack.org/v1.1/" }, { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.rackspacecloud.com/" + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" "servers/api/v1.1/cs-devguide-20110125.pdf" }, { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://docs.rackspacecloud.com/" + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" "servers/api/v1.1/application.wadl" } ], "media-types": [ { - "base" : "application/xml", - "type" : "application/" + "base": "application/xml", + "type": "application/" "vnd.openstack.compute-v1.1+xml" }, { - "base" : "application/json", - "type" : "application/" + "base": "application/json", + "type": "application/" "vnd.openstack.compute-v1.1+json" } ] @@ -651,7 +651,7 @@ class VersionsTest(test.TestCase): def test_version_detail_xml_serializer(self): version_data = { - "version" : { + "version": { "id": "v1.0", "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", @@ -675,12 +675,12 @@ class VersionsTest(test.TestCase): ], "media-types": [ { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.0+xml" + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" }, { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.0+json" + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" } ], }, @@ -787,7 +787,7 @@ class VersionsTest(test.TestCase): def test_version_detail_atom_serializer(self): versions_data = { - "version" : { + "version": { "id": "v1.1", "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", @@ -811,12 +811,12 @@ class VersionsTest(test.TestCase): ], "media-types": [ { - "base" : "application/xml", - "type" : "application/vnd.openstack.compute-v1.1+xml" + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" }, { - "base" : "application/json", - "type" : "application/vnd.openstack.compute-v1.1+json" + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" } ], }, diff --git a/run_tests.sh b/run_tests.sh index 8f2b51757..e39ecd315 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -84,7 +84,9 @@ function run_pep8 { srcfiles+=" `find tools/*`" srcfiles+=" nova setup.py plugins/xenserver/xenapi/etc/xapi.d/plugins/glance" # Just run PEP8 in current environment - ${wrapper} pep8 --repeat --show-pep8 --show-source \ + #${wrapper} pep8 --repeat --show-pep8 --show-source \ + #--exclude=vcsversion.py ${srcfiles} + pep8 --repeat --show-pep8 --show-source \ --exclude=vcsversion.py ${srcfiles} } -- cgit From b847ed1cbac345bd2d7a8c252080656c8109c052 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Wed, 27 Jul 2011 17:16:46 +0000 Subject: Add context argument a lot more places and make unit tests work --- nova/tests/test_libvirt.py | 6 +++--- nova/tests/test_xenapi.py | 16 +++++++++------- nova/tests/xenapi/stubs.py | 4 ++-- nova/virt/driver.py | 7 ++++--- nova/virt/fake.py | 5 +++-- nova/virt/hyperv.py | 3 ++- nova/virt/libvirt/connection.py | 5 +++-- nova/virt/vmwareapi/vmops.py | 4 ++-- nova/virt/vmwareapi_conn.py | 9 +++++---- nova/virt/xenapi/vmops.py | 4 ++-- nova/virt/xenapi_conn.py | 13 +++++++------ 11 files changed, 42 insertions(+), 34 deletions(-) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index ad0931a89..c4af38426 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -365,7 +365,7 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = connection.LibvirtConnection(False) - conn.snapshot(instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id']) snapshot = image_service.show(context, recv_meta['id']) self.assertEquals(snapshot['properties']['image_state'], 'available') @@ -405,7 +405,7 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = connection.LibvirtConnection(False) - conn.snapshot(instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id']) snapshot = image_service.show(context, recv_meta['id']) self.assertEquals(snapshot['properties']['image_state'], 'available') @@ -775,7 +775,7 @@ class LibvirtConnTestCase(test.TestCase): network_info = [(network, mapping)] try: - conn.spawn(instance, network_info) + conn.spawn(context.get_admin_context(), instance, network_info) except Exception, e: count = (0 <= str(e.message).find('Unexpected method call')) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 199a8bc52..fd8416424 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -227,7 +227,7 @@ class XenAPIVMTestCase(test.TestCase): 'mac': 'DE:AD:BE:EF:00:00', 'rxtx_cap': 3})] instance = db.instance_create(self.context, values) - self.conn.spawn(instance, network_info) + self.conn.spawn(self.context, instance, network_info) gt1 = eventlet.spawn(_do_build, 1, self.project.id, self.user.id) gt2 = eventlet.spawn(_do_build, 2, self.project.id, self.user.id) @@ -257,14 +257,15 @@ class XenAPIVMTestCase(test.TestCase): instance = self._create_instance() name = "MySnapshot" - self.assertRaises(exception.Error, self.conn.snapshot, instance, name) + self.assertRaises(exception.Error, self.conn.snapshot, + self.context, instance, name) def test_instance_snapshot(self): stubs.stubout_instance_snapshot(self.stubs) instance = self._create_instance() name = "MySnapshot" - template_vm_ref = self.conn.snapshot(instance, name) + template_vm_ref = self.conn.snapshot(self.context, instance, name) def ensure_vm_was_torn_down(): vm_labels = [] @@ -422,7 +423,7 @@ class XenAPIVMTestCase(test.TestCase): 'label': 'fake', 'mac': 'DE:AD:BE:EF:00:00', 'rxtx_cap': 3})] - self.conn.spawn(instance, network_info) + self.conn.spawn(self.context, instance, network_info) self.create_vm_record(self.conn, os_type, instance_id) self.check_vm_record(self.conn, check_injection) self.assertTrue(instance.os_type) @@ -691,7 +692,7 @@ class XenAPIVMTestCase(test.TestCase): 'label': 'fake', 'mac': 'DE:AD:BE:EF:00:00', 'rxtx_cap': 3})] - self.conn.spawn(instance, network_info) + self.conn.spawn(self.context, instance, network_info) return instance @@ -802,8 +803,9 @@ class XenAPIMigrateInstance(test.TestCase): 'label': 'fake', 'mac': 'DE:AD:BE:EF:00:00', 'rxtx_cap': 3})] - conn.finish_resize(instance, dict(base_copy='hurr', cow='durr'), - network_info) + conn.finish_resize(self.context, instance, + dict(base_copy='hurr', cow='durr'), + network_info) class XenAPIDetermineDiskImageTestCase(test.TestCase): diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 66c79d465..3a142081c 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -28,8 +28,8 @@ from nova import utils def stubout_instance_snapshot(stubs): @classmethod - def fake_fetch_image(cls, session, instance_id, image, user, project, - type): + def fake_fetch_image(cls, context, session, instance_id, image, user, + project, type): from nova.virt.xenapi.fake import create_vdi name_label = "instance-%s" % instance_id #TODO: create fake SR record diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 34dc5f544..40cb877ce 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -61,7 +61,8 @@ class ComputeDriver(object): """Return a list of InstanceInfo for all registered VMs""" raise NotImplementedError() - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, context, instance, network_info, + block_device_mapping=None): """Launch a VM for the specified instance""" raise NotImplementedError() @@ -118,11 +119,11 @@ class ComputeDriver(object): off the instance copies over the COW disk""" raise NotImplementedError() - def snapshot(self, instance, image_id): + def snapshot(self, context, instance, image_id): """Create snapshot from a running VM instance.""" raise NotImplementedError() - def finish_resize(self, instance, disk_info): + def finish_resize(self, context, instance, disk_info): """Completes a resize, turning on the migrated instance""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 26bc421c0..f67c2309e 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -129,7 +129,8 @@ 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, context, instance, network_info, + block_device_mapping=None): """ Create a new instance/VM/domain on the virtualization platform. @@ -153,7 +154,7 @@ class FakeConnection(driver.ComputeDriver): fake_instance = FakeInstance(name, state) self.instances[name] = fake_instance - def snapshot(self, instance, name): + def snapshot(self, context, instance, name): """ Snapshots the specified instance. diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 81c7dea58..8236b5a45 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -139,7 +139,8 @@ class HyperVConnection(driver.ComputeDriver): return instance_infos - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, context, instance, network_info, + block_device_mapping=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 96f9c41f9..9a964dfd2 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -399,7 +399,7 @@ class LibvirtConnection(driver.ComputeDriver): virt_dom.detachDevice(xml) @exception.wrap_exception() - def snapshot(self, instance, image_href): + def snapshot(self, context, instance, image_href): """Create snapshot from a running VM instance. This command only works with qemu 0.14+, the qemu_img flag is @@ -595,7 +595,8 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(ilyaalekseyev): Implementation like in multinics # for xenapi(tr3buchet) @exception.wrap_exception() - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, context, instance, network_info, + block_device_mapping=None): xml = self.to_xml(instance, False, network_info=network_info, block_device_mapping=block_device_mapping) block_device_mapping = block_device_mapping or [] diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 7e7d2dac3..5fa92230d 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -89,7 +89,7 @@ class VMWareVMOps(object): LOG.debug(_("Got total of %s instances") % str(len(lst_vm_names))) return lst_vm_names - def spawn(self, instance, network_info): + def spawn(self, context, instance, network_info): """ Creates a VM instance. @@ -329,7 +329,7 @@ class VMWareVMOps(object): LOG.debug(_("Powered on the VM instance %s") % instance.name) _power_on_vm() - def snapshot(self, instance, snapshot_name): + def snapshot(self, context, instance, snapshot_name): """ Create snapshot from a running VM instance. Steps followed are: diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index ce57847b2..3d209fa99 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -124,13 +124,14 @@ class VMWareESXConnection(driver.ComputeDriver): """List VM instances.""" return self._vmops.list_instances() - def spawn(self, instance, network_info, block_device_mapping=None): + def spawn(self, context, instance, network_info, + block_device_mapping=None): """Create VM instance.""" - self._vmops.spawn(instance, network_info) + self._vmops.spawn(context, instance, network_info) - def snapshot(self, instance, name): + def snapshot(self, context, instance, name): """Create snapshot from a running VM instance.""" - self._vmops.snapshot(instance, name) + self._vmops.snapshot(context, instance, name) def reboot(self, instance, network_info): """Reboot VM instance.""" diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 1c6604836..9d73cfb05 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -209,8 +209,8 @@ class VMOps(object): if instance.vm_mode != vm_mode: # Update database with normalized (or determined) value - db.instance_update(context.get_admin_context(), - instance['id'], {'vm_mode': vm_mode}) + db.instance_update(context, instance['id'], + {'vm_mode': vm_mode}) vm_ref = VMHelper.create_vm(self._session, instance, kernel and kernel.get('file', None) or None, ramdisk and ramdisk.get('file', None) or None, diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 7c355a55b..a1928dfa8 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -194,21 +194,22 @@ 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, context, instance, network_info, + block_device_mapping=None): """Create VM instance""" - self._vmops.spawn(instance, network_info) + self._vmops.spawn(context, instance, network_info) def revert_resize(self, instance): """Reverts a resize, powering back on the instance""" self._vmops.revert_resize(instance) - def finish_resize(self, instance, disk_info, network_info): + def finish_resize(self, context, instance, disk_info, network_info): """Completes a resize, turning on the migrated instance""" - self._vmops.finish_resize(instance, disk_info, network_info) + self._vmops.finish_resize(context, instance, disk_info, network_info) - def snapshot(self, instance, image_id): + def snapshot(self, context, instance, image_id): """ Create snapshot from a running VM instance """ - self._vmops.snapshot(instance, image_id) + self._vmops.snapshot(context, instance, image_id) def reboot(self, instance, network_info): """Reboot VM instance""" -- cgit From 50eb566ba5ce50127ad3df8984dd6895c31361a3 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Wed, 27 Jul 2011 17:56:12 +0000 Subject: Fix context argument in a test; add TODOs --- nova/tests/test_libvirt.py | 2 +- nova/virt/driver.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index c4af38426..d4f8f00d8 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -775,7 +775,7 @@ class LibvirtConnTestCase(test.TestCase): network_info = [(network, mapping)] try: - conn.spawn(context.get_admin_context(), instance, network_info) + conn.spawn(self.context, instance, network_info) except Exception, e: count = (0 <= str(e.message).find('Unexpected method call')) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 40cb877ce..8e17d7540 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -40,6 +40,7 @@ class ComputeDriver(object): def init_host(self, host): """Adopt existing VM's running here""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def get_info(self, instance_name): @@ -52,13 +53,16 @@ class ComputeDriver(object): :num_cpu: (int) the number of virtual CPUs for the domain :cpu_time: (int) the CPU time used in nanoseconds """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def list_instances(self): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def list_instances_detail(self): """Return a list of InstanceInfo for all registered VMs""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def spawn(self, context, instance, network_info, @@ -80,29 +84,36 @@ class ComputeDriver(object): warning in that case. """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def reboot(self, instance, network_info): """Reboot specified VM""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def snapshot_instance(self, context, instance_id, image_id): raise NotImplementedError() def get_console_pool_info(self, console_type): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def get_console_output(self, instance): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def get_ajax_console(self, instance): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def get_diagnostics(self, instance): """Return data about VM diagnostics""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def get_host_ip_addr(self): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def attach_volume(self, context, instance_id, volume_id, mountpoint): @@ -117,6 +128,7 @@ class ComputeDriver(object): def migrate_disk_and_power_off(self, instance, dest): """Transfers the VHD of a running instance to another host, then shuts off the instance copies over the COW disk""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def snapshot(self, context, instance, image_id): @@ -129,30 +141,37 @@ class ComputeDriver(object): def revert_resize(self, instance): """Reverts a resize, powering back on the instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def pause(self, instance, callback): """Pause VM instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def unpause(self, instance, callback): """Unpause paused VM instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def suspend(self, instance, callback): """suspend the specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def resume(self, instance, callback): """resume the specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def rescue(self, instance, callback, network_info): """Rescue the specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def unrescue(self, instance, callback, network_info): """Unrescue the specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def update_available_resource(self, ctxt, host): @@ -165,6 +184,7 @@ class ComputeDriver(object): :param host: hostname that compute manager is currently running """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def live_migration(self, ctxt, instance_ref, dest, @@ -184,20 +204,25 @@ class ComputeDriver(object): expected nova.compute.manager.recover_live_migration. """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def refresh_security_group_rules(self, security_group_id): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def refresh_security_group_members(self, security_group_id): + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def refresh_provider_fw_rules(self, security_group_id): """See: nova/virt/fake.py for docs.""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def reset_network(self, instance): """reset networking for specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token pass def ensure_filtering_rules_for_instance(self, instance_ref): @@ -223,10 +248,12 @@ class ComputeDriver(object): :params instance_ref: nova.db.sqlalchemy.models.Instance object """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def unfilter_instance(self, instance, network_info): """Stop filtering instance""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def set_admin_password(self, context, instance_id, new_pass=None): @@ -237,24 +264,30 @@ class ComputeDriver(object): """Create a file on the VM instance. The file path and contents should be base64-encoded. """ + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def agent_update(self, instance, url, md5hash): """Update agent on the VM instance.""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def inject_network_info(self, instance, nw_info): """inject network info for specified instance""" + # TODO(Vek): Need to pass context in for access to auth_token pass def poll_rescued_instances(self, timeout): """Poll for rescued instances""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def plug_vifs(self, instance, network_info): """Plugs in VIFs to networks.""" + # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() -- cgit From 9a84b87ae04dc5220f95992d9a6c4e210fbc374f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 13:59:53 -0400 Subject: fixed issue with factory for Versions Resource --- nova/api/openstack/versions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 4d40274b3..c39e9dae7 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -104,9 +104,7 @@ class Versions(wsgi.Resource): @classmethod def factory(cls, global_config, **local_config): """Paste factory.""" - def _factory(app): - return cls(app, **local_config) - return _factory + return cls() def __init__(self): metadata = { -- cgit From f1830708f823a9de9c2f1cd24af5bed80302788f Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 27 Jul 2011 11:08:44 -0700 Subject: Some work on testing. Two cases related to lp816713 have some coverage already: using an id as an imageRef (test_create_instance_v1_1_local_href), and using a nova href as a url (test_create_instance_v1_1) --- nova/api/openstack/create_instance_helper.py | 4 +++- nova/tests/api/openstack/test_servers.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 0e6c0a87c..234c16bcc 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -89,7 +89,9 @@ 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 image_href.startswith(req.application_url): + + if isinstance(image_href, basestring) and\ + image_href.startswith(req.application_url): image_href = image_href.split('/').pop() try: image_service, image_id = nova.image.get_image_service(image_href) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 4ca79434f..edc11b761 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1250,7 +1250,8 @@ class ServersTest(test.TestCase): def test_create_instance_v1_1(self): self._setup_for_create_instance() - image_href = 'http://localhost/images/2' + # proper local hrefs must start with 'http://localhost/v1.1/' + image_href = 'http://localhost/v1.1/images/2' flavor_ref = 'http://localhost/flavors/3' expected_flavor = { "id": "3", -- cgit From 4def65d37886fff0dc9f238bca5454abaacb6f76 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Wed, 27 Jul 2011 18:13:04 +0000 Subject: Use auth_token to set x-auth-token header in glance requests --- nova/virt/xenapi/vm_utils.py | 7 +++++-- plugins/xenserver/xenapi/etc/xapi.d/plugins/glance | 22 +++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index aa0e4c2df..b1b0ddd0c 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -377,7 +377,8 @@ class VMHelper(HelperBase): 'glance_host': glance_host, 'glance_port': glance_port, 'sr_path': cls.get_sr_path(session), - 'os_type': os_type} + 'os_type': os_type, + 'auth_token': getattr(context, 'auth_token', None)} kwargs = {'params': pickle.dumps(params)} task = session.async_call_plugin('glance', 'upload_vhd', kwargs) @@ -429,7 +430,8 @@ class VMHelper(HelperBase): 'glance_host': glance_host, 'glance_port': glance_port, 'uuid_stack': uuid_stack, - 'sr_path': cls.get_sr_path(session)} + 'sr_path': cls.get_sr_path(session), + 'auth_token': getattr(context, 'auth_token', None)} kwargs = {'params': pickle.dumps(params)} task = session.async_call_plugin('glance', 'download_vhd', kwargs) @@ -475,6 +477,7 @@ class VMHelper(HelperBase): sr_ref = safe_find_sr(session) glance_client, image_id = nova.image.get_glance_client(image) + glance_client.set_auth_token(getattr(context, 'auth_token', None)) meta, image_file = glance_client.get_image(image_id) virtual_size = int(meta['size']) vdi_size = virtual_size diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance index fbe080b22..86e837849 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance @@ -67,12 +67,17 @@ def _copy_kernel_vdi(dest, copy_args): def _download_tarball(sr_path, staging_path, image_id, glance_host, - glance_port): + glance_port, auth_token): """Download the tarball image from Glance and extract it into the staging area. """ + # Build request headers + headers = {} + if auth_token: + headers['x-auth-token'] = auth_token + conn = httplib.HTTPConnection(glance_host, glance_port) - conn.request('GET', '/v1/images/%s' % image_id) + conn.request('GET', '/v1/images/%s' % image_id, headers=headers) resp = conn.getresponse() if resp.status == httplib.NOT_FOUND: raise Exception("Image '%s' not found in Glance" % image_id) @@ -236,7 +241,8 @@ def _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids): os.link(source, link_name) -def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type): +def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type, + auth_token): """ Create a tarball of the image and then stream that into Glance using chunked-transfer-encoded HTTP. @@ -263,6 +269,10 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type): 'x-image-meta-container-format': 'ovf', 'x-image-meta-property-os-type': os_type} + # If we have an auth_token, set an x-auth-token header + if auth_token: + headers['x-auth-token'] = auth_token + for header, value in headers.iteritems(): conn.putheader(header, value) conn.endheaders() @@ -364,11 +374,12 @@ def download_vhd(session, args): glance_port = params["glance_port"] uuid_stack = params["uuid_stack"] sr_path = params["sr_path"] + auth_token = params["auth_token"] staging_path = _make_staging_area(sr_path) try: _download_tarball(sr_path, staging_path, image_id, glance_host, - glance_port) + glance_port, auth_token) # Right now, it's easier to return a single string via XenAPI, # so we'll json encode the list of VHDs. return json.dumps(_import_vhds(sr_path, staging_path, uuid_stack)) @@ -386,12 +397,13 @@ def upload_vhd(session, args): glance_port = params["glance_port"] sr_path = params["sr_path"] os_type = params["os_type"] + auth_token = params["auth_token"] staging_path = _make_staging_area(sr_path) try: _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids) _upload_tarball(staging_path, image_id, glance_host, glance_port, - os_type) + os_type, auth_token) finally: _cleanup_staging_area(staging_path) -- cgit From 22beaf8802fdc44242f4a96e291c4fbb60af0e3a Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 14:24:35 -0400 Subject: make atom+xml accept header be ignored on 300 responses in the VersionsRequestDeserializer --- nova/api/openstack/versions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index c39e9dae7..e063ff272 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -206,6 +206,14 @@ class VersionV11(object): class VersionsRequestDeserializer(wsgi.RequestDeserializer): + def get_expected_content_type(self, request): + supported_content_types = list(self.supported_content_types) + if request.path != '/': + # Remove atom+xml accept type for 300 responses + del supported_content_types[2] + + return request.best_match_content_type(supported_content_types) + def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" args = {} -- cgit From e2ce48eb3ffde56fa8d74b397682814ad278ae63 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 27 Jul 2011 11:33:56 -0700 Subject: this change will require that local urls be input with a properly constructed local url: http://localhost/v1.1/images/[id]. Such urls are translated to ids at the api layer. Previously, any url ending with and int was ok. --- nova/tests/api/openstack/fakes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 26b1de818..a1d94876f 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -104,8 +104,7 @@ def stub_out_key_pair_funcs(stubs, have_key_pair=True): def stub_out_image_service(stubs): def fake_get_image_service(image_href): - image_id = int(str(image_href).split('/')[-1]) - return (nova.image.fake.FakeImageService(), image_id) + return (nova.image.fake.FakeImageService(), image_href) stubs.Set(nova.image, 'get_image_service', fake_get_image_service) stubs.Set(nova.image, 'get_default_image_service', lambda: nova.image.fake.FakeImageService()) -- cgit From c20a4845afc47d124017de698657c1713dc11e7f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 14:35:47 -0400 Subject: fixed xmlns issue --- nova/api/openstack/versions.py | 8 ++++---- nova/tests/api/openstack/test_versions.py | 17 +++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index e063ff272..b38c48939 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -25,7 +25,7 @@ from nova.api.openstack import wsgi ATOM_XMLNS = "http://www.w3.org/2005/Atom" -OS_XMLNS_BASE = "http://docs.openstack.org/common/api/" +OS_XMLNS_BASE = "http://docs.openstack.org/common/api/v1.0" VERSIONS = { "v1.0": { "version": { @@ -235,7 +235,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _versions_to_xml(self, versions, name="versions", xmlns=None): root = self._xml_doc.createElement(name) - root.setAttribute("xmlns", "%sv1.0" % OS_XMLNS_BASE) + root.setAttribute("xmlns", OS_XMLNS_BASE) root.setAttribute("xmlns:atom", ATOM_XMLNS) for version in versions: @@ -256,7 +256,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _create_version_node(self, version, create_ns=False): version_node = self._xml_doc.createElement('version') if create_ns: - xmlns = "%s%s" % (OS_XMLNS_BASE, version['id']) + xmlns = OS_XMLNS_BASE xmlns_atom = "http://www.w3.org/2005/Atom" version_node.setAttribute('xmlns', xmlns) version_node.setAttribute('xmlns:atom', xmlns_atom) @@ -296,7 +296,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def multi(self, data): self._xml_doc = minidom.Document() node = self._versions_to_xml(data['choices'], 'choices', - xmlns="%sv1.0" % OS_XMLNS_BASE) + xmlns=OS_XMLNS_BASE) return self.to_xml_string(node) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 3f7867ff4..bc9000900 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -173,8 +173,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") root = xml.etree.ElementTree.XML(res.body) self.assertEqual(root.tag.split('}')[1], "version") - self.assertEqual(root.tag.split('}')[0].strip('{'), - "%sv1.0" % OS_XMLNS_BASE) + self.assertEqual(root.tag.split('}')[0].strip('{'), OS_XMLNS_BASE) children = list(root) media_types = children[0] @@ -188,7 +187,7 @@ class VersionsTest(test.TestCase): expected = """ @@ -224,7 +223,7 @@ class VersionsTest(test.TestCase): expected = """ @@ -259,7 +258,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") expected = """ - + @@ -451,7 +450,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") expected = """ - + Date: Wed, 27 Jul 2011 15:29:15 -0400 Subject: update everything to use global VERSIONS --- nova/api/openstack/versions.py | 49 +++++++++++-------------------- nova/tests/api/openstack/test_versions.py | 18 ++++++------ 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index b38c48939..32ee64339 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -143,20 +143,14 @@ class Versions(wsgi.Resource): return self._versions_multi_choice(request) def _versions_list(self, request): - version_objs = [ - { - "id": "v1.1", - "status": "CURRENT", - #TODO(wwolf) get correct value for these - "updated": "2011-07-18T11:30:00Z", - }, - { - "id": "v1.0", - "status": "DEPRECATED", - #TODO(wwolf) get correct value for these - "updated": "2010-10-09T11:30:00Z", - }, - ] + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version]['version'] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "updated": version['updated'], + }) builder = nova.api.openstack.views.versions.get_view_builder(request) versions = [builder.build(version) for version in version_objs] @@ -164,28 +158,19 @@ class Versions(wsgi.Resource): def _versions_multi_choice(self, request): #TODO - version_objs = [ - { - "id": "v1.1", - "status": "CURRENT", + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version]['version'] + version_objs.append({ + "id": version['id'], + "status": version['status'], "links": [ { - "rel": "self", + "rel": "self" } ], - "media-types": VERSIONS['v1.1']['version']['media-types'], - }, - { - "id": "v1.0", - "status": "DEPRECATED", - "links": [ - { - "rel": "self", - } - ], - "media-types": VERSIONS['v1.0']['version']['media-types'], - }, - ] + "media-types": version['media-types'] + }) builder = nova.api.openstack.views.versions.get_view_builder(request) choices = [ diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index bc9000900..f059b140d 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -53,7 +53,7 @@ class VersionsTest(test.TestCase): { "id": "v1.1", "status": "CURRENT", - "updated": "2011-07-18T11:30:00Z", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -63,7 +63,7 @@ class VersionsTest(test.TestCase): { "id": "v1.0", "status": "DEPRECATED", - "updated": "2010-10-09T11:30:00Z", + "updated": "2011-01-21T11:33:21Z", "links": [ { "rel": "self", @@ -259,11 +259,11 @@ class VersionsTest(test.TestCase): expected = """ - + + updated="2011-01-21T11:33:21Z"> """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, @@ -357,7 +357,7 @@ class VersionsTest(test.TestCase): expected = """ Available API Versions - 2011-07-18T11:30:00Z + 2011-01-21T11:33:21Z http://localhost/ Rackspace @@ -367,19 +367,19 @@ class VersionsTest(test.TestCase): http://localhost/v1.1/ Version v1.1 - 2011-07-18T11:30:00Z + 2011-01-21T11:33:21Z - Version v1.1 CURRENT (2011-07-18T11:30:00Z) + Version v1.1 CURRENT (2011-01-21T11:33:21Z) http://localhost/v1.0/ Version v1.0 - 2010-10-09T11:30:00Z + 2011-01-21T11:33:21Z - Version v1.0 DEPRECATED (2010-10-09T11:30:00Z) + Version v1.0 DEPRECATED (2011-01-21T11:33:21Z) -- cgit From f2d8e91b83ff3a3bd1e2f3c53c25a418a578cd27 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 16:34:02 -0400 Subject: moved rest of build logic into builder --- nova/api/openstack/versions.py | 42 ++-------------------------- nova/api/openstack/views/versions.py | 46 +++++++++++++++++++++---------- nova/tests/api/openstack/test_versions.py | 31 +++++++++++++-------- 3 files changed, 54 insertions(+), 65 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 32ee64339..fd0ee46b7 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -135,49 +135,13 @@ class Versions(wsgi.Resource): def dispatch(self, request, *args): """Respond to a request for all OpenStack API versions.""" + builder = nova.api.openstack.views.versions.get_view_builder(request) if request.path == '/': # List Versions - return self._versions_list(request) + return builder.build(VERSIONS) else: # Versions Multiple Choice - return self._versions_multi_choice(request) - - def _versions_list(self, request): - version_objs = [] - for version in VERSIONS: - version = VERSIONS[version]['version'] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "updated": version['updated'], - }) - - builder = nova.api.openstack.views.versions.get_view_builder(request) - versions = [builder.build(version) for version in version_objs] - return dict(versions=versions) - - def _versions_multi_choice(self, request): - #TODO - version_objs = [] - for version in VERSIONS: - version = VERSIONS[version]['version'] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "links": [ - { - "rel": "self" - } - ], - "media-types": version['media-types'] - }) - - builder = nova.api.openstack.views.versions.get_view_builder(request) - choices = [ - builder.build_choices(version, request) - for version in version_objs] - - return dict(choices=choices) + return builder.build_choices(VERSIONS, request) class VersionV10(object): diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 97e35c983..87ec251e6 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -31,21 +31,37 @@ class ViewBuilder(object): """ self.base_url = base_url - def build_choices(self, version_data, request): - version_data['links'][0]['href'] = self._build_versioned_link(request, - version_data['id']) - return version_data - - def build(self, version_data): - """Generic method used to generate a version entity.""" - version = { - "id": version_data["id"], - "status": version_data["status"], - "updated": version_data["updated"], - "links": self._build_links(version_data), - } - - return version + def build_choices(self, VERSIONS, request): + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version]['version'] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "links": [ + { + "rel": "self", + "href": self._build_versioned_link(request, + version['id']) + } + ], + "media-types": version['media-types'] + }) + + return dict(choices=version_objs) + + def build(self, VERSIONS): + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version]['version'] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "updated": version['updated'], + "links": self._build_links(version), + }) + + return dict(versions=version_objs) def _build_versioned_link(self, req, version): return '%s://%s/%s%s' % (req.scheme, req.host, version, req.path) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index f059b140d..6e4042b3f 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -529,20 +529,29 @@ class VersionsTest(test.TestCase): base_url = "http://example.org/" version_data = { - "id": "3.2.1", - "status": "CURRENT", - "updated": "2011-07-18T11:30:00Z"} + "v3.2.1": { + "version": { + "id": "3.2.1", + "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", + } + } + } expected = { - "id": "3.2.1", - "status": "CURRENT", - "updated": "2011-07-18T11:30:00Z", - "links": [ + "versions": [ { - "rel": "self", - "href": "http://example.org/3.2.1/", - }, - ], + "id": "3.2.1", + "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", + "links": [ + { + "rel": "self", + "href": "http://example.org/3.2.1/", + }, + ], + } + ] } builder = views.versions.ViewBuilder(base_url) -- cgit From 9c220b1c4547ad2cdd6110fa029b6f9478bae99f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 16:54:09 -0400 Subject: added test for accept header of atom+xml on 300 responses to make sure it defaults back to json, and reworked some of the logic to make how this happens clearer --- nova/api/openstack/versions.py | 3 ++- nova/tests/api/openstack/test_versions.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index fd0ee46b7..02b0e1df8 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -159,7 +159,8 @@ class VersionsRequestDeserializer(wsgi.RequestDeserializer): supported_content_types = list(self.supported_content_types) if request.path != '/': # Remove atom+xml accept type for 300 responses - del supported_content_types[2] + if 'application/atom+xml' in supported_content_types: + supported_content_types.remove('application/atom+xml') return request.best_match_content_type(supported_content_types) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 6e4042b3f..63bca7066 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -471,6 +471,16 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, ATOM_XMLNS) + def test_multi_choice_server_atom(self): + """ + Make sure multi choice responses do not have content-type + application/atom+xml (should use default of json) + """ + req = webob.Request.blank('/servers/2') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") def test_multi_choice_server(self): req = webob.Request.blank('/servers/2') -- cgit From 77d06c7c82bfafd956f1108b2adbcb378628511f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 17:09:17 -0400 Subject: utilize _create_link_nodes base class function --- nova/api/openstack/versions.py | 11 +++-------- nova/api/openstack/wsgi.py | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 02b0e1df8..928cf467a 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -220,14 +220,9 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): media_types = self._create_media_types(version['media-types']) version_node.appendChild(media_types) - for link in version['links']: - link_node = self._xml_doc.createElement('atom:link') - link_node.setAttribute('rel', link['rel']) - link_node.setAttribute('href', link['href']) - if 'type' in link: - link_node.setAttribute('type', link['type']) - - version_node.appendChild(link_node) + link_nodes = self._create_link_nodes(self._xml_doc, version['links']) + for link in link_nodes: + version_node.appendChild(link) return version_node diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index a28443d12..ca502021d 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -387,6 +387,8 @@ class XMLDictSerializer(DictSerializer): link_node = xml_doc.createElement('atom:link') link_node.setAttribute('rel', link['rel']) link_node.setAttribute('href', link['href']) + if 'type' in link: + link_node.setAttribute('type', link['type']) link_nodes.append(link_node) return link_nodes -- cgit From 22f502cdca94a20ebb061f434a9a78789a3b165d Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 17:19:59 -0400 Subject: move viewbuilder and serializer tests into their own test cases --- nova/tests/api/openstack/test_versions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 63bca7066..7bb762c71 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -535,6 +535,8 @@ class VersionsTest(test.TestCase): self.assertDictMatch(expected, json.loads(res.body)) + +class VersionsViewBuilderTests(VersionsTest): def test_view_builder(self): base_url = "http://example.org/" @@ -580,6 +582,8 @@ class VersionsTest(test.TestCase): self.assertEqual(actual, expected) + +class VersionsSerializerTests(VersionsTest): def test_versions_list_xml_serializer(self): versions_data = { 'versions': [ -- cgit From f9ff78a5ac5f83d789334c36bebfce62af0ea406 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 27 Jul 2011 17:20:42 -0400 Subject: refactoring MetadataXMLDeserializer in wsgi/common --- nova/api/openstack/common.py | 12 +++++++++++- nova/api/openstack/create_instance_helper.py | 7 +++++-- nova/api/openstack/wsgi.py | 13 ------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 24c82035a..a99951764 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -196,7 +196,17 @@ def get_version_from_href(href): return version -class MetadataXMLDeserializer(wsgi.MetadataXMLDeserializer): +class MetadataXMLDeserializer(wsgi.XMLDeserializer): + + def extract_metadata(self, metadata_node): + """Marshal the metadata attribute of a parsed request""" + if metadata_node is None: + return None + metadata = {} + for meta_node in self.find_children_named(metadata_node, "meta"): + key = meta_node.getAttribute("key") + metadata[key] = self.extract_text(meta_node) + return metadata def _extract_metadata_container(self, datastring): dom = minidom.parseString(datastring) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index f8317565e..70532cf79 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -28,6 +28,7 @@ from nova import quota from nova import utils from nova.compute import instance_types +from nova.api.openstack import common from nova.api.openstack import wsgi from nova.auth import manager as auth_manager @@ -285,7 +286,7 @@ class CreateInstanceHelper(object): return password -class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): +class ServerXMLDeserializer(wsgi.XMLDeserializer): """ Deserializer to handle xml-formatted server create requests. @@ -293,6 +294,8 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): and personality attributes """ + metadata_deserializer = common.MetadataXMLDeserializer() + def create(self, string): """Deserialize an xml-formatted server create request""" dom = minidom.parseString(string) @@ -307,7 +310,7 @@ class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) metadata_node = self.find_first_child_named(server_node, "metadata") - metadata = self.extract_metadata(metadata_node) + metadata = self.metadata_deserializer.extract_metadata(metadata_node) if metadata is not None: server["metadata"] = metadata personality = self._extract_personality(server_node) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index a28443d12..d10424d79 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -161,19 +161,6 @@ class XMLDeserializer(TextDeserializer): return {'body': self._from_xml(datastring)} -class MetadataXMLDeserializer(XMLDeserializer): - - def extract_metadata(self, metadata_node): - """Marshal the metadata attribute of a parsed request""" - if metadata_node is None: - return None - metadata = {} - for meta_node in self.find_children_named(metadata_node, "meta"): - key = meta_node.getAttribute("key") - metadata[key] = self.extract_text(meta_node) - return metadata - - class RequestHeadersDeserializer(ActionDispatcher): """Default request headers deserializer""" -- cgit From 9cc71286d6e5339e42d6957570bfc02ea71353fe Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 17:21:14 -0400 Subject: pep8 issue --- nova/tests/api/openstack/test_versions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 7bb762c71..1f2c1deef 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -471,6 +471,7 @@ class VersionsTest(test.TestCase): """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, ATOM_XMLNS) + def test_multi_choice_server_atom(self): """ Make sure multi choice responses do not have content-type -- cgit From c00e933be6500caf25d55d32db5abb0e36600670 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 17:54:35 -0400 Subject: put run_tests.sh back to how it was --- run_tests.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index e39ecd315..8f2b51757 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -84,9 +84,7 @@ function run_pep8 { srcfiles+=" `find tools/*`" srcfiles+=" nova setup.py plugins/xenserver/xenapi/etc/xapi.d/plugins/glance" # Just run PEP8 in current environment - #${wrapper} pep8 --repeat --show-pep8 --show-source \ - #--exclude=vcsversion.py ${srcfiles} - pep8 --repeat --show-pep8 --show-source \ + ${wrapper} pep8 --repeat --show-pep8 --show-source \ --exclude=vcsversion.py ${srcfiles} } -- cgit From 634702e9a6813b8793a82ddd87d24690b05ffc1e Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 17:59:05 -0400 Subject: stub out VERSIONS for the tests --- nova/tests/api/openstack/test_versions.py | 75 ++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 1f2c1deef..dfbc67f15 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -29,7 +29,78 @@ from nova.api.openstack import views ATOM_XMLNS = versions.ATOM_XMLNS OS_XMLNS_BASE = versions.OS_XMLNS_BASE -VERSIONS = versions.VERSIONS +VERSIONS = { + "v1.0": { + "version": { + "id": "v1.0", + "status": "DEPRECATED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.0/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" + } + ], + }, + }, + "v1.1": { + "version": { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v1.1/" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" + } + ], + }, + }, +} class VersionsTest(test.TestCase): @@ -38,6 +109,8 @@ class VersionsTest(test.TestCase): self.context = context.get_admin_context() self.stubs = stubout.StubOutForTesting() fakes.stub_out_auth(self.stubs) + #Stub out VERSIONS + versions.VERSIONS = VERSIONS def tearDown(self): super(VersionsTest, self).tearDown() -- cgit From 03aac3ffd546ab1528b73ee36c8632f30ed8af2f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Wed, 27 Jul 2011 18:07:01 -0400 Subject: use ATOM_XMLNS everywhere --- nova/api/openstack/versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 928cf467a..95681b788 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -207,7 +207,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): version_node = self._xml_doc.createElement('version') if create_ns: xmlns = OS_XMLNS_BASE - xmlns_atom = "http://www.w3.org/2005/Atom" + xmlns_atom = ATOM_XMLNS version_node.setAttribute('xmlns', xmlns) version_node.setAttribute('xmlns:atom', xmlns_atom) -- cgit From 92c8d269a13917de397c1d0ce9fecfaa36195ce9 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 28 Jul 2011 10:23:44 -0400 Subject: Removed v1_1 from individual tests --- nova/tests/api/openstack/test_server_actions.py | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 97c108068..5c23d6d9d 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -350,7 +350,7 @@ class ServerActionsTestV11(test.TestCase): def tearDown(self): self.stubs.UnsetAll() - def test_server_change_password_v1_1(self): + def test_server_change_password(self): mock_method = MockSetAdminPassword() self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) body = {'changePassword': {'adminPass': '1234pass'}} @@ -363,7 +363,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(mock_method.instance_id, '1') self.assertEqual(mock_method.password, '1234pass') - def test_server_change_password_bad_request_v1_1(self): + def test_server_change_password_bad_request(self): body = {'changePassword': {'pass': '12345'}} req = webob.Request.blank('/v1.1/servers/1/action') req.method = 'POST' @@ -372,7 +372,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_change_password_empty_string_v1_1(self): + def test_server_change_password_empty_string(self): body = {'changePassword': {'adminPass': ''}} req = webob.Request.blank('/v1.1/servers/1/action') req.method = 'POST' @@ -381,7 +381,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_change_password_none_v1_1(self): + def test_server_change_password_none(self): body = {'changePassword': {'adminPass': None}} req = webob.Request.blank('/v1.1/servers/1/action') req.method = 'POST' @@ -390,7 +390,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_rebuild_accepted_minimum_v1_1(self): + def test_server_rebuild_accepted_minimum(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -405,7 +405,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) - def test_server_rebuild_rejected_when_building_v1_1(self): + def test_server_rebuild_rejected_when_building(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -426,7 +426,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 409) - def test_server_rebuild_accepted_with_metadata_v1_1(self): + def test_server_rebuild_accepted_with_metadata(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -444,7 +444,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) - def test_server_rebuild_accepted_with_bad_metadata_v1_1(self): + def test_server_rebuild_accepted_with_bad_metadata(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -460,7 +460,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_rebuild_bad_entity_v1_1(self): + def test_server_rebuild_bad_entity(self): body = { "rebuild": { "imageId": 2, @@ -475,7 +475,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_rebuild_bad_personality_v1_1(self): + def test_server_rebuild_bad_personality(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -494,7 +494,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_server_rebuild_personality_v1_1(self): + def test_server_rebuild_personality(self): body = { "rebuild": { "imageRef": "http://localhost/images/2", @@ -513,7 +513,7 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) - def test_resize_server_v11(self): + def test_resize_server(self): req = webob.Request.blank('/v1.1/servers/1/action') req.content_type = 'application/json' @@ -532,7 +532,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 202) self.assertEqual(self.resize_called, True) - def test_create_image_v1_1(self): + def test_create_image(self): body = { 'createImage': { 'name': 'Snapshot 1', @@ -547,7 +547,7 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) - def test_create_image_v1_1_with_metadata(self): + def test_create_image_with_metadata(self): body = { 'createImage': { 'name': 'Snapshot 1', @@ -563,7 +563,7 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) - def test_create_image_v1_1_no_name(self): + def test_create_image_no_name(self): body = { 'createImage': {}, } @@ -574,7 +574,7 @@ class ServerActionsTestV11(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) - def test_create_image_v1_1_bad_metadata(self): + def test_create_image_bad_metadata(self): body = { 'createImage': { 'name': 'geoff', -- cgit From cd065f6669a666387c8f9efb5c0fff7eaf94521c Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 28 Jul 2011 10:31:06 -0400 Subject: moved test --- nova/tests/api/openstack/test_server_actions.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 5c23d6d9d..1651b1645 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -132,15 +132,6 @@ class ServerActionsTest(test.TestCase): # res = req.get_response(fakes.wsgi_app()) # self.assertEqual(res.status_int, 501) - def test_server_change_password_not_a_string_v1_1(self): - body = {'changePassword': {'adminPass': 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_reboot(self): body = dict(server=dict( name='server_test', imageId=2, flavorId=2, metadata={}, @@ -363,6 +354,15 @@ class ServerActionsTestV11(test.TestCase): 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') + 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_bad_request(self): body = {'changePassword': {'pass': '12345'}} req = webob.Request.blank('/v1.1/servers/1/action') -- cgit From 71414e65333692956023647b55be06de6a73f11f Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 28 Jul 2011 14:59:28 -0400 Subject: use wsgi XMLNS/ATOM vars --- nova/api/openstack/versions.py | 14 ++++++-------- nova/api/openstack/wsgi.py | 1 + nova/tests/api/openstack/test_versions.py | 32 +++++++++++++++---------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 95681b788..292043b94 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -24,8 +24,6 @@ import nova.api.openstack.views.versions from nova.api.openstack import wsgi -ATOM_XMLNS = "http://www.w3.org/2005/Atom" -OS_XMLNS_BASE = "http://docs.openstack.org/common/api/v1.0" VERSIONS = { "v1.0": { "version": { @@ -185,8 +183,8 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _versions_to_xml(self, versions, name="versions", xmlns=None): root = self._xml_doc.createElement(name) - root.setAttribute("xmlns", OS_XMLNS_BASE) - root.setAttribute("xmlns:atom", ATOM_XMLNS) + root.setAttribute("xmlns", wsgi.XMLNS_V11) + root.setAttribute("xmlns:atom", wsgi.XMLNS_ATOM) for version in versions: root.appendChild(self._create_version_node(version)) @@ -206,8 +204,8 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def _create_version_node(self, version, create_ns=False): version_node = self._xml_doc.createElement('version') if create_ns: - xmlns = OS_XMLNS_BASE - xmlns_atom = ATOM_XMLNS + xmlns = wsgi.XMLNS_V11 + xmlns_atom = wsgi.XMLNS_ATOM version_node.setAttribute('xmlns', xmlns) version_node.setAttribute('xmlns:atom', xmlns_atom) @@ -241,7 +239,7 @@ class VersionsXMLSerializer(wsgi.XMLDictSerializer): def multi(self, data): self._xml_doc = minidom.Document() node = self._versions_to_xml(data['choices'], 'choices', - xmlns=OS_XMLNS_BASE) + xmlns=wsgi.XMLNS_V11) return self.to_xml_string(node) @@ -257,7 +255,7 @@ class VersionsAtomSerializer(wsgi.XMLDictSerializer): def __init__(self, metadata=None, xmlns=None): self.metadata = metadata or {} if not xmlns: - self.xmlns = ATOM_XMLNS + self.xmlns = wsgi.XMLNS_ATOM else: self.xmlns = xmlns diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index ca502021d..c6ece7d45 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -13,6 +13,7 @@ from nova import wsgi 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' LOG = logging.getLogger('nova.api.openstack.wsgi') diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index dfbc67f15..5d3b3d743 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -26,9 +26,8 @@ from nova import test from nova.tests.api.openstack import fakes from nova.api.openstack import versions from nova.api.openstack import views +from nova.api.openstack import wsgi -ATOM_XMLNS = versions.ATOM_XMLNS -OS_XMLNS_BASE = versions.OS_XMLNS_BASE VERSIONS = { "v1.0": { "version": { @@ -246,7 +245,7 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") root = xml.etree.ElementTree.XML(res.body) self.assertEqual(root.tag.split('}')[1], "version") - self.assertEqual(root.tag.split('}')[0].strip('{'), OS_XMLNS_BASE) + self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) children = list(root) media_types = children[0] @@ -282,7 +281,7 @@ class VersionsTest(test.TestCase): api/v1.0/application.wadl" rel="describedby" type="application/vnd.sun.wadl+xml"/> - """.replace(" ", "").replace("\n", "") % OS_XMLNS_BASE + """.replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11 actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -318,7 +317,7 @@ class VersionsTest(test.TestCase): api/v1.1/application.wadl" rel="describedby" type="application/vnd.sun.wadl+xml"/> - """.replace(" ", "").replace("\n", "") % OS_XMLNS_BASE + """.replace(" ", "").replace("\n", "") % wsgi.XMLNS_V11 actual = res.body.replace(" ", "").replace("\n", "") self.assertEqual(expected, actual) @@ -339,8 +338,8 @@ class VersionsTest(test.TestCase): updated="2011-01-21T11:33:21Z"> - """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, - ATOM_XMLNS) + """.replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11, + wsgi.XMLNS_ATOM) actual = res.body.replace(" ", "").replace("\n", "") @@ -542,8 +541,8 @@ class VersionsTest(test.TestCase): - """.replace(" ", "").replace("\n", "") % (OS_XMLNS_BASE, - ATOM_XMLNS) + """.replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11, + wsgi.XMLNS_ATOM) def test_multi_choice_server_atom(self): """ @@ -610,7 +609,7 @@ class VersionsTest(test.TestCase): self.assertDictMatch(expected, json.loads(res.body)) -class VersionsViewBuilderTests(VersionsTest): +class VersionsViewBuilderTests(test.TestCase): def test_view_builder(self): base_url = "http://example.org/" @@ -657,7 +656,7 @@ class VersionsViewBuilderTests(VersionsTest): self.assertEqual(actual, expected) -class VersionsSerializerTests(VersionsTest): +class VersionsSerializerTests(test.TestCase): def test_versions_list_xml_serializer(self): versions_data = { 'versions': [ @@ -680,7 +679,7 @@ class VersionsSerializerTests(VersionsTest): root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "versions") - self.assertEqual(root.tag.split('}')[0].strip('{'), OS_XMLNS_BASE) + self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) version = list(root)[0] self.assertEqual(version.tag.split('}')[1], "version") self.assertEqual(version.get('id'), @@ -691,7 +690,7 @@ class VersionsSerializerTests(VersionsTest): link = list(version)[0] self.assertEqual(link.tag.split('}')[1], "link") - self.assertEqual(link.tag.split('}')[0].strip('{'), ATOM_XMLNS) + self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM) for key, val in versions_data['versions'][0]['links'][0].items(): self.assertEqual(link.get(key), val) @@ -718,7 +717,7 @@ class VersionsSerializerTests(VersionsTest): root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "choices") - self.assertEqual(root.tag.split('}')[0].strip('{'), OS_XMLNS_BASE) + self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) version = list(root)[0] self.assertEqual(version.tag.split('}')[1], "version") self.assertEqual(version.get('id'), versions_data['choices'][0]['id']) @@ -739,7 +738,7 @@ class VersionsSerializerTests(VersionsTest): link = list(version)[1] self.assertEqual(link.tag.split('}')[1], "link") - self.assertEqual(link.tag.split('}')[0].strip('{'), ATOM_XMLNS) + self.assertEqual(link.tag.split('}')[0].strip('{'), wsgi.XMLNS_ATOM) for key, val in versions_data['choices'][0]['links'][0].items(): self.assertEqual(link.get(key), val) @@ -785,8 +784,7 @@ class VersionsSerializerTests(VersionsTest): root = xml.etree.ElementTree.XML(response) self.assertEqual(root.tag.split('}')[1], "version") - self.assertEqual(root.tag.split('}')[0].strip('{'), - "http://docs.openstack.org/common/api/v1.0") + self.assertEqual(root.tag.split('}')[0].strip('{'), wsgi.XMLNS_V11) children = list(root) media_types = children[0] -- cgit From 1c2ac1e7646d1432f57104c6ee3d1fa434387741 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 28 Jul 2011 15:48:30 -0400 Subject: refactoring and make self links correct (not hard coded) --- nova/api/openstack/versions.py | 136 +++++++++++----------- nova/api/openstack/views/versions.py | 33 ++++-- nova/tests/api/openstack/test_versions.py | 186 +++++++++++++++--------------- 3 files changed, 178 insertions(+), 177 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 292043b94..b546462d4 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -26,74 +26,70 @@ from nova.api.openstack import wsgi VERSIONS = { "v1.0": { - "version": { - "id": "v1.0", - "status": "DEPRECATED", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.0/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute-v1.0+xml" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute-v1.0+json" - } - ], - }, + "id": "v1.0", + "status": "DEPRECATED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" + } + ], }, "v1.1": { - "version": { - "id": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.1/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute-v1.1+xml" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute-v1.1+json" - } - ], - }, + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" + } + ], }, } @@ -136,7 +132,7 @@ class Versions(wsgi.Resource): builder = nova.api.openstack.views.versions.get_view_builder(request) if request.path == '/': # List Versions - return builder.build(VERSIONS) + return builder.build_versions(VERSIONS) else: # Versions Multiple Choice return builder.build_choices(VERSIONS, request) @@ -144,12 +140,14 @@ class Versions(wsgi.Resource): class VersionV10(object): def show(self, req): - return VERSIONS['v1.0'] + builder = nova.api.openstack.views.versions.get_view_builder(req) + return builder.build_version(VERSIONS['v1.0']) class VersionV11(object): def show(self, req): - return VERSIONS['v1.1'] + builder = nova.api.openstack.views.versions.get_view_builder(req) + return builder.build_version(VERSIONS['v1.1']) class VersionsRequestDeserializer(wsgi.RequestDeserializer): diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 87ec251e6..7673ffe9e 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -31,18 +31,17 @@ class ViewBuilder(object): """ self.base_url = base_url - def build_choices(self, VERSIONS, request): + def build_choices(self, VERSIONS, req): version_objs = [] for version in VERSIONS: - version = VERSIONS[version]['version'] + version = VERSIONS[version] version_objs.append({ "id": version['id'], "status": version['status'], "links": [ { "rel": "self", - "href": self._build_versioned_link(request, - version['id']) + "href": self.generate_href(version['id'], req.path) } ], "media-types": version['media-types'] @@ -50,10 +49,10 @@ class ViewBuilder(object): return dict(choices=version_objs) - def build(self, VERSIONS): + def build_versions(self, versions): version_objs = [] - for version in VERSIONS: - version = VERSIONS[version]['version'] + for version in versions: + version = versions[version] version_objs.append({ "id": version['id'], "status": version['status'], @@ -63,8 +62,13 @@ class ViewBuilder(object): return dict(versions=version_objs) - def _build_versioned_link(self, req, version): - return '%s://%s/%s%s' % (req.scheme, req.host, version, req.path) + def build_version(self, version): + + for link in version['links']: + if link['rel'] == 'self': + link['href'] = self.base_url.rstrip('/') + '/' + + return dict(version=version) def _build_links(self, version_data): """Generate a container of links that refer to the provided version.""" @@ -73,12 +77,17 @@ class ViewBuilder(object): links = [ { "rel": "self", - "href": href, + "href": href }, ] return links - def generate_href(self, version_number): + def generate_href(self, version_number, path=None): """Create an url that refers to a specific version_number.""" - return os.path.join(self.base_url, version_number) + '/' + version_number = version_number.strip('/') + if path: + path = path.strip('/') + return os.path.join(self.base_url, version_number, path) + else: + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 5d3b3d743..1373f2e39 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -30,74 +30,70 @@ from nova.api.openstack import wsgi VERSIONS = { "v1.0": { - "version": { - "id": "v1.0", - "status": "DEPRECATED", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.0/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute-v1.0+xml" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute-v1.0+json" - } - ], - }, + "id": "v1.0", + "status": "DEPRECATED", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.0/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.0+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.0+json" + } + ], }, "v1.1": { - "version": { - "id": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://servers.api.openstack.org/v1.1/" - }, - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute-v1.1+xml" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute-v1.1+json" - } - ], - }, + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "" + }, + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl" + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute-v1.1+xml" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute-v1.1+json" + } + ], }, } @@ -160,7 +156,7 @@ class VersionsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://servers.api.openstack.org/v1.0/" + "href": "http://localhost/v1.0/" }, { "rel": "describedby", @@ -206,7 +202,7 @@ class VersionsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://servers.api.openstack.org/v1.1/" + "href": "http://localhost/v1.1/" }, { "rel": "describedby", @@ -269,7 +265,7 @@ class VersionsTest(test.TestCase): type="application/vnd.openstack.compute-v1.0+json"/> - - About This Version 2011-01-21T11:33:21Z - http://servers.api.openstack.org/v1.0/ + http://localhost/v1.0/ Rackspace http://www.rackspace.com/ - + - http://servers.api.openstack.org/v1.0/ + http://localhost/v1.0/ Version v1.0 2011-01-21T11:33:21Z - About This Version 2011-01-21T11:33:21Z - http://servers.api.openstack.org/v1.1/ + http://localhost/v1.1/ Rackspace http://www.rackspace.com/ - + - http://servers.api.openstack.org/v1.1/ + http://localhost/v1.1/ Version v1.1 2011-01-21T11:33:21Z - - + @@ -539,7 +535,7 @@ class VersionsTest(test.TestCase): - + """.replace(" ", "").replace("\n", "") % (wsgi.XMLNS_V11, wsgi.XMLNS_ATOM) @@ -569,7 +565,7 @@ class VersionsTest(test.TestCase): "status": "CURRENT", "links": [ { - "href": "http://localhost:80/v1.1/servers/2", + "href": "http://localhost/v1.1/servers/2", "rel": "self", }, ], @@ -589,7 +585,7 @@ class VersionsTest(test.TestCase): "status": "DEPRECATED", "links": [ { - "href": "http://localhost:80/v1.0/servers/2", + "href": "http://localhost/v1.0/servers/2", "rel": "self", }, ], @@ -615,11 +611,9 @@ class VersionsViewBuilderTests(test.TestCase): version_data = { "v3.2.1": { - "version": { - "id": "3.2.1", - "status": "CURRENT", - "updated": "2011-07-18T11:30:00Z", - } + "id": "3.2.1", + "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", } } @@ -640,7 +634,7 @@ class VersionsViewBuilderTests(test.TestCase): } builder = views.versions.ViewBuilder(base_url) - output = builder.build(version_data) + output = builder.build_versions(version_data) self.assertEqual(output, expected) @@ -701,7 +695,7 @@ class VersionsSerializerTests(test.TestCase): "id": "2.7.1", "updated": "2011-07-18T11:30:00Z", "status": "DEPRECATED", - "media-types": VERSIONS['v1.1']['version']['media-types'], + "media-types": VERSIONS['v1.1']['media-types'], "links": [ { "rel": "self", @@ -751,7 +745,7 @@ class VersionsSerializerTests(test.TestCase): "links": [ { "rel": "self", - "href": "http://servers.api.openstack.org/v1.0/" + "href": "http://localhost/v1.0/" }, { "rel": "describedby", @@ -886,7 +880,7 @@ class VersionsSerializerTests(test.TestCase): "links": [ { "rel": "self", - "href": "http://servers.api.openstack.org/v1.1/" + "href": "http://localhost/v1.1/" }, { "rel": "describedby", @@ -936,7 +930,7 @@ class VersionsSerializerTests(test.TestCase): self.assertEqual(updated.tag.split('}')[1], 'updated') self.assertEqual(updated.text, '2011-01-21T11:33:21Z') self.assertEqual(id.tag.split('}')[1], 'id') - self.assertEqual(id.text, 'http://servers.api.openstack.org/v1.1/') + self.assertEqual(id.text, 'http://localhost/v1.1/') self.assertEqual(author.tag.split('}')[1], 'author') author_name = list(author)[0] @@ -947,7 +941,7 @@ class VersionsSerializerTests(test.TestCase): self.assertEqual(author_uri.text, 'http://www.rackspace.com/') self.assertEqual(link.get('href'), - 'http://servers.api.openstack.org/v1.1/') + 'http://localhost/v1.1/') self.assertEqual(link.get('rel'), 'self') self.assertEqual(entry.tag.split('}')[1], 'entry') @@ -960,7 +954,7 @@ class VersionsSerializerTests(test.TestCase): self.assertEqual(entry_id.tag.split('}')[1], "id") self.assertEqual(entry_id.text, - "http://servers.api.openstack.org/v1.1/") + "http://localhost/v1.1/") self.assertEqual(entry_title.tag.split('}')[1], "title") self.assertEqual(entry_title.get('type'), "text") self.assertEqual(entry_title.text, "Version v1.1") -- 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 c0355038b462cfd75b423a535601c4463c68f80f Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 28 Jul 2011 21:00:38 +0000 Subject: Fix to_dict() and elevated() to preserve auth_token; revert an accidental change from context.get_admin_context() to simply context --- nova/context.py | 6 ++++-- nova/virt/xenapi/vmops.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/context.py b/nova/context.py index a765d1695..a23a02a87 100644 --- a/nova/context.py +++ b/nova/context.py @@ -98,7 +98,8 @@ class RequestContext(object): 'read_deleted': self.read_deleted, 'remote_address': self.remote_address, 'timestamp': utils.isotime(self.timestamp), - 'request_id': self.request_id} + 'request_id': self.request_id, + 'auth_token': self.auth_token} @classmethod def from_dict(cls, values): @@ -112,7 +113,8 @@ class RequestContext(object): read_deleted, self.remote_address, self.timestamp, - self.request_id) + self.request_id, + self.auth_token) def get_admin_context(read_deleted=False): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 57c035ffd..f7a800d58 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -209,8 +209,8 @@ class VMOps(object): if instance.vm_mode != vm_mode: # Update database with normalized (or determined) value - db.instance_update(context, instance['id'], - {'vm_mode': vm_mode}) + db.instance_update(context.get_admin_context(), + instance['id'], {'vm_mode': vm_mode}) vm_ref = VMHelper.create_vm(self._session, instance, kernel and kernel.get('file', None) or None, ramdisk and ramdisk.get('file', None) or None, -- 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 54f652bbffaf8edf9ccfe35e1e1b15c20327340a Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 28 Jul 2011 20:59:07 -0400 Subject: fixed pep8 issues and removed unnecessary factory function --- nova/api/openstack/versions.py | 15 +++++---------- nova/api/openstack/views/versions.py | 3 +-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index b546462d4..40e966c5f 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -38,13 +38,13 @@ VERSIONS = { "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" + "servers/api/v1.0/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" + "servers/api/v1.0/application.wadl" }, ], "media-types": [ @@ -71,13 +71,13 @@ VERSIONS = { "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" + "servers/api/v1.1/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" + "servers/api/v1.1/application.wadl" }, ], "media-types": [ @@ -95,12 +95,7 @@ VERSIONS = { class Versions(wsgi.Resource): - @classmethod - def factory(cls, global_config, **local_config): - """Paste factory.""" - return cls() - - def __init__(self): + eef __init__(self): metadata = { "attributes": { "version": ["status", "id"], diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 7673ffe9e..cdf758b77 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -63,7 +63,6 @@ class ViewBuilder(object): return dict(versions=version_objs) def build_version(self, version): - for link in version['links']: if link['rel'] == 'self': link['href'] = self.base_url.rstrip('/') + '/' @@ -77,7 +76,7 @@ class ViewBuilder(object): links = [ { "rel": "self", - "href": href + "href": href, }, ] -- cgit From ad8d33165f52ddf14dc9bd745db00eb039d74af7 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Thu, 28 Jul 2011 21:00:55 -0400 Subject: fixed typo --- nova/api/openstack/versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 40e966c5f..607cf6a81 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -95,7 +95,7 @@ VERSIONS = { class Versions(wsgi.Resource): - eef __init__(self): + def __init__(self): metadata = { "attributes": { "version": ["status", "id"], -- 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 eca19199bdfcc64948f41d7e6b1728cb17b3baa2 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Fri, 29 Jul 2011 10:08:20 -0400 Subject: fix more spacing issues, and removed self link from versions template data --- nova/api/openstack/versions.py | 8 ------ nova/api/openstack/views/versions.py | 12 +++++---- nova/tests/api/openstack/test_versions.py | 42 +++++++++++++------------------ run_tests.sh | 4 ++- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 607cf6a81..3ef72b7f6 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -30,10 +30,6 @@ VERSIONS = { "status": "DEPRECATED", "updated": "2011-01-21T11:33:21Z", "links": [ - { - "rel": "self", - "href": "" - }, { "rel": "describedby", "type": "application/pdf", @@ -63,10 +59,6 @@ VERSIONS = { "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", "links": [ - { - "rel": "self", - "href": "" - }, { "rel": "describedby", "type": "application/pdf", diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index cdf758b77..547289034 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import os @@ -63,11 +64,12 @@ class ViewBuilder(object): return dict(versions=version_objs) def build_version(self, version): - for link in version['links']: - if link['rel'] == 'self': - link['href'] = self.base_url.rstrip('/') + '/' - - return dict(version=version) + reval = copy.deepcopy(version) + reval['links'].insert(0, { + "rel": "self", + "href": self.base_url.rstrip('/') + '/', + }) + return dict(version=reval) def _build_links(self, version_data): """Generate a container of links that refer to the provided version.""" diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index 1373f2e39..e68455778 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -34,21 +34,17 @@ VERSIONS = { "status": "DEPRECATED", "updated": "2011-01-21T11:33:21Z", "links": [ - { - "rel": "self", - "href": "" - }, { "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" + "servers/api/v1.0/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" + "servers/api/v1.0/application.wadl" }, ], "media-types": [ @@ -67,21 +63,17 @@ VERSIONS = { "status": "CURRENT", "updated": "2011-01-21T11:33:21Z", "links": [ - { - "rel": "self", - "href": "" - }, { "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" + "servers/api/v1.1/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" + "servers/api/v1.1/application.wadl" }, ], "media-types": [ @@ -105,9 +97,11 @@ class VersionsTest(test.TestCase): self.stubs = stubout.StubOutForTesting() fakes.stub_out_auth(self.stubs) #Stub out VERSIONS + self.old_versions = versions.VERSIONS versions.VERSIONS = VERSIONS def tearDown(self): + versions.VERSIONS = self.old_versions super(VersionsTest, self).tearDown() def test_get_version_list(self): @@ -162,25 +156,25 @@ class VersionsTest(test.TestCase): "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" + "servers/api/v1.0/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" + "servers/api/v1.0/application.wadl" } ], "media-types": [ { "base": "application/xml", "type": "application/" - "vnd.openstack.compute-v1.0+xml" + "vnd.openstack.compute-v1.0+xml" }, { "base": "application/json", "type": "application/" - "vnd.openstack.compute-v1.0+json" + "vnd.openstack.compute-v1.0+json" } ] } @@ -208,25 +202,25 @@ class VersionsTest(test.TestCase): "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" + "servers/api/v1.1/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" + "servers/api/v1.1/application.wadl" } ], "media-types": [ { "base": "application/xml", "type": "application/" - "vnd.openstack.compute-v1.1+xml" + "vnd.openstack.compute-v1.1+xml" }, { "base": "application/json", "type": "application/" - "vnd.openstack.compute-v1.1+json" + "vnd.openstack.compute-v1.1+json" } ] } @@ -751,13 +745,13 @@ class VersionsSerializerTests(test.TestCase): "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/cs-devguide-20110125.pdf" + "servers/api/v1.0/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.0/application.wadl" + "servers/api/v1.0/application.wadl" }, ], "media-types": [ @@ -886,13 +880,13 @@ class VersionsSerializerTests(test.TestCase): "rel": "describedby", "type": "application/pdf", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf" + "servers/api/v1.1/cs-devguide-20110125.pdf" }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl" + "servers/api/v1.1/application.wadl" }, ], "media-types": [ diff --git a/run_tests.sh b/run_tests.sh index 8f2b51757..e39ecd315 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -84,7 +84,9 @@ function run_pep8 { srcfiles+=" `find tools/*`" srcfiles+=" nova setup.py plugins/xenserver/xenapi/etc/xapi.d/plugins/glance" # Just run PEP8 in current environment - ${wrapper} pep8 --repeat --show-pep8 --show-source \ + #${wrapper} pep8 --repeat --show-pep8 --show-source \ + #--exclude=vcsversion.py ${srcfiles} + pep8 --repeat --show-pep8 --show-source \ --exclude=vcsversion.py ${srcfiles} } -- cgit From f91413bec1e6698935e00c323befa0655dea1ab1 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 29 Jul 2011 10:22:50 -0400 Subject: Fixed changes missed in merge --- 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 1651b1645..0bc7e4cf3 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -53,8 +53,8 @@ class MockSetAdminPassword(object): def _get_instance(): instance = { "id": 1, - "created_at": "2010-10-10T12:00:00Z", - "updated_at": "2010-11-11T11:00:00Z", + "created_at": "2010-10-10 12:00:00", + "updated_at": "2010-11-11 11:00:00", "admin_pass": "", "user_id": "", "project_id": "", -- cgit From 6d62453f4834447f6c06a58ec52c1037d4142293 Mon Sep 17 00:00:00 2001 From: William Wolf Date: Fri, 29 Jul 2011 10:54:20 -0400 Subject: fix run_tests.sh --- run_tests.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index e39ecd315..8f2b51757 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -84,9 +84,7 @@ function run_pep8 { srcfiles+=" `find tools/*`" srcfiles+=" nova setup.py plugins/xenserver/xenapi/etc/xapi.d/plugins/glance" # Just run PEP8 in current environment - #${wrapper} pep8 --repeat --show-pep8 --show-source \ - #--exclude=vcsversion.py ${srcfiles} - pep8 --repeat --show-pep8 --show-source \ + ${wrapper} pep8 --repeat --show-pep8 --show-source \ --exclude=vcsversion.py ${srcfiles} } -- 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 7a1b622ed6088aaaa711b44c50a26e1a33695f63 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 12:21:46 -0400 Subject: updating HACKING --- HACKING | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/HACKING b/HACKING index 2f364c894..2dcb76591 100644 --- a/HACKING +++ b/HACKING @@ -10,13 +10,14 @@ Imports - thou shalt not import objects, only modules - thou shalt not import more than one module per line - thou shalt not make relative imports +- thou shalt order your imports by the full module path - thou shalt organize your imports according to the following template :: # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} + {{stdlib imports in human alphabetical order by module name}} \n - {{nova imports in human alphabetical order}} + {{nova imports in human alphabetical order by module name}} \n \n {{begin your code}} @@ -42,11 +43,12 @@ Human Alphabetical Order Examples import time import unittest - from nova import flags - from nova import test + import nova.api.ec2 + from nova.api import openstack from nova.auth import users - from nova.endpoint import api + import nova.flags from nova.endpoint import cloud + from nova import test Docstrings ---------- @@ -70,6 +72,61 @@ Docstrings :param foo: the foo parameter :param bar: the bar parameter - :returns: description of the return value + :returns: return_type -- description of the return value + :raises: AttributeError, KeyError """ + +Dictionaries/Lists +------------------ + If a dictionary (dict) or list object is longer than 80 characters, its + items should be split with newlines. Embedded iterables should have their + items indented. Additionally, the last item in the dictionary should have + a trailing comma. This increases readability and simplifies future diffs. + + Example: + + my_dictionary = { + "image": { + "name": "Just a Snapshot", + "size": 2749573, + "properties": { + "user_id": 12, + "arch": "x86_64", + }, + "things": [ + "thing_one", + "thing_two", + ], + "status": "ACTIVE", + }, + } + +Method Signatures +----------------- + Calls to methods 80 characters or longer should format each argument with + newlines. This is mainly for readability. + + unnecessarily_long_function_name('string one', + 'string two', + kwarg1=constants.ACTIVE, + kwarg2=['a', 'b', 'c']) + + + Rather than constructing parameters inline, it is better to break things up: + + list_of_strings = [ + 'what_a_long_string', + 'not as long', + ] + + dict_of_numbers = { + 'one': 1, + 'two': 2, + 'twenty four': 24, + } + + object_one.call_a_method('string three', + 'string four', + kwarg1=list_of_strings, + kwarg2=dict_of_numbers) -- cgit From 4438850e323e85673d504ae16bb09f6d05dc65bb Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 12:44:11 -0400 Subject: expanding --- HACKING | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/HACKING b/HACKING index 2dcb76591..ee919e205 100644 --- a/HACKING +++ b/HACKING @@ -50,6 +50,7 @@ Human Alphabetical Order Examples from nova.endpoint import cloud from nova import test + Docstrings ---------- """A one line docstring looks like this and ends in a period.""" @@ -77,6 +78,7 @@ Docstrings """ + Dictionaries/Lists ------------------ If a dictionary (dict) or list object is longer than 80 characters, its @@ -101,9 +103,38 @@ Dictionaries/Lists "status": "ACTIVE", }, } + + Only use the dict constructor for casting. Do not use it to create a new + dictionary. + + Example (BAD): + + my_dictionary = dict(key1='param1', key2='param2', key3=['a', 'b']) + + +Defining Methods +---------------- + Method signatures longer than 80 characters are very unreadable. If you + encounter this problem, first you should determine if your method is + too big. Otherwise, you should compress your keyword arguments with a + '**kwargs' parameter. You should use the 'kwargs' in your method as a + dictionary to retrieve the necessary keyword arguments. + + Example (BAD): + + def my_method(argument_one, argument_two, kwarg_one='default_one', + kwarg_two='default_two', kwarg_three='default_three'): + + Example (GOOD): + + def my_method(argumet_one, argument_two, **kwargs): + kwarg_one = kwargs.get('kwarg_one', 'default_one') + kwarg_two = kwargs.get('kwarg_one', 'default_one') + kwarg_three = kwargs.get('kwarg_three', 'default_three') -Method Signatures ------------------ + +Calling Methods +--------------- Calls to methods 80 characters or longer should format each argument with newlines. This is mainly for readability. @@ -130,3 +161,26 @@ Method Signatures 'string four', kwarg1=list_of_strings, kwarg2=dict_of_numbers) + +Internationalization (i18n) Strings +---------------------------- + In order to support multiple languages, we have a mechanism to support + automatic translations of exception and log strings. + + Example: + msg = _("An error occurred") + raise HTTPBadRequest(explanation=msg) + + If you have a variable to place within the string, first internationalize + the template string then do the replacement. + + Example: + msg = _("Missing parameter: %s") % ("flavor",) + LOG.error(msg) + + If you have multiple variables to place in the string, use keyword + parameters. This helps our translators reorder parameters when needed. + + Example: + msg = _("The server with id %(s_id)s has no key %(m_key)s") + LOG.error(msg % (s_id="1234, m_key="imageId")) -- cgit From 05ca64bd1fbf70c7066e84426dd08b5beb9cbb6d Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 12:49:48 -0400 Subject: upgrades --- HACKING | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/HACKING b/HACKING index ee919e205..82ccdb962 100644 --- a/HACKING +++ b/HACKING @@ -5,13 +5,23 @@ Step 1: Read http://www.python.org/dev/peps/pep-0008/ Step 2: Read http://www.python.org/dev/peps/pep-0008/ again Step 3: Read on + +General +------- +- Put two newlines twixt toplevel code (funcs, classes, etc) +- Put one newline twixt methods in classes and anywhere else +- Do not write "except:", use "except Exception:" at the very least +- Include your name with TODOs as in "TODO(termie)" +- Do not name anything the same name as a builtin or reserved word + + Imports ------- -- thou shalt not import objects, only modules -- thou shalt not import more than one module per line -- thou shalt not make relative imports -- thou shalt order your imports by the full module path -- thou shalt organize your imports according to the following template +- Do not import objects, only modules +- Do not import more than one module per line +- Do not make relative imports +- Order your imports by the full module path +- Organize your imports according to the following template :: # vim: tabstop=4 shiftwidth=4 softtabstop=4 @@ -23,16 +33,6 @@ Imports {{begin your code}} -General -------- -- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) -- thou shalt put one newline twixt methods in classes and anywhere else -- thou shalt not write "except:", use "except Exception:" at the very least -- thou shalt include your name with TODOs as in "TODO(termie)" -- thou shalt not name anything the same name as a builtin or reserved word -- thou shalt not violate causality in our time cone, or else - - Human Alphabetical Order Examples --------------------------------- :: -- cgit From acf7b94a899b2901f102ad2fd176c895abf456a3 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 12:54:55 -0400 Subject: one last change --- HACKING | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HACKING b/HACKING index 82ccdb962..9d6b43b98 100644 --- a/HACKING +++ b/HACKING @@ -8,11 +8,11 @@ Step 3: Read on General ------- -- Put two newlines twixt toplevel code (funcs, classes, etc) -- Put one newline twixt methods in classes and anywhere else +- Put two newlines between top-level code (funcs, classes, etc) +- Put one newline between methods in classes and anywhere else - Do not write "except:", use "except Exception:" at the very least -- Include your name with TODOs as in "TODO(termie)" -- Do not name anything the same name as a builtin or reserved word +- Include your name with TODOs as in "#TODO(termie)" +- Do not name anything the same name as a built-in or reserved word Imports -- cgit From 5c7b70cef1abe91861dd4f82a365143256e9f2cf Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 13:00:09 -0400 Subject: rewording --- HACKING | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING b/HACKING index 9d6b43b98..cb5f696d1 100644 --- a/HACKING +++ b/HACKING @@ -136,7 +136,7 @@ Defining Methods Calling Methods --------------- Calls to methods 80 characters or longer should format each argument with - newlines. This is mainly for readability. + newlines. This is not a requirement, but a guideline. unnecessarily_long_function_name('string one', 'string two', -- cgit From 58e2d9f46fef753857f559c8f5953e025d022d3c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 13:46:24 -0400 Subject: rewording --- HACKING | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/HACKING b/HACKING index cb5f696d1..4c5193779 100644 --- a/HACKING +++ b/HACKING @@ -116,16 +116,12 @@ Defining Methods ---------------- Method signatures longer than 80 characters are very unreadable. If you encounter this problem, first you should determine if your method is - too big. Otherwise, you should compress your keyword arguments with a - '**kwargs' parameter. You should use the 'kwargs' in your method as a - dictionary to retrieve the necessary keyword arguments. + too big. If not, you can compress your keyword arguments with a + '**kwargs' parameter. You can then use 'kwargs' in your method as a + dictionary to retrieve the necessary keyword arguments. This is just a + guideline, not a requirement. - Example (BAD): - - def my_method(argument_one, argument_two, kwarg_one='default_one', - kwarg_two='default_two', kwarg_three='default_three'): - - Example (GOOD): + Example: def my_method(argumet_one, argument_two, **kwargs): kwarg_one = kwargs.get('kwarg_one', 'default_one') -- cgit From e664443e2f2565150b35d813f6cd95c941033524 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 13:51:49 -0400 Subject: removing 'Defining Methods' paragraph --- HACKING | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/HACKING b/HACKING index 4c5193779..ea0188141 100644 --- a/HACKING +++ b/HACKING @@ -112,23 +112,6 @@ Dictionaries/Lists my_dictionary = dict(key1='param1', key2='param2', key3=['a', 'b']) -Defining Methods ----------------- - Method signatures longer than 80 characters are very unreadable. If you - encounter this problem, first you should determine if your method is - too big. If not, you can compress your keyword arguments with a - '**kwargs' parameter. You can then use 'kwargs' in your method as a - dictionary to retrieve the necessary keyword arguments. This is just a - guideline, not a requirement. - - Example: - - def my_method(argumet_one, argument_two, **kwargs): - kwarg_one = kwargs.get('kwarg_one', 'default_one') - kwarg_two = kwargs.get('kwarg_one', 'default_one') - kwarg_three = kwargs.get('kwarg_three', 'default_three') - - Calling Methods --------------- Calls to methods 80 characters or longer should format each argument with @@ -158,6 +141,7 @@ Calling Methods kwarg1=list_of_strings, kwarg2=dict_of_numbers) + Internationalization (i18n) Strings ---------------------------- In order to support multiple languages, we have a mechanism to support -- cgit From 9fb6c29c10d3693c84e34c05092cac4117fa6eb5 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 15:26:14 -0400 Subject: adding more on return_type in docstrings --- HACKING | 1 + 1 file changed, 1 insertion(+) diff --git a/HACKING b/HACKING index ea0188141..2f33fd19f 100644 --- a/HACKING +++ b/HACKING @@ -74,6 +74,7 @@ Docstrings :param foo: the foo parameter :param bar: the bar parameter :returns: return_type -- description of the return value + :returns: description of the return value :raises: AttributeError, KeyError """ -- cgit From c8a6bd03aa7a74a1adbf3df3c879811ef0cc9944 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 15:32:27 -0400 Subject: removing dict() comment --- HACKING | 7 ------- 1 file changed, 7 deletions(-) diff --git a/HACKING b/HACKING index 2f33fd19f..9e1b29668 100644 --- a/HACKING +++ b/HACKING @@ -105,13 +105,6 @@ Dictionaries/Lists }, } - Only use the dict constructor for casting. Do not use it to create a new - dictionary. - - Example (BAD): - - my_dictionary = dict(key1='param1', key2='param2', key3=['a', 'b']) - Calling Methods --------------- -- cgit From 70574077c469a69547d7702592f07ecd0d551daf Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 15:42:10 -0400 Subject: removing extra verbage --- HACKING | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HACKING b/HACKING index 9e1b29668..70b2b09e7 100644 --- a/HACKING +++ b/HACKING @@ -25,9 +25,9 @@ Imports :: # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order by module name}} + {{stdlib imports in human alphabetical order}} \n - {{nova imports in human alphabetical order by module name}} + {{nova imports in human alphabetical order}} \n \n {{begin your code}} -- cgit From 57cda5b254d34af0a63b3cbcb895b04ec7bf5407 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 15:43:23 -0400 Subject: fixing underline --- HACKING | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING b/HACKING index 70b2b09e7..3d79e9e82 100644 --- a/HACKING +++ b/HACKING @@ -137,7 +137,7 @@ Calling Methods Internationalization (i18n) Strings ----------------------------- +----------------------------------- In order to support multiple languages, we have a mechanism to support automatic translations of exception and log strings. -- cgit From 1fb2a1b1f0b7a59193344103bdbb7d58b3a6e7c2 Mon Sep 17 00:00:00 2001 From: Jason Koelker Date: Fri, 29 Jul 2011 14:53:38 -0500 Subject: require either v4 or v6 --- bin/nova-manage | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 75d74903c..3c50e4fc8 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -662,8 +662,9 @@ class NetworkCommands(object): # check for certain required inputs if not label: raise exception.NetworkNotCreated(req='--label') - if not fixed_range_v4: - raise exception.NetworkNotCreated(req='--fixed_range_v4') + if not (fixed_range_v4 or fixed_range_v6): + req = '--fixed_range_v4 or --fixed_range_v6' + raise exception.NetworkNotCreated(req=req) bridge = bridge or FLAGS.flat_network_bridge if not bridge: -- cgit From 3d9f5b561063f714338c023d3b9ad6a786766279 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 29 Jul 2011 15:58:41 -0400 Subject: typo --- HACKING | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING b/HACKING index 3d79e9e82..232a353fd 100644 --- a/HACKING +++ b/HACKING @@ -157,4 +157,4 @@ Internationalization (i18n) Strings Example: msg = _("The server with id %(s_id)s has no key %(m_key)s") - LOG.error(msg % (s_id="1234, m_key="imageId")) + LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) -- cgit From 241bc43166dd174bd64ef4eda1ab368a5a312799 Mon Sep 17 00:00:00 2001 From: Jason Koelker Date: Fri, 29 Jul 2011 15:05:53 -0500 Subject: stwart the switch to just fixed_range --- bin/nova-manage | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 3c50e4fc8..1c3bd9984 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -632,6 +632,7 @@ class NetworkCommands(object): @args('--label', dest="label", metavar='