Merge lp:~frankban/juju-quickstart/interactive-jenvs into lp:juju-quickstart
- interactive-jenvs
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 110 |
Proposed branch: | lp:~frankban/juju-quickstart/interactive-jenvs |
Merge into: | lp:juju-quickstart |
Diff against target: |
1162 lines (+528/-104) 8 files modified
quickstart/cli/ui.py (+2/-0) quickstart/cli/views.py (+115/-19) quickstart/manage.py (+3/-3) quickstart/models/jenv.py (+61/-9) quickstart/tests/cli/test_views.py (+217/-40) quickstart/tests/helpers.py (+15/-3) quickstart/tests/models/test_jenv.py (+86/-16) quickstart/tests/test_manage.py (+29/-14) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/interactive-jenvs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+244976@code.launchpad.net |
Commit message
Description of the change
Display the jenv environments in interactive mode.
The jenv environments are now displayed in the
interactive session, and it's possible to use them.
Also implemented a simple jenv detail view in uwrid.
Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.
Tests: `make check`.
QA:
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to ~/.juju/
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.
Francesco Banconi (frankban) wrote : | # |
Richard Harding (rharding) wrote : | # |
LGTM no qa
j.c.sackett (jcsackett) wrote : | # |
LGTM, Franceso. Just one question, and it's not directly related to your
branch.
https:/
File quickstart/
https:/
quickstart/
This isn't from your branch, but why are we listing ec2 as an "unknown"?
Francesco Banconi (frankban) wrote : | # |
Thanks for the reviews!
https:/
File quickstart/
https:/
quickstart/
On 2014/12/17 16:13:07, j.c.sackett wrote:
> This isn't from your branch, but why are we listing ec2 as an
"unknown"?
All the jenv files created by "juju user add" don't include the provider
type. So the type can be unknown even if the name suggests a specific
provider.
j.c.sackett (jcsackett) wrote : | # |
QA notes:
I ran juju-quickstart -i locally.
I saw my ec2 setup with a check, as it is default.
My two local envs were listed, with the active one showing a green
bullet.
My manual provider env was listed with nothing special, as expected.
I simlinked the juju ci jenv into my environments; it was listed as an
imported environment, and I could see its details once selected.
QA OK.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Display the jenv environments in interactive mode.
The jenv environments are now displayed in the
interactive session, and it's possible to use them.
Also implemented a simple jenv detail view in uwrid.
Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.
Tests: `make check`.
QA:
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to
~/.juju/
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.
R=rharding, j.c.sackett
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/cli/ui.py' |
2 | --- quickstart/cli/ui.py 2014-01-07 15:41:55 +0000 |
3 | +++ quickstart/cli/ui.py 2014-12-17 12:34:18 +0000 |
4 | @@ -31,6 +31,8 @@ |
5 | # See <http://excess.org/urwid/docs/reference/constants.html |
6 | # foreground-and-background-colors>. |
7 | (None, 'light gray', 'black'), |
8 | + ('active', 'light green', 'black'), |
9 | + ('active status', 'dark green', 'light gray'), |
10 | ('dialog', 'dark gray', 'light gray'), |
11 | ('dialog header', 'light gray,bold', 'dark blue'), |
12 | ('control alert', 'light red', 'light gray'), |
13 | |
14 | === modified file 'quickstart/cli/views.py' |
15 | --- quickstart/cli/views.py 2014-11-10 14:11:11 +0000 |
16 | +++ quickstart/cli/views.py 2014-12-17 12:34:18 +0000 |
17 | @@ -115,7 +115,10 @@ |
18 | forms, |
19 | ui, |
20 | ) |
21 | -from quickstart.models import envs |
22 | +from quickstart.models import ( |
23 | + envs, |
24 | + jenv, |
25 | +) |
26 | |
27 | |
28 | def show(view, *args): |
29 | @@ -152,7 +155,7 @@ |
30 | raise ui.AppExit((env_db, env_data)) |
31 | |
32 | |
33 | -def env_index(app, env_type_db, env_db, save_callable): |
34 | +def env_index(app, env_type_db, env_db, jenv_db, save_callable): |
35 | """Show the Juju environments list. |
36 | |
37 | The env_detail view is displayed when the user clicks on an environment. |
38 | @@ -162,17 +165,22 @@ |
39 | Receives: |
40 | - env_type_db: the environments meta information; |
41 | - env_db: the environments database; |
42 | + - jenv_db: the jenv files database; |
43 | - save_callable: a function called to save a new environment database. |
44 | """ |
45 | + # XXX frankban 16/12/2014: this function is too long, subdivide it. |
46 | env_db = copy.deepcopy(env_db) |
47 | + jenv_db = copy.deepcopy(jenv_db) |
48 | # All the environment views return a tuple (new_env_db, env_data). |
49 | # Set the env_data to None in the case the user quits the application |
50 | # without selecting an environment to use. |
51 | app.set_return_value_on_exit((env_db, None)) |
52 | detail_view = functools.partial( |
53 | - env_detail, app, env_type_db, env_db, save_callable) |
54 | + env_detail, app, env_type_db, env_db, jenv_db, save_callable) |
55 | + jenv_view = functools.partial( |
56 | + jenv_detail, app, env_type_db, env_db, jenv_db, save_callable) |
57 | edit_view = functools.partial( |
58 | - env_edit, app, env_type_db, env_db, save_callable) |
59 | + env_edit, app, env_type_db, env_db, jenv_db, save_callable) |
60 | # Alphabetically sort the existing environments. |
61 | environments = sorted([ |
62 | envs.get_env_data(env_db, env_name) |
63 | @@ -269,8 +277,9 @@ |
64 | # "juju status" for each environment in the list, which is expensive and |
65 | # time consuming. |
66 | focus_position = None |
67 | - errors_found = default_found = False |
68 | + active_found = default_found = errors_found = False |
69 | existing_widgets_num = len(widgets) |
70 | + remaining_jenv_db = copy.deepcopy(jenv_db) |
71 | for position, env_data in enumerate(environments): |
72 | bullet = '\N{BULLET}' |
73 | # Is this environment the default one? |
74 | @@ -279,22 +288,48 @@ |
75 | # The first two positions are the section header and the divider. |
76 | focus_position = position + existing_widgets_num |
77 | bullet = '\N{CHECK MARK}' |
78 | - # Is this environment valid? |
79 | - env_metadata = envs.get_env_metadata(env_type_db, env_data) |
80 | - errors = envs.validate(env_metadata, env_data) |
81 | - if errors: |
82 | - errors_found = True |
83 | - bullet = ('error', bullet) |
84 | + if remaining_jenv_db['environments'].pop(env_data['name'], None): |
85 | + # This is an active environment. Is it running? Who knows... |
86 | + active_found = True |
87 | + bullet = ('active', bullet) |
88 | + else: |
89 | + # Check if this environment is valid. |
90 | + env_metadata = envs.get_env_metadata(env_type_db, env_data) |
91 | + errors = envs.validate(env_metadata, env_data) |
92 | + if errors: |
93 | + errors_found = True |
94 | + bullet = ('error', bullet) |
95 | # Create a label for the environment. |
96 | env_short_description = envs.get_env_short_description(env_data) |
97 | text = [bullet, ' {}'.format(env_short_description)] |
98 | widgets.append(ui.MenuButton(text, ui.thunk(detail_view, env_data))) |
99 | |
100 | + # Alphabetically sort the remaining environments not included in the |
101 | + # environments.yaml file. |
102 | + environments = sorted([ |
103 | + envs.get_env_data(remaining_jenv_db, env_name) |
104 | + for env_name in remaining_jenv_db['environments'] |
105 | + ], key=operator.itemgetter('name')) |
106 | + |
107 | + # List the remaining active environments. Those environments are not |
108 | + # included in the environments.yaml file: they are probably imported and |
109 | + # supposed to be working/active. The user has the ability to select them. |
110 | + widgets.extend([ |
111 | + urwid.Divider(), |
112 | + urwid.Text(('highlight', 'Other active environments')), |
113 | + urwid.Text('(imported/not included in your environments.yaml file):'), |
114 | + urwid.Divider(), |
115 | + ]) |
116 | + bullet = ('active', '\N{BULLET}') |
117 | + for env_data in environments: |
118 | + env_short_description = jenv.get_env_short_description(env_data) |
119 | + text = [bullet, ' {}'.format(env_short_description)] |
120 | + widgets.append(ui.MenuButton(text, ui.thunk(jenv_view, env_data))) |
121 | + |
122 | # Set up the "create a new environment" section. |
123 | widgets.extend([ |
124 | urwid.Divider(), |
125 | - urwid.Text(( |
126 | - 'highlight', 'Create a new environment:')), |
127 | + urwid.Text(('highlight', 'Create a new environment:')), |
128 | urwid.Divider(), |
129 | ]) |
130 | # The Juju GUI can be safely installed in the bootstrap node only if its |
131 | @@ -322,6 +357,8 @@ |
132 | status = [' \N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW} navigate '] |
133 | if default_found: |
134 | status.append(' \N{CHECK MARK} default ') |
135 | + if active_found: |
136 | + status.extend([('active status', ' \N{BULLET}'), ' active ']) |
137 | if errors_found: |
138 | status.extend([('error status', ' \N{BULLET}'), ' has errors ']) |
139 | app.set_status(status) |
140 | @@ -332,7 +369,7 @@ |
141 | app.set_contents(contents) |
142 | |
143 | |
144 | -def env_detail(app, env_type_db, env_db, save_callable, env_data): |
145 | +def env_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data): |
146 | """Show details on a Juju environment. |
147 | |
148 | From this view it is possible to start the environment, set it as default, |
149 | @@ -341,18 +378,20 @@ |
150 | Receives: |
151 | - env_type_db: the environments meta information; |
152 | - env_db: the environments database; |
153 | + - jenv_db: the jenv files database; |
154 | - save_callable: a function called to save a new environment database; |
155 | - env_data: the environment data. |
156 | """ |
157 | env_db = copy.deepcopy(env_db) |
158 | + jenv_db = copy.deepcopy(jenv_db) |
159 | # All the environment views return a tuple (new_env_db, env_data). |
160 | # Set the env_data to None in the case the user quits the application |
161 | # without selecting an environment to use. |
162 | app.set_return_value_on_exit((env_db, None)) |
163 | index_view = functools.partial( |
164 | - env_index, app, env_type_db, env_db, save_callable) |
165 | + env_index, app, env_type_db, env_db, jenv_db, save_callable) |
166 | edit_view = functools.partial( |
167 | - env_edit, app, env_type_db, env_db, save_callable, env_data) |
168 | + env_edit, app, env_type_db, env_db, jenv_db, save_callable, env_data) |
169 | |
170 | def use(env_data): |
171 | # Quit the interactive session returning the (possibly modified) |
172 | @@ -425,7 +464,62 @@ |
173 | app.set_contents(listbox) |
174 | |
175 | |
176 | -def env_edit(app, env_type_db, env_db, save_callable, env_data): |
177 | +def jenv_detail(app, env_type_db, env_db, jenv_db, save_callable, env_data): |
178 | + """Show details on a Juju imported environment. |
179 | + |
180 | + The environment is not included in the environments.yaml file, but just |
181 | + found in the jenv database. |
182 | + From this view it is possible to start the environment. |
183 | + |
184 | + Receives: |
185 | + - env_type_db: the environments meta information; |
186 | + - env_db: the environments database; |
187 | + - jenv_db: the jenv files database; |
188 | + - save_callable: a function called to save a new environment database; |
189 | + - env_data: the environment data. |
190 | + """ |
191 | + env_db = copy.deepcopy(env_db) |
192 | + jenv_db = copy.deepcopy(jenv_db) |
193 | + # All the environment views return a tuple (new_env_db, env_data). |
194 | + # Set the env_data to None in the case the user quits the application |
195 | + # without selecting an environment to use. |
196 | + app.set_return_value_on_exit((env_db, None)) |
197 | + index_view = functools.partial( |
198 | + env_index, app, env_type_db, env_db, jenv_db, save_callable) |
199 | + |
200 | + def use(env_data): |
201 | + # Quit the interactive session returning the (possibly modified) |
202 | + # environment database and the environment data corresponding to the |
203 | + # selected environment. |
204 | + raise ui.AppExit((env_db, env_data)) |
205 | + |
206 | + app.set_title(jenv.get_env_short_description(env_data)) |
207 | + widgets = [] |
208 | + for key, value in jenv.get_env_details(env_data): |
209 | + widgets.append(urwid.Text(['{}: '.format(key), ('highlight', value)])) |
210 | + widgets.extend([ |
211 | + urwid.Divider(), |
212 | + urwid.Text([ |
213 | + ('highlight', 'Imported active environment.\n'), |
214 | + 'This environment is not included in your environments.yaml file.' |
215 | + '\nFor this reason, it is not possible to edit or remove it.\n' |
216 | + 'However, you can use the link below to ', |
217 | + ('highlight', 'use Juju Quickstart'), |
218 | + ' with this environment.', |
219 | + ]), |
220 | + ]) |
221 | + |
222 | + controls = [ |
223 | + ui.MenuButton('back', ui.thunk(index_view)), |
224 | + ui.MenuButton('use', ui.thunk(use, env_data)), |
225 | + ] |
226 | + widgets.append(ui.create_controls(*controls)) |
227 | + listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets)) |
228 | + app.set_contents(listbox) |
229 | + app.set_status([' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate ']) |
230 | + |
231 | + |
232 | +def env_edit(app, env_type_db, env_db, jenv_db, save_callable, env_data): |
233 | """Create or modify a Juju environment. |
234 | |
235 | This view displays an edit form allowing for environment |
236 | @@ -435,6 +529,7 @@ |
237 | Receives: |
238 | - env_type_db: the environments meta information; |
239 | - env_db: the environments database; |
240 | + - jenv_db: the jenv files database; |
241 | - save_callable: a function called to save a new environment database; |
242 | - env_data: the environment data. |
243 | |
244 | @@ -444,15 +539,16 @@ |
245 | env_data includes the "name" key and all the other environment info. |
246 | """ |
247 | env_db = copy.deepcopy(env_db) |
248 | + jenv_db = copy.deepcopy(jenv_db) |
249 | # All the environment views return a tuple (new_env_db, env_data). |
250 | # Set the env_data to None in the case the user quits the application |
251 | # without selecting an environment to use. |
252 | app.set_return_value_on_exit((env_db, None)) |
253 | env_metadata = envs.get_env_metadata(env_type_db, env_data) |
254 | index_view = functools.partial( |
255 | - env_index, app, env_type_db, env_db, save_callable) |
256 | + env_index, app, env_type_db, env_db, jenv_db, save_callable) |
257 | detail_view = functools.partial( |
258 | - env_detail, app, env_type_db, env_db, save_callable) |
259 | + env_detail, app, env_type_db, env_db, jenv_db, save_callable) |
260 | if 'name' in env_data: |
261 | exists = True |
262 | title = 'Edit the {} environment' |
263 | |
264 | === modified file 'quickstart/manage.py' |
265 | --- quickstart/manage.py 2014-12-16 14:12:44 +0000 |
266 | +++ quickstart/manage.py 2014-12-17 12:34:18 +0000 |
267 | @@ -203,7 +203,7 @@ |
268 | return save_callable |
269 | |
270 | |
271 | -def _start_interactive_session(parser, env_type_db, env_db, env_file): |
272 | +def _start_interactive_session(parser, env_type_db, env_db, jenv_db, env_file): |
273 | """Start the Urwid interactive session. |
274 | |
275 | Return the env_data corresponding to the user selected environment. |
276 | @@ -212,7 +212,7 @@ |
277 | """ |
278 | save_callable = _create_save_callable(parser, env_file) |
279 | new_env_db, env_data = views.show( |
280 | - views.env_index, env_type_db, env_db, save_callable) |
281 | + views.env_index, env_type_db, env_db, jenv_db, save_callable) |
282 | if new_env_db != env_db: |
283 | print('changes to the environments file have been saved') |
284 | if env_data is None: |
285 | @@ -290,7 +290,7 @@ |
286 | if interactive: |
287 | # Start the interactive session. |
288 | env_data = _start_interactive_session( |
289 | - parser, env_type_db, env_db, env_file) |
290 | + parser, env_type_db, env_db, jenv_db, env_file) |
291 | else: |
292 | # This is a non-interactive session and we need to validate the |
293 | # selected environment before proceeding. |
294 | |
295 | === modified file 'quickstart/models/jenv.py' |
296 | --- quickstart/models/jenv.py 2014-12-16 15:53:25 +0000 |
297 | +++ quickstart/models/jenv.py 2014-12-17 12:34:18 +0000 |
298 | @@ -130,9 +130,11 @@ |
299 | When a jenv file is created using "juju user add", the resulting YAML data |
300 | is very concise, not even including the environment type. |
301 | For this reason, the environment database returned by this function does |
302 | - not contain the usual fields used as bootstrap options, but just the |
303 | - environment name and the type. If the environment type is not included in |
304 | - the jenv, UNKNOWN_ENV_TYPE is used. |
305 | + not contain the usual fields used as bootstrap options. |
306 | + The included fields are: "name", "type", "user" and "state-servers". |
307 | + |
308 | + If the environment type is not included in the jenv, UNKNOWN_ENV_TYPE is |
309 | + used. |
310 | """ |
311 | db = {'environments': {}} |
312 | path = os.path.expanduser(os.path.join(settings.JUJU_HOME, 'environments')) |
313 | @@ -153,7 +155,7 @@ |
314 | # Validate the jenv contents. |
315 | try: |
316 | data = serializers.yaml_load_from_path(fullpath) |
317 | - validate(data) |
318 | + credentials, servers = validate(data) |
319 | except ValueError as err: |
320 | logging.warn('ignoring invalid jenv file {}: {}'. format( |
321 | filename, bytes(err).decode('utf-8'))) |
322 | @@ -164,7 +166,11 @@ |
323 | except ValueError: |
324 | # This is expected when a jenv is generated with "juju user add". |
325 | env_type = UNKNOWN_ENV_TYPE |
326 | - environments[name] = {'type': env_type} |
327 | + environments[name] = { |
328 | + 'type': env_type, |
329 | + 'user': credentials[0], |
330 | + 'state-servers': servers, |
331 | + } |
332 | return db |
333 | |
334 | |
335 | @@ -178,13 +184,59 @@ |
336 | - the environment file is not found; |
337 | - the environment file contents are not parsable by YAML; |
338 | - the environment data is not valid. |
339 | - """ |
340 | - _get_credentials(data) |
341 | + |
342 | + Return the environment credentials and state servers. |
343 | + """ |
344 | + credentials = _get_credentials(data) |
345 | + servers = _get_state_servers(data) |
346 | + if not len(servers): |
347 | + raise ValueError(b'no state-servers found') |
348 | + return credentials, servers |
349 | + |
350 | + |
351 | +def get_env_short_description(env_data): |
352 | + """Return a short description of the given env_data. |
353 | + |
354 | + The given env_data must include at least the "name" and "type" keys. |
355 | + """ |
356 | + parts = [env_data['name']] |
357 | + env_type = env_data['type'] |
358 | + if env_type != UNKNOWN_ENV_TYPE: |
359 | + parts.append('(type: {})'.format(env_type)) |
360 | + return ' '.join(parts) |
361 | + |
362 | + |
363 | +def get_env_details(env_data): |
364 | + """Return the environment details as a sequence of tuples (label, value). |
365 | + |
366 | + In each tuple, the label is the field name, and the value is the |
367 | + corresponding value in the given env_data. |
368 | + """ |
369 | + details = [] |
370 | + # Add the environment type. |
371 | + env_type = env_data['type'] |
372 | + if env_type != UNKNOWN_ENV_TYPE: |
373 | + details.append(('type', env_type)) |
374 | + # Add the environment name and user. |
375 | + details.extend([ |
376 | + ('name', env_data['name']), |
377 | + ('user', env_data['user']), |
378 | + ]) |
379 | + # Add the state servers. |
380 | + servers = ', '.join(env_data['state-servers']) |
381 | + details.append(('state servers', servers)) |
382 | + return details |
383 | + |
384 | + |
385 | +def _get_state_servers(data): |
386 | + """Return a tuple of Juju state servers for the given jenv data. |
387 | + |
388 | + Raise a ValueError if the state servers cannot be retrieved. |
389 | + """ |
390 | servers = data.get('state-servers') |
391 | if not isinstance(servers, (list, tuple)): |
392 | raise ValueError(b'invalid state-servers field') |
393 | - if not len(servers): |
394 | - raise ValueError(b'no state-servers found') |
395 | + return tuple(servers) |
396 | |
397 | |
398 | def _get_value_from_yaml(data, *args): |
399 | |
400 | === modified file 'quickstart/tests/cli/test_views.py' |
401 | --- quickstart/tests/cli/test_views.py 2014-11-12 15:41:40 +0000 |
402 | +++ quickstart/tests/cli/test_views.py 2014-12-17 12:34:18 +0000 |
403 | @@ -34,7 +34,10 @@ |
404 | ui, |
405 | views, |
406 | ) |
407 | -from quickstart.models import envs |
408 | +from quickstart.models import ( |
409 | + envs, |
410 | + jenv, |
411 | +) |
412 | from quickstart.tests import helpers |
413 | from quickstart.tests.cli import helpers as cli_helpers |
414 | |
415 | @@ -178,7 +181,9 @@ |
416 | # a tuple including a copy of the given env_db and None, the latter |
417 | # meaning no environment has been selected. |
418 | env_db = helpers.make_env_db() |
419 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
420 | + jenv_db = helpers.make_jenv_db() |
421 | + views.env_index( |
422 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
423 | new_env_db, env_data = self.get_on_exit_return_value(self.loop) |
424 | self.assertEqual(env_db, new_env_db) |
425 | self.assertIsNot(env_db, new_env_db) |
426 | @@ -187,7 +192,9 @@ |
427 | def test_view_title(self): |
428 | # The application title is correctly set up. |
429 | env_db = helpers.make_env_db() |
430 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
431 | + jenv_db = helpers.make_jenv_db() |
432 | + views.env_index( |
433 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
434 | self.assertEqual( |
435 | 'Select an existing Juju environment or create a new one', |
436 | self.app.get_title()) |
437 | @@ -195,7 +202,9 @@ |
438 | def test_view_title_no_environments(self): |
439 | # The application title changes if the env_db has no environments. |
440 | env_db = {'environments': {}} |
441 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
442 | + jenv_db = helpers.make_jenv_db() |
443 | + views.env_index( |
444 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
445 | self.assertEqual( |
446 | 'No Juju environments already set up: please create one', |
447 | self.app.get_title()) |
448 | @@ -204,9 +213,11 @@ |
449 | # The view displays a list of the environments in env_db, and buttons |
450 | # to create new environments. |
451 | env_db = helpers.make_env_db() |
452 | + jenv_db = {'environments': {}} |
453 | with local_envs_supported(True): |
454 | views.env_index( |
455 | - self.app, self.env_type_db, env_db, self.save_callable) |
456 | + self.app, self.env_type_db, env_db, jenv_db, |
457 | + self.save_callable) |
458 | buttons = self.get_widgets_in_contents( |
459 | filter_function=self.is_a(ui.MenuButton)) |
460 | # A button is created for each existing environment (see details) and |
461 | @@ -215,13 +226,39 @@ |
462 | expected_buttons_number = len(env_db['environments']) + len(env_types) |
463 | self.assertEqual(expected_buttons_number, len(buttons)) |
464 | |
465 | + def test_view_contents_with_imported_envs(self): |
466 | + # The view displays a list of active imported environments, and buttons |
467 | + # to create new environments. |
468 | + env_db = {'environments': {}} |
469 | + jenv_db = helpers.make_jenv_db() |
470 | + with local_envs_supported(True): |
471 | + views.env_index( |
472 | + self.app, self.env_type_db, env_db, jenv_db, |
473 | + self.save_callable) |
474 | + buttons = self.get_widgets_in_contents( |
475 | + filter_function=self.is_a(ui.MenuButton)) |
476 | + # A button is created for each existing environment (see details) and |
477 | + # for each environment type supported by quickstart (create). |
478 | + env_types = envs.get_supported_env_types(self.env_type_db) |
479 | + expected_buttons_number = ( |
480 | + # The number of active environments. |
481 | + len(jenv_db['environments']) + |
482 | + # The buttons to create new environments. |
483 | + len(env_types) + |
484 | + # The button to automatically create a new local environment. |
485 | + 1 |
486 | + ) |
487 | + self.assertEqual(expected_buttons_number, len(buttons)) |
488 | + |
489 | def test_new_local_environment_disabled(self): |
490 | # The option to create a new local environment is not present if they |
491 | # are not supported in the current platform. |
492 | env_db = helpers.make_env_db() |
493 | + jenv_db = helpers.make_jenv_db() |
494 | with local_envs_supported(False): |
495 | views.env_index( |
496 | - self.app, self.env_type_db, env_db, self.save_callable) |
497 | + self.app, self.env_type_db, env_db, jenv_db, |
498 | + self.save_callable) |
499 | buttons = self.get_widgets_in_contents( |
500 | filter_function=self.is_a(ui.MenuButton)) |
501 | captions = map(cli_helpers.get_button_caption, buttons) |
502 | @@ -235,7 +272,9 @@ |
503 | def test_environment_clicked(self, mock_env_detail): |
504 | # The environment detail view is called when clicking an environment. |
505 | env_db = helpers.make_env_db() |
506 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
507 | + jenv_db = {'environments': {}} |
508 | + views.env_index( |
509 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
510 | buttons = self.get_widgets_in_contents( |
511 | filter_function=self.is_a(ui.MenuButton)) |
512 | # The environments are listed in alphabetical order. |
513 | @@ -250,20 +289,51 @@ |
514 | # corresponding environment data. |
515 | cli_helpers.emit(button) |
516 | mock_env_detail.assert_called_once_with( |
517 | - self.app, self.env_type_db, env_db, self.save_callable, |
518 | - env_data) |
519 | + self.app, self.env_type_db, env_db, jenv_db, |
520 | + self.save_callable, env_data) |
521 | # Reset the mock so that it does not include any calls on the next |
522 | # loop cycle. |
523 | mock_env_detail.reset_mock() |
524 | |
525 | + @mock.patch('quickstart.cli.views.jenv_detail') |
526 | + def test_imported_environment_clicked(self, mock_jenv_detail): |
527 | + # The jenv detail view is called when clicking an imported environment. |
528 | + env_db = {'environments': {}} |
529 | + jenv_db = helpers.make_jenv_db() |
530 | + with local_envs_supported(False): |
531 | + views.env_index( |
532 | + self.app, self.env_type_db, env_db, jenv_db, |
533 | + self.save_callable) |
534 | + buttons = self.get_widgets_in_contents( |
535 | + filter_function=self.is_a(ui.MenuButton)) |
536 | + # The environments are listed in alphabetical order. |
537 | + environments = sorted(jenv_db['environments']) |
538 | + for env_name, button in zip(environments, buttons): |
539 | + env_data = envs.get_env_data(jenv_db, env_name) |
540 | + # The caption includes the environment description. |
541 | + env_description = jenv.get_env_short_description(env_data) |
542 | + self.assertIn( |
543 | + env_description, cli_helpers.get_button_caption(button)) |
544 | + # When the button is clicked, the jenv detail view is called |
545 | + # passing the corresponding environment data. |
546 | + cli_helpers.emit(button) |
547 | + mock_jenv_detail.assert_called_once_with( |
548 | + self.app, self.env_type_db, env_db, jenv_db, |
549 | + self.save_callable, env_data) |
550 | + # Reset the mock so that it does not include any calls on the next |
551 | + # loop cycle. |
552 | + mock_jenv_detail.reset_mock() |
553 | + |
554 | @mock.patch('quickstart.cli.views.env_edit') |
555 | def test_create_new_environment_clicked(self, mock_env_edit): |
556 | # The environment edit view is called when clicking to create a new |
557 | # environment. |
558 | env_db = helpers.make_env_db() |
559 | + jenv_db = {'environments': {}} |
560 | with local_envs_supported(True): |
561 | views.env_index( |
562 | - self.app, self.env_type_db, env_db, self.save_callable) |
563 | + self.app, self.env_type_db, env_db, jenv_db, |
564 | + self.save_callable) |
565 | buttons = self.get_widgets_in_contents( |
566 | filter_function=self.is_a(ui.MenuButton)) |
567 | env_types = envs.get_supported_env_types(self.env_type_db) |
568 | @@ -277,9 +347,11 @@ |
569 | # corresponding environment data. |
570 | cli_helpers.emit(button) |
571 | mock_env_edit.assert_called_once_with( |
572 | - self.app, self.env_type_db, env_db, self.save_callable, |
573 | - {'type': env_type, |
574 | - 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1]}) |
575 | + self.app, self.env_type_db, env_db, jenv_db, |
576 | + self.save_callable, { |
577 | + 'type': env_type, |
578 | + 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1], |
579 | + }) |
580 | # Reset the mock so that it does not include any calls on the next |
581 | # loop cycle. |
582 | mock_env_edit.reset_mock() |
583 | @@ -290,10 +362,12 @@ |
584 | # If that option is clicked, the view quits the application returning |
585 | # the newly created env_data. |
586 | env_db = envs.create_empty_env_db() |
587 | + jenv_db = helpers.make_jenv_db() |
588 | with maas_env_detected(False): |
589 | with local_envs_supported(True): |
590 | views.env_index( |
591 | - self.app, self.env_type_db, env_db, self.save_callable) |
592 | + self.app, self.env_type_db, env_db, jenv_db, |
593 | + self.save_callable) |
594 | buttons = self.get_widgets_in_contents( |
595 | filter_function=self.is_a(ui.MenuButton)) |
596 | # The "create and bootstrap" button is the first one in the contents. |
597 | @@ -314,9 +388,11 @@ |
598 | # environment is not displayed if the current platform does not support |
599 | # local environments. |
600 | env_db = envs.create_empty_env_db() |
601 | + jenv_db = helpers.make_jenv_db() |
602 | with local_envs_supported(False): |
603 | views.env_index( |
604 | - self.app, self.env_type_db, env_db, self.save_callable) |
605 | + self.app, self.env_type_db, env_db, jenv_db, |
606 | + self.save_callable) |
607 | buttons = self.get_widgets_in_contents( |
608 | filter_function=self.is_a(ui.MenuButton)) |
609 | # No "create and bootstrap local" buttons are present. |
610 | @@ -330,9 +406,11 @@ |
611 | # If that option is clicked, the view quits the application returning |
612 | # the newly created env_data. |
613 | env_db = envs.create_empty_env_db() |
614 | + jenv_db = helpers.make_jenv_db() |
615 | with maas_env_detected(True): |
616 | views.env_index( |
617 | - self.app, self.env_type_db, env_db, self.save_callable) |
618 | + self.app, self.env_type_db, env_db, jenv_db, |
619 | + self.save_callable) |
620 | buttons = self.get_widgets_in_contents( |
621 | filter_function=self.is_a(ui.MenuButton)) |
622 | # The "create and bootstrap" button is the first one in the contents. |
623 | @@ -353,9 +431,11 @@ |
624 | # environment is not displayed if no MAAS API endpoints are |
625 | # available on the system |
626 | env_db = envs.create_empty_env_db() |
627 | + jenv_db = helpers.make_jenv_db() |
628 | with maas_env_detected(False): |
629 | views.env_index( |
630 | - self.app, self.env_type_db, env_db, self.save_callable) |
631 | + self.app, self.env_type_db, env_db, jenv_db, |
632 | + self.save_callable) |
633 | buttons = self.get_widgets_in_contents( |
634 | filter_function=self.is_a(ui.MenuButton)) |
635 | # No "create and bootstrap MAAS" buttons are present. |
636 | @@ -365,7 +445,9 @@ |
637 | def test_selected_environment(self): |
638 | # The default environment is already selected in the list. |
639 | env_db = helpers.make_env_db(default='lxc') |
640 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
641 | + jenv_db = helpers.make_jenv_db() |
642 | + views.env_index( |
643 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
644 | env_data = envs.get_env_data(env_db, 'lxc') |
645 | env_description = envs.get_env_short_description(env_data) |
646 | contents = self.app.get_contents() |
647 | @@ -377,31 +459,50 @@ |
648 | def test_status_with_errors(self): |
649 | # The status message explains how errors are displayed. |
650 | env_db = helpers.make_env_db() |
651 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
652 | + jenv_db = {'environments': {}} |
653 | + views.env_index( |
654 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
655 | status = self.app.get_status() |
656 | self.assertEqual(self.base_status + ' \N{BULLET} has errors ', status) |
657 | |
658 | def test_status_with_default(self): |
659 | # The status message explains how default environment is represented. |
660 | env_db = helpers.make_env_db(default='lxc', exclude_invalid=True) |
661 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
662 | + jenv_db = {'environments': {}} |
663 | + views.env_index( |
664 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
665 | status = self.app.get_status() |
666 | self.assertEqual(self.base_status + ' \N{CHECK MARK} default ', status) |
667 | |
668 | - def test_status_with_default_and_errors(self): |
669 | - # The status message includes both default and errors explanations. |
670 | + def test_status_with_active(self): |
671 | + # The status message explains how active environments are displayed. |
672 | + env_db = helpers.make_env_db(exclude_invalid=True) |
673 | + jenv_db = helpers.make_jenv_db() |
674 | + views.env_index( |
675 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
676 | + status = self.app.get_status() |
677 | + self.assertEqual(self.base_status + ' \N{BULLET} active ', status) |
678 | + |
679 | + def test_complete_status(self): |
680 | + # The status message includes default, active and errors explanations. |
681 | env_db = helpers.make_env_db(default='lxc') |
682 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
683 | + jenv_db = helpers.make_jenv_db() |
684 | + views.env_index( |
685 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
686 | status = self.app.get_status() |
687 | self.assertEqual( |
688 | self.base_status + |
689 | - ' \N{CHECK MARK} default \N{BULLET} has errors ', |
690 | + ' \N{CHECK MARK} default ' + |
691 | + ' \N{BULLET} active ' + |
692 | + ' \N{BULLET} has errors ', |
693 | status) |
694 | |
695 | - def test_status(self): |
696 | + def test_base_status(self): |
697 | # The status only includes navigation info if there are no errors. |
698 | env_db = helpers.make_env_db(exclude_invalid=True) |
699 | - views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
700 | + jenv_db = {'environments': {}} |
701 | + views.env_index( |
702 | + self.app, self.env_type_db, env_db, jenv_db, self.save_callable) |
703 | status = self.app.get_status() |
704 | self.assertEqual(self.base_status, status) |
705 | |
706 | @@ -410,13 +511,14 @@ |
707 | |
708 | base_status = ' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate ' |
709 | env_db = helpers.make_env_db(default='lxc') |
710 | + jenv_db = helpers.make_jenv_db() |
711 | |
712 | def call_view(self, env_name='lxc'): |
713 | """Call the view passing the env_data corresponding to env_name.""" |
714 | self.env_data = envs.get_env_data(self.env_db, env_name) |
715 | return views.env_detail( |
716 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
717 | - self.env_data) |
718 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
719 | + self.save_callable, self.env_data) |
720 | |
721 | def test_view_default_return_value_on_exit(self): |
722 | # The view configures the app so that the return value on user exit is |
723 | @@ -483,7 +585,8 @@ |
724 | back_button = self.get_control_buttons()[0] |
725 | cli_helpers.emit(back_button) |
726 | mock_env_index.assert_called_once_with( |
727 | - self.app, self.env_type_db, self.env_db, self.save_callable) |
728 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
729 | + self.save_callable) |
730 | |
731 | def test_use_button(self): |
732 | # The application exits if the "use" button is clicked. |
733 | @@ -522,8 +625,8 @@ |
734 | edit_button = self.get_control_buttons()[3] |
735 | cli_helpers.emit(edit_button) |
736 | mock_env_edit.assert_called_once_with( |
737 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
738 | - self.env_data) |
739 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
740 | + self.save_callable, self.env_data) |
741 | |
742 | def test_remove_button(self): |
743 | # A confirmation dialog is displayed if the "remove" button is clicked. |
744 | @@ -598,9 +701,82 @@ |
745 | self.assertEqual(self.base_status, status) |
746 | |
747 | |
748 | +class TestJenvDetail(EnvViewTestsMixin, unittest.TestCase): |
749 | + |
750 | + env_db = helpers.make_env_db(default='lxc') |
751 | + jenv_db = helpers.make_jenv_db() |
752 | + |
753 | + def call_view(self, env_name='lxc'): |
754 | + """Call the view passing the env_data corresponding to env_name.""" |
755 | + self.env_data = envs.get_env_data(self.jenv_db, env_name) |
756 | + return views.jenv_detail( |
757 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
758 | + self.save_callable, self.env_data) |
759 | + |
760 | + def test_view_default_return_value_on_exit(self): |
761 | + # The view configures the app so that the return value on user exit is |
762 | + # a tuple including a copy of the given env_db and None, the latter |
763 | + # meaning no environment has been selected (for now). |
764 | + self.call_view() |
765 | + new_env_db, env_data = self.get_on_exit_return_value(self.loop) |
766 | + self.assertEqual(self.env_db, new_env_db) |
767 | + self.assertIsNot(self.env_db, new_env_db) |
768 | + self.assertIsNone(env_data) |
769 | + |
770 | + def test_view_title(self): |
771 | + # The application title is correctly set up: it shows the description |
772 | + # of the current jenv environment. |
773 | + self.call_view() |
774 | + env_description = jenv.get_env_short_description(self.env_data) |
775 | + self.assertEqual(env_description, self.app.get_title()) |
776 | + |
777 | + def test_view_contents(self): |
778 | + # The view displays the jenv details. |
779 | + self.call_view() |
780 | + widgets = self.get_widgets_in_contents( |
781 | + filter_function=self.is_a(urwid.Text)) |
782 | + expected_texts = [ |
783 | + '{}: {}'.format(label, value) for label, value |
784 | + in jenv.get_env_details(self.env_data) |
785 | + ] |
786 | + for expected_text, widget in zip(expected_texts, widgets): |
787 | + self.assertEqual(expected_text, widget.text) |
788 | + |
789 | + def test_view_buttons(self): |
790 | + # The "back" and "use" buttons are displayed. |
791 | + self.call_view(env_name='ec2-west') |
792 | + buttons = self.get_control_buttons() |
793 | + captions = map(cli_helpers.get_button_caption, buttons) |
794 | + self.assertEqual(['back', 'use'], captions) |
795 | + |
796 | + @mock.patch('quickstart.cli.views.env_index') |
797 | + def test_back_button(self, mock_env_index): |
798 | + # The index view is called if the "back" button is clicked. |
799 | + self.call_view(env_name='ec2-west') |
800 | + # The "back" button is the first one. |
801 | + back_button = self.get_control_buttons()[0] |
802 | + cli_helpers.emit(back_button) |
803 | + mock_env_index.assert_called_once_with( |
804 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
805 | + self.save_callable) |
806 | + |
807 | + def test_use_button(self): |
808 | + # The application exits if the "use" button is clicked. |
809 | + # The env_db and the current environment data are returned. |
810 | + self.call_view(env_name='ec2-west') |
811 | + # The "use" button is the second one. |
812 | + use_button = self.get_control_buttons()[1] |
813 | + with self.assertRaises(ui.AppExit) as context_manager: |
814 | + cli_helpers.emit(use_button) |
815 | + expected_return_value = (self.env_db, self.env_data) |
816 | + self.assertEqual( |
817 | + expected_return_value, context_manager.exception.return_value) |
818 | + |
819 | + |
820 | class TestEnvEdit(EnvViewTestsMixin, unittest.TestCase): |
821 | |
822 | env_db = helpers.make_env_db(default='lxc') |
823 | + jenv_db = helpers.make_jenv_db() |
824 | |
825 | def call_view(self, env_name='lxc', env_type=None): |
826 | """Call the view passing the env_data corresponding to env_name. |
827 | @@ -613,8 +789,8 @@ |
828 | else: |
829 | self.env_data = {'type': env_type} |
830 | return views.env_edit( |
831 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
832 | - self.env_data) |
833 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
834 | + self.save_callable, self.env_data) |
835 | |
836 | def get_form_contents(self): |
837 | """Return the form contents included in the app page. |
838 | @@ -749,8 +925,8 @@ |
839 | 'ec2-west successfully modified', self.app.get_message()) |
840 | # The application displays the environment detail view. |
841 | mock_env_detail.assert_called_once_with( |
842 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
843 | - new_env_data) |
844 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
845 | + self.save_callable, new_env_data) |
846 | |
847 | @mock.patch('quickstart.cli.views.env_detail') |
848 | def test_save_empty_db(self, mock_env_detail): |
849 | @@ -769,8 +945,8 @@ |
850 | expected_new_env_data.update({'type': 'local', 'is-default': True}) |
851 | envs.set_env_data(self.env_db, None, expected_new_env_data) |
852 | mock_env_detail.assert_called_once_with( |
853 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
854 | - expected_new_env_data) |
855 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
856 | + self.save_callable, expected_new_env_data) |
857 | |
858 | def test_save_invalid_form_data(self): |
859 | # Errors are displayed if the user tries to save invalid data. |
860 | @@ -815,7 +991,8 @@ |
861 | cancel_button = self.get_control_buttons()[1] |
862 | cli_helpers.emit(cancel_button) |
863 | mock_env_index.assert_called_once_with( |
864 | - self.app, self.env_type_db, self.env_db, self.save_callable) |
865 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
866 | + self.save_callable) |
867 | |
868 | @mock.patch('quickstart.cli.views.env_detail') |
869 | def test_modification_view_cancel_button(self, mock_env_detail): |
870 | @@ -826,5 +1003,5 @@ |
871 | cancel_button = self.get_control_buttons()[1] |
872 | cli_helpers.emit(cancel_button) |
873 | mock_env_detail.assert_called_once_with( |
874 | - self.app, self.env_type_db, self.env_db, self.save_callable, |
875 | - self.env_data) |
876 | + self.app, self.env_type_db, self.env_db, self.jenv_db, |
877 | + self.save_callable, self.env_data) |
878 | |
879 | === modified file 'quickstart/tests/helpers.py' |
880 | --- quickstart/tests/helpers.py 2014-12-16 16:16:04 +0000 |
881 | +++ quickstart/tests/helpers.py 2014-12-17 12:34:18 +0000 |
882 | @@ -262,9 +262,21 @@ |
883 | def make_jenv_db(): |
884 | """Create and return a jenv files database.""" |
885 | environments = { |
886 | - 'ec2-west': {'type': '__unknown__'}, |
887 | - 'lxc': {'type': 'local'}, |
888 | - 'test-jenv': {'type': '__unknown__'}, |
889 | + 'ec2-west': { |
890 | + 'type': '__unknown__', |
891 | + 'user': 'who', |
892 | + 'state-servers': ('1.2.3.4:42', '1.2.3.4:47'), |
893 | + }, |
894 | + 'lxc': { |
895 | + 'type': 'local', |
896 | + 'user': 'dalek', |
897 | + 'state-servers': ('localhost:17070', '10.0.3.1:17070'), |
898 | + }, |
899 | + 'test-jenv': { |
900 | + 'type': '__unknown__', |
901 | + 'user': 'my-user', |
902 | + 'state-servers': ('10.0.3.1:17070',), |
903 | + }, |
904 | } |
905 | return {'environments': environments} |
906 | |
907 | |
908 | === modified file 'quickstart/tests/models/test_jenv.py' |
909 | --- quickstart/tests/models/test_jenv.py 2014-12-16 16:04:16 +0000 |
910 | +++ quickstart/tests/models/test_jenv.py 2014-12-17 12:34:18 +0000 |
911 | @@ -200,9 +200,14 @@ |
912 | 'ec2': yaml.safe_dump(self.jenv_data), |
913 | }): |
914 | jenv_db = jenv.get_env_db() |
915 | - self.assertEqual({ |
916 | - 'environments': {'ec2': {'type': 'ec2'}}, |
917 | - }, jenv_db) |
918 | + expected_environments = { |
919 | + 'ec2': { |
920 | + 'type': 'ec2', |
921 | + 'user': 'admin', |
922 | + 'state-servers': ('localhost:17070', '10.0.3.1:17070'), |
923 | + }, |
924 | + } |
925 | + self.assertEqual({'environments': expected_environments}, jenv_db) |
926 | |
927 | def test_multiple_jenv_files(self): |
928 | # Multiple environments are correctly returned. |
929 | @@ -213,22 +218,29 @@ |
930 | 'bootstrap-config': {'type': 'hp'}, |
931 | } |
932 | jenv_data2 = { |
933 | - 'user': 'admin', |
934 | + 'user': 'my-user', |
935 | 'password': 'Secret!', |
936 | - 'state-servers': ['localhost:17070'], |
937 | + 'state-servers': ['1.2.3.4:5', '1.2.3.4:42'], |
938 | 'bootstrap-config': {'type': 'maas'}, |
939 | } |
940 | with self.make_multiple_jenvs({ |
941 | - 'hp': yaml.safe_dump(jenv_data1), |
942 | + 'hp-cloud': yaml.safe_dump(jenv_data1), |
943 | 'maas': yaml.safe_dump(jenv_data2), |
944 | }): |
945 | jenv_db = jenv.get_env_db() |
946 | - self.assertEqual({ |
947 | - 'environments': { |
948 | - 'hp': {'type': 'hp'}, |
949 | - 'maas': {'type': 'maas'}, |
950 | - }, |
951 | - }, jenv_db) |
952 | + expected_environments = { |
953 | + 'hp-cloud': { |
954 | + 'type': 'hp', |
955 | + 'user': 'admin', |
956 | + 'state-servers': ('localhost:17070',), |
957 | + }, |
958 | + 'maas': { |
959 | + 'type': 'maas', |
960 | + 'user': 'my-user', |
961 | + 'state-servers': ('1.2.3.4:5', '1.2.3.4:42'), |
962 | + }, |
963 | + } |
964 | + self.assertEqual({'environments': expected_environments}, jenv_db) |
965 | |
966 | def test_unknown_env_type(self): |
967 | # If the jenv file does not include the env type, jenv.UNKNOWN_ENV_TYPE |
968 | @@ -240,9 +252,14 @@ |
969 | } |
970 | with self.make_jenv('local', yaml.safe_dump(jenv_data)): |
971 | jenv_db = jenv.get_env_db() |
972 | - self.assertEqual({ |
973 | - 'environments': {'local': {'type': jenv.UNKNOWN_ENV_TYPE}}, |
974 | - }, jenv_db) |
975 | + expected_environments = { |
976 | + 'local': { |
977 | + 'type': jenv.UNKNOWN_ENV_TYPE, |
978 | + 'user': 'admin', |
979 | + 'state-servers': ('localhost:17070',), |
980 | + }, |
981 | + } |
982 | + self.assertEqual({'environments': expected_environments}, jenv_db) |
983 | |
984 | def test_extraneous_files(self): |
985 | # Extraneous files are ignored. |
986 | @@ -261,7 +278,9 @@ |
987 | |
988 | def test_validation_success(self): |
989 | # A valid jenv file is successfully validated. |
990 | - jenv.validate(self.jenv_data) |
991 | + credentials, servers = jenv.validate(self.jenv_data) |
992 | + self.assertEqual(('admin', 'Secret!'), credentials) |
993 | + self.assertEqual(('localhost:17070', '10.0.3.1:17070'), servers) |
994 | |
995 | def test_invalid_credentials(self): |
996 | # A ValueError is raised if the credentials cannot be retrieved. |
997 | @@ -296,3 +315,54 @@ |
998 | 'password': 'Secret!', |
999 | 'state-servers': [], |
1000 | }) |
1001 | + |
1002 | + |
1003 | +class TestGetEnvShortDescription(unittest.TestCase): |
1004 | + |
1005 | + def test_env(self): |
1006 | + # The env description includes the environment name and type. |
1007 | + env_data = {'name': 'lxc', 'type': 'local'} |
1008 | + description = jenv.get_env_short_description(env_data) |
1009 | + self.assertEqual('lxc (type: local)', description) |
1010 | + |
1011 | + def test_env_without_type(self): |
1012 | + # Without the type we can only show the environment name. |
1013 | + env_data = {'name': 'ec2', 'type': jenv.UNKNOWN_ENV_TYPE} |
1014 | + description = jenv.get_env_short_description(env_data) |
1015 | + self.assertEqual('ec2', description) |
1016 | + |
1017 | + |
1018 | +class TestGetEnvDetails(unittest.TestCase): |
1019 | + |
1020 | + def test_env(self): |
1021 | + # The environment details are properly returned. |
1022 | + env_data = { |
1023 | + 'name': 'lxc', |
1024 | + 'type': 'local', |
1025 | + 'user': 'who', |
1026 | + 'state-servers': ('1.2.3.4:17060', 'localhost:17070'), |
1027 | + } |
1028 | + expected_details = [ |
1029 | + ('type', 'local'), |
1030 | + ('name', 'lxc'), |
1031 | + ('user', 'who'), |
1032 | + ('state servers', '1.2.3.4:17060, localhost:17070'), |
1033 | + ] |
1034 | + details = jenv.get_env_details(env_data) |
1035 | + self.assertEqual(expected_details, details) |
1036 | + |
1037 | + def test_env_without_type(self): |
1038 | + # The environment type is not included if unknown. |
1039 | + env_data = { |
1040 | + 'name': 'aws', |
1041 | + 'type': jenv.UNKNOWN_ENV_TYPE, |
1042 | + 'user': 'the-doctor', |
1043 | + 'state-servers': ('1.2.3.4:17060',), |
1044 | + } |
1045 | + expected_details = [ |
1046 | + ('name', 'aws'), |
1047 | + ('user', 'the-doctor'), |
1048 | + ('state servers', '1.2.3.4:17060'), |
1049 | + ] |
1050 | + details = jenv.get_env_details(env_data) |
1051 | + self.assertEqual(expected_details, details) |
1052 | |
1053 | === modified file 'quickstart/tests/test_manage.py' |
1054 | --- quickstart/tests/test_manage.py 2014-12-16 16:04:16 +0000 |
1055 | +++ quickstart/tests/test_manage.py 2014-12-17 12:34:18 +0000 |
1056 | @@ -36,7 +36,10 @@ |
1057 | settings, |
1058 | ) |
1059 | from quickstart.cli import views |
1060 | -from quickstart.models import envs |
1061 | +from quickstart.models import ( |
1062 | + envs, |
1063 | + jenv, |
1064 | +) |
1065 | from quickstart.tests import helpers |
1066 | |
1067 | |
1068 | @@ -372,13 +375,15 @@ |
1069 | self.env_type_db = envs.get_env_type_db() |
1070 | self.env_file = self.make_env_file() |
1071 | self.env_db = envs.load(self.env_file) |
1072 | + self.jenv_db = helpers.make_jenv_db() |
1073 | |
1074 | @contextmanager |
1075 | - def patch_interactive_mode(self, env_db, return_value): |
1076 | + def patch_interactive_mode(self, env_db, jenv_db, return_value): |
1077 | """Patch the quickstart.cli.views.show function. |
1078 | |
1079 | Ensure the interactive mode is started by the code in the context block |
1080 | - passing the given env_db. Make the view return the given return_value. |
1081 | + passing the given env_db and jenv_db. |
1082 | + Make the view return the given return_value. |
1083 | """ |
1084 | create_save_callable_path = 'quickstart.manage._create_save_callable' |
1085 | mock_show = mock.Mock(return_value=return_value) |
1086 | @@ -387,16 +392,18 @@ |
1087 | yield |
1088 | mock_save_callable.assert_called_once_with(self.parser, self.env_file) |
1089 | mock_show.assert_called_once_with( |
1090 | - views.env_index, self.env_type_db, env_db, |
1091 | + views.env_index, self.env_type_db, env_db, jenv_db, |
1092 | mock_save_callable()) |
1093 | |
1094 | def test_resulting_env_data(self): |
1095 | # The interactive session can be used to select an environment, in |
1096 | # which case the function returns the corresponding env_data. |
1097 | env_data = envs.get_env_data(self.env_db, 'aws') |
1098 | - with self.patch_interactive_mode(self.env_db, [self.env_db, env_data]): |
1099 | + with self.patch_interactive_mode( |
1100 | + self.env_db, self.jenv_db, [self.env_db, env_data]): |
1101 | obtained_env_data = manage._start_interactive_session( |
1102 | - self.parser, self.env_type_db, self.env_db, self.env_file) |
1103 | + self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1104 | + self.env_file) |
1105 | self.assertEqual(env_data, obtained_env_data) |
1106 | |
1107 | @helpers.mock_print |
1108 | @@ -405,9 +412,11 @@ |
1109 | # during the interactive session. |
1110 | env_data = envs.get_env_data(self.env_db, 'aws') |
1111 | new_env_db = helpers.make_env_db() |
1112 | - with self.patch_interactive_mode(self.env_db, [new_env_db, env_data]): |
1113 | + with self.patch_interactive_mode( |
1114 | + self.env_db, self.jenv_db, [new_env_db, env_data]): |
1115 | manage._start_interactive_session( |
1116 | - self.parser, self.env_type_db, self.env_db, self.env_file) |
1117 | + self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1118 | + self.env_file) |
1119 | mock_print.assert_called_once_with( |
1120 | 'changes to the environments file have been saved') |
1121 | |
1122 | @@ -415,9 +424,11 @@ |
1123 | def test_interactive_mode_quit(self, mock_exit): |
1124 | # If the user explicitly quits the interactive mode, the program exits |
1125 | # without proceeding with the environment bootstrapping. |
1126 | - with self.patch_interactive_mode(self.env_db, [self.env_db, None]): |
1127 | + with self.patch_interactive_mode( |
1128 | + self.env_db, self.jenv_db, [self.env_db, None]): |
1129 | manage._start_interactive_session( |
1130 | - self.parser, self.env_type_db, self.env_db, self.env_file) |
1131 | + self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1132 | + self.env_file) |
1133 | mock_exit.assert_called_once_with('quitting') |
1134 | |
1135 | |
1136 | @@ -488,7 +499,9 @@ |
1137 | self.assertIsNone(result) |
1138 | |
1139 | |
1140 | -class TestSetupEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
1141 | +class TestSetupEnv( |
1142 | + helpers.EnvFileTestsMixin, helpers.JenvFileTestsMixin, |
1143 | + unittest.TestCase): |
1144 | |
1145 | def setUp(self): |
1146 | self.parser = mock.Mock() |
1147 | @@ -596,10 +609,12 @@ |
1148 | env_data = envs.get_env_data(env_db, 'aws') |
1149 | get_env_type_db_path = 'quickstart.models.envs.get_env_type_db' |
1150 | with mock.patch(get_env_type_db_path) as mock_get_env_type_db: |
1151 | - with self.patch_interactive_mode(env_data) as mock_interactive: |
1152 | - manage._setup_env(options, self.parser) |
1153 | + with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)): |
1154 | + jenv_db = jenv.get_env_db() |
1155 | + with self.patch_interactive_mode(env_data) as mock_interactive: |
1156 | + manage._setup_env(options, self.parser) |
1157 | mock_interactive.assert_called_once_with( |
1158 | - self.parser, mock_get_env_type_db(), env_db, env_file) |
1159 | + self.parser, mock_get_env_type_db(), env_db, jenv_db, env_file) |
1160 | # The options is updated with data from the selected environment. |
1161 | self.assertEqual(env_file, options.env_file) |
1162 | self.assertEqual('aws', options.env_name) |
Reviewers: mp+244976_ code.launchpad. net,
Message:
Please take a look.
Description:
Display the jenv environments in interactive mode.
The jenv environments are now displayed in the
interactive session, and it's possible to use them.
Also implemented a simple jenv detail view in uwrid.
Quickstart now also supports highlighting active
environments, i.e. those that are supposed to be
running because they have a corresponding jenv file
in the juju gome.
Tests: `make check`.
QA: environments/ .
user the new juju, bootstrap one or more environments
with quickstart (use .venv/bin/python juju-quickstart --upload-tools
if you are using a non-local provider).
Start the interactive session, and check those environments are
properly marked as acrive.
Run quickstart again to check it is idempotent.
Create new users and copy the resulting jenv file to
~/.juju/
Run quickstart interactive session again, and check the
new environments are reported, and it is possible to use them.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /interactive- jenvs/+ merge/244976
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/188330043/
Affected files (+530, -104 lines): cli/ui. py cli/views. py manage. py models/ jenv.py tests/cli/ test_views. py tests/helpers. py tests/models/ test_jenv. py tests/test_ manage. py
A [revision details]
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/