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

Proposed by Sandy Walsh
Status: Merged
Approved by: Matt Dietz
Approved revision: 651
Merged at revision: 696
Proposed branch: lp:~sandy-walsh/nova/zones
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 487 lines (+374/-9)
11 files modified
nova/api/openstack/__init__.py (+4/-0)
nova/api/openstack/auth.py (+1/-0)
nova/api/openstack/servers.py (+0/-2)
nova/api/openstack/zones.py (+80/-0)
nova/auth/novarc.template (+3/-4)
nova/db/api.py (+28/-0)
nova/db/sqlalchemy/api.py (+44/-0)
nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py (+61/-0)
nova/db/sqlalchemy/migration.py (+2/-2)
nova/db/sqlalchemy/models.py (+10/-1)
nova/tests/api/openstack/test_zones.py (+141/-0)
To merge this branch: bzr merge lp:~sandy-walsh/nova/zones
Reviewer Review Type Date Requested Status
Matt Dietz (community) Approve
Eric Day (community) Approve
Jay Pipes (community) Needs Fixing
Review via email: mp+49730@code.launchpad.net

Commit message

Added http://mynova/v1.0/zones/ api options for add/remove/update/delete zones. child_zones table added to database and migration. Changed novarc vars from CLOUD_SERVERS_* to NOVA_* to work with novatools. See python-novatools on github for help testing this.

Description of the change

This is the first phase of the multi-zone clustering. Right now it's just the API/DB stuff for adding/listing/deleting zones.

Zones are just URL + username/password credentials of child zones.

python-novatools (on github) was also updated to support these changes.

Next we'll get these zones talking to each other.

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

was the bin/nova-combined change accidental?

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

I would s/ChildZone/Zone/ for consistent naming. Having the API deal with 'zones' and the DB deal with 'child zones' may make someone think they are not the same.

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

Thanks for the catch todd. Missed that on my diff.

Eric, yeah, I debated that one. I agree & will change. Thanks.

lp:~sandy-walsh/nova/zones updated
646. By Sandy Walsh

fixed nova-combined debug hack and renamed ChildZone to Zone

647. By Sandy Walsh

better filtering

648. By Sandy Walsh

trunk merge

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

Hi! Great work so far, Sandy.

A couple notes:

1) 223 === added file 'nova/db/sqlalchemy/migrate_repo/versions/003_cactus.py'

I think we wanted to go with >1 migrate scripts per release, with a migration script for each patch that makes changes to the underlying database schema.

So, I'd recommend s/003_cactus.py/003_add_zone_tables.py/.

2) Looks like you are still calling things child zones in the migration script and in the nova.db.api docstrings...

Other than those little things, lgtm.

review: Needs Fixing
lp:~sandy-walsh/nova/zones updated
649. By Sandy Walsh

fixed / renamed migration scripts

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

lgtm

review: Approve
lp:~sandy-walsh/nova/zones updated
650. By Sandy Walsh

changed from 003-004 migration

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

lgtm.

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

Whoops, missed the NASA copyright in the migration file. Should be the Openstack one

review: Needs Fixing
lp:~sandy-walsh/nova/zones updated
651. By Sandy Walsh

copyright notice

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

again.

