Merge lp:~jason-hobbs/maas/lp-1210393 into lp:~maas-committers/maas/trunk
- lp-1210393
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jason Hobbs | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 1991 | ||||
Proposed branch: | lp:~jason-hobbs/maas/lp-1210393 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
456 lines (+329/-44) 2 files modified
etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py (+91/-26) etc/maas/templates/commissioning-user-data/snippets/tests/test_maas_ipmi_autodetect.py (+238/-18) |
||||
To merge this branch: | bzr merge lp:~jason-hobbs/maas/lp-1210393 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+207288@code.launchpad.net |
Commit message
Refactor user selection.
Don't assume a fixed number of users, or rely on any given user existing. Instead, ask the BMC what users exist, and then inspect users from that list. This eliminates the dependency on a 'User10' existing.
Pick the first user with username 'maas'. If no user with username 'maas' exists, pick the first user that has no username set. Otherwise, fail IPMI setup.
Description of the change
Refactor user selection.
Don't assume a fixed number of users, or rely on any given user existing. Instead, ask the BMC what users exist, and then inspect users from that list. This eliminates the dependency on a 'User10' existing.
Pick the first user with username 'maas'. If no user with username 'maas' exists, pick the first user has no username set. Otherwise, fail IPMI setup.
Jason Hobbs (jason-hobbs) wrote : | # |
Gavin,
On 02/19/2014 04:53 PM, Gavin Panella wrote:
> Review: Approve
>
> Looks good, thanks again!
Thanks again for the review all the pointers! I've made all the changes
you suggested and updated the branch.
Regards,
Jason
Julian Edwards (julian-edwards) wrote : | # |
On Wednesday 19 Feb 2014 22:53:25 you wrote:
> Can you name this make_disabled_
> prefix "make", except where they don't, er, they have the prefix "get".
This has been royally p!ssing me off lately... I might just pull the trigger
and change everything to make_<thing>
Preview Diff
1 | === modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py' | |||
2 | --- etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py 2014-02-18 19:26:26 +0000 | |||
3 | +++ etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py 2014-02-20 17:53:42 +0000 | |||
4 | @@ -38,6 +38,10 @@ | |||
5 | 38 | import time | 38 | import time |
6 | 39 | 39 | ||
7 | 40 | 40 | ||
8 | 41 | class IPMIUserError(Exception): | ||
9 | 42 | """An error related to an IPMI user.""" | ||
10 | 43 | |||
11 | 44 | |||
12 | 41 | def run_command(command_args): | 45 | def run_command(command_args): |
13 | 42 | """Run a command. Return output if successful or raise exception if not.""" | 46 | """Run a command. Return output if successful or raise exception if not.""" |
14 | 43 | output = subprocess.check_output(command_args, stderr=subprocess.STDOUT) | 47 | output = subprocess.check_output(command_args, stderr=subprocess.STDOUT) |
15 | @@ -62,18 +66,98 @@ | |||
16 | 62 | return '%s:%s' % (user_number, parameter) | 66 | return '%s:%s' % (user_number, parameter) |
17 | 63 | 67 | ||
18 | 64 | 68 | ||
19 | 65 | def bmc_user_get(user_number, parameter): | ||
20 | 66 | """Get a user parameter via bmc-config commit.""" | ||
21 | 67 | key = format_user_key(user_number, parameter) | ||
22 | 68 | return bmc_get(key) | ||
23 | 69 | |||
24 | 70 | |||
25 | 71 | def bmc_user_set(user_number, parameter, value): | 69 | def bmc_user_set(user_number, parameter, value): |
26 | 72 | """Set a user parameter via bmc-config commit.""" | 70 | """Set a user parameter via bmc-config commit.""" |
27 | 73 | key = format_user_key(user_number, parameter) | 71 | key = format_user_key(user_number, parameter) |
28 | 74 | bmc_set(key, value) | 72 | bmc_set(key, value) |
29 | 75 | 73 | ||
30 | 76 | 74 | ||
31 | 75 | def parse_section(section): | ||
32 | 76 | """Parse the text of a section. | ||
33 | 77 | |||
34 | 78 | Returns a tuple of ('SectionName', section_attributes_dict) | ||
35 | 79 | """ | ||
36 | 80 | # match lines with a word followed by space and then a non commment string | ||
37 | 81 | pattern = r'^\s*(\w+)\s+([^# \t\r\n\v\f]+).*$' | ||
38 | 82 | kv_tuples = re.findall(pattern, section, re.MULTILINE) | ||
39 | 83 | kv_dict = dict(kv_tuples) | ||
40 | 84 | section_name = kv_dict.pop('Section') | ||
41 | 85 | section_record = (section_name, kv_dict) | ||
42 | 86 | |||
43 | 87 | return section_record | ||
44 | 88 | |||
45 | 89 | |||
46 | 90 | def bmc_get_section(section_name): | ||
47 | 91 | """Retrieve the text of a section from the BMC.""" | ||
48 | 92 | command = ('bmc-config', '--checkout', '--section', section_name) | ||
49 | 93 | output = run_command(command) | ||
50 | 94 | return output | ||
51 | 95 | |||
52 | 96 | |||
53 | 97 | def get_user_record(user_number): | ||
54 | 98 | """Return a dictionary of the user settings for a user number.""" | ||
55 | 99 | section = bmc_get_section(user_number) | ||
56 | 100 | _, record = parse_section(section) | ||
57 | 101 | return record | ||
58 | 102 | |||
59 | 103 | |||
60 | 104 | def bmc_list_sections(): | ||
61 | 105 | """Retrieve the names of config sections from the BMC.""" | ||
62 | 106 | command = ('bmc-config', '-L') | ||
63 | 107 | output = run_command(command) | ||
64 | 108 | return output | ||
65 | 109 | |||
66 | 110 | |||
67 | 111 | def list_user_numbers(): | ||
68 | 112 | """List the user numbers on the BMC.""" | ||
69 | 113 | output = bmc_list_sections() | ||
70 | 114 | pattern = r'^(User\d+)$' | ||
71 | 115 | users = re.findall(pattern, output, re.MULTILINE) | ||
72 | 116 | |||
73 | 117 | return users | ||
74 | 118 | |||
75 | 119 | |||
76 | 120 | def pick_user_number_from_list(search_username, user_numbers): | ||
77 | 121 | """Pick the best user number for a user from a list of user numbers. | ||
78 | 122 | |||
79 | 123 | If any any existing user's username matches the search username, pick | ||
80 | 124 | that user. | ||
81 | 125 | |||
82 | 126 | Otherwise, pick the first user that has no username set. | ||
83 | 127 | |||
84 | 128 | If no users match those criteria, raise an IPMIUserError. | ||
85 | 129 | """ | ||
86 | 130 | first_unused = None | ||
87 | 131 | |||
88 | 132 | for user_number in user_numbers: | ||
89 | 133 | # The IPMI spec reserves User1 as anonymous. | ||
90 | 134 | if user_number == 'User1': | ||
91 | 135 | continue | ||
92 | 136 | |||
93 | 137 | user_record = get_user_record(user_number) | ||
94 | 138 | |||
95 | 139 | username = user_record.get('Username') | ||
96 | 140 | |||
97 | 141 | if username == search_username: | ||
98 | 142 | return user_number | ||
99 | 143 | |||
100 | 144 | if username is None and first_unused is None: | ||
101 | 145 | first_unused = user_number | ||
102 | 146 | |||
103 | 147 | return first_unused | ||
104 | 148 | |||
105 | 149 | |||
106 | 150 | def pick_user_number(search_username): | ||
107 | 151 | """Pick the best user number for a username.""" | ||
108 | 152 | user_numbers = list_user_numbers() | ||
109 | 153 | user_number = pick_user_number_from_list(search_username, user_numbers) | ||
110 | 154 | |||
111 | 155 | if not user_number: | ||
112 | 156 | raise IPMIUserError('No IPMI user slots available.') | ||
113 | 157 | |||
114 | 158 | return user_number | ||
115 | 159 | |||
116 | 160 | |||
117 | 77 | def is_ipmi_dhcp(): | 161 | def is_ipmi_dhcp(): |
118 | 78 | output = bmc_get('Lan_Conf:IP_Address_Source') | 162 | output = bmc_get('Lan_Conf:IP_Address_Source') |
119 | 79 | show_re = re.compile('IP_Address_Source\s+Use_DHCP') | 163 | show_re = re.compile('IP_Address_Source\s+Use_DHCP') |
120 | @@ -91,26 +175,8 @@ | |||
121 | 91 | return res.group() | 175 | return res.group() |
122 | 92 | 176 | ||
123 | 93 | 177 | ||
124 | 94 | def get_ipmi_user_number(user): | ||
125 | 95 | for i in range(1, 17): | ||
126 | 96 | ipmi_user_number = "User%s" % i | ||
127 | 97 | # bmc-config fails if no slot for the requested user exists; | ||
128 | 98 | # instead of bailing, just keep trying remaining users. | ||
129 | 99 | try: | ||
130 | 100 | output = bmc_user_get(ipmi_user_number, 'Username') | ||
131 | 101 | except subprocess.CalledProcessError: | ||
132 | 102 | pass | ||
133 | 103 | else: | ||
134 | 104 | if user in output: | ||
135 | 105 | return ipmi_user_number | ||
136 | 106 | return None | ||
137 | 107 | |||
138 | 108 | |||
139 | 109 | def commit_ipmi_user_settings(user, password): | 178 | def commit_ipmi_user_settings(user, password): |
144 | 110 | ipmi_user_number = get_ipmi_user_number(user) | 179 | ipmi_user_number = pick_user_number(user) |
141 | 111 | if ipmi_user_number is None: | ||
142 | 112 | bmc_user_set('User10', 'Username', user) | ||
143 | 113 | ipmi_user_number = get_ipmi_user_number(user) | ||
145 | 114 | bmc_user_set(ipmi_user_number, 'Username', user) | 180 | bmc_user_set(ipmi_user_number, 'Username', user) |
146 | 115 | bmc_user_set(ipmi_user_number, 'Password', password) | 181 | bmc_user_set(ipmi_user_number, 'Password', password) |
147 | 116 | bmc_user_set(ipmi_user_number, 'Enable_User', 'Yes') | 182 | bmc_user_set(ipmi_user_number, 'Enable_User', 'Yes') |
148 | @@ -183,7 +249,6 @@ | |||
149 | 183 | set_ipmi_network_source("Use_DHCP") | 249 | set_ipmi_network_source("Use_DHCP") |
150 | 184 | # allow IPMI 120 seconds to obtain an IP address | 250 | # allow IPMI 120 seconds to obtain an IP address |
151 | 185 | time.sleep(120) | 251 | time.sleep(120) |
152 | 186 | |||
153 | 187 | # create user/pass | 252 | # create user/pass |
154 | 188 | IPMI_MAAS_USER = "maas" | 253 | IPMI_MAAS_USER = "maas" |
155 | 189 | IPMI_MAAS_PASSWORD = generate_random_password() | 254 | IPMI_MAAS_PASSWORD = generate_random_password() |
156 | 190 | 255 | ||
157 | === modified file 'etc/maas/templates/commissioning-user-data/snippets/tests/test_maas_ipmi_autodetect.py' | |||
158 | --- etc/maas/templates/commissioning-user-data/snippets/tests/test_maas_ipmi_autodetect.py 2014-02-18 19:26:26 +0000 | |||
159 | +++ etc/maas/templates/commissioning-user-data/snippets/tests/test_maas_ipmi_autodetect.py 2014-02-20 17:53:42 +0000 | |||
160 | @@ -16,14 +16,24 @@ | |||
161 | 16 | 16 | ||
162 | 17 | import subprocess | 17 | import subprocess |
163 | 18 | 18 | ||
164 | 19 | from collections import OrderedDict | ||
165 | 20 | |||
166 | 19 | from maastesting.testcase import MAASTestCase | 21 | from maastesting.testcase import MAASTestCase |
167 | 20 | from maastesting.factory import factory | 22 | from maastesting.factory import factory |
168 | 21 | 23 | ||
170 | 22 | import snippets.maas_ipmi_autodetect | 24 | from snippets import maas_ipmi_autodetect |
171 | 23 | 25 | ||
172 | 24 | from snippets.maas_ipmi_autodetect import ( | 26 | from snippets.maas_ipmi_autodetect import ( |
173 | 27 | bmc_get_section, | ||
174 | 28 | bmc_list_sections, | ||
175 | 25 | format_user_key, | 29 | format_user_key, |
176 | 30 | get_user_record, | ||
177 | 31 | list_user_numbers, | ||
178 | 32 | parse_section, | ||
179 | 33 | pick_user_number, | ||
180 | 34 | pick_user_number_from_list, | ||
181 | 26 | run_command, | 35 | run_command, |
182 | 36 | IPMIUserError | ||
183 | 27 | ) | 37 | ) |
184 | 28 | 38 | ||
185 | 29 | 39 | ||
186 | @@ -63,8 +73,8 @@ | |||
187 | 63 | self.assertEqual(expected, user_key) | 73 | self.assertEqual(expected, user_key) |
188 | 64 | 74 | ||
189 | 65 | 75 | ||
192 | 66 | class TestBMCMethods(MAASTestCase): | 76 | class TestBMCKeyPairMethods(MAASTestCase): |
193 | 67 | """Tests for the bmc_* methods.""" | 77 | """Tests for methods that use bmc-config --key-pair""" |
194 | 68 | 78 | ||
195 | 69 | scenarios = [ | 79 | scenarios = [ |
196 | 70 | ('bmc_get', dict( | 80 | ('bmc_get', dict( |
197 | @@ -73,9 +83,6 @@ | |||
198 | 73 | ('bmc_set', dict( | 83 | ('bmc_set', dict( |
199 | 74 | method_name='bmc_set', args=['Test:Key', 'myval'], | 84 | method_name='bmc_set', args=['Test:Key', 'myval'], |
200 | 75 | key_pair_fmt='--key-pair=%s=%s', direction='--commit')), | 85 | key_pair_fmt='--key-pair=%s=%s', direction='--commit')), |
201 | 76 | ('bmc_user_get', dict( | ||
202 | 77 | method_name='bmc_user_get', args=['User10', 'Username'], | ||
203 | 78 | key_pair_fmt='--key-pair=%s:%s', direction='--checkout')), | ||
204 | 79 | ('bmc_user_set', dict( | 86 | ('bmc_user_set', dict( |
205 | 80 | method_name='bmc_user_set', args=['User10', 'Username', 'maas'], | 87 | method_name='bmc_user_set', args=['User10', 'Username', 'maas'], |
206 | 81 | key_pair_fmt='--key-pair=%s:%s=%s', direction='--commit')) | 88 | key_pair_fmt='--key-pair=%s:%s=%s', direction='--commit')) |
207 | @@ -84,24 +91,237 @@ | |||
208 | 84 | def test_runs_bmc_config(self): | 91 | def test_runs_bmc_config(self): |
209 | 85 | """Ensure bmc-config is run properly.""" | 92 | """Ensure bmc-config is run properly.""" |
210 | 86 | 93 | ||
212 | 87 | recorder = self.patch(snippets.maas_ipmi_autodetect, 'run_command') | 94 | recorder = self.patch(maas_ipmi_autodetect, 'run_command') |
213 | 88 | 95 | ||
214 | 89 | # Grab the method from the class module where it lives. | 96 | # Grab the method from the class module where it lives. |
216 | 90 | method = getattr(snippets.maas_ipmi_autodetect, self.method_name) | 97 | method = getattr(maas_ipmi_autodetect, self.method_name) |
217 | 91 | 98 | ||
218 | 92 | method(*self.args) | 99 | method(*self.args) |
219 | 93 | 100 | ||
220 | 94 | # call_args[0] is a tuple of ordered args to run_command. | ||
221 | 95 | tokens = recorder.call_args[0][0] | ||
222 | 96 | |||
223 | 97 | # Ensure bmc-config is being called. | ||
224 | 98 | self.assertEqual(tokens[0], 'bmc-config') | ||
225 | 99 | |||
226 | 100 | # Ensure the correct --commit or --checkout is used. | ||
227 | 101 | self.assertIn(self.direction, tokens) | ||
228 | 102 | |||
229 | 103 | # Note that the fmt string must use positional argument specifiers | 101 | # Note that the fmt string must use positional argument specifiers |
230 | 104 | # if the order of appearance of args in the fmt string doesn't match | 102 | # if the order of appearance of args in the fmt string doesn't match |
231 | 105 | # the order of args to the method. | 103 | # the order of args to the method. |
232 | 106 | key_pair_string = self.key_pair_fmt % tuple(self.args) | 104 | key_pair_string = self.key_pair_fmt % tuple(self.args) |
234 | 107 | self.assertIn(key_pair_string, tokens) | 105 | |
235 | 106 | expected_args = ('bmc-config', self.direction, key_pair_string) | ||
236 | 107 | recorder.assert_called_once_with(expected_args) | ||
237 | 108 | |||
238 | 109 | |||
239 | 110 | class TestBMCGetSection(MAASTestCase): | ||
240 | 111 | """Tests for bmc_get_section().""" | ||
241 | 112 | |||
242 | 113 | def test_runs_bmc_config(self): | ||
243 | 114 | """Ensure bmc-config is called with the correct args.""" | ||
244 | 115 | recorder = self.patch(maas_ipmi_autodetect, 'run_command') | ||
245 | 116 | |||
246 | 117 | section = 'foo' | ||
247 | 118 | bmc_get_section(section) | ||
248 | 119 | |||
249 | 120 | recorder.assert_called_once_with( | ||
250 | 121 | ('bmc-config', '--checkout', '--section', section)) | ||
251 | 122 | |||
252 | 123 | |||
253 | 124 | class TestGetUserRecord(MAASTestCase): | ||
254 | 125 | """Tests for get_user_record().""" | ||
255 | 126 | |||
256 | 127 | def test_get_user_record(self): | ||
257 | 128 | """Ensure get_user_record() processes requests properly.""" | ||
258 | 129 | user_number = 'User1' | ||
259 | 130 | user_text = 'some text' | ||
260 | 131 | user_record = {'bar': 'baz'} | ||
261 | 132 | |||
262 | 133 | bgs_mock = self.patch(maas_ipmi_autodetect, 'bmc_get_section') | ||
263 | 134 | bgs_mock.return_value = user_text | ||
264 | 135 | ps_mock = self.patch(maas_ipmi_autodetect, 'parse_section') | ||
265 | 136 | ps_mock.return_value = (user_number, user_record) | ||
266 | 137 | |||
267 | 138 | record = get_user_record(user_number) | ||
268 | 139 | self.assertEqual(record, user_record) | ||
269 | 140 | bgs_mock.assert_called_once_with(user_number) | ||
270 | 141 | ps_mock.assert_called_once_with(user_text) | ||
271 | 142 | |||
272 | 143 | |||
273 | 144 | class TestBMCListSections(MAASTestCase): | ||
274 | 145 | """Tests for bmc_list_sections().""" | ||
275 | 146 | |||
276 | 147 | def test_bmc_list_sections(self): | ||
277 | 148 | """Ensure bmc-config is called with the correct args.""" | ||
278 | 149 | recorder = self.patch(maas_ipmi_autodetect, 'run_command') | ||
279 | 150 | bmc_list_sections() | ||
280 | 151 | recorder.assert_called_once_with(('bmc-config', '-L')) | ||
281 | 152 | |||
282 | 153 | |||
283 | 154 | class TestListUserNumbers(MAASTestCase): | ||
284 | 155 | """Tests for list_user_numbers().""" | ||
285 | 156 | |||
286 | 157 | section_names = ( | ||
287 | 158 | "User1\n" | ||
288 | 159 | "User4\n" | ||
289 | 160 | "User3\n" | ||
290 | 161 | "User16\n" | ||
291 | 162 | "User7more\n" | ||
292 | 163 | "Userworld\n" | ||
293 | 164 | "Otherwise\n" | ||
294 | 165 | "4User5\n" | ||
295 | 166 | "4User\n" | ||
296 | 167 | "User\n" | ||
297 | 168 | "3" | ||
298 | 169 | ) | ||
299 | 170 | |||
300 | 171 | def test_matching(self): | ||
301 | 172 | """Ensure only properly formatted User sections match.""" | ||
302 | 173 | mock = self.patch(maas_ipmi_autodetect, 'bmc_list_sections') | ||
303 | 174 | mock.return_value = self.section_names | ||
304 | 175 | expected = ['User1', 'User4', 'User3', 'User16'] | ||
305 | 176 | user_numbers = list_user_numbers() | ||
306 | 177 | self.assertEqual(expected, user_numbers) | ||
307 | 178 | |||
308 | 179 | def test_empty(self): | ||
309 | 180 | """Ensure an empty list is handled correctly.""" | ||
310 | 181 | mock = self.patch(maas_ipmi_autodetect, 'bmc_list_sections') | ||
311 | 182 | mock.return_value = '' | ||
312 | 183 | results = list_user_numbers() | ||
313 | 184 | self.assertEqual([], results) | ||
314 | 185 | |||
315 | 186 | |||
316 | 187 | class TestParseSection(MAASTestCase): | ||
317 | 188 | """Tests for parse_section().""" | ||
318 | 189 | |||
319 | 190 | section_template = ( | ||
320 | 191 | "Section Test\n" # Section line, no leading space. | ||
321 | 192 | "\tUsername\t\tBob\n" # Normal line. | ||
322 | 193 | " Enabled_User\t\tNo\n" # Leading space, not tab. | ||
323 | 194 | "\t\tPassword\t\tBobPass\n" # Multiple leading tab. | ||
324 | 195 | "\tAnother Value\n" # Separating space, not tab. | ||
325 | 196 | "\tCharacters\t,./;:'\"[]{}|\\`~!@$%^&*()-_+=" # Gunk. | ||
326 | 197 | "\n" # Blank line. | ||
327 | 198 | "\tNotMe\n" # Single word. | ||
328 | 199 | "\t#Or Me\n" # Comment line. | ||
329 | 200 | "\tMe #Neither\n" # Word followed by comment. | ||
330 | 201 | "\tThree\tWord\tLine\n" # More than two words. | ||
331 | 202 | "\tFinal\tLine" # No trailing whitespace. | ||
332 | 203 | ) | ||
333 | 204 | |||
334 | 205 | def test_matching(self): | ||
335 | 206 | """Ensure only properly formatted User sections match.""" | ||
336 | 207 | |||
337 | 208 | record = parse_section(self.section_template) | ||
338 | 209 | |||
339 | 210 | expected_attributes = { | ||
340 | 211 | 'Username': 'Bob', | ||
341 | 212 | 'Enabled_User': 'No', | ||
342 | 213 | 'Password': 'BobPass', | ||
343 | 214 | 'Another': 'Value', | ||
344 | 215 | 'Characters': ",./;:'\"[]{}|\\`~!@$%^&*()-_+=", | ||
345 | 216 | 'Three': 'Word', | ||
346 | 217 | 'Final': 'Line' | ||
347 | 218 | } | ||
348 | 219 | |||
349 | 220 | self.assertEqual('Test', record[0]) | ||
350 | 221 | self.assertEqual(expected_attributes, record[1]) | ||
351 | 222 | |||
352 | 223 | def test_fail_missing_section_line(self): | ||
353 | 224 | """Ensure an exception is raised if the section header is missing.""" | ||
354 | 225 | no_section = self.section_template.replace('Section Test', '') | ||
355 | 226 | self.assertRaises(Exception, parse_section, no_section) | ||
356 | 227 | |||
357 | 228 | |||
358 | 229 | def make_user(update=None): | ||
359 | 230 | """Make a simple user record.""" | ||
360 | 231 | |||
361 | 232 | base = {'Lan_Enable_IPMI_Msgs': 'No'} | ||
362 | 233 | |||
363 | 234 | if update: | ||
364 | 235 | base.update(update) | ||
365 | 236 | |||
366 | 237 | return base | ||
367 | 238 | |||
368 | 239 | |||
369 | 240 | def make_attributes(attributes_update=None): | ||
370 | 241 | """Base user records with updates in an OrderedDict.""" | ||
371 | 242 | |||
372 | 243 | attributes_template = { | ||
373 | 244 | 'User1': { | ||
374 | 245 | 'Enable_User': 'Yes' | ||
375 | 246 | }, | ||
376 | 247 | 'User2': { | ||
377 | 248 | 'Username': 'admin', | ||
378 | 249 | 'Enable_User': 'Yes' | ||
379 | 250 | } | ||
380 | 251 | } | ||
381 | 252 | |||
382 | 253 | base = OrderedDict(attributes_template) | ||
383 | 254 | |||
384 | 255 | if attributes_update is not None: | ||
385 | 256 | base.update(attributes_update) | ||
386 | 257 | |||
387 | 258 | return base | ||
388 | 259 | |||
389 | 260 | |||
390 | 261 | class TestPickUserNumberFromList(MAASTestCase): | ||
391 | 262 | """Tests for pick_user_number_from_list().""" | ||
392 | 263 | |||
393 | 264 | scenarios = [ | ||
394 | 265 | ('Empty user list', dict( | ||
395 | 266 | user_attributes={}, | ||
396 | 267 | expected=None)), | ||
397 | 268 | ('Existing MAAS user', dict( | ||
398 | 269 | user_attributes=make_attributes({ | ||
399 | 270 | 'User4': make_user(), | ||
400 | 271 | 'User5': {'Username': 'maas'}}), | ||
401 | 272 | expected='User5')), | ||
402 | 273 | ('One blank user', dict( | ||
403 | 274 | user_attributes=make_attributes( | ||
404 | 275 | {'User7': make_user()}), | ||
405 | 276 | expected='User7')), | ||
406 | 277 | ('Multiple blank users', dict( | ||
407 | 278 | user_attributes=make_attributes({ | ||
408 | 279 | 'User7': make_user(), | ||
409 | 280 | 'User8': make_user()}), | ||
410 | 281 | expected='User7')), | ||
411 | 282 | ('One not blank user', dict( | ||
412 | 283 | user_attributes=make_attributes( | ||
413 | 284 | {'User7': make_user({'Username': 'foo'})}), | ||
414 | 285 | expected=None)), | ||
415 | 286 | ('One enabled blank user', dict( | ||
416 | 287 | user_attributes=make_attributes({ | ||
417 | 288 | 'User7': {'Enable_User': 'Yes'}}), | ||
418 | 289 | expected='User7')), | ||
419 | 290 | ('Skip User1', dict( | ||
420 | 291 | user_attributes=make_attributes( | ||
421 | 292 | {'User1': make_user()}), | ||
422 | 293 | expected=None)) | ||
423 | 294 | ] | ||
424 | 295 | |||
425 | 296 | def fetch_user_record(self, user_number): | ||
426 | 297 | """Return the mock user data for a user_number.""" | ||
427 | 298 | return self.user_attributes[user_number] | ||
428 | 299 | |||
429 | 300 | def test_user_choice(self): | ||
430 | 301 | """Ensure the correct user, if any, is chosen.""" | ||
431 | 302 | |||
432 | 303 | mock = self.patch(maas_ipmi_autodetect, 'get_user_record') | ||
433 | 304 | mock.side_effect = self.fetch_user_record | ||
434 | 305 | current_users = self.user_attributes.keys() | ||
435 | 306 | user = pick_user_number_from_list('maas', current_users) | ||
436 | 307 | self.assertEqual(self.expected, user) | ||
437 | 308 | |||
438 | 309 | |||
439 | 310 | class TestPickUserNumber(MAASTestCase): | ||
440 | 311 | """Tests for pick_user_number().""" | ||
441 | 312 | |||
442 | 313 | def test_pick_user_number(self): | ||
443 | 314 | """Ensure proper listing and selection of a user.""" | ||
444 | 315 | lun_mock = self.patch(maas_ipmi_autodetect, 'list_user_numbers') | ||
445 | 316 | lun_mock.return_value = ['User1', 'User2'] | ||
446 | 317 | punfl_mock = self.patch(maas_ipmi_autodetect, | ||
447 | 318 | 'pick_user_number_from_list') | ||
448 | 319 | punfl_mock.return_value = 'User2' | ||
449 | 320 | user_number = pick_user_number('maas') | ||
450 | 321 | self.assertEqual('User2', user_number) | ||
451 | 322 | |||
452 | 323 | def test_fail_raise_exception(self): | ||
453 | 324 | """Ensure an exception is raised if no acceptable user is found.""" | ||
454 | 325 | mock = self.patch(maas_ipmi_autodetect, 'list_user_numbers') | ||
455 | 326 | mock.return_value = [] | ||
456 | 327 | self.assertRaises(IPMIUserError, pick_user_number, 'maas') |
Looks good, thanks again!
[1]
+ def test_runs_ bmc_config( self): maas_ipmi_ autodetect, 'run_command') section( section) call_args[ 0][0] l('bmc- config' , args[0])
+ """Ensure bmc-config is called with the correct args."""
+ recorder = self.patch(
+
+ section = 'foo'
+ bmc_get_
+ args = recorder.
+
+ self.assertEqua
+
+ for arg in ['--checkout', '--section', section]:
+ self.assertIn(arg, args)
The ordering is static so you can save some pain by using:
It'll also give a more useful error message if it fails.
I think there's also an assert_ called_ with() that matches any of
multiple calls, but if you need it you ought to check.
[2]
+ bgs_mock = Mock(return_ value=user_ text) maas_ipmi_ autodetect, 'bmc_get_section', bgs_mock) value=( user_number, user_record)) maas_ipmi_ autodetect, 'parse_section', ps_mock)
+ self.patch(
+ ps_mock = Mock(return_
+ self.patch(
self.patch() does some of the above for you. You could also write it as:
bgs_mock = self.patch( maas_ipmi_ autodetect, 'bmc_get_section')
bgs_mock. return_ value = user_text maas_ipmi_ autodetect, 'parse_section')
ps_mock. return_ value = (user_number, user_record)
ps_mock = self.patch(
It only saves the import of Mock, but you might like it.
[3]
+ self.assertEqua l(user_ number, bgs_mock. call_args[ 0][0]) l(user_ text, ps_mock. call_args[ 0][0])
+ self.assertEqua
Again, assert_ called_ once_with( ) is your friend.
If you need to match something with multiple arguments, but you can only
about one or two, there's ANY:
from mock import ANY
thing. assert_ called_ once_with( "important" , ANY, keyword=ANY)
There's also call(), which also works with ANY:
from mock import call
self.assertEqu al(
[call( 1, ANY), call(2, ANY)],
thing. call_args_ list)
[4]
+def disabled_ user(update= None):
+ """Record for a disabled user."""
+ base = {'Enable_User': 'No'}
+
+ if update:
+ base.update(update)
+
+ return base
Can you name this make_disabled_ user()? All test factories have the
prefix "make", except where they don't, er, they have the prefix "get".
[5]
+def base_attributes (attributes_ update= None): deepcopy( attributes_ template) )
+ """Base user records with updates in an OrderedDict."""
+
+ attributes_template = {
+ """Starting point for user records."""
+ 'User1': {
+ 'Enable_User': 'Yes'
+ },
+ 'User2': {
+ 'Username': 'admin',
+ 'Enable_User': 'Yes'
+ }
+ }
+
+ base = OrderedDict(
I don't think the deepcopy() is necessary.
[6]
+ if attributes_update: attributes_ update)
+ base.update(
Another habit we have is never relying on the implicit boolean-ness of
things in Python. Explicitly test for None instead:
if attributes_update is not None:
base.update( attributes_ update)
[7]
+ ('one_disabled_ not_blank_ user', dict(
Fwiw, scenario names don't have to be valid identifiers; they're
free-form text I ...