 38b630a93a
			
		
	
	38b630a93a
	
	
	
		
			
			Fixes bug #910763 According to PEP8, - Object type comparisons should always use isinstance() instead of comparing types directly. Yes: if isinstance(obj, int): No: if type(obj) is type(1): When checking if an object is a string, keep in mind that it might be a unicode string too! In Python 2.3, str and unicode have a common base class, basestring, so you can do: if isinstance(obj, basestring): Change-Id: I7c0fdecf99872f5b8f72b2c2ed4f5c539c33def1
		
			
				
	
	
		
			356 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # 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.
 | |
| 
 | |
| """
 | |
| ZoneManager oversees all communications with child Zones.
 | |
| """
 | |
| 
 | |
| import datetime
 | |
| import thread
 | |
| import traceback
 | |
| import UserDict
 | |
| 
 | |
| from eventlet import greenpool
 | |
| from novaclient import v1_1 as novaclient
 | |
| 
 | |
| from nova import db
 | |
| from nova import flags
 | |
| from nova import log as logging
 | |
| from nova import utils
 | |
| 
 | |
| FLAGS = flags.FLAGS
 | |
| flags.DEFINE_integer('zone_db_check_interval', 60,
 | |
|         'Seconds between getting fresh zone info from db.')
 | |
| flags.DEFINE_integer('zone_failures_to_offline', 3,
 | |
|         'Number of consecutive errors before marking zone offline')
 | |
| flags.DEFINE_integer('reserved_host_disk_mb', 0,
 | |
|         'Amount of disk in MB to reserve for host/dom0')
 | |
| flags.DEFINE_integer('reserved_host_memory_mb', 512,
 | |
|         'Amount of memory in MB to reserve for host/dom0')
 | |
| 
 | |
| 
 | |
| class ZoneState(object):
 | |
|     """Holds the state of all connected child zones."""
 | |
|     def __init__(self):
 | |
|         self.is_active = True
 | |
|         self.name = None
 | |
|         self.capabilities = None
 | |
|         self.attempt = 0
 | |
|         self.last_seen = datetime.datetime.min
 | |
|         self.last_exception = None
 | |
|         self.last_exception_time = None
 | |
| 
 | |
|     def update_credentials(self, zone):
 | |
|         """Update zone credentials from db"""
 | |
|         self.zone_id = zone.id
 | |
|         self.name = zone.name
 | |
|         self.api_url = zone.api_url
 | |
|         self.username = zone.username
 | |
|         self.password = zone.password
 | |
|         self.weight_offset = zone.weight_offset
 | |
|         self.weight_scale = zone.weight_scale
 | |
| 
 | |
|     def update_metadata(self, zone_metadata):
 | |
|         """Update zone metadata after successful communications with
 | |
|            child zone."""
 | |
|         self.last_seen = utils.utcnow()
 | |
|         self.attempt = 0
 | |
|         self.capabilities = ", ".join(["%s=%s" % (k, v)
 | |
|                         for k, v in zone_metadata.iteritems() if k != 'name'])
 | |
|         self.is_active = True
 | |
| 
 | |
|     def to_dict(self):
 | |
|         return dict(name=self.name, capabilities=self.capabilities,
 | |
|                     is_active=self.is_active, api_url=self.api_url,
 | |
|                     id=self.zone_id, weight_scale=self.weight_scale,
 | |
|                     weight_offset=self.weight_offset)
 | |
| 
 | |
|     def log_error(self, exception):
 | |
|         """Something went wrong. Check to see if zone should be
 | |
|            marked as offline."""
 | |
|         self.last_exception = exception
 | |
|         self.last_exception_time = utils.utcnow()
 | |
|         api_url = self.api_url
 | |
|         logging.warning(_("'%(exception)s' error talking to "
 | |
|                           "zone %(api_url)s") % locals())
 | |
| 
 | |
|         max_errors = FLAGS.zone_failures_to_offline
 | |