review: Approve

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-02-16 20:12:54 +0000
3+++ nova/api/openstack/__init__.py 2011-02-17 21:54:13 +0000
4@@ -34,6 +34,7 @@
5 from nova.api.openstack import images
6 from nova.api.openstack import servers
7 from nova.api.openstack import shared_ip_groups
8+from nova.api.openstack import zones
9
10
11 LOG = logging.getLogger('nova.api.openstack')
12@@ -81,6 +82,9 @@
13 server_members['resume'] = 'POST'
14 server_members['reset_network'] = 'POST'
15
16+ mapper.resource("zone", "zones", controller=zones.Controller(),
17+ collection={'detail': 'GET'})
18+
19 mapper.resource("server", "servers", controller=servers.Controller(),
20 collection={'detail': 'GET'},
21 member=server_members)
22
23=== modified file 'nova/api/openstack/auth.py'
24--- nova/api/openstack/auth.py 2011-01-06 18:57:48 +0000
25+++ nova/api/openstack/auth.py 2011-02-17 21:54:13 +0000
26@@ -19,6 +19,7 @@
27 import hashlib
28 import json
29 import time
30+import logging
31
32 import webob.exc
33 import webob.dec
34
35=== modified file 'nova/api/openstack/servers.py'
36--- nova/api/openstack/servers.py 2011-02-17 19:38:11 +0000
37+++ nova/api/openstack/servers.py 2011-02-17 21:54:13 +0000
38@@ -1,5 +1,3 @@
39-# vim: tabstop=4 shiftwidth=4 softtabstop=4
40-
41 # Copyright 2010 OpenStack LLC.
42 # All Rights Reserved.
43 #
44
45=== added file 'nova/api/openstack/zones.py'
46--- nova/api/openstack/zones.py 1970-01-01 00:00:00 +0000
47+++ nova/api/openstack/zones.py 2011-02-17 21:54:13 +0000
48@@ -0,0 +1,80 @@
49+# Copyright 2010 OpenStack LLC.
50+# All Rights Reserved.
51+#
52+# Licensed under the Apache License, Version 2.0 (the "License"); you may
53+# not use this file except in compliance with the License. You may obtain
54+# a copy of the License at
55+#
56+# http://www.apache.org/licenses/LICENSE-2.0
57+#
58+# Unless required by applicable law or agreed to in writing, software
59+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
60+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
61+# License for the specific language governing permissions and limitations
62+# under the License.
63+
64+import common
65+import logging
66+
67+from nova import flags
68+from nova import wsgi
69+from nova import db
70+
71+
72+FLAGS = flags.FLAGS
73+
74+
75+def _filter_keys(item, keys):
76+ """
77+ Filters all model attributes except for keys
78+ item is a dict
79+
80+ """
81+ return dict((k, v) for k, v in item.iteritems() if k in keys)
82+
83+
84+def _scrub_zone(zone):
85+ return _filter_keys(zone, ('id', 'api_url'))
86+
87+
88+class Controller(wsgi.Controller):
89+
90+ _serialization_metadata = {
91+ 'application/xml': {
92+ "attributes": {
93+ "zone": ["id", "api_url"]}}}
94+
95+ def index(self, req):
96+ """Return all zones in brief"""
97+ items = db.zone_get_all(req.environ['nova.context'])
98+ items = common.limited(items, req)
99+ items = [_scrub_zone(item) for item in items]
100+ return dict(zones=items)
101+
102+ def detail(self, req):
103+ """Return all zones in detail"""
104+ return self.index(req)
105+
106+ def show(self, req, id):
107+ """Return data about the given zone id"""
108+ zone_id = int(id)
109+ zone = db.zone_get(req.environ['nova.context'], zone_id)
110+ return dict(zone=_scrub_zone(zone))
111+
112+ def delete(self, req, id):
113+ zone_id = int(id)
114+ db.zone_delete(req.environ['nova.context'], zone_id)
115+ return {}
116+
117+ def create(self, req):
118+ context = req.environ['nova.context']
119+ env = self._deserialize(req.body, req)
120+ zone = db.zone_create(context, env["zone"])
121+ return dict(zone=_scrub_zone(zone))
122+
123+ def update(self, req, id):
124+ context = req.environ['nova.context']
125+ env = self._deserialize(req.body, req)
126+ zone_id = int(id)
127+ zone = db.zone_update(context, zone_id, env["zone"])
128+ return dict(zone=_scrub_zone(zone))
129
130=== modified file 'nova/auth/novarc.template'
131--- nova/auth/novarc.template 2011-01-04 01:06:50 +0000
132+++ nova/auth/novarc.template 2011-02-17 21:54:13 +0000
133@@ -10,7 +10,6 @@
134 export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set
135 alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}"
136 alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}"
137-export CLOUD_SERVERS_API_KEY="%(access)s"
138-export CLOUD_SERVERS_USERNAME="%(user)s"
139-export CLOUD_SERVERS_URL="%(os)s"
140-
141+export NOVA_API_KEY="%(access)s"
142+export NOVA_USERNAME="%(user)s"
143+export NOVA_URL="%(os)s"
144
145=== modified file 'nova/db/api.py'
146--- nova/db/api.py 2011-02-17 19:32:30 +0000
147+++ nova/db/api.py 2011-02-17 21:54:13 +0000
148@@ -1000,3 +1000,31 @@
149 def console_get(context, console_id, instance_id=None):
150 """Get a specific console (possibly on a given instance)."""
151 return IMPL.console_get(context, console_id, instance_id)
152+
153+
154+####################
155+
156+
157+def zone_create(context, values):
158+ """Create a new child Zone entry."""
159+ return IMPL.zone_create(context, values)
160+
161+
162+def zone_update(context, zone_id, values):
163+ """Update a child Zone entry."""
164+ return IMPL.zone_update(context, values)
165+
166+
167+def zone_delete(context, zone_id):
168+ """Delete a child Zone."""
169+ return IMPL.zone_delete(context, zone_id)
170+
171+
172+def zone_get(context, zone_id):
173+ """Get a specific child Zone."""
174+ return IMPL.zone_get(context, zone_id)
175+
176+
177+def zone_get_all(context):
178+ """Get all child Zones."""
179+ return IMPL.zone_get_all(context)
180
181=== modified file 'nova/db/sqlalchemy/api.py'
182--- nova/db/sqlalchemy/api.py 2011-02-17 19:32:30 +0000
183+++ nova/db/sqlalchemy/api.py 2011-02-17 21:54:13 +0000
184@@ -2058,3 +2058,47 @@
185 raise exception.NotFound(_("No console with id %(console_id)s"
186 " %(idesc)s") % locals())
187 return result
188+
189+
190+####################
191+
192+
193+@require_admin_context
194+def zone_create(context, values):
195+ zone = models.Zone()
196+ zone.update(values)
197+ zone.save()
198+ return zone
199+
200+
201+@require_admin_context
202+def zone_update(context, zone_id, values):
203+ zone = session.query(models.Zone).filter_by(id=zone_id).first()
204+ if not zone:
205+ raise exception.NotFound(_("No zone with id %(zone_id)s") % locals())
206+ zone.update(values)
207+ zone.save()
208+ return zone
209+
210+
211+@require_admin_context
212+def zone_delete(context, zone_id):
213+ session = get_session()
214+ with session.begin():
215+ session.execute('delete from zones '
216+ 'where id=:id', {'id': zone_id})
217+
218+
219+@require_admin_context
220+def zone_get(context, zone_id):
221+ session = get_session()
222+ result = session.query(models.Zone).filter_by(id=zone_id).first()
223+ if not result:
224+ raise exception.NotFound(_("No zone with id %(zone_id)s") % locals())
225+ return result
226+
227+
228+@require_admin_context
229+def zone_get_all(context):
230+ session = get_session()
231+ return session.query(models.Zone).all()
232
233=== added file 'nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py'
234--- nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py 1970-01-01 00:00:00 +0000
235+++ nova/db/sqlalchemy/migrate_repo/versions/004_add_zone_tables.py 2011-02-17 21:54:13 +0000
236@@ -0,0 +1,61 @@
237+# Copyright 2010 OpenStack LLC.
238+# All Rights Reserved.
239+#
240+# Licensed under the Apache License, Version 2.0 (the "License"); you may
241+# not use this file except in compliance with the License. You may obtain
242+# a copy of the License at
243+#
244+# http://www.apache.org/licenses/LICENSE-2.0
245+#
246+# Unless required by applicable law or agreed to in writing, software
247+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
248+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
249+# License for the specific language governing permissions and limitations
250+# under the License.
251+
252+from sqlalchemy import *
253+from migrate import *
254+
255+from nova import log as logging
256+
257+
258+meta = MetaData()
259+
260+
261+#
262+# New Tables
263+#
264+zones = Table('zones', meta,
265+ Column('created_at', DateTime(timezone=False)),
266+ Column('updated_at', DateTime(timezone=False)),
267+ Column('deleted_at', DateTime(timezone=False)),
268+ Column('deleted', Boolean(create_constraint=True, name=None)),
269+ Column('id', Integer(), primary_key=True, nullable=False),
270+ Column('api_url',
271+ String(length=255, convert_unicode=False, assert_unicode=None,
272+ unicode_error=None, _warn_on_bytestring=False)),
273+ Column('username',
274+ String(length=255, convert_unicode=False, assert_unicode=None,
275+ unicode_error=None, _warn_on_bytestring=False)),
276+ Column('password',
277+ String(length=255, convert_unicode=False, assert_unicode=None,
278+ unicode_error=None, _warn_on_bytestring=False)),
279+ )
280+
281+
282+#
283+# Tables to alter
284+#
285+
286+# (none currently)
287+
288+
289+def upgrade(migrate_engine):
290+ # Upgrade operations go here. Don't create your own engine;
291+ # bind migrate_engine to your metadata
292+ meta.bind = migrate_engine
293+ for table in (zones, ):
294+ try:
295+ table.create()
296+ except Exception:
297+ logging.info(repr(table))
298
299=== modified file 'nova/db/sqlalchemy/migration.py'
300--- nova/db/sqlalchemy/migration.py 2011-02-16 18:24:46 +0000
301+++ nova/db/sqlalchemy/migration.py 2011-02-17 21:54:13 +0000
302@@ -55,8 +55,8 @@
303 engine = sqlalchemy.create_engine(FLAGS.sql_connection, echo=False)
304 meta.reflect(bind=engine)
305 try:
306- for table in ('auth_tokens', 'export_devices', 'fixed_ips',
307- 'floating_ips', 'instances',
308+ for table in ('auth_tokens', 'zones', 'export_devices',
309+ 'fixed_ips', 'floating_ips', 'instances',
310 'key_pairs', 'networks', 'projects', 'quotas',
311 'security_group_instance_association',
312 'security_group_rules', 'security_groups',
313
314=== modified file 'nova/db/sqlalchemy/models.py'
315--- nova/db/sqlalchemy/models.py 2011-02-14 23:18:59 +0000
316+++ nova/db/sqlalchemy/models.py 2011-02-17 21:54:13 +0000
317@@ -536,6 +536,15 @@
318 pool = relationship(ConsolePool, backref=backref('consoles'))
319
320
321+class Zone(BASE, NovaBase):
322+ """Represents a child zone of this zone."""
323+ __tablename__ = 'zones'
324+ id = Column(Integer, primary_key=True)
325+ api_url = Column(String(255))
326+ username = Column(String(255))
327+ password = Column(String(255))
328+
329+
330 def register_models():
331 """Register Models and create metadata.
332
333@@ -548,7 +557,7 @@
334 Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
335 Network, SecurityGroup, SecurityGroupIngressRule,
336 SecurityGroupInstanceAssociation, AuthToken, User,
337- Project, Certificate, ConsolePool, Console) # , Image, Host
338+ Project, Certificate, ConsolePool, Console, Zone)
339 engine = create_engine(FLAGS.sql_connection, echo=False)
340 for model in models:
341 model.metadata.create_all(engine)
342
343=== added file 'nova/tests/api/openstack/test_zones.py'
344--- nova/tests/api/openstack/test_zones.py 1970-01-01 00:00:00 +0000
345+++ nova/tests/api/openstack/test_zones.py 2011-02-17 21:54:13 +0000
346@@ -0,0 +1,141 @@
347+# Copyright 2010 OpenStack LLC.
348+# All Rights Reserved.
349+#
350+# Licensed under the Apache License, Version 2.0 (the "License"); you may
351+# not use this file except in compliance with the License. You may obtain
352+# a copy of the License at
353+#
354+# http://www.apache.org/licenses/LICENSE-2.0
355+#
356+# Unless required by applicable law or agreed to in writing, software
357+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
358+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
359+# License for the specific language governing permissions and limitations
360+# under the License.
361+
362+import unittest
363+
364+import stubout
365+import webob
366+import json
367+
368+import nova.db
369+from nova import context
370+from nova import flags
371+from nova.api.openstack import zones
372+from nova.tests.api.openstack import fakes
373+
374+
375+FLAGS = flags.FLAGS
376+FLAGS.verbose = True
377+
378+
379+def zone_get(context, zone_id):
380+ return dict(id=1, api_url='http://foo.com', username='bob',
381+ password='xxx')
382+
383+
384+def zone_create(context, values):
385+ zone = dict(id=1)
386+ zone.update(values)
387+ return zone
388+
389+
390+def zone_update(context, zone_id, values):
391+ zone = dict(id=zone_id, api_url='http://foo.com', username='bob',
392+ password='xxx')
393+ zone.update(values)
394+ return zone
395+
396+
397+def zone_delete(context, zone_id):
398+ pass
399+
400+
401+def zone_get_all(context):
402+ return [
403+ dict(id=1, api_url='http://foo.com', username='bob',
404+ password='xxx'),
405+ dict(id=2, api_url='http://blah.com', username='alice',
406+ password='qwerty')
407+ ]
408+
409+
410+class ZonesTest(unittest.TestCase):
411+ def setUp(self):
412+ self.stubs = stubout.StubOutForTesting()
413+ fakes.FakeAuthManager.auth_data = {}
414+ fakes.FakeAuthDatabase.data = {}
415+ fakes.stub_out_networking(self.stubs)
416+ fakes.stub_out_rate_limiting(self.stubs)
417+ fakes.stub_out_auth(self.stubs)
418+
419+ self.allow_admin = FLAGS.allow_admin_api
420+ FLAGS.allow_admin_api = True
421+
422+ self.stubs.Set(nova.db, 'zone_get', zone_get)
423+ self.stubs.Set(nova.db, 'zone_get_all', zone_get_all)
424+ self.stubs.Set(nova.db, 'zone_update', zone_update)
425+ self.stubs.Set(nova.db, 'zone_create', zone_create)
426+ self.stubs.Set(nova.db, 'zone_delete', zone_delete)
427+
428+ def tearDown(self):
429+ self.stubs.UnsetAll()
430+ FLAGS.allow_admin_api = self.allow_admin
431+
432+ def test_get_zone_list(self):
433+ req = webob.Request.blank('/v1.0/zones')
434+ res = req.get_response(fakes.wsgi_app())
435+ res_dict = json.loads(res.body)
436+
437+ self.assertEqual(res.status_int, 200)
438+ self.assertEqual(len(res_dict['zones']), 2)
439+
440+ def test_get_zone_by_id(self):
441+ req = webob.Request.blank('/v1.0/zones/1')
442+ res = req.get_response(fakes.wsgi_app())
443+ res_dict = json.loads(res.body)
444+
445+ self.assertEqual(res_dict['zone']['id'], 1)
446+ self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
447+ self.assertFalse('password' in res_dict['zone'])
448+ self.assertEqual(res.status_int, 200)
449+
450+ def test_zone_delete(self):
451+ req = webob.Request.blank('/v1.0/zones/1')
452+ res = req.get_response(fakes.wsgi_app())
453+
454+ self.assertEqual(res.status_int, 200)
455+
456+ def test_zone_create(self):
457+ body = dict(zone=dict(api_url='http://blah.zoo', username='fred',
458+ password='fubar'))
459+ req = webob.Request.blank('/v1.0/zones')
460+ req.method = 'POST'
461+ req.body = json.dumps(body)
462+
463+ res = req.get_response(fakes.wsgi_app())
464+ res_dict = json.loads(res.body)
465+
466+ self.assertEqual(res.status_int, 200)
467+ self.assertEqual(res_dict['zone']['id'], 1)
468+ self.assertEqual(res_dict['zone']['api_url'], 'http://blah.zoo')
469+ self.assertFalse('username' in res_dict['zone'])
470+
471+ def test_zone_update(self):
472+ body = dict(zone=dict(username='zeb', password='sneaky'))
473+ req = webob.Request.blank('/v1.0/zones/1')
474+ req.method = 'PUT'
475+ req.body = json.dumps(body)
476+
477+ res = req.get_response(fakes.wsgi_app())
478+ res_dict = json.loads(res.body)
479+
480+ self.assertEqual(res.status_int, 200)
481+ self.assertEqual(res_dict['zone']['id'], 1)
482+ self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
483+ self.assertFalse('username' in res_dict['zone'])
484+
485+
486+if __name__ == '__main__':
487+ unittest.main()