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

Proposed by Sandy Walsh
Status: Superseded
Proposed branch: lp:~sandy-walsh/nova/zones4
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~sandy-walsh/nova/zones3
Diff against target: 1345 lines (+675/-86)
21 files modified
nova/api/openstack/servers.py (+20/-1)
nova/api/openstack/zones.py (+19/-14)
nova/compute/api.py (+16/-1)
nova/compute/manager.py (+3/-2)
nova/db/api.py (+1/-0)
nova/flags.py (+3/-2)
nova/manager.py (+27/-1)
nova/network/manager.py (+3/-2)
nova/rpc.py (+59/-18)
nova/scheduler/api.py (+214/-22)
nova/scheduler/driver.py (+7/-0)
nova/scheduler/manager.py (+13/-1)
nova/scheduler/zone_manager.py (+36/-3)
nova/service.py (+8/-2)
nova/tests/api/openstack/test_zones.py (+26/-3)
nova/tests/test_rpc.py (+2/-2)
nova/tests/test_scheduler.py (+161/-0)
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/zones4
Reviewer Review Type Date Requested Status
Rick Harris (community) Approve
Matt Dietz (community) Approve
Review via email: mp+53726@code.launchpad.net

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

Commit message

In this branch we are forwarding incoming requests to child zones when the requested resource is not found in the current zone.

Description of the change

In this branch we are forwarding incoming requests to child zones when the requested resource is not found in the current zone.

For example: If 'nova pause 123' is issued against Zone 1, but instance 123 does not live in Zone 1, the call will be forwarded to all child zones hoping someone can deal with it.

NOTE: This currently only works with OpenStack API requests and routing checks are only being done against Compute/instance_id checks.
Specifically:
* servers.get/pause/unpause/diagnostics/suspend/resume/rescue/unrescue/delete
* servers.create is pending for distributed scheduler
* servers.get_all will get added early in Diablo.

What I've been doing for testing:
1. Set up a Nova deployment in a VM (Zone0)
2. Clone the VM and set --zone_name=zone1 (and change all the IP addresses to the new address in nova.conf, glance.conf and novarc)
3. Set --enable_zone_routing=true on all zones
4. use the --connection_type=fake driver for compute to keep things easy
5. Add Zone1 as a child of Zone0 (nova zone-add)

