Author: Francesco Banconi <francesco.banconi@canonical.com>
Description: Support trusty environments
Origin: backport, http://bazaar.launchpad.net/~juju-gui/juju-quickstart/trunk/revision/66
Bug: https://launchpad.net/bugs/1306537
Bug-Ubuntu: https://launchpad.net/bugs/1306537
Last-Update: 2014-06-27

Modified from upstream commit by Robie Basak <robie.basak@ubuntu.com>, to make
SRU more minimal:
 * Dropped version bump to 1.3.2 in quickstart/__init__.py.
 * Dropped whitespace noise hunk in quickstart/app.py.

------------------------------------------------------------
revno: 66 [merge]
committer: Francesco Banconi <francesco.banconi@canonical.com>
branch nick: juju-quickstart
timestamp: Wed 2014-04-30 11:19:39 -0700
message:
  Support trusty environments.
  
  Add the ability to deploy the trusty charm.
  Introduced the concept of multiple supported
  series for the Juju GUI charm.
  
  Split the app.deploy_gui function in two
  separate function:
  - check_environment inspects the environment
    and returns the data required to deploy the GUI;
  - deploy_gui's only responsibility is to
    return when the GUI service is deployed/exposed
    and the unit created.
  
  Include the default-series field in the auto-generated
  local environment. This is the environment that
  quickstart offers to automatically create when no other
  environments are found.
  
  Also propose "trusty" as the default series when manually 
  creating new environments.
  
  Bump version up: while this branch 
  incidentally fixes bug 1306537 [1],
  the ability to deploy the GUI on trusty
  can be considered a new feature.
  
  My apologies for the long diff.
  
  Tests: `make check`.
  
  QA:
  Use quickstart like the following:
    `.venv/bin/python juju-quickstart [-i]`.
  
  You should be able to deploy the trusty GUI charm.
  
  If you are on trusty, the trusty charm should be deployed
  when the default-series field is missing. 
  This must be tested also using the local provider, in which case
  Juju is currently not able to deploy precise charms when the 
  bootstrap node is trusty (bug 1306537).
  
  In general quickstart should deploy the charm series corresponding
  to the bootstrap node series: so on trusty environments the trusty
  charm should be installed, on precise environments the precise one.
  
  This way, at least when the bootstrap node series is precise or trusty,
  quickstart is able to add the GUI unit to machine 0. You can test it
  using, e.g. an ec2 environment.
  
  This is true also when providing a custom charm URL, e.g.:
    `.venv/bin/python juju-quickstart --gui-charm-url cs:~juju-gui/trusty/juju-gui-1`
  
  In all the other cases, quickstart uses the trusty charm. You can test this
  by using quickstart with an ec2 environment having "default-series: saucy":
  a trusty GUI should be deployed on machine 1.
  
  Two final checks:
  - try to create a new environment with quickstart: the default-series
    field should be pre-filled with "trusty";
  - move temporarily your environments.yaml somewhere else, and let quickstart
    auto-generate a local environment for you: the deployment process should
    succeed and the environment should include the "trusty" default series.
  
  Thanks a lot for all of this, and sorry for the long QA: this is going to
  be released in trusty, so your efforts are really appreciated!
  
  [1] https://bugs.launchpad.net/juju-core/+bug/1306537
  
  R=
  CC=
  https://codereview.appspot.com/90570044

=== modified file 'quickstart/app.py'
--- old/quickstart/app.py	2014-04-21 21:05:13 +0000
+++ new/quickstart/app.py	2014-04-24 15:33:13 +0000
@@ -325,27 +324,41 @@
     return result['Token']
 
 
-def deploy_gui(
-        env, service_name, machine, charm_url=None, check_preexisting=False):
-    """Deploy and expose the given service, reusing the bootstrap node.
-
-    Only deploy the service if not already present in the environment.
-    Do not add a unit to the service if the unit is already there.
-
-    Receive an authenticated Juju Environment instance, the name of the
-    service, the machine where to deploy to (or None for a new machine),
-    the optional Juju GUI charm URL (e.g. cs:~juju-gui/precise/juju-gui-42),
-    and a flag (check_preexisting) that can be set to True in order to make
-    the function check for a pre-existing service and/or unit.
+def check_environment(
+        env, service_name, charm_url, env_type, bootstrap_node_series,
+        check_preexisting):
+    """Inspect the current Juju environment.
+
+    This function is responsible for retrieving all the information required
+    to deploy the Juju GUI charm to the current environment.
+
+    Receive the following arguments:
+        - env: an authenticated Juju Environment instance;
+        - service_name: the name of the service to look for;
+        - charm_url: a fully qualified charm URL if the user requested the
+          deployment of a custom charm, or None otherwise;
+        - env_type: the environment's provider type;
+        - bootstrap_node_series: the bootstrap node series;
+        - check_preexisting: whether to check for a pre-existing service and/or
+          unit.
 
     If the charm URL is not provided, and the service is not already deployed,
