Merge lp:~justin-fathomdb/nova/test-openstack-login into lp:~hudson-openstack/nova/trunk

Proposed by justinsb
Status: Superseded
Proposed branch: lp:~justin-fathomdb/nova/test-openstack-login
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~justin-fathomdb/nova/test-openstack-api
Diff against target: 1177 lines (+1107/-0) (has conflicts)
12 files modified
nova/api/openstack/accounts.py (+85/-0)
nova/api/openstack/users.py (+93/-0)
nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py (+51/-0)
nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py (+83/-0)
nova/tests/api/openstack/test_accounts.py (+125/-0)
nova/tests/api/openstack/test_users.py (+141/-0)
nova/tests/integrated/__init__.py (+20/-0)
nova/tests/integrated/api/__init__.py (+20/-0)
nova/tests/integrated/api/client.py (+213/-0)
nova/tests/integrated/integrated_helpers.py (+188/-0)
nova/tests/integrated/test_login.py (+79/-0)
nova/virt/cpuinfo.xml.template (+9/-0)
Conflict adding file nova/api/openstack/accounts.py.  Moved existing file to nova/api/openstack/accounts.py.moved.
Conflict adding file nova/api/openstack/users.py.  Moved existing file to nova/api/openstack/users.py.moved.
Conflict adding file nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py.  Moved existing file to nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py.moved.
Conflict adding file nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py.  Moved existing file to nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py.moved.
Conflict adding file nova/tests/api/openstack/test_accounts.py.  Moved existing file to nova/tests/api/openstack/test_accounts.py.moved.
Conflict adding file nova/tests/api/openstack/test_users.py.  Moved existing file to nova/tests/api/openstack/test_users.py.moved.
Conflict adding file nova/tests/integrated.  Moved existing file to nova/tests/integrated.moved.
Conflict adding file nova/virt/cpuinfo.xml.template.  Moved existing file to nova/virt/cpuinfo.xml.template.moved.
To merge this branch: bzr merge lp:~justin-fathomdb/nova/test-openstack-login
Reviewer Review Type Date Requested Status
Nova Core security contacts Pending
Review via email: mp+52933@code.launchpad.net

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

Description of the change

Test the login behavior of the OpenStack API. Uncovered bug732866

To post a comment you must log in.
794. By justinsb

Merge branch 'test-openstack-login-messy' into test-openstack-login

795. By justinsb

Merge branch 'nova' into test-openstack-login

Unmerged revisions

795. By justinsb

Merge branch 'nova' into test-openstack-login

794. By justinsb

