Merge lp:~sandy-walsh/nova/zones3 into lp:~hudson-openstack/nova/trunk

Proposed by Sandy Walsh
Status: Superseded
Proposed branch: lp:~sandy-walsh/nova/zones3
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~sandy-walsh/nova/zones2
Diff against target: 681 lines (+292/-71)
17 files modified
nova/api/openstack/zones.py (+12/-4)
nova/compute/manager.py (+3/-2)
nova/flags.py (+3/-2)
nova/manager.py (+30/-1)
nova/network/manager.py (+3/-2)
nova/rpc.py (+59/-18)
nova/scheduler/api.py (+37/-22)
nova/scheduler/driver.py (+7/-0)
nova/scheduler/manager.py (+13/-1)
nova/scheduler/zone_manager.py (+32/-0)
nova/service.py (+8/-2)
nova/tests/api/openstack/test_zones.py (+26/-3)
nova/tests/test_rpc.py (+2/-2)
nova/tests/test_service.py (+19/-9)
nova/tests/test_test.py (+1/-1)
nova/tests/test_zones.py (+34/-0)
nova/volume/manager.py (+3/-2)
To merge this branch: bzr merge lp:~sandy-walsh/nova/zones3
Reviewer Review Type Date Requested Status
Rick Harris (community) Approve
Matt Dietz (community) Approve
Joshua McKenty (community) Needs Information
Todd Willey (community) Approve
Review via email: mp+52565@code.launchpad.net

This proposal has been superseded by a proposal from 2011-03-24.

Commit message

Aggregates capabilities from Compute, Network, Volume to the ZoneManager in Scheduler.

Description of the change

This branch adds a Fanout (broadcast) Queue to all services. This lets anyone talk to all services of a particular type without having to iterate through each.

Compute, Volume and Network now derive from nova.SchedulerDependentManager, which gives them the ability to update all the Scheduler nodes of their capabilities (via the fanout queue).

These capabilities are stored in each Scheduler Zone Manager and available via the Scheduler.API

The OS API '/zones/info' call now returns the aggregated (min, max) values of each of the service capabilities: http://paste.openstack.org/show/815/

To integration test this, simply fire up another scheduler (or more), add some capabilities to Compute, Network or Volume via the services update_service_capabilities() call and watch the events appear in each Scheduler node.

To post a comment you must log in.
Revision history for this message
Todd Willey (xtoddx) wrote :

I think this looks pretty good.

Is there any information (RST docs) regarding what meaningful values for capabilities are?

Also, in some flags we are using x=y,a=b syntax for multi-valued flags (defualt_log_levels, etc). Perhaps we can keep the same format? Long term perhaps we could make a new DEFINE_dict or some-such that would parse those into a dict.

It looks like the only information that is being fanned out is the service capabilities, but would it make sense to keep abilities in the database pointing to the services themselves? In other words, why are service capabilities ephemeral with the services starting and stopping? If we keep them in the db we can modify them while running and modify the capabilities with nova-manage. This of course doesn't make sense for things like changing the hypervisor for compute since that would always require a restart, but would it be practical for other capabilities?

review: Needs Information
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Todd, thanks for the feedback.

I think the meaningful values for caps will come from the distributed scheduler effort. That branch really needs to drive what is required from the services.

RE flags: do you mean "x=1,x=2,x=3,y=9,y=8,y=7" vs. "x:1,2,3;y:9,8,7;" ?

RE storing the capabilities: yes, storing it in the db is a possibility. Since many capabilities are not static (disk remaining, bandwidth usage, cpu, etc) I had concerns about the frequency of the updates and the added load on the db. But this could use the same periodic storage scheme as the fanout mechanism.

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Oh, perhaps the multi value keys where "x:1;2;3, y;9;8;7" ? That would be an easy change. Perhaps move the parser into utils.py as well?

Revision history for this message
Todd Willey (xtoddx) wrote :

From nova/flags.py:

flags.DEFINE_list('default_log_levels',
                  ['amqplib=WARN',
                   'sqlalchemy=WARN',
                   'boto=WARN',
                   'eventlet.wsgi.server=WARN'],
                  'list of logger=LEVEL pairs')

I think that is a good pattern to follow. Your argument ends up looking like

--zone_capabilities=hypervisor=xenserver,os=linux

You'll end up with the list

['hypervisor=xenserver', 'os=linux']

You're definitely treating it as a list by splitting and iterating over it, so DEFINE_list seems reasonable. Based on your comments it looks like you might specify multiple values for any individual capability, in which case using a comma would break the flag list, maybe using a colon as the item separator would be reasonable?

Revision history for this message
Eric Day (eday) wrote :

8: Ahh! How did nova.db get into nova.api? I must have missed this in the last branch. It would be nice to abstract this in nova/scheduler or a nova/zones/ package.

Should SchedulerDependentManager just be in nova/manager.py?