-    the function tries to retrieve it from charmworld. In this case a default
-    charm URL is used if charmworld is not available.
-
-    Return the name of the first running unit belonging to the given service.
+    the function tries to retrieve it from the charmworld API. In this case a
+    default charm URL is used if charmworld is not available.
+
+    Return a tuple including the following values:
+        - charm_url: the charm URL that will be used to deploy the service;
+        - machine: the machine where to deploy to (e.g. "0") or None if a new
+          machine must be created;
+        - service_data: the service info as returned by the mega-watcher for
+          services, or None if the service is not present in the environment;
+        - unit_data: the unit info as returned by the mega-watcher for units,
+          or None if the unit is not present in the environment.
+
     Raise a ProgramExit if the API server returns an error response.
     """
-    service_data, unit_data = None, None
+    print('bootstrap node series: {}'.format(bootstrap_node_series))
+    service_data, unit_data, machine = None, None, None
     if check_preexisting:
         # The service and/or the unit can be already in the environment.
         try:
@@ -355,16 +368,56 @@
         service_data, unit_data = utils.get_service_info(status, service_name)
     if service_data is None:
         # The service does not exist in the environment.
-        print('requesting {} deployment'.format(service_name))
         if charm_url is None:
+            # The user did not provide a customized charm.
+            if bootstrap_node_series in settings.JUJU_GUI_SUPPORTED_SERIES:
+                series = bootstrap_node_series
+            else:
+                series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]
             try:
-                charm_url = utils.get_charm_url()
+                # Try to get the charm URL from charmworld.
+                charm_url = utils.get_charm_url(series)
             except (IOError, ValueError) as err:
+                # Fall back to the default URL for the current series.
                 msg = 'unable to retrieve the {} charm URL from the API: {}'
                 logging.warn(msg.format(service_name, err))
-                charm_url = settings.DEFAULT_CHARM_URL
-        utils.check_gui_charm_url(charm_url)
-        # Deploy the service without units.
+                charm_url = settings.DEFAULT_CHARM_URLS[series]
+    else:
+        # A deployed service already exists in the environment: ignore the
+        # provided charm URL and just use the already deployed charm.
+        charm_url = service_data['CharmURL']
+    charm = utils.parse_gui_charm_url(charm_url)
+    # Deploy on the bootstrap node if we are not using the local provider, and
+    # if the requested charm and the bootstrap node have the same series.
+    if (env_type != 'local') and (charm.series == bootstrap_node_series):
+        machine = '0'
+    return charm_url, machine, service_data, unit_data
+
+
+def deploy_gui(env, service_name, charm_url, machine, service_data, unit_data):
+    """Deploy and expose the given service to the given machine.
+
+    Only deploy the service if not already present in the environment.
+    Do not add a unit to the service if the unit is already there.
+
+    Receive the following arguments:
+        - env: an authenticated Juju Environment instance;
+        - service_name: the name of the service to be deployed;
+        - charm_url: the fully qualified charm URL to be used to deploy the
+          service;
+        - machine: the machine where to deploy to (e.g. "0") or None if a new
+          machine must be created;
+        - service_data: the service info as returned by the mega-watcher for
+          services, or None if the service is not present in the environment;
+        - unit_data: the unit info as returned by the mega-watcher for units,
+          or None if the unit is not present in the environment.
+
+    Return the name of the first running unit belonging to the given service.
+    Raise a ProgramExit if the API server returns an error response.
+    """
+    if service_data is None:
+        # The service is not in the environment: deploy it without units.
+        print('requesting {} deployment'.format(service_name))
         try:
             env.deploy(service_name, charm_url, num_units=0)
         except jujuclient.EnvError as err:
@@ -374,7 +427,6 @@
     else:
         # We already have the service in the environment.
         print('service {} already deployed'.format(service_name))
-        utils.check_gui_charm_url(service_data['CharmURL'])
         service_exposed = service_data.get('Exposed', False)
     # At this point the service is surely deployed in the environment: expose
     # it if necessary and add a unit if it is missing.

=== modified file 'quickstart/cli/views.py'
--- old/quickstart/cli/views.py	2014-01-16 12:46:45 +0000
+++ new/quickstart/cli/views.py	2014-04-23 15:53:32 +0000
@@ -233,8 +233,9 @@
         urwid.Divider(),
     ])
     # The Juju GUI can be safely installed in the bootstrap node only if its
-    # series is "precise". Suggest this setting by pre-filling the value.
-    preferred_series = settings.JUJU_GUI_PREFERRED_SERIES
+    # series matches one of the series supported by the GUI.
+    # Suggest the most recent supported series by pre-filling the value.
+    preferred_series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]
     widgets.extend([
         ui.MenuButton(
             ['\N{BULLET} new ', ('highlight', label), ' environment'],

=== modified file 'quickstart/manage.py'
--- old/quickstart/manage.py	2014-04-22 12:20:10 +0000
+++ new/quickstart/manage.py	2014-04-23 11:22:44 +0000
@@ -153,10 +153,9 @@
         # The user requested a bundle deployment.
         options.bundle and
         # This is the official Juju GUI charm.
-        charm.name == settings.JUJU_GUI_CHARM_NAME and
-        not charm.user and
+        charm.name == settings.JUJU_GUI_CHARM_NAME and not charm.user and
         # The charm at this revision does not support bundle deployments.
-        charm.revision < settings.MINIMUM_CHARM_REVISION_FOR_BUNDLES
+        charm.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[charm.series]
     ):
         return parser.error(
             'bundle deployments not supported by the requested charm '
@@ -449,16 +448,15 @@
 
     print('bootstrapping the {} environment (type: {})'.format(
         options.env_name, options.env_type))
-    is_local = options.env_type == 'local'
     requires_sudo = False
-    if is_local:
+    if options.env_type == 'local':
         # If this is a local environment, notify the user that "sudo" will be
         # required to bootstrap the application, even in newer Juju versions
         # where "sudo" is invoked by juju-core itself.
         print('sudo privileges will be required to bootstrap the environment')
         # If the Juju version is less than 1.17.2 then use sudo for local envs.
         requires_sudo = juju_version < (1, 17, 2)
-    already_bootstrapped, bsn_series = app.bootstrap(
+    already_bootstrapped, bootstrap_node_series = app.bootstrap(
         options.env_name, requires_sudo=requires_sudo, debug=options.debug)
 
     # Retrieve the admin-secret for the current environment.
@@ -478,15 +476,15 @@
     print('connecting to {}'.format(api_url))
     env = app.connect(api_url, admin_secret)
 
-    # It is not possible to deploy on the bootstrap node if we are using the
-    # local provider, or if the bootstrap node series is not compatible with
-    # the Juju GUI charm.
-    machine = '0'
-    if is_local or (bsn_series != settings.JUJU_GUI_PREFERRED_SERIES):
-        machine = None
+    # Inspect the environment and deploy the charm if required.
+    charm_url, machine, service_data, unit_data = app.check_environment(
+        env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,
+        options.env_type, bootstrap_node_series, already_bootstrapped)
     unit_name = app.deploy_gui(
-        env, settings.JUJU_GUI_SERVICE_NAME, machine,
-        charm_url=options.charm_url, check_preexisting=already_bootstrapped)
+        env, settings.JUJU_GUI_SERVICE_NAME, charm_url, machine,
+        service_data, unit_data)
+
+    # Observe the deployment progress.
     address = app.watch(env, unit_name)
     env.close()
     url = 'https://{}'.format(address)

=== modified file 'quickstart/models/envs.py'
--- old/quickstart/models/envs.py	2014-04-21 21:02:09 +0000
+++ new/quickstart/models/envs.py	2014-04-23 11:22:44 +0000
@@ -344,7 +344,14 @@
     either optional or suitable for automatic generation of their values. For
     this reason local environments can be safely created given just their name.
     """