|         self.attempt += 1
 | |
|         if self.attempt >= max_errors:
 | |
|             self.is_active = False
 | |
|             logging.error(_("No answer from zone %(api_url)s "
 | |
|                             "after %(max_errors)d "
 | |
|                             "attempts. Marking inactive.") % locals())
 | |
| 
 | |
| 
 | |
| def _call_novaclient(zone):
 | |
|     """Call novaclient. Broken out for testing purposes. Note that
 | |
|     we have to use the admin credentials for this since there is no
 | |
|     available context."""
 | |
|     client = novaclient.Client(zone.username, zone.password, None,
 | |
|                                zone.api_url, region_name=zone.name)
 | |
|     return client.zones.info()._info
 | |
| 
 | |
| 
 | |
| def _poll_zone(zone):
 | |
|     """Eventlet worker to poll a zone."""
 | |
|     name = zone.name
 | |
|     url = zone.api_url
 | |
|     logging.debug(_("Polling zone: %(name)s @ %(url)s") % locals())
 | |
|     try:
 | |
|         zone.update_metadata(_call_novaclient(zone))
 | |
|     except Exception, e:
 | |
|         zone.log_error(traceback.format_exc())
 | |
| 
 | |
| 
 | |
| class ReadOnlyDict(UserDict.IterableUserDict):
 | |
|     """A read-only dict."""
 | |
|     def __init__(self, source=None):
 | |
|         self.update(source)
 | |
| 
 | |
|     def __setitem__(self, key, item):
 | |
|         raise TypeError
 | |
| 
 | |
|     def __delitem__(self, key):
 | |
|         raise TypeError
 | |
| 
 | |
|     def clear(self):
 | |
|         raise TypeError
 | |
| 
 | |
|     def pop(self, key, *args):
 | |
|         raise TypeError
 | |
| 
 | |
|     def popitem(self):
 | |
|         raise TypeError
 | |
| 
 | |
|     def update(self, source=None):
 | |
|         if source is None:
 | |
|             return
 | |
|         elif isinstance(source, UserDict.UserDict):
 | |
|             self.data = source.data
 | |
|         elif isinstance(source, dict):
 | |
|             self.data = source
 | |
|         else:
 | |
|             raise TypeError
 | |
| 
 | |
| 
 | |
| class HostInfo(object):
 | |
|     """Mutable and immutable information on hosts tracked
 | |
|     by the ZoneManager. This is an attempt to remove the
 | |
|     ad-hoc data structures previously used and lock down
 | |
|     access."""
 | |
| 
 | |
|     def __init__(self, host, caps=None, free_ram_mb=0, free_disk_gb=0):
 | |
|         self.host = host
 | |
| 
 | |
|         # Read-only capability dicts
 | |
|         self.compute = None
 | |
|         self.volume = None
 | |
|         self.network = None
 | |
| 
 | |
|         if caps:
 | |
|             self.compute = ReadOnlyDict(caps.get('compute', None))
 | |
|             self.volume = ReadOnlyDict(caps.get('volume', None))
 | |
|             self.network = ReadOnlyDict(caps.get('network', None))
 | |
| 
 | |
|         # Mutable available resources.
 | |
|         # These will change as resources are virtually "consumed".
 | |
|         self.free_ram_mb = free_ram_mb
 | |
|         self.free_disk_gb = free_disk_gb
 | |
| 
 | |
|     def consume_resources(self, disk_gb, ram_mb):
 | |
|         """Consume some of the mutable resources."""
 | |
|         self.free_disk_gb -= disk_gb
 | |
|         self.free_ram_mb -= ram_mb
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "%s ram:%s disk:%s" % \
 | |
|                     (self.host, self.free_ram_mb, self.free_disk_gb)
 | |
| 
 | |
| 
 | |
| class ZoneManager(object):
 | |
|     """Keeps the zone states updated."""
 | |
|     def __init__(self):
 | |
