Merge lp:~allenap/maas/shared-to-per-tenant-storage into lp:~maas-committers/maas/trunk
- shared-to-per-tenant-storage
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gavin Panella | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1449 | ||||
Proposed branch: | lp:~allenap/maas/shared-to-per-tenant-storage | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Review via email: mp+151858@code.launchpad.net |
Commit message
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
Gavin Panella (allenap) wrote : | # |
Raphaël Badin (rvb) wrote : | # |
Great work!
[0]
I'd suggest adding a test in TestMigrate to make sure the files change ownership properly when there is a valid 'provider-state' file (and a related node with an owner). I know you're sort of testing this at a lower level in 'TestFunctions' but since we suspect this is going to be one of the most used code paths, I'd add that test anyway.
[1]
Can you please reference the "steps" you're testing in 'TestMigrate' the same way you did it in "def migrate():"?
more to come…
Raphaël Badin (rvb) wrote : | # |
[2]
+# -*- coding: utf-8 -*-
I don't see why this is required…?
(fwiw, the only file with that are the auto-generated migration files)
PS: I suspect this is just because of → … s/→/->/ ?
[3]
76 + try:
77 + legacy_user = User.objects.
If a migrations runs and we are in a position to run that code (i.e. more than one user, no 'provider-state' file), if it happens that there is a *real* user with that username, it won't do the right thing. I reckon this is pretty improbable but maybe we should at least raise and exception in that case. What do you think?
Gavin Panella (allenap) wrote : | # |
> [2]
>
> +# -*- coding: utf-8 -*-
>
> I don't see why this is required…?
> (fwiw, the only file with that are the auto-generated migration files)
>
> PS: I suspect this is just because of → … s/→/->/ ?
Yes, that's exactly why. I'll remove it for the benefit of
encoding-challenged text editors :)
>
> [3]
>
> 76 + try:
> 77 + legacy_user = User.objects.
>
> If a migrations runs and we are in a position to run that code
> (i.e. more than one user, no 'provider-state' file), if it happens
> that there is a *real* user with that username, it won't do the
> right thing. I reckon this is pretty improbable but maybe we should
> at least raise and exception in that case. What do you think?
I want the code to be safe to run multiple times, so I think it's
important to be able to pick up an existing legacy user. However, I'll
make the legacy user similar to the other system users, in that it
won't have a UserProfile. Then I can check that here and raise an
exception if it appears to be a regular user with the same name.
Sound okay?
I
Raphaël Badin (rvb) wrote : | # |
[4]
149 +def give_credential
150 + """Gives one user's credentials to another.
'credentials' is a bit vague don't you think? How about 'API credentials'?
[5]
57 +from maasserver.models import (
58 + FileStorage,
59 + Node,
60 + SSHKey,
61 + User,
62 + )
There is no 'User' class in maasserver.models, it's simply an alias for django.
[6]
106 +def get_owned_
107 + """Returns a `QuerySet` of the owners of nodes owned by real users."""
108 + owner_ids = get_owned_
109 + return User.objects.
Adding '.distinct()' to the query on line 108 might speed things up a bit if there is lots of nodes belonging to just a few users.
Raphaël Badin (rvb) wrote : | # |
> > If a migrations runs and we are in a position to run that code
> > (i.e. more than one user, no 'provider-state' file), if it happens
> > that there is a *real* user with that username, it won't do the
> > right thing. I reckon this is pretty improbable but maybe we should
> > at least raise and exception in that case. What do you think?
>
> I want the code to be safe to run multiple times, so I think it's
> important to be able to pick up an existing legacy user. However, I'll
> make the legacy user similar to the other system users, in that it
> won't have a UserProfile. Then I can check that here and raise an
> exception if it appears to be a regular user with the same name.
>
> Sound okay?
Sounds good to me. Thanks!
Gavin Panella (allenap) wrote : | # |
> [0]
>
> I'd suggest adding a test in TestMigrate to make sure the files change
> ownership properly when there is a valid 'provider-state' file (and a related
> node with an owner). I know you're sort of testing this at a lower level in
> 'TestFunctions' but since we suspect this is going to be one of the most used
> code paths, I'd add that test anyway.
Good idea, done.
>
> [1]
>
> Can you please reference the "steps" you're testing in 'TestMigrate' the same
> way you did it in "def migrate():"?
Sure, done.
Gavin Panella (allenap) wrote : | # |
> [4]
>
> 149 +def give_credential
> 150 + """Gives one user's credentials to another.
>
> 'credentials' is a bit vague don't you think? How about 'API credentials'?
I've renamed the function and updated the docstring.
> [5]
>
> 57 +from maasserver.models import (
> 58 + FileStorage,
> 59 + Node,
> 60 + SSHKey,
> 61 + User,
> 62 + )
>
> There is no 'User' class in maasserver.models, it's simply an alias for
> django.
> from Django.
Done.
> [6]
>
> 106 +def get_owned_
> 107 + """Returns a `QuerySet` of the owners of nodes owned by real
> users."""
> 108 + owner_ids = get_owned_
> 109 + return User.objects.
>
> Adding '.distinct()' to the query on line 108 might speed things up a bit if
> there is lots of nodes belonging to just a few users.
Done.
Raphaël Badin (rvb) wrote : | # |
[7]
433 + def test_give(self):
434 + user1 = factory.make_user()
435 + user2 = factory.make_user()
436 + profile = user1.get_profile()
437 + consumer, token = profile.
438 + give_credential
439 + self.assertEqua
440 + self.assertEqua
441 +
442 + def test_consumer_
443 + user1 = factory.make_user()
444 + user2 = factory.make_user()
445 + profile = user1.get_profile()
446 + consumer, token = profile.
447 + self.patch(
448 + self.patch(token, "save")
449 + give_credential
450 + consumer.
451 + token.save.
I don't really see what the second test adds… since you're reloading the objects in the first test and you're getting what you expect, you can be sure the objects have been saved… ?
[8]
456 + def test_give(self):
457 + user1 = factory.make_user()
458 + user2 = factory.make_user()
459 + node = factory.
460 + give_node_
461 + self.assertEqua
462 +
463 + def test_node_
464 + user1 = factory.make_user()
465 + user2 = factory.make_user()
466 + node = factory.
467 + save = self.patch(node, "save")
468 + give_node_
469 + save.assert_
Similar remark here. Although I'd refactor the first test to reload the object before checking the owner, and I'd get rid of the second test.
Raphaël Badin (rvb) wrote : | # |
[9]
580 + self.assertEqual(
581 + (legacy_user, legacy_user, legacy_user, legacy_user),
"(legacy_user, legacy_user, legacy_user, legacy_user)" => "[legacy_user] * 4" ?
I think it's more readable but this is really a detail.
[10]
558 + self.assertEqua
559 + self.assertEqua
Probably better to make that one check.
[11]
472 +class TestMigrateToUs
473 +
474 + def test_migrate(self):
That test is a bit… huge :) . Don't you think it could test for behavior instead of patching methods… and maybe share some code with TestMigrate.
Gavin Panella (allenap) wrote : | # |
> ... I'll make the legacy user similar to the other system users, in
> that it won't have a UserProfile. Then I can check that here and
> raise an exception if it appears to be a regular user with the same
> name.
Actually, I'm not 100% sure this is a good idea. The legacy user needs
to be the same as a regular user. It's not unreasonable for someone to
set its password and log-in to the web UI, at which point the lack of
a UserProfile will cause problems, I think.
I'm inclined to either (a) not check or (b) check only that the user's
full name is "Shared Environment".
Raphaël Badin (rvb) wrote : | # |
> > ... I'll make the legacy user similar to the other system users, in
> > that it won't have a UserProfile. Then I can check that here and
> > raise an exception if it appears to be a regular user with the same
> > name.
>
> Actually, I'm not 100% sure this is a good idea. The legacy user needs
> to be the same as a regular user. It's not unreasonable for someone to
> set its password and log-in to the web UI, at which point the lack of
> a UserProfile will cause problems, I think.
Indeed, you're right, I lost track of that. (I think we need a profile to log in the UI so a user probably won't be able to log in, but as you pointed out, this is a problem.)
>
> I'm inclined to either (a) not check or (b) check only that the user's
> full name is "Shared Environment".
b) does not feel very precise :/
Gavin Panella (allenap) wrote : | # |
> [7]
...
> I don't really see what the second test adds… since you're reloading the
> objects in the first test and you're getting what you expect, you can be sure
> the objects have been saved… ?
Good point. I was adding belts and braces where dungarees were being
worn :)
> [8]
...
> Similar remark here. Although I'd refactor the first test to reload the
> object before checking the owner, and I'd get rid of the second test.
Done.
Gavin Panella (allenap) wrote : | # |
> [9]
>
> 580 + self.assertEqual(
> 581 + (legacy_user, legacy_user, legacy_user, legacy_user),
>
> "(legacy_user, legacy_user, legacy_user, legacy_user)" => "[legacy_user] * 4"
> ?
>
> I think it's more readable but this is really a detail.
We'll agree to differ on this one :)
> [10]
>
> 558 + self.assertEqua
> 559 + self.assertEqua
>
> Probably better to make that one check.
Done.
>
> [11]
>
> 472 +class TestMigrateToUs
> 473 +
> 474 + def test_migrate(self):
>
> That test is a bit… huge :) . Don't you think it could test for behavior
> instead of patching methods… and maybe share some code with
> TestMigrate.
The behaviour of the individual components is tested elsewhere, and
the overall behaviour is tested in TestMigrate, so I sought only to
show that things were wired up correctly. In a way it's also a
reminder not to put more logic into migrate_to_user(); it should be
factored into separate functions.
It was a bit of a what-if experiment too. I've added comments to the
function to explain its rationale, but I'm open to more discussion.
It's interesting to consider the value of this kind of test; perhaps
not very much.
Gavin Panella (allenap) wrote : | # |
> > I'm inclined to either (a) not check or (b) check only that the user's
> > full name is "Shared Environment".
>
> b) does not feel very precise :/
I agree, but it's a small problem in the scale of things here. I say
we just stick with what we've got, i.e. no checks. If someone already
has a user called shared-environment then I'm inclined to think that
it's the user they'd want us to choose anyway!
Raphaël Badin (rvb) wrote : | # |
> I agree, but it's a small problem in the scale of things here. I say
> we just stick with what we've got, i.e. no checks. If someone already
> has a user called shared-environment then I'm inclined to think that
> it's the user they'd want us to choose anyway!
Okay… the consequences of picking up the wrong user are not devastating anyway, plus it's very unlikely that someone would have created a user precisely with that name.
Raphaël Badin (rvb) wrote : | # |
Looks good, thanks for all your work on this branch!
Raphaël Badin (rvb) wrote : | # |
A fallout from renaming the ssh keys:
=======
FAIL: maasserver.
-------
_StringException: Traceback (most recent call last):
File "/home/
'ssh-rsa AAAAB3NzaC1yc2E
MismatchError: !=:
reference = u'ssh-rsa AAAAB3NzaC1yc2E
actual = u'ssh-rsa AAAAB3NzaC1yc&
Gavin Panella (allenap) wrote : | # |
> Looks good, thanks for all your work on this branch!
Thank you for all your help :)
> A fallout from renaming the ssh keys:
Thanks for pointing that out. Fixed.
Preview Diff
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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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 15:28:20 +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') |
This is a work in progress for the shared to per-tenant namespace migration. It might actually be complete, but it's 1am here and I can't tell any more.