-    env_data = {'type': 'local', 'name': name, 'is-default': is_default}
+    env_data = {
+        'type': 'local',
+        'name': name,
+        'is-default': is_default,
+        # Workaround for bug 1306537: force tools for the most recent Juju GUI
+        # supported series to be uploaded.
+        'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1],
+    }
     env_metadata = get_env_metadata(env_type_db, env_data)
     # Retrieve a list of missing required fields.
     missing_fields = [
@@ -402,7 +409,6 @@
     default_series_field = fields.ChoiceField(
         'default-series', choices=settings.JUJU_DEFAULT_SERIES,
         label='default series', required=False,
-        default=settings.JUJU_GUI_PREFERRED_SERIES,
         help='the default Ubuntu series to use for the bootstrap node')
     is_default_field = fields.BoolField(
         'is-default', label='default', allow_mixed=False, required=True,

=== modified file 'quickstart/settings.py'
--- old/quickstart/settings.py	2014-04-03 13:11:27 +0000
+++ new/quickstart/settings.py	2014-04-23 15:53:32 +0000
@@ -18,15 +18,22 @@
 
 from __future__ import unicode_literals
 
+import collections
 import os
 
 
-# The URL containing information about the last Juju GUI charm version.
-CHARMWORLD_API = 'http://manage.jujucharms.com/api/3/charm/precise/juju-gui'
+# The base charmworld API URL containing information about charms.
+# This URL must be formatted with a series and a charm name.
+CHARMWORLD_API = 'http://manage.jujucharms.com/api/3/charm/{series}/{charm}'
 
-# The default Juju GUI charm URL to use when it is not possible to retrieve it
-# from the charmworld API, e.g. due to temporary connection/charmworld errors.
-DEFAULT_CHARM_URL = 'cs:precise/juju-gui-86'
+# The default Juju GUI charm URLs for each supported series. Used when it is
+# not possible to retrieve the charm URL from the charmworld API, e.g. due to
+# temporary connection/charmworld errors.
+# Keep this list sorted by release date (older first).
+DEFAULT_CHARM_URLS = collections.OrderedDict((
+    ('precise', 'cs:precise/juju-gui-90'),
+    ('trusty', 'cs:trusty/juju-gui-2'),
+))
 
 # The quickstart app short description.
 DESCRIPTION = 'set up a Juju environment (including the GUI) in very few steps'
@@ -50,11 +57,9 @@
 JUJU_GUI_SERVICE_NAME = JUJU_GUI_CHARM_NAME
 
 # The set of series supported by the Juju GUI charm.
-JUJU_GUI_SUPPORTED_SERIES = ('precise',)
-
-# The preferred series for the Juju GUI charm.  It will be the newest,
-# assuming our naming convention holds.
-JUJU_GUI_PREFERRED_SERIES = sorted(JUJU_GUI_SUPPORTED_SERIES).pop()
-
-# The minimum Juju GUI charm revision supporting bundle deployments.
-MINIMUM_CHARM_REVISION_FOR_BUNDLES = 80
+JUJU_GUI_SUPPORTED_SERIES = tuple(DEFAULT_CHARM_URLS.keys())
+
+# The minimum Juju GUI charm revision supporting bundle deployments, for each
+# supported series. Assume not listed series to always support bundles.
+MINIMUM_REVISIONS_FOR_BUNDLES = collections.defaultdict(
+    lambda: 0, {'precise': 80})

=== modified file 'quickstart/tests/cli/test_views.py'
--- old/quickstart/tests/cli/test_views.py	2014-01-28 20:35:42 +0000
+++ new/quickstart/tests/cli/test_views.py	2014-04-23 12:20:35 +0000
@@ -212,7 +212,7 @@
             mock_env_edit.assert_called_once_with(
                 self.app, self.env_type_db, env_db, self.save_callable,
                 {'type': env_type,
-                 'default-series': settings.JUJU_GUI_PREFERRED_SERIES})
+                 'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1]})
             # Reset the mock so that it does not include any calls on the next
             # loop cycle.
             mock_env_edit.reset_mock()

=== modified file 'quickstart/tests/helpers.py'
--- old/quickstart/tests/helpers.py	2014-03-14 12:43:23 +0000
+++ new/quickstart/tests/helpers.py	2014-04-23 13:08:12 +0000
@@ -226,13 +226,8 @@
 class WatcherDataTestsMixin(object):
     """Shared methods for testing Juju mega-watcher data."""
 
-    def _make_change(self, entity, action, default_data, data):
-        if data is not None:
-            default_data.update(data)
-        return entity, action, default_data
-
-    def make_service_change(self, action='change', data=None):
-        """Create and return a change on a service.
+    def make_service_data(self, data=None):
+        """Create and return a data dictionary for a service.
 
         The passed data can be used to override default values.
         """
@@ -242,12 +237,30 @@
             'Life': 'alive',
             'Name': 'my-gui',
         }
-        return self._make_change('service', action, default_data, data)
+        if data is not None:
+            default_data.update(data)
+        return default_data
+
+    def make_service_change(self, action='change', data=None):
+        """Create and return a change on a service.
+
+        The passed data can be used to override default values.
+        """
+        return 'service', action, self.make_service_data(data)
+
+    def make_unit_data(self, data=None):
+        """Create and return a data dictionary for a unit.
+
+        The passed data can be used to override default values.
+        """
+        default_data = {'Name': 'my-gui/47', 'Service': 'my-gui'}
+        if data is not None:
+            default_data.update(data)
+        return default_data
 
     def make_unit_change(self, action='change', data=None):
         """Create and return a change on a unit.
 
         The passed data can be used to override default values.
         """
-        default_data = {'Name': 'my-gui/47', 'Service': 'my-gui'}
-        return self._make_change('unit', action, default_data, data)
+        return 'unit', action, self.make_unit_data(data)

=== modified file 'quickstart/tests/models/test_envs.py'
--- old/quickstart/tests/models/test_envs.py	2014-04-21 21:02:09 +0000
+++ new/quickstart/tests/models/test_envs.py	2014-04-23 12:20:35 +0000
@@ -29,6 +29,7 @@
 import mock
 import yaml
 
+from quickstart import settings
 from quickstart.models import (
     envs,
     fields,
@@ -619,8 +620,13 @@
         # value should be the admin-secret.
         admin_secret = env_data.pop('admin-secret', '')
         self.assertNotEqual(0, len(admin_secret))
-        expected = {'type': 'local', 'name': 'my-lxc', 'is-default': False}
-        self.assertEqual(expected, env_data)
+        expected_env_data = {
+            'type': 'local',
+            'name': 'my-lxc',
+            'is-default': False,
+            'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1],
+        }
+        self.assertEqual(expected_env_data, env_data)
 
     def test_default(self):
         # The resulting env_data is correctly structured for default envs.
@@ -629,8 +635,13 @@
         # See the comment about auto-generated fields in the test method above.
         admin_secret = env_data.pop('admin-secret', '')
         self.assertNotEqual(0, len(admin_secret))
-        expected = {'type': 'local', 'name': 'my-default', 'is-default': True}
-        self.assertEqual(expected, env_data)
+        expected_env_data = {
+            'type': 'local',
+            'name': 'my-default',
+            'is-default': True,
+            'default-series': settings.JUJU_GUI_SUPPORTED_SERIES[-1],
+        }
+        self.assertEqual(expected_env_data, env_data)
 
 
 class TestRemoveEnv(

=== modified file 'quickstart/tests/test_app.py'
--- old/quickstart/tests/test_app.py	2014-04-21 21:02:09 +0000
+++ new/quickstart/tests/test_app.py	2014-04-23 15:53:32 +0000
@@ -725,173 +725,357 @@
 
 
 @helpers.mock_print
-class TestDeployGui(
+class TestCheckEnvironment(
         ProgramExitTestsMixin, helpers.WatcherDataTestsMixin,
         unittest.TestCase):
 
-    charm_url = 'cs:precise/juju-gui-100'
-
-    def make_env(self, unit_name=None, service_data=None, unit_data=None):
+    def make_env(self, include_data=False, side_effect=None):
         """Create and return a mock environment object.
 
-        Set up the object so that a call to add_unit returns the given
-        unit_name, and a call to status returns a status object containing the
-        service and unit described by the given service_data and unit_data.
+        If include_data is True, set up the object so that a call to status
+        returns a status object containing service and unit data.
+
+        The side_effect argument can be used to simulate status errors.
         """
         env = mock.Mock()
-        # Set up the add_unit return value.
-        if unit_name is not None:
-            env.add_unit.return_value = {'Units': [unit_name]}
         # Set up the get_status return value.
         status = []
-        if service_data is not None:
-            status.append(self.make_service_change(data=service_data))
-        if unit_data is not None:
-            status.append(self.make_unit_change(data=unit_data))
+        if include_data:
+            status = [self.make_service_change(), self.make_unit_change()]
         env.get_status.return_value = status
+        env.get_status.side_effect = side_effect
         return env
 
-    def patch_get_charm_url(self, side_effect=None):
+    def patch_get_charm_url(self, return_value=None, side_effect=None):
         """Patch the get_charm_url helper function."""
-        if side_effect is None:
-            side_effect = [self.charm_url]
-        mock_get_charm_url = mock.Mock(side_effect=side_effect)
+        mock_get_charm_url = mock.Mock(
+            return_value=return_value, side_effect=side_effect)
         return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url)
 
-    def check_provided_charm_url(
-            self, charm_url, mock_print, expected_logs=None):
-        """Ensure the service is deployed and exposed with the given charm URL.
-
-        Also check the expected warnings, if they are provided, are logged.
-        """
-        env = self.make_env(unit_name='my-gui/42')
-        with helpers.assert_logs(expected_logs or [], level='warn'):
-            app.deploy_gui(env, 'my-gui', '0', charm_url=charm_url)
-        env.assert_has_calls([
-            mock.call.deploy('my-gui', charm_url, num_units=0),
-            mock.call.expose('my-gui'),
-            mock.call.add_unit('my-gui', machine_spec='0'),
-        ])
-        mock_print.assert_has_calls([
-            mock.call('requesting my-gui deployment'),
-            mock.call('charm URL: {}'.format(charm_url)),
-        ])
-
-    def check_existing_charm_url(
-            self, charm_url, mock_print, expected_logs=None):
-        """Ensure the service is correctly found with the given charm URL.
-
-        Also check the expected warnings, if they are provided, are logged.
-        """
-        service_data = {'CharmURL': charm_url}
-        env = self.make_env(unit_name='my-gui/42', service_data=service_data)
-        with helpers.assert_logs(expected_logs or [], level='warn'):
-            app.deploy_gui(env, 'my-gui', '0', check_preexisting=True)
-        env.assert_has_calls([
-            mock.call.get_status(),
-            mock.call.add_unit('my-gui', machine_spec='0'),
-        ])
-        mock_print.assert_has_calls([
-            mock.call('service my-gui already deployed'),
-            mock.call('charm URL: {}'.format(charm_url)),
-        ])
-
-    def test_deployment(self, mock_print):
-        # The function correctly deploys and exposes the service, retrieving
-        # the charm URL from the charmworld API.
-        env = self.make_env(unit_name='my-gui/42')
-        with self.patch_get_charm_url():
-            unit_name = app.deploy_gui(env, 'my-gui', '0')
-        self.assertEqual('my-gui/42', unit_name)
-        env.assert_has_calls([
-            mock.call.deploy('my-gui', self.charm_url, num_units=0),
-            mock.call.expose('my-gui'),
-            mock.call.add_unit('my-gui', machine_spec='0'),
-        ])
+    def test_environment_just_bootstrapped(self, mock_print):
+        # The function correctly retrieves the charm URL and machine, and
+        # handles the case when the charm URL is not provided by the user.
+        # In this scenario, the environment has been bootstrapped by
+        # quickstart, so there is no need to check its status. For this reason,
+        # service_data and unit_data should be set to None.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'trusty'
+        check_preexisting = False
+        with self.patch_get_charm_url(
+                return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
         # There is no need to call status if the environment was just created.
         self.assertFalse(env.get_status.called)
+        # The charm URL has been retrieved from charmworld based on the current
+        # bootstrap node series.
+        self.assertEqual('cs:trusty/juju-gui-42', url)
+        mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
+        # Since the bootstrap node series is supported by the GUI charm, the
+        # GUI unit can be deployed to machine 0.
+        self.assertEqual('0', machine)
+        # When not checking for pre-existing service and/or unit, the
+        # corresponding service and unit data are set to None.
+        self.assertIsNone(service_data)
+        self.assertIsNone(unit_data)
+        # Ensure the function output makes sense.
+        self.assertEqual(2, mock_print.call_count)
         mock_print.assert_has_calls([
-            mock.call('requesting my-gui deployment'),
-            mock.call('charm URL: {}'.format(self.charm_url)),
-            mock.call('my-gui deployment request accepted'),
-            mock.call('exposing service my-gui'),
-            mock.call('requesting new unit deployment'),
-            mock.call('my-gui/42 deployment request accepted'),
+            mock.call('bootstrap node series: trusty'),
+            mock.call('charm URL: cs:trusty/juju-gui-42'),
         ])
 
     def test_existing_environment_without_entities(self, mock_print):
-        # The deployment is processed in an already bootstrapped environment
-        # with no relevant entities in it.
-        env = self.make_env(unit_name='my-gui/42')
-        with self.patch_get_charm_url():
-            unit_name = app.deploy_gui(
-                env, 'my-gui', '0', check_preexisting=True)
-        self.assertEqual('my-gui/42', unit_name)
-        env.assert_has_calls([
-            mock.call.get_status(),
-            mock.call.deploy('my-gui', self.charm_url, num_units=0),
-            mock.call.expose('my-gui'),
-            mock.call.add_unit('my-gui', machine_spec='0'),
-        ])
+        # The function correctly retrieves the charm URL and machine.
+        # In this scenario, the environment was already bootstrapped, but it
+        # does not include the GUI. For this reason, service_data and unit_data
+        # are set to None.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'precise'
+        check_preexisting = True
+        with self.patch_get_charm_url(
+                return_value='cs:precise/juju-gui-42') as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The environment status has been retrieved.
+        env.get_status.assert_called_once_with()
+        # The charm URL has been retrieved from charmworld based on the current
+        # bootstrap node series.
+        self.assertEqual('cs:precise/juju-gui-42', url)
+        mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
+        # Since the bootstrap node series is supported by the GUI charm, the
+        # GUI unit can be deployed to machine 0.
+        self.assertEqual('0', machine)
+        # The service and unit data are set to None.
+        self.assertIsNone(service_data)
+        self.assertIsNone(unit_data)
+        # Ensure the function output makes sense.
+        self.assertEqual(2, mock_print.call_count)
+        mock_print.assert_has_calls([
+            mock.call('bootstrap node series: precise'),
+            mock.call('charm URL: cs:precise/juju-gui-42'),
+        ])
+
+    def test_existing_environment_with_entities(self, mock_print):
+        # The function correctly retrieves the charm URL and machine when the
+        # environment is already bootstrapped and includes a Juju GUI unit.
+        # In this case service_data and unit_data are actually populated.
+        env = self.make_env(include_data=True)
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'precise'
+        check_preexisting = True
+        with self.patch_get_charm_url() as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The environment status has been retrieved.
+        env.get_status.assert_called_once_with()
+        # The charm URL has been retrieved from the environment.
+        self.assertEqual('cs:precise/juju-gui-47', url)
+        self.assertFalse(mock_get_charm_url.called)
+        # Since the bootstrap node series is supported by the GUI charm, the
+        # GUI unit can be safely deployed to machine 0.
+        self.assertEqual('0', machine)
+        # The service and unit data are correctly returned.
+        self.assertEqual(self.make_service_data(), service_data)
+        self.assertEqual(self.make_unit_data(), unit_data)
+
+    def test_bootstrap_node_series_not_supported(self, mock_print):
+        # If the bootstrap node is not suitable for hosting the Juju GUI unit,
+        # the returned machine is set to None.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'saucy'
+        check_preexisting = False
+        with self.patch_get_charm_url(
+                return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The charm URL has been retrieved from charmworld using the most
+        # recent supported series.
+        self.assertEqual('cs:trusty/juju-gui-42', url)
+        mock_get_charm_url.assert_called_once_with('trusty')
+        # The Juju GUI unit cannot be deployed to saucy machine 0.
+        self.assertIsNone(machine)
+        # Ensure the function output makes sense.
+        self.assertEqual(2, mock_print.call_count)
+        mock_print.assert_has_calls([
+            mock.call('bootstrap node series: saucy'),
+            mock.call('charm URL: cs:trusty/juju-gui-42'),
+        ])
+
+    def test_local_provider(self, mock_print):
+        # If the local provider is used the Juju GUI unit cannot be deployed to
+        # machine 0.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'local'
+        bootstrap_node_series = 'trusty'
+        check_preexisting = False
+        with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The charm URL has been correctly retrieved from charmworld.
+        self.assertEqual('cs:trusty/juju-gui-42', url)
+        # The Juju GUI unit cannot be deployed to localhost.
+        self.assertIsNone(machine)
 
     def test_default_charm_url(self, mock_print):
-        # The function correctly deploys and exposes the service, even if it is
-        # not able to retrieve the charm URL from the charmworld API.
+        # A default charm URL suitable to be deployed in the bootstrap node is
+        # returned if the charmworld API is not reachable.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'precise'
+        check_preexisting = False
+        with self.patch_get_charm_url(side_effect=IOError('boo!')):
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The default charm URL for the given series is returned.
+        self.assertEqual(settings.DEFAULT_CHARM_URLS['precise'], url)
+        self.assertEqual('0', machine)
+
+    def test_most_recent_default_charm_url(self, mock_print):
+        # The default charm URL corresponding to the most recent series
+        # supported by the GUI is returned if the charmworld API is not
+        # reachable and the bootstrap node cannot host the Juju GUI unit.
+        env = self.make_env()
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'saucy'
+        check_preexisting = False
+        with self.patch_get_charm_url(side_effect=IOError('boo!')):
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # The default charm URL for the given series is returned.
+        series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]
+        self.assertEqual(settings.DEFAULT_CHARM_URLS[series], url)
+        self.assertIsNone(machine)
+
+    def test_charm_url_provided(self, mock_print):
+        # The function knows when a custom charm URL can be deployed in the
+        # bootstrap node.
+        env = self.make_env()
+        charm_url = 'cs:~juju-gui/trusty/juju-gui-100'
+        env_type = 'ec2'
+        bootstrap_node_series = 'trusty'
+        check_preexisting = False
+        with self.patch_get_charm_url() as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # There is no need to call the charmword API if the charm URL is
+        # provided by the user.
+        self.assertFalse(mock_get_charm_url.called)
+        # The provided charm URL has been correctly returned.
+        self.assertEqual(charm_url, url)
+        # Since the provided charm series is trusty, the charm itself can be
+        # safely deployed to machine 0.
+        self.assertEqual('0', machine)
+        # Ensure the function output makes sense.
+        self.assertEqual(2, mock_print.call_count)
+        mock_print.assert_has_calls([
+            mock.call('bootstrap node series: trusty'),
+            mock.call('charm URL: cs:~juju-gui/trusty/juju-gui-100'),
+        ])
+
+    def test_charm_url_provided_series_not_supported(self, mock_print):
+        # The function knows when a custom charm URL cannot be deployed in the
+        # bootstrap node.
+        env = self.make_env()
+        charm_url = 'cs:~juju-gui/trusty/juju-gui-100'
+        env_type = 'ec2'
+        bootstrap_node_series = 'precise'
+        check_preexisting = False
+        with self.patch_get_charm_url() as mock_get_charm_url:
+            url, machine, service_data, unit_data = app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        # There is no need to call the charmword API if the charm URL is
+        # provided by the user.
+        self.assertFalse(mock_get_charm_url.called)
+        # The provided charm URL has been correctly returned.
+        self.assertEqual(charm_url, url)
+        # Since the provided charm series is not precise, the charm must be
+        # deployed to a new machine.
+        self.assertIsNone(machine)
+        # Ensure the function output makes sense.
+        self.assertEqual(2, mock_print.call_count)
+        mock_print.assert_has_calls([
+            mock.call('bootstrap node series: precise'),
+            mock.call('charm URL: cs:~juju-gui/trusty/juju-gui-100'),
+        ])
+
+    def test_status_error(self, mock_print):
+        # A ProgramExit is raised if an error occurs in the status API call.
+        env = self.make_env(side_effect=self.make_env_error('bad wolf'))
+        charm_url = None
+        env_type = 'ec2'
+        bootstrap_node_series = 'trusty'
+        check_preexisting = True
+        with self.assert_program_exit('bad API response: bad wolf'):
+            app.check_environment(
+                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
+                check_preexisting)
+        env.get_status.assert_called_once_with()
+
+
+@helpers.mock_print
+class TestDeployGui(
+        ProgramExitTestsMixin, helpers.WatcherDataTestsMixin,
+        unittest.TestCase):
+
+    charm_url = 'cs:trusty/juju-gui-42'
+
+    def make_env(self, unit_name=None):
+        """Create and return a mock environment object.
+
+        Set up the mock object so that a call to env.add_unit returns the given
+        unit_name.
+        """
+        env = mock.Mock()
+        # Set up the add_unit return value.
+        if unit_name is not None:
+            env.add_unit.return_value = {'Units': [unit_name]}
+        return env
+
+    def test_deployment(self, mock_print):
+        # The function correctly deploys and exposes the service in the case
+        # the service and its unit are not present in the environment.
         env = self.make_env(unit_name='my-gui/42')
-        log = 'unable to retrieve the my-gui charm URL from the API: boo!'
-        with self.patch_get_charm_url(side_effect=IOError('boo!')):
-            # A warning is logged which notifies we are using the default URL.
-            with helpers.assert_logs([log], level='warn'):
-                app.deploy_gui(env, 'my-gui', '0')
+        service_data = unit_data = None
+        unit_name = app.deploy_gui(
+            env, 'my-gui', self.charm_url, '0', service_data, unit_data)
+        self.assertEqual('my-gui/42', unit_name)
         env.assert_has_calls([
-            mock.call.deploy(
-                'my-gui', settings.DEFAULT_CHARM_URL, num_units=0),
+            # The service has been deployed.
+            mock.call.deploy('my-gui', self.charm_url, num_units=0),
+            # The service has been exposed.
             mock.call.expose('my-gui'),
+            # One service unit has been added.
             mock.call.add_unit('my-gui', machine_spec='0'),
         ])
+        self.assertEqual(5, mock_print.call_count)
         mock_print.assert_has_calls([
             mock.call('requesting my-gui deployment'),
-            mock.call('charm URL: {}'.format(settings.DEFAULT_CHARM_URL)),
+            mock.call('my-gui deployment request accepted'),
+            mock.call('exposing service my-gui'),
+            mock.call('requesting new unit deployment'),
+            mock.call('my-gui/42 deployment request accepted'),
         ])
 
     def test_existing_service(self, mock_print):
         # The deployment is executed reusing an already deployed service.
-        env = self.make_env(unit_name='my-gui/42', service_data={})
+        env = self.make_env(unit_name='my-gui/42')
+        service_data = self.make_service_data()
+        unit_data = None
         unit_name = app.deploy_gui(
-            env, 'my-gui', '0', check_preexisting=True)
+            env, 'my-gui', self.charm_url, '0', service_data, unit_data)
         self.assertEqual('my-gui/42', unit_name)
-        env.assert_has_calls([
-            mock.call.get_status(),
-            mock.call.add_unit('my-gui', machine_spec='0'),
-        ])
+        # One service unit has been added.
+        env.add_unit.assert_called_once_with('my-gui', machine_spec='0')
         # The service is not re-deployed.
         self.assertFalse(env.deploy.called)
         # The service is not re-exposed.
         self.assertFalse(env.expose.called)
+        self.assertEqual(3, mock_print.call_count)
         mock_print.assert_has_calls([
             mock.call('service my-gui already deployed'),
-            mock.call('charm URL: cs:precise/juju-gui-47'),
             mock.call('requesting new unit deployment'),
             mock.call('my-gui/42 deployment request accepted'),
         ])
 
     def test_existing_service_unexposed(self, mock_print):
         # The existing service is exposed if required.
-        service_data = {'Exposed': False}
-        env = self.make_env(unit_name='my-gui/42', service_data=service_data)
+        env = self.make_env(unit_name='my-gui/42')
+        service_data = self.make_service_data({'Exposed': False})
+        unit_data = None
         unit_name = app.deploy_gui(
-            env, 'my-gui', '1', check_preexisting=True)
+            env, 'my-gui', self.charm_url, '1', service_data, unit_data)
         self.assertEqual('my-gui/42', unit_name)
         env.assert_has_calls([
-            mock.call.get_status(),
+            # The service has been exposed.
             mock.call.expose('my-gui'),
+            # One service unit has been added.
             mock.call.add_unit('my-gui', machine_spec='1'),
         ])
         # The service is not re-deployed.
         self.assertFalse(env.deploy.called)
+        self.assertEqual(4, mock_print.call_count)
         mock_print.assert_has_calls([
             mock.call('service my-gui already deployed'),
-            mock.call('charm URL: cs:precise/juju-gui-47'),
             mock.call('exposing service my-gui'),
             mock.call('requesting new unit deployment'),
             mock.call('my-gui/42 deployment request accepted'),
@@ -899,105 +1083,49 @@
 
     def test_existing_service_and_unit(self, mock_print):
         # A unit is reused if a suitable one is already present.
-        env = self.make_env(service_data={}, unit_data={})
+        env = self.make_env()
+        service_data = self.make_service_data()
+        unit_data = self.make_unit_data()
         unit_name = app.deploy_gui(
-            env, 'my-gui', '0', check_preexisting=True)
+            env, 'my-gui', self.charm_url, '0', service_data, unit_data)
         self.assertEqual('my-gui/47', unit_name)
-        env.get_status.assert_called_once_with()
         # The service is not re-deployed.
         self.assertFalse(env.deploy.called)
         # The service is not re-exposed.
         self.assertFalse(env.expose.called)
         # The unit is not re-added.
         self.assertFalse(env.add_unit.called)
+        self.assertEqual(2, mock_print.call_count)
         mock_print.assert_has_calls([
             mock.call('service my-gui already deployed'),
-            mock.call('charm URL: cs:precise/juju-gui-47'),
             mock.call('reusing unit my-gui/47'),
         ])
 
     def test_new_machine(self, mock_print):
         # The unit is correctly deployed in a new machine.
         env = self.make_env(unit_name='my-gui/42')
-        with self.patch_get_charm_url():
-            unit_name = app.deploy_gui(env, 'my-gui', None)
+        service_data = unit_data = None
+        unit_name = app.deploy_gui(
+            env, 'my-gui', self.charm_url, None, service_data, unit_data)
         self.assertEqual('my-gui/42', unit_name)
         env.assert_has_calls([
+            # The service has been deployed.
             mock.call.deploy('my-gui', self.charm_url, num_units=0),
+            # The service has been exposed.
             mock.call.expose('my-gui'),
+            # One service unit has been added to a new machine.
             mock.call.add_unit('my-gui', machine_spec=None),
         ])
 
-    def test_offical_charm_url_provided(self, mock_print):
-        # The function correctly deploys and exposes the service using a user
-        # provided revision of the Juju GUI charm URL.
-        self.check_provided_charm_url('cs:precise/juju-gui-4242', mock_print)
-
-    def test_customized_charm_url_provided(self, mock_print):
-        # A customized charm URL is correctly recognized and logged if provided
-        # by the user.
-        self.check_provided_charm_url(
-            'cs:~juju-gui/precise/juju-gui-42', mock_print,
-            expected_logs=['using a customized juju-gui charm'])
-
-    def test_outdated_charm_url_provided(self, mock_print):
-        # An outdated charm URL is correctly recognized and logged if provided
-        # by the user.
-        self.check_provided_charm_url(
-            'cs:precise/juju-gui-1', mock_print,
-            expected_logs=[
-                'charm is outdated and may not support bundle deployments'])
-
-    def test_unexpected_charm_url_provided(self, mock_print):
-        # An unexpected charm URL is correctly recognized and logged if
-        # provided by the user.
-        self.check_provided_charm_url(
-            'cs:precise/exterminate-the-gui-666', mock_print,
-            expected_logs=[
-                'unexpected URL for the juju-gui charm: '
-                'the service may not work as expected'])
-
-    def test_offical_charm_url_existing(self, mock_print):
-        # An existing official charm URL is correctly found.
-        self.check_existing_charm_url('cs:precise/juju-gui-4242', mock_print)
-
-    def test_customized_charm_url_existing(self, mock_print):
-        # An existing customized charm URL is correctly found and logged.
-        self.check_existing_charm_url(
-            'cs:~juju-gui/precise/juju-gui-42', mock_print,
-            expected_logs=['using a customized juju-gui charm'])
-
-    def test_outdated_charm_url_existing(self, mock_print):
-        # An existing but outdated charm URL is correctly found and logged.
-        self.check_existing_charm_url(
-            'cs:precise/juju-gui-1', mock_print,
-            expected_logs=[
-                'charm is outdated and may not support bundle deployments'])
-
-    def test_unexpected_charm_url_existing(self, mock_print):
-        # An existing but unexpected charm URL is correctly found and logged.
-        self.check_existing_charm_url(
-            'cs:precise/exterminate-the-gui-666', mock_print,
-            expected_logs=[
-                'unexpected URL for the juju-gui charm: '
-                'the service may not work as expected'])
-
-    def test_status_error(self, mock_print):
-        # A ProgramExit is raised if an error occurs in the status API call.
-        env = self.make_env()
-        env.get_status.side_effect = self.make_env_error('bad wolf')
-        with self.assert_program_exit('bad API response: bad wolf'):
-            app.deploy_gui(
-                env, 'another-gui', '0', check_preexisting=True)
-        env.get_status.assert_called_once_with()
-
     def test_deploy_error(self, mock_print):
         # A ProgramExit is raised if an error occurs in the deploy API call.
         env = self.make_env()
         env.deploy.side_effect = self.make_env_error('bad wolf')
-        with self.patch_get_charm_url():
-            with self.assert_program_exit('bad API response: bad wolf'):
-                app.deploy_gui(env, 'another-gui', '0')
+        service_data = unit_data = None
+        with self.assert_program_exit('bad API response: bad wolf'):
+            app.deploy_gui(
+                env, 'another-gui', self.charm_url, '0',
+                service_data, unit_data)
         env.deploy.assert_called_once_with(
             'another-gui', self.charm_url, num_units=0)
 
@@ -1005,18 +1133,22 @@
         # A ProgramExit is raised if an error occurs in the expose API call.
         env = self.make_env()
         env.expose.side_effect = self.make_env_error('bad wolf')
-        with self.patch_get_charm_url():
-            with self.assert_program_exit('bad API response: bad wolf'):
-                app.deploy_gui(env, 'another-gui', '0')
+        service_data = unit_data = None
+        with self.assert_program_exit('bad API response: bad wolf'):
+            app.deploy_gui(
+                env, 'another-gui', self.charm_url, '0',
+                service_data, unit_data)
         env.expose.assert_called_once_with('another-gui')
 
     def test_add_unit_error(self, mock_print):
         # A ProgramExit is raised if an error occurs in the add_unit API call.
         env = self.make_env()
         env.add_unit.side_effect = self.make_env_error('bad wolf')
-        with self.patch_get_charm_url():
-            with self.assert_program_exit('bad API response: bad wolf'):
-                app.deploy_gui(env, 'another-gui', '0')
+        service_data = unit_data = None
+        with self.assert_program_exit('bad API response: bad wolf'):
+            app.deploy_gui(
+                env, 'another-gui', self.charm_url, '0',
+                service_data, unit_data)
         env.add_unit.assert_called_once_with('another-gui', machine_spec='0')
 
     def test_other_errors(self, mock_print):
@@ -1024,9 +1156,11 @@
         error = ValueError('explode!')
         env = self.make_env(unit_name='my-gui/42')
         env.expose.side_effect = error
-        with self.patch_get_charm_url():
-            with self.assertRaises(ValueError) as context_manager:
-                app.deploy_gui(env, 'juju-gui', '0')
+        service_data = unit_data = None
+        with self.assertRaises(ValueError) as context_manager:
+            app.deploy_gui(
+                env, 'juju-gui', self.charm_url, '0',
+                service_data, unit_data)
         env.deploy.assert_called_once_with(
             'juju-gui', self.charm_url, num_units=0)
         env.expose.assert_called_once_with('juju-gui')

=== modified file 'quickstart/tests/test_manage.py'
--- old/quickstart/tests/test_manage.py	2014-04-22 12:20:10 +0000
+++ new/quickstart/tests/test_manage.py	2014-04-23 13:08:12 +0000
@@ -719,9 +719,19 @@
     def test_no_bundle(self, mock_app, mock_open):
         # The application runs correctly if no bundle is provided.
         mock_app.ensure_dependencies.return_value = (1, 18, 0)
-        mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
+        mock_app.bootstrap.return_value = (True, 'trusty')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        service_data = {'Name': 'juju-gui'}
+        unit_data = {'Name': 'juju-gui/0'}
+        mock_app.check_environment.return_value = (
+            'cs:trusty/juju-gui-42', '0', service_data, unit_data)
         mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error
+        # Make mock_app.watch return the Juju GUI unit address.
         mock_app.watch.return_value = '1.2.3.4'
+        # Make mock_app.create_auth_token return a fake authentication token.
         mock_app.create_auth_token.return_value = 'AUTHTOKEN'
         options = self.make_options()
         manage.run(options)
@@ -736,10 +746,13 @@
             mock.call('wss://1.2.3.4:443/ws', options.admin_secret),
             mock.call().close(),
         ])
+        mock_app.check_environment.assert_called_once_with(
+            mock_app.connect(), settings.JUJU_GUI_SERVICE_NAME,
+            options.charm_url, options.env_type, mock_app.bootstrap()[1],
+            mock_app.bootstrap()[0])
         mock_app.deploy_gui.assert_called_once_with(
-            mock_app.connect(), settings.JUJU_GUI_SERVICE_NAME, '0',
-            charm_url=options.charm_url,
-            check_preexisting=mock_app.bootstrap()[0])
+            mock_app.connect(), settings.JUJU_GUI_SERVICE_NAME,
+            'cs:trusty/juju-gui-42', '0', service_data, unit_data)
         mock_app.watch.assert_called_once_with(
             mock_app.connect(), mock_app.deploy_gui())
         mock_app.create_auth_token.assert_called_once_with(mock_app.connect())
@@ -748,8 +761,16 @@
         self.assertFalse(mock_app.deploy_bundle.called)
 
     def test_no_token(self, mock_app, mock_open):
+        # The process continues even if the authentication token cannot be
+        # retrieved.
         mock_app.create_auth_token.return_value = None
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         options = self.make_options()
         manage.run(options)
         mock_app.create_auth_token.assert_called_once_with(mock_app.connect())
@@ -761,7 +782,14 @@
         options = self.make_options(
             bundle='/my/bundle/file.yaml', bundle_yaml='mybundle: contents',
             bundle_name='mybundle', bundle_services=['service1', 'service2'])
-        mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
+        mock_app.bootstrap.return_value = (True, 'trusty')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:trusty/juju-gui-42', '0', None, None)
+        # Make mock_app.watch return the Juju GUI unit address.
         mock_app.watch.return_value = 'gui.example.com'
         manage.run(options)
         mock_app.deploy_bundle.assert_called_once_with(
@@ -774,7 +802,13 @@
         options = self.make_options(env_type='local')
         versions = [
             (1, 17, 2), (1, 17, 10), (1, 18, 0), (1, 18, 2), (2, 16, 1)]
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         for version in versions:
             mock_app.ensure_dependencies.return_value = version
             manage.run(options)
@@ -788,7 +822,13 @@
         # Sudo privileges are required if the Juju version is < 1.17.2.
         options = self.make_options(env_type='local')
         versions = [(0, 7, 9), (1, 0, 0), (1, 16, 42), (1, 17, 0), (1, 17, 1)]
-        mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
+        mock_app.bootstrap.return_value = (True, 'trusty')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:trusty/juju-gui-42', '0', None, None)
         for version in versions:
             mock_app.ensure_dependencies.return_value = version
             manage.run(options)
@@ -800,14 +840,26 @@
         # Sudo privileges are never required for non-local environments.
         options = self.make_options(env_type='ec2')
         mock_app.ensure_dependencies.return_value = (1, 14, 0)
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         manage.run(options)
         mock_app.bootstrap.assert_called_once_with(
             options.env_name, requires_sudo=False, debug=options.debug)
 
     def test_no_browser(self, mock_app, mock_open):
         # It is possible to avoid opening the GUI in the browser.
-        mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
+        mock_app.bootstrap.return_value = (True, 'trusty')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:trusty/juju-gui-42', '0', None, None)
         options = self.make_options(open_browser=False)
         manage.run(options)
         self.assertFalse(mock_open.called)
@@ -816,7 +868,13 @@
         # If an admin secret is fetched from jenv it is used, even if one is
         # found in environments.yaml, as set in options.admin_secret.
         mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_success
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         options = self.make_options(admin_secret='secret in environments.yaml')
         manage.run(options)
         mock_app.connect.assert_has_calls([
@@ -827,7 +885,13 @@
         # If an admin secret is not fetched from jenv, then the one from
         # environments.yaml is used, as found in options.admin_secret.
         mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         options = self.make_options(admin_secret='secret in environments.yaml')
         manage.run(options)
         mock_app.connect.assert_has_calls([
@@ -838,7 +902,13 @@
         # If admin-secret cannot be found anywhere a ProgramExit is called.
         mock_app.ProgramExit = app.ProgramExit
         mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error
+        # Make mock_app.bootstrap return the already_bootstrapped flag and the
+        # bootstrap node series.
         mock_app.bootstrap.return_value = (True, 'precise')
+        # Make mock_app.check_environment return the charm URL, the machine
+        # where to deploy the charm, the service and unit data.
+        mock_app.check_environment.return_value = (
+            'cs:precise/juju-gui-42', '0', None, None)
         options = self.make_options(
             admin_secret=None,
             env_name='local',

=== modified file 'quickstart/tests/test_utils.py'
--- old/quickstart/tests/test_utils.py	2014-03-14 12:03:55 +0000
+++ new/quickstart/tests/test_utils.py	2014-04-23 12:20:35 +0000
@@ -36,6 +36,7 @@
     settings,
     utils,
 )
+from quickstart.models import charms
 from quickstart.tests import helpers
 
 
@@ -158,19 +159,25 @@
 
 
 @mock.patch('__builtin__.print', mock.Mock())
-class TestCheckGuiCharmUrl(unittest.TestCase):
+class TestParseGuiCharmUrl(unittest.TestCase):
+
+    def test_charm_instance_returned(self):
+        # A charm instance is correctly returned.
+        charm = utils.parse_gui_charm_url('cs:trusty/juju-gui-42')
+        self.assertIsInstance(charm, charms.Charm)
+        self.assertEqual('cs:trusty/juju-gui-42', charm.url())
 
     def test_customized(self):
         # A customized charm URL is properly logged.
         expected = 'using a customized juju-gui charm'
         with helpers.assert_logs([expected], level='warn'):
-            utils.check_gui_charm_url('cs:~juju-gui/precise/juju-gui-28')
+            utils.parse_gui_charm_url('cs:~juju-gui/precise/juju-gui-28')
 
     def test_outdated(self):
         # An outdated charm URL is properly logged.
         expected = 'charm is outdated and may not support bundle deployments'
         with helpers.assert_logs([expected], level='warn'):
-            utils.check_gui_charm_url('cs:precise/juju-gui-1')
+            utils.parse_gui_charm_url('cs:precise/juju-gui-1')
 
     def test_unexpected(self):
         # An unexpected charm URL is properly logged.
@@ -178,12 +185,12 @@
             'unexpected URL for the juju-gui charm: the service may not work '
             'as expected')
         with helpers.assert_logs([expected], level='warn'):
-            utils.check_gui_charm_url('cs:precise/another-gui-42')
+            utils.parse_gui_charm_url('cs:precise/another-gui-42')
 
     def test_official(self):
         # No warnings are logged if an up to date charm is passed.
         with mock.patch('logging.warn') as mock_warn:
-            utils.check_gui_charm_url('cs:precise/juju-gui-100')
+            utils.parse_gui_charm_url('cs:precise/juju-gui-100')
         self.assertFalse(mock_warn.called)
 
 
@@ -304,18 +311,20 @@
 
     def test_charm_url(self):
         # The Juju GUI charm URL is correctly returned.
-        contents = json.dumps({'charm': {'url': 'cs:precise/juju-gui-42'}})
+        contents = json.dumps({'charm': {'url': 'cs:trusty/juju-gui-42'}})
         with self.patch_urlread(contents=contents) as mock_urlread:
-            charm_url = utils.get_charm_url()
-        self.assertEqual('cs:precise/juju-gui-42', charm_url)
-        mock_urlread.assert_called_once_with(settings.CHARMWORLD_API)
+            charm_url = utils.get_charm_url('trusty')
+        self.assertEqual('cs:trusty/juju-gui-42', charm_url)
+        mock_urlread.assert_called_once_with(
+            'http://manage.jujucharms.com/api/3/charm/trusty/juju-gui')
 
     def test_io_error(self):
         # IOErrors are properly propagated.
         with self.patch_urlread(error=True) as mock_urlread:
             with self.assertRaises(IOError) as context_manager:
-                utils.get_charm_url()
-        mock_urlread.assert_called_once_with(settings.CHARMWORLD_API)
+                utils.get_charm_url('precise')
+        mock_urlread.assert_called_once_with(
+            'http://manage.jujucharms.com/api/3/charm/precise/juju-gui')
         self.assertEqual('bad wolf', bytes(context_manager.exception))
 
     def test_value_error(self):
@@ -323,8 +332,9 @@
         contents = json.dumps({'charm': {}})
         with self.patch_urlread(contents=contents) as mock_urlread:
             with self.assertRaises(ValueError) as context_manager:
-                utils.get_charm_url()
-        mock_urlread.assert_called_once_with(settings.CHARMWORLD_API)
+                utils.get_charm_url('trusty')
+        mock_urlread.assert_called_once_with(
+            'http://manage.jujucharms.com/api/3/charm/trusty/juju-gui')
         self.assertEqual(
             'unable to find the charm URL', bytes(context_manager.exception))
 

=== modified file 'quickstart/utils.py'
--- old/quickstart/utils.py	2014-03-14 12:03:55 +0000
+++ new/quickstart/utils.py	2014-04-23 11:22:44 +0000
@@ -105,24 +105,32 @@
     return retcode, output.decode('utf-8'), error.decode('utf-8')
 
 
-def check_gui_charm_url(charm_url):
-    """Print (to stdout or to logs) info and warnings about the charm URL."""
+def parse_gui_charm_url(charm_url):
+    """Parse the given charm URL.
+
+    Check if the charm looks like a Juju GUI charm.
+    Print (to stdout or to logs) info and warnings about the charm URL.
+
+    Return the parsed charm object as an instance of
+    quickstart.models.charms.Charm.
+    """
     print('charm URL: {}'.format(charm_url))
     charm = charms.Charm.from_url(charm_url)
     charm_name = settings.JUJU_GUI_CHARM_NAME
-    if charm.name == charm_name:
-        if charm.user or charm.is_local():
-            # This is not the official Juju GUI charm.
-            logging.warn('using a customized {} charm'.format(charm_name))
-        elif charm.revision < settings.MINIMUM_CHARM_REVISION_FOR_BUNDLES:
-            # This is the official Juju GUI charm, but it is outdated.
-            logging.warn(
-                'charm is outdated and may not support bundle deployments')
-    else:
+    if charm.name != charm_name:
         # This does not seem to be a Juju GUI charm.
         logging.warn(
             'unexpected URL for the {} charm: '
             'the service may not work as expected'.format(charm_name))
+        return charm
+    if charm.user or charm.is_local():
+        # This is not the official Juju GUI charm.
+        logging.warn('using a customized {} charm'.format(charm_name))
+    elif charm.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[charm.series]:
+        # This is the official Juju GUI charm, but it is outdated.
+        logging.warn(
+            'charm is outdated and may not support bundle deployments')
+    return charm
 
 
 def convert_bundle_url(bundle_url):
@@ -142,13 +150,15 @@
             bundle_id)
 
 
-def get_charm_url():
+def get_charm_url(series):
     """Return the charm URL of the latest Juju GUI charm revision.
 
     Raise an IOError if any problems occur connecting to the API endpoint.
     Raise a ValueError if the API returns invalid data.
     """
-    charm_info = json.loads(urlread(settings.CHARMWORLD_API))
+    url = settings.CHARMWORLD_API.format(
+        series=series, charm=settings.JUJU_GUI_CHARM_NAME)
+    charm_info = json.loads(urlread(url))
     charm_url = charm_info.get('charm', {}).get('url')
     if charm_url is None:
         raise ValueError(b'unable to find the charm URL')

