diff --git a/.mailmap b/.mailmap index 7e031fc7c14d..3f0238ee9290 100644 --- a/.mailmap +++ b/.mailmap @@ -29,6 +29,7 @@ + @@ -36,6 +37,7 @@ + @@ -44,5 +46,4 @@ - diff --git a/Authors b/Authors index 0aaf084b55e2..7e8b6369ff66 100644 --- a/Authors +++ b/Authors @@ -44,6 +44,7 @@ Josh Kearney Josh Kleinpeter Joshua McKenty Justin Santa Barbara +Justin Shepherd Kei Masumoto Ken Pepple Kevin Bringard diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3cf78e32c2db..54731061300c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -594,10 +594,7 @@ class ControllerV10(Controller): def _parse_update(self, context, server_id, inst_dict, update_dict): if 'adminPass' in inst_dict['server']: update_dict['admin_pass'] = inst_dict['server']['adminPass'] - try: - self.compute_api.set_admin_password(context, server_id) - except exception.TimeoutException: - return exc.HTTPRequestTimeout() + self.compute_api.set_admin_password(context, server_id) def _action_rebuild(self, info, request, instance_id): context = request.environ['nova.context'] diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 98d5f6eb6d3e..07235a2a79af 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -305,9 +305,9 @@ class AuthManager(object): if check_type == 's3': sign = signer.Signer(user.secret.encode()) expected_signature = sign.s3_authorization(headers, verb, path) - LOG.debug('user.secret: %s', user.secret) - LOG.debug('expected_signature: %s', expected_signature) - LOG.debug('signature: %s', signature) + LOG.debug(_('user.secret: %s'), user.secret) + LOG.debug(_('expected_signature: %s'), expected_signature) + LOG.debug(_('signature: %s'), signature) if signature != expected_signature: LOG.audit(_("Invalid signature for user %s"), user.name) raise exception.InvalidSignature(signature=signature, @@ -317,10 +317,20 @@ class AuthManager(object): # secret isn't unicode expected_signature = signer.Signer(user.secret.encode()).generate( params, verb, server_string, path) - LOG.debug('user.secret: %s', user.secret) - LOG.debug('expected_signature: %s', expected_signature) - LOG.debug('signature: %s', signature) + LOG.debug(_('user.secret: %s'), user.secret) + LOG.debug(_('expected_signature: %s'), expected_signature) + LOG.debug(_('signature: %s'), signature) if signature != expected_signature: + (addr_str, port_str) = utils.parse_server_string(server_string) + # If the given server_string contains port num, try without it. + if port_str != '': + host_only_signature = signer.Signer( + user.secret.encode()).generate(params, verb, + addr_str, path) + LOG.debug(_('host_only_signature: %s'), + host_only_signature) + if signature == host_only_signature: + return (user, project) LOG.audit(_("Invalid signature for user %s"), user.name) raise exception.InvalidSignature(signature=signature, user=user) diff --git a/nova/compute/api.py b/nova/compute/api.py index be26d8ca33fe..63884be97f9e 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -482,6 +482,17 @@ class API(base.Base): """Generic handler for RPC calls to the scheduler.""" rpc.cast(context, FLAGS.scheduler_topic, args) + def _find_host(self, context, instance_id): + """Find the host associated with an instance.""" + for attempts in xrange(10): + instance = self.get(context, instance_id) + host = instance["host"] + if host: + return host + time.sleep(1) + raise exception.Error(_("Unable to find host for Instance %s") + % instance_id) + def snapshot(self, context, instance_id, name): """Snapshot the given instance. @@ -635,8 +646,12 @@ class API(base.Base): def set_admin_password(self, context, instance_id, password=None): """Set the root/admin password for the given instance.""" - self._cast_compute_message( - 'set_admin_password', context, instance_id, password) + host = self._find_host(context, instance_id) + + rpc.cast(context, + self.db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "set_admin_password", + "args": {"instance_id": instance_id, "new_pass": password}}) def inject_file(self, context, instance_id): """Write a file to the given instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index c6f9570732e2..556b3b3b9d4c 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -40,6 +40,7 @@ import os import socket import sys import tempfile +import time import functools from eventlet import greenthread @@ -130,6 +131,7 @@ class ComputeManager(manager.SchedulerDependentManager): self.network_manager = utils.import_object(FLAGS.network_manager) self.volume_manager = utils.import_object(FLAGS.volume_manager) self.network_api = network.API() + self._last_host_check = 0 super(ComputeManager, self).__init__(service_name="compute", *args, **kwargs) @@ -404,21 +406,30 @@ class ComputeManager(manager.SchedulerDependentManager): def set_admin_password(self, context, instance_id, new_pass=None): """Set the root/admin password for an instance on this host.""" context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) - instance_id = instance_ref['id'] - instance_state = instance_ref['state'] - expected_state = power_state.RUNNING - if instance_state != expected_state: - LOG.warn(_('trying to reset the password on a non-running ' - 'instance: %(instance_id)s (state: %(instance_state)s ' - 'expected: %(expected_state)s)') % locals()) - LOG.audit(_('instance %s: setting admin password'), - instance_ref['name']) + if new_pass is None: # Generate a random password new_pass = utils.generate_password(FLAGS.password_length) - self.driver.set_admin_password(instance_ref, new_pass) - self._update_state(context, instance_id) + + while True: + instance_ref = self.db.instance_get(context, instance_id) + instance_id = instance_ref["id"] + instance_state = instance_ref["state"] + expected_state = power_state.RUNNING + + if instance_state != expected_state: + time.sleep(5) + continue + else: + try: + self.driver.set_admin_password(instance_ref, new_pass) + LOG.audit(_("Instance %s: Root password set"), + instance_ref["name"]) + break + except Exception, e: + # Catch all here because this could be anything. + LOG.exception(e) + continue @exception.wrap_exception @checks_instance_lock @@ -1083,6 +1094,13 @@ class ComputeManager(manager.SchedulerDependentManager): unicode(ex)) error_list.append(ex) + try: + self._report_driver_status() + except Exception as ex: + LOG.warning(_("Error during report_driver_status(): %s"), + unicode(ex)) + error_list.append(ex) + try: self._poll_instance_states(context) except Exception as ex: @@ -1092,6 +1110,16 @@ class ComputeManager(manager.SchedulerDependentManager): return error_list + def _report_driver_status(self): + curr_time = time.time() + if curr_time - self._last_host_check> FLAGS.host_state_interval: + self._last_host_check = curr_time + LOG.info(_("Updating host status")) + # This will grab info about the host and queue it + # to be sent to the Schedulers. + self.update_service_capabilities( + self.driver.get_host_stats(refresh=True)) + def _poll_instance_states(self, context): vm_instances = self.driver.list_instances_detail() vm_instances = dict((vm.name, vm) for vm in vm_instances) diff --git a/nova/exception.py b/nova/exception.py index 5caad4cf3e7a..9905fb19bd76 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -457,6 +457,11 @@ class ZoneNotFound(NotFound): message = _("Zone %(zone_id)s could not be found.") +class SchedulerHostFilterDriverNotFound(NotFound): + message = _("Scheduler Host Filter Driver %(driver_name)s could" + " not be found.") + + class InstanceMetadataNotFound(NotFound): message = _("Instance %(instance_id)s has no metadata with " "key %(metadata_key)s.") diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 6bb3bf3cd0c0..816ae5513126 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -76,11 +76,9 @@ def zone_update(context, zone_id, data): return db.zone_update(context, zone_id, data) -def get_zone_capabilities(context, service=None): - """Returns a dict of key, value capabilities for this zone, - or for a particular class of services running in this zone.""" - return _call_scheduler('get_zone_capabilities', context=context, - params=dict(service=service)) +def get_zone_capabilities(context): + """Returns a dict of key, value capabilities for this zone.""" + return _call_scheduler('get_zone_capabilities', context=context) def update_service_capabilities(context, service_name, host, capabilities): diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py new file mode 100644 index 000000000000..483f3225cc5f --- /dev/null +++ b/nova/scheduler/host_filter.py @@ -0,0 +1,288 @@ +# 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. + +""" +Host Filter is a driver mechanism for requesting instance resources. +Three drivers 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. + +Note: These are "required" capability filters. These capabilities +used must be present or the host will be excluded. The hosts +returned are then weighed by the Weighted Scheduler. Weights +can take the more esoteric factors into consideration (such as +server affinity and customer separation). +""" + +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_driver', + 'nova.scheduler.host_filter.AllHostsFilter', + 'Which driver to use for filtering hosts.') + + +class HostFilter(object): + """Base class for host filter drivers.""" + + 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 driver""" + return "%s.%s" % (self.__module__, self.__class__.__name__) + + +class AllHostsFilter(HostFilter): + """NOP host filter driver. 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 FlavorFilter(HostFilter): + """HostFilter driver hard-coded to work with flavors.""" + + def instance_type_to_filter(self, instance_type): + """Use instance_type to filter hosts.""" + return (self._full_name(), instance_type) + + 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'] + if host_ram_mb>= instance_type['memory_mb'] and \ + disk_bytes>= instance_type['local_gb']: + 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 driver 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 + + +DRIVERS = [AllHostsFilter, FlavorFilter, JsonFilter] + + +def choose_driver(driver_name=None): + """Since the caller may specify which driver to use we need + to have an authoritative list of what is permissible. This + function checks the driver name against a predefined set + of acceptable drivers.""" + + if not driver_name: + driver_name = FLAGS.default_host_filter_driver + for driver in DRIVERS: + if "%s.%s" % (driver.__module__, driver.__name__) == driver_name: + return driver() + raise exception.SchedulerHostFilterDriverNotFound(driver_name=driver_name) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 7d62cfc4e0a4..55cd7208baeb 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -60,10 +60,9 @@ class SchedulerManager(manager.Manager): """Get a list of zones from the ZoneManager.""" return self.zone_manager.get_zone_list() - def get_zone_capabilities(self, context=None, service=None): - """Get the normalized set of capabilites for this zone, - or for a particular service.""" - return self.zone_manager.get_zone_capabilities(context, service) + def get_zone_capabilities(self, context=None): + """Get the normalized set of capabilites for this zone.""" + return self.zone_manager.get_zone_capabilities(context) def update_service_capabilities(self, context=None, service_name=None, host=None, capabilities={}): diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index 198f9d4cc769..3ddf6f3c3274 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -106,28 +106,26 @@ class ZoneManager(object): def __init__(self): self.last_zone_db_check = datetime.min self.zone_states = {} # { : ZoneState } - self.service_states = {} # { : { : { cap k : v }}} + self.service_states = {} # { : { : { cap k : v }}} self.green_pool = greenpool.GreenPool() def get_zone_list(self): """Return the list of zones we know about.""" return [zone.to_dict() for zone in self.zone_states.values()] - def get_zone_capabilities(self, context, service=None): + def get_zone_capabilities(self, context): """Roll up all the individual host info to generic 'service' capabilities. Each capability is aggregated into _min and _max values.""" - service_dict = self.service_states - if service: - service_dict = {service: self.service_states.get(service, {})} + hosts_dict = self.service_states # TODO(sandy) - be smarter about fabricating this structure. # But it's likely to change once we understand what the Best-Match # code will need better. combined = {} # { _ : (min, max), ... } - for service_name, host_dict in service_dict.iteritems(): - for host, caps_dict in host_dict.iteritems(): - for cap, value in caps_dict.iteritems(): + for host, host_dict in hosts_dict.iteritems(): + for service_name, service_dict in host_dict.iteritems(): + for cap, value in service_dict.iteritems(): key = "%s_%s" % (service_name, cap) min_value, max_value = combined.get(key, (value, value)) min_value = min(min_value, value) @@ -171,6 +169,6 @@ class ZoneManager(object): """Update the per-service capabilities based on this notification.""" logging.debug(_("Received %(service_name)s service update from " "%(host)s: %(capabilities)s") % locals()) - service_caps = self.service_states.get(service_name, {}) - service_caps[host] = capabilities - self.service_states[service_name] = service_caps + service_caps = self.service_states.get(host, {}) + service_caps[service_name] = capabilities + self.service_states[host] = service_caps diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 5c643fcef1de..89edece423da 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -134,6 +134,10 @@ def fake_compute_api(cls, req, id): return True +def find_host(self, context, instance_id): + return "nova" + + class ServersTest(test.TestCase): def setUp(self): @@ -473,6 +477,7 @@ class ServersTest(test.TestCase): "_get_kernel_ramdisk_from_image", kernel_ramdisk_mapping) self.stubs.Set(nova.api.openstack.common, "get_image_id_from_image_hash", image_id_from_hash) + self.stubs.Set(nova.compute.api.API, "_find_host", find_host) def _test_create_instance_helper(self): self._setup_for_create_instance() @@ -767,6 +772,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_update', server_update) + self.stubs.Set(nova.compute.api.API, "_find_host", find_host) req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py index a3f191aaac71..5d5799b59fac 100644 --- a/nova/tests/api/openstack/test_zones.py +++ b/nova/tests/api/openstack/test_zones.py @@ -75,7 +75,7 @@ def zone_get_all_db(context): ] -def zone_capabilities(method, context, params): +def zone_capabilities(method, context): return dict() diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py index f8a1b1564ab4..f02dd94b782b 100644 --- a/nova/tests/test_auth.py +++ b/nova/tests/test_auth.py @@ -101,9 +101,43 @@ class _AuthManagerBaseTestCase(test.TestCase): self.assertEqual('private-party', u.access) def test_004_signature_is_valid(self): - #self.assertTrue(self.manager.authenticate(**boto.generate_url ...? )) - pass - #raise NotImplementedError + with user_generator(self.manager, name='admin', secret='admin', + access='admin'): + with project_generator(self.manager, name="admin", + manager_user='admin'): + accesskey = 'admin:admin' + expected_result = (self.manager.get_user('admin'), + self.manager.get_project('admin')) + # captured sig and query string using boto 1.9b/euca2ools 1.2 + sig = 'd67Wzd9Bwz8xid9QU+lzWXcF2Y3tRicYABPJgrqfrwM=' + auth_params = {'AWSAccessKeyId': 'admin:admin', + 'Action': 'DescribeAvailabilityZones', + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'Timestamp': '2011-04-22T11:29:29', + 'Version': '2009-11-30'} + self.assertTrue(expected_result, self.manager.authenticate( + accesskey, + sig, + auth_params, + 'GET', + '127.0.0.1:8773', + '/services/Cloud/')) + # captured sig and query string using RightAWS 1.10.0 + sig = 'ECYLU6xdFG0ZqRVhQybPJQNJ5W4B9n8fGs6+/fuGD2c=' + auth_params = {'AWSAccessKeyId': 'admin:admin', + 'Action': 'DescribeAvailabilityZones', + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'Timestamp': '2011-04-22T11:29:49.000Z', + 'Version': '2008-12-01'} + self.assertTrue(expected_result, self.manager.authenticate( + accesskey, + sig, + auth_params, + 'GET', + '127.0.0.1', + '/services/Cloud')) def test_005_can_get_credentials(self): return diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 393110791548..55e7ae0c46c6 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -21,6 +21,7 @@ Tests For Compute import datetime import mox +import stubout from nova import compute from nova import context @@ -52,6 +53,10 @@ class FakeTime(object): self.counter += t +def nop_report_driver_status(self): + pass + + class ComputeTestCase(test.TestCase): """Test case for compute""" def setUp(self): @@ -649,6 +654,10 @@ class ComputeTestCase(test.TestCase): def test_run_kill_vm(self): """Detect when a vm is terminated behind the scenes""" + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(compute_manager.ComputeManager, + '_report_driver_status', nop_report_driver_status) + instance_id = self._create_instance() self.compute.run_instance(self.context, instance_id) diff --git a/nova/tests/test_host_filter.py b/nova/tests/test_host_filter.py new file mode 100644 index 000000000000..c029d41e6afa --- /dev/null +++ b/nova/tests/test_host_filter.py @@ -0,0 +1,208 @@ +# 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 Filter Drivers. +""" + +import json + +from nova import exception +from nova import flags +from nova import test +from nova.scheduler import host_filter + +FLAGS = flags.FLAGS + + +class FakeZoneManager: + pass + + +class HostFilterTestCase(test.TestCase): + """Test case for host filter drivers.""" + + 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): + self.old_flag = FLAGS.default_host_filter_driver + FLAGS.default_host_filter_driver = \ + 'nova.scheduler.host_filter.AllHostsFilter' + self.instance_type = dict(name='tiny', + memory_mb=50, + vcpus=10, + local_gb=500, + flavorid=1, + swap=500, + rxtx_quota=30000, + rxtx_cap=200) + + 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 tearDown(self): + FLAGS.default_host_filter_driver = self.old_flag + + def test_choose_driver(self): + # Test default driver ... + driver = host_filter.choose_driver() + self.assertEquals(driver._full_name(), + 'nova.scheduler.host_filter.AllHostsFilter') + # Test valid driver ... + driver = host_filter.choose_driver( + 'nova.scheduler.host_filter.FlavorFilter') + self.assertEquals(driver._full_name(), + 'nova.scheduler.host_filter.FlavorFilter') + # Test invalid driver ... + try: + host_filter.choose_driver('does not exist') + self.fail("Should not find driver") + except exception.SchedulerHostFilterDriverNotFound: + pass + + def test_all_host_driver(self): + driver = host_filter.AllHostsFilter() + cooked = driver.instance_type_to_filter(self.instance_type) + hosts = driver.filter_hosts(self.zone_manager, cooked) + self.assertEquals(10, len(hosts)) + for host, capabilities in hosts: + self.assertTrue(host.startswith('host')) + + def test_flavor_driver(self): + driver = host_filter.FlavorFilter() + # filter all hosts that can support 50 ram and 500 disk + name, cooked = driver.instance_type_to_filter(self.instance_type) + self.assertEquals('nova.scheduler.host_filter.FlavorFilter', name) + hosts = driver.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_driver(self): + driver = host_filter.JsonFilter() + # filter all hosts that can support 50 ram and 500 disk + name, cooked = driver.instance_type_to_filter(self.instance_type) + self.assertEquals('nova.scheduler.host_filter.JsonFilter', name) + hosts = driver.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 = driver.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 = driver.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 = driver.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: + driver.filter_hosts(self.zone_manager, cooked) + self.fail("Should give KeyError") + except KeyError, e: + pass + + self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps([]))) + self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps({}))) + self.assertTrue(driver.filter_hosts(self.zone_manager, json.dumps( + ['not', True, False, True, False] + ))) + + try: + driver.filter_hosts(self.zone_manager, json.dumps( + 'not', True, False, True, False + )) + self.fail("Should give KeyError") + except KeyError, e: + pass + + self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( + ['=', '$foo', 100] + ))) + self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( + ['=', '$.....', 100] + ))) + self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( + ['>', ['and', ['or', ['not', ['<', ['>=', ['<=', ['in', ]]]]]]]] + ))) + + self.assertFalse(driver.filter_hosts(self.zone_manager, json.dumps( + ['=', {}, ['>', '$missing....foo']] + ))) diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index e08d229b0de5..e7b5c826e878 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -250,3 +250,28 @@ class GetFromPathTestCase(test.TestCase): input = {'a': [1, 2, {'b': 'b_1'}]} self.assertEquals([1, 2, {'b': 'b_1'}], f(input, "a")) self.assertEquals(['b_1'], f(input, "a/b")) + + +class GenericUtilsTestCase(test.TestCase): + def test_parse_server_string(self): + result = utils.parse_server_string('::1') + self.assertEqual(('::1', ''), result) + result = utils.parse_server_string('[::1]:8773') + self.assertEqual(('::1', '8773'), result) + result = utils.parse_server_string('2001:db8::192.168.1.1') + self.assertEqual(('2001:db8::192.168.1.1', ''), result) + result = utils.parse_server_string('[2001:db8::192.168.1.1]:8773') + self.assertEqual(('2001:db8::192.168.1.1', '8773'), result) + result = utils.parse_server_string('192.168.1.1') + self.assertEqual(('192.168.1.1', ''), result) + result = utils.parse_server_string('192.168.1.2:8773') + self.assertEqual(('192.168.1.2', '8773'), result) + result = utils.parse_server_string('192.168.1.3') + self.assertEqual(('192.168.1.3', ''), result) + result = utils.parse_server_string('www.example.com:8443') + self.assertEqual(('www.example.com', '8443'), result) + result = utils.parse_server_string('www.example.com') + self.assertEqual(('www.example.com', ''), result) + # error case + result = utils.parse_server_string('www.exa:mple.com:8443') + self.assertEqual(('', ''), result) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 375480a2edc8..6072f5455e30 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -17,6 +17,7 @@ """Test suite for XenAPI.""" import functools +import json import os import re import stubout @@ -665,3 +666,52 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase): self.fake_instance.image_id = glance_stubs.FakeGlance.IMAGE_VHD self.fake_instance.kernel_id = None self.assert_disk_type(vm_utils.ImageType.DISK_VHD) + + +class FakeXenApi(object): + """Fake XenApi for testing HostState.""" + + class FakeSR(object): + def get_record(self, ref): + return {'virtual_allocation': 10000, + 'physical_utilisation': 20000} + + SR = FakeSR() + + +class FakeSession(object): + """Fake Session class for HostState testing.""" + + def async_call_plugin(self, *args): + return None + + def wait_for_task(self, *args): + vm = {'total': 10, + 'overhead': 20, + 'free': 30, + 'free-computed': 40} + return json.dumps({'host_memory': vm}) + + def get_xenapi(self): + return FakeXenApi() + + +class HostStateTestCase(test.TestCase): + """Tests HostState, which holds metrics from XenServer that get + reported back to the Schedulers.""" + + def _fake_safe_find_sr(self, session): + """None SR ref since we're ignoring it in FakeSR.""" + return None + + def test_host_state(self): + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(vm_utils, 'safe_find_sr', self._fake_safe_find_sr) + host_state = xenapi_conn.HostState(FakeSession()) + stats = host_state._stats + self.assertEquals(stats['disk_total'], 10000) + self.assertEquals(stats['disk_used'], 20000) + self.assertEquals(stats['host_memory_total'], 10) + self.assertEquals(stats['host_memory_overhead'], 20) + self.assertEquals(stats['host_memory_free'], 30) + self.assertEquals(stats['host_memory_free_computed'], 40) diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py index 688dc704d0de..e132809dc30e 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -78,38 +78,32 @@ class ZoneManagerTestCase(test.TestCase): def test_service_capabilities(self): zm = zone_manager.ZoneManager() - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, {}) zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2))) zm.update_service_capabilities("svc1", "host1", dict(a=2, b=3)) - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(2, 2), svc1_b=(3, 3))) zm.update_service_capabilities("svc1", "host2", dict(a=20, b=30)) - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30))) zm.update_service_capabilities("svc10", "host1", dict(a=99, b=99)) - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30), svc10_a=(99, 99), svc10_b=(99, 99))) zm.update_service_capabilities("svc1", "host3", dict(c=5)) - caps = zm.get_zone_capabilities(self, None) + caps = zm.get_zone_capabilities(None) self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30), svc1_c=(5, 5), svc10_a=(99, 99), svc10_b=(99, 99))) - caps = zm.get_zone_capabilities(self, 'svc1') - self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30), - svc1_c=(5, 5))) - caps = zm.get_zone_capabilities(self, 'svc10') - self.assertEquals(caps, dict(svc10_a=(99, 99), svc10_b=(99, 99))) - def test_refresh_from_db_replace_existing(self): zm = zone_manager.ZoneManager() zone_state = zone_manager.ZoneState() diff --git a/nova/utils.py b/nova/utils.py index bfcf79216a88..80bf1197fbd2 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -709,3 +709,33 @@ def check_isinstance(obj, cls): raise Exception(_('Expected object of type: %s') % (str(cls))) # TODO(justinsb): Can we make this better?? return cls() # Ugly PyLint hack + + +def parse_server_string(server_str): + """ + Parses the given server_string and returns a list of host and port. + If it's not a combination of host part and port, the port element + is a null string. If the input is invalid expression, return a null + list. + """ + try: + # First of all, exclude pure IPv6 address (w/o port). + if netaddr.valid_ipv6(server_str): + return (server_str, '') + + # Next, check if this is IPv6 address with a port number combination. + if server_str.find("]:") != -1: + (address, port) = server_str.replace('[', '', 1).split(']:') + return (address, port) + + # Third, check if this is a combination of an address and a port + if server_str.find(':') == -1: + return (server_str, '') + + # This must be a combination of an address and a port + (address, port) = server_str.split(':') + return (address, port) + + except: + LOG.debug(_('Invalid server_string: %s' % server_str)) + return ('', '') diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 9026e737e1a0..1142e97a4610 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -486,3 +486,11 @@ class HyperVConnection(driver.ComputeDriver): def update_available_resource(self, ctxt, host): """This method is supported only by libvirt.""" return + + def update_host_status(self): + """See xenapi_conn.py implementation.""" + pass + + def get_host_stats(self, refresh=False): + """See xenapi_conn.py implementation.""" + pass diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 9780c69a621b..555e44ce2c26 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -1582,6 +1582,14 @@ class LibvirtConnection(driver.ComputeDriver): """See comments of same method in firewall_driver.""" self.firewall_driver.unfilter_instance(instance_ref) + def update_host_status(self): + """See xenapi_conn.py implementation.""" + pass + + def get_host_stats(self, refresh=False): + """See xenapi_conn.py implementation.""" + pass + class FirewallDriver(object): def prepare_instance_filter(self, instance, network_info=None): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 30f31517ddc9..fe9a74dd6b29 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -428,11 +428,12 @@ class VMOps(object): """ # Need to uniquely identify this request. - transaction_id = str(uuid.uuid4()) + key_init_transaction_id = str(uuid.uuid4()) # The simple Diffie-Hellman class is used to manage key exchange. dh = SimpleDH() - args = {'id': transaction_id, 'pub': str(dh.get_public())} - resp = self._make_agent_call('key_init', instance, '', args) + key_init_args = {'id': key_init_transaction_id, + 'pub': str(dh.get_public())} + resp = self._make_agent_call('key_init', instance, '', key_init_args) if resp is None: # No response from the agent return @@ -446,8 +447,9 @@ class VMOps(object): dh.compute_shared(agent_pub) enc_pass = dh.encrypt(new_pass) # Send the encrypted password - args['enc_pass'] = enc_pass - resp = self._make_agent_call('password', instance, '', args) + password_transaction_id = str(uuid.uuid4()) + password_args = {'id': password_transaction_id, 'enc_pass': enc_pass} + resp = self._make_agent_call('password', instance, '', password_args) if resp is None: # No response from the agent return diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 0cabccf082f4..8e90852776e3 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -57,6 +57,8 @@ reactor thread if the VM.get_by_name_label or VM.get_record calls block. - suffix "_rec" for record objects """ +import json +import random import sys import urlparse import xmlrpclib @@ -67,10 +69,12 @@ from eventlet import timeout from nova import context from nova import db +from nova import exception from nova import utils from nova import flags from nova import log as logging from nova.virt import driver +from nova.virt.xenapi import vm_utils from nova.virt.xenapi.vmops import VMOps from nova.virt.xenapi.volumeops import VolumeOps @@ -168,6 +172,13 @@ class XenAPIConnection(driver.ComputeDriver): session = XenAPISession(url, user, pw) self._vmops = VMOps(session) self._volumeops = VolumeOps(session) + self._host_state = None + + @property + def HostState(self): + if not self._host_state: + self._host_state = HostState(self.session) + return self._host_state def init_host(self, host): #FIXME(armando): implement this @@ -315,6 +326,16 @@ class XenAPIConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') + def update_host_status(self): + """Update the status info of the host, and return those values + to the calling program.""" + return self.HostState.update_status() + + def get_host_stats(self, refresh=False): + """Return the current state of the host. If 'refresh' is + True, run the update first.""" + return self.HostState.get_host_stats(refresh=refresh) + class XenAPISession(object): """The session to invoke XenAPI SDK calls""" @@ -436,6 +457,65 @@ class XenAPISession(object): raise +class HostState(object): + """Manages information about the XenServer host this compute + node is running on. + """ + def __init__(self, session): + super(HostState, self).__init__() + self._session = session + self._stats = {} + self.update_status() + + def get_host_stats(self, refresh=False): + """Return the current state of the host. If 'refresh' is + True, run the update first. + """ + if refresh: + self.update_status() + return self._stats + + def update_status(self): + """Since under Xenserver, a compute node runs on a given host, + we can get host status information using xenapi. + """ + LOG.debug(_("Updating host stats")) + # Make it something unlikely to match any actual instance ID + task_id = random.randint(-80000, -70000) + task = self._session.async_call_plugin("xenhost", "host_data", {}) + task_result = self._session.wait_for_task(task, task_id) + if not task_result: + task_result = json.dumps("") + try: + data = json.loads(task_result) + except ValueError as e: + # Invalid JSON object + LOG.error(_("Unable to get updated status: %s") % e) + return + # Get the SR usage + try: + sr_ref = vm_utils.safe_find_sr(self._session) + except exception.NotFound as e: + # No SR configured + LOG.error(_("Unable to get SR for this host: %s") % e) + return + sr_rec = self._session.get_xenapi().SR.get_record(sr_ref) + total = int(sr_rec["virtual_allocation"]) + used = int(sr_rec["physical_utilisation"]) + data["disk_total"] = total + data["disk_used"] = used + data["disk_available"] = total - used + host_memory = data.get('host_memory', None) + if host_memory: + data["host_memory_total"] = host_memory.get('total', 0) + data["host_memory_overhead"] = host_memory.get('overhead', 0) + data["host_memory_free"] = host_memory.get('free', 0) + data["host_memory_free_computed"] = \ + host_memory.get('free-computed', 0) + del data['host_memory'] + self._stats = data + + def _parse_xmlrpc_value(val): """Parse the given value as if it were an XML-RPC value. This is sometimes used as the format for the task.result field.""" diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index 5496a6bd5643..9e761f2640bc 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -53,7 +53,6 @@ class TimeoutError(StandardError): pass -@jsonify def key_init(self, arg_dict): """Handles the Diffie-Hellman key exchange with the agent to establish the shared secret key used to encrypt/decrypt sensitive @@ -72,7 +71,6 @@ def key_init(self, arg_dict): return resp -@jsonify def password(self, arg_dict): """Writes a request to xenstore that tells the agent to set the root password for the given VM. The password should be @@ -80,7 +78,6 @@ def password(self, arg_dict): previous call to key_init. The encrypted password value should be passed as the value for the 'enc_pass' key in arg_dict. """ - pub = int(arg_dict["pub"]) enc_pass = arg_dict["enc_pass"] arg_dict["value"] = json.dumps({"name": "password", "value": enc_pass}) request_id = arg_dict["id"] diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py index d33c7346b8a5..6c589ed29f12 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py @@ -59,12 +59,12 @@ def read_record(self, arg_dict): cmd = ["xenstore-read", "/local/domain/%(dom_id)s/%(path)s" % arg_dict] try: ret, result = _run_command(cmd) - return result.rstrip("\n") + return result.strip() except pluginlib.PluginError, e: if arg_dict.get("ignore_missing_path", False): cmd = ["xenstore-exists", "/local/domain/%(dom_id)s/%(path)s" % arg_dict] - ret, result = _run_command(cmd).strip() + ret, result = _run_command(cmd) # If the path exists, the cmd should return "0" if ret != 0: # No such path, so ignore the error and return the @@ -171,7 +171,7 @@ def _paths_from_ls(recs): def _run_command(cmd): """Abstracts out the basics of issuing system commands. If the command returns anything in stderr, a PluginError is raised with that information. - Otherwise, the output from stdout is returned. + Otherwise, a tuple of (return code, stdout data) is returned. """ pipe = subprocess.PIPE proc = subprocess.Popen(cmd, stdin=pipe, stdout=pipe, stderr=pipe, @@ -180,7 +180,7 @@ def _run_command(cmd): err = proc.stderr.read() if err: raise pluginlib.PluginError(err) - return proc.stdout.read() + return (ret, proc.stdout.read()) if __name__ == "__main__": diff --git a/tools/pip-requires b/tools/pip-requires index 013c3ac49d13..8f8018765f86 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -31,3 +31,6 @@ sphinx glance nova-adminclient suds==0.4 +coverage +nosexcover +GitPython

AltStyle によって変換されたページ (->オリジナル) /