As for using a DB to store this data (Todd's comment), we need to move away from compute/network/volume hosts writing to the DB directly for security concerns. We should be using the msg queue for all communications/data and let the scheduler verify the source once we have a mechanism to do so.

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

eric, agree on moving SchedulerDependentManager into nova/manager.py and abstracting out the db call into the scheduler api. Will do.

Todd, colon is bad for URI's. I'll mess around the the List option.

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Todd, how about x=1;2;3, y=9;8;7 .. that way we can use DEFINE_list and have multi-values?

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

All fixed up as requested ... thanks again for the feedback!

Revision history for this message
Todd Willey (xtoddx) wrote :

I'm on board with that style of flag.

review: Approve
Revision history for this message
Joshua McKenty (joshua-mckenty) wrote :

Typo on 79 (missing i).
Seems like service_name should be a class property rather than an instance property, any reason we can't do it this way? Then the super __init__ call doesn't need an extra arg.
I think TopicConsumer was the last of my code from the first nova-hacking weekend - I'm glad to see it go.

Log message on 231 seems wrong - is it actually "writing" at that point, or just initing?
I like how you've dropped _call_scheduler out of the API class - but do we even NEED that class any more? The rest of the class methods could be dropped down to functions as well.

467 - can we call this zone_capabilities? Caps seems like a pseudonym for "limits".
Looks like we need a simple test of the fanout in test_rpc.py.

Doesn't seem like there's any way to remove a host from service capabilities once it's been added - is this necessary?

review: Needs Information
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Joshua, great feedback ... thanks for that. All good suggestions. I'll get on them.

Hmm, I was going to add something to decay the hosts after time. I may have forgotten that, lemme see.

If not, I can easily add it to Zones4 (coming soon).

If you'd like to see the direction this is heading have a look here (WIP):
https://code.launchpad.net/~sandy-walsh/nova/zones4/+merge/53726

Cheers!

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Changes made ... the fanout test should be an integration test vs. a unit test. We'll need to revisit that once we get the integration test framework ironed out.

Revision history for this message
Rick Harris (rconradharris) wrote :

Overall this patch looks really good Sandy. Nice tests, and some really
helpful comments in there (as usual).

Generally speaking, I think a few whitespaces cleanups would make a make this
patch even better (in particular lining up arg-lists). Not a huge deal,
obviously, but worth mentioning.

In terms of design and implementation, I think it looks good. This matches up
with what we whiteboarded (AFAICT), so I'm all for it.

Specifics
=========

> 10 +from nova import log as logging

Imported but not used.

> 35 + for cap in caps:
> 36 + key_values = cap.split('=')
> 37 + zone[key_values[0]] = key_values[1]

Might be slightly easier to read with tuple-unpacking (also, passing 1 to
split in case the value has a '=' in it):

    for cap in caps:
        key, value = cap.split('=', 1)
        zone[key] = value

> 62 + super(ComputeManager, self).__init__(service_name="compute",
> 63 + *args, **kwargs)

Might be better if args aligned:

    super(ComputeManager, self).__init__(service_name="compute",
                                         *args, **kwargs)
> 77 +DEFINE_list('zone_capabilities',
> 78 + ['hypervisor=xenserver;kvm', 'os=linux;windows'],
> 79 + 'Key/Multi-value list representng capabilities of this zone')

Same as above:

    DEFINE_list('zone_capabilities',
                ['hypervisor=xenserver;kvm', 'os=linux;windows'],
                'Key/Multi-value list representng capabilities of this zone')

> 88 +from nova import log as logging

Might be worth using the LOG pattern here to get more specific logging:

    LOG = logging.getLogger('nova.manager')

> 106 + update_service_capabilities is called with non-None values."""
> 107 + def __init__(self, host=None, db_driver=None, service_name="undefined"):

Spacing. Might be better as:

       update_service_capabilities is called with non-None values.
    """

    def __init__(self, host=None, db_driver=None, service_name="undefined"):

> 313 + params=dict(service=service))

Couple spaces ---> thataway (to line up with open paren +1)

> 209 + LOG.info(_("Created '%(exchange)s' fanout exchange "
> 210 + "with '%(key)s' routing key"),
> 211 + dict(exchange=self.exchange, key=self.routing_key))

Could use a bit of formatting work:

    LOG.info(_("Created '%(exchange)s' fanout exchange "
               "with '%(key)s' routing key"),
             dict(exchange=self.exchange, key=self.routing_key))

Although, this might be personal preference on my part, so, if that change
doesn't look good to you, please feel free to ignore! :-)

> 638 + svc10_a=(99, 99), svc10_b=(99, 99)))

Spacing, a couple of spaces <---- thattaway.

review: Needs Fixing
Revision history for this message
Matt Dietz (cerberus) wrote :

I don't see anything glaring ;-)

lgtm. Just waiting on Rick

review: Approve
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Thanks for the feedback Rick ... I agree with your suggestions. Doing the code reviews on glance and seeing the work you and jay have been doing might make me change my style :)

Stay tuned.

Revision history for this message
Rick Harris (rconradharris) wrote :

Looks great, thanks for the fix-ups, Sandy!

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

