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

Proposed by Sandy Walsh
Status: Merged
Approved by: Jay Pipes
Approved revision: 673
Merged at revision: 782
Proposed branch: lp:~sandy-walsh/nova/zones2
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 655 lines (+443/-22)
9 files modified
nova/api/openstack/__init__.py (+3/-3)
nova/api/openstack/zones.py (+20/-4)
nova/flags.py (+4/-0)
nova/scheduler/api.py (+49/-0)
nova/scheduler/manager.py (+10/-0)
nova/scheduler/zone_manager.py (+143/-0)
nova/tests/api/openstack/test_zones.py (+41/-15)
nova/tests/test_zones.py (+172/-0)
tools/pip-requires (+1/-0)
To merge this branch: bzr merge lp:~sandy-walsh/nova/zones2
Reviewer Review Type Date Requested Status
Devin Carlen (community) Approve
Eric Day (community) Approve
Jay Pipes (community) Approve
Review via email: mp+50247@code.launchpad.net

Commit message

Introduces the ZoneManager to the Scheduler which polls the child zones and caches their availability and capabilities.

Description of the change

This branch establishes a ZoneManager within the scheduler.

The ZoneManager takes the child zone structure created in the first phase of this bp and polls the child zones using the public API (and python-novatools, a new pip requirement).

From this polling, the parent zone determines the name & capabilities of the child zones. It also tracks if a zone is online or offline (after successive errors).

Additionally 'novatools zone-list' will now show the status of the child zones as gathered by the scheduler if available. It can take a minute or so for the first polling to occur (configurable).

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

Scheduler API - I would create a scheduler API much like we did for compute/volume/network, and use that API interface instead of making RPC calls directly from nova/api/openstack/zones.py. We want to keep nova.api modules thin wrappers over the core modules so we can reuse those core calls easily (say if some other nova.api module or another core component wants to grab the same info).

zone_capabilities flag - Should this be a list of k/v pairs rather than just a tag list? Something like "hypervisors=xen,kvm;volume=iscsi". Eventually we probably want to think about how this could be auto-generated from child zones/plugins. For example, when a compute worker configured for xen it automatically sets this in the scheduler/API so other zones can discover it. This isn't a blocker for merge since we can figure this out later.

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Hi!

Good second round of coding! :)

72 + for item in items:
73 + item['api_url'] = item['api_url'].replace('\\/', '/')

Are there a lot of URLs with \/ in them?

76 + if len(items) == 0:

Could just shorten the above to: if not items:

106 +DEFINE_string('zone_capabilities', 'xen, linux',
107 + 'comma-delimited list of tags which represent boolean'
108 + ' capabilities of this zone')

I was under the impression that we wanted to store these kind of things in the database, no? I think a flag of boolean-like capabilities is a bit limiting.

277 + def _poll_zones(self, context):
278 + """Try to connect to each child zone and get update."""
279 + green_pool = GreenPool()
280 + green_pool.imap(_poll_zone, self.zone_states.values())

Would it be a bit more efficient to have the ZoneManager initialize a GreenPool in its constructure and have ZoneManager._poll_zones() simply use that pool?

148 +# Copyright (c) 2010 Openstack, LLC.

s/2010/2011 :)

286 + logging.debug("Updating zone cache from db.")

Missed an i18n above

Other than those little nits, looks good. :)

I encourage you to add a bit of overview documentation to the doc/source/ RST files when you get a chance in a future commit.

-jay

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

Good feedback guys ... I'm on it!

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

> zone_capabilities flag - Should this be a list of k/v pairs rather than just a
> tag list? Something like "hypervisors=xen,kvm;volume=iscsi". Eventually we
> probably want to think about how this could be auto-generated from child
> zones/plugins. For example, when a compute worker configured for xen it
> automatically sets this in the scheduler/API so other zones can discover it.

Agreed. Phase 3 is going to be getting Compute, Volume, etc to send messages into the Scheduler service as well with their capabilities (so your API note is timely).

I meant to put the feature in this branch that aggregates the child zone capabilities collected into the /zone/info query ... forgot. I'll do that next in Phase 3.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

