Merge lp:~frankban/juju-quickstart/new-auth-api-endpoint into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 121
Proposed branch: lp:~frankban/juju-quickstart/new-auth-api-endpoint
Merge into: lp:juju-quickstart
Diff against target: 1044 lines (+441/-91)
12 files modified
quickstart/app.py (+36/-19)
quickstart/jujutools.py (+42/-1)
quickstart/manage.py (+19/-7)
quickstart/models/charms.py (+4/-0)
quickstart/models/jenv.py (+17/-0)
quickstart/settings.py (+8/-2)
quickstart/tests/helpers.py (+1/-0)
quickstart/tests/models/test_charms.py (+28/-0)
quickstart/tests/models/test_jenv.py (+24/-0)
quickstart/tests/test_app.py (+79/-44)
quickstart/tests/test_jujutools.py (+118/-0)
quickstart/tests/test_manage.py (+65/-18)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/new-auth-api-endpoint
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+249102@code.launchpad.net

Description of the change

Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
  properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
  `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
  to ensure backward compatibility.

Done, thank you!

https://codereview.appspot.com/199490043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+249102_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
   properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
   `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
   to ensure backward compatibility.

Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/new-auth-api-endpoint/+merge/249102

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/199490043/

Affected files (+443, -91 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/jujutools.py
   M quickstart/manage.py
   M quickstart/models/charms.py
   M quickstart/models/jenv.py
   M quickstart/settings.py
   M quickstart/tests/helpers.py
   M quickstart/tests/models/test_charms.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_jujutools.py
   M quickstart/tests/test_manage.py

Revision history for this message
Martin Hilton (martin-hilton) wrote :

LGTM: No QA

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
Is it really necessary to have or_none in the name of this function?

https://codereview.appspot.com/199490043/

Revision history for this message
Jeff Pihach (hatch) wrote :

LGTM with some possible cleanups.
QA OK!

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
On 2015/02/10 10:47:32, martin.hilton wrote:
> Is it really necessary to have or_none in the name of this function?

+1

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py
File quickstart/models/jenv.py (right):

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py#newcode134
quickstart/models/jenv.py:134: data =
serializers.yaml_load_from_path(jenv_path)
I believe there are other places in the code which require information
from the jenv file so I figured that this would already be abstracted
out into a utility method already so you could just fetch the value.

https://codereview.appspot.com/199490043/

Revision history for this message
Francesco Banconi (frankban) wrote :

Thanks for the reviews!

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
On 2015/02/10 10:47:32, martin.hilton wrote:
> Is it really necessary to have or_none in the name of this function?

Not strictly necessary, but in the caller context this can help: while
most of the times app functions return values, this can also return
none, i.e. do not rely on the fact the env uuid is always known.

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py
File quickstart/models/jenv.py (right):

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py#newcode134
quickstart/models/jenv.py:134: data =
serializers.yaml_load_from_path(jenv_path)
On 2015/02/10 15:12:09, jeff.pihach wrote:
> I believe there are other places in the code which require information
from the
> jenv file so I figured that this would already be abstracted out into
a utility
> method already so you could just fetch the value.

A slight refactor of the code in the jenv models can help, I agree.
On the other hand, the repeated code is still just two lines, so perhaps
not something for this branch.

https://codereview.appspot.com/199490043/

Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
   properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
   `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
   to ensure backward compatibility.

Done, thank you!

R=martin.hilton, jeff.pihach
CC=
https://codereview.appspot.com/199490043

https://codereview.appspot.com/199490043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2015-02-09 12:34:33 +0000
+++ quickstart/app.py 2015-02-09 18:28:10 +0000
@@ -180,9 +180,9 @@
180def check_bootstrapped(env_name):180def check_bootstrapped(env_name):
181 """Check if the environment named env_name is already bootstrapped.181 """Check if the environment named env_name is already bootstrapped.
182182
183 If so, return the environment API URL to be used to connect to the Juju API183 If so, return the environment API address to be used to connect to the Juju
184 server. If not already bootstrapped, or if the API URL cannot be retrieved,184 API server. If not already bootstrapped, or if the API address cannot be
185 return None.185 retrieved, return None.
186 """186 """
187 if not jenv.exists(env_name):187 if not jenv.exists(env_name):
188 return None188 return None
@@ -190,20 +190,21 @@
190 try:190 try:
191 candidates = jenv.get_value(env_name, 'state-servers')191 candidates = jenv.get_value(env_name, 'state-servers')
192 except ValueError as err:192 except ValueError as err:
193 logging.warn(b'cannot retrieve the Juju API URL: {}'.format(err))193 logging.warn(b'cannot retrieve the Juju API address: {}'.format(err))
194 return None194 return None
195 # Look for a reachable API URL.195 # Look for a reachable API address.
196 if not candidates:196 if not candidates:
197 logging.warn('cannot retrieve the Juju API URL: no addresses found')197 logging.warn(
198 'cannot retrieve the Juju API address: no addresses found')
198 return None199 return None
199 for candidate in candidates:200 for candidate in candidates:
200 error = netutils.check_listening(candidate)201 error = netutils.check_listening(candidate)
201 if error is None:202 if error is None:
202 # Juju API URL found.203 # Juju API address found.
203 return 'wss://{}'.format(candidate)204 return candidate
204 logging.debug(error)205 logging.debug(error)
205 logging.warn(206 logging.warn(
206 'cannot retrieve the Juju API URL: cannot connect to any of the '207 'cannot retrieve the Juju API address: cannot connect to any of the '
207 'following addresses: {}'.format(', '.join(candidates)))208 'following addresses: {}'.format(', '.join(candidates)))
208 return None209 return None
209210
@@ -279,6 +280,21 @@
279 raise ProgramExit('the state server is not ready:\n{}'.format(details))280 raise ProgramExit('the state server is not ready:\n{}'.format(details))
280281
281282
283def get_env_uuid_or_none(env_name):
284 """Return the Juju environment unique id for the given environment name.
285
286 Parse the jenv file to retrieve the environment UUID.
287
288 Return None if the environment UUID is not present in the jenv file.
289 Raise a ProgramExit if the jenv file is not valid.
290 """
291 try:
292 return jenv.get_env_uuid(env_name)
293 except ValueError as err:
294 msg = b'cannot retrieve environment unique identifier: {}'.format(err)
295 raise ProgramExit(msg)
296
297
282def get_credentials(env_name):298def get_credentials(env_name):
283 """Return the Juju credentials for the given environment name.299 """Return the Juju credentials for the given environment name.
284300
@@ -292,11 +308,13 @@
292 raise ProgramExit(msg)308 raise ProgramExit(msg)
293309
294310
295def get_api_url(env_name, juju_command):311def get_api_address(env_name, juju_command):
296 """Return a Juju API URL for the given environment name.312 """Return a Juju API address for the given environment name.
313
314 Only the address is returned, without the schema or the path. For instance:
315 "api.example.com:17070".
297316
298 Use the Juju CLI in a subprocess in order to retrieve the API addresses.317 Use the Juju CLI in a subprocess in order to retrieve the API addresses.
299 Return the complete URL, e.g. "wss://api.example.com:17070".
300 Raise a ProgramExit if any error occurs.318 Raise a ProgramExit if any error occurs.
301 """319 """
302 retcode, output, error = utils.call(320 retcode, output, error = utils.call(
@@ -305,8 +323,7 @@
305 raise ProgramExit(error)323 raise ProgramExit(error)
306 # Assuming there is always at least one API address, grab the first one324 # Assuming there is always at least one API address, grab the first one
307 # from the JSON output.325 # from the JSON output.
308 api_address = json.loads(output)[0]326 return json.loads(output)[0]
309 return 'wss://{}'.format(api_address)
310327
311328
312def connect(api_url, username, password):329def connect(api_url, username, password):
@@ -391,7 +408,8 @@
391 default charm URL is used if the charm store service is not available.408 default charm URL is used if the charm store service is not available.
392409
393 Return a tuple including the following values:410 Return a tuple including the following values:
394 - charm_url: the charm URL that will be used to deploy the service;411 - charm: the charm that will be used to deploy the service, as an
412 instance of "quickstart.models.charms.Charm";
395 - machine: the machine where to deploy to (e.g. "0") or None if a new413 - machine: the machine where to deploy to (e.g. "0") or None if a new
396 machine must be created;414 machine must be created;
397 - service_data: the service info as returned by the mega-watcher for415 - service_data: the service info as returned by the mega-watcher for
@@ -442,7 +460,7 @@
442 (charm.series == bootstrap_node_series)460 (charm.series == bootstrap_node_series)
443 ):461 ):
444 machine = '0'462 machine = '0'
445 return charm_url, machine, service_data, unit_data463 return charm, machine, service_data, unit_data
446464
447465
448def deploy_gui(env, service_name, charm_url, machine, service_data, unit_data):466def deploy_gui(env, service_name, charm_url, machine, service_data, unit_data):
@@ -572,9 +590,8 @@
572def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id):590def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id):
573 """Deploy a bundle.591 """Deploy a bundle.
574592
575 Receive an API URL to a WebSocket server supporting bundle deployments, the593 Receive the environment connection to use for deploying the bundle, the
576 admin_secret to use in the authentication process, the bundle YAML encoded594 bundle YAML encoded contents, the bundle name to be imported and its id.
577 contents and the bundle name to be imported.
578595
579 Raise a ProgramExit if the API server returns an error response.596 Raise a ProgramExit if the API server returns an error response.
580 """597 """
581598
=== modified file 'quickstart/jujutools.py'
--- quickstart/jujutools.py 2015-02-09 12:58:04 +0000
+++ quickstart/jujutools.py 2015-02-09 18:28:10 +0000
@@ -30,6 +30,47 @@
30from quickstart.models import charms30from quickstart.models import charms
3131
3232
33def get_api_url(api_address, juju_version, env_uuid, prefix='', charm=None):
34 """Return the Juju WebSocket API endpoint.
35
36 Receives the Juju API server address, the Juju version and the unique
37 identifier of the current environment.
38
39 Optionally receive a prefix to be used in the path.
40
41 Optionally also receive the Juju GUI charm object as an instance of
42 "quickstart.models.charms.Charm". If provided, the function checks that
43 the specified Juju GUI charm supports the new Juju API endpoint.
44 If not supported, the old endpoint is returned.
45
46 The environment UUID can be None, in which case the old-style API URL
47 (not including the environment UUID) is returned.
48 """
49 base_url = 'wss://{}'.format(api_address)
50 prefix = prefix.strip('/')
51 if prefix:
52 base_url = '{}/{}'.format(base_url, prefix)
53 if (env_uuid is None) or (juju_version < (1, 22, 0)):
54 return base_url
55 complete_url = '{}/environment/{}/api'.format(base_url, env_uuid)
56 if charm is None:
57 return complete_url
58 # If a customized Juju GUI charm is in use, there is no way to check if the
59 # GUI server is recent enough to support the new Juju API endpoints.
60 # In these cases, assume the customized charm is recent enough.
61 if (
62 charm.name != settings.JUJU_GUI_CHARM_NAME or
63 charm.user or
64 charm.is_local()
65 ):
66 return complete_url
67 # This is the promulgated Juju GUI charm. Check if it supports new APIs.
68 revision, series = charm.revision, charm.series
69 if revision < settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT[series]:
70 return base_url
71 return complete_url
72
73
33def get_service_info(status, service_name):74def get_service_info(status, service_name):
34 """Retrieve information on the given service and on its first alive unit.75 """Retrieve information on the given service and on its first alive unit.
3576
@@ -62,7 +103,7 @@
62 Print (to stdout or to logs) info and warnings about the charm URL.103 Print (to stdout or to logs) info and warnings about the charm URL.
63104
64 Return the parsed charm object as an instance of105 Return the parsed charm object as an instance of
65 quickstart.models.charms.Charm.106 "quickstart.models.charms.Charm".
66 """107 """
67 print('charm URL: {}'.format(charm_url))108 print('charm URL: {}'.format(charm_url))
68 charm = charms.Charm.from_url(charm_url)109 charm = charms.Charm.from_url(charm_url)
69110
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2015-02-09 12:58:04 +0000
+++ quickstart/manage.py 2015-02-09 18:28:10 +0000
@@ -32,6 +32,7 @@
32import quickstart32import quickstart
33from quickstart import (33from quickstart import (
34 app,34 app,
35 jujutools,
35 netutils,36 netutils,
36 packaging,37 packaging,
37 platform_support,38 platform_support,
@@ -526,8 +527,8 @@
526 # Bootstrap the Juju environment or reuse an already bootstrapped one.527 # Bootstrap the Juju environment or reuse an already bootstrapped one.
527 already_bootstrapped = True528 already_bootstrapped = True
528 env_type = options.env_type529 env_type = options.env_type
529 api_url = app.check_bootstrapped(options.env_name)530 api_address = app.check_bootstrapped(options.env_name)
530 if api_url is None:531 if api_address is None:
531 print('bootstrapping the {} environment'.format(options.env_name))532 print('bootstrapping the {} environment'.format(options.env_name))
532 if env_type == 'local':533 if env_type == 'local':
533 # If this is a local environment, notify the user that "sudo" will534 # If this is a local environment, notify the user that "sudo" will
@@ -551,9 +552,15 @@
551552
552 # If the environment was not already bootstrapped, we need to retrieve553 # If the environment was not already bootstrapped, we need to retrieve
553 # the API address.554 # the API address.
554 if api_url is None:555 if api_address is None:
555 print('retrieving the Juju API address')556 print('retrieving the Juju API address')
556 api_url = app.get_api_url(options.env_name, juju_command)557 api_address = app.get_api_address(options.env_name, juju_command)
558
559 # Retrieve the Juju environment unique identifier.
560 env_uuid = app.get_env_uuid_or_none(options.env_name)
561
562 # Build the Juju API endpoint based on the Juju version and environment id.
563 api_url = jujutools.get_api_url(api_address, juju_version, env_uuid)
557564
558 # Retrieve the admin-secret for the current environment.565 # Retrieve the admin-secret for the current environment.
559 print('retrieving the Juju environment credentials')566 print('retrieving the Juju environment credentials')
@@ -571,21 +578,26 @@
571 print('environment type: {}'.format(env_type))578 print('environment type: {}'.format(env_type))
572579
573 # Inspect the environment and deploy the charm if required.580 # Inspect the environment and deploy the charm if required.
574 charm_url, machine, service_data, unit_data = app.check_environment(581 charm, machine, service_data, unit_data = app.check_environment(
575 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,582 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,
576 env_type, bootstrap_node_series, already_bootstrapped)583 env_type, bootstrap_node_series, already_bootstrapped)
577 unit_name = app.deploy_gui(584 unit_name = app.deploy_gui(
578 env, settings.JUJU_GUI_SERVICE_NAME, charm_url, machine,585 env, settings.JUJU_GUI_SERVICE_NAME, charm.url(), machine,
579 service_data, unit_data)586 service_data, unit_data)
580587
581 # Observe the deployment progress.588 # Observe the deployment progress.
582 address = app.watch(env, unit_name)589 address = app.watch(env, unit_name)
583 env.close()590 env.close()
591
592 # Print out Juju GUI unit and credential information.
584 url = 'https://{}'.format(address)593 url = 'https://{}'.format(address)
585 print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format(594 print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format(
586 url, username, password))595 url, username, password))
587 gui_api_url = 'wss://{}:443/ws'.format(address)596
597 # Connect to the GUI server WebSocket API.
588 print('connecting to the Juju GUI server')598 print('connecting to the Juju GUI server')
599 gui_api_url = jujutools.get_api_url(
600 address + ':443', juju_version, env_uuid, prefix='ws', charm=charm)
589 gui_env = app.connect(gui_api_url, username, password)601 gui_env = app.connect(gui_api_url, username, password)
590602
591 # Handle bundle deployment.603 # Handle bundle deployment.
592604
=== modified file 'quickstart/models/charms.py'
--- quickstart/models/charms.py 2013-12-06 17:17:19 +0000
+++ quickstart/models/charms.py 2015-02-09 18:28:10 +0000
@@ -118,6 +118,10 @@
118 def __repr__(self):118 def __repr__(self):
119 return b'<Charm: {}>'.format(bytes(self))119 return b'<Charm: {}>'.format(bytes(self))
120120
121 def __eq__(self, other):
122 """Two charms are equal if they have the same URL."""
123 return isinstance(other, self.__class__) and self.url() == other.url()
124
121 def url(self):125 def url(self):
122 """Return the charm URL."""126 """Return the charm URL."""
123 user_part = '~{}/'.format(self.user) if self.user else ''127 user_part = '~{}/'.format(self.user) if self.user else ''
124128
=== modified file 'quickstart/models/jenv.py'
--- quickstart/models/jenv.py 2015-01-12 12:10:38 +0000
+++ quickstart/models/jenv.py 2015-02-09 18:28:10 +0000
@@ -122,6 +122,23 @@
122 return username, password122 return username, password
123123
124124
125def get_env_uuid(env_name):
126 """Return the Juju environment unique identifier.
127
128 Return None if the environment UUID is not included in the jenv file.
129 Raise a ValueError if:
130 - the environment file is not found;
131 - the environment file contents are not parsable by YAML.
132 """
133 jenv_path = _get_jenv_path(env_name)
134 data = serializers.yaml_load_from_path(jenv_path)
135 try:
136 return _get_value_from_yaml(data, 'environ-uuid')
137 except ValueError:
138 # This is probably an old version of Juju.
139 return None
140
141
125def get_env_db():142def get_env_db():
126 """Return an environment database parsing the existing jenv files.143 """Return an environment database parsing the existing jenv files.
127144
128145
=== modified file 'quickstart/settings.py'
--- quickstart/settings.py 2015-01-12 15:07:51 +0000
+++ quickstart/settings.py 2015-02-09 18:28:10 +0000
@@ -40,8 +40,8 @@
40# temporary connection/charm store errors.40# temporary connection/charm store errors.
41# Keep this list sorted by release date (older first).41# Keep this list sorted by release date (older first).
42DEFAULT_CHARM_URLS = collections.OrderedDict((42DEFAULT_CHARM_URLS = collections.OrderedDict((
43 ('precise', 'cs:precise/juju-gui-104'),43 ('precise', 'cs:precise/juju-gui-106'),
44 ('trusty', 'cs:trusty/juju-gui-16'),44 ('trusty', 'cs:trusty/juju-gui-18'),
45))45))
4646
47# The quickstart app short description.47# The quickstart app short description.
@@ -88,3 +88,9 @@
88# supported series. Assume not listed series to always support bundles.88# supported series. Assume not listed series to always support bundles.
89MINIMUM_REVISIONS_FOR_BUNDLES = collections.defaultdict(89MINIMUM_REVISIONS_FOR_BUNDLES = collections.defaultdict(
90 lambda: 0, {'precise': 80})90 lambda: 0, {'precise': 80})
91
92# The minimum Juju GUI charm revision supporting the new Juju API endpoints
93# including the environment UUID. Assume not listed series to always support
94# new endpoints.
95MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT = collections.defaultdict(
96 lambda: 0, {'precise': 107, 'trusty': 19})
9197
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2014-12-17 11:47:43 +0000
+++ quickstart/tests/helpers.py 2015-02-09 18:28:10 +0000
@@ -143,6 +143,7 @@
143 jenv_data = {143 jenv_data = {
144 'user': 'admin',144 'user': 'admin',
145 'password': 'Secret!',145 'password': 'Secret!',
146 'environ-uuid': '__unique_identifier__',
146 'state-servers': ['localhost:17070', '10.0.3.1:17070'],147 'state-servers': ['localhost:17070', '10.0.3.1:17070'],
147 'bootstrap-config': {148 'bootstrap-config': {
148 'admin-secret': 'Secret!',149 'admin-secret': 'Secret!',
149150
=== modified file 'quickstart/tests/models/test_charms.py'
--- quickstart/tests/models/test_charms.py 2013-12-06 17:17:19 +0000
+++ quickstart/tests/models/test_charms.py 2015-02-09 18:28:10 +0000
@@ -205,3 +205,31 @@
205 # The is_local method returns True for local charms.205 # The is_local method returns True for local charms.
206 charm = self.make_charm(schema='local')206 charm = self.make_charm(schema='local')
207 self.assertTrue(charm.is_local())207 self.assertTrue(charm.is_local())
208
209 def test_equality(self):
210 # Two charms are equal if they have the same URL.
211 self.assertEqual(self.make_charm(), self.make_charm())
212
213 def test_equality_different_name(self):
214 # Two charms with different names are not equal.
215 self.assertNotEqual(
216 self.make_charm(name='django'),
217 self.make_charm(name='rails'))
218
219 def test_equality_different_revision(self):
220 # Two charms with different revisions are not equal.
221 self.assertNotEqual(
222 self.make_charm(revision=0),
223 self.make_charm(revision=1))
224
225 def test_equality_different_user(self):
226 # Two charms with different users are not equal.
227 self.assertNotEqual(
228 self.make_charm(user=''),
229 self.make_charm(user='who'))
230
231 def test_equality_different_types(self):
232 # A charm never equals a non-charm object.
233 self.assertNotEqual(self.make_charm(), 42)
234 self.assertNotEqual(self.make_charm(), True)
235 self.assertNotEqual(self.make_charm(), 'oranges')
208236
=== modified file 'quickstart/tests/models/test_jenv.py'
--- quickstart/tests/models/test_jenv.py 2015-01-13 11:46:06 +0000
+++ quickstart/tests/models/test_jenv.py 2015-02-09 18:28:10 +0000
@@ -168,6 +168,30 @@
168 jenv.get_credentials('local')168 jenv.get_credentials('local')
169169
170170
171class TestGetEnvUuid(helpers.JenvFileTestsMixin, unittest.TestCase):
172
173 def test_uuid_found(self):
174 # The environment UUID is correctly returned when included in the jenv.
175 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
176 env_uuid = jenv.get_env_uuid('local')
177 self.assertEqual('__unique_identifier__', env_uuid)
178
179 def test_uuid_not_found(self):
180 # None is returned if the environment UUID is not present in the jenv.
181 data = {'user': 'jean-luc', 'password': 'Secret!'}
182 with self.make_jenv('local', yaml.safe_dump(data)):
183 env_uuid = jenv.get_env_uuid('local')
184 self.assertIsNone(env_uuid)
185
186 def test_invalid_jenv(self):
187 # A ValueError is raised if there are errors parsing the jenv file.
188 expected_error = 'unable to parse file'
189 with self.make_jenv('ec2', ':'):
190 with self.assertRaises(ValueError) as context_manager:
191 jenv.get_env_uuid('ec2')
192 self.assertIn(expected_error, bytes(context_manager.exception))
193
194
171class TestGetEnvDb(helpers.JenvFileTestsMixin, unittest.TestCase):195class TestGetEnvDb(helpers.JenvFileTestsMixin, unittest.TestCase):
172196
173 def test_no_juju_home(self):197 def test_no_juju_home(self):
174198
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2015-01-12 15:00:52 +0000
+++ quickstart/tests/test_app.py 2015-02-09 18:28:10 +0000
@@ -20,6 +20,7 @@
2020
21from contextlib import contextmanager21from contextlib import contextmanager
22import json22import json
23import os
23import unittest24import unittest
2425
25import jujuclient26import jujuclient
@@ -31,6 +32,7 @@
31 platform_support,32 platform_support,
32 settings,33 settings,
33)34)
35from quickstart.models import charms
34from quickstart.tests import helpers36from quickstart.tests import helpers
3537
3638
@@ -457,56 +459,56 @@
457class TestCheckBootstrapped(helpers.JenvFileTestsMixin, unittest.TestCase):459class TestCheckBootstrapped(helpers.JenvFileTestsMixin, unittest.TestCase):
458460
459 def test_no_jenv_file(self):461 def test_no_jenv_file(self):
460 # A None API URL is returned if the jenv file is not present.462 # A None API address is returned if the jenv file is not present.
461 with self.make_jenv('ec2', ''):463 with self.make_jenv('ec2', ''):
462 with helpers.assert_logs([], level='warn'):464 with helpers.assert_logs([], level='warn'):
463 api_url = app.check_bootstrapped('hp')465 api_address = app.check_bootstrapped('hp')
464 self.assertIsNone(api_url)466 self.assertIsNone(api_address)
465467
466 def test_invalid_jenv_file(self):468 def test_invalid_jenv_file(self):
467 # A None API URL is returned if the list of API addresses cannot be469 # A None API address is returned if the list of API addresses cannot be
468 # retrieved from the jenv file.470 # retrieved from the jenv file.
469 with self.make_jenv('ec2', '') as path:471 with self.make_jenv('ec2', '') as path:
470 logs = [472 logs = [
471 'cannot retrieve the Juju API URL: '473 'cannot retrieve the Juju API address: '
472 'cannot read {}: invalid YAML contents: '474 'cannot read {}: invalid YAML contents: '
473 'state-servers key not found in the root section'.format(path)475 'state-servers key not found in the root section'.format(path)
474 ]476 ]
475 with helpers.assert_logs(logs, level='warn'):477 with helpers.assert_logs(logs, level='warn'):
476 api_url = app.check_bootstrapped('ec2')478 api_address = app.check_bootstrapped('ec2')
477 self.assertIsNone(api_url)479 self.assertIsNone(api_address)
478480
479 def test_no_api_addresses(self):481 def test_no_api_addresses(self):
480 # A None API URL is returned if the list of API addresses is empty.482 # A None API address is returned if the list of API addresses is empty.
481 jenv_data = {'state-servers': []}483 jenv_data = {'state-servers': []}
482 logs = ['cannot retrieve the Juju API URL: no addresses found']484 logs = ['cannot retrieve the Juju API address: no addresses found']
483 with self.make_jenv('local', yaml.safe_dump(jenv_data)):485 with self.make_jenv('local', yaml.safe_dump(jenv_data)):
484 with helpers.assert_logs(logs, level='warn'):486 with helpers.assert_logs(logs, level='warn'):
485 api_url = app.check_bootstrapped('local')487 api_address = app.check_bootstrapped('local')
486 self.assertIsNone(api_url)488 self.assertIsNone(api_address)
487489
488 def test_api_address_not_listening(self):490 def test_api_address_not_listening(self):
489 # A None API URL is returned if there is no reachable API address.491 # A None API address is returned if there is no reachable API address.
490 logs = [492 logs = [
491 'cannot retrieve the Juju API URL: '493 'cannot retrieve the Juju API address: '
492 'cannot connect to any of the following addresses: '494 'cannot connect to any of the following addresses: '
493 'localhost:17070, 10.0.3.1:17070'495 'localhost:17070, 10.0.3.1:17070'
494 ]496 ]
495 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):497 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
496 with helpers.assert_logs(logs, level='warn'):498 with helpers.assert_logs(logs, level='warn'):
497 with helpers.patch_socket_create_connection('bad wolf'):499 with helpers.patch_socket_create_connection('bad wolf'):
498 api_url = app.check_bootstrapped('local')500 api_address = app.check_bootstrapped('local')
499 self.assertIsNone(api_url)501 self.assertIsNone(api_address)
500502
501 def test_bootstrapped(self):503 def test_bootstrapped(self):
502 # The first listening API URL is returned if the environment is already504 # The first listening API address is returned if the environment is
503 # bootstrapped.505 # already bootstrapped.
504 with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):506 with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):
505 with helpers.assert_logs([], level='warn'):507 with helpers.assert_logs([], level='warn'):
506 with helpers.patch_socket_create_connection():508 with helpers.patch_socket_create_connection():
507 api_url = app.check_bootstrapped('hp')509 api_address = app.check_bootstrapped('hp')
508 # The first API address is returned.510 # The first API address is returned.
509 self.assertEqual('wss://localhost:17070', api_url)511 self.assertEqual('localhost:17070', api_address)
510512
511513
512class TestBootstrap(514class TestBootstrap(
@@ -700,6 +702,34 @@
700 mock_call.assert_has_calls(expected_calls)702 mock_call.assert_has_calls(expected_calls)
701703
702704
705class TestGetEnvUuidOrNone(
706 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
707
708 def test_success(self):
709 # The environment UUID is successfully retrieved.
710 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
711 env_uuid = app.get_env_uuid_or_none('ec2')
712 self.assertEqual('__unique_identifier__', env_uuid)
713
714 def test_no_uuid(self):
715 # None is returned if the environment UUID is not found.
716 data = {'user': 'jean-luc', 'password': 'Secret!'}
717 with self.make_jenv('ec2', yaml.safe_dump(data)):
718 env_uuid = app.get_env_uuid_or_none('ec2')
719 self.assertIsNone(env_uuid)
720
721 def test_error(self):
722 # A ProgramExit is raised if the environment UUID cannot be retrieved.
723 with self.make_jenv('ec2', '') as path:
724 os.remove(path)
725 expected_error = (
726 'cannot retrieve environment unique identifier: unable to '
727 "open file {}: [Errno 2] No such file or directory: '{}'"
728 ''.format(path, path))
729 with self.assert_program_exit(expected_error):
730 app.get_env_uuid_or_none('ec2')
731
732
703class TestGetCredentials(733class TestGetCredentials(
704 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):734 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
705735
@@ -722,27 +752,27 @@
722 app.get_credentials('ec2')752 app.get_credentials('ec2')
723753
724754
725class TestGetApiUrl(755class TestGetApiAddress(
726 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):756 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
727757
728 env_name = 'ec2'758 env_name = 'ec2'
729 juju_command = settings.JUJU_CMD_PATHS['default']759 juju_command = settings.JUJU_CMD_PATHS['default']
730760
731 def test_success(self):761 def test_success(self):
732 # The API URL is correctly returned.762 # The API address is correctly returned.
733 api_addresses = json.dumps(['api.example.com:17070', 'not-today'])763 api_addresses = json.dumps(['api.example.com:17070', 'not-today'])
734 with self.patch_call(retcode=0, output=api_addresses) as mock_call:764 with self.patch_call(retcode=0, output=api_addresses) as mock_call:
735 api_url = app.get_api_url(self.env_name, self.juju_command)765 api_address = app.get_api_address(self.env_name, self.juju_command)
736 self.assertEqual('wss://api.example.com:17070', api_url)766 self.assertEqual('api.example.com:17070', api_address)
737 mock_call.assert_called_once_with(767 mock_call.assert_called_once_with(
738 self.juju_command, 'api-endpoints', '-e', self.env_name,768 self.juju_command, 'api-endpoints', '-e', self.env_name,
739 '--format', 'json')769 '--format', 'json')
740770
741 def test_failure(self):771 def test_failure(self):
742 # A ProgramExit is raised if an error occurs retrieving the API URL.772 # A ProgramExit is raised if an error occurs retrieving the address.
743 with self.patch_call(retcode=1, error='bad wolf') as mock_call:773 with self.patch_call(retcode=1, error='bad wolf') as mock_call:
744 with self.assert_program_exit('bad wolf'):774 with self.assert_program_exit('bad wolf'):
745 app.get_api_url(self.env_name, self.juju_command)775 app.get_api_address(self.env_name, self.juju_command)
746 mock_call.assert_called_once_with(776 mock_call.assert_called_once_with(
747 self.juju_command, 'api-endpoints', '-e', self.env_name,777 self.juju_command, 'api-endpoints', '-e', self.env_name,
748 '--format', 'json')778 '--format', 'json')
@@ -904,6 +934,11 @@
904 return mock.patch(934 return mock.patch(
905 'quickstart.netutils.get_charm_url', mock_get_charm_url)935 'quickstart.netutils.get_charm_url', mock_get_charm_url)
906936
937 def assert_charm_equal(self, expected_url, charm):
938 """Ensure the given charm has the expected URL."""
939 expected_charm = charms.Charm.from_url(expected_url)
940 self.assertEqual(expected_charm, charm)
941
907 def test_environment_just_bootstrapped(self, mock_print):942 def test_environment_just_bootstrapped(self, mock_print):
908 # The function correctly retrieves the charm URL and machine, and943 # The function correctly retrieves the charm URL and machine, and
909 # handles the case when the charm URL is not provided by the user.944 # handles the case when the charm URL is not provided by the user.
@@ -917,14 +952,14 @@
917 check_preexisting = False952 check_preexisting = False
918 with self.patch_get_charm_url(953 with self.patch_get_charm_url(
919 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:954 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
920 url, machine, service_data, unit_data = app.check_environment(955 charm, machine, service_data, unit_data = app.check_environment(
921 env, 'my-gui', charm_url, env_type, bootstrap_node_series,956 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
922 check_preexisting)957 check_preexisting)
923 # There is no need to call status if the environment was just created.958 # There is no need to call status if the environment was just created.
924 self.assertFalse(env.get_status.called)959 self.assertFalse(env.get_status.called)
925 # The charm URL has been retrieved from the charm store API based on960 # The charm URL has been retrieved from the charm store API based on
926 # the current bootstrap node series.961 # the current bootstrap node series.
927 self.assertEqual('cs:trusty/juju-gui-42', url)962 self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
928 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)963 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
929 # Since the bootstrap node series is supported by the GUI charm, the964 # Since the bootstrap node series is supported by the GUI charm, the
930 # GUI unit can be deployed to machine 0.965 # GUI unit can be deployed to machine 0.
@@ -952,14 +987,14 @@
952 check_preexisting = True987 check_preexisting = True
953 with self.patch_get_charm_url(988 with self.patch_get_charm_url(
954 return_value='cs:precise/juju-gui-42') as mock_get_charm_url:989 return_value='cs:precise/juju-gui-42') as mock_get_charm_url:
955 url, machine, service_data, unit_data = app.check_environment(990 charm, machine, service_data, unit_data = app.check_environment(
956 env, 'my-gui', charm_url, env_type, bootstrap_node_series,991 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
957 check_preexisting)992 check_preexisting)
958 # The environment status has been retrieved.993 # The environment status has been retrieved.
959 env.get_status.assert_called_once_with()994 env.get_status.assert_called_once_with()
960 # The charm URL has been retrieved from the charm store API based on995 # The charm URL has been retrieved from the charm store API based on
961 # the current bootstrap node series.996 # the current bootstrap node series.
962 self.assertEqual('cs:precise/juju-gui-42', url)997 self.assert_charm_equal('cs:precise/juju-gui-42', charm)
963 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)998 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
964 # Since the bootstrap node series is supported by the GUI charm, the999 # Since the bootstrap node series is supported by the GUI charm, the
965 # GUI unit can be deployed to machine 0.1000 # GUI unit can be deployed to machine 0.
@@ -984,13 +1019,13 @@
984 bootstrap_node_series = 'precise'1019 bootstrap_node_series = 'precise'
985 check_preexisting = True1020 check_preexisting = True
986 with self.patch_get_charm_url() as mock_get_charm_url:1021 with self.patch_get_charm_url() as mock_get_charm_url:
987 url, machine, service_data, unit_data = app.check_environment(1022 charm, machine, service_data, unit_data = app.check_environment(
988 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1023 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
989 check_preexisting)1024 check_preexisting)
990 # The environment status has been retrieved.1025 # The environment status has been retrieved.
991 env.get_status.assert_called_once_with()1026 env.get_status.assert_called_once_with()
992 # The charm URL has been retrieved from the environment.1027 # The charm URL has been retrieved from the environment.
993 self.assertEqual('cs:precise/juju-gui-47', url)1028 self.assert_charm_equal('cs:precise/juju-gui-47', charm)
994 self.assertFalse(mock_get_charm_url.called)1029 self.assertFalse(mock_get_charm_url.called)
995 # Since the bootstrap node series is supported by the GUI charm, the1030 # Since the bootstrap node series is supported by the GUI charm, the
996 # GUI unit can be safely deployed to machine 0.1031 # GUI unit can be safely deployed to machine 0.
@@ -1009,12 +1044,12 @@
1009 check_preexisting = False1044 check_preexisting = False
1010 with self.patch_get_charm_url(1045 with self.patch_get_charm_url(
1011 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:1046 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
1012 url, machine, service_data, unit_data = app.check_environment(1047 charm, machine, service_data, unit_data = app.check_environment(
1013 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1048 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1014 check_preexisting)1049 check_preexisting)
1015 # The charm URL has been retrieved from the charm store API using the1050 # The charm URL has been retrieved from the charm store API using the
1016 # most recent supported series.1051 # most recent supported series.
1017 self.assertEqual('cs:trusty/juju-gui-42', url)1052 self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
1018 mock_get_charm_url.assert_called_once_with('trusty')1053 mock_get_charm_url.assert_called_once_with('trusty')
1019 # The Juju GUI unit cannot be deployed to saucy machine 0.1054 # The Juju GUI unit cannot be deployed to saucy machine 0.
1020 self.assertIsNone(machine)1055 self.assertIsNone(machine)
@@ -1034,11 +1069,11 @@
1034 bootstrap_node_series = 'trusty'1069 bootstrap_node_series = 'trusty'
1035 check_preexisting = False1070 check_preexisting = False
1036 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):1071 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):
1037 url, machine, service_data, unit_data = app.check_environment(1072 charm, machine, service_data, unit_data = app.check_environment(
1038 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1073 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1039 check_preexisting)1074 check_preexisting)
1040 # The charm URL has been correctly retrieved from the charm store API.1075 # The charm URL has been correctly retrieved from the charm store API.
1041 self.assertEqual('cs:trusty/juju-gui-42', url)1076 self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
1042 # The Juju GUI unit cannot be deployed to localhost.1077 # The Juju GUI unit cannot be deployed to localhost.
1043 self.assertIsNone(machine)1078 self.assertIsNone(machine)
10441079
@@ -1051,7 +1086,7 @@
1051 bootstrap_node_series = 'trusty'1086 bootstrap_node_series = 'trusty'
1052 check_preexisting = False1087 check_preexisting = False
1053 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):1088 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):
1054 url, machine, service_data, unit_data = app.check_environment(1089 _, machine, _, _ = app.check_environment(
1055 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1090 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1056 check_preexisting)1091 check_preexisting)
1057 self.assertIsNone(machine)1092 self.assertIsNone(machine)
@@ -1065,11 +1100,11 @@
1065 bootstrap_node_series = 'precise'1100 bootstrap_node_series = 'precise'
1066 check_preexisting = False1101 check_preexisting = False
1067 with self.patch_get_charm_url(side_effect=IOError('boo!')):1102 with self.patch_get_charm_url(side_effect=IOError('boo!')):
1068 url, machine, service_data, unit_data = app.check_environment(1103 charm, machine, service_data, unit_data = app.check_environment(
1069 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1104 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1070 check_preexisting)1105 check_preexisting)
1071 # The default charm URL for the given series is returned.1106 # The default charm URL for the given series is returned.
1072 self.assertEqual(settings.DEFAULT_CHARM_URLS['precise'], url)1107 self.assert_charm_equal(settings.DEFAULT_CHARM_URLS['precise'], charm)
1073 self.assertEqual('0', machine)1108 self.assertEqual('0', machine)
10741109
1075 def test_most_recent_default_charm_url(self, mock_print):1110 def test_most_recent_default_charm_url(self, mock_print):
@@ -1082,12 +1117,12 @@
1082 bootstrap_node_series = 'saucy'1117 bootstrap_node_series = 'saucy'
1083 check_preexisting = False1118 check_preexisting = False
1084 with self.patch_get_charm_url(side_effect=IOError('boo!')):1119 with self.patch_get_charm_url(side_effect=IOError('boo!')):
1085 url, machine, service_data, unit_data = app.check_environment(1120 charm, machine, service_data, unit_data = app.check_environment(
1086 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1121 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1087 check_preexisting)1122 check_preexisting)
1088 # The default charm URL for the given series is returned.1123 # The default charm URL for the given series is returned.
1089 series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]1124 series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]
1090 self.assertEqual(settings.DEFAULT_CHARM_URLS[series], url)1125 self.assert_charm_equal(settings.DEFAULT_CHARM_URLS[series], charm)
1091 self.assertIsNone(machine)1126 self.assertIsNone(machine)
10921127
1093 def test_charm_url_provided(self, mock_print):1128 def test_charm_url_provided(self, mock_print):
@@ -1099,14 +1134,14 @@
1099 bootstrap_node_series = 'trusty'1134 bootstrap_node_series = 'trusty'
1100 check_preexisting = False1135 check_preexisting = False
1101 with self.patch_get_charm_url() as mock_get_charm_url:1136 with self.patch_get_charm_url() as mock_get_charm_url:
1102 url, machine, service_data, unit_data = app.check_environment(1137 charm, machine, service_data, unit_data = app.check_environment(
1103 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1138 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1104 check_preexisting)1139 check_preexisting)
1105 # There is no need to call the charmword API if the charm URL is1140 # There is no need to call the charmword API if the charm URL is
1106 # provided by the user.1141 # provided by the user.
1107 self.assertFalse(mock_get_charm_url.called)1142 self.assertFalse(mock_get_charm_url.called)
1108 # The provided charm URL has been correctly returned.1143 # The provided charm URL has been correctly returned.
1109 self.assertEqual(charm_url, url)1144 self.assert_charm_equal(charm_url, charm)
1110 # Since the provided charm series is trusty, the charm itself can be1145 # Since the provided charm series is trusty, the charm itself can be
1111 # safely deployed to machine 0.1146 # safely deployed to machine 0.
1112 self.assertEqual('0', machine)1147 self.assertEqual('0', machine)
@@ -1126,14 +1161,14 @@
1126 bootstrap_node_series = 'precise'1161 bootstrap_node_series = 'precise'
1127 check_preexisting = False1162 check_preexisting = False
1128 with self.patch_get_charm_url() as mock_get_charm_url:1163 with self.patch_get_charm_url() as mock_get_charm_url:
1129 url, machine, service_data, unit_data = app.check_environment(1164 charm, machine, service_data, unit_data = app.check_environment(
1130 env, 'my-gui', charm_url, env_type, bootstrap_node_series,1165 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1131 check_preexisting)1166 check_preexisting)
1132 # There is no need to call the charmword API if the charm URL is1167 # There is no need to call the charmword API if the charm URL is
1133 # provided by the user.1168 # provided by the user.
1134 self.assertFalse(mock_get_charm_url.called)1169 self.assertFalse(mock_get_charm_url.called)
1135 # The provided charm URL has been correctly returned.1170 # The provided charm URL has been correctly returned.
1136 self.assertEqual(charm_url, url)1171 self.assert_charm_equal(charm_url, charm)
1137 # Since the provided charm series is not precise, the charm must be1172 # Since the provided charm series is not precise, the charm must be
1138 # deployed to a new machine.1173 # deployed to a new machine.
1139 self.assertIsNone(machine)1174 self.assertIsNone(machine)
11401175
=== modified file 'quickstart/tests/test_jujutools.py'
--- quickstart/tests/test_jujutools.py 2015-02-09 12:58:04 +0000
+++ quickstart/tests/test_jujutools.py 2015-02-09 18:28:10 +0000
@@ -28,6 +28,124 @@
28from quickstart.tests import helpers28from quickstart.tests import helpers
2929
3030
31class TestGetApiUrl(unittest.TestCase):
32
33 def test_new_url(self):
34 # The new Juju API endpoint is returned if a recent Juju is used.
35 url = jujutools.get_api_url('1.2.3.4:17070', (1, 22, 0), 'env-uuid')
36 self.assertEqual('wss://1.2.3.4:17070/environment/env-uuid/api', url)
37
38 def test_new_url_with_prefix(self):
39 # The new Juju API endpoint is returned with the given path prefix.
40 url = jujutools.get_api_url(
41 '1.2.3.4:17070', (1, 22, 0), 'env-uuid', prefix='/my/path/')
42 self.assertEqual(
43 'wss://1.2.3.4:17070/my/path/environment/env-uuid/api', url)
44
45 def test_old_juju(self):
46 # The old Juju API endpoint is returned if the Juju in use is not a
47 # recent version.
48 url = jujutools.get_api_url('1.2.3.4:17070', (1, 21, 7), 'env-uuid')
49 self.assertEqual('wss://1.2.3.4:17070', url)
50
51 def test_old_juju_with_prefix(self):
52 # The old Juju API endpoint is returned with the given path prefix.
53 url = jujutools.get_api_url(
54 '1.2.3.4:8888', (1, 21, 7), 'env-uuid', 'proxy/')
55 self.assertEqual('wss://1.2.3.4:8888/proxy', url)
56
57 def test_no_env_uuid(self):
58 # The old Juju API endpoint is returned if the environment unique
59 # identifier is unreachable.
60 url = jujutools.get_api_url('1.2.3.4:17070', (1, 23, 42), None)
61 self.assertEqual('wss://1.2.3.4:17070', url)
62
63 def test_no_env_uuid_with_prefix(self):
64 # The old Juju API endpoint is returned with the given path prefix.
65 url = jujutools.get_api_url(
66 '1.2.3.4:17070', (1, 23, 42), None, 'my/prefix')
67 self.assertEqual('wss://1.2.3.4:17070/my/prefix', url)
68
69 def test_new_charm_old_juju(self):
70 # The old Juju API endpoints are used if and old version of Juju is in
71 # use, even if the Juju GUI charm is recent.
72 charm = charms.Charm.from_url('cs:trusty/juju-gui-42')
73 url = jujutools.get_api_url(
74 '1.2.3.4:5678', (1, 21, 7), 'env-uuid', charm=charm)
75 self.assertEqual('wss://1.2.3.4:5678', url)
76
77 def test_customized_charm_unexpected_name(self):
78 # If a customized Juju GUI charm is used, then we assume it supports
79 # the new Juju Login API endpoint (unexpected charm name).
80 charm = charms.Charm.from_url('cs:trusty/the-amazing-gui-0')
81 url = jujutools.get_api_url(
82 'example.com:17070', (1, 22, 2), 'uuid', charm=charm)
83 self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
84
85 def test_customized_charm_unexpected_user(self):
86 # If a customized Juju GUI charm is used, then we assume it supports
87 # the new Juju Login API endpoint (unexpected charm user).
88 charm = charms.Charm.from_url('cs:~who/trusty/juju-gui-0')
89 url = jujutools.get_api_url(
90 'example.com:17070', (1, 22, 2), 'uuid', charm=charm)
91 self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
92
93 def test_customized_charm_unexpected_schema(self):
94 # If a customized Juju GUI charm is used, then we assume it supports
95 # the new Juju Login API endpoint (local charm).
96 charm = charms.Charm.from_url('local:precise/juju-gui-0')
97 url = jujutools.get_api_url(
98 'example.com:17070', (1, 22, 2), 'uuid', prefix='/', charm=charm)
99 self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
100
101 def test_customized_charm_unexpected_series(self):
102 # If a customized Juju GUI charm is used, then we assume it supports
103 # the new Juju Login API endpoint (unsupported charm series).
104 charm = charms.Charm.from_url('cs:vivid/juju-gui-0')
105 url = jujutools.get_api_url(
106 'example.com:22', (1, 22, 2), 'uuid', prefix='ws', charm=charm)
107 self.assertEqual('wss://example.com:22/ws/environment/uuid/api', url)
108
109 def test_recent_precise_charm(self):
110 # The new API endpoints are used if a recent precise charm is in use.
111 charm = charms.Charm.from_url('cs:precise/juju-gui-107')
112 url = jujutools.get_api_url(
113 '1.2.3.4:4747', (1, 42, 0), 'env-id', charm=charm)
114 self.assertEqual('wss://1.2.3.4:4747/environment/env-id/api', url)
115
116 def test_recent_trusty_charm(self):
117 # The new API endpoints are used if a recent trusty charm is in use.
118 charm = charms.Charm.from_url('cs:trusty/juju-gui-19')
119 url = jujutools.get_api_url(
120 '1.2.3.4:4747', (1, 42, 0), 'env-id', charm=charm)
121 self.assertEqual('wss://1.2.3.4:4747/environment/env-id/api', url)
122
123 def test_old_precise_charm(self):
124 # The old API endpoint is returned if the precise Juju GUI charm in use
125 # is outdated.
126 charm = charms.Charm.from_url('cs:precise/juju-gui-106')
127 url = jujutools.get_api_url(
128 '1.2.3.4:4747', (1, 42, 0), 'env-uuid', charm=charm)
129 self.assertEqual('wss://1.2.3.4:4747', url)
130
131 def test_old_trusty_charm(self):
132 # The old API endpoint is returned if the trusty Juju GUI charm in use
133 # is outdated.
134 charm = charms.Charm.from_url('cs:trusty/juju-gui-18')
135 url = jujutools.get_api_url(
136 '1.2.3.4:4747', (1, 42, 0), 'env-uuid', prefix='ws', charm=charm)
137 self.assertEqual('wss://1.2.3.4:4747/ws', url)
138
139 def test_recent_charm_and_prefix(self):
140 # The new API endpoint is returned if a recent charm and a prefix are
141 # both provided. This test exercises the real case in which the GUI
142 # server API endpoint is returned.
143 charm = charms.Charm.from_url('cs:trusty/juju-gui-42')
144 url = jujutools.get_api_url(
145 '1.2.3.4:17070', (1, 22, 0), 'env-id', prefix='ws', charm=charm)
146 self.assertEqual('wss://1.2.3.4:17070/ws/environment/env-id/api', url)
147
148
31class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase):149class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase):
32150
33 def test_service_and_unit(self):151 def test_service_and_unit(self):
34152
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2015-01-12 12:10:38 +0000
+++ quickstart/tests/test_manage.py 2015-02-09 18:28:10 +0000
@@ -40,6 +40,7 @@
40 views,40 views,
41)41)
42from quickstart.models import (42from quickstart.models import (
43 charms,
43 envs,44 envs,
44 jenv,45 jenv,
45)46)
@@ -808,7 +809,7 @@
808 env = mock.Mock()809 env = mock.Mock()
809 defaults = {810 defaults = {
810 # Dependencies are installed.811 # Dependencies are installed.
811 'ensure_dependencies': (1, 18, 0),812 'ensure_dependencies': (1, 22, 0),
812 # Ensure the current Juju version is supported.813 # Ensure the current Juju version is supported.
813 'check_juju_supported': None,814 'check_juju_supported': None,
814 # Ensure the SSH keys are properly configured.815 # Ensure the SSH keys are properly configured.
@@ -819,15 +820,17 @@
819 'bootstrap': False,820 'bootstrap': False,
820 # Status is then called, returning the bootstrap node series.821 # Status is then called, returning the bootstrap node series.
821 'status': 'trusty',822 'status': 'trusty',
822 # The API URL must be retrieved (the environment was not ready).823 # The API address must be retrieved (the environment is not ready).
823 'get_api_url': 'wss://1.2.3.4:17070',824 'get_api_address': '1.2.3.4:17070',
825 # Retrieve the environment unique identifier.
826 'get_env_uuid_or_none': 'env-uuid',
824 # Retrieve the environment credentials.827 # Retrieve the environment credentials.
825 'get_credentials': ('MyUser', 'Secret!'),828 'get_credentials': ('MyUser', 'Secret!'),
826 # Connect to the Juju Environment API endpoint.829 # Connect to the Juju Environment API endpoint.
827 'connect': env,830 'connect': env,
828 # The environment is then checked.831 # The environment is then checked.
829 'check_environment': (832 'check_environment': (
830 'cs:trusty/juju-gui-42',833 charms.Charm.from_url('cs:trusty/juju-gui-42'),
831 '0',834 '0',
832 {'Name': 'juju-gui'},835 {'Name': 'juju-gui'},
833 {'Name': 'juju-gui/0'}836 {'Name': 'juju-gui/0'}
@@ -835,7 +838,7 @@
835 # Deploy the Juju GUI charm.838 # Deploy the Juju GUI charm.
836 'deploy_gui': 'juju-gui/0',839 'deploy_gui': 'juju-gui/0',
837 # Watch the deployment progress and return the unit address.840 # Watch the deployment progress and return the unit address.
838 'watch': '1.2.3.4',841 'watch': '1.2.3.5',
839 # Create the login token for the Juju GUI.842 # Create the login token for the Juju GUI.
840 'create_auth_token': 'TOKEN',843 'create_auth_token': 'TOKEN',
841 }844 }
@@ -859,7 +862,7 @@
859 # Ensure the functions have been used correctly.862 # Ensure the functions have been used correctly.
860 mock_app.ensure_dependencies.assert_called_once_with(863 mock_app.ensure_dependencies.assert_called_once_with(
861 options.distro_only, options.platform, self.juju_command)864 options.distro_only, options.platform, self.juju_command)
862 mock_app.check_juju_supported.assert_called_once_with((1, 18, 0))865 mock_app.check_juju_supported.assert_called_once_with((1, 22, 0))
863 mock_app.ensure_ssh_keys.assert_called_once_with()866 mock_app.ensure_ssh_keys.assert_called_once_with()
864 mock_app.check_bootstrapped.assert_called_once_with(options.env_name)867 mock_app.check_bootstrapped.assert_called_once_with(options.env_name)
865 mock_app.bootstrap.assert_called_once_with(868 mock_app.bootstrap.assert_called_once_with(
@@ -870,13 +873,20 @@
870 constraints=options.constraints)873 constraints=options.constraints)
871 mock_app.status.assert_called_once_with(874 mock_app.status.assert_called_once_with(
872 options.env_name, self.juju_command)875 options.env_name, self.juju_command)
873 mock_app.get_api_url.assert_called_once_with(876 mock_app.get_api_address.assert_called_once_with(
874 options.env_name, self.juju_command)877 options.env_name, self.juju_command)
878 mock_app.get_env_uuid_or_none.assert_called_once_with(options.env_name)
875 mock_app.get_credentials.assert_called_once_with(options.env_name)879 mock_app.get_credentials.assert_called_once_with(options.env_name)
876 mock_app.connect.assert_has_calls([880 mock_app.connect.assert_has_calls([
877 mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'),881 mock.call(
882 'wss://1.2.3.4:17070/environment/env-uuid/api',
883 'MyUser',
884 'Secret!'),
878 mock.call().close(),885 mock.call().close(),
879 mock.call('wss://1.2.3.4:443/ws', 'MyUser', 'Secret!'),886 mock.call(
887 'wss://1.2.3.5:443/ws/environment/env-uuid/api',
888 'MyUser',
889 'Secret!'),
880 mock.call().close(),890 mock.call().close(),
881 ])891 ])
882 mock_app.check_environment.assert_called_once_with(892 mock_app.check_environment.assert_called_once_with(
@@ -887,24 +897,61 @@
887 '0', {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})897 '0', {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})
888 mock_app.watch.assert_called_once_with(env, 'juju-gui/0')898 mock_app.watch.assert_called_once_with(env, 'juju-gui/0')
889 mock_app.create_auth_token.assert_called_once_with(env)899 mock_app.create_auth_token.assert_called_once_with(env)
890 mock_open.assert_called_once_with('https://1.2.3.4/?authtoken=TOKEN')900 mock_open.assert_called_once_with('https://1.2.3.5/?authtoken=TOKEN')
891 # Ensure some of the app function have not been called.901 # Ensure some of the app function have not been called.
892 self.assertFalse(mock_app.get_env_type.called)902 self.assertFalse(mock_app.get_env_type.called)
893 self.assertFalse(mock_app.deploy_bundle.called)903 self.assertFalse(mock_app.deploy_bundle.called)
894904
905 def test_old_juju_api_endpoint(self, mock_app, mock_open):
906 # The old Juju WebSocket API endpoint is used if the Juju version is
907 # not recent enough.
908 self.configure_app(mock_app, ensure_dependencies=(1, 19, 0))
909 # Run the application.
910 options = self.make_options()
911 with self.patch_get_juju_command():
912 manage.run(options)
913 mock_app.connect.assert_has_calls([
914 mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'),
915 mock.call().close(),
916 mock.call('wss://1.2.3.5:443/ws', 'MyUser', 'Secret!'),
917 mock.call().close(),
918 ])
919
920 def test_new_api_endpoint_old_charm(self, mock_app, mock_open):
921 # Even if the Juju version is new, the old GUI server login API is used
922 # if the charm in the environment is not recent enough.
923 self.configure_app(mock_app, check_environment=(
924 charms.Charm.from_url('cs:trusty/juju-gui-0'),
925 '0',
926 {'Name': 'juju-gui'},
927 {'Name': 'juju-gui/0'}
928 ))
929 # Run the application.
930 options = self.make_options()
931 with self.patch_get_juju_command():
932 manage.run(options)
933 mock_app.connect.assert_has_calls([
934 mock.call(
935 'wss://1.2.3.4:17070/environment/env-uuid/api',
936 'MyUser',
937 'Secret!'),
938 mock.call().close(),
939 mock.call('wss://1.2.3.5:443/ws', 'MyUser', 'Secret!'),
940 mock.call().close(),
941 ])
942
895 def test_already_bootstrapped(self, mock_app, mock_open):943 def test_already_bootstrapped(self, mock_app, mock_open):
896 # The application correctly reuses an already bootstrapped environment.944 # The application correctly reuses an already bootstrapped environment.
897 env = self.configure_app(945 env = self.configure_app(mock_app, check_bootstrapped='example.com')
898 mock_app, check_bootstrapped='wss://example.com')
899 # Run the application.946 # Run the application.
900 options = self.make_options()947 options = self.make_options()
901 with self.patch_get_juju_command():948 with self.patch_get_juju_command():
902 manage.run(options)949 manage.run(options)
903 # The environment type is retrieved from the jenv.950 # The environment type is retrieved from the jenv.
904 mock_app.get_env_type.assert_called_once_with(env)951 mock_app.get_env_type.assert_called_once_with(env)
905 # No reason to call bootstrap or get_api_url functions.952 # No reason to call bootstrap or get_api_address functions.
906 self.assertFalse(mock_app.bootstrap.called)953 self.assertFalse(mock_app.bootstrap.called)
907 self.assertFalse(mock_app.get_api_url.called)954 self.assertFalse(mock_app.get_api_address.called)
908955
909 def test_already_bootstrapped_race(self, mock_app, mock_open):956 def test_already_bootstrapped_race(self, mock_app, mock_open):
910 # The application correctly reuses an already bootstrapped environment.957 # The application correctly reuses an already bootstrapped environment.
@@ -915,8 +962,8 @@
915 options = self.make_options()962 options = self.make_options()
916 with self.patch_get_juju_command():963 with self.patch_get_juju_command():
917 manage.run(options)964 manage.run(options)
918 # The bootstrap and get_api_url functions are still called, but this965 # The bootstrap and get_api_address functions are still called, but
919 # time also get_env_type is required.966 # this time also get_env_type is required.
920 # The environment type is retrieved from the jenv.967 # The environment type is retrieved from the jenv.
921 mock_app.bootstrap.assert_called_once_with(968 mock_app.bootstrap.assert_called_once_with(
922 options.env_name, self.juju_command,969 options.env_name, self.juju_command,
@@ -924,7 +971,7 @@
924 upload_tools=options.upload_tools,971 upload_tools=options.upload_tools,
925 upload_series=options.upload_series,972 upload_series=options.upload_series,
926 constraints=options.constraints)973 constraints=options.constraints)
927 mock_app.get_api_url.assert_called_once_with(974 mock_app.get_api_address.assert_called_once_with(
928 options.env_name, self.juju_command)975 options.env_name, self.juju_command)
929 mock_app.get_env_type.assert_called_once_with(env)976 mock_app.get_env_type.assert_called_once_with(env)
930977
@@ -938,7 +985,7 @@
938 manage.run(options)985 manage.run(options)
939 # Ensure the browser is still open without an auth token.986 # Ensure the browser is still open without an auth token.
940 mock_app.create_auth_token.assert_called_once_with(env)987 mock_app.create_auth_token.assert_called_once_with(env)
941 mock_open.assert_called_once_with('https://1.2.3.4')988 mock_open.assert_called_once_with('https://1.2.3.5')
942989
943 def test_bundle(self, mock_app, mock_open):990 def test_bundle(self, mock_app, mock_open):
944 # A bundle is correctly deployed by the application.991 # A bundle is correctly deployed by the application.

Subscribers

People subscribed via source and target branches