Merge branch 'test-openstack-login-messy' into test-openstack-login

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'nova/api/openstack/accounts.py'
2--- nova/api/openstack/accounts.py 1970-01-01 00:00:00 +0000
3+++ nova/api/openstack/accounts.py 2011-03-15 05:15:51 +0000
4@@ -0,0 +1,85 @@
5+# Copyright 2011 OpenStack LLC.
6+# All Rights Reserved.
7+#
8+# Licensed under the Apache License, Version 2.0 (the "License"); you may
9+# not use this file except in compliance with the License. You may obtain
10+# a copy of the License at
11+#
12+# http://www.apache.org/licenses/LICENSE-2.0
13+#
14+# Unless required by applicable law or agreed to in writing, software
15+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17+# License for the specific language governing permissions and limitations
18+# under the License.
19+
20+import common
21+
22+from nova import exception
23+from nova import flags
24+from nova import log as logging
25+from nova import wsgi
26+
27+from nova.auth import manager
28+from nova.api.openstack import faults
29+
30+FLAGS = flags.FLAGS
31+LOG = logging.getLogger('nova.api.openstack')
32+
33+
34+def _translate_keys(account):
35+ return dict(id=account.id,
36+ name=account.name,
37+ description=account.description,
38+ manager=account.project_manager_id)
39+
40+
41+class Controller(wsgi.Controller):
42+
43+ _serialization_metadata = {
44+ 'application/xml': {
45+ "attributes": {
46+ "account": ["id", "name", "description", "manager"]}}}
47+
48+ def __init__(self):
49+ self.manager = manager.AuthManager()
50+
51+ def _check_admin(self, context):
52+ """We cannot depend on the db layer to check for admin access
53+ for the auth manager, so we do it here"""
54+ if not context.is_admin:
55+ raise exception.NotAuthorized(_("Not admin user."))
56+
57+ def index(self, req):
58+ raise faults.Fault(exc.HTTPNotImplemented())
59+
60+ def detail(self, req):
61+ raise faults.Fault(exc.HTTPNotImplemented())
62+
63+ def show(self, req, id):
64+ """Return data about the given account id"""
65+ account = self.manager.get_project(id)
66+ return dict(account=_translate_keys(account))
67+
68+ def delete(self, req, id):
69+ self._check_admin(req.environ['nova.context'])
70+ self.manager.delete_project(id)
71+ return {}
72+
73+ def create(self, req):
74+ """We use update with create-or-update semantics
75+ because the id comes from an external source"""
76+ raise faults.Fault(exc.HTTPNotImplemented())
77+
78+ def update(self, req, id):
79+ """This is really create or update."""
80+ self._check_admin(req.environ['nova.context'])
81+ env = self._deserialize(req.body, req.get_content_type())
82+ description = env['account'].get('description')
83+ manager = env['account'].get('manager')
84+ try:
85+ account = self.manager.get_project(id)
86+ self.manager.modify_project(id, manager, description)
87+ except exception.NotFound:
88+ account = self.manager.create_project(id, manager, description)
89+ return dict(account=_translate_keys(account))
90
91=== renamed file 'nova/api/openstack/accounts.py' => 'nova/api/openstack/accounts.py.moved'
92=== added file 'nova/api/openstack/users.py'
93--- nova/api/openstack/users.py 1970-01-01 00:00:00 +0000
94+++ nova/api/openstack/users.py 2011-03-15 05:15:51 +0000
95@@ -0,0 +1,93 @@
96+# Copyright 2011 OpenStack LLC.
97+# All Rights Reserved.
98+#
99+# Licensed under the Apache License, Version 2.0 (the "License"); you may
100+# not use this file except in compliance with the License. You may obtain
101+# a copy of the License at
102+#
103+# http://www.apache.org/licenses/LICENSE-2.0
104+#
105+# Unless required by applicable law or agreed to in writing, software
106+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
107+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
108+# License for the specific language governing permissions and limitations
109+# under the License.
110+
111+import common
112+
113+from nova import exception
114+from nova import flags
115+from nova import log as logging
116+from nova import wsgi
117+
118+from nova.auth import manager
119+
120+FLAGS = flags.FLAGS
121+LOG = logging.getLogger('nova.api.openstack')
122+
123+
124+def _translate_keys(user):
125+ return dict(id=user.id,
126+ name=user.name,
127+ access=user.access,
128+ secret=user.secret,
129+ admin=user.admin)
130+
131+
132+class Controller(wsgi.Controller):
133+
134+ _serialization_metadata = {
135+ 'application/xml': {
136+ "attributes": {
137+ "user": ["id", "name", "access", "secret", "admin"]}}}
138+
139+ def __init__(self):
140+ self.manager = manager.AuthManager()
141+
142+ def _check_admin(self, context):
143+ """We cannot depend on the db layer to check for admin access
144+ for the auth manager, so we do it here"""
145+ if not context.is_admin:
146+ raise exception.NotAuthorized(_("Not admin user"))
147+
148+ def index(self, req):
149+ """Return all users in brief"""
150+ users = self.manager.get_users()
151+ users = common.limited(users, req)
152+ users = [_translate_keys(user) for user in users]
153+ return dict(users=users)
154+
155+ def detail(self, req):
156+ """Return all users in detail"""
157+ return self.index(req)
158+
159+ def show(self, req, id):
160+ """Return data about the given user id"""
161+ user = self.manager.get_user(id)
162+ return dict(user=_translate_keys(user))
163+
164+ def delete(self, req, id):
165+ self._check_admin(req.environ['nova.context'])
166+ self.manager.delete_user(id)
167+ return {}
168+
169+ def create(self, req):
170+ self._check_admin(req.environ['nova.context'])
171+ env = self._deserialize(req.body, req.get_content_type())
172+ is_admin = env['user'].get('admin') in ('T', 'True', True)
173+ name = env['user'].get('name')
174+ access = env['user'].get('access')
175+ secret = env['user'].get('secret')
176+ user = self.manager.create_user(name, access, secret, is_admin)
177+ return dict(user=_translate_keys(user))
178+
179+ def update(self, req, id):
180+ self._check_admin(req.environ['nova.context'])
181+ env = self._deserialize(req.body, req.get_content_type())
182+ is_admin = env['user'].get('admin')
183+ if is_admin is not None:
184+ is_admin = is_admin in ('T', 'True', True)
185+ access = env['user'].get('access')
186+ secret = env['user'].get('secret')
187+ self.manager.modify_user(id, access, secret, is_admin)
188+ return dict(user=_translate_keys(self.manager.get_user(id)))
189
190=== renamed file 'nova/api/openstack/users.py' => 'nova/api/openstack/users.py.moved'
191=== added file 'nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py'
192--- nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py 1970-01-01 00:00:00 +0000
193+++ nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py 2011-03-15 05:15:51 +0000
194@@ -0,0 +1,51 @@
195+# vim: tabstop=4 shiftwidth=4 softtabstop=4
196+
197+# Copyright 2010 OpenStack LLC.
198+#
199+# Licensed under the Apache License, Version 2.0 (the "License"); you may
200+# not use this file except in compliance with the License. You may obtain
201+# a copy of the License at
202+#
203+# http://www.apache.org/licenses/LICENSE-2.0
204+#
205+# Unless required by applicable law or agreed to in writing, software
206+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
207+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
208+# License for the specific language governing permissions and limitations
209+# under the License.
210+
211+from sqlalchemy import *
212+from sqlalchemy.sql import text
213+from migrate import *
214+
215+from nova import log as logging
216+
217+
218+meta = MetaData()
219+
220+instances = Table('instances', meta,
221+ Column('id', Integer(), primary_key=True, nullable=False),
222+ )
223+
224+instances_os_type = Column('os_type',
225+ String(length=255, convert_unicode=False,
226+ assert_unicode=None, unicode_error=None,
227+ _warn_on_bytestring=False),
228+ nullable=True)
229+
230+
231+def upgrade(migrate_engine):
232+ # Upgrade operations go here. Don't create your own engine;
233+ # bind migrate_engine to your metadata
234+ meta.bind = migrate_engine
235+
236+ instances.create_column(instances_os_type)
237+ migrate_engine.execute(instances.update()\
238+ .where(instances.c.os_type == None)\
239+ .values(os_type='linux'))
240+
241+
242+def downgrade(migrate_engine):
243+ meta.bind = migrate_engine
244+
245+ instances.drop_column('os_type')
246
247=== renamed file 'nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py' => 'nova/db/sqlalchemy/migrate_repo/versions/010_add_os_type_to_instances.py.moved'
248=== added file 'nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py'
249--- nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py 1970-01-01 00:00:00 +0000
250+++ nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py 2011-03-15 05:15:51 +0000
251@@ -0,0 +1,83 @@
252+# vim: tabstop=4 shiftwidth=4 softtabstop=4
253+
254+# Copyright 2010 United States Government as represented by the
255+# Administrator of the National Aeronautics and Space Administration.
256+# All Rights Reserved.
257+#
258+# Licensed under the Apache License, Version 2.0 (the "License"); you may
259+# not use this file except in compliance with the License. You may obtain
260+# a copy of the License at
261+#
262+# http://www.apache.org/licenses/LICENSE-2.0
263+#
264+# Unless required by applicable law or agreed to in writing, software
265+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
266+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
267+# License for the specific language governing permissions and limitations
268+# under the License.
269+
270+from migrate import *
271+from nova import log as logging
272+from sqlalchemy import *
273+
274+
275+meta = MetaData()
276+
277+instances = Table('instances', meta,
278+ Column('id', Integer(), primary_key=True, nullable=False),
279+ )
280+
281+#
282+# New Tables
283+#
284+
285+compute_nodes = Table('compute_nodes', meta,
286+ Column('created_at', DateTime(timezone=False)),
287+ Column('updated_at', DateTime(timezone=False)),
288+ Column('deleted_at', DateTime(timezone=False)),
289+ Column('deleted', Boolean(create_constraint=True, name=None)),
290+ Column('id', Integer(), primary_key=True, nullable=False),
291+ Column('service_id', Integer(), nullable=False),
292+
293+ Column('vcpus', Integer(), nullable=False),
294+ Column('memory_mb', Integer(), nullable=False),
295+ Column('local_gb', Integer(), nullable=False),
296+ Column('vcpus_used', Integer(), nullable=False),
297+ Column('memory_mb_used', Integer(), nullable=False),
298+ Column('local_gb_used', Integer(), nullable=False),
299+ Column('hypervisor_type',
300+ Text(convert_unicode=False, assert_unicode=None,
301+ unicode_error=None, _warn_on_bytestring=False),
302+ nullable=False),
303+ Column('hypervisor_version', Integer(), nullable=False),
304+ Column('cpu_info',
305+ Text(convert_unicode=False, assert_unicode=None,
306+ unicode_error=None, _warn_on_bytestring=False),
307+ nullable=False),
308+ )
309+
310+
311+#
312+# Tables to alter
313+#
314+instances_launched_on = Column(
315+ 'launched_on',
316+ Text(convert_unicode=False, assert_unicode=None,
317+ unicode_error=None, _warn_on_bytestring=False),
318+ nullable=True)
319+
320+
321+def upgrade(migrate_engine):
322+ # Upgrade operations go here. Don't create your own engine;
323+ # bind migrate_engine to your metadata
324+ meta.bind = migrate_engine
325+
326+ try:
327+ compute_nodes.create()
328+ except Exception:
329+ logging.info(repr(compute_nodes))
330+ logging.exception('Exception while creating table')
331+ meta.drop_all(tables=[compute_nodes])
332+ raise
333+
334+ instances.create_column(instances_launched_on)
335
336=== renamed file 'nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py' => 'nova/db/sqlalchemy/migrate_repo/versions/011_live_migration.py.moved'
337=== added file 'nova/tests/api/openstack/test_accounts.py'
338--- nova/tests/api/openstack/test_accounts.py 1970-01-01 00:00:00 +0000
339+++ nova/tests/api/openstack/test_accounts.py 2011-03-15 05:15:51 +0000
340@@ -0,0 +1,125 @@
341+# Copyright 2010 OpenStack LLC.
342+# All Rights Reserved.
343+#
344+# Licensed under the Apache License, Version 2.0 (the "License"); you may
345+# not use this file except in compliance with the License. You may obtain
346+# a copy of the License at
347+#
348+# http://www.apache.org/licenses/LICENSE-2.0
349+#
350+# Unless required by applicable law or agreed to in writing, software
351+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
352+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
353+# License for the specific language governing permissions and limitations
354+# under the License.
355+
356+
357+import json
358+
359+import stubout
360+import webob
361+
362+import nova.api
363+import nova.api.openstack.auth
364+from nova import context
365+from nova import flags
366+from nova import test
367+from nova.auth.manager import User
368+from nova.tests.api.openstack import fakes
369+
370+
371+FLAGS = flags.FLAGS
372+FLAGS.verbose = True
373+
374+
375+def fake_init(self):
376+ self.manager = fakes.FakeAuthManager()
377+
378+
379+def fake_admin_check(self, req):
380+ return True
381+
382+
383+class AccountsTest(test.TestCase):
384+ def setUp(self):
385+ super(AccountsTest, self).setUp()
386+ self.stubs = stubout.StubOutForTesting()
387+ self.stubs.Set(nova.api.openstack.accounts.Controller, '__init__',
388+ fake_init)
389+ self.stubs.Set(nova.api.openstack.accounts.Controller, '_check_admin',
390+ fake_admin_check)
391+ fakes.FakeAuthManager.clear_fakes()
392+ fakes.FakeAuthDatabase.data = {}
393+ fakes.stub_out_networking(self.stubs)
394+ fakes.stub_out_rate_limiting(self.stubs)
395+ fakes.stub_out_auth(self.stubs)
396+
397+ self.allow_admin = FLAGS.allow_admin_api
398+ FLAGS.allow_admin_api = True
399+ fakemgr = fakes.FakeAuthManager()
400+ joeuser = User('guy1', 'guy1', 'acc1', 'fortytwo!', False)
401+ superuser = User('guy2', 'guy2', 'acc2', 'swordfish', True)
402+ fakemgr.add_user(joeuser.access, joeuser)
403+ fakemgr.add_user(superuser.access, superuser)
404+ fakemgr.create_project('test1', joeuser)
405+ fakemgr.create_project('test2', superuser)
406+
407+ def tearDown(self):
408+ self.stubs.UnsetAll()
409+ FLAGS.allow_admin_api = self.allow_admin
410+ super(AccountsTest, self).tearDown()
411+
412+ def test_get_account(self):
413+ req = webob.Request.blank('/v1.0/accounts/test1')
414+ res = req.get_response(fakes.wsgi_app())
415+ res_dict = json.loads(res.body)
416+
417+ self.assertEqual(res_dict['account']['id'], 'test1')
418+ self.assertEqual(res_dict['account']['name'], 'test1')
419+ self.assertEqual(res_dict['account']['manager'], 'guy1')
420+ self.assertEqual(res.status_int, 200)
421+
422+ def test_account_delete(self):
423+ req = webob.Request.blank('/v1.0/accounts/test1')
424+ req.method = 'DELETE'
425+ res = req.get_response(fakes.wsgi_app())
426+ self.assertTrue('test1' not in fakes.FakeAuthManager.projects)
427+ self.assertEqual(res.status_int, 200)
428+
429+ def test_account_create(self):
430+ body = dict(account=dict(description='test account',
431+ manager='guy1'))
432+ req = webob.Request.blank('/v1.0/accounts/newacct')
433+ req.headers["Content-Type"] = "application/json"
434+ req.method = 'PUT'
435+ req.body = json.dumps(body)
436+
437+ res = req.get_response(fakes.wsgi_app())
438+ res_dict = json.loads(res.body)
439+
440+ self.assertEqual(res.status_int, 200)
441+ self.assertEqual(res_dict['account']['id'], 'newacct')
442+ self.assertEqual(res_dict['account']['name'], 'newacct')
443+ self.assertEqual(res_dict['account']['description'], 'test account')
444+ self.assertEqual(res_dict['account']['manager'], 'guy1')
445+ self.assertTrue('newacct' in
446+ fakes.FakeAuthManager.projects)
447+ self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3)
448+
449+ def test_account_update(self):
450+ body = dict(account=dict(description='test account',
451+ manager='guy2'))
452+ req = webob.Request.blank('/v1.0/accounts/test1')
453+ req.headers["Content-Type"] = "application/json"
454+ req.method = 'PUT'
455+ req.body = json.dumps(body)
456+
457+ res = req.get_response(fakes.wsgi_app())
458+ res_dict = json.loads(res.body)
459+
460+ self.assertEqual(res.status_int, 200)
461+ self.assertEqual(res_dict['account']['id'], 'test1')
462+ self.assertEqual(res_dict['account']['name'], 'test1')
463+ self.assertEqual(res_dict['account']['description'], 'test account')
464+ self.assertEqual(res_dict['account']['manager'], 'guy2')
465+ self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2)
466
467=== renamed file 'nova/tests/api/openstack/test_accounts.py' => 'nova/tests/api/openstack/test_accounts.py.moved'
468=== added file 'nova/tests/api/openstack/test_users.py'
469--- nova/tests/api/openstack/test_users.py 1970-01-01 00:00:00 +0000
470+++ nova/tests/api/openstack/test_users.py 2011-03-15 05:15:51 +0000
471@@ -0,0 +1,141 @@
472+# Copyright 2010 OpenStack LLC.
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+import json
488+
489+import stubout
490+import webob
491+
492+import nova.api
493+import nova.api.openstack.auth
494+from nova import context
495+from nova import flags
496+from nova import test
497+from nova.auth.manager import User, Project
498+from nova.tests.api.openstack import fakes
499+
500+
501+FLAGS = flags.FLAGS
502+FLAGS.verbose = True
503+
504+
505+def fake_init(self):
506+ self.manager = fakes.FakeAuthManager()
507+
508+
509+def fake_admin_check(self, req):
510+ return True
511+
512+
513+class UsersTest(test.TestCase):
514+ def setUp(self):
515+ super(UsersTest, self).setUp()
516+ self.stubs = stubout.StubOutForTesting()
517+ self.stubs.Set(nova.api.openstack.users.Controller, '__init__',
518+ fake_init)
519+ self.stubs.Set(nova.api.openstack.users.Controller, '_check_admin',
520+ fake_admin_check)
521+ fakes.FakeAuthManager.auth_data = {}
522+ fakes.FakeAuthManager.projects = dict(testacct=Project('testacct',
523+ 'testacct',
524+ 'guy1',
525+ 'test',
526+ []))
527+ fakes.FakeAuthDatabase.data = {}
528+ fakes.stub_out_networking(self.stubs)
529+ fakes.stub_out_rate_limiting(self.stubs)
530+ fakes.stub_out_auth(self.stubs)
531+
532+ self.allow_admin = FLAGS.allow_admin_api
533+ FLAGS.allow_admin_api = True
534+ fakemgr = fakes.FakeAuthManager()
535+ fakemgr.add_user('acc1', User('guy1', 'guy1', 'acc1',
536+ 'fortytwo!', False))
537+ fakemgr.add_user('acc2', User('guy2', 'guy2', 'acc2',
538+ 'swordfish', True))
539+
540+ def tearDown(self):
541+ self.stubs.UnsetAll()
542+ FLAGS.allow_admin_api = self.allow_admin
543+ super(UsersTest, self).tearDown()
544+
545+ def test_get_user_list(self):
546+ req = webob.Request.blank('/v1.0/users')
547+ res = req.get_response(fakes.wsgi_app())
548+ res_dict = json.loads(res.body)
549+
550+ self.assertEqual(res.status_int, 200)
551+ self.assertEqual(len(res_dict['users']), 2)
552+
553+ def test_get_user_by_id(self):
554+ req = webob.Request.blank('/v1.0/users/guy2')
555+ res = req.get_response(fakes.wsgi_app())
556+ res_dict = json.loads(res.body)
557+
558+ self.assertEqual(res_dict['user']['id'], 'guy2')
559+ self.assertEqual(res_dict['user']['name'], 'guy2')
560+ self.assertEqual(res_dict['user']['secret'], 'swordfish')
561+ self.assertEqual(res_dict['user']['admin'], True)
562+ self.assertEqual(res.status_int, 200)
563+
564+ def test_user_delete(self):
565+ req = webob.Request.blank('/v1.0/users/guy1')
566+ req.method = 'DELETE'
567+ res = req.get_response(fakes.wsgi_app())
568+ self.assertTrue('guy1' not in [u.id for u in
569+ fakes.FakeAuthManager.auth_data.values()])
570+ self.assertEqual(res.status_int, 200)
571+
572+ def test_user_create(self):
573+ body = dict(user=dict(name='test_guy',
574+ access='acc3',
575+ secret='invasionIsInNormandy',
576+ admin=True))
577+ req = webob.Request.blank('/v1.0/users')
578+ req.headers["Content-Type"] = "application/json"
579+ req.method = 'POST'
580+ req.body = json.dumps(body)
581+
582+ res = req.get_response(fakes.wsgi_app())
583+ res_dict = json.loads(res.body)
584+
585+ self.assertEqual(res.status_int, 200)
586+ self.assertEqual(res_dict['user']['id'], 'test_guy')
587+ self.assertEqual(res_dict['user']['name'], 'test_guy')
588+ self.assertEqual(res_dict['user']['access'], 'acc3')
589+ self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy')
590+ self.assertEqual(res_dict['user']['admin'], True)
591+ self.assertTrue('test_guy' in [u.id for u in
592+ fakes.FakeAuthManager.auth_data.values()])
593+ self.assertEqual(len(fakes.FakeAuthManager.auth_data.values()), 3)
594+
595+ def test_user_update(self):
596+ body = dict(user=dict(name='guy2',
597+ access='acc2',
598+ secret='invasionIsInNormandy'))
599+ req = webob.Request.blank('/v1.0/users/guy2')
600+ req.headers["Content-Type"] = "application/json"
601+ req.method = 'PUT'
602+ req.body = json.dumps(body)
603+
604+ res = req.get_response(fakes.wsgi_app())
605+ res_dict = json.loads(res.body)
606+
607+ self.assertEqual(res.status_int, 200)
608+ self.assertEqual(res_dict['user']['id'], 'guy2')
609+ self.assertEqual(res_dict['user']['name'], 'guy2')
610+ self.assertEqual(res_dict['user']['access'], 'acc2')
611+ self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy')
612+ self.assertEqual(res_dict['user']['admin'], True)
613
614=== renamed file 'nova/tests/api/openstack/test_users.py' => 'nova/tests/api/openstack/test_users.py.moved'
615=== added directory 'nova/tests/integrated'
616=== renamed directory 'nova/tests/integrated' => 'nova/tests/integrated.moved'
617=== added file 'nova/tests/integrated/__init__.py'
618--- nova/tests/integrated/__init__.py 1970-01-01 00:00:00 +0000
619+++ nova/tests/integrated/__init__.py 2011-03-15 05:15:51 +0000
620@@ -0,0 +1,20 @@
621+# vim: tabstop=4 shiftwidth=4 softtabstop=4
622+
623+# Copyright (c) 2011 Justin Santa Barbara
624+#
625+# Licensed under the Apache License, Version 2.0 (the "License"); you may
626+# not use this file except in compliance with the License. You may obtain
627+# a copy of the License at
628+#
629+# http://www.apache.org/licenses/LICENSE-2.0
630+#
631+# Unless required by applicable law or agreed to in writing, software
632+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
633+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
634+# License for the specific language governing permissions and limitations
635+# under the License.
636+
637+"""
638+:mod:`integrated` -- Tests whole systems, using mock services where needed
639+=================================
640+"""
641
642=== added directory 'nova/tests/integrated/api'
643=== added file 'nova/tests/integrated/api/__init__.py'
644--- nova/tests/integrated/api/__init__.py 1970-01-01 00:00:00 +0000
645+++ nova/tests/integrated/api/__init__.py 2011-03-15 05:15:51 +0000
646@@ -0,0 +1,20 @@
647+# vim: tabstop=4 shiftwidth=4 softtabstop=4
648+
649+# Copyright (c) 2011 Justin Santa Barbara
650+#
651+# Licensed under the Apache License, Version 2.0 (the "License"); you may
652+# not use this file except in compliance with the License. You may obtain
653+# a copy of the License at
654+#
655+# http://www.apache.org/licenses/LICENSE-2.0
656+#
657+# Unless required by applicable law or agreed to in writing, software
658+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
659+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
660+# License for the specific language governing permissions and limitations
661+# under the License.
662+
663+"""
664+:mod:`api` -- OpenStack API client, for testing rather than production
665+=================================
666+"""
667
668=== added file 'nova/tests/integrated/api/client.py'
669--- nova/tests/integrated/api/client.py 1970-01-01 00:00:00 +0000
670+++ nova/tests/integrated/api/client.py 2011-03-15 05:15:51 +0000
671@@ -0,0 +1,213 @@
672+# vim: tabstop=4 shiftwidth=4 softtabstop=4
673+
674+# Copyright (c) 2011 Justin Santa Barbara
675+#
676+# Licensed under the Apache License, Version 2.0 (the "License"); you may
677+# not use this file except in compliance with the License. You may obtain
678+# a copy of the License at
679+#
680+# http://www.apache.org/licenses/LICENSE-2.0
681+#
682+# Unless required by applicable law or agreed to in writing, software
683+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
684+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
685+# License for the specific language governing permissions and limitations
686+# under the License.
687+
688+import json
689+import httplib
690+import urlparse
691+
692+from nova import log as logging
693+
694+
695+LOG = logging.getLogger('nova.tests.api')
696+
697+
698+class OpenStackApiException(Exception):
699+ def __init__(self, message=None, response=None):
700+ self.response = response
701+ if not message:
702+ message = 'Unspecified error'
703+
704+ if response:
705+ _status = response.status
706+ _body = response.read()
707+
708+ message = _('%(message)s\nStatus Code: %(_status)s\n'
709+ 'Body: %(_body)s') % locals()
710+
711+ super(OpenStackApiException, self).__init__(message)
712+
713+
714+class OpenStackApiAuthenticationException(OpenStackApiException):
715+ def __init__(self, response=None, message=None):
716+ if not message:
717+ message = _("Authentication error")
718+ super(OpenStackApiAuthenticationException, self).__init__(message,
719+ response)
720+
721+
722+class OpenStackApiNotFoundException(OpenStackApiException):
723+ def __init__(self, response=None, message=None):
724+ if not message:
725+ message = _("Item not found")
726+ super(OpenStackApiNotFoundException, self).__init__(message, response)
727+
728+
729+class TestOpenStackClient(object):
730+ """ A really basic OpenStack API client that is under our control,
731+ so we can make changes / insert hooks for testing"""
732+
733+ def __init__(self, auth_user, auth_key, auth_uri):
734+ super(TestOpenStackClient, self).__init__()
735+ self.auth_result = None
736+ self.auth_user = auth_user
737+ self.auth_key = auth_key
738+ self.auth_uri = auth_uri
739+
740+ def request(self, url, method='GET', body=None, headers=None):
741+ if headers is None:
742+ headers = {}
743+
744+ parsed_url = urlparse.urlparse(url)
745+ port = parsed_url.port
746+ hostname = parsed_url.hostname
747+ scheme = parsed_url.scheme
748+
749+ if scheme == 'http':
750+ conn = httplib.HTTPConnection(hostname,
751+ port=port)
752+ elif scheme == 'https':
753+ conn = httplib.HTTPSConnection(hostname,
754+ port=port)
755+ else:
756+ raise OpenStackApiException("Unknown scheme: %s" % url)
757+
758+ relative_url = parsed_url.path
759+ if parsed_url.query:
760+ relative_url = relative_url + parsed_url.query
761+ LOG.info(_("Doing %(method)s on %(relative_url)s") % locals())
762+ if body:
763+ LOG.info(_("Body: %s") % body)
764+
765+ conn.request(method, relative_url, body, headers)
766+ response = conn.getresponse()
767+ return response
768+
769+ def _authenticate(self):
770+ if self.auth_result:
771+ return self.auth_result
772+
773+ auth_uri = self.auth_uri
774+ headers = {'X-Auth-User': self.auth_user,
775+ 'X-Auth-Key': self.auth_key}
776+ response = self.request(auth_uri,
777+ headers=headers)
778+
779+ http_status = response.status
780+ LOG.debug(_("%(auth_uri)s => code %(http_status)s") % locals())
781+
782+ # Until bug732866 is fixed, we can't check this properly...
783+ # bug732866
784+ #if http_status == 401:
785+ if http_status != 204:
786+ raise OpenStackApiAuthenticationException(response=response)
787+
788+ auth_headers = {}
789+ for k, v in response.getheaders():
790+ auth_headers[k] = v
791+
792+ self.auth_result = auth_headers
793+ return self.auth_result
794+
795+ def api_request(self, relative_uri, check_response_status=None, **kwargs):
796+ auth_result = self._authenticate()
797+
798+ #NOTE(justinsb): httplib 'helpfully' converts headers to lower case
799+ base_uri = auth_result['x-server-management-url']
800+ full_uri = base_uri + relative_uri
801+
802+ headers = kwargs.setdefault('headers', {})
803+ headers['X-Auth-Token'] = auth_result['x-auth-token']
804+
805+ response = self.request(full_uri, **kwargs)
806+
807+ http_status = response.status
808+ LOG.debug(_("%(relative_uri)s => code %(http_status)s") % locals())
809+
810+ if check_response_status:
811+ if not http_status in check_response_status:
812+ if http_status == 404:
813+ raise OpenStackApiNotFoundException(response=response)
814+ else:
815+ raise OpenStackApiException(
816+ message=_("Unexpected status code"),
817+ response=response)
818+
819+ return response
820+
821+ def _decode_json(self, response):
822+ body = response.read()
823+ LOG.debug(_("Decoding JSON: %s") % (body))
824+ return json.loads(body)
825+
826+ def api_get(self, relative_uri, **kwargs):
827+ kwargs.setdefault('check_response_status', [200])
828+ response = self.api_request(relative_uri, **kwargs)
829+ return self._decode_json(response)
830+
831+ def api_post(self, relative_uri, body, **kwargs):
832+ kwargs['method'] = 'POST'
833+ if body:
834+ headers = kwargs.setdefault('headers', {})
835+ headers['Content-Type'] = 'application/json'
836+ kwargs['body'] = json.dumps(body)
837+
838+ kwargs.setdefault('check_response_status', [200])
839+ response = self.api_request(relative_uri, **kwargs)
840+ return self._decode_json(response)
841+
842+ def api_delete(self, relative_uri, **kwargs):
843+ kwargs['method'] = 'DELETE'
844+ kwargs.setdefault('check_response_status', [200, 202])
845+ return self.api_request(relative_uri, **kwargs)
846+
847+ def get_server(self, server_id):
848+ return self.api_get('/servers/%s' % server_id)['server']
849+
850+ def get_servers(self, detail=True):
851+ rel_url = '/servers/detail' if detail else '/servers'
852+ return self.api_get(rel_url)['servers']
853+
854+ def post_server(self, server):
855+ return self.api_post('/servers', server)['server']
856+
857+ def delete_server(self, server_id):
858+ return self.api_delete('/servers/%s' % server_id)
859+
860+ def get_image(self, image_id):
861+ return self.api_get('/images/%s' % image_id)['image']
862+
863+ def get_images(self, detail=True):
864+ rel_url = '/images/detail' if detail else '/images'
865+ return self.api_get(rel_url)['images']
866+
867+ def post_image(self, image):
868+ return self.api_post('/images', image)['image']
869+
870+ def delete_image(self, image_id):
871+ return self.api_delete('/images/%s' % image_id)
872+
873+ def get_flavor(self, flavor_id):
874+ return self.api_get('/flavors/%s' % flavor_id)['flavor']
875+
876+ def get_flavors(self, detail=True):
877+ rel_url = '/flavors/detail' if detail else '/flavors'
878+ return self.api_get(rel_url)['flavors']
879+
880+ def post_flavor(self, flavor):
881+ return self.api_post('/flavors', flavor)['flavor']
882+
883+ def delete_flavor(self, flavor_id):
884+ return self.api_delete('/flavors/%s' % flavor_id)
885
886=== added file 'nova/tests/integrated/integrated_helpers.py'
887--- nova/tests/integrated/integrated_helpers.py 1970-01-01 00:00:00 +0000
888+++ nova/tests/integrated/integrated_helpers.py 2011-03-15 05:15:51 +0000
889@@ -0,0 +1,188 @@
890+# vim: tabstop=4 shiftwidth=4 softtabstop=4
891+
892+# Copyright 2011 Justin Santa Barbara
893+# All Rights Reserved.
894+#
895+# Licensed under the Apache License, Version 2.0 (the "License"); you may
896+# not use this file except in compliance with the License. You may obtain
897+# a copy of the License at
898+#
899+# http://www.apache.org/licenses/LICENSE-2.0
900+#
901+# Unless required by applicable law or agreed to in writing, software
902+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
903+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
904+# License for the specific language governing permissions and limitations
905+# under the License.
906+
907+"""
908+Provides common functionality for integrated unit tests
909+"""
910+
911+import random
912+import string
913+
914+from nova import exception
915+from nova import flags
916+from nova import service
917+from nova import test # For the flags
918+from nova.auth import manager
919+from nova.exception import Error
920+from nova.log import logging
921+from nova.tests.integrated.api import client
922+
923+
924+FLAGS = flags.FLAGS
925+
926+LOG = logging.getLogger('nova.tests.integrated')
927+
928+
929+def generate_random_alphanumeric(length):
930+ """Creates a random alphanumeric string of specified length"""
931+ return ''.join(random.choice(string.ascii_uppercase + string.digits)
932+ for _x in range(length))
933+
934+
935+def generate_random_numeric(length):
936+ """Creates a random numeric string of specified length"""
937+ return ''.join(random.choice(string.digits)
938+ for _x in range(length))
939+
940+
941+def generate_new_element(items, prefix, numeric=False):
942+ """Creates a random string with prefix, that is not in 'items' list"""
943+ while True:
944+ if numeric:
945+ candidate = prefix + generate_random_numeric(8)
946+ else:
947+ candidate = prefix + generate_random_alphanumeric(8)
948+ if not candidate in items:
949+ return candidate
950+ print "Random collision on %s" % candidate
951+
952+
953+class TestUser(object):
954+ def __init__(self, name, secret, auth_url):
955+ self.name = name
956+ self.secret = secret
957+ self.auth_url = auth_url
958+
959+ if not auth_url:
960+ raise exception.Error("auth_url is required")
961+ self.openstack_api = client.TestOpenStackClient(self.name,
962+ self.secret,
963+ self.auth_url)
964+
965+
966+class IntegratedUnitTestContext(object):
967+ __INSTANCE = None
968+
969+ def __init__(self):
970+ self.auth_manager = manager.AuthManager()
971+
972+ self.wsgi_server = None
973+ self.wsgi_apps = []
974+ self.api_service = None
975+
976+ self.services = []
977+ self.auth_url = None
978+ self.project_name = None
979+
980+ self.setup()
981+
982+ def setup(self):
983+ self._start_services()
984+
985+ self._create_test_user()
986+
987+ def _create_test_user(self):
988+ self.test_user = self._create_unittest_user()
989+
990+ # No way to currently pass this through the OpenStack API
991+ self.project_name = 'openstack'
992+ self._configure_project(self.project_name, self.test_user)
993+
994+ def _start_services(self):
995+ # WSGI shutdown broken :-(
996+ # bug731668
997+ if not self.api_service:
998+ self._start_api_service()
999+
1000+ def cleanup(self):
1001+ for service in self.services:
1002+ service.kill()
1003+ self.services = []
1004+ # TODO(justinsb): Shutdown WSGI & anything else we startup
1005+ # bug731668
1006+ # WSGI shutdown broken :-(
1007+ # self.wsgi_server.terminate()
1008+ # self.wsgi_server = None
1009+ self.test_user = None
1010+
1011+ def _create_unittest_user(self):
1012+ users = self.auth_manager.get_users()
1013+ user_names = [user.name for user in users]
1014+ auth_name = generate_new_element(user_names, 'unittest_user_')
1015+ auth_key = generate_random_alphanumeric(16)
1016+
1017+ # Right now there's a bug where auth_name and auth_key are reversed
1018+ # bug732907
1019+ auth_key = auth_name
1020+
1021+ self.auth_manager.create_user(auth_name, auth_name, auth_key, False)
1022+ return TestUser(auth_name, auth_key, self.auth_url)
1023+
1024+ def _configure_project(self, project_name, user):
1025+ projects = self.auth_manager.get_projects()
1026+ project_names = [project.name for project in projects]
1027+ if not project_name in project_names:
1028+ project = self.auth_manager.create_project(project_name,
1029+ user.name,
1030+ description=None,
1031+ member_users=None)
1032+ else:
1033+ self.auth_manager.add_to_project(user.name, project_name)
1034+
1035+ def _start_api_service(self):
1036+ api_service = service.ApiService.create()
1037+ api_service.start()
1038+
1039+ if not api_service:
1040+ raise Exception("API Service was None")
1041+
1042+ # WSGI shutdown broken :-(
1043+ #self.services.append(volume_service)
1044+ self.api_service = api_service
1045+
1046+ self.auth_url = 'http://localhost:8774/v1.0'
1047+
1048+ return api_service
1049+
1050+ # WSGI shutdown broken :-(
1051+ # bug731668
1052+ #@staticmethod
1053+ #def get():
1054+ # if not IntegratedUnitTestContext.__INSTANCE:
1055+ # IntegratedUnitTestContext.startup()
1056+ # #raise Error("Must call IntegratedUnitTestContext::startup")
1057+ # return IntegratedUnitTestContext.__INSTANCE
1058+
1059+ @staticmethod
1060+ def startup():
1061+ # Because WSGI shutdown is broken at the moment, we have to recycle
1062+ # bug731668
1063+ if IntegratedUnitTestContext.__INSTANCE:
1064+ #raise Error("Multiple calls to IntegratedUnitTestContext.startup")
1065+ IntegratedUnitTestContext.__INSTANCE.setup()
1066+ else:
1067+ IntegratedUnitTestContext.__INSTANCE = IntegratedUnitTestContext()
1068+ return IntegratedUnitTestContext.__INSTANCE
1069+
1070+ @staticmethod
1071+ def shutdown():
1072+ if not IntegratedUnitTestContext.__INSTANCE:
1073+ raise Error("Must call IntegratedUnitTestContext::startup")
1074+ IntegratedUnitTestContext.__INSTANCE.cleanup()
1075+ # WSGI shutdown broken :-(
1076+ # bug731668
1077+ #IntegratedUnitTestContext.__INSTANCE = None
1078
1079=== added file 'nova/tests/integrated/test_login.py'
1080--- nova/tests/integrated/test_login.py 1970-01-01 00:00:00 +0000
1081+++ nova/tests/integrated/test_login.py 2011-03-15 05:15:51 +0000
1082@@ -0,0 +1,79 @@
1083+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1084+
1085+# Copyright 2011 Justin Santa Barbara
1086+# All Rights Reserved.
1087+#
1088+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1089+# not use this file except in compliance with the License. You may obtain
1090+# a copy of the License at
1091+#
1092+# http://www.apache.org/licenses/LICENSE-2.0
1093+#
1094+# Unless required by applicable law or agreed to in writing, software
1095+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1096+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1097+# License for the specific language governing permissions and limitations
1098+# under the License.
1099+
1100+import unittest
1101+
1102+from nova import flags
1103+from nova import test
1104+from nova.log import logging
1105+from nova.tests.integrated import integrated_helpers
1106+from nova.tests.integrated.api import client
1107+
1108+
1109+LOG = logging.getLogger('nova.tests.integrated')
1110+
1111+FLAGS = flags.FLAGS
1112+FLAGS.verbose = True
1113+
1114+
1115+class LoginTest(test.TestCase):
1116+ def setUp(self):
1117+ super(LoginTest, self).setUp()
1118+ context = integrated_helpers.IntegratedUnitTestContext.startup()
1119+ self.user = context.test_user
1120+ self.api = self.user.openstack_api
1121+
1122+ def tearDown(self):
1123+ integrated_helpers.IntegratedUnitTestContext.shutdown()
1124+ super(LoginTest, self).tearDown()
1125+
1126+ def test_login(self):
1127+ """Simple check - we list flavors - so we know we're logged in"""
1128+ flavors = self.api.get_flavors()
1129+ for flavor in flavors:
1130+ LOG.debug(_("flavor: %s") % flavor)
1131+
1132+ def test_bad_login_password(self):
1133+ """Test that I get a 401 with a bad username"""
1134+ bad_credentials_api = client.TestOpenStackClient(self.user.name,
1135+ "notso_password",
1136+ self.user.auth_url)
1137+
1138+ self.assertRaises(client.OpenstackApiAuthenticationException,
1139+ bad_credentials_api.get_flavors)
1140+
1141+ def test_bad_login_username(self):
1142+ """Test that I get a 401 with a bad password"""
1143+ bad_credentials_api = client.TestOpenStackClient("notso_username",
1144+ self.user.secret,
1145+ self.user.auth_url)
1146+
1147+ self.assertRaises(client.OpenstackApiAuthenticationException,
1148+ bad_credentials_api.get_flavors)
1149+
1150+ def test_bad_login_both_bad(self):
1151+ """Test that I get a 401 with both bad username and bad password"""
1152+ bad_credentials_api = client.TestOpenStackClient("notso_username",
1153+ "notso_password",
1154+ self.user.auth_url)
1155+
1156+ self.assertRaises(client.OpenstackApiAuthenticationException,
1157+ bad_credentials_api.get_flavors)
1158+
1159+
1160+if __name__ == "__main__":
1161+ unittest.main()
1162
1163=== added file 'nova/virt/cpuinfo.xml.template'
1164--- nova/virt/cpuinfo.xml.template 1970-01-01 00:00:00 +0000
1165+++ nova/virt/cpuinfo.xml.template 2011-03-15 05:15:51 +0000
1166@@ -0,0 +1,9 @@
1167+<cpu>
1168+ <arch>$arch</arch>
1169+ <model>$model</model>
1170+ <vendor>$vendor</vendor>
1171+ <topology sockets="$topology.sockets" cores="$topology.cores" threads="$topology.threads"/>
1172+#for $var in $features
1173+ <features name="$var" />
1174+#end for
1175+</cpu>
1176
1177=== renamed file 'nova/virt/cpuinfo.xml.template' => 'nova/virt/cpuinfo.xml.template.moved'