From ca7bf95e610bdc47f01b8fb7b459269bb8e5df66 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Thu, 11 Aug 2011 18:11:59 -0700 Subject: Initial version --- nova/api/__init__.py | 6 ++ nova/api/ec2/__init__.py | 3 - nova/api/openstack/create_instance_helper.py | 4 +- nova/api/openstack/userdatarequesthandler.py | 110 +++++++++++++++++++++ nova/network/linux_net.py | 5 + nova/tests/api/openstack/fakes.py | 2 + .../api/openstack/test_userdatarequesthandler.py | 80 +++++++++++++++ 7 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 nova/api/openstack/userdatarequesthandler.py create mode 100644 nova/tests/api/openstack/test_userdatarequesthandler.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 747015af5..6e6b092b3 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -15,3 +15,9 @@ # 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 + + +flags.DEFINE_boolean('use_forwarded_for', False, + 'Treat X-Forwarded-For as the canonical remote address. ' + 'Only enable this if you have a sanitizing proxy.') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 8b6e47cfb..e497b499a 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -37,9 +37,6 @@ from nova.auth import manager FLAGS = flags.FLAGS LOG = logging.getLogger("nova.api") -flags.DEFINE_boolean('use_forwarded_for', False, - 'Treat X-Forwarded-For as the canonical remote address. ' - 'Only enable this if you have a sanitizing proxy.') flags.DEFINE_integer('lockout_attempts', 5, 'Number of failed auths before lockout.') flags.DEFINE_integer('lockout_minutes', 15, diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 1425521a9..144697790 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -122,6 +122,7 @@ class CreateInstanceHelper(object): raise exc.HTTPBadRequest(explanation=msg) zone_blob = server_dict.get('blob') + user_data = server_dict.get('user_data') name = server_dict['name'] self._validate_server_name(name) name = name.strip() @@ -161,7 +162,8 @@ class CreateInstanceHelper(object): zone_blob=zone_blob, reservation_id=reservation_id, min_count=min_count, - max_count=max_count)) + max_count=max_count, + user_data=user_data)) except quota.QuotaError as error: self._handle_quota_error(error) except exception.ImageNotFound as error: diff --git a/nova/api/openstack/userdatarequesthandler.py b/nova/api/openstack/userdatarequesthandler.py new file mode 100644 index 000000000..5daa37e95 --- /dev/null +++ b/nova/api/openstack/userdatarequesthandler.py @@ -0,0 +1,110 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""User data request handler.""" + +import base64 +import webob.dec +import webob.exc + +from nova import log as logging +from nova import context +from nova import exception +from nova import db +from nova import flags +from nova import wsgi + + +LOG = logging.getLogger('nova.api.openstack.userdata') +FLAGS = flags.FLAGS + + +class Controller(object): + """ The server user-data API controller for the Openstack API """ + + def __init__(self): + super(Controller, self).__init__() + + @staticmethod + def _format_user_data(instance_ref): + return base64.b64decode(instance_ref['user_data']) + + def get_user_data(self, address): + ctxt = context.get_admin_context() + try: + instance_ref = db.instance_get_by_fixed_ip(ctxt, address) + except exception.NotFound: + instance_ref = None + if not instance_ref: + return None + + data = {'user-data': self._format_user_data(instance_ref)} + return data + + +class UserdataRequestHandler(wsgi.Application): + """Serve user-data from the OS API.""" + + def __init__(self): + self.cc = Controller() + + def print_data(self, data): + if isinstance(data, dict): + output = '' + for key in data: + if key == '_name': + continue + output += key + if isinstance(data[key], dict): + if '_name' in data[key]: + output += '=' + str(data[key]['_name']) + else: + output += '/' + output += '\n' + # Cut off last \n + return output[:-1] + elif isinstance(data, list): + return '\n'.join(data) + else: + return str(data) + + def lookup(self, path, data): + items = path.split('/') + for item in items: + if item: + if not isinstance(data, dict): + return data + if not item in data: + return None + data = data[item] + return data + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + remote_address = "10.0.1.6"#req.remote_addr + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + + data = self.cc.get_user_data(remote_address) + if data is None: + LOG.error(_('Failed to get user data for ip: %s'), remote_address) + raise webob.exc.HTTPNotFound() + data = self.lookup(req.path_info, data) + if data is None: + raise webob.exc.HTTPNotFound() + return self.print_data(data) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 4e1e1f85a..d8fff8a32 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -371,6 +371,11 @@ def metadata_forward(): '-p tcp -m tcp --dport 80 -j DNAT ' '--to-destination %s:%s' % \ (FLAGS.ec2_dmz_host, FLAGS.ec2_port)) + iptables_manager.ipv4['nat'].add_rule('PREROUTING', + '-s 0.0.0.0/0 -d 169.254.169.253/32 ' + '-p tcp -m tcp --dport 80 -j DNAT ' + '--to-destination %s:%s' % \ + (FLAGS.osapi_host, FLAGS.osapi_port)) iptables_manager.apply() diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index d11fbf788..aa5aeef16 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -36,6 +36,7 @@ from nova.api.openstack import auth from nova.api.openstack import extensions from nova.api.openstack import versions from nova.api.openstack import limits +from nova.api.openstack import userdatarequesthandler from nova.auth.manager import User, Project import nova.image.fake from nova.image import glance @@ -99,6 +100,7 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, mapper['/v1.0'] = api10 mapper['/v1.1'] = api11 mapper['/'] = openstack.FaultWrapper(versions.Versions()) + mapper['/latest'] = userdatarequesthandler.UserdataRequestHandler() return mapper diff --git a/nova/tests/api/openstack/test_userdatarequesthandler.py b/nova/tests/api/openstack/test_userdatarequesthandler.py new file mode 100644 index 000000000..0c63076b4 --- /dev/null +++ b/nova/tests/api/openstack/test_userdatarequesthandler.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import json +import unittest +import webob + +from nova import context +from nova import db +from nova import exception +from nova import flags +from nova import test +from nova import log as logging + +from nova.tests.api.openstack import fakes + +LOG = logging.getLogger('nova.api.openstack.userdata') + +USER_DATA_STRING = ("This is an encoded string") +ENCODE_STRING = base64.b64encode(USER_DATA_STRING) + + +def return_server_by_address(context, address): + instance = {"user_data": ENCODE_STRING} + instance["fixed_ips"] = {"address": address, + "floating_ips": []} + return instance + + +def return_non_existing_server_by_address(context, address): + raise exception.NotFound() + + +class TestUserdatarequesthandler(test.TestCase): + + def setUp(self): + super(TestUserdatarequesthandler, self).setUp() + self.stubs.Set(db, 'instance_get_by_fixed_ip', + return_server_by_address) + + def test_user_data(self): + req = webob.Request.blank('/latest/user-data') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.body, USER_DATA_STRING) + + def test_user_data_non_existing_fixed_address(self): + self.stubs.Set(db, 'instance_get_by_fixed_ip', + return_non_existing_server_by_address) + self.flags(use_forwarded_for=False) + req = webob.Request.blank('/latest/user-data') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) + + def test_user_data_invalid_url(self): + req = webob.Request.blank('/latest/user-data-invalid') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) + + def test_user_data_with_use_forwarded_header(self): + self.flags(use_forwarded_for=True) + req = webob.Request.blank('/latest/user-data') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.body, USER_DATA_STRING) -- cgit From 7507ba23004c989c75962c47efbd2ce5e5178a90 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Thu, 11 Aug 2011 18:22:35 -0700 Subject: added userdata entry in the api paste ini --- etc/nova/api-paste.ini | 7 +++++++ nova/api/openstack/userdatarequesthandler.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index abe8c20c4..46a3b0af9 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -69,6 +69,7 @@ use = egg:Paste#urlmap /: osversions /v1.0: openstackapi10 /v1.1: openstackapi11 +/latest: osuserdata [pipeline:openstackapi10] pipeline = faultwrap auth ratelimit osapiapp10 @@ -76,6 +77,9 @@ pipeline = faultwrap auth ratelimit osapiapp10 [pipeline:openstackapi11] pipeline = faultwrap auth ratelimit extensions osapiapp11 +[pipeline:osuserdata] +pipeline = logrequest osappud + [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -99,3 +103,6 @@ pipeline = faultwrap osversionapp [app:osversionapp] paste.app_factory = nova.api.openstack.versions:Versions.factory + +[app:osappud] +paste.app_factory = nova.api.openstack.userdatarequesthandler:UserdataRequestHandler.factory diff --git a/nova/api/openstack/userdatarequesthandler.py b/nova/api/openstack/userdatarequesthandler.py index 5daa37e95..f0205419b 100644 --- a/nova/api/openstack/userdatarequesthandler.py +++ b/nova/api/openstack/userdatarequesthandler.py @@ -96,7 +96,7 @@ class UserdataRequestHandler(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): - remote_address = "10.0.1.6"#req.remote_addr + remote_address = req.remote_addr if FLAGS.use_forwarded_for: remote_address = req.headers.get('X-Forwarded-For', remote_address) -- cgit From 0bc781425bea1162cd81bdc95f49d50068857057 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Fri, 12 Aug 2011 10:01:04 -0500 Subject: start of day --- nova/scheduler/abstract_scheduler.py | 180 +++++----------- nova/scheduler/base_scheduler.py | 403 +++++++++++++++++++++++++++++++++++ 2 files changed, 459 insertions(+), 124 deletions(-) create mode 100644 nova/scheduler/base_scheduler.py diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index eb924732a..a6457cc50 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -14,10 +14,10 @@ # under the License. """ -The AbsractScheduler is a base class Scheduler for creating instances -across zones. There are two expansion points to this class for: -1. Assigning Weights to hosts for requested instances -2. Filtering Hosts based on required instance capabilities +The AbsractScheduler is an abstract class Scheduler for creating instances +locally or across zones. Two methods should be overridden in order to +customize the behavior: filter_hosts() and weigh_hosts(). The default +behavior is to simply select all hosts and weight them the same. """ import operator @@ -185,13 +185,11 @@ class AbstractScheduler(driver.Scheduler): for zone_id, result in child_results: if not result: continue - assert isinstance(zone_id, int) for zone_rec in zones: if zone_rec['id'] != zone_id: continue - for item in result: try: offset = zone_rec['weight_offset'] @@ -202,10 +200,10 @@ class AbstractScheduler(driver.Scheduler): item['raw_weight'] = raw_weight except KeyError: LOG.exception(_("Bad child zone scaling values " - "for Zone: %(zone_id)s") % locals()) + "for Zone: %(zone_id)s") % locals()) def schedule_run_instance(self, context, instance_id, request_spec, - *args, **kwargs): + *args, **kwargs): """This method is called from nova.compute.api to provision an instance. However we need to look at the parameters being passed in to see if this is a request to: @@ -214,13 +212,11 @@ class AbstractScheduler(driver.Scheduler): to simply create the instance (either in this zone or a child zone). """ - # TODO(sandy): We'll have to look for richer specs at some point. - blob = request_spec.get('blob') if blob: self._provision_resource(context, request_spec, instance_id, - request_spec, kwargs) + request_spec, kwargs) return None num_instances = request_spec.get('num_instances', 1) @@ -238,7 +234,7 @@ class AbstractScheduler(driver.Scheduler): build_plan_item = build_plan.pop(0) self._provision_resource(context, build_plan_item, instance_id, - request_spec, kwargs) + request_spec, kwargs) # Returning None short-circuits the routing to Compute (since # we've already done it here) @@ -251,58 +247,49 @@ class AbstractScheduler(driver.Scheduler): anything about the children. """ return self._schedule(context, "compute", request_spec, - *args, **kwargs) + *args, **kwargs) - # TODO(sandy): We're only focused on compute instances right now, - # so we don't implement the default "schedule()" method required - # of Schedulers. def schedule(self, context, topic, request_spec, *args, **kwargs): """The schedule() contract requires we return the one best-suited host for this request. """ - raise driver.NoValidHost(_('No hosts were available')) + # TODO(sandy): We're only focused on compute instances right now, + # so we don't implement the default "schedule()" method required + # of Schedulers. + msg = _("No host selection for %s defined." % topic) + raise driver.NoValidHost(msg) def _schedule(self, context, topic, request_spec, *args, **kwargs): """Returns a list of hosts that meet the required specs, ordered by their fitness. """ - if topic != "compute": - raise NotImplementedError(_("Scheduler only understands" - " Compute nodes (for now)")) - - num_instances = request_spec.get('num_instances', 1) - instance_type = request_spec['instance_type'] - - weighted = [] - host_list = None - - for i in xrange(num_instances): - # Filter local hosts based on requirements ... - # - # The first pass through here will pass 'None' as the - # host_list.. which tells the filter to build the full - # list of hosts. - # On a 2nd pass, the filter can modify the host_list with - # any updates it needs to make based on resources that - # may have been consumed from a previous build.. - host_list = self.filter_hosts(topic, request_spec, host_list) - if not host_list: - LOG.warn(_("Filter returned no hosts after processing " - "%(i)d of %(num_instances)d instances") % locals()) - break - - # then weigh the selected hosts. - # weighted = [{weight=weight, hostname=hostname, - # capabilities=capabs}, ...] - weights = self.weigh_hosts(topic, request_spec, host_list) - weights.sort(key=operator.itemgetter('weight')) - best_weight = weights[0] - weighted.append(best_weight) - self.consume_resources(topic, best_weight['capabilities'], - instance_type) - - # Next, tack on the best weights from the child zones ... + msg = _("Scheduler only understands Compute nodes (for now)") + raise NotImplementedError(msg) + + # Get all available hosts. + all_hosts = self.zone_manager.service_states.iteritems() + print "-"*88 + ss = self.zone_manager.service_states + print ss + print "KEYS", ss.keys() + print "-"*88 + + unfiltered_hosts = [(host, services[host]) + for host, services in all_hosts + if topic in services[host]] + + # Filter local hosts based on requirements ... + filtered_hosts = self.filter_hosts(topic, request_spec, host_list) + if not filtered_hosts: + LOG.warn(_("No hosts available")) + return [] + + # weigh the selected hosts. + # weighted_hosts = [{weight=weight, hostname=hostname, + # capabilities=capabs}, ...] + weighted_hosts = self.weigh_hosts(topic, request_spec, filtered_hosts) + # Next, tack on the host weights from the child zones json_spec = json.dumps(request_spec) all_zones = db.zone_get_all(context) child_results = self._call_zone_method(context, "select", @@ -314,14 +301,13 @@ class AbstractScheduler(driver.Scheduler): # it later if needed. This implicitly builds a zone # path structure. host_dict = {"weight": weighting["weight"], - "child_zone": child_zone, - "child_blob": weighting["blob"]} - weighted.append(host_dict) - - weighted.sort(key=operator.itemgetter('weight')) - return weighted + "child_zone": child_zone, + "child_blob": weighting["blob"]} + weighted_hosts.append(host_dict) + weighted_hosts.sort(key=operator.itemgetter('weight')) + return weighted_hosts - def compute_filter(self, hostname, capabilities, request_spec): + def basic_ram_filter(self, hostname, capabilities, request_spec): """Return whether or not we can schedule to this compute node. Derived classes should override this and return True if the host is acceptable for scheduling. @@ -330,74 +316,20 @@ class AbstractScheduler(driver.Scheduler): requested_mem = instance_type['memory_mb'] * 1024 * 1024 return capabilities['host_memory_free'] >= requested_mem - def hold_filter_hosts(self, topic, request_spec, hosts=None): - """Filter the full host list (from the ZoneManager)""" - # NOTE(dabo): The logic used by the current _schedule() method - # is incorrect. Since this task is just to refactor the classes, - # I'm not fixing the logic now - that will be the next task. - # So for now this method is just renamed; afterwards this will - # become the filter_hosts() method, and the one below will - # be removed. - filter_name = request_spec.get('filter', None) - # Make sure that the requested filter is legitimate. - selected_filter = host_filter.choose_host_filter(filter_name) - - # TODO(sandy): We're only using InstanceType-based specs - # currently. Later we'll need to snoop for more detailed - # host filter requests. - instance_type = request_spec['instance_type'] - name, query = selected_filter.instance_type_to_filter(instance_type) - return selected_filter.filter_hosts(self.zone_manager, query) - def filter_hosts(self, topic, request_spec, host_list=None): - """Return a list of hosts which are acceptable for scheduling. - Return value should be a list of (hostname, capability_dict)s. - Derived classes may override this, but may find the - '_filter' function more appropriate. + """Filter the full host list returned from the ZoneManager. By default, + this method only applies the basic_ram_filter(), meaning all hosts + with at least enough RAM for the requested instance are returned. + + Override in subclasses to provide greater selectivity. """ - def _default_filter(self, hostname, capabilities, request_spec): - """Default filter function if there's no _filter""" - # NOTE(sirp): The default logic is the equivalent to - # AllHostsFilter - return True - - filter_func = getattr(self, '%s_filter' % topic, _default_filter) - - if host_list is None: - first_run = True - host_list = self.zone_manager.service_states.iteritems() - else: - first_run = False - - filtered_hosts = [] - for host, services in host_list: - if first_run: - if topic not in services: - continue - services = services[topic] - if filter_func(host, services, request_spec): - filtered_hosts.append((host, services)) - return filtered_hosts + return [(host, services) for host, services in host_list + if basic_ram_filter(host, services, request_spec)] def weigh_hosts(self, topic, request_spec, hosts): - """Derived classes may override this to provide more sophisticated - scheduling objectives + """This version assigns a weight of 1 to all hosts, making selection + of any host basically a random event. Override this method in your + subclass to add logic to prefer one potential host over another. """ - # NOTE(sirp): The default logic is the same as the NoopCostFunction return [dict(weight=1, hostname=hostname, capabilities=capabilities) for hostname, capabilities in hosts] - - def compute_consume(self, capabilities, instance_type): - """Consume compute resources for selected host""" - - requested_mem = max(instance_type['memory_mb'], 0) * 1024 * 1024 - capabilities['host_memory_free'] -= requested_mem - - def consume_resources(self, topic, capabilities, instance_type): - """Consume resources for a specific host. 'host' is a tuple - of the hostname and the services""" - - consume_func = getattr(self, '%s_consume' % topic, None) - if not consume_func: - return - consume_func(capabilities, instance_type) diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py new file mode 100644 index 000000000..43a6ab2b1 --- /dev/null +++ b/nova/scheduler/base_scheduler.py @@ -0,0 +1,403 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +The BaseScheduler is the base class Scheduler for creating instances +across zones. There are two expansion points to this class for: +1. Assigning Weights to hosts for requested instances +2. Filtering Hosts based on required instance capabilities +""" + +import operator +import json + +import M2Crypto + +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions + +from nova import crypto +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import rpc + +from nova.compute import api as compute_api +from nova.scheduler import api +from nova.scheduler import driver + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.scheduler.abstract_scheduler') + + +class InvalidBlob(exception.NovaException): + message = _("Ill-formed or incorrectly routed 'blob' data sent " + "to instance create request.") + + +class AbstractScheduler(driver.Scheduler): + """Base class for creating Schedulers that can work across any nova + deployment, from simple designs to multiply-nested zones. + """ + + def _call_zone_method(self, context, method, specs, zones): + """Call novaclient zone method. Broken out for testing.""" + return api.call_zone_method(context, method, specs=specs, zones=zones) + + def _provision_resource_locally(self, context, build_plan_item, + request_spec, kwargs): + """Create the requested resource in this Zone.""" + host = build_plan_item['hostname'] + base_options = request_spec['instance_properties'] + image = request_spec['image'] + + # TODO(sandy): I guess someone needs to add block_device_mapping + # support at some point? Also, OS API has no concept of security + # groups. + instance = compute_api.API().create_db_entry_for_new_instance(context, + image, base_options, None, []) + + instance_id = instance['id'] + kwargs['instance_id'] = instance_id + + rpc.cast(context, + db.queue_get_for(context, "compute", host), + {"method": "run_instance", + "args": kwargs}) + LOG.debug(_("Provisioning locally via compute node %(host)s") + % locals()) + + def _decrypt_blob(self, blob): + """Returns the decrypted blob or None if invalid. Broken out + for testing.""" + decryptor = crypto.decryptor(FLAGS.build_plan_encryption_key) + try: + json_entry = decryptor(blob) + return json.dumps(json_entry) + except M2Crypto.EVP.EVPError: + pass + return None + + def _ask_child_zone_to_create_instance(self, context, zone_info, + request_spec, kwargs): + """Once we have determined that the request should go to one + of our children, we need to fabricate a new POST /servers/ + call with the same parameters that were passed into us. + + Note that we have to reverse engineer from our args to get back the + image, flavor, ipgroup, etc. since the original call could have + come in from EC2 (which doesn't use these things).""" + + instance_type = request_spec['instance_type'] + instance_properties = request_spec['instance_properties'] + + name = instance_properties['display_name'] + image_ref = instance_properties['image_ref'] + meta = instance_properties['metadata'] + flavor_id = instance_type['flavorid'] + reservation_id = instance_properties['reservation_id'] + + files = kwargs['injected_files'] + ipgroup = None # Not supported in OS API ... yet + + child_zone = zone_info['child_zone'] + child_blob = zone_info['child_blob'] + zone = db.zone_get(context, child_zone) + url = zone.api_url + LOG.debug(_("Forwarding instance create call to child zone %(url)s" + ". ReservationID=%(reservation_id)s") + % locals()) + nova = None + try: + nova = novaclient.Client(zone.username, zone.password, None, url) + nova.authenticate() + except novaclient_exceptions.BadRequest, e: + raise exception.NotAuthorized(_("Bad credentials attempting " + "to talk to zone at %(url)s.") % locals()) + + nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files, + child_blob, reservation_id=reservation_id) + + def _provision_resource_from_blob(self, context, build_plan_item, + instance_id, request_spec, kwargs): + """Create the requested resource locally or in a child zone + based on what is stored in the zone blob info. + + Attempt to decrypt the blob to see if this request is: + 1. valid, and + 2. intended for this zone or a child zone. + + Note: If we have "blob" that means the request was passed + into us from a parent zone. If we have "child_blob" that + means we gathered the info from one of our children. + It's possible that, when we decrypt the 'blob' field, it + contains "child_blob" data. In which case we forward the + request.""" + + host_info = None + if "blob" in build_plan_item: + # Request was passed in from above. Is it for us? + host_info = self._decrypt_blob(build_plan_item['blob']) + elif "child_blob" in build_plan_item: + # Our immediate child zone provided this info ... + host_info = build_plan_item + + if not host_info: + raise InvalidBlob() + + # Valid data ... is it for us? + if 'child_zone' in host_info and 'child_blob' in host_info: + self._ask_child_zone_to_create_instance(context, host_info, + request_spec, kwargs) + else: + self._provision_resource_locally(context, host_info, request_spec, + kwargs) + + def _provision_resource(self, context, build_plan_item, instance_id, + request_spec, kwargs): + """Create the requested resource in this Zone or a child zone.""" + if "hostname" in build_plan_item: + self._provision_resource_locally(context, build_plan_item, + request_spec, kwargs) + return + + self._provision_resource_from_blob(context, build_plan_item, + instance_id, request_spec, kwargs) + + def _adjust_child_weights(self, child_results, zones): + """Apply the Scale and Offset values from the Zone definition + to adjust the weights returned from the child zones. Alters + child_results in place. + """ + for zone_id, result in child_results: + if not result: + continue + + assert isinstance(zone_id, int) + + for zone_rec in zones: + if zone_rec['id'] != zone_id: + continue + + for item in result: + try: + offset = zone_rec['weight_offset'] + scale = zone_rec['weight_scale'] + raw_weight = item['weight'] + cooked_weight = offset + scale * raw_weight + item['weight'] = cooked_weight + item['raw_weight'] = raw_weight + except KeyError: + LOG.exception(_("Bad child zone scaling values " + "for Zone: %(zone_id)s") % locals()) + + def schedule_run_instance(self, context, instance_id, request_spec, + *args, **kwargs): + """This method is called from nova.compute.api to provision + an instance. However we need to look at the parameters being + passed in to see if this is a request to: + 1. Create a Build Plan and then provision, or + 2. Use the Build Plan information in the request parameters + to simply create the instance (either in this zone or + a child zone). + """ + + # TODO(sandy): We'll have to look for richer specs at some point. + + blob = request_spec.get('blob') + if blob: + self._provision_resource(context, request_spec, instance_id, + request_spec, kwargs) + return None + + num_instances = request_spec.get('num_instances', 1) + LOG.debug(_("Attempting to build %(num_instances)d instance(s)") % + locals()) + + # Create build plan and provision ... + build_plan = self.select(context, request_spec) + if not build_plan: + raise driver.NoValidHost(_('No hosts were available')) + + for num in xrange(num_instances): + if not build_plan: + break + + build_plan_item = build_plan.pop(0) + self._provision_resource(context, build_plan_item, instance_id, + request_spec, kwargs) + + # Returning None short-circuits the routing to Compute (since + # we've already done it here) + return None + + def select(self, context, request_spec, *args, **kwargs): + """Select returns a list of weights and zone/host information + corresponding to the best hosts to service the request. Any + child zone information has been encrypted so as not to reveal + anything about the children. + """ + return self._schedule(context, "compute", request_spec, + *args, **kwargs) + + # TODO(sandy): We're only focused on compute instances right now, + # so we don't implement the default "schedule()" method required + # of Schedulers. + def schedule(self, context, topic, request_spec, *args, **kwargs): + """The schedule() contract requires we return the one + best-suited host for this request. + """ + raise driver.NoValidHost(_('No hosts were available')) + + def _schedule(self, context, topic, request_spec, *args, **kwargs): + """Returns a list of hosts that meet the required specs, + ordered by their fitness. + """ + + if topic != "compute": + raise NotImplementedError(_("Scheduler only understands" + " Compute nodes (for now)")) + + num_instances = request_spec.get('num_instances', 1) + instance_type = request_spec['instance_type'] + + weighted = [] + host_list = None + + for i in xrange(num_instances): + # Filter local hosts based on requirements ... + # + # The first pass through here will pass 'None' as the + # host_list.. which tells the filter to build the full + # list of hosts. + # On a 2nd pass, the filter can modify the host_list with + # any updates it needs to make based on resources that + # may have been consumed from a previous build.. + host_list = self.filter_hosts(topic, request_spec, host_list) + if not host_list: + LOG.warn(_("Filter returned no hosts after processing " + "%(i)d of %(num_instances)d instances") % locals()) + break + + # then weigh the selected hosts. + # weighted = [{weight=weight, hostname=hostname, + # capabilities=capabs}, ...] + weights = self.weigh_hosts(topic, request_spec, host_list) + weights.sort(key=operator.itemgetter('weight')) + best_weight = weights[0] + weighted.append(best_weight) + self.consume_resources(topic, best_weight['capabilities'], + instance_type) + + # Next, tack on the best weights from the child zones ... + json_spec = json.dumps(request_spec) + all_zones = db.zone_get_all(context) + child_results = self._call_zone_method(context, "select", + specs=json_spec, zones=all_zones) + self._adjust_child_weights(child_results, all_zones) + for child_zone, result in child_results: + for weighting in result: + # Remember the child_zone so we can get back to + # it later if needed. This implicitly builds a zone + # path structure. + host_dict = {"weight": weighting["weight"], + "child_zone": child_zone, + "child_blob": weighting["blob"]} + weighted.append(host_dict) + + weighted.sort(key=operator.itemgetter('weight')) + return weighted + + def compute_filter(self, hostname, capabilities, request_spec): + """Return whether or not we can schedule to this compute node. + Derived classes should override this and return True if the host + is acceptable for scheduling. + """ + instance_type = request_spec['instance_type'] + requested_mem = instance_type['memory_mb'] * 1024 * 1024 + return capabilities['host_memory_free'] >= requested_mem + + def hold_filter_hosts(self, topic, request_spec, hosts=None): + """Filter the full host list (from the ZoneManager)""" + # NOTE(dabo): The logic used by the current _schedule() method + # is incorrect. Since this task is just to refactor the classes, + # I'm not fixing the logic now - that will be the next task. + # So for now this method is just renamed; afterwards this will + # become the filter_hosts() method, and the one below will + # be removed. + filter_name = request_spec.get('filter', None) + # Make sure that the requested filter is legitimate. + selected_filter = host_filter.choose_host_filter(filter_name) + + # TODO(sandy): We're only using InstanceType-based specs + # currently. Later we'll need to snoop for more detailed + # host filter requests. + instance_type = request_spec['instance_type'] + name, query = selected_filter.instance_type_to_filter(instance_type) + return selected_filter.filter_hosts(self.zone_manager, query) + + def filter_hosts(self, topic, request_spec, host_list=None): + """Return a list of hosts which are acceptable for scheduling. + Return value should be a list of (hostname, capability_dict)s. + Derived classes may override this, but may find the + '_filter' function more appropriate. + """ + def _default_filter(self, hostname, capabilities, request_spec): + """Default filter function if there's no _filter""" + # NOTE(sirp): The default logic is the equivalent to + # AllHostsFilter + return True + + filter_func = getattr(self, '%s_filter' % topic, _default_filter) + + if host_list is None: + first_run = True + host_list = self.zone_manager.service_states.iteritems() + else: + first_run = False + + filtered_hosts = [] + for host, services in host_list: + if first_run: + if topic not in services: + continue + services = services[topic] + if filter_func(host, services, request_spec): + filtered_hosts.append((host, services)) + return filtered_hosts + + def weigh_hosts(self, topic, request_spec, hosts): + """Derived classes may override this to provide more sophisticated + scheduling objectives + """ + # NOTE(sirp): The default logic is the same as the NoopCostFunction + return [dict(weight=1, hostname=hostname, capabilities=capabilities) + for hostname, capabilities in hosts] + + def compute_consume(self, capabilities, instance_type): + """Consume compute resources for selected host""" + + requested_mem = max(instance_type['memory_mb'], 0) * 1024 * 1024 + capabilities['host_memory_free'] -= requested_mem + + def consume_resources(self, topic, capabilities, instance_type): + """Consume resources for a specific host. 'host' is a tuple + of the hostname and the services""" + + consume_func = getattr(self, '%s_consume' % topic, None) + if not consume_func: + return + consume_func(capabilities, instance_type) -- cgit From 90c6641d47e9c1012b9fb3e53fe0da21ae3d42b7 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Fri, 12 Aug 2011 13:58:26 -0500 Subject: Created the filters directory in nova/scheduler --- nova/scheduler/__init__.py | 2 + nova/scheduler/abstract_scheduler.py | 30 +-- nova/scheduler/base_scheduler.py | 312 +-------------------- nova/scheduler/filters/__init__.py | 18 ++ nova/scheduler/filters/abstract_filter.py | 87 ++++++ nova/scheduler/filters/all_hosts_filter.py | 31 +++ nova/scheduler/filters/instance_type_filter.py | 86 ++++++ nova/scheduler/filters/json_filter.py | 141 ++++++++++ nova/scheduler/host_filter.py | 314 ---------------------- nova/tests/scheduler/test_abstract_scheduler.py | 3 + nova/tests/scheduler/test_host_filter.py | 4 +- nova/tests/scheduler/test_least_cost_scheduler.py | 7 +- 12 files changed, 391 insertions(+), 644 deletions(-) create mode 100644 nova/scheduler/filters/__init__.py create mode 100644 nova/scheduler/filters/abstract_filter.py create mode 100644 nova/scheduler/filters/all_hosts_filter.py create mode 100644 nova/scheduler/filters/instance_type_filter.py create mode 100644 nova/scheduler/filters/json_filter.py delete mode 100644 nova/scheduler/host_filter.py diff --git a/nova/scheduler/__init__.py b/nova/scheduler/__init__.py index 8359a7aeb..25078f015 100644 --- a/nova/scheduler/__init__.py +++ b/nova/scheduler/__init__.py @@ -21,5 +21,7 @@ .. automodule:: nova.scheduler :platform: Unix :synopsis: Module that picks a compute node to run a VM instance. +.. moduleauthor:: Sandy Walsh +.. moduleauthor:: Ed Leafe .. moduleauthor:: Chris Behrens """ diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index a6457cc50..a0734f322 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -269,18 +269,13 @@ class AbstractScheduler(driver.Scheduler): # Get all available hosts. all_hosts = self.zone_manager.service_states.iteritems() - print "-"*88 - ss = self.zone_manager.service_states - print ss - print "KEYS", ss.keys() - print "-"*88 - - unfiltered_hosts = [(host, services[host]) + unfiltered_hosts = [(host, services[topic]) for host, services in all_hosts - if topic in services[host]] + if topic in services] # Filter local hosts based on requirements ... - filtered_hosts = self.filter_hosts(topic, request_spec, host_list) + filtered_hosts = self.filter_hosts(topic, request_spec, + unfiltered_hosts) if not filtered_hosts: LOG.warn(_("No hosts available")) return [] @@ -307,22 +302,19 @@ class AbstractScheduler(driver.Scheduler): weighted_hosts.sort(key=operator.itemgetter('weight')) return weighted_hosts - def basic_ram_filter(self, hostname, capabilities, request_spec): - """Return whether or not we can schedule to this compute node. - Derived classes should override this and return True if the host - is acceptable for scheduling. - """ - instance_type = request_spec['instance_type'] - requested_mem = instance_type['memory_mb'] * 1024 * 1024 - return capabilities['host_memory_free'] >= requested_mem - - def filter_hosts(self, topic, request_spec, host_list=None): + def filter_hosts(self, topic, request_spec, host_list): """Filter the full host list returned from the ZoneManager. By default, this method only applies the basic_ram_filter(), meaning all hosts with at least enough RAM for the requested instance are returned. Override in subclasses to provide greater selectivity. """ + def basic_ram_filter(hostname, capabilities, request_spec): + """Only return hosts with sufficient available RAM.""" + instance_type = request_spec['instance_type'] + requested_mem = instance_type['memory_mb'] * 1024 * 1024 + return capabilities['host_memory_free'] >= requested_mem + return [(host, services) for host, services in host_list if basic_ram_filter(host, services, request_spec)] diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py index 43a6ab2b1..e14ee349e 100644 --- a/nova/scheduler/base_scheduler.py +++ b/nova/scheduler/base_scheduler.py @@ -20,324 +20,22 @@ across zones. There are two expansion points to this class for: 2. Filtering Hosts based on required instance capabilities """ -import operator -import json - -import M2Crypto - -from novaclient import v1_1 as novaclient -from novaclient import exceptions as novaclient_exceptions - -from nova import crypto -from nova import db -from nova import exception from nova import flags from nova import log as logging -from nova import rpc -from nova.compute import api as compute_api -from nova.scheduler import api -from nova.scheduler import driver +from nova.scheduler import abstract_scheduler +from nova.scheduler import host_filter FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.scheduler.abstract_scheduler') - +LOG = logging.getLogger('nova.scheduler.base_scheduler') -class InvalidBlob(exception.NovaException): - message = _("Ill-formed or incorrectly routed 'blob' data sent " - "to instance create request.") - -class AbstractScheduler(driver.Scheduler): +class BaseScheduler(abstract_scheduler.AbstractScheduler): """Base class for creating Schedulers that can work across any nova deployment, from simple designs to multiply-nested zones. """ - - def _call_zone_method(self, context, method, specs, zones): - """Call novaclient zone method. Broken out for testing.""" - return api.call_zone_method(context, method, specs=specs, zones=zones) - - def _provision_resource_locally(self, context, build_plan_item, - request_spec, kwargs): - """Create the requested resource in this Zone.""" - host = build_plan_item['hostname'] - base_options = request_spec['instance_properties'] - image = request_spec['image'] - - # TODO(sandy): I guess someone needs to add block_device_mapping - # support at some point? Also, OS API has no concept of security - # groups. - instance = compute_api.API().create_db_entry_for_new_instance(context, - image, base_options, None, []) - - instance_id = instance['id'] - kwargs['instance_id'] = instance_id - - rpc.cast(context, - db.queue_get_for(context, "compute", host), - {"method": "run_instance", - "args": kwargs}) - LOG.debug(_("Provisioning locally via compute node %(host)s") - % locals()) - - def _decrypt_blob(self, blob): - """Returns the decrypted blob or None if invalid. Broken out - for testing.""" - decryptor = crypto.decryptor(FLAGS.build_plan_encryption_key) - try: - json_entry = decryptor(blob) - return json.dumps(json_entry) - except M2Crypto.EVP.EVPError: - pass - return None - - def _ask_child_zone_to_create_instance(self, context, zone_info, - request_spec, kwargs): - """Once we have determined that the request should go to one - of our children, we need to fabricate a new POST /servers/ - call with the same parameters that were passed into us. - - Note that we have to reverse engineer from our args to get back the - image, flavor, ipgroup, etc. since the original call could have - come in from EC2 (which doesn't use these things).""" - - instance_type = request_spec['instance_type'] - instance_properties = request_spec['instance_properties'] - - name = instance_properties['display_name'] - image_ref = instance_properties['image_ref'] - meta = instance_properties['metadata'] - flavor_id = instance_type['flavorid'] - reservation_id = instance_properties['reservation_id'] - - files = kwargs['injected_files'] - ipgroup = None # Not supported in OS API ... yet - - child_zone = zone_info['child_zone'] - child_blob = zone_info['child_blob'] - zone = db.zone_get(context, child_zone) - url = zone.api_url - LOG.debug(_("Forwarding instance create call to child zone %(url)s" - ". ReservationID=%(reservation_id)s") - % locals()) - nova = None - try: - nova = novaclient.Client(zone.username, zone.password, None, url) - nova.authenticate() - except novaclient_exceptions.BadRequest, e: - raise exception.NotAuthorized(_("Bad credentials attempting " - "to talk to zone at %(url)s.") % locals()) - - nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files, - child_blob, reservation_id=reservation_id) - - def _provision_resource_from_blob(self, context, build_plan_item, - instance_id, request_spec, kwargs): - """Create the requested resource locally or in a child zone - based on what is stored in the zone blob info. - - Attempt to decrypt the blob to see if this request is: - 1. valid, and - 2. intended for this zone or a child zone. - - Note: If we have "blob" that means the request was passed - into us from a parent zone. If we have "child_blob" that - means we gathered the info from one of our children. - It's possible that, when we decrypt the 'blob' field, it - contains "child_blob" data. In which case we forward the - request.""" - - host_info = None - if "blob" in build_plan_item: - # Request was passed in from above. Is it for us? - host_info = self._decrypt_blob(build_plan_item['blob']) - elif "child_blob" in build_plan_item: - # Our immediate child zone provided this info ... - host_info = build_plan_item - - if not host_info: - raise InvalidBlob() - - # Valid data ... is it for us? - if 'child_zone' in host_info and 'child_blob' in host_info: - self._ask_child_zone_to_create_instance(context, host_info, - request_spec, kwargs) - else: - self._provision_resource_locally(context, host_info, request_spec, - kwargs) - - def _provision_resource(self, context, build_plan_item, instance_id, - request_spec, kwargs): - """Create the requested resource in this Zone or a child zone.""" - if "hostname" in build_plan_item: - self._provision_resource_locally(context, build_plan_item, - request_spec, kwargs) - return - - self._provision_resource_from_blob(context, build_plan_item, - instance_id, request_spec, kwargs) - - def _adjust_child_weights(self, child_results, zones): - """Apply the Scale and Offset values from the Zone definition - to adjust the weights returned from the child zones. Alters - child_results in place. - """ - for zone_id, result in child_results: - if not result: - continue - - assert isinstance(zone_id, int) - - for zone_rec in zones: - if zone_rec['id'] != zone_id: - continue - - for item in result: - try: - offset = zone_rec['weight_offset'] - scale = zone_rec['weight_scale'] - raw_weight = item['weight'] - cooked_weight = offset + scale * raw_weight - item['weight'] = cooked_weight - item['raw_weight'] = raw_weight - except KeyError: - LOG.exception(_("Bad child zone scaling values " - "for Zone: %(zone_id)s") % locals()) - - def schedule_run_instance(self, context, instance_id, request_spec, - *args, **kwargs): - """This method is called from nova.compute.api to provision - an instance. However we need to look at the parameters being - passed in to see if this is a request to: - 1. Create a Build Plan and then provision, or - 2. Use the Build Plan information in the request parameters - to simply create the instance (either in this zone or - a child zone). - """ - - # TODO(sandy): We'll have to look for richer specs at some point. - - blob = request_spec.get('blob') - if blob: - self._provision_resource(context, request_spec, instance_id, - request_spec, kwargs) - return None - - num_instances = request_spec.get('num_instances', 1) - LOG.debug(_("Attempting to build %(num_instances)d instance(s)") % - locals()) - - # Create build plan and provision ... - build_plan = self.select(context, request_spec) - if not build_plan: - raise driver.NoValidHost(_('No hosts were available')) - - for num in xrange(num_instances): - if not build_plan: - break - - build_plan_item = build_plan.pop(0) - self._provision_resource(context, build_plan_item, instance_id, - request_spec, kwargs) - - # Returning None short-circuits the routing to Compute (since - # we've already done it here) - return None - - def select(self, context, request_spec, *args, **kwargs): - """Select returns a list of weights and zone/host information - corresponding to the best hosts to service the request. Any - child zone information has been encrypted so as not to reveal - anything about the children. - """ - return self._schedule(context, "compute", request_spec, - *args, **kwargs) - - # TODO(sandy): We're only focused on compute instances right now, - # so we don't implement the default "schedule()" method required - # of Schedulers. - def schedule(self, context, topic, request_spec, *args, **kwargs): - """The schedule() contract requires we return the one - best-suited host for this request. - """ - raise driver.NoValidHost(_('No hosts were available')) - - def _schedule(self, context, topic, request_spec, *args, **kwargs): - """Returns a list of hosts that meet the required specs, - ordered by their fitness. - """ - - if topic != "compute": - raise NotImplementedError(_("Scheduler only understands" - " Compute nodes (for now)")) - - num_instances = request_spec.get('num_instances', 1) - instance_type = request_spec['instance_type'] - - weighted = [] - host_list = None - - for i in xrange(num_instances): - # Filter local hosts based on requirements ... - # - # The first pass through here will pass 'None' as the - # host_list.. which tells the filter to build the full - # list of hosts. - # On a 2nd pass, the filter can modify the host_list with - # any updates it needs to make based on resources that - # may have been consumed from a previous build.. - host_list = self.filter_hosts(topic, request_spec, host_list) - if not host_list: - LOG.warn(_("Filter returned no hosts after processing " - "%(i)d of %(num_instances)d instances") % locals()) - break - - # then weigh the selected hosts. - # weighted = [{weight=weight, hostname=hostname, - # capabilities=capabs}, ...] - weights = self.weigh_hosts(topic, request_spec, host_list) - weights.sort(key=operator.itemgetter('weight')) - best_weight = weights[0] - weighted.append(best_weight) - self.consume_resources(topic, best_weight['capabilities'], - instance_type) - - # Next, tack on the best weights from the child zones ... - json_spec = json.dumps(request_spec) - all_zones = db.zone_get_all(context) - child_results = self._call_zone_method(context, "select", - specs=json_spec, zones=all_zones) - self._adjust_child_weights(child_results, all_zones) - for child_zone, result in child_results: - for weighting in result: - # Remember the child_zone so we can get back to - # it later if needed. This implicitly builds a zone - # path structure. - host_dict = {"weight": weighting["weight"], - "child_zone": child_zone, - "child_blob": weighting["blob"]} - weighted.append(host_dict) - - weighted.sort(key=operator.itemgetter('weight')) - return weighted - - def compute_filter(self, hostname, capabilities, request_spec): - """Return whether or not we can schedule to this compute node. - Derived classes should override this and return True if the host - is acceptable for scheduling. - """ - instance_type = request_spec['instance_type'] - requested_mem = instance_type['memory_mb'] * 1024 * 1024 - return capabilities['host_memory_free'] >= requested_mem - - def hold_filter_hosts(self, topic, request_spec, hosts=None): + def filter_hosts(self, topic, request_spec, hosts=None): """Filter the full host list (from the ZoneManager)""" - # NOTE(dabo): The logic used by the current _schedule() method - # is incorrect. Since this task is just to refactor the classes, - # I'm not fixing the logic now - that will be the next task. - # So for now this method is just renamed; afterwards this will - # become the filter_hosts() method, and the one below will - # be removed. filter_name = request_spec.get('filter', None) # Make sure that the requested filter is legitimate. selected_filter = host_filter.choose_host_filter(filter_name) diff --git a/nova/scheduler/filters/__init__.py b/nova/scheduler/filters/__init__.py new file mode 100644 index 000000000..27160ca0a --- /dev/null +++ b/nova/scheduler/filters/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from all_hosts_filter import AllHostsFilter +from instance_type_filter import InstanceTypeFilter +from json_filter import JsonFilter diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py new file mode 100644 index 000000000..05982820f --- /dev/null +++ b/nova/scheduler/filters/abstract_filter.py @@ -0,0 +1,87 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +The Host Filter classes are a way to ensure that only hosts that are +appropriate are considered when creating a new instance. Hosts that are +either incompatible or insufficient to accept a newly-requested instance +are removed by Host Filter classes from consideration. Those that pass +the filter are then passed on for weighting or other process for ordering. + +Three filters are included: AllHosts, Flavor & JSON. AllHosts just +returns the full, unfiltered list of hosts. Flavor is a hard coded +matching mechanism based on flavor criteria and JSON is an ad-hoc +filter grammar. + +Why JSON? The requests for instances may come in through the +REST interface from a user or a parent Zone. +Currently Flavors and/or InstanceTypes are used for +specifing the type of instance desired. Specific Nova users have +noted a need for a more expressive way of specifying instances. +Since we don't want to get into building full DSL this is a simple +form as an example of how this could be done. In reality, most +consumers will use the more rigid filters such as FlavorFilter. +""" + +import json + +from nova import exception +from nova import flags +from nova import log as logging + +import nova.scheduler + + +LOG = logging.getLogger('nova.scheduler.host_filter') +FLAGS = flags.FLAGS +flags.DEFINE_string('default_host_filter', + 'nova.scheduler.host_filter.AllHostsFilter', + 'Which filter to use for filtering hosts') + + +class AbstractHostFilter(object): + """Base class for host filters.""" + def instance_type_to_filter(self, instance_type): + """Convert instance_type into a filter for most common use-case.""" + raise NotImplementedError() + + def filter_hosts(self, zone_manager, query): + """Return a list of hosts that fulfill the filter.""" + raise NotImplementedError() + + def _full_name(self): + """module.classname of the filter.""" + return "%s.%s" % (self.__module__, self.__class__.__name__) + + +def _get_filters(): + from nova.scheduler import filters + return [itm for itm in dir(filters) + if issubclass(itm, AbstractHostFilter)] + + +def choose_host_filter(filter_name=None): + """Since the caller may specify which filter to use we need + to have an authoritative list of what is permissible. This + function checks the filter name against a predefined set + of acceptable filters. + """ + if not filter_name: + filter_name = FLAGS.default_host_filter + for filter_class in _get_filters(): + host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) + if host_match == filter_name: + return filter_class() + raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) diff --git a/nova/scheduler/filters/all_hosts_filter.py b/nova/scheduler/filters/all_hosts_filter.py new file mode 100644 index 000000000..bc4acfd1a --- /dev/null +++ b/nova/scheduler/filters/all_hosts_filter.py @@ -0,0 +1,31 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import nova.scheduler + + +class AllHostsFilter(nova.scheduler.host_filter.AbstractHostFilter): + """NOP host filter. Returns all hosts in ZoneManager.""" + def instance_type_to_filter(self, instance_type): + """Return anything to prevent base-class from raising + exception. + """ + return (self._full_name(), instance_type) + + def filter_hosts(self, zone_manager, query): + """Return a list of hosts from ZoneManager list.""" + return [(host, services) + for host, services in zone_manager.service_states.iteritems()] diff --git a/nova/scheduler/filters/instance_type_filter.py b/nova/scheduler/filters/instance_type_filter.py new file mode 100644 index 000000000..03ffc46c6 --- /dev/null +++ b/nova/scheduler/filters/instance_type_filter.py @@ -0,0 +1,86 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from nova.scheduler import host_filter + + +class InstanceTypeFilter(host_filter.AbstractHostFilter): + """HostFilter hard-coded to work with InstanceType records.""" + def instance_type_to_filter(self, instance_type): + """Use instance_type to filter hosts.""" + return (self._full_name(), instance_type) + + def _satisfies_extra_specs(self, capabilities, instance_type): + """Check that the capabilities provided by the compute service + satisfy the extra specs associated with the instance type""" + if 'extra_specs' not in instance_type: + return True + # NOTE(lorinh): For now, we are just checking exact matching on the + # values. Later on, we want to handle numerical + # values so we can represent things like number of GPU cards + try: + for key, value in instance_type['extra_specs'].iteritems(): + if capabilities[key] != value: + return False + except KeyError: + return False + return True + + def filter_hosts(self, zone_manager, query): + """Return a list of hosts that can create instance_type.""" + instance_type = query + selected_hosts = [] + for host, services in zone_manager.service_states.iteritems(): + capabilities = services.get('compute', {}) + if not capabilities: + continue + host_ram_mb = capabilities['host_memory_free'] + disk_bytes = capabilities['disk_available'] + spec_ram = instance_type['memory_mb'] + spec_disk = instance_type['local_gb'] + extra_specs = instance_type['extra_specs'] + + if ((host_ram_mb >= spec_ram) and (disk_bytes >= spec_disk) and + self._satisfies_extra_specs(capabilities, instance_type)): + selected_hosts.append((host, capabilities)) + return selected_hosts + + +# host entries (currently) are like: +# {'host_name-description': 'Default install of XenServer', +# 'host_hostname': 'xs-mini', +# 'host_memory_total': 8244539392, +# 'host_memory_overhead': 184225792, +# 'host_memory_free': 3868327936, +# 'host_memory_free_computed': 3840843776, +# 'host_other_config': {}, +# 'host_ip_address': '192.168.1.109', +# 'host_cpu_info': {}, +# 'disk_available': 32954957824, +# 'disk_total': 50394562560, +# 'disk_used': 17439604736, +# 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f', +# 'host_name_label': 'xs-mini'} + +# instance_type table has: +# name = Column(String(255), unique=True) +# memory_mb = Column(Integer) +# vcpus = Column(Integer) +# local_gb = Column(Integer) +# flavorid = Column(Integer, unique=True) +# swap = Column(Integer, nullable=False, default=0) +# rxtx_quota = Column(Integer, nullable=False, default=0) +# rxtx_cap = Column(Integer, nullable=False, default=0) diff --git a/nova/scheduler/filters/json_filter.py b/nova/scheduler/filters/json_filter.py new file mode 100644 index 000000000..358abdc4d --- /dev/null +++ b/nova/scheduler/filters/json_filter.py @@ -0,0 +1,141 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import operator + +from nova.scheduler import host_filter + + +class JsonFilter(host_filter.AbstractHostFilter): + """Host Filter to allow simple JSON-based grammar for + selecting hosts. + """ + def _op_comp(self, args, op): + """Returns True if the specified operator can successfully + compare the first item in the args with all the rest. Will + return False if only one item is in the list. + """ + if len(args) < 2: + return False + bad = [arg for arg in args[1:] + if not op(args[0], arg)] + return not bool(bad) + + def _equals(self, args): + """First term is == all the other terms.""" + return self._op_comp(args, operator.eq) + + def _less_than(self, args): + """First term is < all the other terms.""" + return self._op_comp(args, operator.lt) + + def _greater_than(self, args): + """First term is > all the other terms.""" + return self._op_comp(args, operator.gt) + + def _in(self, args): + """First term is in set of remaining terms""" + return self._op_comp(args, operator.contains) + + def _less_than_equal(self, args): + """First term is <= all the other terms.""" + return self._op_comp(args, operator.le) + + def _greater_than_equal(self, args): + """First term is >= all the other terms.""" + return self._op_comp(args, operator.ge) + + def _not(self, args): + """Flip each of the arguments.""" + return [not arg for arg in args] + + def _or(self, args): + """True if any arg is True.""" + return any(args) + + def _and(self, args): + """True if all args are True.""" + return all(args) + + commands = { + '=': _equals, + '<': _less_than, + '>': _greater_than, + 'in': _in, + '<=': _less_than_equal, + '>=': _greater_than_equal, + 'not': _not, + 'or': _or, + 'and': _and, + } + + def instance_type_to_filter(self, instance_type): + """Convert instance_type into JSON filter object.""" + required_ram = instance_type['memory_mb'] + required_disk = instance_type['local_gb'] + query = ['and', + ['>=', '$compute.host_memory_free', required_ram], + ['>=', '$compute.disk_available', required_disk]] + return (self._full_name(), json.dumps(query)) + + def _parse_string(self, string, host, services): + """Strings prefixed with $ are capability lookups in the + form '$service.capability[.subcap*]'. + """ + if not string: + return None + if not string.startswith("$"): + return string + + path = string[1:].split(".") + for item in path: + services = services.get(item, None) + if not services: + return None + return services + + def _process_filter(self, zone_manager, query, host, services): + """Recursively parse the query structure.""" + if not query: + return True + cmd = query[0] + method = self.commands[cmd] + cooked_args = [] + for arg in query[1:]: + if isinstance(arg, list): + arg = self._process_filter(zone_manager, arg, host, services) + elif isinstance(arg, basestring): + arg = self._parse_string(arg, host, services) + if arg is not None: + cooked_args.append(arg) + result = method(self, cooked_args) + return result + + def filter_hosts(self, zone_manager, query): + """Return a list of hosts that can fulfill the requirements + specified in the query. + """ + expanded = json.loads(query) + filtered_hosts = [] + for host, services in zone_manager.service_states.iteritems(): + result = self._process_filter(zone_manager, expanded, host, + services) + if isinstance(result, list): + # If any succeeded, include the host + result = any(result) + if result: + filtered_hosts.append((host, services)) + return filtered_hosts diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py deleted file mode 100644 index 45a8f40d8..000000000 --- a/nova/scheduler/host_filter.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright (c) 2011 Openstack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -The Host Filter classes are a way to ensure that only hosts that are -appropriate are considered when creating a new instance. Hosts that are -either incompatible or insufficient to accept a newly-requested instance -are removed by Host Filter classes from consideration. Those that pass -the filter are then passed on for weighting or other process for ordering. - -Three filters are included: AllHosts, Flavor & JSON. AllHosts just -returns the full, unfiltered list of hosts. Flavor is a hard coded -matching mechanism based on flavor criteria and JSON is an ad-hoc -filter grammar. - -Why JSON? The requests for instances may come in through the -REST interface from a user or a parent Zone. -Currently Flavors and/or InstanceTypes are used for -specifing the type of instance desired. Specific Nova users have -noted a need for a more expressive way of specifying instances. -Since we don't want to get into building full DSL this is a simple -form as an example of how this could be done. In reality, most -consumers will use the more rigid filters such as FlavorFilter. -""" - -import json - -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - -LOG = logging.getLogger('nova.scheduler.host_filter') - -FLAGS = flags.FLAGS -flags.DEFINE_string('default_host_filter', - 'nova.scheduler.host_filter.AllHostsFilter', - 'Which filter to use for filtering hosts.') - - -class HostFilter(object): - """Base class for host filters.""" - - def instance_type_to_filter(self, instance_type): - """Convert instance_type into a filter for most common use-case.""" - raise NotImplementedError() - - def filter_hosts(self, zone_manager, query): - """Return a list of hosts that fulfill the filter.""" - raise NotImplementedError() - - def _full_name(self): - """module.classname of the filter.""" - return "%s.%s" % (self.__module__, self.__class__.__name__) - - -class AllHostsFilter(HostFilter): - """ NOP host filter. Returns all hosts in ZoneManager. - This essentially does what the old Scheduler+Chance used - to give us. - """ - - def instance_type_to_filter(self, instance_type): - """Return anything to prevent base-class from raising - exception.""" - return (self._full_name(), instance_type) - - def filter_hosts(self, zone_manager, query): - """Return a list of hosts from ZoneManager list.""" - return [(host, services) - for host, services in zone_manager.service_states.iteritems()] - - -class InstanceTypeFilter(HostFilter): - """HostFilter hard-coded to work with InstanceType records.""" - - def instance_type_to_filter(self, instance_type): - """Use instance_type to filter hosts.""" - return (self._full_name(), instance_type) - - def _satisfies_extra_specs(self, capabilities, instance_type): - """Check that the capabilities provided by the compute service - satisfy the extra specs associated with the instance type""" - - if 'extra_specs' not in instance_type: - return True - - # Note(lorinh): For now, we are just checking exact matching on the - # values. Later on, we want to handle numerical - # values so we can represent things like number of GPU cards - - try: - for key, value in instance_type['extra_specs'].iteritems(): - if capabilities[key] != value: - return False - except KeyError: - return False - - return True - - def filter_hosts(self, zone_manager, query): - """Return a list of hosts that can create instance_type.""" - instance_type = query - selected_hosts = [] - for host, services in zone_manager.service_states.iteritems(): - capabilities = services.get('compute', {}) - host_ram_mb = capabilities['host_memory_free'] - disk_bytes = capabilities['disk_available'] - spec_ram = instance_type['memory_mb'] - spec_disk = instance_type['local_gb'] - extra_specs = instance_type['extra_specs'] - - if ((host_ram_mb >= spec_ram) and (disk_bytes >= spec_disk) and - self._satisfies_extra_specs(capabilities, instance_type)): - selected_hosts.append((host, capabilities)) - return selected_hosts - -#host entries (currently) are like: -# {'host_name-description': 'Default install of XenServer', -# 'host_hostname': 'xs-mini', -# 'host_memory_total': 8244539392, -# 'host_memory_overhead': 184225792, -# 'host_memory_free': 3868327936, -# 'host_memory_free_computed': 3840843776, -# 'host_other_config': {}, -# 'host_ip_address': '192.168.1.109', -# 'host_cpu_info': {}, -# 'disk_available': 32954957824, -# 'disk_total': 50394562560, -# 'disk_used': 17439604736, -# 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f', -# 'host_name_label': 'xs-mini'} - -# instance_type table has: -#name = Column(String(255), unique=True) -#memory_mb = Column(Integer) -#vcpus = Column(Integer) -#local_gb = Column(Integer) -#flavorid = Column(Integer, unique=True) -#swap = Column(Integer, nullable=False, default=0) -#rxtx_quota = Column(Integer, nullable=False, default=0) -#rxtx_cap = Column(Integer, nullable=False, default=0) - - -class JsonFilter(HostFilter): - """Host Filter to allow simple JSON-based grammar for - selecting hosts. - """ - - def _equals(self, args): - """First term is == all the other terms.""" - if len(args) < 2: - return False - lhs = args[0] - for rhs in args[1:]: - if lhs != rhs: - return False - return True - - def _less_than(self, args): - """First term is < all the other terms.""" - if len(args) < 2: - return False - lhs = args[0] - for rhs in args[1:]: - if lhs >= rhs: - return False - return True - - def _greater_than(self, args): - """First term is > all the other terms.""" - if len(args) < 2: - return False - lhs = args[0] - for rhs in args[1:]: - if lhs <= rhs: - return False - return True - - def _in(self, args): - """First term is in set of remaining terms""" - if len(args) < 2: - return False - return args[0] in args[1:] - - def _less_than_equal(self, args): - """First term is <= all the other terms.""" - if len(args) < 2: - return False - lhs = args[0] - for rhs in args[1:]: - if lhs > rhs: - return False - return True - - def _greater_than_equal(self, args): - """First term is >= all the other terms.""" - if len(args) < 2: - return False - lhs = args[0] - for rhs in args[1:]: - if lhs < rhs: - return False - return True - - def _not(self, args): - """Flip each of the arguments.""" - if len(args) == 0: - return False - return [not arg for arg in args] - - def _or(self, args): - """True if any arg is True.""" - return True in args - - def _and(self, args): - """True if all args are True.""" - return False not in args - - commands = { - '=': _equals, - '<': _less_than, - '>': _greater_than, - 'in': _in, - '<=': _less_than_equal, - '>=': _greater_than_equal, - 'not': _not, - 'or': _or, - 'and': _and, - } - - def instance_type_to_filter(self, instance_type): - """Convert instance_type into JSON filter object.""" - required_ram = instance_type['memory_mb'] - required_disk = instance_type['local_gb'] - query = ['and', - ['>=', '$compute.host_memory_free', required_ram], - ['>=', '$compute.disk_available', required_disk]] - return (self._full_name(), json.dumps(query)) - - def _parse_string(self, string, host, services): - """Strings prefixed with $ are capability lookups in the - form '$service.capability[.subcap*]' - """ - if not string: - return None - if string[0] != '$': - return string - - path = string[1:].split('.') - for item in path: - services = services.get(item, None) - if not services: - return None - return services - - def _process_filter(self, zone_manager, query, host, services): - """Recursively parse the query structure.""" - if len(query) == 0: - return True - cmd = query[0] - method = self.commands[cmd] # Let exception fly. - cooked_args = [] - for arg in query[1:]: - if isinstance(arg, list): - arg = self._process_filter(zone_manager, arg, host, services) - elif isinstance(arg, basestring): - arg = self._parse_string(arg, host, services) - if arg != None: - cooked_args.append(arg) - result = method(self, cooked_args) - return result - - def filter_hosts(self, zone_manager, query): - """Return a list of hosts that can fulfill filter.""" - expanded = json.loads(query) - hosts = [] - for host, services in zone_manager.service_states.iteritems(): - r = self._process_filter(zone_manager, expanded, host, services) - if isinstance(r, list): - r = True in r - if r: - hosts.append((host, services)) - return hosts - - -FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter] - - -def choose_host_filter(filter_name=None): - """Since the caller may specify which filter to use we need - to have an authoritative list of what is permissible. This - function checks the filter name against a predefined set - of acceptable filters. - """ - if not filter_name: - filter_name = FLAGS.default_host_filter - for filter_class in FILTERS: - host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) - if host_match == filter_name: - return filter_class() - raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) diff --git a/nova/tests/scheduler/test_abstract_scheduler.py b/nova/tests/scheduler/test_abstract_scheduler.py index f4f5cc233..aa97e2344 100644 --- a/nova/tests/scheduler/test_abstract_scheduler.py +++ b/nova/tests/scheduler/test_abstract_scheduler.py @@ -77,6 +77,9 @@ class FakeZoneManager(zone_manager.ZoneManager): 'host3': { 'compute': {'host_memory_free': 3221225472}, }, + 'host4': { + 'compute': {'host_memory_free': 999999999}, + }, } diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py index 7e664d3f9..818be2f45 100644 --- a/nova/tests/scheduler/test_host_filter.py +++ b/nova/tests/scheduler/test_host_filter.py @@ -20,7 +20,7 @@ import json from nova import exception from nova import test -from nova.scheduler import host_filter +from nova.scheduler import filters class FakeZoneManager: @@ -55,7 +55,7 @@ class HostFilterTestCase(test.TestCase): def setUp(self): super(HostFilterTestCase, self).setUp() - default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter' + default_host_filter = 'nova.scheduler.filteris.AllHostsFilter' self.flags(default_host_filter=default_host_filter) self.instance_type = dict(name='tiny', memory_mb=50, diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py index de7581d0a..16ec4420b 100644 --- a/nova/tests/scheduler/test_least_cost_scheduler.py +++ b/nova/tests/scheduler/test_least_cost_scheduler.py @@ -122,11 +122,14 @@ class LeastCostSchedulerTestCase(test.TestCase): self.flags(least_cost_scheduler_cost_functions=[ 'nova.scheduler.least_cost.compute_fill_first_cost_fn'], compute_fill_first_cost_fn_weight=1) - num = 1 instance_type = {'memory_mb': 1024} request_spec = {'instance_type': instance_type} - hosts = self.sched.filter_hosts('compute', request_spec, None) + all_hosts = self.sched.zone_manager.service_states.iteritems() + all_hosts = [(host, services["compute"]) + for host, services in all_hosts + if "compute" in services] + hosts = self.sched.filter_hosts('compute', request_spec, host_list) expected = [] for idx, (hostname, caps) in enumerate(hosts): -- cgit From d940fa4619584dac967176d045407f0919da0a74 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Fri, 12 Aug 2011 16:19:46 -0500 Subject: end of day --- nova/scheduler/filters/__init__.py | 1 + nova/scheduler/filters/abstract_filter.py | 54 +------------------ nova/scheduler/filters/all_hosts_filter.py | 3 +- nova/scheduler/filters/instance_type_filter.py | 5 +- nova/scheduler/filters/json_filter.py | 39 ++++++++++---- nova/scheduler/host_filter.py | 75 ++++++++++++++++++++++++++ nova/tests/scheduler/test_host_filter.py | 34 ++++++------ 7 files changed, 129 insertions(+), 82 deletions(-) create mode 100644 nova/scheduler/host_filter.py diff --git a/nova/scheduler/filters/__init__.py b/nova/scheduler/filters/__init__.py index 27160ca0a..4c9187c5a 100644 --- a/nova/scheduler/filters/__init__.py +++ b/nova/scheduler/filters/__init__.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from abstract_filter import AbstractHostFilter from all_hosts_filter import AllHostsFilter from instance_type_filter import InstanceTypeFilter from json_filter import JsonFilter diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py index 05982820f..fe5610923 100644 --- a/nova/scheduler/filters/abstract_filter.py +++ b/nova/scheduler/filters/abstract_filter.py @@ -13,44 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -""" -The Host Filter classes are a way to ensure that only hosts that are -appropriate are considered when creating a new instance. Hosts that are -either incompatible or insufficient to accept a newly-requested instance -are removed by Host Filter classes from consideration. Those that pass -the filter are then passed on for weighting or other process for ordering. - -Three filters are included: AllHosts, Flavor & JSON. AllHosts just -returns the full, unfiltered list of hosts. Flavor is a hard coded -matching mechanism based on flavor criteria and JSON is an ad-hoc -filter grammar. - -Why JSON? The requests for instances may come in through the -REST interface from a user or a parent Zone. -Currently Flavors and/or InstanceTypes are used for -specifing the type of instance desired. Specific Nova users have -noted a need for a more expressive way of specifying instances. -Since we don't want to get into building full DSL this is a simple -form as an example of how this could be done. In reality, most -consumers will use the more rigid filters such as FlavorFilter. -""" - -import json - -from nova import exception -from nova import flags -from nova import log as logging import nova.scheduler +from nova import flags - -LOG = logging.getLogger('nova.scheduler.host_filter') FLAGS = flags.FLAGS flags.DEFINE_string('default_host_filter', - 'nova.scheduler.host_filter.AllHostsFilter', + 'nova.scheduler.filters.AllHostsFilter', 'Which filter to use for filtering hosts') - class AbstractHostFilter(object): """Base class for host filters.""" def instance_type_to_filter(self, instance_type): @@ -64,24 +35,3 @@ class AbstractHostFilter(object): def _full_name(self): """module.classname of the filter.""" return "%s.%s" % (self.__module__, self.__class__.__name__) - - -def _get_filters(): - from nova.scheduler import filters - return [itm for itm in dir(filters) - if issubclass(itm, AbstractHostFilter)] - - -def choose_host_filter(filter_name=None): - """Since the caller may specify which filter to use we need - to have an authoritative list of what is permissible. This - function checks the filter name against a predefined set - of acceptable filters. - """ - if not filter_name: - filter_name = FLAGS.default_host_filter - for filter_class in _get_filters(): - host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) - if host_match == filter_name: - return filter_class() - raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) diff --git a/nova/scheduler/filters/all_hosts_filter.py b/nova/scheduler/filters/all_hosts_filter.py index bc4acfd1a..e80d829ca 100644 --- a/nova/scheduler/filters/all_hosts_filter.py +++ b/nova/scheduler/filters/all_hosts_filter.py @@ -15,9 +15,10 @@ import nova.scheduler +from nova.scheduler.filters import abstract_filter -class AllHostsFilter(nova.scheduler.host_filter.AbstractHostFilter): +class AllHostsFilter(abstract_filter.AbstractHostFilter): """NOP host filter. Returns all hosts in ZoneManager.""" def instance_type_to_filter(self, instance_type): """Return anything to prevent base-class from raising diff --git a/nova/scheduler/filters/instance_type_filter.py b/nova/scheduler/filters/instance_type_filter.py index 03ffc46c6..62b9ee414 100644 --- a/nova/scheduler/filters/instance_type_filter.py +++ b/nova/scheduler/filters/instance_type_filter.py @@ -14,10 +14,11 @@ # under the License. -from nova.scheduler import host_filter +import nova.scheduler +from nova.scheduler.filters import abstract_filter -class InstanceTypeFilter(host_filter.AbstractHostFilter): +class InstanceTypeFilter(abstract_filter.AbstractHostFilter): """HostFilter hard-coded to work with InstanceType records.""" def instance_type_to_filter(self, instance_type): """Use instance_type to filter hosts.""" diff --git a/nova/scheduler/filters/json_filter.py b/nova/scheduler/filters/json_filter.py index 358abdc4d..889b96915 100644 --- a/nova/scheduler/filters/json_filter.py +++ b/nova/scheduler/filters/json_filter.py @@ -14,49 +14,64 @@ # under the License. +import json import operator -from nova.scheduler import host_filter +import nova.scheduler +from nova.scheduler.filters import abstract_filter +def debug(*args): + with file("/tmp/debug", "a") as dbg: + msg = " ".join([str(arg) for arg in args]) + dbg.write("%s\n" % msg) -class JsonFilter(host_filter.AbstractHostFilter): + +class JsonFilter(abstract_filter.AbstractHostFilter): """Host Filter to allow simple JSON-based grammar for selecting hosts. """ - def _op_comp(self, args, op): + def _op_compare(self, args, op): """Returns True if the specified operator can successfully compare the first item in the args with all the rest. Will return False if only one item is in the list. """ if len(args) < 2: return False - bad = [arg for arg in args[1:] - if not op(args[0], arg)] + if op is operator.contains: + debug("ARGS", type(args), args) + debug("op", op) + debug("REVERSED!!!") + # operator.contains reverses the param order. + bad = [arg for arg in args[1:] + if not op(args, args[0])] + else: + bad = [arg for arg in args[1:] + if not op(args[0], arg)] return not bool(bad) def _equals(self, args): """First term is == all the other terms.""" - return self._op_comp(args, operator.eq) + return self._op_compare(args, operator.eq) def _less_than(self, args): """First term is < all the other terms.""" - return self._op_comp(args, operator.lt) + return self._op_compare(args, operator.lt) def _greater_than(self, args): """First term is > all the other terms.""" - return self._op_comp(args, operator.gt) + return self._op_compare(args, operator.gt) def _in(self, args): """First term is in set of remaining terms""" - return self._op_comp(args, operator.contains) + return self._op_compare(args, operator.contains) def _less_than_equal(self, args): """First term is <= all the other terms.""" - return self._op_comp(args, operator.le) + return self._op_compare(args, operator.le) def _greater_than_equal(self, args): """First term is >= all the other terms.""" - return self._op_comp(args, operator.ge) + return self._op_compare(args, operator.ge) def _not(self, args): """Flip each of the arguments.""" @@ -129,6 +144,8 @@ class JsonFilter(host_filter.AbstractHostFilter): specified in the query. """ expanded = json.loads(query) + + debug("expanded", type(expanded), expanded) filtered_hosts = [] for host, services in zone_manager.service_states.iteritems(): result = self._process_filter(zone_manager, expanded, host, diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py new file mode 100644 index 000000000..f5191f5c9 --- /dev/null +++ b/nova/scheduler/host_filter.py @@ -0,0 +1,75 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +The Host Filter classes are a way to ensure that only hosts that are +appropriate are considered when creating a new instance. Hosts that are +either incompatible or insufficient to accept a newly-requested instance +are removed by Host Filter classes from consideration. Those that pass +the filter are then passed on for weighting or other process for ordering. + +Three filters are included: AllHosts, Flavor & JSON. AllHosts just +returns the full, unfiltered list of hosts. Flavor is a hard coded +matching mechanism based on flavor criteria and JSON is an ad-hoc +filter grammar. + +Why JSON? The requests for instances may come in through the +REST interface from a user or a parent Zone. +Currently Flavors and/or InstanceTypes are used for +specifing the type of instance desired. Specific Nova users have +noted a need for a more expressive way of specifying instances. +Since we don't want to get into building full DSL this is a simple +form as an example of how this could be done. In reality, most +consumers will use the more rigid filters such as FlavorFilter. +""" + +import json +import types + +from nova import exception +from nova import flags +from nova import log as logging + +import nova.scheduler + + +LOG = logging.getLogger('nova.scheduler.host_filter') +FLAGS = flags.FLAGS + + +def _get_filters(): + from nova.scheduler import filters + def get_itm(nm): + return getattr(filters, nm) + + return [get_itm(itm) for itm in dir(filters) + if (type(get_itm(itm)) is types.TypeType) + and issubclass(get_itm(itm), filters.AbstractHostFilter)] + + +def choose_host_filter(filter_name=None): + """Since the caller may specify which filter to use we need + to have an authoritative list of what is permissible. This + function checks the filter name against a predefined set + of acceptable filters. + """ + if not filter_name: + filter_name = FLAGS.default_host_filter + for filter_class in _get_filters(): + host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) + if (host_match.startswith("nova.scheduler.filters") and + (host_match.split(".")[-1] == filter_name)): + return filter_class() + raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py index 818be2f45..a64b25138 100644 --- a/nova/tests/scheduler/test_host_filter.py +++ b/nova/tests/scheduler/test_host_filter.py @@ -20,6 +20,7 @@ import json from nova import exception from nova import test +from nova.scheduler import host_filter from nova.scheduler import filters @@ -55,7 +56,7 @@ class HostFilterTestCase(test.TestCase): def setUp(self): super(HostFilterTestCase, self).setUp() - default_host_filter = 'nova.scheduler.filteris.AllHostsFilter' + default_host_filter = 'AllHostsFilter' self.flags(default_host_filter=default_host_filter) self.instance_type = dict(name='tiny', memory_mb=50, @@ -98,13 +99,10 @@ class HostFilterTestCase(test.TestCase): def test_choose_filter(self): # Test default filter ... hf = host_filter.choose_host_filter() - self.assertEquals(hf._full_name(), - 'nova.scheduler.host_filter.AllHostsFilter') + self.assertEquals(hf._full_name().split(".")[-1], 'AllHostsFilter') # Test valid filter ... - hf = host_filter.choose_host_filter( - 'nova.scheduler.host_filter.InstanceTypeFilter') - self.assertEquals(hf._full_name(), - 'nova.scheduler.host_filter.InstanceTypeFilter') + hf = host_filter.choose_host_filter('InstanceTypeFilter') + self.assertEquals(hf._full_name().split(".")[-1], 'InstanceTypeFilter') # Test invalid filter ... try: host_filter.choose_host_filter('does not exist') @@ -113,7 +111,7 @@ class HostFilterTestCase(test.TestCase): pass def test_all_host_filter(self): - hf = host_filter.AllHostsFilter() + hf = filters.AllHostsFilter() cooked = hf.instance_type_to_filter(self.instance_type) hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(10, len(hosts)) @@ -121,11 +119,10 @@ class HostFilterTestCase(test.TestCase): self.assertTrue(host.startswith('host')) def test_instance_type_filter(self): - hf = host_filter.InstanceTypeFilter() + hf = filters.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', - name) + self.assertEquals(name.split(".")[-1], 'InstanceTypeFilter') hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -134,21 +131,20 @@ class HostFilterTestCase(test.TestCase): self.assertEquals('host10', just_hosts[5]) def test_instance_type_filter_extra_specs(self): - hf = host_filter.InstanceTypeFilter() + hf = filters.InstanceTypeFilter() # filter all hosts that can support 50 ram and 500 disk name, cooked = hf.instance_type_to_filter(self.gpu_instance_type) - self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', - name) + self.assertEquals(name.split(".")[-1], 'InstanceTypeFilter') hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(1, len(hosts)) just_hosts = [host for host, caps in hosts] self.assertEquals('host07', just_hosts[0]) def test_json_filter(self): - hf = host_filter.JsonFilter() + hf = filters.JsonFilter() # filter all hosts that can support 50 ram and 500 disk name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals('nova.scheduler.host_filter.JsonFilter', name) + self.assertEquals(name.split(".")[-1], 'JsonFilter') hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(6, len(hosts)) just_hosts = [host for host, caps in hosts] @@ -191,6 +187,12 @@ class HostFilterTestCase(test.TestCase): raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] cooked = json.dumps(raw) + def debug(*args): + with file("/tmp/debug", "a") as dbg: + msg = " ".join([str(arg) for arg in args]) + dbg.write("%s\n" % msg) + + debug("cooked", cooked, type(cooked)) hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(5, len(hosts)) -- cgit From 8666aca320ce95840a378231bfe81bc4e759df6e Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Mon, 15 Aug 2011 11:50:54 -0700 Subject: Fixed merging issue --- nova/api/openstack/create_instance_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 7e9d48c02..c8798536e 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -164,7 +164,7 @@ class CreateInstanceHelper(object): reservation_id=reservation_id, min_count=min_count, max_count=max_count, - user_data=user_data)) + user_data=user_data, availability_zone=availability_zone)) except quota.QuotaError as error: self._handle_quota_error(error) -- cgit From 066b675e3ce5c2bd67dde124cbe01b68bd1eded8 Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 15 Aug 2011 13:22:14 -0700 Subject: fix bug which DescribeInstances in EC2 api was returning deleted instances --- nova/db/sqlalchemy/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e5d35a20b..e7b71d494 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1209,7 +1209,8 @@ 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')) + options(joinedload('instance_type')).\ + filter_by(deleted=can_read_deleted(context)) # 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. -- cgit From f06f80591a41f5d1b373677937bbbcddcfb0bb7c Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 15 Aug 2011 13:48:09 -0700 Subject: added cloud unit test for describe_instances to ensure doesn't return deleted instances --- nova/tests/test_cloud.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index b2afc53c9..07a35c447 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -487,6 +487,16 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def test_describe_instances_deleted(self): + args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args) + inst2 = db.instance_create(self.context, args) + db.instance_destroy(self.context, inst1.id) + result = self.cloud.describe_instances(self.context) + result = result['reservationSet'][0]['instancesSet'] + print result + self.assertEqual(1, len(result)) + def _block_device_mapping_create(self, instance_id, mappings): volumes = [] for bdm in mappings: -- cgit From 3e561f148fcba627f8fbd4ab1089f426fbc2e61b Mon Sep 17 00:00:00 2001 From: John Tran Date: Mon, 15 Aug 2011 13:58:44 -0700 Subject: adding sqlalchemi api tests for test_instance_get_all_by_filter to ensure doesn't return deleted instances --- nova/tests/test_cloud.py | 1 - nova/tests/test_db_api.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 07a35c447..39358eeff 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -494,7 +494,6 @@ class CloudTestCase(test.TestCase): db.instance_destroy(self.context, inst1.id) result = self.cloud.describe_instances(self.context) result = result['reservationSet'][0]['instancesSet'] - print result self.assertEqual(1, len(result)) def _block_device_mapping_create(self, instance_id, mappings): diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 0c07cbb7c..ed363d1be 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -76,3 +76,18 @@ class DbApiTestCase(test.TestCase): self.assertEqual(instance['id'], result['id']) self.assertEqual(result['fixed_ips'][0]['floating_ips'][0].address, '1.2.1.2') + + def test_instance_get_all_by_filters(self): + args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args) + inst2 = db.instance_create(self.context, args) + result = db.instance_get_all_by_filters(self.context, {}) + self.assertTrue(2, len(result)) + + def test_instance_get_all_by_filters_deleted(self): + args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args) + inst2 = db.instance_create(self.context, args) + db.instance_destroy(self.context, inst1.id) + result = db.instance_get_all_by_filters(self.context, {}) + self.assertTrue(1, len(result)) -- cgit From 55dd18f30eee4f4a75c825c33d4a78b2ef94be4a Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 15 Aug 2011 17:09:39 -0500 Subject: got tests passing with logic changes --- nova/scheduler/abstract_scheduler.py | 53 +++--- nova/scheduler/base_scheduler.py | 50 +----- nova/scheduler/filters/__init__.py | 17 ++ nova/scheduler/filters/abstract_filter.py | 2 +- nova/scheduler/filters/json_filter.py | 14 +- nova/scheduler/host_filter.py | 25 +-- nova/scheduler/least_cost.py | 134 +++++++-------- nova/tests/scheduler/test_host_filter.py | 2 - nova/tests/scheduler/test_least_cost_scheduler.py | 16 +- nova/tests/test_host_filter.py | 200 ---------------------- 10 files changed, 125 insertions(+), 388 deletions(-) delete mode 100644 nova/tests/test_host_filter.py diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index a0734f322..2f1ede0a4 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -45,20 +45,19 @@ LOG = logging.getLogger('nova.scheduler.abstract_scheduler') class InvalidBlob(exception.NovaException): message = _("Ill-formed or incorrectly routed 'blob' data sent " - "to instance create request.") + "to instance create request.") class AbstractScheduler(driver.Scheduler): """Base class for creating Schedulers that can work across any nova deployment, from simple designs to multiply-nested zones. """ - def _call_zone_method(self, context, method, specs, zones): """Call novaclient zone method. Broken out for testing.""" return api.call_zone_method(context, method, specs=specs, zones=zones) def _provision_resource_locally(self, context, build_plan_item, - request_spec, kwargs): + request_spec, kwargs): """Create the requested resource in this Zone.""" host = build_plan_item['hostname'] base_options = request_spec['instance_properties'] @@ -68,21 +67,21 @@ class AbstractScheduler(driver.Scheduler): # support at some point? Also, OS API has no concept of security # groups. instance = compute_api.API().create_db_entry_for_new_instance(context, - image, base_options, None, []) + image, base_options, None, []) instance_id = instance['id'] kwargs['instance_id'] = instance_id - rpc.cast(context, - db.queue_get_for(context, "compute", host), - {"method": "run_instance", - "args": kwargs}) + queue = db.queue_get_for(context, "compute", host) + params = {"method": "run_instance", "args": kwargs} + rpc.cast(context, queue, params) LOG.debug(_("Provisioning locally via compute node %(host)s") - % locals()) + % locals()) def _decrypt_blob(self, blob): """Returns the decrypted blob or None if invalid. Broken out - for testing.""" + for testing. + """ decryptor = crypto.decryptor(FLAGS.build_plan_encryption_key) try: json_entry = decryptor(blob) @@ -92,15 +91,15 @@ class AbstractScheduler(driver.Scheduler): return None def _ask_child_zone_to_create_instance(self, context, zone_info, - request_spec, kwargs): + request_spec, kwargs): """Once we have determined that the request should go to one of our children, we need to fabricate a new POST /servers/ call with the same parameters that were passed into us. Note that we have to reverse engineer from our args to get back the image, flavor, ipgroup, etc. since the original call could have - come in from EC2 (which doesn't use these things).""" - + come in from EC2 (which doesn't use these things). + """ instance_type = request_spec['instance_type'] instance_properties = request_spec['instance_properties'] @@ -109,30 +108,26 @@ class AbstractScheduler(driver.Scheduler): meta = instance_properties['metadata'] flavor_id = instance_type['flavorid'] reservation_id = instance_properties['reservation_id'] - files = kwargs['injected_files'] ipgroup = None # Not supported in OS API ... yet - child_zone = zone_info['child_zone'] child_blob = zone_info['child_blob'] zone = db.zone_get(context, child_zone) url = zone.api_url LOG.debug(_("Forwarding instance create call to child zone %(url)s" - ". ReservationID=%(reservation_id)s") - % locals()) + ". ReservationID=%(reservation_id)s") % locals()) nova = None try: nova = novaclient.Client(zone.username, zone.password, None, url) nova.authenticate() except novaclient_exceptions.BadRequest, e: raise exception.NotAuthorized(_("Bad credentials attempting " - "to talk to zone at %(url)s.") % locals()) - + "to talk to zone at %(url)s.") % locals()) nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files, - child_blob, reservation_id=reservation_id) + child_blob, reservation_id=reservation_id) def _provision_resource_from_blob(self, context, build_plan_item, - instance_id, request_spec, kwargs): + instance_id, request_spec, kwargs): """Create the requested resource locally or in a child zone based on what is stored in the zone blob info. @@ -145,8 +140,8 @@ class AbstractScheduler(driver.Scheduler): means we gathered the info from one of our children. It's possible that, when we decrypt the 'blob' field, it contains "child_blob" data. In which case we forward the - request.""" - + request. + """ host_info = None if "blob" in build_plan_item: # Request was passed in from above. Is it for us? @@ -161,21 +156,20 @@ class AbstractScheduler(driver.Scheduler): # Valid data ... is it for us? if 'child_zone' in host_info and 'child_blob' in host_info: self._ask_child_zone_to_create_instance(context, host_info, - request_spec, kwargs) + request_spec, kwargs) else: self._provision_resource_locally(context, host_info, request_spec, - kwargs) + kwargs) def _provision_resource(self, context, build_plan_item, instance_id, - request_spec, kwargs): + request_spec, kwargs): """Create the requested resource in this Zone or a child zone.""" if "hostname" in build_plan_item: self._provision_resource_locally(context, build_plan_item, - request_spec, kwargs) + request_spec, kwargs) return - self._provision_resource_from_blob(context, build_plan_item, - instance_id, request_spec, kwargs) + instance_id, request_spec, kwargs) def _adjust_child_weights(self, child_results, zones): """Apply the Scale and Offset values from the Zone definition @@ -231,7 +225,6 @@ class AbstractScheduler(driver.Scheduler): for num in xrange(num_instances): if not build_plan: break - build_plan_item = build_plan.pop(0) self._provision_resource(context, build_plan_item, instance_id, request_spec, kwargs) diff --git a/nova/scheduler/base_scheduler.py b/nova/scheduler/base_scheduler.py index e14ee349e..35e5af035 100644 --- a/nova/scheduler/base_scheduler.py +++ b/nova/scheduler/base_scheduler.py @@ -43,40 +43,13 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): # TODO(sandy): We're only using InstanceType-based specs # currently. Later we'll need to snoop for more detailed # host filter requests. - instance_type = request_spec['instance_type'] + instance_type = request_spec.get("instance_type", None) + if instance_type is None: + # No way to select; return the specified hosts + return hosts or [] name, query = selected_filter.instance_type_to_filter(instance_type) return selected_filter.filter_hosts(self.zone_manager, query) - def filter_hosts(self, topic, request_spec, host_list=None): - """Return a list of hosts which are acceptable for scheduling. - Return value should be a list of (hostname, capability_dict)s. - Derived classes may override this, but may find the - '_filter' function more appropriate. - """ - def _default_filter(self, hostname, capabilities, request_spec): - """Default filter function if there's no _filter""" - # NOTE(sirp): The default logic is the equivalent to - # AllHostsFilter - return True - - filter_func = getattr(self, '%s_filter' % topic, _default_filter) - - if host_list is None: - first_run = True - host_list = self.zone_manager.service_states.iteritems() - else: - first_run = False - - filtered_hosts = [] - for host, services in host_list: - if first_run: - if topic not in services: - continue - services = services[topic] - if filter_func(host, services, request_spec): - filtered_hosts.append((host, services)) - return filtered_hosts - def weigh_hosts(self, topic, request_spec, hosts): """Derived classes may override this to provide more sophisticated scheduling objectives @@ -84,18 +57,3 @@ class BaseScheduler(abstract_scheduler.AbstractScheduler): # NOTE(sirp): The default logic is the same as the NoopCostFunction return [dict(weight=1, hostname=hostname, capabilities=capabilities) for hostname, capabilities in hosts] - - def compute_consume(self, capabilities, instance_type): - """Consume compute resources for selected host""" - - requested_mem = max(instance_type['memory_mb'], 0) * 1024 * 1024 - capabilities['host_memory_free'] -= requested_mem - - def consume_resources(self, topic, capabilities, instance_type): - """Consume resources for a specific host. 'host' is a tuple - of the hostname and the services""" - - consume_func = getattr(self, '%s_consume' % topic, None) - if not consume_func: - return - consume_func(capabilities, instance_type) diff --git a/nova/scheduler/filters/__init__.py b/nova/scheduler/filters/__init__.py index 4c9187c5a..b86fb795f 100644 --- a/nova/scheduler/filters/__init__.py +++ b/nova/scheduler/filters/__init__.py @@ -13,6 +13,23 @@ # License for the specific language governing permissions and limitations # under the License. +""" +There are three filters included: AllHosts, InstanceType & JSON. + +AllHosts just returns the full, unfiltered list of hosts. +InstanceType is a hard coded matching mechanism based on flavor criteria. +JSON is an ad-hoc filter grammar. + +Why JSON? The requests for instances may come in through the +REST interface from a user or a parent Zone. +Currently InstanceTypes are used for specifing the type of instance desired. +Specific Nova users have noted a need for a more expressive way of specifying +instance requirements. Since we don't want to get into building full DSL, +this filter is a simple form as an example of how this could be done. +In reality, most consumers will use the more rigid filters such as the +InstanceType filter. +""" + from abstract_filter import AbstractHostFilter from all_hosts_filter import AllHostsFilter from instance_type_filter import InstanceTypeFilter diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py index fe5610923..d9d272130 100644 --- a/nova/scheduler/filters/abstract_filter.py +++ b/nova/scheduler/filters/abstract_filter.py @@ -19,7 +19,7 @@ from nova import flags FLAGS = flags.FLAGS flags.DEFINE_string('default_host_filter', - 'nova.scheduler.filters.AllHostsFilter', + 'AllHostsFilter', 'Which filter to use for filtering hosts') class AbstractHostFilter(object): diff --git a/nova/scheduler/filters/json_filter.py b/nova/scheduler/filters/json_filter.py index 889b96915..caf22f5d5 100644 --- a/nova/scheduler/filters/json_filter.py +++ b/nova/scheduler/filters/json_filter.py @@ -20,11 +20,6 @@ import operator import nova.scheduler from nova.scheduler.filters import abstract_filter -def debug(*args): - with file("/tmp/debug", "a") as dbg: - msg = " ".join([str(arg) for arg in args]) - dbg.write("%s\n" % msg) - class JsonFilter(abstract_filter.AbstractHostFilter): """Host Filter to allow simple JSON-based grammar for @@ -38,12 +33,7 @@ class JsonFilter(abstract_filter.AbstractHostFilter): if len(args) < 2: return False if op is operator.contains: - debug("ARGS", type(args), args) - debug("op", op) - debug("REVERSED!!!") - # operator.contains reverses the param order. - bad = [arg for arg in args[1:] - if not op(args, args[0])] + bad = not args[0] in args[1:] else: bad = [arg for arg in args[1:] if not op(args[0], arg)] @@ -144,8 +134,6 @@ class JsonFilter(abstract_filter.AbstractHostFilter): specified in the query. """ expanded = json.loads(query) - - debug("expanded", type(expanded), expanded) filtered_hosts = [] for host, services in zone_manager.service_states.iteritems(): result = self._process_filter(zone_manager, expanded, host, diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index f5191f5c9..be618f3f3 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -20,43 +20,32 @@ either incompatible or insufficient to accept a newly-requested instance are removed by Host Filter classes from consideration. Those that pass the filter are then passed on for weighting or other process for ordering. -Three filters are included: AllHosts, Flavor & JSON. AllHosts just -returns the full, unfiltered list of hosts. Flavor is a hard coded -matching mechanism based on flavor criteria and JSON is an ad-hoc -filter grammar. - -Why JSON? The requests for instances may come in through the -REST interface from a user or a parent Zone. -Currently Flavors and/or InstanceTypes are used for -specifing the type of instance desired. Specific Nova users have -noted a need for a more expressive way of specifying instances. -Since we don't want to get into building full DSL this is a simple -form as an example of how this could be done. In reality, most -consumers will use the more rigid filters such as FlavorFilter. +Filters are in the 'filters' directory that is off the 'scheduler' +directory of nova. Additional filters can be created and added to that +directory; be sure to add them to the filters/__init__.py file so that +they are part of the nova.schedulers.filters namespace. """ -import json import types from nova import exception from nova import flags -from nova import log as logging - import nova.scheduler -LOG = logging.getLogger('nova.scheduler.host_filter') FLAGS = flags.FLAGS def _get_filters(): + # Imported here to avoid circular imports from nova.scheduler import filters def get_itm(nm): return getattr(filters, nm) return [get_itm(itm) for itm in dir(filters) if (type(get_itm(itm)) is types.TypeType) - and issubclass(get_itm(itm), filters.AbstractHostFilter)] + and issubclass(get_itm(itm), filters.AbstractHostFilter) + and get_itm(itm) is not filters.AbstractHostFilter] def choose_host_filter(filter_name=None): diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py index a58b11289..903d786cd 100644 --- a/nova/scheduler/least_cost.py +++ b/nova/scheduler/least_cost.py @@ -22,14 +22,12 @@ The cost-function and weights are tabulated, and the host with the least cost is then selected for provisioning. """ -# TODO(dabo): This class will be removed in the next merge prop; it remains now -# because much of the code will be refactored into different classes. import collections from nova import flags from nova import log as logging -from nova.scheduler import abstract_scheduler +from nova.scheduler import base_scheduler from nova import utils from nova import exception @@ -37,14 +35,16 @@ LOG = logging.getLogger('nova.scheduler.least_cost') FLAGS = flags.FLAGS flags.DEFINE_list('least_cost_scheduler_cost_functions', - ['nova.scheduler.least_cost.noop_cost_fn'], - 'Which cost functions the LeastCostScheduler should use.') + ['nova.scheduler.least_cost.noop_cost_fn'], + 'Which cost functions the LeastCostScheduler should use.') # TODO(sirp): Once we have enough of these rules, we can break them out into a # cost_functions.py file (perhaps in a least_cost_scheduler directory) flags.DEFINE_integer('noop_cost_fn_weight', 1, - 'How much weight to give the noop cost function') + 'How much weight to give the noop cost function') +flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1, + 'How much weight to give the fill-first cost function') def noop_cost_fn(host): @@ -52,19 +52,64 @@ def noop_cost_fn(host): return 1 -flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1, - 'How much weight to give the fill-first cost function') - - def compute_fill_first_cost_fn(host): """Prefer hosts that have less ram available, filter_hosts will exclude - hosts that don't have enough ram""" - hostname, caps = host - free_mem = caps['host_memory_free'] + hosts that don't have enough ram. + """ + hostname, service = host + caps = service.get("compute", {}) + free_mem = caps.get("host_memory_free", 0) return free_mem -class LeastCostScheduler(abstract_scheduler.AbstractScheduler): +def normalize_list(L): + """Normalize an array of numbers such that each element satisfies: + 0 <= e <= 1 + """ + if not L: + return L + max_ = max(L) + if max_ > 0: + return [(float(e) / max_) for e in L] + return L + + +def weighted_sum(domain, weighted_fns, normalize=True): + """Use the weighted-sum method to compute a score for an array of objects. + Normalize the results of the objective-functions so that the weights are + meaningful regardless of objective-function's range. + + domain - input to be scored + weighted_fns - list of weights and functions like: + [(weight, objective-functions)] + + Returns an unsorted list of scores. To pair with hosts do: + zip(scores, hosts) + """ + # Table of form: + # { domain1: [score1, score2, ..., scoreM] + # ... + # domainN: [score1, score2, ..., scoreM] } + score_table = collections.defaultdict(list) + for weight, fn in weighted_fns: + scores = [fn(elem) for elem in domain] + if normalize: + norm_scores = normalize_list(scores) + else: + norm_scores = scores + for idx, score in enumerate(norm_scores): + weighted_score = score * weight + score_table[idx].append(weighted_score) + + # Sum rows in table to compute score for each element in domain + domain_scores = [] + for idx in sorted(score_table): + elem_score = sum(score_table[idx]) + domain_scores.append(elem_score) + return domain_scores + + +class LeastCostScheduler(base_scheduler.BaseScheduler): def __init__(self, *args, **kwargs): self.cost_fns_cache = {} super(LeastCostScheduler, self).__init__(*args, **kwargs) @@ -73,10 +118,8 @@ class LeastCostScheduler(abstract_scheduler.AbstractScheduler): """Returns a list of tuples containing weights and cost functions to use for weighing hosts """ - if topic in self.cost_fns_cache: return self.cost_fns_cache[topic] - cost_fns = [] for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions: if '.' in cost_fn_str: @@ -85,7 +128,6 @@ class LeastCostScheduler(abstract_scheduler.AbstractScheduler): short_name = cost_fn_str cost_fn_str = "%s.%s.%s" % ( __name__, self.__class__.__name__, short_name) - if not (short_name.startswith('%s_' % topic) or short_name.startswith('noop')): continue @@ -96,15 +138,14 @@ class LeastCostScheduler(abstract_scheduler.AbstractScheduler): cost_fn = utils.import_class(cost_fn_str) except exception.ClassNotFound: raise exception.SchedulerCostFunctionNotFound( - cost_fn_str=cost_fn_str) + cost_fn_str=cost_fn_str) try: flag_name = "%s_weight" % cost_fn.__name__ weight = getattr(FLAGS, flag_name) except AttributeError: raise exception.SchedulerWeightFlagNotFound( - flag_name=flag_name) - + flag_name=flag_name) cost_fns.append((weight, cost_fn)) self.cost_fns_cache[topic] = cost_fns @@ -114,13 +155,13 @@ class LeastCostScheduler(abstract_scheduler.AbstractScheduler): """Returns a list of dictionaries of form: [ {weight: weight, hostname: hostname, capabilities: capabs} ] """ - cost_fns = self.get_cost_fns(topic) costs = weighted_sum(domain=hosts, weighted_fns=cost_fns) weighted = [] weight_log = [] - for cost, (hostname, caps) in zip(costs, hosts): + for cost, (hostname, service) in zip(costs, hosts): + caps = service[topic] weight_log.append("%s: %s" % (hostname, "%.2f" % cost)) weight_dict = dict(weight=cost, hostname=hostname, capabilities=caps) @@ -128,52 +169,3 @@ class LeastCostScheduler(abstract_scheduler.AbstractScheduler): LOG.debug(_("Weighted Costs => %s") % weight_log) return weighted - - -def normalize_list(L): - """Normalize an array of numbers such that each element satisfies: - 0 <= e <= 1""" - if not L: - return L - max_ = max(L) - if max_ > 0: - return [(float(e) / max_) for e in L] - return L - - -def weighted_sum(domain, weighted_fns, normalize=True): - """Use the weighted-sum method to compute a score for an array of objects. - Normalize the results of the objective-functions so that the weights are - meaningful regardless of objective-function's range. - - domain - input to be scored - weighted_fns - list of weights and functions like: - [(weight, objective-functions)] - - Returns an unsorted list of scores. To pair with hosts do: - zip(scores, hosts) - """ - # Table of form: - # { domain1: [score1, score2, ..., scoreM] - # ... - # domainN: [score1, score2, ..., scoreM] } - score_table = collections.defaultdict(list) - for weight, fn in weighted_fns: - scores = [fn(elem) for elem in domain] - - if normalize: - norm_scores = normalize_list(scores) - else: - norm_scores = scores - - for idx, score in enumerate(norm_scores): - weighted_score = score * weight - score_table[idx].append(weighted_score) - - # Sum rows in table to compute score for each element in domain - domain_scores = [] - for idx in sorted(score_table): - elem_score = sum(score_table[idx]) - domain_scores.append(elem_score) - - return domain_scores diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py index a64b25138..a961b1b06 100644 --- a/nova/tests/scheduler/test_host_filter.py +++ b/nova/tests/scheduler/test_host_filter.py @@ -192,9 +192,7 @@ class HostFilterTestCase(test.TestCase): msg = " ".join([str(arg) for arg in args]) dbg.write("%s\n" % msg) - debug("cooked", cooked, type(cooked)) hosts = hf.filter_hosts(self.zone_manager, cooked) - self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] just_hosts.sort() diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py index 16ec4420b..d6eaaa223 100644 --- a/nova/tests/scheduler/test_least_cost_scheduler.py +++ b/nova/tests/scheduler/test_least_cost_scheduler.py @@ -15,6 +15,7 @@ """ Tests For Least Cost Scheduler """ +import copy from nova import test from nova.scheduler import least_cost @@ -81,7 +82,7 @@ class LeastCostSchedulerTestCase(test.TestCase): super(LeastCostSchedulerTestCase, self).tearDown() def assertWeights(self, expected, num, request_spec, hosts): - weighted = self.sched.weigh_hosts(num, request_spec, hosts) + weighted = self.sched.weigh_hosts("compute", request_spec, hosts) self.assertDictListMatch(weighted, expected, approx_equal=True) def test_no_hosts(self): @@ -125,19 +126,20 @@ class LeastCostSchedulerTestCase(test.TestCase): num = 1 instance_type = {'memory_mb': 1024} request_spec = {'instance_type': instance_type} - all_hosts = self.sched.zone_manager.service_states.iteritems() + svc_states = self.sched.zone_manager.service_states.iteritems() all_hosts = [(host, services["compute"]) - for host, services in all_hosts + for host, services in svc_states if "compute" in services] - hosts = self.sched.filter_hosts('compute', request_spec, host_list) + hosts = self.sched.filter_hosts('compute', request_spec, all_hosts) expected = [] - for idx, (hostname, caps) in enumerate(hosts): + for idx, (hostname, services) in enumerate(hosts): + caps = copy.deepcopy(services["compute"]) # Costs are normalized so over 10 hosts, each host with increasing # free ram will cost 1/N more. Since the lowest cost host has some # free ram, we add in the 1/N for the base_cost weight = 0.1 + (0.1 * idx) - weight_dict = dict(weight=weight, hostname=hostname) - expected.append(weight_dict) + wtd_dict = dict(hostname=hostname, weight=weight, capabilities=caps) + expected.append(wtd_dict) self.assertWeights(expected, num, request_spec, hosts) diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py deleted file mode 100644 index 3a1389a49..000000000 --- a/nova/tests/test_host_filter.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Tests For Scheduler Host Filters. -""" - -import json - -from nova import exception -from nova import test -from nova.scheduler import host_filter - - -class FakeZoneManager: - pass - - -class HostFilterTestCase(test.TestCase): - """Test case for host filters.""" - - def _host_caps(self, multiplier): - # Returns host capabilities in the following way: - # host1 = memory:free 10 (100max) - # disk:available 100 (1000max) - # hostN = memory:free 10 + 10N - # disk:available 100 + 100N - # in other words: hostN has more resources than host0 - # which means ... don't go above 10 hosts. - return {'host_name-description': 'XenServer %s' % multiplier, - 'host_hostname': 'xs-%s' % multiplier, - 'host_memory_total': 100, - 'host_memory_overhead': 10, - 'host_memory_free': 10 + multiplier * 10, - 'host_memory_free-computed': 10 + multiplier * 10, - 'host_other-config': {}, - 'host_ip_address': '192.168.1.%d' % (100 + multiplier), - 'host_cpu_info': {}, - 'disk_available': 100 + multiplier * 100, - 'disk_total': 1000, - 'disk_used': 0, - 'host_uuid': 'xxx-%d' % multiplier, - 'host_name-label': 'xs-%s' % multiplier} - - def setUp(self): - super(HostFilterTestCase, self).setUp() - default_host_filter = 'nova.scheduler.host_filter.AllHostsFilter' - self.flags(default_host_filter=default_host_filter) - self.instance_type = dict(name='tiny', - memory_mb=50, - vcpus=10, - local_gb=500, - flavorid=1, - swap=500, - rxtx_quota=30000, - rxtx_cap=200, - extra_specs={}) - - self.zone_manager = FakeZoneManager() - states = {} - for x in xrange(10): - states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)} - self.zone_manager.service_states = states - - def test_choose_filter(self): - # Test default filter ... - hf = host_filter.choose_host_filter() - self.assertEquals(hf._full_name(), - 'nova.scheduler.host_filter.AllHostsFilter') - # Test valid filter ... - hf = host_filter.choose_host_filter( - 'nova.scheduler.host_filter.InstanceTypeFilter') - self.assertEquals(hf._full_name(), - 'nova.scheduler.host_filter.InstanceTypeFilter') - # Test invalid filter ... - try: - host_filter.choose_host_filter('does not exist') - self.fail("Should not find host filter.") - except exception.SchedulerHostFilterNotFound: - pass - - def test_all_host_filter(self): - hf = host_filter.AllHostsFilter() - cooked = hf.instance_type_to_filter(self.instance_type) - hosts = hf.filter_hosts(self.zone_manager, cooked) - self.assertEquals(10, len(hosts)) - for host, capabilities in hosts: - self.assertTrue(host.startswith('host')) - - def test_instance_type_filter(self): - hf = host_filter.InstanceTypeFilter() - # filter all hosts that can support 50 ram and 500 disk - name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter', - name) - hosts = hf.filter_hosts(self.zone_manager, cooked) - self.assertEquals(6, len(hosts)) - just_hosts = [host for host, caps in hosts] - just_hosts.sort() - self.assertEquals('host05', just_hosts[0]) - self.assertEquals('host10', just_hosts[5]) - - def test_json_filter(self): - hf = host_filter.JsonFilter() - # filter all hosts that can support 50 ram and 500 disk - name, cooked = hf.instance_type_to_filter(self.instance_type) - self.assertEquals('nova.scheduler.host_filter.JsonFilter', name) - hosts = hf.filter_hosts(self.zone_manager, cooked) - self.assertEquals(6, len(hosts)) - just_hosts = [host for host, caps in hosts] - just_hosts.sort() - self.assertEquals('host05', just_hosts[0]) - self.assertEquals('host10', just_hosts[5]) - - # Try some custom queries - - raw = ['or', - ['and', - ['<', '$compute.host_memory_free', 30], - ['<', '$compute.disk_available', 300], - ], - ['and', - ['>', '$compute.host_memory_free', 70], - ['>', '$compute.disk_available', 700], - ], - ] - - cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) - - self.assertEquals(5, len(hosts)) - just_hosts = [host for host, caps in hosts] - just_hosts.sort() - for index, host in zip([1, 2, 8, 9, 10], just_hosts): - self.assertEquals('host%02d' % index, host) - - raw = ['not', - ['=', '$compute.host_memory_free', 30], - ] - cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) - - self.assertEquals(9, len(hosts)) - just_hosts = [host for host, caps in hosts] - just_hosts.sort() - for index, host in zip([1, 2, 4, 5, 6, 7, 8, 9, 10], just_hosts): - self.assertEquals('host%02d' % index, host) - - raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] - cooked = json.dumps(raw) - hosts = hf.filter_hosts(self.zone_manager, cooked) - - self.assertEquals(5, len(hosts)) - just_hosts = [host for host, caps in hosts] - just_hosts.sort() - for index, host in zip([2, 4, 6, 8, 10], just_hosts): - self.assertEquals('host%02d' % index, host) - - # Try some bogus input ... - raw = ['unknown command', ] - cooked = json.dumps(raw) - try: - hf.filter_hosts(self.zone_manager, cooked) - self.fail("Should give KeyError") - except KeyError, e: - pass - - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps([]))) - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps({}))) - self.assertTrue(hf.filter_hosts(self.zone_manager, json.dumps( - ['not', True, False, True, False]))) - - try: - hf.filter_hosts(self.zone_manager, json.dumps( - 'not', True, False, True, False)) - self.fail("Should give KeyError") - except KeyError, e: - pass - - self.assertFalse(hf.filter_hosts(self.zone_manager, - json.dumps(['=', '$foo', 100]))) - self.assertFalse(hf.filter_hosts(self.zone_manager, - json.dumps(['=', '$.....', 100]))) - self.assertFalse(hf.filter_hosts(self.zone_manager, - json.dumps( - ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]]))) - - self.assertFalse(hf.filter_hosts(self.zone_manager, - json.dumps(['=', {}, ['>', '$missing....foo']]))) -- cgit From 9a4b1deb5f9abdc88809ff80bccdfb503e66dccd Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Mon, 15 Aug 2011 15:09:42 -0700 Subject: Removed newly added userdatarequesthandler for OS API, there is no need to add this handler since the existing Ec2 API metadatarequesthandler does the same job --- etc/nova/api-paste.ini | 7 -- nova/api/__init__.py | 6 -- nova/api/ec2/__init__.py | 3 + nova/api/openstack/userdatarequesthandler.py | 110 --------------------- nova/network/linux_net.py | 5 - nova/tests/api/openstack/fakes.py | 2 - .../api/openstack/test_userdatarequesthandler.py | 80 --------------- 7 files changed, 3 insertions(+), 210 deletions(-) delete mode 100644 nova/api/openstack/userdatarequesthandler.py delete mode 100644 nova/tests/api/openstack/test_userdatarequesthandler.py diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 46a3b0af9..abe8c20c4 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -69,7 +69,6 @@ use = egg:Paste#urlmap /: osversions /v1.0: openstackapi10 /v1.1: openstackapi11 -/latest: osuserdata [pipeline:openstackapi10] pipeline = faultwrap auth ratelimit osapiapp10 @@ -77,9 +76,6 @@ pipeline = faultwrap auth ratelimit osapiapp10 [pipeline:openstackapi11] pipeline = faultwrap auth ratelimit extensions osapiapp11 -[pipeline:osuserdata] -pipeline = logrequest osappud - [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -103,6 +99,3 @@ pipeline = faultwrap osversionapp [app:osversionapp] paste.app_factory = nova.api.openstack.versions:Versions.factory - -[app:osappud] -paste.app_factory = nova.api.openstack.userdatarequesthandler:UserdataRequestHandler.factory diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 6e6b092b3..747015af5 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -15,9 +15,3 @@ # 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 - - -flags.DEFINE_boolean('use_forwarded_for', False, - 'Treat X-Forwarded-For as the canonical remote address. ' - 'Only enable this if you have a sanitizing proxy.') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 2e9278b52..96df97393 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -37,6 +37,9 @@ from nova.auth import manager FLAGS = flags.FLAGS LOG = logging.getLogger("nova.api") +flags.DEFINE_boolean('use_forwarded_for', False, + 'Treat X-Forwarded-For as the canonical remote address. ' + 'Only enable this if you have a sanitizing proxy.') flags.DEFINE_integer('lockout_attempts', 5, 'Number of failed auths before lockout.') flags.DEFINE_integer('lockout_minutes', 15, diff --git a/nova/api/openstack/userdatarequesthandler.py b/nova/api/openstack/userdatarequesthandler.py deleted file mode 100644 index f0205419b..000000000 --- a/nova/api/openstack/userdatarequesthandler.py +++ /dev/null @@ -1,110 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""User data request handler.""" - -import base64 -import webob.dec -import webob.exc - -from nova import log as logging -from nova import context -from nova import exception -from nova import db -from nova import flags -from nova import wsgi - - -LOG = logging.getLogger('nova.api.openstack.userdata') -FLAGS = flags.FLAGS - - -class Controller(object): - """ The server user-data API controller for the Openstack API """ - - def __init__(self): - super(Controller, self).__init__() - - @staticmethod - def _format_user_data(instance_ref): - return base64.b64decode(instance_ref['user_data']) - - def get_user_data(self, address): - ctxt = context.get_admin_context() - try: - instance_ref = db.instance_get_by_fixed_ip(ctxt, address) - except exception.NotFound: - instance_ref = None - if not instance_ref: - return None - - data = {'user-data': self._format_user_data(instance_ref)} - return data - - -class UserdataRequestHandler(wsgi.Application): - """Serve user-data from the OS API.""" - - def __init__(self): - self.cc = Controller() - - def print_data(self, data): - if isinstance(data, dict): - output = '' - for key in data: - if key == '_name': - continue - output += key - if isinstance(data[key], dict): - if '_name' in data[key]: - output += '=' + str(data[key]['_name']) - else: - output += '/' - output += '\n' - # Cut off last \n - return output[:-1] - elif isinstance(data, list): - return '\n'.join(data) - else: - return str(data) - - def lookup(self, path, data): - items = path.split('/') - for item in items: - if item: - if not isinstance(data, dict): - return data - if not item in data: - return None - data = data[item] - return data - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - remote_address = req.remote_addr - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - - data = self.cc.get_user_data(remote_address) - if data is None: - LOG.error(_('Failed to get user data for ip: %s'), remote_address) - raise webob.exc.HTTPNotFound() - data = self.lookup(req.path_info, data) - if data is None: - raise webob.exc.HTTPNotFound() - return self.print_data(data) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index d8fff8a32..4e1e1f85a 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -371,11 +371,6 @@ def metadata_forward(): '-p tcp -m tcp --dport 80 -j DNAT ' '--to-destination %s:%s' % \ (FLAGS.ec2_dmz_host, FLAGS.ec2_port)) - iptables_manager.ipv4['nat'].add_rule('PREROUTING', - '-s 0.0.0.0/0 -d 169.254.169.253/32 ' - '-p tcp -m tcp --dport 80 -j DNAT ' - '--to-destination %s:%s' % \ - (FLAGS.osapi_host, FLAGS.osapi_port)) iptables_manager.apply() diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index aa5aeef16..d11fbf788 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -36,7 +36,6 @@ from nova.api.openstack import auth from nova.api.openstack import extensions from nova.api.openstack import versions from nova.api.openstack import limits -from nova.api.openstack import userdatarequesthandler from nova.auth.manager import User, Project import nova.image.fake from nova.image import glance @@ -100,7 +99,6 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, mapper['/v1.0'] = api10 mapper['/v1.1'] = api11 mapper['/'] = openstack.FaultWrapper(versions.Versions()) - mapper['/latest'] = userdatarequesthandler.UserdataRequestHandler() return mapper diff --git a/nova/tests/api/openstack/test_userdatarequesthandler.py b/nova/tests/api/openstack/test_userdatarequesthandler.py deleted file mode 100644 index 0c63076b4..000000000 --- a/nova/tests/api/openstack/test_userdatarequesthandler.py +++ /dev/null @@ -1,80 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 -import json -import unittest -import webob - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova import test -from nova import log as logging - -from nova.tests.api.openstack import fakes - -LOG = logging.getLogger('nova.api.openstack.userdata') - -USER_DATA_STRING = ("This is an encoded string") -ENCODE_STRING = base64.b64encode(USER_DATA_STRING) - - -def return_server_by_address(context, address): - instance = {"user_data": ENCODE_STRING} - instance["fixed_ips"] = {"address": address, - "floating_ips": []} - return instance - - -def return_non_existing_server_by_address(context, address): - raise exception.NotFound() - - -class TestUserdatarequesthandler(test.TestCase): - - def setUp(self): - super(TestUserdatarequesthandler, self).setUp() - self.stubs.Set(db, 'instance_get_by_fixed_ip', - return_server_by_address) - - def test_user_data(self): - req = webob.Request.blank('/latest/user-data') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.body, USER_DATA_STRING) - - def test_user_data_non_existing_fixed_address(self): - self.stubs.Set(db, 'instance_get_by_fixed_ip', - return_non_existing_server_by_address) - self.flags(use_forwarded_for=False) - req = webob.Request.blank('/latest/user-data') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - - def test_user_data_invalid_url(self): - req = webob.Request.blank('/latest/user-data-invalid') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - - def test_user_data_with_use_forwarded_header(self): - self.flags(use_forwarded_for=True) - req = webob.Request.blank('/latest/user-data') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.body, USER_DATA_STRING) -- cgit From d8a156f9ed0729c4c5553fe3b28f6c3afb93d54f Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Mon, 15 Aug 2011 17:31:24 -0500 Subject: pep8 cleanup --- nova/compute/manager.py | 3 ++- nova/scheduler/abstract_scheduler.py | 6 +++--- nova/scheduler/filters/abstract_filter.py | 4 ++-- nova/scheduler/host_filter.py | 1 + nova/tests/scheduler/test_host_filter.py | 5 ----- nova/tests/scheduler/test_least_cost_scheduler.py | 3 ++- nova/virt/libvirt/connection.py | 3 ++- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 16b8e14b4..52fcf5c49 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1315,7 +1315,8 @@ class ComputeManager(manager.SchedulerDependentManager): # This nwfilter is necessary on the destination host. # In addition, this method is creating filtering rule # onto destination host. - self.driver.ensure_filtering_rules_for_instance(instance_ref, network_info) + self.driver.ensure_filtering_rules_for_instance(instance_ref, + network_info) # Preparation for block migration if block_migration: diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index 2f1ede0a4..77db67773 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -15,7 +15,7 @@ """ The AbsractScheduler is an abstract class Scheduler for creating instances -locally or across zones. Two methods should be overridden in order to +locally or across zones. Two methods should be overridden in order to customize the behavior: filter_hosts() and weigh_hosts(). The default behavior is to simply select all hosts and weight them the same. """ @@ -298,8 +298,8 @@ class AbstractScheduler(driver.Scheduler): def filter_hosts(self, topic, request_spec, host_list): """Filter the full host list returned from the ZoneManager. By default, this method only applies the basic_ram_filter(), meaning all hosts - with at least enough RAM for the requested instance are returned. - + with at least enough RAM for the requested instance are returned. + Override in subclasses to provide greater selectivity. """ def basic_ram_filter(hostname, capabilities, request_spec): diff --git a/nova/scheduler/filters/abstract_filter.py b/nova/scheduler/filters/abstract_filter.py index d9d272130..a1d00d562 100644 --- a/nova/scheduler/filters/abstract_filter.py +++ b/nova/scheduler/filters/abstract_filter.py @@ -18,10 +18,10 @@ import nova.scheduler from nova import flags FLAGS = flags.FLAGS -flags.DEFINE_string('default_host_filter', - 'AllHostsFilter', +flags.DEFINE_string('default_host_filter', 'AllHostsFilter', 'Which filter to use for filtering hosts') + class AbstractHostFilter(object): """Base class for host filters.""" def instance_type_to_filter(self, instance_type): diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index be618f3f3..4bc5158cc 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -39,6 +39,7 @@ FLAGS = flags.FLAGS def _get_filters(): # Imported here to avoid circular imports from nova.scheduler import filters + def get_itm(nm): return getattr(filters, nm) diff --git a/nova/tests/scheduler/test_host_filter.py b/nova/tests/scheduler/test_host_filter.py index a961b1b06..17431fc7e 100644 --- a/nova/tests/scheduler/test_host_filter.py +++ b/nova/tests/scheduler/test_host_filter.py @@ -187,11 +187,6 @@ class HostFilterTestCase(test.TestCase): raw = ['in', '$compute.host_memory_free', 20, 40, 60, 80, 100] cooked = json.dumps(raw) - def debug(*args): - with file("/tmp/debug", "a") as dbg: - msg = " ".join([str(arg) for arg in args]) - dbg.write("%s\n" % msg) - hosts = hf.filter_hosts(self.zone_manager, cooked) self.assertEquals(5, len(hosts)) just_hosts = [host for host, caps in hosts] diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py index d6eaaa223..af58de527 100644 --- a/nova/tests/scheduler/test_least_cost_scheduler.py +++ b/nova/tests/scheduler/test_least_cost_scheduler.py @@ -139,7 +139,8 @@ class LeastCostSchedulerTestCase(test.TestCase): # free ram will cost 1/N more. Since the lowest cost host has some # free ram, we add in the 1/N for the base_cost weight = 0.1 + (0.1 * idx) - wtd_dict = dict(hostname=hostname, weight=weight, capabilities=caps) + wtd_dict = dict(hostname=hostname, weight=weight, + capabilities=caps) expected.append(wtd_dict) self.assertWeights(expected, num, request_spec, hosts) diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 2b17e244a..c009641ef 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -1539,7 +1539,8 @@ class LibvirtConnection(driver.ComputeDriver): # basic-filtering must be set here. self.firewall_driver.setup_basic_filtering(instance_ref, network_info) # setting up n)ova-instance-instance-xx mainly. - self.firewall_driver.prepare_instance_filter(instance_ref, network_info) + self.firewall_driver.prepare_instance_filter(instance_ref, + network_info) # wait for completion timeout_count = range(FLAGS.live_migration_retry_count) -- cgit From 0385ef219b47fca0e98130d1c4c54c1673519f48 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 16 Aug 2011 12:02:39 -0400 Subject: Cleanup the '_base' directory in libvirt tests. --- 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 688518bb8..6a213b4f0 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -836,6 +836,7 @@ class LibvirtConnTestCase(test.TestCase): count = (0 <= str(e.message).find('Unexpected method call')) shutil.rmtree(os.path.join(FLAGS.instances_path, instance.name)) + shutil.rmtree(os.path.join(FLAGS.instances_path, '_base')) self.assertTrue(count) -- cgit From ca13037d2cd130f5b970d3af219566f3a70a9cb5 Mon Sep 17 00:00:00 2001 From: John Tran Date: Tue, 16 Aug 2011 09:18:13 -0700 Subject: test improvements per peer review --- nova/tests/test_cloud.py | 10 ++++++---- nova/tests/test_db_api.py | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 39358eeff..0793784f8 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -488,13 +488,15 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp2['id']) def test_describe_instances_deleted(self): - args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} - inst1 = db.instance_create(self.context, args) - inst2 = db.instance_create(self.context, args) + args1 = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args1) + args2 = {'reservation_id': 'b', 'image_ref': 1, 'host': 'host1'} + inst2 = db.instance_create(self.context, args2) db.instance_destroy(self.context, inst1.id) result = self.cloud.describe_instances(self.context) result = result['reservationSet'][0]['instancesSet'] - self.assertEqual(1, len(result)) + self.assertEqual(result[0]['instanceId'], + ec2utils.id_to_ec2_id(inst2.id)) def _block_device_mapping_create(self, instance_id, mappings): volumes = [] diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index ed363d1be..038c07f40 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -85,9 +85,11 @@ class DbApiTestCase(test.TestCase): self.assertTrue(2, len(result)) def test_instance_get_all_by_filters_deleted(self): - args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} - inst1 = db.instance_create(self.context, args) - inst2 = db.instance_create(self.context, args) + args1 = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args1) + args2 = {'reservation_id': 'b', 'image_ref': 1, 'host': 'host1'} + inst2 = db.instance_create(self.context, args2) db.instance_destroy(self.context, inst1.id) - result = db.instance_get_all_by_filters(self.context, {}) - self.assertTrue(1, len(result)) + result = db.instance_get_all_by_filters(self.context.elevated(), {}) + self.assertEqual(1, len(result)) + self.assertEqual(result[0].id, inst2.id) -- cgit From c890722ddfec7b6ef1911bfbbfd834ac1e3666d5 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 16 Aug 2011 23:15:54 -0400 Subject: Remove instances.admin_pass column. --- .../versions/037_instances_drop_admin_pass.py | 37 ++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 1 - nova/virt/xenapi/vmops.py | 3 -- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py b/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py new file mode 100644 index 000000000..b957666c2 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py @@ -0,0 +1,37 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, MetaData, Table, String + +meta = MetaData() + +admin_pass = Column( + 'admin_pass', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True, + autoload_with=migrate_engine) + instances.drop_column('admin_pass') + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True, + autoload_with=migrate_engine) + instances.create_column(admin_pass) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f2a4680b0..a8e9c36db 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -173,7 +173,6 @@ class Instance(BASE, NovaBase): base_name += "-rescue" return base_name - admin_pass = Column(String(255)) user_id = Column(String(255)) project_id = Column(String(255)) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index eb0a846b5..9a6215f88 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -709,9 +709,6 @@ class VMOps(object): if resp['returncode'] != '0': LOG.error(_('Failed to update password: %(resp)r') % locals()) return None - db.instance_update(nova_context.get_admin_context(), - instance['id'], - dict(admin_pass=new_pass)) return resp['message'] def inject_file(self, instance, path, contents): -- cgit From 79f3b1512166a37790c5cb2863140d696c717455 Mon Sep 17 00:00:00 2001 From: Troy Toman Date: Wed, 17 Aug 2011 02:41:17 -0500 Subject: Changed return code to 413 for metadata, personality and instance quota issues --- Authors | 1 + nova/api/openstack/common.py | 3 ++- nova/api/openstack/create_instance_helper.py | 13 ++++++++++--- nova/api/openstack/faults.py | 2 +- nova/api/openstack/server_metadata.py | 3 ++- nova/quota.py | 2 +- nova/tests/api/openstack/test_image_metadata.py | 4 ++-- nova/tests/api/openstack/test_server_actions.py | 4 ++-- nova/tests/api/openstack/test_server_metadata.py | 4 ++-- 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Authors b/Authors index 02fe46c79..864679929 100644 --- a/Authors +++ b/Authors @@ -101,6 +101,7 @@ Stephanie Reese Thierry Carrez Todd Willey Trey Morris +Troy Toman Tushar Patil Vasiliy Shlykov Vishvananda Ishaya diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index b2a675653..d9eb832f2 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -241,7 +241,8 @@ def check_img_metadata_quota_limit(context, metadata): quota_metadata = quota.allowed_metadata_items(context, num_metadata) if quota_metadata < num_metadata: expl = _("Image metadata limit exceeded") - raise webob.exc.HTTPBadRequest(explanation=expl) + raise webob.exc.HTTPRequestEntityTooLarge(explanation=expl, + headers={'Retry-After': 0}) class MetadataXMLDeserializer(wsgi.XMLDeserializer): diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 4e1da549e..b4a08dac0 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -180,13 +180,20 @@ class CreateInstanceHelper(object): """ if error.code == "OnsetFileLimitExceeded": expl = _("Personality file limit exceeded") - raise exc.HTTPBadRequest(explanation=expl) + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) if error.code == "OnsetFilePathLimitExceeded": expl = _("Personality file path too long") - raise exc.HTTPBadRequest(explanation=expl) + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) if error.code == "OnsetFileContentLimitExceeded": expl = _("Personality file content too long") - raise exc.HTTPBadRequest(explanation=expl) + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) + if error.code == "InstanceLimitExceeded": + expl = _("Instance quotas have been exceeded") + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) # if the original error is okay, just reraise it raise error diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 1ab45d4f1..0ed6f1ff0 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -53,7 +53,7 @@ class Fault(webob.exc.HTTPException): fault_name: { 'code': code, 'message': self.wrapped_exc.explanation}} - if code == 413: + if code == 413 and self.wrapped_exc.headers['Retry-After']: retry = self.wrapped_exc.headers['Retry-After'] fault_data[fault_name]['retryAfter'] = retry diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 2b235f79a..8ac3319c9 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -151,7 +151,8 @@ class Controller(object): def _handle_quota_error(self, error): """Reraise quota errors as api-specific http exceptions.""" if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) raise error diff --git a/nova/quota.py b/nova/quota.py index 58766e846..48e598659 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -164,5 +164,5 @@ def allowed_injected_file_path_bytes(context): class QuotaError(exception.ApiError): - """Quota Exceeeded.""" + """Quota Exceeded.""" pass diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py index 56a0932e7..21743eeef 100644 --- a/nova/tests/api/openstack/test_image_metadata.py +++ b/nova/tests/api/openstack/test_image_metadata.py @@ -230,7 +230,7 @@ class ImageMetaDataTest(test.TestCase): req.body = json_string req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) - self.assertEqual(400, res.status_int) + self.assertEqual(413, res.status_int) def test_too_many_metadata_items_on_put(self): req = webob.Request.blank('/v1.1/images/3/metadata/blah') @@ -238,4 +238,4 @@ class ImageMetaDataTest(test.TestCase): req.body = '{"meta": {"blah": "blah"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) - self.assertEqual(400, res.status_int) + self.assertEqual(413, res.status_int) diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 687a19390..80a27e30f 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -392,7 +392,7 @@ class ServerActionsTest(test.TestCase): req.body = json.dumps(body) req.headers["content-type"] = "application/json" response = req.get_response(fakes.wsgi_app()) - self.assertEqual(400, response.status_int) + self.assertEqual(413, response.status_int) def test_create_backup_no_name(self): """Name is required for backups""" @@ -865,7 +865,7 @@ class ServerActionsTestV11(test.TestCase): req.body = json.dumps(body) req.headers["content-type"] = "application/json" response = req.get_response(fakes.wsgi_app()) - self.assertEqual(400, response.status_int) + self.assertEqual(413, response.status_int) def test_create_image_no_name(self): body = { diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index ec446f0f0..8512bd518 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -417,9 +417,9 @@ class ServerMetaDataTest(test.TestCase): req.body = json_string req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) - self.assertEqual(400, res.status_int) + self.assertEqual(413, res.status_int) - def test_to_many_metadata_items_on_update_item(self): + def test_too_many_metadata_items_on_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata_max) req = webob.Request.blank('/v1.1/servers/1/metadata/key1') -- cgit From 228b185f1366df62da42b646ce98711de4195a5d Mon Sep 17 00:00:00 2001 From: Troy Toman Date: Wed, 17 Aug 2011 03:03:25 -0500 Subject: Removed a change from faults.py that was not required." --- nova/api/openstack/faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 0ed6f1ff0..1ab45d4f1 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -53,7 +53,7 @@ class Fault(webob.exc.HTTPException): fault_name: { 'code': code, 'message': self.wrapped_exc.explanation}} - if code == 413 and self.wrapped_exc.headers['Retry-After']: + if code == 413: retry = self.wrapped_exc.headers['Retry-After'] fault_data[fault_name]['retryAfter'] = retry -- cgit From ecc4e9ee389115e3793f94aaf53f8fbe59e7ac66 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Wed, 17 Aug 2011 19:58:22 +0000 Subject: Added the host 'enabled' status to the host_data returned by the plugin. --- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index cd9694ce1..36c61f78d 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -258,6 +258,7 @@ def cleanup(dct): # out["host_suspend-image-sr-uuid"] = dct.get("suspend-image-sr-uuid", "") # out["host_crash-dump-sr-uuid"] = dct.get("crash-dump-sr-uuid", "") # out["host_local-cache-sr"] = dct.get("local-cache-sr", "") + out["enabled"] = dct.get("enabled", "true") == "true" out["host_memory"] = omm = {} omm["total"] = safe_int(dct.get("memory-total", "")) omm["overhead"] = safe_int(dct.get("memory-overhead", "")) -- cgit From 6cdee8590528a95e9e3c7f2fc156cc9ebb8b39b2 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 17 Aug 2011 16:25:53 -0700 Subject: Make all services use the same launching strategy --- bin/nova-api | 44 +++++++++++++++++--------------------------- nova/service.py | 47 ++++++++++++++++++++++++++++------------------- nova/utils.py | 41 +++-------------------------------------- nova/wsgi.py | 3 --- 4 files changed, 48 insertions(+), 87 deletions(-) diff --git a/bin/nova-api b/bin/nova-api index fe8e83366..d2086dc92 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -19,12 +19,15 @@ """Starter script for Nova API. -Starts both the EC2 and OpenStack APIs in separate processes. +Starts both the EC2 and OpenStack APIs in separate greenthreads. """ +import eventlet +eventlet.monkey_patch() + +import gettext import os -import signal import sys @@ -33,32 +36,19 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath( if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): sys.path.insert(0, possible_topdir) -import nova.service -import nova.utils +gettext.install('nova', unicode=1) from nova import flags - - -FLAGS = flags.FLAGS - - -def main(): - """Launch EC2 and OSAPI services.""" - nova.utils.Bootstrapper.bootstrap_binary(sys.argv) - - launcher = nova.service.Launcher() - - for api in FLAGS.enabled_apis: - service = nova.service.WSGIService(api) - launcher.launch_service(service) - - signal.signal(signal.SIGTERM, lambda *_: launcher.stop()) - - try: - launcher.wait() - except KeyboardInterrupt: - launcher.stop() - +from nova import log as logging +from nova import service +from nova import utils if __name__ == '__main__': - sys.exit(main()) + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + services = [] + for api in flags.FLAGS.enabled_apis: + services.append(service.WSGIService(api)) + service.serve(*services) + service.wait() diff --git a/nova/service.py b/nova/service.py index 6e9eddc5a..e0735d26f 100644 --- a/nova/service.py +++ b/nova/service.py @@ -20,13 +20,12 @@ """Generic Node baseclass for all workers that run on hosts.""" import inspect -import multiprocessing import os +import signal +import eventlet import greenlet -from eventlet import greenthread - from nova import context from nova import db from nova import exception @@ -77,10 +76,7 @@ class Launcher(object): """ service.start() - try: - service.wait() - except KeyboardInterrupt: - service.stop() + service.wait() def launch_service(self, service): """Load and start the given service. @@ -89,10 +85,8 @@ class Launcher(object): :returns: None """ - process = multiprocessing.Process(target=self.run_service, - args=(service,)) - process.start() - self._services.append(process) + gt = eventlet.spawn(self.run_service, service) + self._services.append(gt) def stop(self): """Stop all services which are currently running. @@ -101,8 +95,7 @@ class Launcher(object): """ for service in self._services: - if service.is_alive(): - service.terminate() + service.kill() def wait(self): """Waits until all services have been stopped, and then returns. @@ -111,7 +104,10 @@ class Launcher(object): """ for service in self._services: - service.join() + try: + service.wait() + except greenlet.GreenletExit: + pass class Service(object): @@ -121,6 +117,7 @@ class Service(object): periodic_interval=None, *args, **kwargs): self.host = host self.binary = binary + self.name = binary self.topic = topic self.manager_class_name = manager manager_class = utils.import_class(self.manager_class_name) @@ -173,7 +170,7 @@ class Service(object): finally: consumer_set.close() - self.consumer_set_thread = greenthread.spawn(_wait) + self.consumer_set_thread = eventlet.spawn(_wait) if self.report_interval: pulse = utils.LoopingCall(self.report_state) @@ -339,7 +336,17 @@ class WSGIService(object): self.server.wait() +# NOTE(vish): the global launcher is to maintain the existing +# functionality of calling service.serve + +# service.wait +_launcher = None + + def serve(*services): + global _launcher + if not _launcher: + _launcher = Launcher() + signal.signal(signal.SIGTERM, lambda *args: _launcher.stop()) try: if not services: services = [Service.create()] @@ -354,7 +361,7 @@ def serve(*services): flags.DEFINE_flag(flags.HelpXMLFlag()) FLAGS.ParseNewFlags() - name = '_'.join(x.binary for x in services) + name = '_'.join(x.name for x in services) logging.debug(_('Serving %s'), name) logging.debug(_('Full set of FLAGS:')) for flag in FLAGS: @@ -362,9 +369,11 @@ def serve(*services): logging.debug('%(flag)s : %(flag_get)s' % locals()) for x in services: - x.start() + _launcher.launch_service(x) def wait(): - while True: - greenthread.sleep(5) + try: + _launcher.wait() + except KeyboardInterrupt: + _launcher.stop() diff --git a/nova/utils.py b/nova/utils.py index 7276b6bd5..54126f644 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -260,8 +260,9 @@ def default_flagfile(filename='nova.conf', args=None): filename = "./nova.conf" if not os.path.exists(filename): filename = '/etc/nova/nova.conf' - flagfile = '--flagfile=%s' % filename - args.insert(1, flagfile) + if os.path.exists(filename): + flagfile = '--flagfile=%s' % filename + args.insert(1, flagfile) def debug(arg): @@ -837,39 +838,3 @@ def bool_from_str(val): return True if int(val) else False except ValueError: return val.lower() == 'true' - - -class Bootstrapper(object): - """Provides environment bootstrapping capabilities for entry points.""" - - @staticmethod - def bootstrap_binary(argv): - """Initialize the Nova environment using command line arguments.""" - Bootstrapper.setup_flags(argv) - Bootstrapper.setup_logging() - Bootstrapper.log_flags() - - @staticmethod - def setup_logging(): - """Initialize logging and log a message indicating the Nova version.""" - logging.setup() - logging.audit(_("Nova Version (%s)") % - version.version_string_with_vcs()) - - @staticmethod - def setup_flags(input_flags): - """Initialize flags, load flag file, and print help if needed.""" - default_flagfile(args=input_flags) - FLAGS(input_flags or []) - flags.DEFINE_flag(flags.HelpFlag()) - flags.DEFINE_flag(flags.HelpshortFlag()) - flags.DEFINE_flag(flags.HelpXMLFlag()) - FLAGS.ParseNewFlags() - - @staticmethod - def log_flags(): - """Log the list of all active flags being used.""" - logging.audit(_("Currently active flags:")) - for key in FLAGS: - value = FLAGS.get(key, None) - logging.audit(_("%(key)s : %(value)s" % locals())) diff --git a/nova/wsgi.py b/nova/wsgi.py index c8ddb97d7..f2846aa73 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -39,9 +39,6 @@ from nova import log as logging from nova import utils -eventlet.patcher.monkey_patch(socket=True, time=True) - - FLAGS = flags.FLAGS LOG = logging.getLogger('nova.wsgi') -- cgit From 9011bf57d8caf8a0bd11dfb33cf968b2b65fe294 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 11:21:35 -0500 Subject: Added rescue mode extension. --- nova/api/openstack/contrib/rescue.py | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 nova/api/openstack/contrib/rescue.py diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py new file mode 100644 index 000000000..efb882fd6 --- /dev/null +++ b/nova/api/openstack/contrib/rescue.py @@ -0,0 +1,72 @@ +# Copyright 2011 Openstack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The rescue mode extension.""" + +import webob +from webob import exc + +from nova import compute +from nova import log as logging +from nova.api.openstack import extensions as exts +from nova.api.openstack import faults + +LOG = logging.getLogger("nova.api.contrib.rescue") + + +class Rescue(exts.ExtensionDescriptor): + """The Rescue API controller for the OpenStack API.""" + def __init__(self): + super(Rescue, self).__init__() + self.compute_api = compute.API() + + def _rescue(self, input_dict, req, instance_id): + """Enable or disable rescue mode.""" + context = req.environ["nova.context"] + action = input_dict["rescue"]["action"] + + try: + if action == "rescue": + self.compute_api.rescue(context, instance_id) + elif action == "unrescue": + self.compute_api.unrescue(context, instance_id) + except Exception, e: + LOG.exception(_("Error in %(action)s: %(e)s") % locals()) + return faults.Fault(exc.HTTPBadRequest()) + + return webob.Response(status_int=202) + + def get_name(self): + return "Rescue" + + def get_alias(self): + return "rescue" + + def get_description(self): + return "Instance rescue mode" + + def get_namespace(self): + return "http://docs.openstack.org/ext/rescue/api/v1.1" + + def get_updated(self): + return "2011-08-18T00:00:00+00:00" + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + exts.ActionExtension("servers", "rescue", self._rescue), + exts.ActionExtension("servers", "unrescue", self._rescue), + ] + + return actions -- cgit From a68c1cde2e73e6d39d7ff6024cd3ff289c465619 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 12:20:40 -0500 Subject: Refactored a little and updated unit test. --- nova/api/openstack/contrib/rescue.py | 12 ++++++++---- nova/tests/api/openstack/test_extensions.py | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index efb882fd6..dac269efb 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -31,10 +31,10 @@ class Rescue(exts.ExtensionDescriptor): super(Rescue, self).__init__() self.compute_api = compute.API() - def _rescue(self, input_dict, req, instance_id): - """Enable or disable rescue mode.""" + def _rescue(self, input_dict, req, instance_id, exit_rescue=False): + """Rescue an instance.""" context = req.environ["nova.context"] - action = input_dict["rescue"]["action"] + action = "unrescue" if exit_rescue else "rescue" try: if action == "rescue": @@ -47,6 +47,10 @@ class Rescue(exts.ExtensionDescriptor): return webob.Response(status_int=202) + def _unrescue(self, input_dict, req, instance_id): + """Unrescue an instance.""" + self._rescue(input_dict, req, instance_id, exit_rescue=True) + def get_name(self): return "Rescue" @@ -66,7 +70,7 @@ class Rescue(exts.ExtensionDescriptor): """Return the actions the extension adds, as required by contract.""" actions = [ exts.ActionExtension("servers", "rescue", self._rescue), - exts.ActionExtension("servers", "unrescue", self._rescue), + exts.ActionExtension("servers", "unrescue", self._unrescue), ] return actions diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 5d3208e10..34a4b3f89 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -94,6 +94,7 @@ class ExtensionControllerTest(test.TestCase): "Quotas", "SecurityGroups", "Volumes", + "Rescue", ] self.ext_list.sort() -- cgit From a9d87715133ae79518cef6aafd87c95e26f20765 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 12:25:22 -0500 Subject: Minor housecleaning. --- nova/api/openstack/contrib/rescue.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index dac269efb..65ce2874b 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -22,17 +22,22 @@ from nova import log as logging from nova.api.openstack import extensions as exts from nova.api.openstack import faults + LOG = logging.getLogger("nova.api.contrib.rescue") class Rescue(exts.ExtensionDescriptor): - """The Rescue API controller for the OpenStack API.""" + """The Rescue controller for the OpenStack API.""" def __init__(self): super(Rescue, self).__init__() self.compute_api = compute.API() def _rescue(self, input_dict, req, instance_id, exit_rescue=False): - """Rescue an instance.""" + """Rescue an instance. + + If exit_rescue is True, rescue mode should be torn down and the + instance restored to its original state. + """ context = req.environ["nova.context"] action = "unrescue" if exit_rescue else "rescue" -- cgit From 125a2affec7713cdbcb925537d34aea29a2e4230 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 10:55:39 -0700 Subject: more cleanup of binaries per review --- bin/nova-ajax-console-proxy | 7 +++---- bin/nova-api | 8 +++----- bin/nova-compute | 5 ++--- bin/nova-console | 5 ++--- bin/nova-direct-api | 11 +++++++---- bin/nova-network | 5 ++--- bin/nova-objectstore | 14 +++++++------- bin/nova-scheduler | 5 ++--- bin/nova-vncproxy | 15 ++++++--------- bin/nova-volume | 5 ++--- 10 files changed, 36 insertions(+), 44 deletions(-) diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy index 2329581a2..0a789b4b9 100755 --- a/bin/nova-ajax-console-proxy +++ b/bin/nova-ajax-console-proxy @@ -24,7 +24,6 @@ from eventlet import greenthread from eventlet.green import urllib2 import exceptions -import gettext import os import sys import time @@ -38,11 +37,11 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging from nova import rpc +from nova import service from nova import utils from nova import wsgi @@ -141,5 +140,5 @@ if __name__ == '__main__': acp = AjaxConsoleProxy() acp.register_listeners() server = wsgi.Server("AJAX Console Proxy", acp, port=acp_port) - server.start() - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-api b/bin/nova-api index d2086dc92..38e2624d8 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -26,7 +26,6 @@ Starts both the EC2 and OpenStack APIs in separate greenthreads. import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -36,7 +35,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath( if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -47,8 +45,8 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - services = [] + servers = [] for api in flags.FLAGS.enabled_apis: - services.append(service.WSGIService(api)) - service.serve(*services) + servers.append(service.WSGIService(api)) + service.serve(*servers) service.wait() diff --git a/bin/nova-compute b/bin/nova-compute index cd7c78def..9aef201e6 100755 --- a/bin/nova-compute +++ b/bin/nova-compute @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')): sys.path.insert(0, POSSIBLE_TOPDIR) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Server(binary='nova-compute') + service.serve(server) service.wait() diff --git a/bin/nova-console b/bin/nova-console index 40608b995..7f76fdc29 100755 --- a/bin/nova-console +++ b/bin/nova-console @@ -21,7 +21,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -33,7 +32,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -44,5 +42,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Server(binary='nova-console') + service.serve(server) service.wait() diff --git a/bin/nova-direct-api b/bin/nova-direct-api index c6cf9b2ff..106e89ba9 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -20,7 +20,9 @@ """Starter script for Nova Direct API.""" -import gettext +import eventlet +eventlet.monkey_patch() + import os import sys @@ -32,12 +34,12 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import compute from nova import flags from nova import log as logging from nova import network +from nova import service from nova import utils from nova import volume from nova import wsgi @@ -97,5 +99,6 @@ if __name__ == '__main__': with_auth, host=FLAGS.direct_host, port=FLAGS.direct_port) - server.start() - server.wait() + + service.serve(server) + service.wait() diff --git a/bin/nova-network b/bin/nova-network index 101761ef7..ce93e9354 100755 --- a/bin/nova-network +++ b/bin/nova-network @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Server(binary='nova-compute') + service.serve(server) service.wait() diff --git a/bin/nova-objectstore b/bin/nova-objectstore index 4d5aec445..c7a76e120 100755 --- a/bin/nova-objectstore +++ b/bin/nova-objectstore @@ -17,11 +17,11 @@ # License for the specific language governing permissions and limitations # under the License. -""" - Daemon for nova objectstore. Supports S3 API. -""" +"""Daemon for nova objectstore. Supports S3 API.""" + +import eventlet +eventlet.monkey_patch() -import gettext import os import sys @@ -33,10 +33,10 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging +from nova import service from nova import utils from nova import wsgi from nova.objectstore import s3server @@ -54,5 +54,5 @@ if __name__ == '__main__': router, port=FLAGS.s3_port, host=FLAGS.s3_host) - server.start() - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 0c205a80f..07d1c55e6 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Server(binary='nova-compute') + service.serve(server) service.wait() diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy index bdbb30a7f..dc08e2433 100755 --- a/bin/nova-vncproxy +++ b/bin/nova-vncproxy @@ -19,7 +19,8 @@ """VNC Console Proxy Server.""" import eventlet -import gettext +eventlet.monkey_patch() + import os import sys @@ -29,7 +30,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -41,7 +41,7 @@ from nova.vnc import auth from nova.vnc import proxy -LOG = logging.getLogger('nova.vnc-proxy') +LOG = logging.getLogger('nova.vncproxy') FLAGS = flags.FLAGS @@ -81,7 +81,7 @@ if __name__ == "__main__": FLAGS(sys.argv) logging.setup() - LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), + LOG.audit(_("Starting nova-vncproxy node (version %s)"), version.version_string_with_vcs()) if not (os.path.exists(FLAGS.vncproxy_wwwroot) and @@ -107,13 +107,10 @@ if __name__ == "__main__": else: with_auth = auth.VNCNovaAuthMiddleware(with_logging) - service.serve() - server = wsgi.Server("VNC Proxy", with_auth, host=FLAGS.vncproxy_host, port=FLAGS.vncproxy_port) - server.start() server.start_tcp(handle_flash_socket_policy, 843, host=FLAGS.vncproxy_host) - - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-volume b/bin/nova-volume index 8dcdbc500..1451de44a 100755 --- a/bin/nova-volume +++ b/bin/nova-volume @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Server(binary='nova-volume') + service.serve(server) service.wait() -- cgit From 0cf36be73e7de4942f395a2a7dfeb58df5870821 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 10:56:14 -0700 Subject: add separate api binaries --- bin/nova-api-ec2 | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ bin/nova-api-os | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100755 bin/nova-api-ec2 create mode 100755 bin/nova-api-os diff --git a/bin/nova-api-ec2 b/bin/nova-api-ec2 new file mode 100755 index 000000000..9fac7b63a --- /dev/null +++ b/bin/nova-api-ec2 @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Starter script for Nova API. + +Starts both the EC2 and OpenStack APIs in separate greenthreads. + +""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + server = service.WSGIService('ec2') + service.serve(server) + service.wait() diff --git a/bin/nova-api-os b/bin/nova-api-os new file mode 100755 index 000000000..9d9a7b05e --- /dev/null +++ b/bin/nova-api-os @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Starter script for Nova API. + +Starts both the EC2 and OpenStack APIs in separate greenthreads. + +""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + server = service.WSGIService('osapi') + service.serve(server) + service.wait() -- cgit From 788e5c5e94c224c3909c4f12ecc569bba3ba1c9e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 11:00:47 -0700 Subject: remove signal handling and clean up service.serve --- nova/service.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/nova/service.py b/nova/service.py index e0735d26f..8ffd39629 100644 --- a/nova/service.py +++ b/nova/service.py @@ -21,7 +21,6 @@ import inspect import os -import signal import eventlet import greenlet @@ -346,33 +345,21 @@ def serve(*services): global _launcher if not _launcher: _launcher = Launcher() - signal.signal(signal.SIGTERM, lambda *args: _launcher.stop()) - try: - if not services: - services = [Service.create()] - except Exception: - logging.exception('in Service.create()') - raise - finally: - # After we've loaded up all our dynamic bits, check - # whether we should print help - flags.DEFINE_flag(flags.HelpFlag()) - flags.DEFINE_flag(flags.HelpshortFlag()) - flags.DEFINE_flag(flags.HelpXMLFlag()) - FLAGS.ParseNewFlags() - - name = '_'.join(x.name for x in services) - logging.debug(_('Serving %s'), name) - logging.debug(_('Full set of FLAGS:')) - for flag in FLAGS: - flag_get = FLAGS.get(flag, None) - logging.debug('%(flag)s : %(flag_get)s' % locals()) - for x in services: _launcher.launch_service(x) def wait(): + # After we've loaded up all our dynamic bits, check + # whether we should print help + flags.DEFINE_flag(flags.HelpFlag()) + flags.DEFINE_flag(flags.HelpshortFlag()) + flags.DEFINE_flag(flags.HelpXMLFlag()) + FLAGS.ParseNewFlags() + logging.debug(_('Full set of FLAGS:')) + for flag in FLAGS: + flag_get = FLAGS.get(flag, None) + logging.debug('%(flag)s : %(flag_get)s' % locals()) try: _launcher.wait() except KeyboardInterrupt: -- cgit From 97552f05d5d26e596ddf0cda8169f3a5d131a55a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 11:28:02 -0700 Subject: fix typo --- bin/nova-compute | 2 +- bin/nova-console | 2 +- bin/nova-network | 2 +- bin/nova-scheduler | 2 +- bin/nova-volume | 2 +- nova/service.py | 35 +++++++++++++++++++---------------- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/bin/nova-compute b/bin/nova-compute index 9aef201e6..5239fae72 100755 --- a/bin/nova-compute +++ b/bin/nova-compute @@ -43,6 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Server(binary='nova-compute') + server = service.Service.create(binary='nova-compute') service.serve(server) service.wait() diff --git a/bin/nova-console b/bin/nova-console index 7f76fdc29..22f6ef171 100755 --- a/bin/nova-console +++ b/bin/nova-console @@ -42,6 +42,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Server(binary='nova-console') + server = service.Service.create(binary='nova-console') service.serve(server) service.wait() diff --git a/bin/nova-network b/bin/nova-network index ce93e9354..57759d30a 100755 --- a/bin/nova-network +++ b/bin/nova-network @@ -43,6 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Server(binary='nova-compute') + server = service.Service.create(binary='nova-network') service.serve(server) service.wait() diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 07d1c55e6..3b627e62d 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -43,6 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Server(binary='nova-compute') + server = service.Service.create(binary='nova-compute') service.serve(server) service.wait() diff --git a/bin/nova-volume b/bin/nova-volume index 1451de44a..5405aebbb 100755 --- a/bin/nova-volume +++ b/bin/nova-volume @@ -43,6 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Server(binary='nova-volume') + server = service.Service.create(binary='nova-volume') service.serve(server) service.wait() diff --git a/nova/service.py b/nova/service.py index 8ffd39629..959e79052 100644 --- a/nova/service.py +++ b/nova/service.py @@ -67,24 +67,24 @@ class Launcher(object): self._services = [] @staticmethod - def run_service(service): - """Start and wait for a service to finish. + def run_server(server): + """Start and wait for a server to finish. - :param service: Service to run and wait for. + :param service: Server to run and wait for. :returns: None """ - service.start() - service.wait() + server.start() + server.wait() - def launch_service(self, service): - """Load and start the given service. + def launch_server(self, server): + """Load and start the given server. - :param service: The service you would like to start. + :param server: The server you would like to start. :returns: None """ - gt = eventlet.spawn(self.run_service, service) + gt = eventlet.spawn(self.run_server, server) self._services.append(gt) def stop(self): @@ -110,13 +110,16 @@ class Launcher(object): class Service(object): - """Base class for workers that run on hosts.""" + """Service object for binaries running on hosts. + + A service takes a manager and enables rpc by listening to queues based + on topic. It also periodically runs tasks on the manager and reports + it state to the database services table.""" def __init__(self, host, binary, topic, manager, report_interval=None, periodic_interval=None, *args, **kwargs): self.host = host self.binary = binary - self.name = binary self.topic = topic self.manager_class_name = manager manager_class = utils.import_class(self.manager_class_name) @@ -289,9 +292,9 @@ class WSGIService(object): """Provides ability to launch API from a 'paste' configuration.""" def __init__(self, name, loader=None): - """Initialize, but do not start the WSGI service. + """Initialize, but do not start the WSGI server. - :param name: The name of the WSGI service given to the loader. + :param name: The name of the WSGI server given to the loader. :param loader: Loads the WSGI application using the given name. :returns: None @@ -341,12 +344,12 @@ class WSGIService(object): _launcher = None -def serve(*services): +def serve(*servers): global _launcher if not _launcher: _launcher = Launcher() - for x in services: - _launcher.launch_service(x) + for server in servers: + _launcher.launch_server(server) def wait(): -- cgit From 05e8c1755d8fde5a9a3bde02e339938f670694c6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 11:28:43 -0700 Subject: one more --- bin/nova-scheduler | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 3b627e62d..2e168cbc6 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -43,6 +43,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - server = service.Service.create(binary='nova-compute') + server = service.Service.create(binary='nova-scheduler') service.serve(server) service.wait() -- cgit From a4d63f18971bad12ea812c63bcee35d8070333f7 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 11:31:28 -0700 Subject: fix docstrings in new api bins --- bin/nova-api-ec2 | 6 +----- bin/nova-api-os | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/bin/nova-api-ec2 b/bin/nova-api-ec2 index 9fac7b63a..df50f713d 100755 --- a/bin/nova-api-ec2 +++ b/bin/nova-api-ec2 @@ -17,11 +17,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Starter script for Nova API. - -Starts both the EC2 and OpenStack APIs in separate greenthreads. - -""" +"""Starter script for Nova EC2 API.""" import eventlet eventlet.monkey_patch() diff --git a/bin/nova-api-os b/bin/nova-api-os index 9d9a7b05e..374e850ea 100755 --- a/bin/nova-api-os +++ b/bin/nova-api-os @@ -17,11 +17,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Starter script for Nova API. - -Starts both the EC2 and OpenStack APIs in separate greenthreads. - -""" +"""Starter script for Nova OS API.""" import eventlet eventlet.monkey_patch() -- cgit From b98c14c411ae09d9a8b5b2112d0e1b01b71ced44 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 18 Aug 2011 14:34:14 -0400 Subject: Don't send 'injected_files' and 'admin_pass' to db.update. --- nova/compute/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 66458fb36..47e7864c4 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -393,11 +393,12 @@ class ComputeManager(manager.SchedulerDependentManager): updates['host'] = self.host updates['launched_on'] = self.host # NOTE(vish): used by virt but not in database - updates['injected_files'] = kwargs.get('injected_files', []) - updates['admin_pass'] = kwargs.get('admin_password', None) instance = self.db.instance_update(context, instance_id, updates) + instance['injected_files'] = kwargs.get('injected_files', []) + instance['admin_pass'] = kwargs.get('admin_password', None) + self.db.instance_set_state(context, instance_id, power_state.NOSTATE, -- cgit From 6b8c26d230d06c35921e2e0a2d30d9d3d745eff4 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 18 Aug 2011 14:44:10 -0400 Subject: Remove old comment. --- nova/compute/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 47e7864c4..091b3b6b2 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -392,7 +392,6 @@ class ComputeManager(manager.SchedulerDependentManager): updates = {} updates['host'] = self.host updates['launched_on'] = self.host - # NOTE(vish): used by virt but not in database instance = self.db.instance_update(context, instance_id, updates) -- cgit From af9681bc82d7509cb2f65d213bd4d8ae24286663 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Thu, 18 Aug 2011 13:47:09 -0500 Subject: Moved compute calls to their own handler --- nova/compute/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index e909e9959..0c5d4349d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1068,14 +1068,20 @@ class API(base.Base): """Unpause the given instance.""" self._cast_compute_message('unpause_instance', context, instance_id) + def _make_compute_call_for_host(self context, host, params): + """Call method deliberately designed to make host/service only calls""" + queue = self.db.queue_get_for(context, FLAGS.compute_topic, host) + kwargs = {'method': method, 'args': params} + return rpc.call(context, queue, kwargs) + def set_host_enabled(self, context, host, enabled): """Sets the specified host's ability to accept new instances.""" - return self._call_compute_message("set_host_enabled", context, + return self._make_compute_call_for_host("set_host_enabled", context, host=host, params={"enabled": enabled}) def host_power_action(self, context, host, action): """Reboots, shuts down or powers up the host.""" - return self._call_compute_message("host_power_action", context, + return self._make_compute_call_for_host("host_power_action", context, host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") -- cgit From 69996e83f10387b83bdc7e5e76b62fe67ea6c2ab Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Thu, 18 Aug 2011 13:55:38 -0500 Subject: Syntax error --- nova/compute/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 0c5d4349d..3110cd92d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1068,7 +1068,7 @@ class API(base.Base): """Unpause the given instance.""" self._cast_compute_message('unpause_instance', context, instance_id) - def _make_compute_call_for_host(self context, host, params): + def _make_compute_call_for_host(self, context, host, params): """Call method deliberately designed to make host/service only calls""" queue = self.db.queue_get_for(context, FLAGS.compute_topic, host) kwargs = {'method': method, 'args': params} -- cgit From c6c004c44595f218f66eee8f6f9173c6108be8a4 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 18 Aug 2011 14:39:25 -0500 Subject: Updated the distributed scheduler docs with the latest changes to the classes. --- doc/source/devref/distributed_scheduler.rst | 56 ++++++++++++++-------------- doc/source/images/base_scheduler.png | Bin 0 -> 17068 bytes doc/source/images/zone_overview.png | Bin 0 -> 51587 bytes 3 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 doc/source/images/base_scheduler.png create mode 100755 doc/source/images/zone_overview.png diff --git a/doc/source/devref/distributed_scheduler.rst b/doc/source/devref/distributed_scheduler.rst index e33fda4d2..c63e62f7f 100644 --- a/doc/source/devref/distributed_scheduler.rst +++ b/doc/source/devref/distributed_scheduler.rst @@ -31,9 +31,9 @@ This is the purpose of the Distributed Scheduler (DS). The DS utilizes the Capab So, how does this all work? -This document will explain the strategy employed by the `ZoneAwareScheduler` and its derivations. You should read the :doc:`devguide/zones` documentation before reading this. +This document will explain the strategy employed by the `BaseScheduler`, which is the base for all schedulers designed to work across zones, and its derivations. You should read the :doc:`devguide/zones` documentation before reading this. - .. image:: /images/zone_aware_scheduler.png + .. image:: /images/base_scheduler.png Costs & Weights --------------- @@ -52,32 +52,32 @@ This Weight is computed for each Instance requested. If the customer asked for 1 .. image:: /images/costs_weights.png -nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler +nova.scheduler.base_scheduler.BaseScheduler ------------------------------------------------------ -As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `ZoneAwareScheduler` uses this information to make its decisions. +As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `BaseScheduler` uses this information to make its decisions. Here is how it works: 1. The compute nodes are filtered and the nodes remaining are weighed. - 2. Filtering the hosts is a simple matter of ensuring the compute node has ample resources (CPU, RAM, Disk, etc) to fulfil the request. + 2. Filtering the hosts is a simple matter of ensuring the compute node has ample resources (CPU, RAM, Disk, etc) to fulfil the request. 3. Weighing of the remaining compute nodes assigns a number based on their suitability for the request. 4. The same request is sent to each child Zone and step #1 is done there too. The resulting weighted list is returned to the parent. 5. The parent Zone sorts and aggregates all the weights and a final build plan is constructed. 6. The build plan is executed upon. Concurrently, instance create requests are sent to each of the selected hosts, be they local or in a child zone. Child Zones may forward the requests to their child Zones as needed. - .. image:: /images/zone_aware_overview.png + .. image:: /images/zone_overview.png -`ZoneAwareScheduler` by itself is not capable of handling all the provisioning itself. Derived classes are used to select which host filtering and weighing strategy will be used. +`BaseScheduler` by itself is not capable of handling all the provisioning itself. You should also specify the filter classes and weighting classes to be used in determining which host is selected for new instance creation. Filtering and Weighing ---------------------- -The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `ZoneAwareScheduler` are flexible and extensible. +The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `BaseScheduler` are flexible and extensible. .. image:: /images/filtering.png Requesting a new instance ------------------------- -Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table. +Prior to the `BaseScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table. `nova.compute.api.create()` performed the following actions: 1. it validated all the fields passed into it. @@ -89,11 +89,11 @@ Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to .. image:: /images/nova.compute.api.create.png -Generally, the standard schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones. +Generally, the simplest schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones. The problem with this approach is each request is scattered amongst each of the schedulers. If we are asking for 1000 instances, each scheduler gets the requests one-at-a-time. There is no possability of optimizing the requests to take into account all 1000 instances as a group. We call this Single-Shot vs. All-at-Once. -For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently: +For the `BaseScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently: 1. it validates all the fields passed into it. 2. it creates a single `reservation_id` for all of instances created. This is a UUID. 3. it creates a single `run_instance` request in the scheduler queue @@ -109,21 +109,19 @@ For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to The Catch --------- -This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But, for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world. +This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world. -When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many many `select` calls issued to child Zones asking for estimates. +When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many, many `select` calls issued to child Zones asking for estimates. -Instead, we take a rather innovative approach to the problem. We encrypt all the child zone internal details and pass them back the to parent Zone. If the parent zone decides to use a child Zone for the instance it simply passes the encrypted data back to the child during the `POST /servers` call as an extra parameter. The child Zone can then decrypt the hint and go directly to the Compute node previously selected. If the estimate isn't used, it is simply discarded by the parent. It's for this reason that it is so important that each Zone defines a unique encryption key via `--build_plan_encryption_key` +Instead, we take a rather innovative approach to the problem. We encrypt all the child Zone internal details and pass them back the to parent Zone. In the case of a nested Zone layout, each nesting layer will encrypt the data from all of its children and pass that to its parent Zone. In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. Every Zone interface adds another layer of encryption, using its unique key. -In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. +Once a host is selected, it will either be local to the Zone that received the initial API call, or one of its child Zones. In the latter case, the parent Zone it simply passes the encrypted data for the selected host back to each of its child Zones during the `POST /servers` call as an extra parameter. If the child Zone can decrypt the data, then it is the correct Zone for the selected host; all other Zones will not be able to decrypt the data and will discard the request. This is why it is critical that each Zone has a unique value specified in its config in `--build_plan_encryption_key`: it controls the ability to locate the selected host without having to hard-code path information or other identifying information. The child Zone can then act on the decrypted data and either go directly to the Compute node previously selected if it is located in that Zone, or repeat the process with its child Zones until the target Zone containing the selected host is reached. -Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.zone_aware_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use. +Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.base_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use. Reservation IDs --------------- -NOTE: The features described in this section are related to the up-coming 'merge-4' branch. - The OpenStack API allows a user to list all the instances they own via the `GET /servers/` command or the details on a particular instance via `GET /servers/###`. This mechanism is usually sufficient since OS API only allows for creating one instance at a time, unlike the EC2 API which allows you to specify a quantity of instances to be created. NOTE: currently the `GET /servers` command is not Zone-aware since all operations done in child Zones are done via a single administrative account. Therefore, asking a child Zone to `GET /servers` would return all the active instances ... and that would not be what the user intended. Later, when the Keystone Auth system is integrated with Nova, this functionality will be enabled. @@ -137,23 +135,23 @@ Finally, we need to give the user a way to get information on each of the instan Host Filter ----------- -As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.host_filter` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms. +As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.filters` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms. -The filter used is determined by the `--default_host_filter` flag, which points to a Python Class. By default this flag is set to `nova.scheduler.host_filter.AllHostsFilter` which simply returns all available hosts. But there are others: +The filter used is determined by the `--default_host_filters` flag, which points to a Python Class. By default this flag is set to `[AllHostsFilter]` which simply returns all available hosts. But there are others: - * `nova.scheduler.host_filter.InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`. + * `InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`. - * `nova.scheduler.host_filter.JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples. + * `JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples. -To create your own `HostFilter` the user simply has to derive from `nova.scheduler.host_filter.HostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of all available hosts is in the `ZoneManager` object passed into the call as well as the filter query. The host tuple contains (``, ``) where `` is whatever you want it to be. +To create your own `HostFilter` the user simply has to derive from `nova.scheduler.filters.AbstractHostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of available hosts is in the `host_list` parameter passed into the call as well as the filter query. The host tuple contains (``, ``) where `` is whatever you want it to be. By default, it is the capabilities reported by the host. Cost Scheduler Weighing ----------------------- -Every `ZoneAwareScheduler` derivation must also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `ZoneAwareScheduler` base class when all the results have been assembled. +Every `BaseScheduler` subclass should also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `BaseScheduler` when all the results have been assembled. -Simple Zone Aware Scheduling +Simple Scheduling Across Zones ---------------------------- -The easiest way to get started with the `ZoneAwareScheduler` is to use the `nova.scheduler.host_filter.HostFilterScheduler`. This scheduler uses the default Host Filter and the `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things. +The `BaseScheduler` uses the default `filter_hosts` method, which will use either any filters specified in the request's `filter` parameter, or, if that is not specified, the filters specified in the `FLAGS.default_host_filters` setting. Its `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things. The `--scheduler_driver` flag is how you specify the scheduler class name. @@ -168,14 +166,14 @@ All this Zone and Distributed Scheduler stuff can seem a little daunting to conf --enable_zone_routing=true --zone_name=zone1 --build_plan_encryption_key=c286696d887c9aa0611bbb3e2025a45b - --scheduler_driver=nova.scheduler.host_filter.HostFilterScheduler - --default_host_filter=nova.scheduler.host_filter.AllHostsFilter + --scheduler_driver=nova.scheduler.base_scheduler.BaseScheduler + --default_host_filter=nova.scheduler.filters.AllHostsFilter `--allow_admin_api` must be set for OS API to enable the new `/zones/*` commands. `--enable_zone_routing` must be set for OS API commands such as `create()`, `pause()` and `delete()` to get routed from Zone to Zone when looking for instances. `--zone_name` is only required in child Zones. The default Zone name is `nova`, but you may want to name your child Zones something useful. Duplicate Zone names are not an issue. `build_plan_encryption_key` is the SHA-256 key for encrypting/decrypting the Host information when it leaves a Zone. Be sure to change this key for each Zone you create. Do not duplicate keys. -`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler`. +`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.base_scheduler.BaseScheduler`. `default_host_filter` is the host filter to be used for filtering candidate Compute nodes. Some optional flags which are handy for debugging are: diff --git a/doc/source/images/base_scheduler.png b/doc/source/images/base_scheduler.png new file mode 100644 index 000000000..75d029338 Binary files /dev/null and b/doc/source/images/base_scheduler.png differ diff --git a/doc/source/images/zone_overview.png b/doc/source/images/zone_overview.png new file mode 100755 index 000000000..cc891df0a Binary files /dev/null and b/doc/source/images/zone_overview.png differ -- cgit From 19495e51bc86bf1bc333759e3825ab4b5592ff66 Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Thu, 18 Aug 2011 19:40:59 +0000 Subject: Need to pass the action --- 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 3110cd92d..598270ba1 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1068,10 +1068,10 @@ class API(base.Base): """Unpause the given instance.""" self._cast_compute_message('unpause_instance', context, instance_id) - def _make_compute_call_for_host(self, context, host, params): + def _make_compute_call_for_host(self, action, context, host, params): """Call method deliberately designed to make host/service only calls""" queue = self.db.queue_get_for(context, FLAGS.compute_topic, host) - kwargs = {'method': method, 'args': params} + kwargs = {'method': action, 'args': params} return rpc.call(context, queue, kwargs) def set_host_enabled(self, context, host, enabled): -- cgit From fe28c88a6bfff9d8e0d83751ab89e83173aaf092 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 14:56:22 -0500 Subject: Review feedback. --- nova/api/openstack/contrib/rescue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index 65ce2874b..5ee071696 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -48,7 +48,7 @@ class Rescue(exts.ExtensionDescriptor): self.compute_api.unrescue(context, instance_id) except Exception, e: LOG.exception(_("Error in %(action)s: %(e)s") % locals()) - return faults.Fault(exc.HTTPBadRequest()) + return faults.Fault(exc.HTTPInternalServerError()) return webob.Response(status_int=202) @@ -60,7 +60,7 @@ class Rescue(exts.ExtensionDescriptor): return "Rescue" def get_alias(self): - return "rescue" + return "os-rescue" def get_description(self): return "Instance rescue mode" -- cgit From 508b45a3fda9caa92c90282045495acb6e2f638b Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 15:08:51 -0500 Subject: Better docstring for _unrescue(). --- nova/api/openstack/contrib/rescue.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index 5ee071696..a30ed6dff 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -53,7 +53,11 @@ class Rescue(exts.ExtensionDescriptor): return webob.Response(status_int=202) def _unrescue(self, input_dict, req, instance_id): - """Unrescue an instance.""" + """Unrescue an instance. + + We pass exit_rescue=True here so _rescue() knows we would like to exit + rescue mode. + """ self._rescue(input_dict, req, instance_id, exit_rescue=True) def get_name(self): -- cgit From bbcb84a5fed2c537bd6d2143e344fa96f669d231 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Thu, 18 Aug 2011 20:25:32 +0000 Subject: DB password should be an empty string for MySQLdb --- nova/db/sqlalchemy/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 07f281938..643e2338e 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -73,9 +73,11 @@ def get_engine(): elif MySQLdb and "mysql" in connection_dict.drivername: LOG.info(_("Using mysql/eventlet db_pool.")) + # MySQLdb won't accept 'None' in the password field + password = connection_dict.password or '' pool_args = { "db": connection_dict.database, - "passwd": connection_dict.password, + "passwd": password, "host": connection_dict.host, "user": connection_dict.username, "min_size": FLAGS.sql_min_pool_size, -- cgit From 56129e4a0b0c5cb2f8766e023bcaff77fc990008 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Thu, 18 Aug 2011 13:45:45 -0700 Subject: Added more unit testcases for userdata functionality --- nova/tests/test_metadata.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index bfc7a6d44..b06e5c136 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -23,12 +23,21 @@ import httplib import webob +from nova import exception from nova import test from nova import wsgi from nova.api.ec2 import metadatarequesthandler from nova.db.sqlalchemy import api +USER_DATA_STRING = ("This is an encoded string") +ENCODE_USER_DATA_STRING = base64.b64encode(USER_DATA_STRING) + + +def return_non_existing_server_by_address(context, address): + raise exception.NotFound() + + class MetadataTestCase(test.TestCase): """Test that metadata is returning proper values.""" @@ -79,3 +88,34 @@ class MetadataTestCase(test.TestCase): self.stubs.Set(api, 'security_group_get_by_instance', sg_get) self.assertEqual(self.request('/meta-data/security-groups'), 'default\nother') + + def test_user_data_non_existing_fixed_address(self): + self.stubs.Set(api, 'instance_get_all_by_filters', + return_non_existing_server_by_address) + request = webob.Request.blank('/user-data') + request.remote_addr = "127.1.1.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 404) + + def test_user_data_none_fixed_address(self): + self.stubs.Set(api, 'instance_get_all_by_filters', + return_non_existing_server_by_address) + request = webob.Request.blank('/user-data') + request.remote_addr = None + response = request.get_response(self.app) + self.assertEqual(response.status_int, 500) + + def test_user_data_invalid_url(self): + request = webob.Request.blank('/user-data-invalid') + request.remote_addr = "127.0.0.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 404) + + def test_user_data_with_use_forwarded_header(self): + self.instance['user_data'] = ENCODE_USER_DATA_STRING + self.flags(use_forwarded_for=True) + request = webob.Request.blank('/user-data') + request.remote_addr = "127.0.0.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, USER_DATA_STRING) -- cgit From 041dcdb2eba968d5be17c9a10bf333e1307f0537 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 18 Aug 2011 16:56:23 -0400 Subject: Added 'update' method to ServersXMLSerializer --- nova/api/openstack/servers.py | 6 ++ nova/tests/api/openstack/test_servers.py | 121 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 335ecad86..41e63ec3c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -923,6 +923,12 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): node.setAttribute('adminPass', server_dict['server']['adminPass']) return self.to_xml_string(node, True) + def update(self, server_dict): + xml_doc = minidom.Document() + node = self._server_to_xml_detailed(xml_doc, + server_dict['server']) + return self.to_xml_string(node, True) + def create_resource(version='1.0'): controller = { diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index a510d7d97..437620854 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -3740,3 +3740,124 @@ class ServerXMLSerializationTest(test.TestCase): """.replace(" ", "") % (locals())) self.assertEqual(expected.toxml(), actual.toxml()) + + def test_update(self): + serializer = servers.ServerXMLSerializer() + + fixture = { + "server": { + "id": 1, + "uuid": FAKE_UUID, + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": self.IMAGE_BOOKMARK, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": self.FLAVOR_BOOKMARK, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + } + } + + output = serializer.serialize(fixture, 'update') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + expected_uuid = FAKE_UUID + expected = minidom.parseString(""" + + + + + + + + + + + + Stack + + + 1 + + + + + + + + + + + + + + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) -- cgit From f86a5cc4bc43923077ffe1d4098e550841f1c4f0 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 15:58:12 -0500 Subject: Review feedback. --- nova/api/openstack/contrib/rescue.py | 40 +++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index a30ed6dff..399bb7f35 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -26,39 +26,37 @@ from nova.api.openstack import faults LOG = logging.getLogger("nova.api.contrib.rescue") +def wrap_errors(fn): + """"Ensure errors are not passed along.""" + def wrapped(*args): + try: + fn(*args) + except Exception, e: + return faults.Fault(exc.HTTPInternalServerError()) + return wrapped + + class Rescue(exts.ExtensionDescriptor): """The Rescue controller for the OpenStack API.""" def __init__(self): super(Rescue, self).__init__() self.compute_api = compute.API() - def _rescue(self, input_dict, req, instance_id, exit_rescue=False): - """Rescue an instance. - - If exit_rescue is True, rescue mode should be torn down and the - instance restored to its original state. - """ + @wrap_errors + def _rescue(self, input_dict, req, instance_id): + """Rescue an instance.""" context = req.environ["nova.context"] - action = "unrescue" if exit_rescue else "rescue" - - try: - if action == "rescue": - self.compute_api.rescue(context, instance_id) - elif action == "unrescue": - self.compute_api.unrescue(context, instance_id) - except Exception, e: - LOG.exception(_("Error in %(action)s: %(e)s") % locals()) - return faults.Fault(exc.HTTPInternalServerError()) + self.compute_api.rescue(context, instance_id) return webob.Response(status_int=202) + @wrap_errors def _unrescue(self, input_dict, req, instance_id): - """Unrescue an instance. + """Rescue an instance.""" + context = req.environ["nova.context"] + self.compute_api.unrescue(context, instance_id) - We pass exit_rescue=True here so _rescue() knows we would like to exit - rescue mode. - """ - self._rescue(input_dict, req, instance_id, exit_rescue=True) + return webob.Response(status_int=202) def get_name(self): return "Rescue" -- cgit From 22ba538b3cb3ddd22cef0fc06b136db433a8d202 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 16:07:02 -0500 Subject: Oops. --- nova/api/openstack/contrib/rescue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py index 399bb7f35..3de128895 100644 --- a/nova/api/openstack/contrib/rescue.py +++ b/nova/api/openstack/contrib/rescue.py @@ -52,7 +52,7 @@ class Rescue(exts.ExtensionDescriptor): @wrap_errors def _unrescue(self, input_dict, req, instance_id): - """Rescue an instance.""" + """Unrescue an instance.""" context = req.environ["nova.context"] self.compute_api.unrescue(context, instance_id) -- cgit From c718702496a98cefb434b4b21c3ea22fc6c8dc2d Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 18 Aug 2011 17:09:34 -0500 Subject: Added unit test. --- nova/tests/api/openstack/contrib/test_rescue.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 nova/tests/api/openstack/contrib/test_rescue.py diff --git a/nova/tests/api/openstack/contrib/test_rescue.py b/nova/tests/api/openstack/contrib/test_rescue.py new file mode 100644 index 000000000..fc8e4be4e --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_rescue.py @@ -0,0 +1,55 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import webob + +from nova import compute +from nova import test +from nova.tests.api.openstack import fakes + + +def rescue(self, context, instance_id): + pass + + +def unrescue(self, context, instance_id): + pass + + +class RescueTest(test.TestCase): + def setUp(self): + super(RescueTest, self).setUp() + self.stubs.Set(compute.api.API, "rescue", rescue) + self.stubs.Set(compute.api.API, "unrescue", unrescue) + + def test_rescue(self): + body = dict(rescue=None) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + def test_unrescue(self): + body = dict(unrescue=None) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) -- cgit From 32e57db9fdc5c48b3546640e838f5eb260080442 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 18 Aug 2011 16:22:22 -0700 Subject: rename the test method --- nova/tests/test_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py index 8f92406ff..760b150be 100644 --- a/nova/tests/test_service.py +++ b/nova/tests/test_service.py @@ -205,6 +205,6 @@ class TestLauncher(test.TestCase): def test_launch_app(self): self.assertEquals(0, self.service.port) launcher = service.Launcher() - launcher.launch_service(self.service) + launcher.launch_server(self.service) self.assertEquals(0, self.service.port) launcher.stop() -- cgit