235 +def _call_novatools(zone):
236 + """Call novatools. Broken out for testing purposes."""
237 + os = novatools.OpenStack(zone.username, zone.password, zone.api_url)
238 + return os.zones.info()._info
   In this code, I would prefer a name that isn't likely to conflict with common Python module names. While 'os' isn't imported into the namespace for that code, it's a good practice not to use common names, especially when you can pick any name, as they can introduce hard-to-detect bugs.

272 + # Cleanup zones removed from db ...
273 + for zone_id in self.zone_states.keys():
   Simpler: for zone_id in self.zone_states:
(.keys() is redundant)

299 +def zone_get_all_scheduler(x, y, z):
 - and -
308 +def zone_get_all_scheduler_empty(x, y, z):
   Is there a reason for three args named x,y,z?

393 +def exploding_novatools(zone):
   Love it!

454 + zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
455 + username='user1', password='pass1'))
   Fake URLs for testing should use 'example.com'. 'foo.com' is an actual domain name.

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

> 73 + item['api_url'] = item['api_url'].replace('\\/', '/')

The data coming back from the queue seems to be escaped/not unmarshalled properly. I need to investigate this further.

Revision history for this message
Jay Pipes (jaypipes) wrote :

Hi Sandy! Please set the merge prop to Work In Progress while you work on any changes, then set back to Needs Review when you've pushed your changes up to LP. That way, reviewers are sent an email asking them to re-review the patches. Thanks!

Revision history for this message
Jay Pipes (jaypipes) wrote :

excellent work, Sandy. looking forward to phase 3.

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

36: still importing rpc, not needed
37,160,215,348: according to the hacking file, "thou shalt not import objects, only modules"

Otherwise, lgtm!

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

lgtm

review: Approve
Revision history for this message
Devin Carlen (devcamcar) wrote :

lgtm

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (23.1 KiB)

The attempt to merge lp:~sandy-walsh/nova/zones2 into lp:nova failed. Below is the output from the failed tests.

AdminAPITest
    test_admin_disabled ok
    test_admin_enabled ok
APITest
    test_exceptions_are_converted_to_faults ok
Test
    test_authorize_token ok
    test_authorize_user ok
    test_bad_token ok
    test_bad_user ok
    test_no_user ok
    test_token_expiry ok
TestLimiter
    test_authorize_token ok
LimiterTest
    test_limiter_custom_max_limit ok
    test_limiter_limit_and_offset ok
    test_limiter_limit_medium ok
    test_limiter_limit_over_max ok
    test_limiter_limit_zero ok
    test_limiter_nothing ok
    test_limiter_offset_bad ok
    test_limiter_offset_blank ok
    test_limiter_offset_medium ok
    test_limiter_offset_over_max ok
    test_limiter_offset_zero ok
TestFaults
    test_fault_parts ok
    test_raise ok
    test_retry_header ok
FlavorsTest
    test_get_flavor_by_id ok
    test_get_flavor_list ok
GlanceImageServiceTest
    test_create ok
    test_create_and_show_non_existing_image ok
    test_delete ok
    test_update ok
ImageControllerWithGlanceServiceTest
    test_get_image_details ok
    test_get_image_index ok
LocalImageServiceTest
    test_create ok
    test_create_and_show_non_existing_image ok
    test_delete ok
    test_update ok
LimiterTest
    test_minute ok
    test_one_per_period ok
    test_second ok
    test_users_get_separate_buckets ok
    test_we_can_go_indefinitely_if_we_spread_out_requests ok
WSGIAppProxyTest
    test_200 ok
    test_403 ok
    test_failure ...

Revision history for this message
Jay Pipes (jaypipes) wrote :

we need to install novatools on Hudson...

Revision history for this message
Thierry Carrez (ttx) wrote :

I'll quickly look into packaging it for Ubuntu, which should simplify this operation.

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

Any updates on the state of hudson + novaclient? Also, why is it `class API:` and not `class API(object):`?

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

Heh, thanks Todd. I missed that.

Latest news on novaclient: license issues are settled, just need to get a tarball to base the PPA on.

Revision history for this message
Thierry Carrez (ttx) wrote :