|         self.last_zone_db_check = datetime.datetime.min
 | |
|         self.zone_states = {}  # { <zone_id> : ZoneState }
 | |
|         self.service_states = {}  # { <host> : { <service> : { 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_host_list(self):
 | |
|         """Returns a list of dicts for each host that the Zone Manager
 | |
|         knows about. Each dict contains the host_name and the service
 | |
|         for that host.
 | |
|         """
 | |
|         all_hosts = self.service_states.keys()
 | |
|         ret = []
 | |
|         for host in self.service_states:
 | |
|             for svc in self.service_states[host]:
 | |
|                 ret.append({"service": svc, "host_name": host})
 | |
|         return ret
 | |
| 
 | |
|     def _compute_node_get_all(self, context):
 | |
|         """Broken out for testing."""
 | |
|         return db.compute_node_get_all(context)
 | |
| 
 | |
|     def _instance_get_all(self, context):
 | |
|         """Broken out for testing."""
 | |
|         return db.instance_get_all(context)
 | |
| 
 | |
|     def get_all_host_data(self, context):
 | |
|         """Returns a dict of all the hosts the ZoneManager
 | |
|         knows about. Also, each of the consumable resources in HostInfo
 | |
|         are pre-populated and adjusted based on data in the db.
 | |
| 
 | |
|         For example:
 | |
|         {'192.168.1.100': HostInfo(), ...}
 | |
| 
 | |
|         Note: this can be very slow with a lot of instances.
 | |
|         InstanceType table isn't required since a copy is stored
 | |
|         with the instance (in case the InstanceType changed since the
 | |
|         instance was created)."""
 | |
| 
 | |
|         # Make a compute node dict with the bare essential metrics.
 | |
|         compute_nodes = self._compute_node_get_all(context)
 | |
|         host_info_map = {}
 | |
|         for compute in compute_nodes:
 | |
|             all_disk = compute['local_gb']
 | |
|             all_ram = compute['memory_mb']
 | |
|             service = compute['service']
 | |
|             if not service:
 | |
|                 logging.warn(_("No service for compute ID %s") % compute['id'])
 | |
|                 continue
 | |
| 
 | |
|             host = service['host']
 | |
|             caps = self.service_states.get(host, None)
 | |
|             host_info = HostInfo(host, caps=caps,
 | |
|                     free_disk_gb=all_disk, free_ram_mb=all_ram)
 | |
|             # Reserve resources for host/dom0
 | |
|             host_info.consume_resources(FLAGS.reserved_host_disk_mb * 1024,
 | |
|                     FLAGS.reserved_host_memory_mb)
 | |
|             host_info_map[host] = host_info
 | |
| 
 | |
|         # "Consume" resources from the host the instance resides on.
 | |
|         instances = self._instance_get_all(context)
 | |
|         for instance in instances:
 | |
|             host = instance['host']
 | |
|             if not host:
 | |
|                 continue
 | |
|             host_info = host_info_map.get(host, None)
 | |
|             if not host_info:
 | |
|                 continue
 | |
|             disk = instance['local_gb']
 | |
|             ram = instance['memory_mb']
 | |
|             host_info.consume_resources(disk, ram)
 | |
| 
 | |
|         return host_info_map
 | |
| 
 | |
|     def get_zone_capabilities(self, context):
 | |
|         """Roll up all the individual host info to generic 'service'
 | |
|            capabilities. Each capability is aggregated into
 | |
|            <cap>_min and <cap>_max values."""
 | |
|         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 = {}  # { <service>_<cap> : (min, max), ... }
 | |
|         stale_host_services = {}  # { host1 : [svc1, svc2], host2 :[svc1]}
 | |
|         for host, host_dict in hosts_dict.iteritems():
 | |
|             for service_name, service_dict in host_dict.iteritems():
 | |
|                 if not service_dict.get("enabled", True):
 | |
|                     # Service is disabled; do no include it
 | |
|                     continue
 | |
