~frankban/juju-quickstart/deploy-changeset

« back to all changes in this revision

Viewing changes to quickstart/tests/test_app.py

  • Committer: Francesco Banconi
  • Date: 2015-02-10 15:19:24 UTC
  • mfrom: (119.2.9 new-auth-api-endpoint)
  • Revision ID: francesco.banconi@canonical.com-20150210151924-m6br1cg2m0gvmctq
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

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
 
21
21
from contextlib import contextmanager
22
22
import json
 
23
import os
23
24
import unittest
24
25
 
25
26
import jujuclient
31
32
    platform_support,
32
33
    settings,
33
34
)
 
35
from quickstart.models import charms
34
36
from quickstart.tests import helpers
35
37
 
36
38
 
457
459
class TestCheckBootstrapped(helpers.JenvFileTestsMixin, unittest.TestCase):
458
460
 
459
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
463
        with self.make_jenv('ec2', ''):
462
464
            with helpers.assert_logs([], level='warn'):
463
 
                api_url = app.check_bootstrapped('hp')
464
 
        self.assertIsNone(api_url)
 
465
                api_address = app.check_bootstrapped('hp')
 
466
        self.assertIsNone(api_address)
465
467
 
466
468
    def test_invalid_jenv_file(self):
467
 
        # A None API URL is returned if the list of API addresses cannot be
 
469
        # A None API address is returned if the list of API addresses cannot be
468
470
        # retrieved from the jenv file.
469
471
        with self.make_jenv('ec2', '') as path:
470
472
            logs = [
471
 
                'cannot retrieve the Juju API URL: '
 
473
                'cannot retrieve the Juju API address: '
472
474
                'cannot read {}: invalid YAML contents: '
473
475
                'state-servers key not found in the root section'.format(path)
474
476
            ]
475
477
            with helpers.assert_logs(logs, level='warn'):
476
 
                api_url = app.check_bootstrapped('ec2')
477
 
        self.assertIsNone(api_url)
 
478
                api_address = app.check_bootstrapped('ec2')
 
479
        self.assertIsNone(api_address)
478
480
 
479
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
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
485
        with self.make_jenv('local', yaml.safe_dump(jenv_data)):
484
486
            with helpers.assert_logs(logs, level='warn'):
485
 
                api_url = app.check_bootstrapped('local')
486
 
        self.assertIsNone(api_url)
 
487
                api_address = app.check_bootstrapped('local')
 
488
        self.assertIsNone(api_address)
487
489
 
488
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
492
        logs = [
491
 
            'cannot retrieve the Juju API URL: '
 
493
            'cannot retrieve the Juju API address: '
492
494
            'cannot connect to any of the following addresses: '
493
495
            'localhost:17070, 10.0.3.1:17070'
494
496
        ]
495
497
        with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
496
498
            with helpers.assert_logs(logs, level='warn'):
497
499
                with helpers.patch_socket_create_connection('bad wolf'):
498
 
                    api_url = app.check_bootstrapped('local')
499
 
        self.assertIsNone(api_url)
 
500
                    api_address = app.check_bootstrapped('local')
 
501
        self.assertIsNone(api_address)
500
502
 
501
503
    def test_bootstrapped(self):
502
 
        # The first listening API URL is returned if the environment is already
503
 
        # bootstrapped.
 
504
        # The first listening API address is returned if the environment is
 
505
        # already bootstrapped.
504
506
        with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):
505
507
            with helpers.assert_logs([], level='warn'):
506
508
                with helpers.patch_socket_create_connection():
507
 
                    api_url = app.check_bootstrapped('hp')
 
509
                    api_address = app.check_bootstrapped('hp')
508
510
        # The first API address is returned.
509
 
        self.assertEqual('wss://localhost:17070', api_url)
 
511
        self.assertEqual('localhost:17070', api_address)
510
512
 
511
513
 
