Merge lp:~allenap/maas/shared-to-per-tenant-storage-1.2 into lp:maas/1.2
- shared-to-per-tenant-storage-1.2
- Merge into 1.2
Proposed by
Gavin Panella
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Raphaël Badin | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1367 | ||||
Proposed branch: | lp:~allenap/maas/shared-to-per-tenant-storage-1.2 | ||||
Merge into: | lp:maas/1.2 | ||||
Diff against target: |
632 lines (+570/-6) 9 files modified
src/maasserver/models/user.py (+1/-0) src/maasserver/support/pertenant/migration.py (+179/-0) src/maasserver/support/pertenant/tests/test_migration.py (+384/-0) src/maasserver/tests/data/test_rsa0.pub (+1/-1) src/maasserver/tests/data/test_rsa1.pub (+1/-1) src/maasserver/tests/data/test_rsa2.pub (+1/-1) src/maasserver/tests/data/test_rsa3.pub (+1/-1) src/maasserver/tests/data/test_rsa4.pub (+1/-1) src/maasserver/tests/test_sshkey.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~allenap/maas/shared-to-per-tenant-storage-1.2 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+152020@code.launchpad.net |
Commit message
Backport of r1449 from trunk: Add the mechanism for migrating shared-namespace file storage usage over to the per-tenant model.
There is a carefully constructed 4-step process that's intended to minimise disruption. Ultimately some eggs do need to be broken, but this branch catches their contents and tries damn hard to bake a tasty cake with it.
Description of the change
To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/models/user.py' |
2 | --- src/maasserver/models/user.py 2012-08-13 04:47:10 +0000 |
3 | +++ src/maasserver/models/user.py 2013-03-06 17:48:23 +0000 |
4 | @@ -15,6 +15,7 @@ |
5 | 'create_user', |
6 | 'get_auth_tokens', |
7 | 'get_creds_tuple', |
8 | + 'SYSTEM_USERS', |
9 | ] |
10 | |
11 | from maasserver import worker_user |
12 | |
13 | === added file 'src/maasserver/support/pertenant/migration.py' |
14 | --- src/maasserver/support/pertenant/migration.py 1970-01-01 00:00:00 +0000 |
15 | +++ src/maasserver/support/pertenant/migration.py 2013-03-06 17:48:23 +0000 |
16 | @@ -0,0 +1,179 @@ |
17 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
18 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
19 | + |
20 | +"""Shared namespace --> per-tenant namespace migration. |
21 | + |
22 | +Perform the following steps to migrate: |
23 | + |
24 | +1. When no files exist (i.e. no Juju environments exist): do nothing |
25 | +1a. When no *unowned* files exist: do nothing. |
26 | + |
27 | +2. When there's only one user: assign ownership of all files to user. |
28 | + |
29 | +3. When there are multiple users and a `provider-state` file: parse that file |
30 | + to extract the instance id of the bootstrap node. From that instance id, |
31 | + get the identity of the user who deployed this environment (that's the |
32 | + owner of the bootstrap node). Then proceed as in 4, using that user as the |
33 | + "legacy" user. |
34 | + |
35 | +4. When there are multiple users: create a new "legacy" user, assign ownership |
36 | + of all files and allocated/owned nodes to this user, copy all public SSH |
37 | + keys to this user, and move all API credentials to this user. |
38 | + |
39 | +There's not a lot we can do about SSH keys authorised to connect to the |
40 | +already deployed nodes in #3, but this set will only ever decrease: nodes |
41 | +allocated after this migration will permit access from any of the users with |
42 | +SSH keys prior to the migration. |
43 | +""" |
44 | + |
45 | +from __future__ import ( |
46 | + absolute_import, |
47 | + print_function, |
48 | + unicode_literals, |
49 | + ) |
50 | + |
51 | +__metaclass__ = type |
52 | +__all__ = [ |
53 | + "migrate", |
54 | + ] |
55 | + |
56 | +from django.contrib.auth.models import User |
57 | +from maasserver.models import ( |
58 | + FileStorage, |
59 | + Node, |
60 | + SSHKey, |
61 | + ) |
62 | +from maasserver.models.user import ( |
63 | + get_auth_tokens, |
64 | + SYSTEM_USERS, |
65 | + ) |
66 | +from maasserver.support.pertenant.utils import get_bootstrap_node_owner |
67 | +from maasserver.utils.orm import get_one |
68 | + |
69 | + |
70 | +legacy_user_name = "shared-environment" |
71 | + |
72 | + |
73 | +def get_legacy_user(): |
74 | + """Return the legacy namespace user, creating it if need be.""" |
75 | + try: |
76 | + legacy_user = User.objects.get(username=legacy_user_name) |
77 | + except User.DoesNotExist: |
78 | + # Create the legacy user with a local, probably non-working, email |
79 | + # address, and an unusable password. |
80 | + legacy_user = User.objects.create_user( |
81 | + email="%s@localhost" % legacy_user_name, |
82 | + username=legacy_user_name) |
83 | + legacy_user.first_name = "Shared" |
84 | + legacy_user.last_name = "Environment" |
85 | + legacy_user.is_active = True |
86 | + return legacy_user |
87 | + |
88 | + |
89 | +def get_unowned_files(): |
90 | + """Returns a `QuerySet` of unowned files.""" |
91 | + return FileStorage.objects.filter(owner=None) |
92 | + |
93 | + |
94 | +def get_real_users(): |
95 | + """Returns a `QuerySet` of real. not system, users.""" |
96 | + users = User.objects.exclude(username__in=SYSTEM_USERS) |
97 | + users = users.exclude(username=legacy_user_name) |
98 | + return users |
99 | + |
100 | + |
101 | +def get_owned_nodes(): |
102 | + """Returns a `QuerySet` of nodes owned by real users.""" |
103 | + return Node.objects.filter(owner__in=get_real_users()) |
104 | + |
105 | + |
106 | +def get_owned_nodes_owners(): |
107 | + """Returns a `QuerySet` of the owners of nodes owned by real users.""" |
108 | + owner_ids = get_owned_nodes().values_list("owner", flat=True) |
109 | + return User.objects.filter(id__in=owner_ids.distinct()) |
110 | + |
111 | + |
112 | +def get_destination_user(): |
113 | + """Return the user to which resources should be assigned.""" |
114 | + real_users = get_real_users() |
115 | + if real_users.count() == 1: |
116 | + return get_one(real_users) |
117 | + else: |
118 | + bootstrap_user = get_bootstrap_node_owner() |
119 | + if bootstrap_user is None: |
120 | + return get_legacy_user() |
121 | + else: |
122 | + return bootstrap_user |
123 | + |
124 | + |
125 | +def get_ssh_keys(user): |
126 | + """Return the SSH key strings belonging to the specified user.""" |
127 | + return SSHKey.objects.filter(user=user).values_list("key", flat=True) |
128 | + |
129 | + |
130 | +def copy_ssh_keys(user_from, user_dest): |
131 | + """Copies SSH keys from one user to another. |
132 | + |
133 | + This is idempotent, and does not clobber the destination user's existing |
134 | + keys. |
135 | + """ |
136 | + user_from_keys = get_ssh_keys(user_from) |
137 | + user_dest_keys = get_ssh_keys(user_dest) |
138 | + for key in set(user_from_keys).difference(user_dest_keys): |
139 | + ssh_key = SSHKey(user=user_dest, key=key) |
140 | + ssh_key.save() |
141 | + |
142 | + |
143 | +def give_file_to_user(file, user): |
144 | + """Give a file to a user.""" |
145 | + file.owner = user |
146 | + file.save() |
147 | + |
148 | + |
149 | +def give_api_credentials_to_user(user_from, user_dest): |
150 | + """Gives one user's API credentials to another. |
151 | + |
152 | + This ensures that users of the shared namespace environment continue to |
153 | + operate within the legacy shared namespace environment by default via the |
154 | + API (e.g. maas-cli and Juju). |
155 | + """ |
156 | + for token in get_auth_tokens(user_from): |
157 | + consumer = token.consumer |
158 | + consumer.user = user_dest |
159 | + consumer.save() |
160 | + token.user = user_dest |
161 | + token.save() |
162 | + |
163 | + |
164 | +def give_node_to_user(node, user): |
165 | + """Changes a node's ownership for the legacy shared environment.""" |
166 | + node.owner = user |
167 | + node.save() |
168 | + |
169 | + |
170 | +def migrate_to_user(user): |
171 | + """Migrate files and nodes to the specified user. |
172 | + |
173 | + This also copies, to the destination user, the public SSH keys of any |
174 | + owned nodes' owners. This is so that those users who had allocated nodes |
175 | + (i.e. active users of a shared-namespace environment) can access newly |
176 | + created nodes in the legacy shared-namespace environment. |
177 | + """ |
178 | + for unowned_file in get_unowned_files(): |
179 | + give_file_to_user(unowned_file, user) |
180 | + for node_owner in get_owned_nodes_owners(): |
181 | + copy_ssh_keys(node_owner, user) |
182 | + give_api_credentials_to_user(node_owner, user) |
183 | + for owned_node in get_owned_nodes(): |
184 | + give_node_to_user(owned_node, user) |
185 | + |
186 | + |
187 | +def migrate(): |
188 | + """Migrate files to a per-tenant namespace.""" |
189 | + if get_unowned_files().exists(): |
190 | + # 2, 3, and 4 |
191 | + user = get_destination_user() |
192 | + migrate_to_user(user) |
193 | + else: |
194 | + # 1 and 1a |
195 | + pass |
196 | |
197 | === added file 'src/maasserver/support/pertenant/tests/test_migration.py' |
198 | --- src/maasserver/support/pertenant/tests/test_migration.py 1970-01-01 00:00:00 +0000 |
199 | +++ src/maasserver/support/pertenant/tests/test_migration.py 2013-03-06 17:48:23 +0000 |
200 | @@ -0,0 +1,384 @@ |
201 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
202 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
203 | + |
204 | +"""Test `maasserver.support.pertenant.migration.""" |
205 | + |
206 | +from __future__ import ( |
207 | + absolute_import, |
208 | + print_function, |
209 | + unicode_literals, |
210 | + ) |
211 | + |
212 | +__metaclass__ = type |
213 | +__all__ = [] |
214 | + |
215 | +from django.contrib.auth.models import User |
216 | +from maasserver.models import ( |
217 | + Node, |
218 | + SSHKey, |
219 | + ) |
220 | +from maasserver.support.pertenant import migration |
221 | +from maasserver.support.pertenant.migration import ( |
222 | + copy_ssh_keys, |
223 | + get_destination_user, |
224 | + get_legacy_user, |
225 | + get_owned_nodes, |
226 | + get_owned_nodes_owners, |
227 | + get_real_users, |
228 | + get_ssh_keys, |
229 | + get_unowned_files, |
230 | + give_api_credentials_to_user, |
231 | + give_file_to_user, |
232 | + give_node_to_user, |
233 | + legacy_user_name, |
234 | + migrate, |
235 | + migrate_to_user, |
236 | + ) |
237 | +from maasserver.support.pertenant.tests.test_utils import ( |
238 | + make_provider_state_file, |
239 | + ) |
240 | +from maasserver.testing import ( |
241 | + get_data, |
242 | + reload_object, |
243 | + ) |
244 | +from maasserver.testing.factory import factory |
245 | +from maasserver.testing.testcase import TestCase |
246 | +from mock import ( |
247 | + call, |
248 | + sentinel, |
249 | + ) |
250 | +from testtools.matchers import MatchesStructure |
251 | + |
252 | + |
253 | +def get_ssh_key_string(num=0): |
254 | + return get_data('data/test_rsa%d.pub' % num) |
255 | + |
256 | + |
257 | +class TestFunctions(TestCase): |
258 | + |
259 | + def find_legacy_user(self): |
260 | + return User.objects.filter(username=legacy_user_name) |
261 | + |
262 | + def test_get_legacy_user_creates_user(self): |
263 | + self.assertEqual([], list(self.find_legacy_user())) |
264 | + legacy_user = get_legacy_user() |
265 | + self.assertEqual([legacy_user], list(self.find_legacy_user())) |
266 | + self.assertThat( |
267 | + legacy_user, MatchesStructure.byEquality( |
268 | + first_name="Shared", last_name="Environment", |
269 | + email=legacy_user_name + "@localhost", is_active=True)) |
270 | + |
271 | + def test_get_legacy_user_creates_user_only_once(self): |
272 | + legacy_user1 = get_legacy_user() |
273 | + self.assertEqual([legacy_user1], list(self.find_legacy_user())) |
274 | + legacy_user2 = get_legacy_user() |
275 | + self.assertEqual([legacy_user2], list(self.find_legacy_user())) |
276 | + self.assertEqual(legacy_user1, legacy_user2) |
277 | + |
278 | + def test_get_unowned_files_no_files(self): |
279 | + self.assertEqual([], list(get_unowned_files())) |
280 | + |
281 | + def test_get_unowned_files(self): |
282 | + user = factory.make_user() |
283 | + files = [ |
284 | + factory.make_file_storage(owner=None), |
285 | + factory.make_file_storage(owner=user), |
286 | + factory.make_file_storage(owner=None), |
287 | + ] |
288 | + self.assertSetEqual( |
289 | + {files[0], files[2]}, |
290 | + set(get_unowned_files())) |
291 | + |
292 | + def test_get_real_users_no_users(self): |
293 | + get_legacy_user() # Ensure at least the legacy user exists. |
294 | + self.assertEqual([], list(get_real_users())) |
295 | + |
296 | + def test_get_real_users(self): |
297 | + get_legacy_user() # Ensure at least the legacy user exists. |
298 | + users = [ |
299 | + factory.make_user(), |
300 | + factory.make_user(), |
301 | + ] |
302 | + self.assertSetEqual(set(users), set(get_real_users())) |
303 | + |
304 | + def test_get_owned_nodes_no_nodes(self): |
305 | + self.assertEqual([], list(get_owned_nodes())) |
306 | + |
307 | + def test_get_owned_nodes_no_owned_nodes(self): |
308 | + factory.make_node() |
309 | + self.assertEqual([], list(get_owned_nodes())) |
310 | + |
311 | + def test_get_owned_nodes_with_owned_nodes(self): |
312 | + nodes = { |
313 | + factory.make_node(owner=factory.make_user()), |
314 | + factory.make_node(owner=factory.make_user()), |
315 | + } |
316 | + self.assertSetEqual(nodes, set(get_owned_nodes())) |
317 | + |
318 | + def test_get_owned_nodes_with_nodes_owned_by_system_users(self): |
319 | + factory.make_node(owner=get_legacy_user()), |
320 | + self.assertEqual([], list(get_owned_nodes())) |
321 | + |
322 | + def test_get_owned_nodes_owners_no_users(self): |
323 | + self.assertEqual([], list(get_owned_nodes_owners())) |
324 | + |
325 | + def test_get_owned_nodes_owners_no_nodes(self): |
326 | + factory.make_user() |
327 | + self.assertEqual([], list(get_owned_nodes_owners())) |
328 | + |
329 | + def test_get_owned_nodes_owners_no_owned_nodes(self): |
330 | + factory.make_user() |
331 | + factory.make_node(owner=None) |
332 | + self.assertEqual([], list(get_owned_nodes_owners())) |
333 | + |
334 | + def test_get_owned_nodes_owners(self): |
335 | + user1 = factory.make_user() |
336 | + user2 = factory.make_user() |
337 | + factory.make_user() |
338 | + factory.make_node(owner=user1) |
339 | + factory.make_node(owner=user2) |
340 | + factory.make_node(owner=None) |
341 | + self.assertSetEqual({user1, user2}, set(get_owned_nodes_owners())) |
342 | + |
343 | + def test_get_destination_user_one_real_user(self): |
344 | + user = factory.make_user() |
345 | + self.assertEqual(user, get_destination_user()) |
346 | + |
347 | + def test_get_destination_user_two_real_users(self): |
348 | + factory.make_user() |
349 | + factory.make_user() |
350 | + self.assertEqual(get_legacy_user(), get_destination_user()) |
351 | + |
352 | + def test_get_destination_user_no_real_users(self): |
353 | + self.assertEqual(get_legacy_user(), get_destination_user()) |
354 | + |
355 | + def test_get_destination_user_with_user_from_juju_state(self): |
356 | + user1, user2 = factory.make_user(), factory.make_user() |
357 | + node = factory.make_node(owner=user1) |
358 | + make_provider_state_file(node) |
359 | + self.assertEqual(user1, get_destination_user()) |
360 | + |
361 | + def test_get_destination_user_with_orphaned_juju_state(self): |
362 | + user1, user2 = factory.make_user(), factory.make_user() |
363 | + node = factory.make_node(owner=user1) |
364 | + make_provider_state_file(node) |
365 | + node.delete() # Orphan the state. |
366 | + self.assertEqual(get_legacy_user(), get_destination_user()) |
367 | + |
368 | + |
369 | +class TestCopySSHKeys(TestCase): |
370 | + """Tests for copy_ssh_keys().""" |
371 | + |
372 | + def test_noop_when_there_are_no_keys(self): |
373 | + user1 = factory.make_user() |
374 | + user2 = factory.make_user() |
375 | + copy_ssh_keys(user1, user2) |
376 | + ssh_keys = SSHKey.objects.filter(user__in={user1, user2}) |
377 | + self.assertEqual([], list(ssh_keys)) |
378 | + |
379 | + def test_copy(self): |
380 | + user1 = factory.make_user() |
381 | + key1 = factory.make_sshkey(user1) |
382 | + user2 = factory.make_user() |
383 | + copy_ssh_keys(user1, user2) |
384 | + user2s_ssh_keys = SSHKey.objects.filter(user=user2) |
385 | + self.assertSetEqual( |
386 | + {key1.key}, {ssh_key.key for ssh_key in user2s_ssh_keys}) |
387 | + |
388 | + def test_copy_is_idempotent(self): |
389 | + # When the destination user already has a key, copy_ssh_keys() is a |
390 | + # noop for that key. |
391 | + user1 = factory.make_user() |
392 | + key1 = factory.make_sshkey(user1) |
393 | + user2 = factory.make_user() |
394 | + key2 = factory.make_sshkey(user2, key1.key) |
395 | + copy_ssh_keys(user1, user2) |
396 | + user2s_ssh_keys = SSHKey.objects.filter(user=user2) |
397 | + self.assertSetEqual( |
398 | + {key2.key}, {ssh_key.key for ssh_key in user2s_ssh_keys}) |
399 | + |
400 | + def test_copy_does_not_clobber(self): |
401 | + # When the destination user already has some keys, copy_ssh_keys() |
402 | + # adds to them; it does not remove them. |
403 | + user1 = factory.make_user() |
404 | + key1 = factory.make_sshkey(user1, get_ssh_key_string(1)) |
405 | + user2 = factory.make_user() |
406 | + key2 = factory.make_sshkey(user2, get_ssh_key_string(2)) |
407 | + copy_ssh_keys(user1, user2) |
408 | + user2s_ssh_keys = SSHKey.objects.filter(user=user2) |
409 | + self.assertSetEqual( |
410 | + {key1.key, key2.key}, |
411 | + {ssh_key.key for ssh_key in user2s_ssh_keys}) |
412 | + |
413 | + |
414 | +class TestGiveFileToUser(TestCase): |
415 | + |
416 | + def test_give_unowned_file(self): |
417 | + user = factory.make_user() |
418 | + file = factory.make_file_storage(owner=None) |
419 | + give_file_to_user(file, user) |
420 | + self.assertEqual(user, file.owner) |
421 | + |
422 | + def test_give_owned_file(self): |
423 | + user1 = factory.make_user() |
424 | + user2 = factory.make_user() |
425 | + file = factory.make_file_storage(owner=user1) |
426 | + give_file_to_user(file, user2) |
427 | + self.assertEqual(user2, file.owner) |
428 | + |
429 | + def test_file_saved(self): |
430 | + user = factory.make_user() |
431 | + file = factory.make_file_storage(owner=None) |
432 | + save = self.patch(file, "save") |
433 | + give_file_to_user(file, user) |
434 | + save.assert_called_once() |
435 | + |
436 | + |
437 | +class TestGiveCredentialsToUser(TestCase): |
438 | + |
439 | + def test_give(self): |
440 | + user1 = factory.make_user() |
441 | + user2 = factory.make_user() |
442 | + profile = user1.get_profile() |
443 | + consumer, token = profile.create_authorisation_token() |
444 | + give_api_credentials_to_user(user1, user2) |
445 | + self.assertEqual(user2, reload_object(consumer).user) |
446 | + self.assertEqual(user2, reload_object(token).user) |
447 | + |
448 | + |
449 | +class TestGiveNodeToUser(TestCase): |
450 | + |
451 | + def test_give(self): |
452 | + user1 = factory.make_user() |
453 | + user2 = factory.make_user() |
454 | + node = factory.make_node(owner=user1) |
455 | + give_node_to_user(node, user2) |
456 | + self.assertEqual(user2, reload_object(node).owner) |
457 | + |
458 | + |
459 | +class TestMigrateToUser(TestCase): |
460 | + |
461 | + def test_migrate(self): |
462 | + # This is a mechanical test, to demonstrate that migrate_to_user() is |
463 | + # wired up correctly: it should not really contain much logic because |
464 | + # it is meant only as a convenient wrapper around other functions. |
465 | + # Those functions are unit tested individually, and the overall |
466 | + # behaviour of migrate() is tested too; this is another layer of |
467 | + # verification. It's also a reminder not to stuff logic into |
468 | + # migrate_to_user(); extract it into functions instead and unit test |
469 | + # those. |
470 | + |
471 | + # migrate_to_user() will give all unowned files to a specified user. |
472 | + get_unowned_files = self.patch(migration, "get_unowned_files") |
473 | + get_unowned_files.return_value = [sentinel.file1, sentinel.file2] |
474 | + give_file_to_user = self.patch(migration, "give_file_to_user") |
475 | + # migrate_to_user() will copy all SSH keys and give all API |
476 | + # credentials belonging to node owners over to a specified user. |
477 | + get_owned_nodes_owners = self.patch( |
478 | + migration, "get_owned_nodes_owners") |
479 | + get_owned_nodes_owners.return_value = [ |
480 | + sentinel.node_owner1, sentinel.node_owner2] |
481 | + copy_ssh_keys = self.patch(migration, "copy_ssh_keys") |
482 | + give_api_credentials_to_user = self.patch( |
483 | + migration, "give_api_credentials_to_user") |
484 | + # migrate_to_user() will give all owned nodes to a specified user. |
485 | + get_owned_nodes = self.patch(migration, "get_owned_nodes") |
486 | + get_owned_nodes.return_value = [sentinel.node1, sentinel.node2] |
487 | + give_node_to_user = self.patch(migration, "give_node_to_user") |
488 | + |
489 | + migrate_to_user(sentinel.user) |
490 | + |
491 | + # Each unowned file is given to the destination user one at a time. |
492 | + get_unowned_files.assert_called_once() |
493 | + self.assertEqual( |
494 | + [call(sentinel.file1, sentinel.user), |
495 | + call(sentinel.file2, sentinel.user)], |
496 | + give_file_to_user.call_args_list) |
497 | + # The SSH keys of each node owner are copied to the destination user, |
498 | + # one at a time, and the credentials of these users are given to the |
499 | + # destination user. |
500 | + get_owned_nodes_owners.assert_called_once() |
501 | + self.assertEqual( |
502 | + [call(sentinel.node_owner1, sentinel.user), |
503 | + call(sentinel.node_owner2, sentinel.user)], |
504 | + copy_ssh_keys.call_args_list) |
505 | + self.assertEqual( |
506 | + [call(sentinel.node_owner1, sentinel.user), |
507 | + call(sentinel.node_owner2, sentinel.user)], |
508 | + give_api_credentials_to_user.call_args_list) |
509 | + # Each owned node is given to the destination user one at a time. |
510 | + get_owned_nodes.assert_called_once() |
511 | + self.assertEqual( |
512 | + [call(sentinel.node1, sentinel.user), |
513 | + call(sentinel.node2, sentinel.user)], |
514 | + give_node_to_user.call_args_list) |
515 | + |
516 | + |
517 | +class TestMigrate(TestCase): |
518 | + |
519 | + def test_migrate_runs_when_no_files_exist(self): |
520 | + migrate() |
521 | + |
522 | + def test_migrate_runs_when_no_unowned_files_exist(self): |
523 | + factory.make_file_storage(owner=factory.make_user()) |
524 | + migrate() |
525 | + |
526 | + def test_migrate_all_files_to_single_user_when_only_one_user(self): |
527 | + user = factory.make_user() |
528 | + stored = factory.make_file_storage(owner=None) |
529 | + migrate() |
530 | + self.assertEqual(user, reload_object(stored).owner) |
531 | + |
532 | + def test_migrate_all_files_to_new_legacy_user_when_multiple_users(self): |
533 | + stored = factory.make_file_storage(owner=None) |
534 | + user1 = factory.make_user() |
535 | + user2 = factory.make_user() |
536 | + migrate() |
537 | + stored = reload_object(stored) |
538 | + self.assertNotIn(stored.owner, {user1, user2, None}) |
539 | + |
540 | + def test_migrate_all_nodes_to_new_legacy_user_when_multiple_users(self): |
541 | + factory.make_file_storage(owner=None) |
542 | + user1 = factory.make_user() |
543 | + node1 = factory.make_node(owner=user1) |
544 | + user2 = factory.make_user() |
545 | + node2 = factory.make_node(owner=user2) |
546 | + migrate() |
547 | + self.assertNotIn(reload_object(node1).owner, {user1, user2, None}) |
548 | + self.assertNotIn(reload_object(node2).owner, {user1, user2, None}) |
549 | + |
550 | + def test_migrate_all_nodes_to_bootstrap_owner_when_multiple_users(self): |
551 | + user1 = factory.make_user() |
552 | + node1 = factory.make_node(owner=user1) |
553 | + user2 = factory.make_user() |
554 | + node2 = factory.make_node(owner=user2) |
555 | + make_provider_state_file(node1) |
556 | + migrate() |
557 | + self.assertEqual( |
558 | + (user1, user1), |
559 | + (reload_object(node1).owner, |
560 | + reload_object(node2).owner)) |
561 | + |
562 | + def test_migrate_ancillary_data_to_legacy_user_when_multiple_users(self): |
563 | + factory.make_file_storage(owner=None) |
564 | + # Create two users, both with API credentials, an SSH key and a node. |
565 | + user1 = factory.make_user() |
566 | + consumer1, token1 = user1.get_profile().create_authorisation_token() |
567 | + key1 = factory.make_sshkey(user1, get_ssh_key_string(1)) |
568 | + node1 = factory.make_node(owner=user1) |
569 | + user2 = factory.make_user() |
570 | + consumer2, token2 = user2.get_profile().create_authorisation_token() |
571 | + key2 = factory.make_sshkey(user2, get_ssh_key_string(2)) |
572 | + node2 = factory.make_node(owner=user2) |
573 | + migrate() |
574 | + # The SSH keys have been copied to the legacy user. |
575 | + legacy_user = get_legacy_user() |
576 | + legacy_users_ssh_keys = get_ssh_keys(legacy_user) |
577 | + self.assertSetEqual({key1.key, key2.key}, set(legacy_users_ssh_keys)) |
578 | + # The API credentials have been moved to the legacy user. |
579 | + legacy_users_nodes = Node.objects.filter(owner=legacy_user) |
580 | + self.assertSetEqual({node1, node2}, set(legacy_users_nodes)) |
581 | + self.assertEqual( |
582 | + (legacy_user, legacy_user, legacy_user, legacy_user), |
583 | + (reload_object(consumer1).user, reload_object(token1).user, |
584 | + reload_object(consumer2).user, reload_object(token2).user)) |
585 | |
586 | === modified file 'src/maasserver/tests/data/test_rsa0.pub' |
587 | --- src/maasserver/tests/data/test_rsa0.pub 2012-04-06 09:52:45 +0000 |
588 | +++ src/maasserver/tests/data/test_rsa0.pub 2013-03-06 17:48:23 +0000 |
589 | @@ -1,1 +1,1 @@ |
590 | -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdrzzDZNwyMVBvBTT6kBnrfPZv/AUbkxj7G5CaMTdw6xkKthV22EntD3lxaQxRKzQTfCc2d/CC1K4ushCcRs1S6SQ2zJ2jDq1UmOUkDMgvNh4JVhJYSKc6mu8i3s7oGSmBado5wvtlpSzMrscOpf8Qe/wmT5fH12KB9ipJqoFNQMVbVcVarE/v6wpn3GZC62YRb5iaz9/M+t92Qhu50W2u+KfouqtKB2lwIDDKZMww38ExtdMouh2FZpxaoh4Uey5bRp3tM3JgnWcX6fyUOp2gxJRPIlD9rrZhX5IkEkZM8MQbdPTQLgIf98oFph5RG6w1t02BvI9nJKM7KkKEfBHt ubuntu@server-7476 |
591 | +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdrzzDZNwyMVBvBTT6kBnrfPZv/AUbkxj7G5CaMTdw6xkKthV22EntD3lxaQxRKzQTfCc2d/CC1K4ushCcRs1S6SQ2zJ2jDq1UmOUkDMgvNh4JVhJYSKc6mu8i3s7oGSmBado5wvtlpSzMrscOpf8Qe/wmT5fH12KB9ipJqoFNQMVbVcVarE/v6wpn3GZC62YRb5iaz9/M+t92Qhu50W2u+KfouqtKB2lwIDDKZMww38ExtdMouh2FZpxaoh4Uey5bRp3tM3JgnWcX6fyUOp2gxJRPIlD9rrZhX5IkEkZM8MQbdPTQLgIf98oFph5RG6w1t02BvI9nJKM7KkKEfBHt ubuntu@test_rsa0.pub |
592 | |
593 | === modified file 'src/maasserver/tests/data/test_rsa1.pub' |
594 | --- src/maasserver/tests/data/test_rsa1.pub 2012-04-06 09:52:45 +0000 |
595 | +++ src/maasserver/tests/data/test_rsa1.pub 2013-03-06 17:48:23 +0000 |
596 | @@ -1,1 +1,1 @@ |
597 | -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6Gkj1y8/0T7q/FqBSr9xRBO9GzT+JeoWNXaqhUBg179Zd53XM4qblVwz/rsMa70te8CYNIFU+GbcNY1tNCo78NlHjQA8H98COnbVWKxvABECHrJ8nbYB4lWH9wI8/uvR0um6yUb/tZYbiSqnQxhoGAF/uQQfhqzc+tc7uTjnsa6krrNqQCdpFbAVVy+vZzvcJl6CX8nu5uJ8jedWfXOZJFcQPH+VwkUT0oV+1zVeLpE4LFkRO52JrC9Dy1xgrYM0EhcrShBdD1GQx9IXdW4Z9PIaVcq/y4Qv574yHMvi+6hwG6xpCtRXmy0lG0LiG60c1yOredkO6U0MJIVbeZ/+r ubuntu@server-7493 |
598 | +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6Gkj1y8/0T7q/FqBSr9xRBO9GzT+JeoWNXaqhUBg179Zd53XM4qblVwz/rsMa70te8CYNIFU+GbcNY1tNCo78NlHjQA8H98COnbVWKxvABECHrJ8nbYB4lWH9wI8/uvR0um6yUb/tZYbiSqnQxhoGAF/uQQfhqzc+tc7uTjnsa6krrNqQCdpFbAVVy+vZzvcJl6CX8nu5uJ8jedWfXOZJFcQPH+VwkUT0oV+1zVeLpE4LFkRO52JrC9Dy1xgrYM0EhcrShBdD1GQx9IXdW4Z9PIaVcq/y4Qv574yHMvi+6hwG6xpCtRXmy0lG0LiG60c1yOredkO6U0MJIVbeZ/+r ubuntu@test_rsa1.pub |
599 | |
600 | === modified file 'src/maasserver/tests/data/test_rsa2.pub' |
601 | --- src/maasserver/tests/data/test_rsa2.pub 2012-04-06 09:52:45 +0000 |
602 | +++ src/maasserver/tests/data/test_rsa2.pub 2013-03-06 17:48:23 +0000 |
603 | @@ -1,1 +1,1 @@ |
604 | -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKVdMk4Q+13uUvXjb6iU+oB2Auk0HpaILZ8Pw/V63PTJ+QXtEp0vTe6DEvr9uF2vl6tF+AosiG4krEwqBNGx/h8MmFO7BgNTxn9eU2VwfHzmQ2nqkXHsXgp66cNT0Yd0nfvVV/fsMpKN9fUaYrXjAlFxvC9iQ33Rp6vj/X+oqDvYf3xZjbuZy+BxdJnmiTAJcFouTyrdy1Em1EZITq5M4EXw93/O2vAPYSFPAeELBE+mIMJxOCY1Fm101oAqO0qof3Rb2hZxc2WINjmqZIxoi+sviU0ny/dIFknhYEg1Xh2hObPn0nN5+4VHjBTdRmpRXqggotc53sYC5udVmFsW8B ubuntu@server-7493 |
605 | +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKVdMk4Q+13uUvXjb6iU+oB2Auk0HpaILZ8Pw/V63PTJ+QXtEp0vTe6DEvr9uF2vl6tF+AosiG4krEwqBNGx/h8MmFO7BgNTxn9eU2VwfHzmQ2nqkXHsXgp66cNT0Yd0nfvVV/fsMpKN9fUaYrXjAlFxvC9iQ33Rp6vj/X+oqDvYf3xZjbuZy+BxdJnmiTAJcFouTyrdy1Em1EZITq5M4EXw93/O2vAPYSFPAeELBE+mIMJxOCY1Fm101oAqO0qof3Rb2hZxc2WINjmqZIxoi+sviU0ny/dIFknhYEg1Xh2hObPn0nN5+4VHjBTdRmpRXqggotc53sYC5udVmFsW8B ubuntu@test_rsa2.pub |
606 | |
607 | === modified file 'src/maasserver/tests/data/test_rsa3.pub' |
608 | --- src/maasserver/tests/data/test_rsa3.pub 2012-04-06 09:52:45 +0000 |
609 | +++ src/maasserver/tests/data/test_rsa3.pub 2013-03-06 17:48:23 +0000 |
610 | @@ -1,1 +1,1 @@ |
611 | -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDai2ir5yxckoYTHUbFL6pe01Kx+Dy6nw9p7LhFaBixUOh8G7eIgFBguYcir2ZKBfM/lbTnW+MSiGF2VMlXX0+X9Ux2iwPSJa2wIA7Cc5prCz/RnMRKQ+2S1JJuORoi8tDI0p1R0sGWMXCwaj30oRN0THWz884+d3YlDD/O39h74gnLNEx/TQig/r/Aev3VfeKO6dlbbX81vSad2JVncislyMq1TgJdhn2/JI8t+LW0xVc6ZgQr94YB2M2DNjFSisP2vDrV5LWM+IqiF8T/YHkcSsANr8WWvZWa79uHyRBU3xr2qZZqMjMVL0B/NOJYXyGBIJ7HQnlVLmqFenKl8ZtL ubuntu@server-7493 |
612 | +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDai2ir5yxckoYTHUbFL6pe01Kx+Dy6nw9p7LhFaBixUOh8G7eIgFBguYcir2ZKBfM/lbTnW+MSiGF2VMlXX0+X9Ux2iwPSJa2wIA7Cc5prCz/RnMRKQ+2S1JJuORoi8tDI0p1R0sGWMXCwaj30oRN0THWz884+d3YlDD/O39h74gnLNEx/TQig/r/Aev3VfeKO6dlbbX81vSad2JVncislyMq1TgJdhn2/JI8t+LW0xVc6ZgQr94YB2M2DNjFSisP2vDrV5LWM+IqiF8T/YHkcSsANr8WWvZWa79uHyRBU3xr2qZZqMjMVL0B/NOJYXyGBIJ7HQnlVLmqFenKl8ZtL ubuntu@test_rsa3.pub |
613 | |
614 | === modified file 'src/maasserver/tests/data/test_rsa4.pub' |
615 | --- src/maasserver/tests/data/test_rsa4.pub 2012-04-06 09:52:45 +0000 |
616 | +++ src/maasserver/tests/data/test_rsa4.pub 2013-03-06 17:48:23 +0000 |
617 | @@ -1,1 +1,1 @@ |
618 | -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNDA4vXVTxHuKikIXeA6/K/X7hKpJcOJV0HcXUHlSNa9phNW0f8vbci+BxcLAqIz/U+BPiQ9lCxz7so+qCTFrM4poOdkTyup8VUxUqntiaxgiCJZ1of+eMe39+S9XQk6RogiCpExanhD9xPLkK/mLr5phnQwDjEDJwD4OOF0rYsbYoqje/0Pd+Tm0PIepq/qwsu5PAKPJU8dfnp8BWLCuIJ+DA2lfRUjmxWwLczfM/4hu1bZlYp1mzJJgMIOY92/pUToYxvBiIiKs3qWh6HC5Vxo5Vz4w5WLnTnIPDvpYBvWj8LGXJwHuhqlzed2icwPk8krip2BzwsHotru3UXtKf ubuntu@server-7493 |
619 | +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNDA4vXVTxHuKikIXeA6/K/X7hKpJcOJV0HcXUHlSNa9phNW0f8vbci+BxcLAqIz/U+BPiQ9lCxz7so+qCTFrM4poOdkTyup8VUxUqntiaxgiCJZ1of+eMe39+S9XQk6RogiCpExanhD9xPLkK/mLr5phnQwDjEDJwD4OOF0rYsbYoqje/0Pd+Tm0PIepq/qwsu5PAKPJU8dfnp8BWLCuIJ+DA2lfRUjmxWwLczfM/4hu1bZlYp1mzJJgMIOY92/pUToYxvBiIiKs3qWh6HC5Vxo5Vz4w5WLnTnIPDvpYBvWj8LGXJwHuhqlzed2icwPk8krip2BzwsHotru3UXtKf ubuntu@test_rsa4.pub |
620 | |
621 | === modified file 'src/maasserver/tests/test_sshkey.py' |
622 | --- src/maasserver/tests/test_sshkey.py 2012-05-29 10:24:47 +0000 |
623 | +++ src/maasserver/tests/test_sshkey.py 2013-03-06 17:48:23 +0000 |
624 | @@ -226,7 +226,7 @@ |
625 | key = SSHKey(key=key_string, user=user) |
626 | display = key.display_html() |
627 | self.assertEqual( |
628 | - 'ssh-rsa AAAAB3NzaC1yc2E… ubuntu@server-7476', display) |
629 | + 'ssh-rsa AAAAB3NzaC1yc… ubuntu@test_rsa0.pub', display) |
630 | |
631 | def test_sshkey_display_is_marked_as_HTML_safe(self): |
632 | key_string = get_data('data/test_rsa0.pub') |