python-novaclient is now packaged in PPA and available on Hudson, no more known blockers in getting this in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/openstack/__init__.py'
2--- nova/api/openstack/__init__.py 2011-03-03 01:50:48 +0000
3+++ nova/api/openstack/__init__.py 2011-03-09 12:55:18 +0000
4@@ -77,8 +77,8 @@
5
6 server_members['pause'] = 'POST'
7 server_members['unpause'] = 'POST'
8- server_members["diagnostics"] = "GET"
9- server_members["actions"] = "GET"
10+ server_members['diagnostics'] = 'GET'
11+ server_members['actions'] = 'GET'
12 server_members['suspend'] = 'POST'
13 server_members['resume'] = 'POST'
14 server_members['rescue'] = 'POST'
15@@ -87,7 +87,7 @@
16 server_members['inject_network_info'] = 'POST'
17
18 mapper.resource("zone", "zones", controller=zones.Controller(),
19- collection={'detail': 'GET'})
20+ collection={'detail': 'GET', 'info': 'GET'}),
21
22 mapper.resource("server", "servers", controller=servers.Controller(),
23 collection={'detail': 'GET'},
24
25=== modified file 'nova/api/openstack/zones.py'
26--- nova/api/openstack/zones.py 2011-02-21 07:16:10 +0000
27+++ nova/api/openstack/zones.py 2011-03-09 12:55:18 +0000
28@@ -1,4 +1,4 @@
29-# Copyright 2010 OpenStack LLC.
30+# Copyright 2011 OpenStack LLC.
31 # All Rights Reserved.
32 #
33 # Licensed under the Apache License, Version 2.0 (the "License"); you may
34@@ -18,6 +18,7 @@
35 from nova import flags
36 from nova import wsgi
37 from nova import db
38+from nova.scheduler import api
39
40
41 FLAGS = flags.FLAGS
42@@ -32,6 +33,10 @@
43 return dict((k, v) for k, v in item.iteritems() if k in keys)
44
45
46+def _exclude_keys(item, keys):
47+ return dict((k, v) for k, v in item.iteritems() if k not in keys)
48+
49+
50 def _scrub_zone(zone):
51 return _filter_keys(zone, ('id', 'api_url'))
52
53@@ -41,19 +46,30 @@
54 _serialization_metadata = {
55 'application/xml': {
56 "attributes": {
57- "zone": ["id", "api_url"]}}}
58+ "zone": ["id", "api_url", "name", "capabilities"]}}}
59
60 def index(self, req):
61 """Return all zones in brief"""
62- items = db.zone_get_all(req.environ['nova.context'])
63+ # Ask the ZoneManager in the Scheduler for most recent data,
64+ # or fall-back to the database ...
65+ items = api.API().get_zone_list(req.environ['nova.context'])
66+ if not items:
67+ items = db.zone_get_all(req.environ['nova.context'])
68+
69 items = common.limited(items, req)
70- items = [_scrub_zone(item) for item in items]
71+ items = [_exclude_keys(item, ['username', 'password'])
72+ for item in items]
73 return dict(zones=items)
74
75 def detail(self, req):
76 """Return all zones in detail"""
77 return self.index(req)
78
79+ def info(self, req):
80+ """Return name and capabilities for this zone."""
81+ return dict(zone=dict(name=FLAGS.zone_name,
82+ capabilities=FLAGS.zone_capabilities))
83+
84 def show(self, req, id):
85 """Return data about the given zone id"""
86 zone_id = int(id)
87
88=== modified file 'nova/flags.py'
89--- nova/flags.py 2011-02-25 01:04:25 +0000
90+++ nova/flags.py 2011-03-09 12:55:18 +0000
91@@ -354,3 +354,7 @@
92
93 DEFINE_string('node_availability_zone', 'nova',
94 'availability zone of this node')
95+
96+DEFINE_string('zone_name', 'nova', 'name of this zone')
97+DEFINE_string('zone_capabilities', 'kypervisor:xenserver;os:linux',
98+ 'Key/Value tags which represent capabilities of this zone')
99
100=== added file 'nova/scheduler/api.py'
101--- nova/scheduler/api.py 1970-01-01 00:00:00 +0000
102+++ nova/scheduler/api.py 2011-03-09 12:55:18 +0000
103@@ -0,0 +1,49 @@
104+# Copyright (c) 2011 Openstack, LLC.
105+# All Rights Reserved.
106+#
107+# Licensed under the Apache License, Version 2.0 (the "License"); you may
108+# not use this file except in compliance with the License. You may obtain
109+# a copy of the License at
110+#
111+# http://www.apache.org/licenses/LICENSE-2.0
112+#
113+# Unless required by applicable law or agreed to in writing, software
114+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
115+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
116+# License for the specific language governing permissions and limitations
117+# under the License.
118+
119+"""
120+Handles all requests relating to schedulers.
121+"""
122+
123+from nova import flags
124+from nova import log as logging
125+from nova import rpc
126+
127+FLAGS = flags.FLAGS
128+LOG = logging.getLogger('nova.scheduler.api')
129+
130+
131+class API(object):
132+ """API for interacting with the scheduler."""
133+
134+ def _call_scheduler(self, method, context, params=None):
135+ """Generic handler for RPC calls to the scheduler.
136+
137+ :param params: Optional dictionary of arguments to be passed to the
138+ scheduler worker
139+
140+ :retval: Result returned by scheduler worker
141+ """
142+ if not params:
143+ params = {}
144+ queue = FLAGS.scheduler_topic
145+ kwargs = {'method': method, 'args': params}
146+ return rpc.call(context, queue, kwargs)
147+
148+ def get_zone_list(self, context):
149+ items = self._call_scheduler('get_zone_list', context)
150+ for item in items:
151+ item['api_url'] = item['api_url'].replace('\\/', '/')
152+ return items
153
154=== modified file 'nova/scheduler/manager.py'
155--- nova/scheduler/manager.py 2011-01-19 15:41:30 +0000
156+++ nova/scheduler/manager.py 2011-03-09 12:55:18 +0000
157@@ -29,6 +29,7 @@
158 from nova import manager
159 from nova import rpc
160 from nova import utils
161+from nova.scheduler import zone_manager
162
163 LOG = logging.getLogger('nova.scheduler.manager')
164 FLAGS = flags.FLAGS
165@@ -43,12 +44,21 @@
166 if not scheduler_driver:
167 scheduler_driver = FLAGS.scheduler_driver
168 self.driver = utils.import_object(scheduler_driver)
169+ self.zone_manager = zone_manager.ZoneManager()
170 super(SchedulerManager, self).__init__(*args, **kwargs)
171
172 def __getattr__(self, key):
173 """Converts all method calls to use the schedule method"""
174 return functools.partial(self._schedule, key)
175
176+ def periodic_tasks(self, context=None):
177+ """Poll child zones periodically to get status."""
178+ self.zone_manager.ping(context)
179+
180+ def get_zone_list(self, context=None):
181+ """Get a list of zones from the ZoneManager."""
182+ return self.zone_manager.get_zone_list()
183+
184 def _schedule(self, method, context, topic, *args, **kwargs):
185 """Tries to call schedule_* method on the driver to retrieve host.
186
187
188=== added file 'nova/scheduler/zone_manager.py'
189--- nova/scheduler/zone_manager.py 1970-01-01 00:00:00 +0000
190+++ nova/scheduler/zone_manager.py 2011-03-09 12:55:18 +0000
191@@ -0,0 +1,143 @@
192+# Copyright (c) 2011 Openstack, LLC.
193+# All Rights Reserved.
194+#
195+# Licensed under the Apache License, Version 2.0 (the "License"); you may
196+# not use this file except in compliance with the License. You may obtain
197+# a copy of the License at
198+#
199+# http://www.apache.org/licenses/LICENSE-2.0
200+#
201+# Unless required by applicable law or agreed to in writing, software
202+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
203+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
204+# License for the specific language governing permissions and limitations
205+# under the License.
206+
207+"""
208+ZoneManager oversees all communications with child Zones.
209+"""
210+
211+import novaclient
212+import thread
213+import traceback
214+
215+from datetime import datetime
216+from eventlet import greenpool
217+
218+from nova import db
219+from nova import flags
220+from nova import log as logging
221+
222+FLAGS = flags.FLAGS
223+flags.DEFINE_integer('zone_db_check_interval', 60,
224+ 'Seconds between getting fresh zone info from db.')
225+flags.DEFINE_integer('zone_failures_to_offline', 3,
226+ 'Number of consecutive errors before marking zone offline')
227+
228+
229+class ZoneState(object):
230+ """Holds the state of all connected child zones."""
231+ def __init__(self):
232+ self.is_active = True
233+ self.name = None
234+ self.capabilities = None
235+ self.attempt = 0
236+ self.last_seen = datetime.min
237+ self.last_exception = None
238+ self.last_exception_time = None
239+
240+ def update_credentials(self, zone):
241+ """Update zone credentials from db"""
242+ self.zone_id = zone.id
243+ self.api_url = zone.api_url
244+ self.username = zone.username
245+ self.password = zone.password
246+
247+ def update_metadata(self, zone_metadata):
248+ """Update zone metadata after successful communications with
249+ child zone."""
250+ self.last_seen = datetime.now()
251+ self.attempt = 0
252+ self.name = zone_metadata["name"]
253+ self.capabilities = zone_metadata["capabilities"]
254+ self.is_active = True
255+
256+ def to_dict(self):
257+ return dict(name=self.name, capabilities=self.capabilities,
258+ is_active=self.is_active, api_url=self.api_url,
259+ id=self.zone_id)
260+
261+ def log_error(self, exception):
262+ """Something went wrong. Check to see if zone should be
263+ marked as offline."""
264+ self.last_exception = exception
265+ self.last_exception_time = datetime.now()
266+ api_url = self.api_url
267+ logging.warning(_("'%(exception)s' error talking to "
268+ "zone %(api_url)s") % locals())
269+
270+ max_errors = FLAGS.zone_failures_to_offline
271+ self.attempt += 1
272+ if self.attempt >= max_errors:
273+ self.is_active = False
274+ logging.error(_("No answer from zone %(api_url)s "
275+ "after %(max_errors)d "
276+ "attempts. Marking inactive.") % locals())
277+
278+
279+def _call_novaclient(zone):
280+ """Call novaclient. Broken out for testing purposes."""
281+ client = novaclient.OpenStack(zone.username, zone.password, zone.api_url)
282+ return client.zones.info()._info
283+
284+
285+def _poll_zone(zone):
286+ """Eventlet worker to poll a zone."""
287+ logging.debug(_("Polling zone: %s") % zone.api_url)
288+ try:
289+ zone.update_metadata(_call_novaclient(zone))
290+ except Exception, e:
291+ zone.log_error(traceback.format_exc())
292+
293+
294+class ZoneManager(object):
295+ """Keeps the zone states updated."""
296+ def __init__(self):
297+ self.last_zone_db_check = datetime.min
298+ self.zone_states = {}
299+ self.green_pool = greenpool.GreenPool()
300+
301+ def get_zone_list(self):
302+ """Return the list of zones we know about."""
303+ return [zone.to_dict() for zone in self.zone_states.values()]
304+
305+ def _refresh_from_db(self, context):
306+ """Make our zone state map match the db."""
307+ # Add/update existing zones ...
308+ zones = db.zone_get_all(context)
309+ existing = self.zone_states.keys()
310+ db_keys = []
311+ for zone in zones:
312+ db_keys.append(zone.id)
313+ if zone.id not in existing:
314+ self.zone_states[zone.id] = ZoneState()
315+ self.zone_states[zone.id].update_credentials(zone)
316+
317+ # Cleanup zones removed from db ...
318+ keys = self.zone_states.keys() # since we're deleting
319+ for zone_id in keys:
320+ if zone_id not in db_keys:
321+ del self.zone_states[zone_id]
322+
323+ def _poll_zones(self, context):
324+ """Try to connect to each child zone and get update."""
325+ self.green_pool.imap(_poll_zone, self.zone_states.values())
326+
327+ def ping(self, context=None):
328+ """Ping should be called periodically to update zone status."""
329+ diff = datetime.now() - self.last_zone_db_check
330+ if diff.seconds >= FLAGS.zone_db_check_interval:
331+ logging.debug(_("Updating zone cache from db."))
332+ self.last_zone_db_check = datetime.now()
333+ self._refresh_from_db(context)
334+ self._poll_zones(context)
335
336=== modified file 'nova/tests/api/openstack/test_zones.py'
337--- nova/tests/api/openstack/test_zones.py 2011-02-28 19:44:17 +0000
338+++ nova/tests/api/openstack/test_zones.py 2011-03-09 12:55:18 +0000
339@@ -1,4 +1,4 @@
340-# Copyright 2010 OpenStack LLC.
341+# Copyright 2011 OpenStack LLC.
342 # All Rights Reserved.
343 #
344 # Licensed under the Apache License, Version 2.0 (the "License"); you may
345@@ -24,6 +24,7 @@
346 from nova import test
347 from nova.api.openstack import zones
348 from nova.tests.api.openstack import fakes
349+from nova.scheduler import api
350
351
352 FLAGS = flags.FLAGS
353@@ -31,7 +32,7 @@
354
355
356 def zone_get(context, zone_id):
357- return dict(id=1, api_url='http://foo.com', username='bob',
358+ return dict(id=1, api_url='http://example.com', username='bob',
359 password='xxx')
360
361
362@@ -42,7 +43,7 @@
363
364
365 def zone_update(context, zone_id, values):
366- zone = dict(id=zone_id, api_url='http://foo.com', username='bob',
367+ zone = dict(id=zone_id, api_url='http://example.com', username='bob',
368 password='xxx')
369 zone.update(values)
370 return zone
371@@ -52,12 +53,26 @@
372 pass
373
374
375-def zone_get_all(context):
376- return [
377- dict(id=1, api_url='http://foo.com', username='bob',
378- password='xxx'),
379- dict(id=2, api_url='http://blah.com', username='alice',
380- password='qwerty')]
381+def zone_get_all_scheduler(*args):
382+ return [
383+ dict(id=1, api_url='http://example.com', username='bob',
384+ password='xxx'),
385+ dict(id=2, api_url='http://example.org', username='alice',
386+ password='qwerty')
387+ ]
388+
389+
390+def zone_get_all_scheduler_empty(*args):
391+ return []
392+
393+
394+def zone_get_all_db(context):
395+ return [
396+ dict(id=1, api_url='http://example.com', username='bob',
397+ password='xxx'),
398+ dict(id=2, api_url='http://example.org', username='alice',
399+ password='qwerty')
400+ ]
401
402
403 class ZonesTest(test.TestCase):
404@@ -74,7 +89,6 @@
405 FLAGS.allow_admin_api = True
406
407 self.stubs.Set(nova.db, 'zone_get', zone_get)
408- self.stubs.Set(nova.db, 'zone_get_all', zone_get_all)
409 self.stubs.Set(nova.db, 'zone_update', zone_update)
410 self.stubs.Set(nova.db, 'zone_create', zone_create)
411 self.stubs.Set(nova.db, 'zone_delete', zone_delete)
412@@ -84,7 +98,19 @@
413 FLAGS.allow_admin_api = self.allow_admin
414 super(ZonesTest, self).tearDown()
415
416- def test_get_zone_list(self):
417+ def test_get_zone_list_scheduler(self):
418+ self.stubs.Set(api.API, '_call_scheduler', zone_get_all_scheduler)
419+ req = webob.Request.blank('/v1.0/zones')
420+ res = req.get_response(fakes.wsgi_app())
421+ res_dict = json.loads(res.body)
422+
423+ self.assertEqual(res.status_int, 200)
424+ self.assertEqual(len(res_dict['zones']), 2)
425+
426+ def test_get_zone_list_db(self):
427+ self.stubs.Set(api.API, '_call_scheduler',
428+ zone_get_all_scheduler_empty)
429+ self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db)
430 req = webob.Request.blank('/v1.0/zones')
431 res = req.get_response(fakes.wsgi_app())
432 res_dict = json.loads(res.body)
433@@ -98,7 +124,7 @@
434 res_dict = json.loads(res.body)
435
436 self.assertEqual(res_dict['zone']['id'], 1)
437- self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
438+ self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')
439 self.assertFalse('password' in res_dict['zone'])
440 self.assertEqual(res.status_int, 200)
441
442@@ -109,7 +135,7 @@
443 self.assertEqual(res.status_int, 200)
444
445 def test_zone_create(self):
446- body = dict(zone=dict(api_url='http://blah.zoo', username='fred',
447+ body = dict(zone=dict(api_url='http://example.com', username='fred',
448 password='fubar'))
449 req = webob.Request.blank('/v1.0/zones')
450 req.method = 'POST'
451@@ -120,7 +146,7 @@
452
453 self.assertEqual(res.status_int, 200)
454 self.assertEqual(res_dict['zone']['id'], 1)
455- self.assertEqual(res_dict['zone']['api_url'], 'http://blah.zoo')
456+ self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')
457 self.assertFalse('username' in res_dict['zone'])
458
459 def test_zone_update(self):
460@@ -134,5 +160,5 @@
461
462 self.assertEqual(res.status_int, 200)
463 self.assertEqual(res_dict['zone']['id'], 1)
464- self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
465+ self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')
466 self.assertFalse('username' in res_dict['zone'])
467
468=== added file 'nova/tests/test_zones.py'
469--- nova/tests/test_zones.py 1970-01-01 00:00:00 +0000
470+++ nova/tests/test_zones.py 2011-03-09 12:55:18 +0000
471@@ -0,0 +1,172 @@
472+# Copyright 2010 United States Government as represented by the
473+# All Rights Reserved.
474+#
475+# Licensed under the Apache License, Version 2.0 (the "License"); you may
476+# not use this file except in compliance with the License. You may obtain
477+# a copy of the License at
478+#
479+# http://www.apache.org/licenses/LICENSE-2.0
480+#
481+# Unless required by applicable law or agreed to in writing, software
482+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
483+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
484+# License for the specific language governing permissions and limitations
485+# under the License.
486+"""
487+Tests For ZoneManager
488+"""
489+
490+import datetime
491+import mox
492+import novaclient
493+
494+from nova import context
495+from nova import db
496+from nova import flags
497+from nova import service
498+from nova import test
499+from nova import rpc
500+from nova import utils
501+from nova.auth import manager as auth_manager
502+from nova.scheduler import zone_manager
503+
504+FLAGS = flags.FLAGS
505+
506+
507+class FakeZone:
508+ """Represents a fake zone from the db"""
509+ def __init__(self, *args, **kwargs):
510+ for k, v in kwargs.iteritems():
511+ setattr(self, k, v)
512+
513+
514+def exploding_novaclient(zone):
515+ """Used when we want to simulate a novaclient call failing."""
516+ raise Exception("kaboom")
517+
518+
519+class ZoneManagerTestCase(test.TestCase):
520+ """Test case for zone manager"""
521+ def test_ping(self):
522+ zm = zone_manager.ZoneManager()
523+ self.mox.StubOutWithMock(zm, '_refresh_from_db')
524+ self.mox.StubOutWithMock(zm, '_poll_zones')
525+ zm._refresh_from_db(mox.IgnoreArg())
526+ zm._poll_zones(mox.IgnoreArg())
527+
528+ self.mox.ReplayAll()
529+ zm.ping(None)
530+ self.mox.VerifyAll()
531+
532+ def test_refresh_from_db_new(self):
533+ zm = zone_manager.ZoneManager()
534+
535+ self.mox.StubOutWithMock(db, 'zone_get_all')
536+ db.zone_get_all(mox.IgnoreArg()).AndReturn([
537+ FakeZone(id=1, api_url='http://foo.com', username='user1',
538+ password='pass1'),
539+ ])
540+
541+ self.assertEquals(len(zm.zone_states), 0)
542+
543+ self.mox.ReplayAll()
544+ zm._refresh_from_db(None)
545+ self.mox.VerifyAll()
546+
547+ self.assertEquals(len(zm.zone_states), 1)
548+ self.assertEquals(zm.zone_states[1].username, 'user1')
549+
550+ def test_refresh_from_db_replace_existing(self):
551+ zm = zone_manager.ZoneManager()
552+ zone_state = zone_manager.ZoneState()
553+ zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
554+ username='user1', password='pass1'))
555+ zm.zone_states[1] = zone_state
556+
557+ self.mox.StubOutWithMock(db, 'zone_get_all')
558+ db.zone_get_all(mox.IgnoreArg()).AndReturn([
559+ FakeZone(id=1, api_url='http://foo.com', username='user2',
560+ password='pass2'),
561+ ])
562+
563+ self.assertEquals(len(zm.zone_states), 1)
564+
565+ self.mox.ReplayAll()
566+ zm._refresh_from_db(None)
567+ self.mox.VerifyAll()
568+
569+ self.assertEquals(len(zm.zone_states), 1)
570+ self.assertEquals(zm.zone_states[1].username, 'user2')
571+
572+ def test_refresh_from_db_missing(self):
573+ zm = zone_manager.ZoneManager()
574+ zone_state = zone_manager.ZoneState()
575+ zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
576+ username='user1', password='pass1'))
577+ zm.zone_states[1] = zone_state
578+
579+ self.mox.StubOutWithMock(db, 'zone_get_all')
580+ db.zone_get_all(mox.IgnoreArg()).AndReturn([])
581+
582+ self.assertEquals(len(zm.zone_states), 1)
583+
584+ self.mox.ReplayAll()
585+ zm._refresh_from_db(None)
586+ self.mox.VerifyAll()
587+
588+ self.assertEquals(len(zm.zone_states), 0)
589+
590+ def test_refresh_from_db_add_and_delete(self):
591+ zm = zone_manager.ZoneManager()
592+ zone_state = zone_manager.ZoneState()
593+ zone_state.update_credentials(FakeZone(id=1, api_url='http://foo.com',
594+ username='user1', password='pass1'))
595+ zm.zone_states[1] = zone_state
596+
597+ self.mox.StubOutWithMock(db, 'zone_get_all')
598+
599+ db.zone_get_all(mox.IgnoreArg()).AndReturn([
600+ FakeZone(id=2, api_url='http://foo.com', username='user2',
601+ password='pass2'),
602+ ])
603+ self.assertEquals(len(zm.zone_states), 1)
604+
605+ self.mox.ReplayAll()
606+ zm._refresh_from_db(None)
607+ self.mox.VerifyAll()
608+
609+ self.assertEquals(len(zm.zone_states), 1)
610+ self.assertEquals(zm.zone_states[2].username, 'user2')
611+
612+ def test_poll_zone(self):
613+ self.mox.StubOutWithMock(zone_manager, '_call_novaclient')
614+ zone_manager._call_novaclient(mox.IgnoreArg()).AndReturn(
615+ dict(name='zohan', capabilities='hairdresser'))
616+
617+ zone_state = zone_manager.ZoneState()
618+ zone_state.update_credentials(FakeZone(id=2,
619+ api_url='http://foo.com', username='user2',
620+ password='pass2'))
621+ zone_state.attempt = 1
622+
623+ self.mox.ReplayAll()
624+ zone_manager._poll_zone(zone_state)
625+ self.mox.VerifyAll()
626+ self.assertEquals(zone_state.attempt, 0)
627+ self.assertEquals(zone_state.name, 'zohan')
628+
629+ def test_poll_zone_fails(self):
630+ self.stubs.Set(zone_manager, "_call_novaclient", exploding_novaclient)
631+
632+ zone_state = zone_manager.ZoneState()
633+ zone_state.update_credentials(FakeZone(id=2,
634+ api_url='http://foo.com', username='user2',
635+ password='pass2'))
636+ zone_state.attempt = FLAGS.zone_failures_to_offline - 1
637+
638+ self.mox.ReplayAll()
639+ zone_manager._poll_zone(zone_state)
640+ self.mox.VerifyAll()
641+ self.assertEquals(zone_state.attempt, 3)
642+ self.assertFalse(zone_state.is_active)
643+ self.assertEquals(zone_state.name, None)
644
645=== modified file 'tools/pip-requires'
646--- tools/pip-requires 2011-01-23 20:52:09 +0000
647+++ tools/pip-requires 2011-03-09 12:55:18 +0000
648@@ -10,6 +10,7 @@
649 carrot==0.10.5
650 eventlet==0.9.12
651 lockfile==0.8
652+python-novaclient==2.3
653 python-daemon==1.5.5
654 python-gflags==1.3
655 redis==2.0.0