Merge lp:~frankban/juju-quickstart/more-cloud-providers into lp:juju-quickstart
- more-cloud-providers
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 46 | ||||
Proposed branch: | lp:~frankban/juju-quickstart/more-cloud-providers | ||||
Merge into: | lp:juju-quickstart | ||||
Diff against target: |
730 lines (+354/-76) 10 files modified
quickstart/__init__.py (+1/-1) quickstart/cli/forms.py (+30/-14) quickstart/cli/views.py (+11/-5) quickstart/manage.py (+5/-2) quickstart/models/envs.py (+127/-3) quickstart/models/fields.py (+9/-0) quickstart/tests/cli/test_forms.py (+94/-40) quickstart/tests/cli/test_views.py (+4/-3) quickstart/tests/models/test_envs.py (+61/-8) quickstart/tests/models/test_fields.py (+12/-0) |
||||
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/more-cloud-providers | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+201616@code.launchpad.net |
Commit message
Description of the change
Add support for OpenStack and Azure.
Tests: `make check`
QA: `.venv/bin/python juju-quickstart -i`
Ensure you can successfully create an openstack/HP cloud
and an azure environment.
If you already subscribed to any of those, please
check everything works ok. If not, no problem,
I already bootstrapped HP Cloud and azure.
Since this is the 1.0 version, please ensure
the environment management works well, and in general
quickstart bootstraps the environments and
deploys the GUI as expected.
Thank you!
Francesco Banconi (frankban) wrote : | # |
Richard Harding (rharding) wrote : | # |
Code looks good, a couple of comments.
Starting QA.
https:/
File quickstart/
https:/
quickstart/
wahoo!
https:/
File quickstart/
https:/
quickstart/
should this be a suggestion field to help users move along faster? Just
suggest a sensible default like US East which I think even things like
the AMZ web ui does? It's a suggestion field in the open stack example
above.
https:/
File quickstart/
https:/
quickstart/
TestCreateStrin
I think that the suggestion field is its own new class and should have
its own tests vs getting shoed into the String widget tests. This seems
like a path down like the one in the charm where the one suite of tests
contained others that weren't directly related.
Richard Harding (rharding) wrote : | # |
In creating an Azure environment there's a space issue before 'saucy' in
the list of default series.
The dual nature of this is kind of strange. There's a lot of text/links
there. I'd probably leave off the 'left_empty' since that's just
defaulting to precise. Why not just prefill that in and let users change
it?
Do you think we can skip the "Click the choices to auto-fill..." on each
field? Are the underline links a common enough/
don't have/use a mouse does it help/matter?
The list of providers to create config for says 'openstack' but
everything inside of that references both openstack and HP. Should that
original selection be "openstack (HP Cloud)"?
I don't have openstack creds but looks sane.
I created 3 environments and removed two of them. One was the default.
I'd expect the last one left to be made the default now.
On the main interactive landing page it says "Use the links below to
create new environments." I don't really think of those as links. The
underlined bits in the choice fields are more "link like". I'd suggest
rephrasing this as just "Create new environments:"
Better yet, I'd try to match this up a bit. The main header can just be
"Manage Juju environments" and add a header to the top section as
"Manage existing environments:" and then the lower one could be the
"Create a new environment". I think it would flow and read a bit nicer.
When you select an existing environment the header is "name (type:
local, default). However that same information is listed below. Can it
be simpler and just be "name"?
I created a local environment, set it up, and got the gui loaded up on
it. I then changed it to be the default and selected 'use' think it
would open my existing session. Instead I got a request for my sudo
password, it went through the 'installing ppa...', asking for my ssh
passphrase, and only after that did it open. Can this shortcut to just
open it up and detect/know the other steps aren't necessary?
I removed my last environment and got the 'please create one' which is
nice. The bullet/selection for 'automatically create and bootstrap a
local' does not have a line of space between it and the above content
like the other ones do. I'd propose moving that down one newline.
One other note, removing the environment did not ask or do anything
about destroying it. Now that I've removed my config there's not a good
way for me to shut it down? Should we ask to destroy-environment when
removing a config?
Overall this is really great and nice to use. I'm with-holding the L G T
M because I think the destroy question and the re-opening without asking
might require enough code changes (if there's agreement to make those
changes) to want to re-review.
Madison Scott-Clary (makyo) wrote : | # |
I agree that some of the points Rick brings up - notably destroying an
environment - are worth considering, though I think they may be worth
their own branches. However, if that's the case, then I think we should
withhold the move to 1.0.0 until the destroy confirmation is made, since
that's potentially big. That means, either include that here or push to
another branch with 1.0.0; otherwise, LGTM! Thanks for the work!
- 50. By Francesco Banconi
-
Checkpoint.
- 51. By Francesco Banconi
-
UI changes as per review.
- 52. By Francesco Banconi
-
All changes as per review.
- 53. By Francesco Banconi
-
Checkpoint.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File quickstart/
https:/
quickstart/
On 2014/01/15 19:29:04, rharding wrote:
> should this be a suggestion field to help users move along faster?
Just suggest
> a sensible default like US East which I think even things like the AMZ
web ui
> does? It's a suggestion field in the open stack example above.
Suggestion fields are used to suggest a value while leaving the field
open to arbitrary values. The choice field here means validation is
involved. AFAICT Azure does not use US East as default.
https:/
File quickstart/
https:/
quickstart/
TestCreateStrin
On 2014/01/15 19:29:04, rharding wrote:
> I think that the suggestion field is its own new class and should have
its own
> tests vs getting shoed into the String widget tests. This seems like a
path down
> like the one in the charm where the one suite of tests contained
others that
> weren't directly related.
I am not sure I understood this comment. The suggestion field itself
already has its own tests in test_fields.py. Here we are just testing
that suggestions are included in the string widget if required.
Francesco Banconi (frankban) wrote : | # |
This is really good stuff Rick and Matthew.
Thank you both for your great reviews!
Please see the inline comments below, and,
if you can, please QA the new changes.
On 2014/01/15 20:51:45, rharding wrote:
> In creating an Azure environment there's a space issue before 'saucy'
in the
> list of default series.
Unfortunately this is due to the fact we are
using a Grid widget to display choices.
This is done so that line wrapping works as
expected on small terminals. This can be better
seen, e.g., in the azure location choices.
There is no easy way to fix this in Urwid.
I agree it's a bit ugly but I also think
we can live with that for 1.0. What do you think?
> The dual nature of this is kind of strange. There's a lot of
text/links there.
> I'd probably leave off the 'left_empty' since that's just defaulting
to precise.
> Why not just prefill that in and let users change it?
For optional fields, rather than pre-filling
values, I'd prefer to leave the field empty
(and consequently expose an option to empty
the field). Defaults can change over time,
and pre-filling with a value is semantically
different than not writing the field at all
in the envs.yaml file. The default series is
a good example of this. Leaving the field
empty means that juju-core, in a few months, will
bootstrap trusty machines. Explicitly selecting
precise avoids this behavior, and we want to
support both needs.
> Do you think we can skip the "Click the choices to auto-fill..." on
each field?
> Are the underline links a common enough/
have/use a
> mouse does it help/matter?
This makes the UI more explicit and has been
requested in previous reviews.
> The list of providers to create config for says 'openstack' but
everything
> inside of that references both openstack and HP. Should that original
selection
> be "openstack (HP Cloud)"?
This is a very good point Rick.
Fixed, now the labels for new environments
are more human readable.
> I don't have openstack creds but looks sane.
Cool.
> I created 3 environments and removed two of them. One was the default.
I'd
> expect the last one left to be made the default now.
Good idea, done.
> On the main interactive landing page it says "Use the links below to
create new
> environments." I don't really think of those as links. The underlined
bits in
> the choice fields are more "link like". I'd suggest rephrasing this as
just
> "Create new environments:"
Done.
> Better yet, I'd try to match this up a bit. The main header can just
be "Manage
> Juju environments" and add a header to the top section as "Manage
existing
> environments:" and then the lower one could be the "Create a new
environment". I
> think it would flow and read a bit nicer.
Done.
> When you select an existing environment the header is "name (type:
local,
> default). However that same information is listed below. Can it be
simpler and
> just be "name"?
I find that summary useful, especially when the
environment is the default one: most of the times
you get all the info you require without having
to read the table below. I'll change it if you are
not convinced.
> I created a local environment, set it up, and got the gui loaded up o...
Richard Harding (rharding) wrote : | # |
Thanks for the out of band email and updates.
LGTM with the notes that this is 0.9 and 1.0 will have the checking for
catching Juju errors in bringing up the machine as a follow up card.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Add support for OpenStack and Azure.
Tests: `make check`
QA: `.venv/bin/python juju-quickstart -i`
Ensure you can successfully create an openstack/HP cloud
and an azure environment.
If you already subscribed to any of those, please
check everything works ok. If not, no problem,
I already bootstrapped HP Cloud and azure.
Since this is the 1.0 version, please ensure
the environment management works well, and in general
quickstart bootstraps the environments and
deploys the GUI as expected.
Thank you!
R=rharding, matthew.scott
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/__init__.py' |
2 | --- quickstart/__init__.py 2014-01-09 11:04:03 +0000 |
3 | +++ quickstart/__init__.py 2014-01-16 12:48:07 +0000 |
4 | @@ -22,7 +22,7 @@ |
5 | from __future__ import unicode_literals |
6 | |
7 | |
8 | -VERSION = (0, 6, 0) |
9 | +VERSION = (1, 0, 0) |
10 | |
11 | |
12 | def get_version(): |
13 | |
14 | === modified file 'quickstart/cli/forms.py' |
15 | --- quickstart/cli/forms.py 2014-01-08 12:00:44 +0000 |
16 | +++ quickstart/cli/forms.py 2014-01-16 12:48:07 +0000 |
17 | @@ -56,23 +56,34 @@ |
18 | ]) |
19 | |
20 | |
21 | -def _create_choices_widget(choices, required, edit_widget): |
22 | - """Create and return a choices widget. |
23 | +def _create_buttons_grid_widget(choices, edit_widget): |
24 | + """Create and return a grid of button widgets. |
25 | |
26 | - The widget displays the given choices for a specific form field. Clicking a |
27 | - choice updates the corresponding edit widget. |
28 | + Buttons are associated with the given choices, and when clicked, udpate |
29 | + the given edit_widget's text. |
30 | """ |
31 | - widgets = [urwid.Text('possible values are:')] |
32 | callback = edit_widget.set_edit_text |
33 | buttons = [ |
34 | ui.MenuButton(('field button', choice), ui.thunk(callback, choice)) |
35 | for choice in choices |
36 | ] |
37 | cell_width = max(button.base_widget.pack()[0] for button in buttons) |
38 | - widgets.append(urwid.GridFlow(buttons, cell_width, 1, 0, 'left')) |
39 | + return urwid.GridFlow(buttons, cell_width, 1, 0, 'left') |
40 | + |
41 | + |
42 | +def _create_choices_widget(choices, required, edit_widget): |
43 | + """Create and return a choices widget. |
44 | + |
45 | + The widget displays the given choices for a specific form field. Clicking a |
46 | + choice updates the corresponding edit widget. |
47 | + """ |
48 | + widgets = [urwid.Text('possible values are:')] |
49 | + buttons_grid = _create_buttons_grid_widget(choices, edit_widget) |
50 | + widgets.append(buttons_grid) |
51 | if not required: |
52 | button = ui.MenuButton( |
53 | - ('field button', 'left empty'), ui.thunk(callback, '')) |
54 | + ('field button', 'left empty'), |
55 | + ui.thunk(edit_widget.set_edit_text, '')) |
56 | widgets.append(urwid.Columns([ |
57 | ('pack', urwid.Text('but this field can also be ')), button, |
58 | ])) |
59 | @@ -118,18 +129,23 @@ |
60 | # Display the field help message below the edit widget. |
61 | widgets.append(urwid.Text(field.help)) |
62 | if not field.readonly: |
63 | + # Can we display suggestions for this field? |
64 | + suggestions = getattr(field, 'suggestions', ()) |
65 | + if suggestions: |
66 | + widgets.append( |
67 | + _create_buttons_grid_widget(suggestions, edit_widget)) |
68 | # Can the value be automatically generated? |
69 | generate_callable = getattr(field, 'generate', None) |
70 | if generate_callable is not None: |
71 | widgets.append( |
72 | _create_generate_widget(generate_callable, edit_widget)) |
73 | - # If the value must be in a range of choices, display the possible choices |
74 | - # as part of the help message. |
75 | - choices = getattr(field, 'choices', None) |
76 | - if choices is not None: |
77 | - choices_widget = _create_choices_widget( |
78 | - tuple(choices), field.required, edit_widget) |
79 | - widgets.append(choices_widget) |
80 | + # If the value must be in a range of choices, display the possible |
81 | + # choices as part of the help message. |
82 | + choices = getattr(field, 'choices', None) |
83 | + if choices is not None: |
84 | + choices_widget = _create_choices_widget( |
85 | + tuple(choices), field.required, edit_widget) |
86 | + widgets.append(choices_widget) |
87 | if field.default is not None: |
88 | widgets.append( |
89 | urwid.Text('default if not set: {}'.format(field.default))) |
90 | |
91 | === modified file 'quickstart/cli/views.py' |
92 | --- quickstart/cli/views.py 2014-01-09 18:10:40 +0000 |
93 | +++ quickstart/cli/views.py 2014-01-16 12:48:07 +0000 |
94 | @@ -172,7 +172,10 @@ |
95 | |
96 | if environments: |
97 | title = 'Select an existing Juju environment or create a new one' |
98 | - widgets = [] |
99 | + widgets = [ |
100 | + urwid.Text(('highlight', 'Manage existing environments:')), |
101 | + urwid.Divider(), |
102 | + ] |
103 | else: |
104 | title = 'No Juju environments already set up: please create one' |
105 | widgets = [ |
106 | @@ -190,6 +193,7 @@ |
107 | 'start your Juju experience in a local environment (LXC), ' |
108 | 'just click the link below:' |
109 | ]), |
110 | + urwid.Divider(), |
111 | ui.MenuButton( |
112 | '\N{BULLET} automatically create and bootstrap a local ' |
113 | 'environment', ui.thunk(create_and_start_local_env)), |
114 | @@ -202,12 +206,14 @@ |
115 | # time consuming. |
116 | focus_position = None |
117 | errors_found = default_found = False |
118 | + existing_widgets_num = len(widgets) |
119 | for position, env_data in enumerate(environments): |
120 | bullet = '\N{BULLET}' |
121 | # Is this environment the default one? |
122 | if env_data['is-default']: |
123 | default_found = True |
124 | - focus_position = position |
125 | + # The first two positions are the section header and the divider. |
126 | + focus_position = position + existing_widgets_num |
127 | bullet = '\N{CHECK MARK}' |
128 | # Is this environment valid? |
129 | env_metadata = envs.get_env_metadata(env_type_db, env_data) |
130 | @@ -223,7 +229,7 @@ |
131 | widgets.extend([ |
132 | urwid.Divider(), |
133 | urwid.Text(( |
134 | - 'highlight', 'Use the links below to create new environments')), |
135 | + 'highlight', 'Create a new environment:')), |
136 | urwid.Divider(), |
137 | ]) |
138 | # The Juju GUI can be safely installed in the bootstrap node only if its |
139 | @@ -231,11 +237,11 @@ |
140 | preferred_series = settings.JUJU_GUI_PREFERRED_SERIES |
141 | widgets.extend([ |
142 | ui.MenuButton( |
143 | - ['\N{BULLET} new ', ('highlight', env_type), ' environment'], |
144 | + ['\N{BULLET} new ', ('highlight', label), ' environment'], |
145 | ui.thunk(edit_view, { |
146 | 'type': env_type, 'default-series': preferred_series}) |
147 | ) |
148 | - for env_type in envs.get_supported_env_types(env_type_db) |
149 | + for env_type, label in envs.get_supported_env_types(env_type_db) |
150 | ]) |
151 | # Set up the application status messages. |
152 | status = [' \N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW} navigate '] |
153 | |
154 | === modified file 'quickstart/manage.py' |
155 | --- quickstart/manage.py 2014-01-13 15:46:27 +0000 |
156 | +++ quickstart/manage.py 2014-01-16 12:48:07 +0000 |
157 | @@ -430,9 +430,12 @@ |
158 | gui_env.close() |
159 | print( |
160 | 'done!\n\n' |
161 | - 'Run "juju quickstart -e {}" again if you want\n' |
162 | + 'Run "juju quickstart -e {env_name}" again if you want\n' |
163 | 'to reopen and log in to the GUI browser later.\n' |
164 | 'Run "juju quickstart -i" if you want to manage\n' |
165 | 'or bootstrap your Juju environments using the\n' |
166 | - 'interactive session.'.format(options.env_name) |
167 | + 'interactive session.\n' |
168 | + 'Run "juju destroy-environment -e {env_name} [-y]"\n' |
169 | + 'to destroy the environment you just bootstrapped.'.format( |
170 | + env_name=options.env_name) |
171 | ) |
172 | |
173 | === modified file 'quickstart/models/envs.py' |
174 | --- quickstart/models/envs.py 2014-01-10 15:44:36 +0000 |
175 | +++ quickstart/models/envs.py 2014-01-16 12:48:07 +0000 |
176 | @@ -341,13 +341,17 @@ |
177 | Raise a ValueError if the environment is not present in env_db. |
178 | Without errors, the env_db is modified in place and None is returned. |
179 | """ |
180 | + environments = env_db['environments'] |
181 | try: |
182 | - del env_db['environments'][env_name] |
183 | + del environments[env_name] |
184 | except KeyError: |
185 | raise ValueError( |
186 | b'the environment named {!r} does not exist'.format(env_name)) |
187 | if env_db.get('default') == env_name: |
188 | del env_db['default'] |
189 | + # If only one environment remains, set it as default. |
190 | + if len(environments) == 1: |
191 | + env_db['default'] = environments.keys()[0] |
192 | |
193 | |
194 | def get_env_type_db(): |
195 | @@ -384,6 +388,11 @@ |
196 | 'eu-west-1', 'sa-east-1', |
197 | 'us-east-1', 'us-west-1', 'us-west-2', |
198 | ) |
199 | + hp_regions = ( |
200 | + 'az-1.region-a.geo-1', 'az-2.region-a.geo-1', 'az-3.region-a.geo-1') |
201 | + azure_locations = ( |
202 | + 'East US', 'West US', 'West Europe', 'North Europe', |
203 | + 'Southeast Asia', 'East Asia') |
204 | # Define the env_type_db dictionary: this is done inside this function in |
205 | # order to avoid instantiating fields at import time. |
206 | # This is an ordered dict so that views can expose options to create new |
207 | @@ -405,6 +414,7 @@ |
208 | }, |
209 | }) |
210 | env_type_db['ec2'] = { |
211 | + 'label': 'Amazon EC2', |
212 | 'description': ( |
213 | 'The ec2 provider enable you to run Juju on the EC2 cloud. ' |
214 | 'This process requires you to have an Amazon Web Services (AWS) ' |
215 | @@ -443,7 +453,114 @@ |
216 | is_default_field, |
217 | ), |
218 | } |
219 | + env_type_db['openstack'] = { |
220 | + 'label': 'OpenStack (or HP Public Cloud)', |
221 | + 'description': ( |
222 | + 'The openstack provider enable you to run Juju on OpenStack ' |
223 | + 'private and public clouds. Use this also if you want to ' |
224 | + 'set up Juju on HP Public Cloud. See ' |
225 | + 'https://juju.ubuntu.com/docs/config-openstack.html and ' |
226 | + 'https://juju.ubuntu.com/docs/config-hpcloud.html for more ' |
227 | + 'details on the openstack provider configuration.' |
228 | + ), |
229 | + 'fields': ( |
230 | + provider_field, |
231 | + name_field, |
232 | + admin_secret_field, |
233 | + default_series_field, |
234 | + fields.BoolField( |
235 | + 'use-floating-ip', label='use floating IP', allow_mixed=False, |
236 | + required=True, |
237 | + help='Specifies whether the use of a floating IP address is ' |
238 | + 'required to give the nodes a public IP address. ' |
239 | + 'Some installations assign public IP addresses by ' |
240 | + 'default without requiring a floating IP address.'), |
241 | + fields.AutoGeneratedStringField( |
242 | + 'control-bucket', label='control bucket', required=True, |
243 | + help='the globally unique swift bucket name'), |
244 | + fields.SuggestionsStringField( |
245 | + 'auth-url', label='authentication URL', required=True, |
246 | + help='The Keystone URL to use in the authentication process. ' |
247 | + 'For HP Public Cloud, use the value suggested below:', |
248 | + suggestions=['https://region-a.geo-1.identity.hpcloudsvc.com' |
249 | + ':35357/v2.0/']), |
250 | + fields.StringField( |
251 | + 'tenant-name', label='tenant name', required=True, |
252 | + help='The OpenStack tenant name. For HP Public Cloud, this is ' |
253 | + 'listed as the project name on the ' |
254 | + 'https://account.hpcloud.com/projects page.'), |
255 | + fields.SuggestionsStringField( |
256 | + 'region', label='region', required=True, |
257 | + help='The OpenStack region to use. ' |
258 | + 'For HP Public Cloud, use one of the following:', |
259 | + suggestions=hp_regions), |
260 | + fields.ChoiceField( |
261 | + 'auth-mode', label='authentication mode', required=False, |
262 | + default='userpass', choices=('userpass', 'keypair'), |
263 | + help='The way Juju authenticates to OpenStack. The userpass ' |
264 | + 'authentication requires you to fill in your user name ' |
265 | + 'and password. The keypair mode requires access key and ' |
266 | + 'secret key to be properly set up. For HP Public Cloud ' |
267 | + 'these information can be retrieved on the ' |
268 | + 'https://account.hpcloud.com/account/api_keys page.'), |
269 | + fields.StringField( |
270 | + 'username', label='user name', required=False, |
271 | + help='the user name to use for the userpass authentication'), |
272 | + fields.PasswordField( |
273 | + 'password', label='password', required=False, |
274 | + help='the user name to use for the userpass authentication'), |
275 | + fields.StringField( |
276 | + 'access-key', label='access key', required=False, |
277 | + help='the access key to use for the keypair authentication'), |
278 | + fields.PasswordField( |
279 | + 'secret-key', label='secret key', required=False, |
280 | + help='the secret key to use for the keypair authentication'), |
281 | + is_default_field, |
282 | + ), |
283 | + } |
284 | + env_type_db['azure'] = { |
285 | + 'label': 'Windows Azure', |
286 | + 'description': ( |
287 | + 'The azure provider enable you to run Juju on Windows Azure. ' |
288 | + 'It requires you to have an Windows Azure account. If you have ' |
289 | + 'not signed up for one yet, it can obtained at ' |
290 | + 'http://www.windowsazure.com/. See ' |
291 | + 'https://juju.ubuntu.com/docs/config-azure.html for more ' |
292 | + 'details on the azure provider configuration.' |
293 | + ), |
294 | + 'fields': ( |
295 | + provider_field, |
296 | + name_field, |
297 | + admin_secret_field, |
298 | + default_series_field, |
299 | + fields.ChoiceField( |
300 | + 'location', choices=azure_locations, label='location', |
301 | + required=True, help='the region to use'), |
302 | + fields.StringField( |
303 | + 'management-subscription-id', required=True, |
304 | + label='management subscription ID', |
305 | + help='this information can be retrieved from ' |
306 | + 'https://manage.windowsazure.com (Settings)'), |
307 | + fields.StringField( |
308 | + 'management-certificate-path', required=True, |
309 | + label='management certificate path', |
310 | + help='the path to the pem file associated to the certificate ' |
311 | + 'uploaded in the Azure management console: ' |
312 | + 'https://manage.windowsazure.com ' |
313 | + '(Settings -> Management Certificates)'), |
314 | + fields.StringField( |
315 | + 'storage-account-name', required=True, |
316 | + label='storage account name', |
317 | + help='the name you used when creating a storage account in ' |
318 | + 'the Azure management console: ' |
319 | + 'https://manage.windowsazure.com (Storage). ' |
320 | + 'You must create the storage account in the same ' |
321 | + 'region/location specified by the location key value.'), |
322 | + is_default_field, |
323 | + ), |
324 | + } |
325 | env_type_db['local'] = { |
326 | + 'label': 'local (LXC)', |
327 | 'description': ( |
328 | 'The LXC local provider enables you to run Juju on a single ' |
329 | 'system like your local computer or a single server. ' |
330 | @@ -479,8 +596,15 @@ |
331 | |
332 | |
333 | def get_supported_env_types(env_type_db): |
334 | - """Return a list of supported provider type names.""" |
335 | - return [env_type for env_type in env_type_db if env_type != '__fallback__'] |
336 | + """Return a list of supported (provider type, label) tuples. |
337 | + |
338 | + Each tuple represents an environment type supported by Quickstart. |
339 | + """ |
340 | + return [ |
341 | + (env_type, metadata['label']) |
342 | + for env_type, metadata in env_type_db.items() |
343 | + if env_type != '__fallback__' |
344 | + ] |
345 | |
346 | |
347 | def get_env_metadata(env_type_db, env_data): |
348 | |
349 | === modified file 'quickstart/models/fields.py' |
350 | --- quickstart/models/fields.py 2014-01-07 15:41:55 +0000 |
351 | +++ quickstart/models/fields.py 2014-01-16 12:48:07 +0000 |
352 | @@ -293,6 +293,15 @@ |
353 | ) |
354 | |
355 | |
356 | +class SuggestionsStringField(StringField): |
357 | + """A string field storing possible value suggestions.""" |
358 | + |
359 | + def __init__(self, name, suggestions=(), **kwargs): |
360 | + """Initialize the choices field with the given choices.""" |
361 | + super(SuggestionsStringField, self).__init__(name, **kwargs) |
362 | + self.suggestions = tuple(suggestions) |
363 | + |
364 | + |
365 | class AutoGeneratedStringField(StringField): |
366 | """Can automatically generate string values if they are not provided. |
367 | |
368 | |
369 | === modified file 'quickstart/tests/cli/test_forms.py' |
370 | --- quickstart/tests/cli/test_forms.py 2014-01-08 12:00:44 +0000 |
371 | +++ quickstart/tests/cli/test_forms.py 2014-01-16 12:48:07 +0000 |
372 | @@ -18,6 +18,7 @@ |
373 | |
374 | from __future__ import unicode_literals |
375 | |
376 | +import collections |
377 | import unittest |
378 | |
379 | import mock |
380 | @@ -85,6 +86,38 @@ |
381 | self.assertEqual(expected_choices, obtained_choices) |
382 | |
383 | |
384 | +class TestCreateButtonsGridWidget(unittest.TestCase): |
385 | + |
386 | + def setUp(self): |
387 | + # Set up a mock edit widget and choices. |
388 | + self.mock_edit_widget = mock.Mock() |
389 | + self.choices = ['Kirk', 'Picard', 'Sisko'] |
390 | + self.widget = forms._create_buttons_grid_widget( |
391 | + self.choices, self.mock_edit_widget) |
392 | + |
393 | + def get_buttons(self): |
394 | + """Return a list of buttons included in self.widget.""" |
395 | + return [button for button, _ in self.widget.contents] |
396 | + |
397 | + def test_widget_structure(self): |
398 | + # The resulting widget is a urwid.GridFlow including all the expected |
399 | + # button widgets. |
400 | + self.assertIsInstance(self.widget, urwid.GridFlow) |
401 | + buttons = self.get_buttons() |
402 | + self.assertEqual(len(self.choices), len(buttons)) |
403 | + self.assertEqual( |
404 | + self.choices, map(cli_helpers.get_button_caption, buttons)) |
405 | + |
406 | + def test_button_click(self): |
407 | + # The given edit widget is updated when buttons are clicked. |
408 | + for button in self.get_buttons(): |
409 | + choice = cli_helpers.get_button_caption(button) |
410 | + # Click the button. |
411 | + cli_helpers.emit(button) |
412 | + # Ensure the edit widget has been updated accordingly. |
413 | + self.mock_edit_widget.set_edit_text.assert_called_with(choice) |
414 | + |
415 | + |
416 | class TestCreateChoicesWidget(ChoicesTestsMixin, unittest.TestCase): |
417 | |
418 | def setUp(self): |
419 | @@ -141,42 +174,49 @@ |
420 | class TestCreateStringWidget(ChoicesTestsMixin, unittest.TestCase): |
421 | |
422 | def inspect_widget(self, widget, field): |
423 | - """Return a list of sub-widgets composing the given string widget. |
424 | - |
425 | - The sub-widgets are: |
426 | - - the wrapper edit widget; |
427 | - - the base edit widget; |
428 | - - the caption text widget; |
429 | - - the help widget (or None if not present); |
430 | - - the error text widget (or None if not present); |
431 | - - the generate text widget (or None if not present); |
432 | - - the choices text widget (or None if not present); |
433 | - - the default text widget (or None if not present). |
434 | + """Return a dict of sub-widgets composing the given string widget. |
435 | + |
436 | + The dictionary includes the following keys: |
437 | + |
438 | + - wrapper: the wrapper edit widget; |
439 | + - edit: the base edit widget; |
440 | + - caption: the caption text widget; |
441 | + - help: the help widget (or None if not present); |
442 | + - error: the error text widget (or None if not present); |
443 | + - suggestions: the suggested values grid (or None if not present); |
444 | + - generate: the generate text widget (or None if not present); |
445 | + - choices: the choices text widget (or None if not present); |
446 | + - default: the default text widget (or None if not present). |
447 | """ |
448 | - help = error = generate = choices = default = None |
449 | + widgets = collections.defaultdict(lambda: None) |
450 | # Retrieve the Pile contents ignoring the last Divider widget. |
451 | contents = list(widget.contents)[:-1] |
452 | first_widget = contents.pop(0)[0] |
453 | if isinstance(first_widget, urwid.Text): |
454 | # This is the error message. |
455 | - error = first_widget |
456 | - wrapper = contents.pop(0)[0] |
457 | + widgets.update({ |
458 | + 'error': first_widget, |
459 | + 'wrapper': contents.pop(0)[0] |
460 | + }) |
461 | else: |
462 | # The widget has no errors. |
463 | - wrapper = first_widget |
464 | - caption_attrs, edit_attrs = wrapper.base_widget.contents |
465 | + widgets['wrapper'] = first_widget |
466 | + caption_attrs, edit_attrs = widgets['wrapper'].base_widget.contents |
467 | + widgets.update({ |
468 | + 'edit': edit_attrs[0], |
469 | + 'caption': caption_attrs[0], |
470 | + }) |
471 | if field.help: |
472 | - help = contents.pop(0)[0] |
473 | + widgets['help'] = contents.pop(0)[0] |
474 | + if hasattr(field, 'suggestions'): |
475 | + widgets['suggestions'] = contents.pop(0)[0] |
476 | if hasattr(field, 'generate'): |
477 | - generate = contents.pop(0)[0] |
478 | + widgets['generate'] = contents.pop(0)[0] |
479 | if hasattr(field, 'choices'): |
480 | - choices = contents.pop(0)[0] |
481 | + widgets['choices'] = contents.pop(0)[0] |
482 | if field.default is not None: |
483 | - default = contents.pop(0)[0] |
484 | - return ( |
485 | - wrapper, edit_attrs[0], caption_attrs[0], |
486 | - help, error, generate, choices, default, |
487 | - ) |
488 | + widgets['default'] = contents.pop(0)[0] |
489 | + return widgets |
490 | |
491 | def test_widget_structure(self): |
492 | # The widget includes all the information about a field. |
493 | @@ -184,28 +224,29 @@ |
494 | 'first-name', label='first name', help='your first name', |
495 | default='Jean') |
496 | widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name') |
497 | - wrapper, edit, caption, help, error, generate, choices, default = ( |
498 | - self.inspect_widget(widget, field)) |
499 | + widgets = self.inspect_widget(widget, field) |
500 | # Since the field is not read-only, the widget is properly enabled. |
501 | - self.assertNotIsInstance(wrapper, urwid.WidgetDisable) |
502 | + self.assertNotIsInstance(widgets['wrapper'], urwid.WidgetDisable) |
503 | # The edit widget is set to the given value. |
504 | - self.assertEqual('Luc', edit.get_edit_text()) |
505 | + self.assertEqual('Luc', widgets['edit'].get_edit_text()) |
506 | # The caption and help are properly set. |
507 | - self.assertEqual('\N{BULLET} first name: ', caption.text) |
508 | - self.assertEqual('your first name', help.text) |
509 | + self.assertEqual('\N{BULLET} first name: ', widgets['caption'].text) |
510 | + self.assertEqual('your first name', widgets['help'].text) |
511 | # The error is displayed. |
512 | - self.assertEqual('invalid name', error.text) |
513 | - # The field is not able to generate a value, and there are no choices. |
514 | - self.assertIsNone(generate) |
515 | - self.assertIsNone(choices) |
516 | + self.assertEqual('invalid name', widgets['error'].text) |
517 | + # The field is not able to generate a value, and there are no choices |
518 | + # or suggestions. |
519 | + self.assertIsNone(widgets['generate']) |
520 | + self.assertIsNone(widgets['suggestions']) |
521 | + self.assertIsNone(widgets['choices']) |
522 | # The default value is properly displayed. |
523 | - self.assertEqual('default if not set: Jean', default.text) |
524 | + self.assertEqual('default if not set: Jean', widgets['default'].text) |
525 | |
526 | def test_value_getter(self): |
527 | # The returned value getter function returns the current widget value. |
528 | field = fields.StringField('first-name') |
529 | widget, value_getter = forms.create_string_widget(field, 'Luc', None) |
530 | - edit = self.inspect_widget(widget, field)[1] |
531 | + edit = self.inspect_widget(widget, field)['edit'] |
532 | self.assertEqual('Luc', value_getter()) |
533 | # The value getter is lazy and always returns the current value. |
534 | edit.set_edit_text('Jean-Luc') |
535 | @@ -227,15 +268,28 @@ |
536 | # The widget is disabled if the field is read-only. |
537 | field = fields.StringField('first-name', readonly=True) |
538 | widget, _ = forms.create_string_widget(field, 'Jean-Luc', None) |
539 | - wrapper = self.inspect_widget(widget, field)[0] |
540 | + wrapper = self.inspect_widget(widget, field)['wrapper'] |
541 | self.assertIsInstance(wrapper, urwid.WidgetDisable) |
542 | |
543 | + def test_suggestions(self): |
544 | + # Suggested values, if present, are properly displayed below the edit |
545 | + # widget. |
546 | + field = fields.SuggestionsStringField( |
547 | + 'captain', suggestions=('Kirk', 'Picard')) |
548 | + widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name') |
549 | + suggestions = self.inspect_widget(widget, field)['suggestions'] |
550 | + captions = [ |
551 | + cli_helpers.get_button_caption(button) |
552 | + for button, attrs in suggestions.contents |
553 | + ] |
554 | + self.assertEqual(['Kirk', 'Picard'], captions) |
555 | + |
556 | def test_autogenerated_field(self): |
557 | # The widgets allows for automatically generating field values. |
558 | field = fields.AutoGeneratedStringField('password') |
559 | with mock.patch.object(field, 'generate', lambda: 'auto-generated!'): |
560 | widget, value_getter = forms.create_string_widget(field, '', None) |
561 | - generate = self.inspect_widget(widget, field)[5] |
562 | + generate = self.inspect_widget(widget, field)['generate'] |
563 | # The generate button is the first widget in the urwid.Columns. |
564 | generate_button = generate.contents[0][0] |
565 | # Click the generate button, and ensure the value has been generated. |
566 | @@ -246,7 +300,7 @@ |
567 | # Possible choices are properly displayed below the edit widget. |
568 | field = fields.ChoiceField('captain', choices=('Kirk', 'Picard')) |
569 | widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name') |
570 | - choices = self.inspect_widget(widget, field)[6] |
571 | + choices = self.inspect_widget(widget, field)['choices'] |
572 | self.assert_choices(['Kirk', 'Picard', 'left empty'], choices) |
573 | |
574 | def test_choices_required_field(self): |
575 | @@ -255,7 +309,7 @@ |
576 | field = fields.ChoiceField( |
577 | 'captain', required=True, choices=('Janeway', 'Sisko')) |
578 | widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name') |
579 | - choices = self.inspect_widget(widget, field)[6] |
580 | + choices = self.inspect_widget(widget, field)['choices'] |
581 | self.assert_choices(['Janeway', 'Sisko'], choices) |
582 | |
583 | |
584 | |
585 | === modified file 'quickstart/tests/cli/test_views.py' |
586 | --- quickstart/tests/cli/test_views.py 2014-01-09 16:18:18 +0000 |
587 | +++ quickstart/tests/cli/test_views.py 2014-01-16 12:48:07 +0000 |
588 | @@ -200,9 +200,10 @@ |
589 | buttons = self.get_widgets_in_contents( |
590 | filter_function=self.is_a(ui.MenuButton)) |
591 | env_types = envs.get_supported_env_types(self.env_type_db) |
592 | - for env_type, button in zip(env_types, buttons[-len(env_types):]): |
593 | - # The caption includes the environment type. |
594 | - expected_caption = 'new {} environment'.format(env_type) |
595 | + for type_tuple, button in zip(env_types, buttons[-len(env_types):]): |
596 | + env_type, label = type_tuple |
597 | + # The caption includes the environment type label. |
598 | + expected_caption = 'new {} environment'.format(label) |
599 | caption = cli_helpers.get_button_caption(button) |
600 | self.assertIn(expected_caption, caption) |
601 | # When the button is clicked, the edit view is called passing the |
602 | |
603 | === modified file 'quickstart/tests/models/test_envs.py' |
604 | --- quickstart/tests/models/test_envs.py 2014-01-10 15:44:36 +0000 |
605 | +++ quickstart/tests/models/test_envs.py 2014-01-16 12:48:07 +0000 |
606 | @@ -584,14 +584,29 @@ |
607 | |
608 | def test_default_environment_removal(self): |
609 | # An environment is successfully removed even if it is the default one. |
610 | + environments = self.env_db['environments'] |
611 | + environments['a-third-one'] = {'type': 'openstack'} |
612 | envs.remove_env(self.env_db, 'lxc') |
613 | - environments = self.env_db['environments'] |
614 | self.assertNotIn('lxc', environments) |
615 | # The other environments are not removed. |
616 | self.assertIn('aws', environments) |
617 | - # The environments database no longer includes a default environment. |
618 | + # Since there are two remaining environments, the environments database |
619 | + # no longer includes a default environment. |
620 | self.assertNotIn('default', self.env_db) |
621 | |
622 | + def test_one_remaining_environment(self): |
623 | + # If, after a removal, only one environment remains, it is |
624 | + # automatically set as default. |
625 | + envs.remove_env(self.env_db, 'lxc') |
626 | + self.assertEqual(1, len(self.env_db['environments'])) |
627 | + self.assertEqual('aws', self.env_db['default']) |
628 | + |
629 | + def test_remove_all_environments(self): |
630 | + # Removing all the environments results in an empty environments db. |
631 | + for env_name in list(self.env_db['environments'].keys()): |
632 | + envs.remove_env(self.env_db, env_name) |
633 | + self.assertEqual(envs.create_empty_env_db(), self.env_db) |
634 | + |
635 | def test_invalid_environment_name(self): |
636 | # A ValueError is raised if the environment is not present in env_db. |
637 | expected = "the environment named u'no-such' does not exist" |
638 | @@ -621,6 +636,11 @@ |
639 | # provider type key. |
640 | self.assertNotEqual(0, len(self.env_type_db)) |
641 | for provider_type, env_metadata in self.env_type_db.items(): |
642 | + # Check the label metadata (not present in the fallback provider). |
643 | + if provider_type != '__fallback__': |
644 | + self.assertIn('label', env_metadata, provider_type) |
645 | + self.assertIsInstance( |
646 | + env_metadata['label'], unicode, provider_type) |
647 | # Check the description metadata. |
648 | self.assertIn('description', env_metadata, provider_type) |
649 | self.assertIsInstance( |
650 | @@ -665,18 +685,51 @@ |
651 | self.assert_fields(expected, env_metadata) |
652 | self.assert_required_fields(expected_required, env_metadata) |
653 | |
654 | + def test_openstack_environment(self): |
655 | + # The openstack environment metadata includes the expected fields. |
656 | + self.assertIn('openstack', self.env_type_db) |
657 | + env_metadata = self.env_type_db['openstack'] |
658 | + expected = [ |
659 | + 'type', 'name', 'admin-secret', 'default-series', |
660 | + 'use-floating-ip', 'control-bucket', 'auth-url', 'tenant-name', |
661 | + 'region', 'auth-mode', 'username', 'password', 'access-key', |
662 | + 'secret-key', 'is-default'] |
663 | + expected_required = [ |
664 | + 'type', 'name', 'admin-secret', 'use-floating-ip', |
665 | + 'control-bucket', 'auth-url', 'tenant-name', 'region', |
666 | + 'is-default'] |
667 | + self.assert_fields(expected, env_metadata) |
668 | + self.assert_required_fields(expected_required, env_metadata) |
669 | + |
670 | + def test_azure_environment(self): |
671 | + # The azure environment metadata includes the expected fields. |
672 | + self.assertIn('azure', self.env_type_db) |
673 | + env_metadata = self.env_type_db['azure'] |
674 | + expected = [ |
675 | + 'type', 'name', 'admin-secret', 'default-series', 'location', |
676 | + 'management-subscription-id', 'management-certificate-path', |
677 | + 'storage-account-name', 'is-default'] |
678 | + expected_required = [ |
679 | + 'type', 'name', 'admin-secret', 'location', |
680 | + 'management-subscription-id', 'management-certificate-path', |
681 | + 'storage-account-name', 'is-default'] |
682 | + self.assert_fields(expected, env_metadata) |
683 | + self.assert_required_fields(expected_required, env_metadata) |
684 | + |
685 | |
686 | class TestGetSupportedEnvTypes(unittest.TestCase): |
687 | |
688 | def test_env_types(self): |
689 | # All the supported env_types but the fallback one are returned. |
690 | env_type_db = envs.get_env_type_db() |
691 | - expected = set(env_type_db) |
692 | - expected.remove('__fallback__') |
693 | - supported_env_types = envs.get_supported_env_types(env_type_db) |
694 | - obtained = set(supported_env_types) |
695 | - self.assertEqual(len(obtained), len(supported_env_types)) |
696 | - self.assertEqual(expected, obtained) |
697 | + expected_env_types = [ |
698 | + ('ec2', 'Amazon EC2'), |
699 | + ('openstack', 'OpenStack (or HP Public Cloud)'), |
700 | + ('azure', 'Windows Azure'), |
701 | + ('local', 'local (LXC)'), |
702 | + ] |
703 | + obtained_env_types = envs.get_supported_env_types(env_type_db) |
704 | + self.assertEqual(expected_env_types, obtained_env_types) |
705 | |
706 | |
707 | class TestGetEnvMetadata(unittest.TestCase): |
708 | |
709 | === modified file 'quickstart/tests/models/test_fields.py' |
710 | --- quickstart/tests/models/test_fields.py 2014-01-07 15:41:55 +0000 |
711 | +++ quickstart/tests/models/test_fields.py 2014-01-16 12:48:07 +0000 |
712 | @@ -457,6 +457,18 @@ |
713 | field.validate(None) |
714 | |
715 | |
716 | +class TestSuggestionsStringField(TestStringField): |
717 | + |
718 | + field_class = fields.SuggestionsStringField |
719 | + |
720 | + def test_suggestions(self): |
721 | + # Suggested values are properly stored as a field attribute. |
722 | + suggestions = ('these', 'are', 'the', 'voyages') |
723 | + field = self.field_class( |
724 | + 'word', suggestions=suggestions, label='selected word') |
725 | + self.assertEqual(suggestions, field.suggestions) |
726 | + |
727 | + |
728 | class TestPasswordField(TestStringField): |
729 | |
730 | field_class = fields.PasswordField |
Reviewers: mp+201616_ code.launchpad. net,
Message:
Please take a look.
Description:
Add support for OpenStack and Azure.
Tests: `make check`
QA: `.venv/bin/python juju-quickstart -i`
Ensure you can successfully create an openstack/HP cloud
and an azure environment.
If you already subscribed to any of those, please
check everything works ok. If not, no problem,
I already bootstrapped HP Cloud and azure.
Since this is the 1.0 version, please ensure
the environment management works well, and in general
quickstart bootstraps the environments and
deploys the GUI as expected.
Thank you!
https:/ /code.launchpad .net/~frankban/ juju-quickstart /more-cloud- providers/ +merge/ 201616
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/52080044/
Affected files (+288, -55 lines): __init_ _.py cli/forms. py models/ envs.py models/ fields. py tests/cli/ test_forms. py tests/models/ test_envs. py tests/models/ test_fields. py
A [revision details]
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/