(make sure the instance id's are different in each zone)

Example of calls being sent to child zones:
http://paste.openstack.org/show/964/

To post a comment you must log in.
Revision history for this message
Eric Day (eday) wrote :

Hi Sandy,

I don't think WSGI middleware is the correct place to be handling zones forwarding. What if we write a tool directly against nova.compute.api or if some other component (like a compute or network worker) issues a nova.compute.api call? I think all logic and routing should instead be handled inside inside nova.compute and/or nova.scheduler. Nothing in nova.api should ever be aware of what is happening.

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

Thanks Eric,

That was the way I was initially intending to go.

The closer I got to it, I noticed that all calls would need to be re-marshaled to be sent to the children. We'd need a new client library abstraction layer (since forwarded requests may be OS API or EC2 API). I was hoping to avoid all that by simply forwarding the already marshaled message.

This client library abstraction is likely to get a little unwieldy since it needs to support OS/EC2 and whomever else comes along (actually, sounds like another endorsement for DirectAPI.)

Also, with this approach I was assuming an idiom of "API checks parameters and bails if they're wrong. Work is done in the services." ... but I see the counter-argument.

Let me think more about client library abstraction. Since we have big API changes coming soon, might be timely to consider this.

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

Thinking a little more about it, but I've got a concern about that approach. Not for technical reasons, but for schedule/political reasons: Since API is such a touchy subject and still under development, this could turn in a major blocker against getting anything practical done with Zones for a long time.

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

FWIW, I think making the call to novaclient (and re-marshal) from nova.compute.* is the correct thing to do. I would move forward with the OpenStack API for this as well, as that seems to be where we are committed at the moment. If we need to change this in the future, it will be a simple HTTP formatting issue inside novaclient (the API into novaclient shouldn't change).

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

Yup ... that's the direction I'll take. Thanks again Eric.

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

Ok, all refactored out of middleware now. The <service>/api.py does the checking now and the external api just bails out after a redirect. It's all handled in decorators now.

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

Nice work, Sandy.

Some really minor nits/suggestions:

> 402 + try:
> 403 + manager = getattr(nova, collection)

The getattr can be outside of the try-block (AFAICT).

> 404 + if isinstance(item_id, int) or item_id.isdigit():
> 405 + result = manager.get(int(item_id))
> 406 + else:
> 407 + result = manager.find(name=item_id)

One common way of handling this is to just go ahead and cast to int and
handle the possible ValueError (EAFP), like:

    try:
        result = manager.get(int(item_id))
    except ValueError:
        result = manager.find(name=item_id)

> 371 +def _wrap_method(function, self):
> 372 + """Wrap method to supply self."""
> 373 + def _wrap(*args, **kwargs):
> 374 + return function(self, *args, **kwargs)
> 375 + return _wrap

For the newly added decorators, it might be worth using @functools.wraps so
that the outer-func inherits the inner-funcs attributes. Can make debugging a
little easier.

> 397 +def _issue_novaclient_command(nova, zone, collection, method_name, \
> 398 + item_id):

Trailing '\' isn't needed.

Could line-up item_id with opening param.

> 264 + @scheduler_api.reroute_compute("diagnostics")
> 265 def get_diagnostics(self, context, instance_id):

Should that be 'get_diagnostics'? I ask because it's the only one that differs
in that regard.

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

The exception raising idea is really interesting. Hard to follow at first, but the tests make it clear IMO. I think this looks pretty pretty promising.

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

Thanks rick ... great feedback. I've implemented your changes.

re: get_diagnostics, the name in the decorator is the name of the method in novaclient, so it may not map 1:1 to the method being wrapped (as in this case).

Basically the decorator is saying, "if this method can't find the instance, check with the child zones by calling <method> in novaclient"

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

(oh, push pending, will change WIP to let you know)

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

> 788 +def redirect_handler(f):
> 648 +def _wrap_method(function, self):

Didn't like the idea of using @functools.wraps to propagate the inner func's metadata?

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

I do. Good suggestion. But I'll fix that up on the next branch. Haven't spent enough time with it and don't want to miss the window today.

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

Good deal, lgtm. Thanks!

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

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

lp:~sandy-walsh/nova/zones4 updated
720. By Sandy Walsh

trunk merge

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/api/openstack/servers.py'
--- nova/api/openstack/servers.py 2011-03-24 05:08:17 +0000
+++ nova/api/openstack/servers.py 2011-03-24 12:14:14 +0000
@@ -38,6 +38,7 @@
38from nova.compute import power_state38from nova.compute import power_state
39from nova.quota import QuotaError39from nova.quota import QuotaError
40import nova.api.openstack40import nova.api.openstack
41from nova.scheduler import api as scheduler_api
4142
4243
43LOG = logging.getLogger('server')44LOG = logging.getLogger('server')
@@ -88,15 +89,18 @@
88 for inst in limited_list]89 for inst in limited_list]
89 return dict(servers=servers)90 return dict(servers=servers)
9091
92 @scheduler_api.redirect_handler
91 def show(self, req, id):93 def show(self, req, id):
92 """ Returns server details by server id """94 """ Returns server details by server id """
93 try:95 try:
94 instance = self.compute_api.get(req.environ['nova.context'], id)96 instance = self.compute_api.routing_get(
97 req.environ['nova.context'], id)
95 builder = self._get_view_builder(req)98 builder = self._get_view_builder(req)
96 return builder.build(instance, is_detail=True)99 return builder.build(instance, is_detail=True)
97 except exception.NotFound:100 except exception.NotFound:
98 return faults.Fault(exc.HTTPNotFound())101 return faults.Fault(exc.HTTPNotFound())
99102
103 @scheduler_api.redirect_handler
100 def delete(self, req, id):104 def delete(self, req, id):
101 """ Destroys a server """105 """ Destroys a server """
102 try:106 try:
@@ -227,6 +231,7 @@
227 # if the original error is okay, just reraise it231 # if the original error is okay, just reraise it
228 raise error232 raise error
229233
234 @scheduler_api.redirect_handler
230 def update(self, req, id):235 def update(self, req, id):
231 """ Updates the server name or password """236 """ Updates the server name or password """
232 if len(req.body) == 0:237 if len(req.body) == 0:
@@ -252,6 +257,7 @@
252 return faults.Fault(exc.HTTPNotFound())257 return faults.Fault(exc.HTTPNotFound())
253 return exc.HTTPNoContent()258 return exc.HTTPNoContent()
254259
260 @scheduler_api.redirect_handler
255 def action(self, req, id):261 def action(self, req, id):
256 """Multi-purpose method used to reboot, rebuild, or262 """Multi-purpose method used to reboot, rebuild, or
257 resize a server"""263 resize a server"""
@@ -317,6 +323,7 @@
317 return faults.Fault(exc.HTTPUnprocessableEntity())323 return faults.Fault(exc.HTTPUnprocessableEntity())
318 return exc.HTTPAccepted()324 return exc.HTTPAccepted()
319325
326 @scheduler_api.redirect_handler
320 def lock(self, req, id):327 def lock(self, req, id):
321 """328 """
322 lock the instance with id329 lock the instance with id
@@ -332,6 +339,7 @@
332 return faults.Fault(exc.HTTPUnprocessableEntity())339 return faults.Fault(exc.HTTPUnprocessableEntity())
333 return exc.HTTPAccepted()340 return exc.HTTPAccepted()
334341
342 @scheduler_api.redirect_handler
335 def unlock(self, req, id):343 def unlock(self, req, id):
336 """344 """
337 unlock the instance with id345 unlock the instance with id
@@ -347,6 +355,7 @@
347 return faults.Fault(exc.HTTPUnprocessableEntity())355 return faults.Fault(exc.HTTPUnprocessableEntity())
348 return exc.HTTPAccepted()356 return exc.HTTPAccepted()
349357
358 @scheduler_api.redirect_handler
350 def get_lock(self, req, id):359 def get_lock(self, req, id):
351 """360 """
352 return the boolean state of (instance with id)'s lock361 return the boolean state of (instance with id)'s lock
@@ -361,6 +370,7 @@
361 return faults.Fault(exc.HTTPUnprocessableEntity())370 return faults.Fault(exc.HTTPUnprocessableEntity())
362 return exc.HTTPAccepted()371 return exc.HTTPAccepted()
363372
373 @scheduler_api.redirect_handler
364 def reset_network(self, req, id):374 def reset_network(self, req, id):
365 """375 """
366 Reset networking on an instance (admin only).376 Reset networking on an instance (admin only).
@@ -375,6 +385,7 @@
375 return faults.Fault(exc.HTTPUnprocessableEntity())385 return faults.Fault(exc.HTTPUnprocessableEntity())
376 return exc.HTTPAccepted()386 return exc.HTTPAccepted()
377387
388 @scheduler_api.redirect_handler
378 def inject_network_info(self, req, id):389 def inject_network_info(self, req, id):
379 """390 """
380 Inject network info for an instance (admin only).391 Inject network info for an instance (admin only).
@@ -389,6 +400,7 @@
389 return faults.Fault(exc.HTTPUnprocessableEntity())400 return faults.Fault(exc.HTTPUnprocessableEntity())
390 return exc.HTTPAccepted()401 return exc.HTTPAccepted()
391402
403 @scheduler_api.redirect_handler
392 def pause(self, req, id):404 def pause(self, req, id):
393 """ Permit Admins to Pause the server. """405 """ Permit Admins to Pause the server. """
394 ctxt = req.environ['nova.context']406 ctxt = req.environ['nova.context']
@@ -400,6 +412,7 @@
400 return faults.Fault(exc.HTTPUnprocessableEntity())412 return faults.Fault(exc.HTTPUnprocessableEntity())
401 return exc.HTTPAccepted()413 return exc.HTTPAccepted()
402414
415 @scheduler_api.redirect_handler
403 def unpause(self, req, id):416 def unpause(self, req, id):
404 """ Permit Admins to Unpause the server. """417 """ Permit Admins to Unpause the server. """
405 ctxt = req.environ['nova.context']418 ctxt = req.environ['nova.context']
@@ -411,6 +424,7 @@
411 return faults.Fault(exc.HTTPUnprocessableEntity())424 return faults.Fault(exc.HTTPUnprocessableEntity())
412 return exc.HTTPAccepted()425 return exc.HTTPAccepted()
413426
427 @scheduler_api.redirect_handler
414 def suspend(self, req, id):428 def suspend(self, req, id):
415 """permit admins to suspend the server"""429 """permit admins to suspend the server"""
416 context = req.environ['nova.context']430 context = req.environ['nova.context']
@@ -422,6 +436,7 @@
422 return faults.Fault(exc.HTTPUnprocessableEntity())436 return faults.Fault(exc.HTTPUnprocessableEntity())
423 return exc.HTTPAccepted()437 return exc.HTTPAccepted()
424438
439 @scheduler_api.redirect_handler
425 def resume(self, req, id):440 def resume(self, req, id):
426 """permit admins to resume the server from suspend"""441 """permit admins to resume the server from suspend"""
427 context = req.environ['nova.context']442 context = req.environ['nova.context']
@@ -433,6 +448,7 @@
433 return faults.Fault(exc.HTTPUnprocessableEntity())448 return faults.Fault(exc.HTTPUnprocessableEntity())
434 return exc.HTTPAccepted()449 return exc.HTTPAccepted()
435450
451 @scheduler_api.redirect_handler
436 def rescue(self, req, id):452 def rescue(self, req, id):
437 """Permit users to rescue the server."""453 """Permit users to rescue the server."""
438 context = req.environ["nova.context"]454 context = req.environ["nova.context"]
@@ -444,6 +460,7 @@
444 return faults.Fault(exc.HTTPUnprocessableEntity())460 return faults.Fault(exc.HTTPUnprocessableEntity())
445 return exc.HTTPAccepted()461 return exc.HTTPAccepted()
446462
463 @scheduler_api.redirect_handler
447 def unrescue(self, req, id):464 def unrescue(self, req, id):
448 """Permit users to unrescue the server."""465 """Permit users to unrescue the server."""
449 context = req.environ["nova.context"]466 context = req.environ["nova.context"]
@@ -455,6 +472,7 @@
455 return faults.Fault(exc.HTTPUnprocessableEntity())472 return faults.Fault(exc.HTTPUnprocessableEntity())
456 return exc.HTTPAccepted()473 return exc.HTTPAccepted()
457474
475 @scheduler_api.redirect_handler
458 def get_ajax_console(self, req, id):476 def get_ajax_console(self, req, id):
459 """ Returns a url to an instance's ajaxterm console. """477 """ Returns a url to an instance's ajaxterm console. """
460 try:478 try:
@@ -464,6 +482,7 @@
464 return faults.Fault(exc.HTTPNotFound())482 return faults.Fault(exc.HTTPNotFound())
465 return exc.HTTPAccepted()483 return exc.HTTPAccepted()
466484
485 @scheduler_api.redirect_handler
467 def diagnostics(self, req, id):486 def diagnostics(self, req, id):
468 """Permit Admins to retrieve server diagnostics."""487 """Permit Admins to retrieve server diagnostics."""
469 ctxt = req.environ["nova.context"]488 ctxt = req.environ["nova.context"]
470489
=== 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-24 12:14:14 +0000
@@ -16,8 +16,8 @@
16import common16import common
1717
18from nova import flags18from nova import flags
19from nova import log as logging
19from nova import wsgi20from nova import wsgi
20from nova import db
21from nova.scheduler import api21from nova.scheduler import api
2222
2323
@@ -38,7 +38,8 @@
3838
3939
40def _scrub_zone(zone):40def _scrub_zone(zone):
41 return _filter_keys(zone, ('id', 'api_url'))41 return _exclude_keys(zone, ('username', 'password', 'created_at',
42 'deleted', 'deleted_at', 'updated_at'))
4243
4344
44class Controller(wsgi.Controller):45class Controller(wsgi.Controller):
@@ -52,13 +53,9 @@
52 """Return all zones in brief"""53 """Return all zones in brief"""
53 # Ask the ZoneManager in the Scheduler for most recent data,54 # Ask the ZoneManager in the Scheduler for most recent data,
54 # or fall-back to the database ...55 # or fall-back to the database ...
55 items = api.API().get_zone_list(req.environ['nova.context'])56 items = api.get_zone_list(req.environ['nova.context'])
56 if not items:
57 items = db.zone_get_all(req.environ['nova.context'])
58
59 items = common.limited(items, req)57 items = common.limited(items, req)
60 items = [_exclude_keys(item, ['username', 'password'])58 items = [_scrub_zone(item) for item in items]
61 for item in items]
62 return dict(zones=items)59 return dict(zones=items)
6360
64 def detail(self, req):61 def detail(self, req):
@@ -67,29 +64,37 @@
6764
68 def info(self, req):65 def info(self, req):
69 """Return name and capabilities for this zone."""66 """Return name and capabilities for this zone."""
70 return dict(zone=dict(name=FLAGS.zone_name,67 items = api.get_zone_capabilities(req.environ['nova.context'])
71 capabilities=FLAGS.zone_capabilities))68
69 zone = dict(name=FLAGS.zone_name)
70 caps = FLAGS.zone_capabilities
71 for cap in caps:
72 key_values = cap.split('=')
73 zone[key_values[0]] = key_values[1]
74 for item, (min_value, max_value) in items.iteritems():
75 zone[item] = "%s,%s" % (min_value, max_value)
76 return dict(zone=zone)
7277
73 def show(self, req, id):78 def show(self, req, id):
74 """Return data about the given zone id"""79 """Return data about the given zone id"""
75 zone_id = int(id)80 zone_id = int(id)
76 zone = db.zone_get(req.environ['nova.context'], zone_id)81 zone = api.zone_get(req.environ['nova.context'], zone_id)
77 return dict(zone=_scrub_zone(zone))82 return dict(zone=_scrub_zone(zone))
7883
79 def delete(self, req, id):84 def delete(self, req, id):
80 zone_id = int(id)85 zone_id = int(id)
81 db.zone_delete(req.environ['nova.context'], zone_id)86 api.zone_delete(req.environ['nova.context'], zone_id)
82 return {}87 return {}
8388
84 def create(self, req):89 def create(self, req):
85 context = req.environ['nova.context']90 context = req.environ['nova.context']
86 env = self._deserialize(req.body, req.get_content_type())91 env = self._deserialize(req.body, req.get_content_type())
87 zone = db.zone_create(context, env["zone"])92 zone = api.zone_create(context, env["zone"])
88 return dict(zone=_scrub_zone(zone))93 return dict(zone=_scrub_zone(zone))
8994
90 def update(self, req, id):95 def update(self, req, id):
91 context = req.environ['nova.context']96 context = req.environ['nova.context']
92 env = self._deserialize(req.body, req.get_content_type())97 env = self._deserialize(req.body, req.get_content_type())
93 zone_id = int(id)98 zone_id = int(id)
94 zone = db.zone_update(context, zone_id, env["zone"])99 zone = api.zone_update(context, zone_id, env["zone"])
95 return dict(zone=_scrub_zone(zone))100 return dict(zone=_scrub_zone(zone))
96101
=== modified file 'nova/compute/api.py'
--- nova/compute/api.py 2011-03-23 21:04:42 +0000
+++ nova/compute/api.py 2011-03-24 12:14:14 +0000
@@ -34,6 +34,7 @@
34from nova import utils34from nova import utils
35from nova import volume35from nova import volume
36from nova.compute import instance_types36from nova.compute import instance_types
37from nova.scheduler import api as scheduler_api
37from nova.db import base38from nova.db import base
3839
39FLAGS = flags.FLAGS40FLAGS = flags.FLAGS
@@ -352,6 +353,7 @@
352 rv = self.db.instance_update(context, instance_id, kwargs)353 rv = self.db.instance_update(context, instance_id, kwargs)
353 return dict(rv.iteritems())354 return dict(rv.iteritems())
354355
356 @scheduler_api.reroute_compute("delete")
355 def delete(self, context, instance_id):357 def delete(self, context, instance_id):
356 LOG.debug(_("Going to try to terminate %s"), instance_id)358 LOG.debug(_("Going to try to terminate %s"), instance_id)
357 try:359 try:
@@ -384,6 +386,13 @@
384 rv = self.db.instance_get(context, instance_id)386 rv = self.db.instance_get(context, instance_id)
385 return dict(rv.iteritems())387 return dict(rv.iteritems())
386388
389 @scheduler_api.reroute_compute("get")
390 def routing_get(self, context, instance_id):
391 """Use this method instead of get() if this is the only
392 operation you intend to to. It will route to novaclient.get
393 if the instance is not found."""
394 return self.get(context, instance_id)
395
387 def get_all(self, context, project_id=None, reservation_id=None,396 def get_all(self, context, project_id=None, reservation_id=None,
388 fixed_ip=None):397 fixed_ip=None):
389 """Get all instances, possibly filtered by one of the398 """Get all instances, possibly filtered by one of the
@@ -527,14 +536,17 @@
527 "instance_id": instance_id,536 "instance_id": instance_id,
528 "flavor_id": flavor_id}})537 "flavor_id": flavor_id}})
529538
539 @scheduler_api.reroute_compute("pause")
530 def pause(self, context, instance_id):540 def pause(self, context, instance_id):
531 """Pause the given instance."""541 """Pause the given instance."""
532 self._cast_compute_message('pause_instance', context, instance_id)542 self._cast_compute_message('pause_instance', context, instance_id)
533543
544 @scheduler_api.reroute_compute("unpause")
534 def unpause(self, context, instance_id):545 def unpause(self, context, instance_id):
535 """Unpause the given instance."""546 """Unpause the given instance."""
536 self._cast_compute_message('unpause_instance', context, instance_id)547 self._cast_compute_message('unpause_instance', context, instance_id)
537548
549 @scheduler_api.reroute_compute("diagnostics")
538 def get_diagnostics(self, context, instance_id):550 def get_diagnostics(self, context, instance_id):
539 """Retrieve diagnostics for the given instance."""551 """Retrieve diagnostics for the given instance."""
540 return self._call_compute_message(552 return self._call_compute_message(
@@ -546,18 +558,22 @@
546 """Retrieve actions for the given instance."""558 """Retrieve actions for the given instance."""
547 return self.db.instance_get_actions(context, instance_id)559 return self.db.instance_get_actions(context, instance_id)
548560
561 @scheduler_api.reroute_compute("suspend")
549 def suspend(self, context, instance_id):562 def suspend(self, context, instance_id):
550 """suspend the instance with instance_id"""563 """suspend the instance with instance_id"""
551 self._cast_compute_message('suspend_instance', context, instance_id)564 self._cast_compute_message('suspend_instance', context, instance_id)
552565
566 @scheduler_api.reroute_compute("resume")
553 def resume(self, context, instance_id):567 def resume(self, context, instance_id):
554 """resume the instance with instance_id"""568 """resume the instance with instance_id"""
555 self._cast_compute_message('resume_instance', context, instance_id)569 self._cast_compute_message('resume_instance', context, instance_id)
556570
571 @scheduler_api.reroute_compute("rescue")
557 def rescue(self, context, instance_id):572 def rescue(self, context, instance_id):
558 """Rescue the given instance."""573 """Rescue the given instance."""
559 self._cast_compute_message('rescue_instance', context, instance_id)574 self._cast_compute_message('rescue_instance', context, instance_id)
560575
576 @scheduler_api.reroute_compute("unrescue")
561 def unrescue(self, context, instance_id):577 def unrescue(self, context, instance_id):
562 """Unrescue the given instance."""578 """Unrescue the given instance."""
563 self._cast_compute_message('unrescue_instance', context, instance_id)579 self._cast_compute_message('unrescue_instance', context, instance_id)
@@ -573,7 +589,6 @@
573589
574 def get_ajax_console(self, context, instance_id):590 def get_ajax_console(self, context, instance_id):
575 """Get a url to an AJAX Console"""591 """Get a url to an AJAX Console"""
576 instance = self.get(context, instance_id)
577 output = self._call_compute_message('get_ajax_console',592 output = self._call_compute_message('get_ajax_console',
578 context,593 context,
579 instance_id)594 instance_id)
580595
=== modified file 'nova/compute/manager.py'
--- nova/compute/manager.py 2011-03-24 10:01:22 +0000
+++ nova/compute/manager.py 2011-03-24 12:14:14 +0000
@@ -111,7 +111,7 @@
111 return decorated_function111 return decorated_function
112112
113113
114class ComputeManager(manager.Manager):114class ComputeManager(manager.SchedulerDependentManager):
115115
116 """Manages the running instances from creation to destruction."""116 """Manages the running instances from creation to destruction."""
117117
@@ -132,7 +132,8 @@
132132
133 self.network_manager = utils.import_object(FLAGS.network_manager)133 self.network_manager = utils.import_object(FLAGS.network_manager)
134 self.volume_manager = utils.import_object(FLAGS.volume_manager)134 self.volume_manager = utils.import_object(FLAGS.volume_manager)
135 super(ComputeManager, self).__init__(*args, **kwargs)135 super(ComputeManager, self).__init__(service_name="compute",
136 *args, **kwargs)
136137
137 def init_host(self):138 def init_host(self):
138 """Do any initialization that needs to be run if this is a139 """Do any initialization that needs to be run if this is a
139140
=== modified file 'nova/db/api.py'
--- nova/db/api.py 2011-03-23 04:31:50 +0000
+++ nova/db/api.py 2011-03-24 12:14:14 +0000
@@ -71,6 +71,7 @@
71 """No more available blades"""71 """No more available blades"""
72 pass72 pass
7373
74
74###################75###################
7576
7677
7778
=== modified file 'nova/flags.py'
--- nova/flags.py 2011-03-18 11:35:00 +0000
+++ nova/flags.py 2011-03-24 12:14:14 +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-24 12:14:14 +0000
@@ -53,8 +53,9 @@
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
@@ -74,3 +75,28 @@
74 """Do any initialization that needs to be run if this is a standalone75 """Do any initialization that needs to be run if this is a standalone
75 service. Child classes should override this method."""76 service. Child classes should override this method."""
76 pass77 pass
78
79
80class SchedulerDependentManager(Manager):
81 """Periodically send capability updates to the Scheduler services.
82 Services that need to update the Scheduler of their capabilities
83 should derive from this class. Otherwise they can derive from
84 manager.Manager directly. Updates are only sent after
85 update_service_capabilities is called with non-None values."""
86 def __init__(self, host=None, db_driver=None, service_name="undefined"):
87 self.last_capabilities = None
88 self.service_name = service_name
89 super(SchedulerDependentManager, self).__init__(host, db_driver)
90
91 def update_service_capabilities(self, capabilities):
92 """Remember these capabilities to send on next periodic update."""
93 self.last_capabilities = capabilities
94
95 def periodic_tasks(self, context=None):
96 """Pass data back to the scheduler at a periodic interval"""
97 if self.last_capabilities:
98 logging.debug(_("Notifying Schedulers of capabilities ..."))
99 api.update_service_capabilities(context, self.service_name,
100 self.host, self.last_capabilities)
101
102 super(SchedulerDependentManager, self).periodic_tasks(context)
77103
=== modified file 'nova/network/manager.py'
--- nova/network/manager.py 2011-03-23 05:29:32 +0000
+++ nova/network/manager.py 2011-03-24 12:14:14 +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-24 12:14:14 +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-24 12:14:14 +0000
@@ -17,33 +17,225 @@
17Handles all requests relating to schedulers.17Handles all requests relating to schedulers.
18"""18"""
1919
20import novaclient
21
22from nova import db
23from nova import exception
20from nova import flags24from nova import flags
21from nova import log as logging25from nova import log as logging
22from nova import rpc26from nova import rpc
2327
28from eventlet import greenpool
29
24FLAGS = flags.FLAGS30FLAGS = flags.FLAGS
31flags.DEFINE_bool('enable_zone_routing',
32 False,
33 'When True, routing to child zones will occur.')
34
25LOG = logging.getLogger('nova.scheduler.api')35LOG = logging.getLogger('nova.scheduler.api')
2636
2737
28class API(object):38def _call_scheduler(method, context, params=None):
29 """API for interacting with the scheduler."""39 """Generic handler for RPC calls to the scheduler.
3040
31 def _call_scheduler(self, method, context, params=None):41 :param params: Optional dictionary of arguments to be passed to the
32 """Generic handler for RPC calls to the scheduler.42 scheduler worker
3343
34 :param params: Optional dictionary of arguments to be passed to the44 :retval: Result returned by scheduler worker
35 scheduler worker45 """
3646 if not params:
37 :retval: Result returned by scheduler worker47 params = {}
38 """48 queue = FLAGS.scheduler_topic
39 if not params:49 kwargs = {'method': method, 'args': params}
40 params = {}50 return rpc.call(context, queue, kwargs)
41 queue = FLAGS.scheduler_topic51
42 kwargs = {'method': method, 'args': params}52
43 return rpc.call(context, queue, kwargs)53def get_zone_list(context):
4454 """Return a list of zones assoicated with this zone."""
45 def get_zone_list(self, context):55 items = _call_scheduler('get_zone_list', context)
46 items = self._call_scheduler('get_zone_list', context)56 for item in items:
47 for item in items:57 item['api_url'] = item['api_url'].replace('\\/', '/')
48 item['api_url'] = item['api_url'].replace('\\/', '/')58 if not items:
49 return items59 items = db.zone_get_all(context)
60 return items
61
62
63def zone_get(context, zone_id):
64 return db.zone_get(context, zone_id)
65
66
67def zone_delete(context, zone_id):
68 return db.zone_delete(context, zone_id)
69
70
71def zone_create(context, data):
72 return db.zone_create(context, data)
73
74
75def zone_update(context, zone_id, data):
76 return db.zone_update(context, zone_id, data)
77
78
79def get_zone_capabilities(context, service=None):
80 """Returns a dict of key, value capabilities for this zone,
81 or for a particular class of services running in this zone."""
82 return _call_scheduler('get_zone_capabilities', context=context,
83 params=dict(service=service))
84
85
86def update_service_capabilities(context, service_name, host, capabilities):
87 """Send an update to all the scheduler services informing them
88 of the capabilities of this service."""
89 kwargs = dict(method='update_service_capabilities',
90 args=dict(service_name=service_name, host=host,
91 capabilities=capabilities))
92 return rpc.fanout_cast(context, 'scheduler', kwargs)
93
94
95def _wrap_method(function, self):
96 """Wrap method to supply self."""
97 def _wrap(*args, **kwargs):
98 return function(self, *args, **kwargs)
99 return _wrap
100
101
102def _process(func, zone):
103 """Worker stub for green thread pool. Give the worker
104 an authenticated nova client and zone info."""
105 nova = novaclient.OpenStack(zone.username, zone.password, zone.api_url)
106 nova.authenticate()
107 return func(nova, zone)
108
109
110def child_zone_helper(zone_list, func):
111 """Fire off a command to each zone in the list.
112 The return is [novaclient return objects] from each child zone.
113 For example, if you are calling server.pause(), the list will
114 be whatever the response from server.pause() is. One entry
115 per child zone called."""
116 green_pool = greenpool.GreenPool()
117 return [result for result in green_pool.imap(
118 _wrap_method(_process, func), zone_list)]
119
120
121def _issue_novaclient_command(nova, zone, collection, method_name, item_id):
122 """Use novaclient to issue command to a single child zone.
123 One of these will be run in parallel for each child zone."""
124 manager = getattr(nova, collection)
125 result = None
126 try:
127 try:
128 result = manager.get(int(item_id))
129 except ValueError, e:
130 result = manager.find(name=item_id)
131 except novaclient.NotFound:
132 url = zone.api_url
133 LOG.debug(_("%(collection)s '%(item_id)s' not found on '%(url)s'" %
134 locals()))
135 return None
136
137 if method_name.lower() not in ['get', 'find']:
138 result = getattr(result, method_name)()
139 return result
140
141
142def wrap_novaclient_function(f, collection, method_name, item_id):
143 """Appends collection, method_name and item_id to the incoming
144 (nova, zone) call from child_zone_helper."""
145 def inner(nova, zone):
146 return f(nova, zone, collection, method_name, item_id)
147
148 return inner
149
150
151class RedirectResult(exception.Error):
152 """Used to the HTTP API know that these results are pre-cooked
153 and they can be returned to the caller directly."""
154 def __init__(self, results):
155 self.results = results
156 super(RedirectResult, self).__init__(
157 message=_("Uncaught Zone redirection exception"))
158
159
160class reroute_compute(object):
161 """Decorator used to indicate that the method should
162 delegate the call the child zones if the db query
163 can't find anything."""
164 def __init__(self, method_name):
165 self.method_name = method_name
166
167 def __call__(self, f):
168 def wrapped_f(*args, **kwargs):
169 collection, context, item_id = \
170 self.get_collection_context_and_id(args, kwargs)
171 try:
172 # Call the original function ...
173 return f(*args, **kwargs)
174 except exception.InstanceNotFound, e:
175 LOG.debug(_("Instance %(item_id)s not found "
176 "locally: '%(e)s'" % locals()))
177
178 if not FLAGS.enable_zone_routing:
179 raise
180
181 zones = db.zone_get_all(context)
182 if not zones:
183 raise
184
185 # Ask the children to provide an answer ...
186 LOG.debug(_("Asking child zones ..."))
187 result = self._call_child_zones(zones,
188 wrap_novaclient_function(_issue_novaclient_command,
189 collection, self.method_name, item_id))
190 # Scrub the results and raise another exception
191 # so the API layers can bail out gracefully ...
192 raise RedirectResult(self.unmarshall_result(result))
193 return wrapped_f
194
195 def _call_child_zones(self, zones, function):
196 """Ask the child zones to perform this operation.
197 Broken out for testing."""
198 return child_zone_helper(zones, function)
199
200 def get_collection_context_and_id(self, args, kwargs):
201 """Returns a tuple of (novaclient collection name, security
202 context and resource id. Derived class should override this."""
203 context = kwargs.get('context', None)
204 instance_id = kwargs.get('instance_id', None)
205 if len(args) > 0 and not context:
206 context = args[1]
207 if len(args) > 1 and not instance_id:
208 instance_id = args[2]
209 return ("servers", context, instance_id)
210
211 def unmarshall_result(self, zone_responses):
212 """Result is a list of responses from each child zone.
213 Each decorator derivation is responsible to turning this
214 into a format expected by the calling method. For
215 example, this one is expected to return a single Server
216 dict {'server':{k:v}}. Others may return a list of them, like
217 {'servers':[{k,v}]}"""
218 reduced_response = []
219 for zone_response in zone_responses:
220 if not zone_response:
221 continue
222
223 server = zone_response.__dict__
224
225 for k in server.keys():
226 if k[0] == '_' or k == 'manager':
227 del server[k]
228
229 reduced_response.append(dict(server=server))
230 if reduced_response:
231 return reduced_response[0] # first for now.
232 return {}
233
234
235def redirect_handler(f):
236 def new_f(*args, **kwargs):
237 try:
238 return f(*args, **kwargs)
239 except RedirectResult, e:
240 return e.results
241 return new_f
50242
=== modified file 'nova/scheduler/driver.py'
--- nova/scheduler/driver.py 2011-03-10 04:30:52 +0000
+++ nova/scheduler/driver.py 2011-03-24 12:14:14 +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-24 12:14:14 +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-24 12:14:14 +0000
@@ -58,8 +58,9 @@
58 child zone."""58 child zone."""
59 self.last_seen = datetime.now()59 self.last_seen = datetime.now()
60 self.attempt = 060 self.attempt = 0
61 self.name = zone_metadata["name"]61 self.name = zone_metadata.get("name", "n/a")
62 self.capabilities = zone_metadata["capabilities"]62 self.capabilities = ", ".join(["%s=%s" % (k, v)
63 for k, v in zone_metadata.iteritems() if k != 'name'])
63 self.is_active = True64 self.is_active = True
6465
65 def to_dict(self):66 def to_dict(self):
@@ -104,13 +105,37 @@
104 """Keeps the zone states updated."""105 """Keeps the zone states updated."""
105 def __init__(self):106 def __init__(self):
106 self.last_zone_db_check = datetime.min107 self.last_zone_db_check = datetime.min
107 self.zone_states = {}108 self.zone_states = {} # { <zone_id> : ZoneState }
109 self.service_states = {} # { <service> : { <host> : { cap k : v }}}
108 self.green_pool = greenpool.GreenPool()110 self.green_pool = greenpool.GreenPool()
109111
110 def get_zone_list(self):112 def get_zone_list(self):
111 """Return the list of zones we know about."""113 """Return the list of zones we know about."""
112 return [zone.to_dict() for zone in self.zone_states.values()]114 return [zone.to_dict() for zone in self.zone_states.values()]
113115
116 def get_zone_capabilities(self, context, service=None):
117 """Roll up all the individual host info to generic 'service'
118 capabilities. Each capability is aggregated into
119 <cap>_min and <cap>_max values."""
120 service_dict = self.service_states
121 if service:
122 service_dict = {service: self.service_states.get(service, {})}
123
124 # TODO(sandy) - be smarter about fabricating this structure.
125 # But it's likely to change once we understand what the Best-Match
126 # code will need better.
127 combined = {} # { <service>_<cap> : (min, max), ... }
128 for service_name, host_dict in service_dict.iteritems():
129 for host, caps_dict in host_dict.iteritems():
130 for cap, value in caps_dict.iteritems():
131 key = "%s_%s" % (service_name, cap)
132 min_value, max_value = combined.get(key, (value, value))
133 min_value = min(min_value, value)
134 max_value = max(max_value, value)
135 combined[key] = (min_value, max_value)
136
137 return combined
138
114 def _refresh_from_db(self, context):139 def _refresh_from_db(self, context):
115 """Make our zone state map match the db."""140 """Make our zone state map match the db."""
116 # Add/update existing zones ...141 # Add/update existing zones ...
@@ -141,3 +166,11 @@
141 self.last_zone_db_check = datetime.now()166 self.last_zone_db_check = datetime.now()
142 self._refresh_from_db(context)167 self._refresh_from_db(context)
143 self._poll_zones(context)168 self._poll_zones(context)
169
170 def update_service_capabilities(self, service_name, host, capabilities):
171 """Update the per-service capabilities based on this notification."""
172 logging.debug(_("Received %(service_name)s service update from "
173 "%(host)s: %(capabilities)s") % locals())
174 service_caps = self.service_states.get(service_name, {})
175 service_caps[host] = capabilities
176 self.service_states[service_name] = service_caps
144177
=== modified file 'nova/service.py'
--- nova/service.py 2011-03-18 13:56:05 +0000
+++ nova/service.py 2011-03-24 12:14:14 +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-24 12:14:14 +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-24 12:14:14 +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_scheduler.py'
--- nova/tests/test_scheduler.py 2011-03-10 06:23:13 +0000
+++ nova/tests/test_scheduler.py 2011-03-24 12:14:14 +0000
@@ -21,6 +21,9 @@
2121
22import datetime22import datetime
23import mox23import mox
24import novaclient.exceptions
25import stubout
26import webob
2427
25from mox import IgnoreArg28from mox import IgnoreArg
26from nova import context29from nova import context
@@ -32,6 +35,7 @@
32from nova import rpc35from nova import rpc
33from nova import utils36from nova import utils
34from nova.auth import manager as auth_manager37from nova.auth import manager as auth_manager
38from nova.scheduler import api
35from nova.scheduler import manager39from nova.scheduler import manager
36from nova.scheduler import driver40from nova.scheduler import driver
37from nova.compute import power_state41from nova.compute import power_state
@@ -937,3 +941,160 @@
937 db.instance_destroy(self.context, instance_id)941 db.instance_destroy(self.context, instance_id)
938 db.service_destroy(self.context, s_ref['id'])942 db.service_destroy(self.context, s_ref['id'])
939 db.service_destroy(self.context, s_ref2['id'])943 db.service_destroy(self.context, s_ref2['id'])
944
945
946class FakeZone(object):
947 def __init__(self, api_url, username, password):
948 self.api_url = api_url
949 self.username = username
950 self.password = password
951
952
953def zone_get_all(context):
954 return [
955 FakeZone('http://example.com', 'bob', 'xxx'),
956 ]
957
958
959class FakeRerouteCompute(api.reroute_compute):
960 def _call_child_zones(self, zones, function):
961 return []
962
963 def get_collection_context_and_id(self, args, kwargs):
964 return ("servers", None, 1)
965
966 def unmarshall_result(self, zone_responses):
967 return dict(magic="found me")
968
969
970def go_boom(self, context, instance):
971 raise exception.InstanceNotFound("boom message", instance)
972
973
974def found_instance(self, context, instance):
975 return dict(name='myserver')
976
977
978class FakeResource(object):
979 def __init__(self, attribute_dict):
980 for k, v in attribute_dict.iteritems():
981 setattr(self, k, v)
982
983 def pause(self):
984 pass
985
986
987class ZoneRedirectTest(test.TestCase):
988 def setUp(self):
989 super(ZoneRedirectTest, self).setUp()
990 self.stubs = stubout.StubOutForTesting()
991
992 self.stubs.Set(db, 'zone_get_all', zone_get_all)
993
994 self.enable_zone_routing = FLAGS.enable_zone_routing
995 FLAGS.enable_zone_routing = True
996
997 def tearDown(self):
998 self.stubs.UnsetAll()
999 FLAGS.enable_zone_routing = self.enable_zone_routing
1000 super(ZoneRedirectTest, self).tearDown()
1001
1002 def test_trap_found_locally(self):
1003 decorator = FakeRerouteCompute("foo")
1004 try:
1005 result = decorator(found_instance)(None, None, 1)
1006 except api.RedirectResult, e:
1007 self.fail(_("Successful database hit should succeed"))
1008
1009 def test_trap_not_found_locally(self):
1010 decorator = FakeRerouteCompute("foo")
1011 try:
1012 result = decorator(go_boom)(None, None, 1)
1013 self.assertFail(_("Should have rerouted."))
1014 except api.RedirectResult, e:
1015 self.assertEquals(e.results['magic'], 'found me')
1016
1017 def test_routing_flags(self):
1018 FLAGS.enable_zone_routing = False
1019 decorator = FakeRerouteCompute("foo")
1020 try:
1021 result = decorator(go_boom)(None, None, 1)
1022 self.assertFail(_("Should have thrown exception."))
1023 except exception.InstanceNotFound, e:
1024 self.assertEquals(e.message, 'boom message')
1025
1026 def test_get_collection_context_and_id(self):
1027 decorator = api.reroute_compute("foo")
1028 self.assertEquals(decorator.get_collection_context_and_id(
1029 (None, 10, 20), {}), ("servers", 10, 20))
1030 self.assertEquals(decorator.get_collection_context_and_id(
1031 (None, 11,), dict(instance_id=21)), ("servers", 11, 21))
1032 self.assertEquals(decorator.get_collection_context_and_id(
1033 (None,), dict(context=12, instance_id=22)), ("servers", 12, 22))
1034
1035 def test_unmarshal_single_server(self):
1036 decorator = api.reroute_compute("foo")
1037 self.assertEquals(decorator.unmarshall_result([]), {})
1038 self.assertEquals(decorator.unmarshall_result(
1039 [FakeResource(dict(a=1, b=2)), ]),
1040 dict(server=dict(a=1, b=2)))
1041 self.assertEquals(decorator.unmarshall_result(
1042 [FakeResource(dict(a=1, _b=2)), ]),
1043 dict(server=dict(a=1,)))
1044 self.assertEquals(decorator.unmarshall_result(
1045 [FakeResource(dict(a=1, manager=2)), ]),
1046 dict(server=dict(a=1,)))
1047 self.assertEquals(decorator.unmarshall_result(
1048 [FakeResource(dict(_a=1, manager=2)), ]),
1049 dict(server={}))
1050
1051
1052class FakeServerCollection(object):
1053 def get(self, instance_id):
1054 return FakeResource(dict(a=10, b=20))
1055
1056 def find(self, name):
1057 return FakeResource(dict(a=11, b=22))
1058
1059
1060class FakeEmptyServerCollection(object):
1061 def get(self, f):
1062 raise novaclient.NotFound(1)
1063
1064 def find(self, name):
1065 raise novaclient.NotFound(2)
1066
1067
1068class FakeNovaClient(object):
1069 def __init__(self, collection):
1070 self.servers = collection
1071
1072
1073class DynamicNovaClientTest(test.TestCase):
1074 def test_issue_novaclient_command_found(self):
1075 zone = FakeZone('http://example.com', 'bob', 'xxx')
1076 self.assertEquals(api._issue_novaclient_command(
1077 FakeNovaClient(FakeServerCollection()),
1078 zone, "servers", "get", 100).a, 10)
1079
1080 self.assertEquals(api._issue_novaclient_command(
1081 FakeNovaClient(FakeServerCollection()),
1082 zone, "servers", "find", "name").b, 22)
1083
1084 self.assertEquals(api._issue_novaclient_command(
1085 FakeNovaClient(FakeServerCollection()),
1086 zone, "servers", "pause", 100), None)
1087
1088 def test_issue_novaclient_command_not_found(self):
1089 zone = FakeZone('http://example.com', 'bob', 'xxx')
1090 self.assertEquals(api._issue_novaclient_command(
1091 FakeNovaClient(FakeEmptyServerCollection()),
1092 zone, "servers", "get", 100), None)
1093
1094 self.assertEquals(api._issue_novaclient_command(
1095 FakeNovaClient(FakeEmptyServerCollection()),
1096 zone, "servers", "find", "name"), None)
1097
1098 self.assertEquals(api._issue_novaclient_command(
1099 FakeNovaClient(FakeEmptyServerCollection()),
1100 zone, "servers", "any", "name"), None)
9401101
=== 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-24 12:14:14 +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-24 12:14:14 +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-24 12:14:14 +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-24 12:14:14 +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