| 
 | |
|                 #Check if the service capabilities became stale
 | |
|                 if self.host_service_caps_stale(host, service_name):
 | |
|                     if host not in stale_host_services:
 | |
|                         stale_host_services[host] = []  # Adding host key once
 | |
|                     stale_host_services[host].append(service_name)
 | |
|                     continue
 | |
|                 for cap, value in service_dict.iteritems():
 | |
|                     if cap == "timestamp":  # Timestamp is not needed
 | |
|                         continue
 | |
|                     key = "%s_%s" % (service_name, cap)
 | |
|                     min_value, max_value = combined.get(key, (value, value))
 | |
|                     min_value = min(min_value, value)
 | |
|                     max_value = max(max_value, value)
 | |
|                     combined[key] = (min_value, max_value)
 | |
| 
 | |
|         # Delete the expired host services
 | |
|         self.delete_expired_host_services(stale_host_services)
 | |
|         return combined
 | |
| 
 | |
|     def _refresh_from_db(self, context):
 | |
|         """Make our zone state map match the db."""
 | |
|         # Add/update existing zones ...
 | |
|         zones = db.zone_get_all(context)
 | |
|         existing = self.zone_states.keys()
 | |
|         db_keys = []
 | |
|         for zone in zones:
 | |
|             db_keys.append(zone.id)
 | |
|             if zone.id not in existing:
 | |
|                 self.zone_states[zone.id] = ZoneState()
 | |
|             self.zone_states[zone.id].update_credentials(zone)
 | |
| 
 | |
|         # Cleanup zones removed from db ...
 | |
|         keys = self.zone_states.keys()  # since we're deleting
 | |
|         for zone_id in keys:
 | |
|             if zone_id not in db_keys:
 | |
|                 del self.zone_states[zone_id]
 | |
| 
 | |
|     def _poll_zones(self, context):
 | |
|         """Try to connect to each child zone and get update."""
 | |
|         self.green_pool.imap(_poll_zone, self.zone_states.values())
 | |
| 
 | |
|     def ping(self, context):
 | |
|         """Ping should be called periodically to update zone status."""
 | |
|         diff = utils.utcnow() - self.last_zone_db_check
 | |
|         if diff.seconds >= FLAGS.zone_db_check_interval:
 | |
|             logging.debug(_("Updating zone cache from db."))
 | |
|             self.last_zone_db_check = utils.utcnow()
 | |
|             self._refresh_from_db(context)
 | |
|         self._poll_zones(context)
 | |
| 
 | |
|     def update_service_capabilities(self, service_name, host, capabilities):
 | |
|         """Update the per-service capabilities based on this notification."""
 | |
|         logging.debug(_("Received %(service_name)s service update from "
 | |
|                 "%(host)s.") % locals())
 | |
|         service_caps = self.service_states.get(host, {})
 | |
|         capabilities["timestamp"] = utils.utcnow()  # Reported time
 | |
|         service_caps[service_name] = capabilities
 | |
|         self.service_states[host] = service_caps
 | |
| 
 | |
|     def host_service_caps_stale(self, host, service):
 | |
|         """Check if host service capabilites are not recent enough."""
 | |
|         allowed_time_diff = FLAGS.periodic_interval * 3
 | |
|         caps = self.service_states[host][service]
 | |
|         if (utils.utcnow() - caps["timestamp"]) <= \
 | |
|             datetime.timedelta(seconds=allowed_time_diff):
 | |
|             return False
 | |
|         return True
 | |
| 
 | |
|     def delete_expired_host_services(self, host_services_dict):
 | |
|         """Delete all the inactive host services information."""
 | |
|         for host, services in host_services_dict.iteritems():
 | |
|             service_caps = self.service_states[host]
 | |
|             for service in services:
 | |
|                 del service_caps[service]
 | |
|                 if len(service_caps) == 0:  # Delete host if no services
 | |
|                     del self.service_states[host]
 |