512
514
class TestBootstrap(
700
702
        mock_call.assert_has_calls(expected_calls)
701
703
 
702
704
 
 
705
class 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
 
703
733
class TestGetCredentials(
704
734
        helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
705
735
 
722
752
                app.get_credentials('ec2')
723
753
 
724
754
 
725
 
class TestGetApiUrl(
 
755
class TestGetApiAddress(
726
756
        helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
727
757
 
728
758
    env_name = 'ec2'
729
759
    juju_command = settings.JUJU_CMD_PATHS['default']
730
760
 
731
761
    def test_success(self):
732
 
        # The API URL is correctly returned.
 
762
        # The API address is correctly returned.
733
763
        api_addresses = json.dumps(['api.example.com:17070', 'not-today'])
734
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)
736
 
        self.assertEqual('wss://api.example.com:17070', api_url)
 
765
            api_address = app.get_api_address(self.env_name, self.juju_command)
 
766
        self.assertEqual('api.example.com:17070', api_address)
737
767
        mock_call.assert_called_once_with(
738
768
            self.juju_command, 'api-endpoints', '-e', self.env_name,
739
769
            '--format', 'json')
740
770
 
741
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
773
        with self.patch_call(retcode=1, error='bad wolf') as mock_call:
744
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
776
        mock_call.assert_called_once_with(
747
777
            self.juju_command, 'api-endpoints', '-e', self.env_name,
748
778
            '--format', 'json')
904
934
        return mock.patch(
905
935
            'quickstart.netutils.get_charm_url', mock_get_charm_url)
906
936
 
 
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
942
    def test_environment_just_bootstrapped(self, mock_print):
908
943
        # The function correctly retrieves the charm URL and machine, and
909
944
        # handles the case when the charm URL is not provided by the user.
917
952
        check_preexisting = False
918
953
        with self.patch_get_charm_url(
919
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
956
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
922
957
                check_preexisting)
923
958
        # There is no need to call status if the environment was just created.
924
959
        self.assertFalse(env.get_status.called)
925
960
        # The charm URL has been retrieved from the charm store API based on
926
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
963
        mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
929
964
        # Since the bootstrap node series is supported by the GUI charm, the
930
965
        # GUI unit can be deployed to machine 0.
952
987
        check_preexisting = True
953
988
        with self.patch_get_charm_url(
954
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
991
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
957
992
                check_preexisting)
958
993
        # The environment status has been retrieved.
959
994
        env.get_status.assert_called_once_with()
960
995
        # The charm URL has been retrieved from the charm store API based on
961
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
998
        mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
964
999
        # Since the bootstrap node series is supported by the GUI charm, the
965
1000
        # GUI unit can be deployed to machine 0.
984
1019
        bootstrap_node_series = 'precise'
985
1020
        check_preexisting = True
986
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
1023
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
989
1024
                check_preexisting)
990
1025
        # The environment status has been retrieved.
991
1026
        env.get_status.assert_called_once_with()
992
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
1029
        self.assertFalse(mock_get_charm_url.called)
995
1030
        # Since the bootstrap node series is supported by the GUI charm, the
996
1031
        # GUI unit can be safely deployed to machine 0.
1009
1044
        check_preexisting = False
1010
1045
        with self.patch_get_charm_url(
1011
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
1048
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1014
1049
                check_preexisting)
1015
1050
        # The charm URL has been retrieved from the charm store API using the
1016
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
1053
        mock_get_charm_url.assert_called_once_with('trusty')
1019
1054
        # The Juju GUI unit cannot be deployed to saucy machine 0.
1020
1055
        self.assertIsNone(machine)
1034
1069
        bootstrap_node_series = 'trusty'
1035
1070
        check_preexisting = False
1036
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
1073
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1039
1074
                check_preexisting)
1040
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
1077
        # The Juju GUI unit cannot be deployed to localhost.
1043
1078
        self.assertIsNone(machine)
1044
1079
 
1051
1086
        bootstrap_node_series = 'trusty'
1052
1087
        check_preexisting = False
1053
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
1090
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1056
1091
                check_preexisting)
1057
1092
        self.assertIsNone(machine)
1065
1100
        bootstrap_node_series = 'precise'
1066
1101
        check_preexisting = False
1067
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
1104
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1070
1105
                check_preexisting)
1071
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
1108
        self.assertEqual('0', machine)
1074
1109
 
1075
1110
    def test_most_recent_default_charm_url(self, mock_print):
1082
1117
        bootstrap_node_series = 'saucy'
1083
1118
        check_preexisting = False
1084
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
1121
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1087
1122
                check_preexisting)
1088
1123
        # The default charm URL for the given series is returned.
1089
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
1126
        self.assertIsNone(machine)
1092
1127
 
1093
1128
    def test_charm_url_provided(self, mock_print):
1099
1134
        bootstrap_node_series = 'trusty'
1100
1135
        check_preexisting = False
1101
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
1138
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1104
1139
                check_preexisting)
1105
1140
        # There is no need to call the charmword API if the charm URL is
1106
1141
        # provided by the user.
1107
1142
        self.assertFalse(mock_get_charm_url.called)
1108
1143
        # The provided charm URL has been correctly returned.
1109
 
        self.assertEqual(charm_url, url)
 
1144
        self.assert_charm_equal(charm_url, charm)
1110
1145
        # Since the provided charm series is trusty, the charm itself can be
1111
1146
        # safely deployed to machine 0.
1112
1147
        self.assertEqual('0', machine)
1126
1161
        bootstrap_node_series = 'precise'
1127
1162
        check_preexisting = False
1128
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
1165
                env, 'my-gui', charm_url, env_type, bootstrap_node_series,
1131
1166
                check_preexisting)
1132
1167
        # There is no need to call the charmword API if the charm URL is
1133
1168
        # provided by the user.
1134
1169
        self.assertFalse(mock_get_charm_url.called)
1135
1170
        # The provided charm URL has been correctly returned.
1136
 
        self.assertEqual(charm_url, url)
 
1171
        self.assert_charm_equal(charm_url, charm)
1137
1172
        # Since the provided charm series is not precise, the charm must be
1138
1173
        # deployed to a new machine.
1139
1174
        self.assertIsNone(machine)