No proposals found for merge of lp:~sandy-walsh/nova/zones2 into lp:nova.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/api/openstack/zones.py'
--- nova/api/openstack/zones.py 2011-03-11 19:49:32 +0000
+++ nova/api/openstack/zones.py 2011-03-23 19:38:07 +0000
@@ -15,9 +15,9 @@
1515
16import common16import common
1717
18from nova import db
18from nova import flags19from nova import flags
19from nova import wsgi20from nova import wsgi
20from nova import db
21from nova.scheduler import api21from nova.scheduler import api
2222
2323
@@ -52,7 +52,7 @@
52 """Return all zones in brief"""52 """Return all zones in brief"""
53 # Ask the ZoneManager in the Scheduler for most recent data,53 # Ask the ZoneManager in the Scheduler for most recent data,
54 # or fall-back to the database ...54 # or fall-back to the database ...
55 items = api.API().get_zone_list(req.environ['nova.context'])55 items = api.get_zone_list(req.environ['nova.context'])
56 if not items:56 if not items:
57 items = db.zone_get_all(req.environ['nova.context'])57 items = db.zone_get_all(req.environ['nova.context'])
5858
@@ -67,8 +67,16 @@
6767
68 def info(self, req):68 def info(self, req):
69 """Return name and capabilities for this zone."""69 """Return name and capabilities for this zone."""
70 return dict(zone=dict(name=FLAGS.zone_name,70 items = api.get_zone_capabilities(req.environ['nova.context'])
71 capabilities=FLAGS.zone_capabilities))71
72 zone = dict(name=FLAGS.zone_name)
73 caps = FLAGS.zone_capabilities
74 for cap in caps:
75 key, value = cap.split('=')
76 zone[key] = value
77 for item, (min_value, max_value) in items.iteritems():
78 zone[item] = "%s,%s" % (min_value, max_value)
79 return dict(zone=zone)
7280
73 def show(self, req, id):81 def show(self, req, id):
74 """Return data about the given zone id"""82 """Return data about the given zone id"""
7583
=== modified file 'nova/compute/manager.py'
--- nova/compute/manager.py 2011-03-21 15:41:03 +0000
+++ nova/compute/manager.py 2011-03-23 19:38:07 +0000
@@ -105,7 +105,7 @@
105 return decorated_function105 return decorated_function
106106
107107
108class ComputeManager(manager.Manager):108class ComputeManager(manager.SchedulerDependentManager):
109109
110 """Manages the running instances from creation to destruction."""110 """Manages the running instances from creation to destruction."""
111111
@@ -124,7 +124,8 @@
124124
125 self.network_manager = utils.import_object(FLAGS.network_manager)125 self.network_manager = utils.import_object(FLAGS.network_manager)
126 self.volume_manager = utils.import_object(FLAGS.volume_manager)126 self.volume_manager = utils.import_object(FLAGS.volume_manager)
127 super(ComputeManager, self).__init__(*args, **kwargs)127 super(ComputeManager, self).__init__(service_name="compute",
128 *args, **kwargs)
128129
129 def init_host(self):130 def init_host(self):
130 """Do any initialization that needs to be run if this is a131 """Do any initialization that needs to be run if this is a
131132
=== modified file 'nova/flags.py'
--- nova/flags.py 2011-03-18 11:35:00 +0000
+++ nova/flags.py 2011-03-23 19:38:07 +0000
@@ -358,5 +358,6 @@
358 'availability zone of this node')358 'availability zone of this node')
359359
360DEFINE_string('zone_name', 'nova', 'name of this zone')360DEFINE_string('zone_name', 'nova', 'name of this zone')
361DEFINE_string('zone_capabilities', 'kypervisor:xenserver;os:linux',361DEFINE_list('zone_capabilities',
362 'Key/Value tags which represent capabilities of this zone')362 ['hypervisor=xenserver;kvm', 'os=linux;windows'],
363 'Key/Multi-value list representng capabilities of this zone')
363364
=== modified file 'nova/manager.py'
--- nova/manager.py 2010-12-15 00:05:39 +0000
+++ nova/manager.py 2011-03-23 19:38:07 +0000
@@ -53,11 +53,14 @@
5353
54from nova import utils54from nova import utils
55from nova import flags55from nova import flags
56from nova import log as logging
56from nova.db import base57from nova.db import base
5758from nova.scheduler import api
5859
59FLAGS = flags.FLAGS60FLAGS = flags.FLAGS
6061
62LOG = logging.getLogger('nova.manager')
63
6164
62class Manager(base.Base):65class Manager(base.Base):
63 def __init__(self, host=None, db_driver=None):66 def __init__(self, host=None, db_driver=None):
@@ -74,3 +77,29 @@
74 """Do any initialization that needs to be run if this is a standalone77 """Do any initialization that needs to be run if this is a standalone
75 service. Child classes should override this method."""78 service. Child classes should override this method."""
76 pass79 pass
80
81
82class SchedulerDependentManager(Manager):
83 """Periodically send capability updates to the Scheduler services.
84 Services that need to update the Scheduler of their capabilities
85 should derive from this class. Otherwise they can derive from
86 manager.Manager directly. Updates are only sent after
87 update_service_capabilities is called with non-None values."""
88
89 def __init__(self, host=None, db_driver=None, service_name="undefined"):
90 self.last_capabilities = None
91 self.service_name = service_name
92 super(SchedulerDependentManager, self).__init__(host, db_driver)
93
94 def update_service_capabilities(self, capabilities):
95 """Remember these capabilities to send on next periodic update."""
96 self.last_capabilities = capabilities
97
98 def periodic_tasks(self, context=None):
99 """Pass data back to the scheduler at a periodic interval"""
100 if self.last_capabilities:
101 LOG.debug(_("Notifying Schedulers of capabilities ..."))
102 api.update_service_capabilities(context, self.service_name,
103 self.host, self.last_capabilities)
104
105 super(SchedulerDependentManager, self).periodic_tasks(context)
77106
=== modified file 'nova/network/manager.py'
--- nova/network/manager.py 2011-03-23 05:29:32 +0000
+++ nova/network/manager.py 2011-03-23 19:38:07 +0000
@@ -105,7 +105,7 @@
105 pass105 pass
106106
107107
108class NetworkManager(manager.Manager):108class NetworkManager(manager.SchedulerDependentManager):
109 """Implements common network manager functionality.109 """Implements common network manager functionality.
110110
111 This class must be subclassed to support specific topologies.111 This class must be subclassed to support specific topologies.
@@ -116,7 +116,8 @@
116 if not network_driver:116 if not network_driver:
117 network_driver = FLAGS.network_driver117 network_driver = FLAGS.network_driver
118 self.driver = utils.import_object(network_driver)118 self.driver = utils.import_object(network_driver)
119 super(NetworkManager, self).__init__(*args, **kwargs)119 super(NetworkManager, self).__init__(service_name='network',
120 *args, **kwargs)
120121
121 def init_host(self):122 def init_host(self):
122 """Do any initialization that needs to be run if this is a123 """Do any initialization that needs to be run if this is a
123124
=== modified file 'nova/rpc.py'
--- nova/rpc.py 2011-03-18 13:56:05 +0000
+++ nova/rpc.py 2011-03-23 19:38:07 +0000
@@ -137,24 +137,7 @@
137 return timer137 return timer
138138
139139
140class Publisher(messaging.Publisher):140class AdapterConsumer(Consumer):
141 """Publisher base class"""
142 pass
143
144
145class TopicConsumer(Consumer):
146 """Consumes messages on a specific topic"""
147 exchange_type = "topic"
148
149 def __init__(self, connection=None, topic="broadcast"):
150 self.queue = topic
151 self.routing_key = topic
152 self.exchange = FLAGS.control_exchange
153 self.durable = False
154 super(TopicConsumer, self).__init__(connection=connection)
155
156
157class AdapterConsumer(TopicConsumer):
158 """Calls methods on a proxy object based on method and args"""141 """Calls methods on a proxy object based on method and args"""
159 def __init__(self, connection=None, topic="broadcast", proxy=None):142 def __init__(self, connection=None, topic="broadcast", proxy=None):
160 LOG.debug(_('Initing the Adapter Consumer for %s') % topic)143 LOG.debug(_('Initing the Adapter Consumer for %s') % topic)
@@ -207,6 +190,41 @@
207 return190 return
208191
209192
193class Publisher(messaging.Publisher):
194 """Publisher base class"""
195 pass
196
197
198class TopicAdapterConsumer(AdapterConsumer):
199 """Consumes messages on a specific topic"""
200 exchange_type = "topic"
201
202 def __init__(self, connection=None, topic="broadcast", proxy=None):
203 self.queue = topic
204 self.routing_key = topic
205 self.exchange = FLAGS.control_exchange
206 self.durable = False
207 super(TopicAdapterConsumer, self).__init__(connection=connection,
208 topic=topic, proxy=proxy)
209
210
211class FanoutAdapterConsumer(AdapterConsumer):
212 """Consumes messages from a fanout exchange"""
213 exchange_type = "fanout"
214
215 def __init__(self, connection=None, topic="broadcast", proxy=None):
216 self.exchange = "%s_fanout" % topic
217 self.routing_key = topic
218 unique = uuid.uuid4().hex
219 self.queue = "%s_fanout_%s" % (topic, unique)
220 self.durable = False
221 LOG.info(_("Created '%(exchange)s' fanout exchange "
222 "with '%(key)s' routing key"),
223 dict(exchange=self.exchange, key=self.routing_key))
224 super(FanoutAdapterConsumer, self).__init__(connection=connection,
225 topic=topic, proxy=proxy)
226
227
210class TopicPublisher(Publisher):228class TopicPublisher(Publisher):
211 """Publishes messages on a specific topic"""229 """Publishes messages on a specific topic"""
212 exchange_type = "topic"230 exchange_type = "topic"
@@ -218,6 +236,19 @@
218 super(TopicPublisher, self).__init__(connection=connection)236 super(TopicPublisher, self).__init__(connection=connection)
219237
220238
239class FanoutPublisher(Publisher):
240 """Publishes messages to a fanout exchange."""
241 exchange_type = "fanout"
242
243 def __init__(self, topic, connection=None):
244 self.exchange = "%s_fanout" % topic
245 self.queue = "%s_fanout" % topic
246 self.durable = False
247 LOG.info(_("Creating '%(exchange)s' fanout exchange"),
248 dict(exchange=self.exchange))
249 super(FanoutPublisher, self).__init__(connection=connection)
250
251
221class DirectConsumer(Consumer):252class DirectConsumer(Consumer):
222 """Consumes messages directly on a channel specified by msg_id"""253 """Consumes messages directly on a channel specified by msg_id"""
223 exchange_type = "direct"254 exchange_type = "direct"
@@ -360,6 +391,16 @@
360 publisher.close()391 publisher.close()
361392
362393
394def fanout_cast(context, topic, msg):
395 """Sends a message on a fanout exchange without waiting for a response"""
396 LOG.debug(_("Making asynchronous fanout cast..."))
397 _pack_context(msg, context)
398 conn = Connection.instance()
399 publisher = FanoutPublisher(topic, connection=conn)
400 publisher.send(msg)
401 publisher.close()
402
403
363def generic_response(message_data, message):404def generic_response(message_data, message):
364 """Logs a result and exits"""405 """Logs a result and exits"""
365 LOG.debug(_('response %s'), message_data)406 LOG.debug(_('response %s'), message_data)
366407
=== modified file 'nova/scheduler/api.py'
--- nova/scheduler/api.py 2011-02-25 21:40:15 +0000
+++ nova/scheduler/api.py 2011-03-23 19:38:07 +0000
@@ -25,25 +25,40 @@
25LOG = logging.getLogger('nova.scheduler.api')25LOG = logging.getLogger('nova.scheduler.api')
2626
2727
28class API(object):28def _call_scheduler(method, context, params=None):
29 """API for interacting with the scheduler."""29 """Generic handler for RPC calls to the scheduler.
3030
31 def _call_scheduler(self, method, context, params=None):31 :param params: Optional dictionary of arguments to be passed to the
32 """Generic handler for RPC calls to the scheduler.32 scheduler worker
3333
34 :param params: Optional dictionary of arguments to be passed to the34 :retval: Result returned by scheduler worker
35 scheduler worker35 """
3636 if not params:
37 :retval: Result returned by scheduler worker37 params = {}
38 """38 queue = FLAGS.scheduler_topic
39 if not params:39 kwargs = {'method': method, 'args': params}
40 params = {}40 return rpc.call(context, queue, kwargs)
41 queue = FLAGS.scheduler_topic41
42 kwargs = {'method': method, 'args': params}42
43 return rpc.call(context, queue, kwargs)43def get_zone_list(context):
4444 """Return a list of zones assoicated with this zone."""
45 def get_zone_list(self, context):45 items = _call_scheduler('get_zone_list', context)
46 items = self._call_scheduler('get_zone_list', context)46 for item in items:
47 for item in items:47 item['api_url'] = item['api_url'].replace('\\/', '/')
48 item['api_url'] = item['api_url'].replace('\\/', '/')48 return items
49 return items49
50
51def get_zone_capabilities(context, service=None):
52 """Returns a dict of key, value capabilities for this zone,
53 or for a particular class of services running in this zone."""
54 return _call_scheduler('get_zone_capabilities', context=context,
55 params=dict(service=service))
56
57
58def update_service_capabilities(context, service_name, host, capabilities):
59 """Send an update to all the scheduler services informing them
60 of the capabilities of this service."""
61 kwargs = dict(method='update_service_capabilities',
62 args=dict(service_name=service_name, host=host,
63 capabilities=capabilities))
64 return rpc.fanout_cast(context, 'scheduler', kwargs)
5065
=== modified file 'nova/scheduler/driver.py'
--- nova/scheduler/driver.py 2011-03-10 04:30:52 +0000
+++ nova/scheduler/driver.py 2011-03-23 19:38:07 +0000
@@ -49,6 +49,13 @@
49class Scheduler(object):49class Scheduler(object):
50 """The base class that all Scheduler clases should inherit from."""50 """The base class that all Scheduler clases should inherit from."""
5151
52 def __init__(self):
53 self.zone_manager = None
54
55 def set_zone_manager(self, zone_manager):
56 """Called by the Scheduler Service to supply a ZoneManager."""
57 self.zone_manager = zone_manager
58
52 @staticmethod59 @staticmethod
53 def service_is_up(service):60 def service_is_up(service):
54 """Check whether a service is up based on last heartbeat."""61 """Check whether a service is up based on last heartbeat."""
5562
=== modified file 'nova/scheduler/manager.py'
--- nova/scheduler/manager.py 2011-03-14 17:59:41 +0000
+++ nova/scheduler/manager.py 2011-03-23 19:38:07 +0000
@@ -41,10 +41,11 @@
41class SchedulerManager(manager.Manager):41class SchedulerManager(manager.Manager):
42 """Chooses a host to run instances on."""42 """Chooses a host to run instances on."""
43 def __init__(self, scheduler_driver=None, *args, **kwargs):43 def __init__(self, scheduler_driver=None, *args, **kwargs):
44 self.zone_manager = zone_manager.ZoneManager()
44 if not scheduler_driver:45 if not scheduler_driver:
45 scheduler_driver = FLAGS.scheduler_driver46 scheduler_driver = FLAGS.scheduler_driver
46 self.driver = utils.import_object(scheduler_driver)47 self.driver = utils.import_object(scheduler_driver)
47 self.zone_manager = zone_manager.ZoneManager()48 self.driver.set_zone_manager(self.zone_manager)
48 super(SchedulerManager, self).__init__(*args, **kwargs)49 super(SchedulerManager, self).__init__(*args, **kwargs)
4950
50 def __getattr__(self, key):51 def __getattr__(self, key):
@@ -59,6 +60,17 @@
59 """Get a list of zones from the ZoneManager."""60 """Get a list of zones from the ZoneManager."""
60 return self.zone_manager.get_zone_list()61 return self.zone_manager.get_zone_list()
6162
63 def get_zone_capabilities(self, context=None, service=None):
64 """Get the normalized set of capabilites for this zone,
65 or for a particular service."""
66 return self.zone_manager.get_zone_capabilities(context, service)
67
68 def update_service_capabilities(self, context=None, service_name=None,
69 host=None, capabilities={}):
70 """Process a capability update from a service node."""
71 self.zone_manager.update_service_capabilities(service_name,
72 host, capabilities)
73
62 def _schedule(self, method, context, topic, *args, **kwargs):74 def _schedule(self, method, context, topic, *args, **kwargs):
63 """Tries to call schedule_* method on the driver to retrieve host.75 """Tries to call schedule_* method on the driver to retrieve host.
6476
6577
=== modified file 'nova/scheduler/zone_manager.py'
--- nova/scheduler/zone_manager.py 2011-03-03 14:55:02 +0000
+++ nova/scheduler/zone_manager.py 2011-03-23 19:38:07 +0000
@@ -105,12 +105,36 @@
105 def __init__(self):105 def __init__(self):
106 self.last_zone_db_check = datetime.min106 self.last_zone_db_check = datetime.min
107 self.zone_states = {}107 self.zone_states = {}
108 self.service_states = {} # { <service> : { <host> : { cap k : v }}}
108 self.green_pool = greenpool.GreenPool()109 self.green_pool = greenpool.GreenPool()
109110
110 def get_zone_list(self):111 def get_zone_list(self):
111 """Return the list of zones we know about."""112 """Return the list of zones we know about."""
112 return [zone.to_dict() for zone in self.zone_states.values()]113 return [zone.to_dict() for zone in self.zone_states.values()]
113114
115 def get_zone_capabilities(self, context, service=None):
116 """Roll up all the individual host info to generic 'service'
117 capabilities. Each capability is aggregated into
118 <cap>_min and <cap>_max values."""
119 service_dict = self.service_states
120 if service:
121 service_dict = {service: self.service_states.get(service, {})}
122
123 # TODO(sandy) - be smarter about fabricating this structure.
124 # But it's likely to change once we understand what the Best-Match
125 # code will need better.
126 combined = {} # { <service>_<cap> : (min, max), ... }
127 for service_name, host_dict in service_dict.iteritems():
128 for host, caps_dict in host_dict.iteritems():
129 for cap, value in caps_dict.iteritems():
130 key = "%s_%s" % (service_name, cap)
131 min_value, max_value = combined.get(key, (value, value))
132 min_value = min(min_value, value)
133 max_value = max(max_value, value)
134 combined[key] = (min_value, max_value)
135
136 return combined
137
114 def _refresh_from_db(self, context):138 def _refresh_from_db(self, context):
115 """Make our zone state map match the db."""139 """Make our zone state map match the db."""
116 # Add/update existing zones ...140 # Add/update existing zones ...
@@ -141,3 +165,11 @@
141 self.last_zone_db_check = datetime.now()165 self.last_zone_db_check = datetime.now()
142 self._refresh_from_db(context)166 self._refresh_from_db(context)
143 self._poll_zones(context)167 self._poll_zones(context)
168
169 def update_service_capabilities(self, service_name, host, capabilities):
170 """Update the per-service capabilities based on this notification."""
171 logging.debug(_("Received %(service_name)s service update from "
172 "%(host)s: %(capabilities)s") % locals())
173 service_caps = self.service_states.get(service_name, {})
174 service_caps[host] = capabilities
175 self.service_states[service_name] = service_caps
144176
=== modified file 'nova/service.py'
--- nova/service.py 2011-03-18 13:56:05 +0000
+++ nova/service.py 2011-03-23 19:38:07 +0000
@@ -97,18 +97,24 @@
9797
98 conn1 = rpc.Connection.instance(new=True)98 conn1 = rpc.Connection.instance(new=True)
99 conn2 = rpc.Connection.instance(new=True)99 conn2 = rpc.Connection.instance(new=True)
100 conn3 = rpc.Connection.instance(new=True)
100 if self.report_interval:101 if self.report_interval:
101 consumer_all = rpc.AdapterConsumer(102 consumer_all = rpc.TopicAdapterConsumer(
102 connection=conn1,103 connection=conn1,
103 topic=self.topic,104 topic=self.topic,
104 proxy=self)105 proxy=self)
105 consumer_node = rpc.AdapterConsumer(106 consumer_node = rpc.TopicAdapterConsumer(
106 connection=conn2,107 connection=conn2,
107 topic='%s.%s' % (self.topic, self.host),108 topic='%s.%s' % (self.topic, self.host),
108 proxy=self)109 proxy=self)
110 fanout = rpc.FanoutAdapterConsumer(
111 connection=conn3,
112 topic=self.topic,
113 proxy=self)
109114
110 self.timers.append(consumer_all.attach_to_eventlet())115 self.timers.append(consumer_all.attach_to_eventlet())
111 self.timers.append(consumer_node.attach_to_eventlet())116 self.timers.append(consumer_node.attach_to_eventlet())
117 self.timers.append(fanout.attach_to_eventlet())
112118
113 pulse = utils.LoopingCall(self.report_state)119 pulse = utils.LoopingCall(self.report_state)
114 pulse.start(interval=self.report_interval, now=False)120 pulse.start(interval=self.report_interval, now=False)
115121
=== modified file 'nova/tests/api/openstack/test_zones.py'
--- nova/tests/api/openstack/test_zones.py 2011-03-11 19:49:32 +0000
+++ nova/tests/api/openstack/test_zones.py 2011-03-23 19:38:07 +0000
@@ -75,6 +75,10 @@
75 ]75 ]
7676
7777
78def zone_capabilities(method, context, params):
79 return dict()
80
81
78class ZonesTest(test.TestCase):82class ZonesTest(test.TestCase):
79 def setUp(self):83 def setUp(self):
80 super(ZonesTest, self).setUp()84 super(ZonesTest, self).setUp()
@@ -93,13 +97,18 @@
93 self.stubs.Set(nova.db, 'zone_create', zone_create)97 self.stubs.Set(nova.db, 'zone_create', zone_create)
94 self.stubs.Set(nova.db, 'zone_delete', zone_delete)98 self.stubs.Set(nova.db, 'zone_delete', zone_delete)
9599
100 self.old_zone_name = FLAGS.zone_name
101 self.old_zone_capabilities = FLAGS.zone_capabilities
102
96 def tearDown(self):103 def tearDown(self):
97 self.stubs.UnsetAll()104 self.stubs.UnsetAll()
98 FLAGS.allow_admin_api = self.allow_admin105 FLAGS.allow_admin_api = self.allow_admin
106 FLAGS.zone_name = self.old_zone_name
107 FLAGS.zone_capabilities = self.old_zone_capabilities
99 super(ZonesTest, self).tearDown()108 super(ZonesTest, self).tearDown()
100109
101 def test_get_zone_list_scheduler(self):110 def test_get_zone_list_scheduler(self):
102 self.stubs.Set(api.API, '_call_scheduler', zone_get_all_scheduler)111 self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler)
103 req = webob.Request.blank('/v1.0/zones')112 req = webob.Request.blank('/v1.0/zones')
104 res = req.get_response(fakes.wsgi_app())113 res = req.get_response(fakes.wsgi_app())
105 res_dict = json.loads(res.body)114 res_dict = json.loads(res.body)
@@ -108,8 +117,7 @@
108 self.assertEqual(len(res_dict['zones']), 2)117 self.assertEqual(len(res_dict['zones']), 2)
109118
110 def test_get_zone_list_db(self):119 def test_get_zone_list_db(self):
111 self.stubs.Set(api.API, '_call_scheduler',120 self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty)
112 zone_get_all_scheduler_empty)
113 self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db)121 self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db)
114 req = webob.Request.blank('/v1.0/zones')122 req = webob.Request.blank('/v1.0/zones')
115 req.headers["Content-Type"] = "application/json"123 req.headers["Content-Type"] = "application/json"
@@ -167,3 +175,18 @@
167 self.assertEqual(res_dict['zone']['id'], 1)175 self.assertEqual(res_dict['zone']['id'], 1)
168 self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')176 self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')
169 self.assertFalse('username' in res_dict['zone'])177 self.assertFalse('username' in res_dict['zone'])
178
179 def test_zone_info(self):
180 FLAGS.zone_name = 'darksecret'
181 FLAGS.zone_capabilities = ['cap1=a;b', 'cap2=c;d']
182 self.stubs.Set(api, '_call_scheduler', zone_capabilities)
183
184 body = dict(zone=dict(username='zeb', password='sneaky'))
185 req = webob.Request.blank('/v1.0/zones/info')
186
187 res = req.get_response(fakes.wsgi_app())
188 res_dict = json.loads(res.body)
189 self.assertEqual(res.status_int, 200)
190 self.assertEqual(res_dict['zone']['name'], 'darksecret')
191 self.assertEqual(res_dict['zone']['cap1'], 'a;b')
192 self.assertEqual(res_dict['zone']['cap2'], 'c;d')
170193
=== modified file 'nova/tests/test_rpc.py'
--- nova/tests/test_rpc.py 2011-01-19 20:26:09 +0000
+++ nova/tests/test_rpc.py 2011-03-23 19:38:07 +0000
@@ -36,7 +36,7 @@
36 super(RpcTestCase, self).setUp()36 super(RpcTestCase, self).setUp()
37 self.conn = rpc.Connection.instance(True)37 self.conn = rpc.Connection.instance(True)
38 self.receiver = TestReceiver()38 self.receiver = TestReceiver()
39 self.consumer = rpc.AdapterConsumer(connection=self.conn,39 self.consumer = rpc.TopicAdapterConsumer(connection=self.conn,
40 topic='test',40 topic='test',
41 proxy=self.receiver)41 proxy=self.receiver)
42 self.consumer.attach_to_eventlet()42 self.consumer.attach_to_eventlet()
@@ -97,7 +97,7 @@
9797
98 nested = Nested()98 nested = Nested()
99 conn = rpc.Connection.instance(True)99 conn = rpc.Connection.instance(True)
100 consumer = rpc.AdapterConsumer(connection=conn,100 consumer = rpc.TopicAdapterConsumer(connection=conn,
101 topic='nested',101 topic='nested',
102 proxy=nested)102 proxy=nested)
103 consumer.attach_to_eventlet()103 consumer.attach_to_eventlet()
104104
=== modified file 'nova/tests/test_service.py'
--- nova/tests/test_service.py 2011-03-10 06:16:03 +0000
+++ nova/tests/test_service.py 2011-03-23 19:38:07 +0000
@@ -109,20 +109,29 @@
109 app = service.Service.create(host=host, binary=binary)109 app = service.Service.create(host=host, binary=binary)
110110
111 self.mox.StubOutWithMock(rpc,111 self.mox.StubOutWithMock(rpc,
112 'AdapterConsumer',112 'TopicAdapterConsumer',
113 use_mock_anything=True)113 use_mock_anything=True)
114 rpc.AdapterConsumer(connection=mox.IgnoreArg(),114 self.mox.StubOutWithMock(rpc,
115 'FanoutAdapterConsumer',
116 use_mock_anything=True)
117 rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
115 topic=topic,118 topic=topic,
116 proxy=mox.IsA(service.Service)).AndReturn(119 proxy=mox.IsA(service.Service)).AndReturn(
117 rpc.AdapterConsumer)120 rpc.TopicAdapterConsumer)
118121
119 rpc.AdapterConsumer(connection=mox.IgnoreArg(),122 rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
120 topic='%s.%s' % (topic, host),123 topic='%s.%s' % (topic, host),
121 proxy=mox.IsA(service.Service)).AndReturn(124 proxy=mox.IsA(service.Service)).AndReturn(
122 rpc.AdapterConsumer)125 rpc.TopicAdapterConsumer)
123126
124 rpc.AdapterConsumer.attach_to_eventlet()127 rpc.FanoutAdapterConsumer(connection=mox.IgnoreArg(),
125 rpc.AdapterConsumer.attach_to_eventlet()128 topic=topic,
129 proxy=mox.IsA(service.Service)).AndReturn(
130 rpc.FanoutAdapterConsumer)
131
132 rpc.TopicAdapterConsumer.attach_to_eventlet()
133 rpc.TopicAdapterConsumer.attach_to_eventlet()
134 rpc.FanoutAdapterConsumer.attach_to_eventlet()
126135
127 service_create = {'host': host,136 service_create = {'host': host,
128 'binary': binary,137 'binary': binary,
@@ -279,6 +288,7 @@
279 self.mox.StubOutWithMock(service.rpc.Connection, 'instance')288 self.mox.StubOutWithMock(service.rpc.Connection, 'instance')
280 service.rpc.Connection.instance(new=mox.IgnoreArg())289 service.rpc.Connection.instance(new=mox.IgnoreArg())
281 service.rpc.Connection.instance(new=mox.IgnoreArg())290 service.rpc.Connection.instance(new=mox.IgnoreArg())
291 service.rpc.Connection.instance(new=mox.IgnoreArg())
282 self.mox.StubOutWithMock(serv.manager.driver,292 self.mox.StubOutWithMock(serv.manager.driver,
283 'update_available_resource')293 'update_available_resource')
284 serv.manager.driver.update_available_resource(mox.IgnoreArg(), host)294 serv.manager.driver.update_available_resource(mox.IgnoreArg(), host)
285295
=== modified file 'nova/tests/test_test.py'
--- nova/tests/test_test.py 2011-02-21 22:55:06 +0000
+++ nova/tests/test_test.py 2011-03-23 19:38:07 +0000
@@ -34,7 +34,7 @@
3434
35 def test_rpc_consumer_isolation(self):35 def test_rpc_consumer_isolation(self):
36 connection = rpc.Connection.instance(new=True)36 connection = rpc.Connection.instance(new=True)
37 consumer = rpc.TopicConsumer(connection, topic='compute')37 consumer = rpc.TopicAdapterConsumer(connection, topic='compute')
38 consumer.register_callback(38 consumer.register_callback(
39 lambda x, y: self.fail('I should never be called'))39 lambda x, y: self.fail('I should never be called'))
40 consumer.attach_to_eventlet()40 consumer.attach_to_eventlet()
4141
=== modified file 'nova/tests/test_zones.py'
--- nova/tests/test_zones.py 2011-03-03 14:55:02 +0000
+++ nova/tests/test_zones.py 2011-03-23 19:38:07 +0000
@@ -76,6 +76,40 @@
76 self.assertEquals(len(zm.zone_states), 1)76 self.assertEquals(len(zm.zone_states), 1)
77 self.assertEquals(zm.zone_states[1].username, 'user1')77 self.assertEquals(zm.zone_states[1].username, 'user1')
7878
79 def test_service_capabilities(self):
80 zm = zone_manager.ZoneManager()
81 caps = zm.get_zone_capabilities(self, None)
82 self.assertEquals(caps, {})
83
84 zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
85 caps = zm.get_zone_capabilities(self, None)
86 self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
87
88 zm.update_service_capabilities("svc1", "host1", dict(a=2, b=3))
89 caps = zm.get_zone_capabilities(self, None)
90 self.assertEquals(caps, dict(svc1_a=(2, 2), svc1_b=(3, 3)))
91
92 zm.update_service_capabilities("svc1", "host2", dict(a=20, b=30))
93 caps = zm.get_zone_capabilities(self, None)
94 self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30)))
95
96 zm.update_service_capabilities("svc10", "host1", dict(a=99, b=99))
97 caps = zm.get_zone_capabilities(self, None)
98 self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
99 svc10_a=(99, 99), svc10_b=(99, 99)))
100
101 zm.update_service_capabilities("svc1", "host3", dict(c=5))
102 caps = zm.get_zone_capabilities(self, None)
103 self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
104 svc1_c=(5, 5), svc10_a=(99, 99),
105 svc10_b=(99, 99)))
106
107 caps = zm.get_zone_capabilities(self, 'svc1')
108 self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
109 svc1_c=(5, 5)))
110 caps = zm.get_zone_capabilities(self, 'svc10')
111 self.assertEquals(caps, dict(svc10_a=(99, 99), svc10_b=(99, 99)))
112
79 def test_refresh_from_db_replace_existing(self):113 def test_refresh_from_db_replace_existing(self):
80 zm = zone_manager.ZoneManager()114 zm = zone_manager.ZoneManager()
81 zone_state = zone_manager.ZoneState()115 zone_state = zone_manager.ZoneState()
82116
=== modified file 'nova/volume/manager.py'
--- nova/volume/manager.py 2011-03-03 13:54:11 +0000
+++ nova/volume/manager.py 2011-03-23 19:38:07 +0000
@@ -64,14 +64,15 @@
64 'if True, will not discover local volumes')64 'if True, will not discover local volumes')
6565
6666
67class VolumeManager(manager.Manager):67class VolumeManager(manager.SchedulerDependentManager):
68 """Manages attachable block storage devices."""68 """Manages attachable block storage devices."""
69 def __init__(self, volume_driver=None, *args, **kwargs):69 def __init__(self, volume_driver=None, *args, **kwargs):
70 """Load the driver from the one specified in args, or from flags."""70 """Load the driver from the one specified in args, or from flags."""
71 if not volume_driver:71 if not volume_driver:
72 volume_driver = FLAGS.volume_driver72 volume_driver = FLAGS.volume_driver
73 self.driver = utils.import_object(volume_driver)73 self.driver = utils.import_object(volume_driver)
74 super(VolumeManager, self).__init__(*args, **kwargs)74 super(VolumeManager, self).__init__(service_name='volume',
75 *args, **kwargs)
75 # NOTE(vish): Implementation specific db handling is done76 # NOTE(vish): Implementation specific db handling is done
76 # by the driver.77 # by the driver.
77 self.driver.db = self.db78 self.driver.db = self.db