Merge lp:~frankban/juju-gui/quickstart-real-bootstrap into lp:juju-gui/juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 4
Proposed branch: lp:~frankban/juju-gui/quickstart-real-bootstrap
Merge into: lp:juju-gui/juju-quickstart
Diff against target: 584 lines (+365/-33)
7 files modified
quickstart/app.py (+39/-5)
quickstart/manage.py (+28/-2)
quickstart/tests/helpers.py (+40/-12)
quickstart/tests/test_app.py (+155/-0)
quickstart/tests/test_manage.py (+17/-3)
quickstart/tests/test_utils.py (+47/-7)
quickstart/utils.py (+39/-4)
To merge this branch: bzr merge lp:~frankban/juju-gui/quickstart-real-bootstrap
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+191410@code.launchpad.net

Description of the change

Bootstrap the Juju environment.

Wait until the API server is ready.
Retrieve the API address.

Also added --debug support (logging).

Tests: `make check`.

QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'

- run `.venv/bin/python juju-quickstart -e local`
  -> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
  -> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
  -> the debug messages are shown, the env is bootstrapped,
     the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
  -? like above but this time no debug messages are shown.

https://codereview.appspot.com/14441074/

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

Reviewers: mp+191410_code.launchpad.net,

Message:
Please take a look.

Description:
Bootstrap the Juju environment.

Wait until the API server is ready.
Retrieve the API address.

Also added --debug support (logging).

Tests: `make check`.

QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'

- run `.venv/bin/python juju-quickstart -e local`
   -> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
   -> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
   -> the debug messages are shown, the env is bootstrapped,
      the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
   -? like above but this time no debug messages are shown.

https://code.launchpad.net/~frankban/juju-gui/quickstart-real-bootstrap/+merge/191410

(do not edit description out of merge proposal)

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

Affected files (+371, -33 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/tests/helpers.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM Francesco. I have not yet done QA but can if needed.

https://codereview.appspot.com/14441074/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/14441074/diff/1/quickstart/manage.py#newcode78
quickstart/manage.py:78: if root.handlers:
A comment here explaining what you're doing would be welcome.

https://codereview.appspot.com/14441074/

15. By Francesco Banconi

Changes as per review.

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

Please take a look.

https://codereview.appspot.com/14441074/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/14441074/diff/1/quickstart/manage.py#newcode78
quickstart/manage.py:78: if root.handlers:
On 2013/10/16 14:51:20, bac wrote:
> A comment here explaining what you're doing would be welcome.

Done.

https://codereview.appspot.com/14441074/

Revision history for this message
Gary Poster (gary) wrote :

LGTM with an extremely small suggestion that you are welcome to ignore.
It is interesting and convenient that so many of the commands have
(seemingly) reliably parsable output. The branch has nice and
interesting tests. Thank you!

https://codereview.appspot.com/14441074/diff/5001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/14441074/diff/5001/quickstart/app.py#newcode49
quickstart/app.py:49: retcode, output, error = utils.call('juju',
'status', '-e', env_name)
I'm OK with this for expediency, but wouldn't the API (Kapil's library)
be better in a revision?

[later] Oh, it's YAML, yeah? I guess that's OK. I think it would be
nice to add a comment to that effect, but others might disagree. Just a
suggestion.

https://codereview.appspot.com/14441074/diff/5001/quickstart/app.py#newcode70
quickstart/app.py:70: 'juju', 'api-endpoints', '-e', env_name,
'--format', 'json')
yay, --format json

https://codereview.appspot.com/14441074/diff/5001/quickstart/utils.py
File quickstart/utils.py (right):

https://codereview.appspot.com/14441074/diff/5001/quickstart/utils.py#newcode69
quickstart/utils.py:69: # Switch to using "juju switch --raw" in order
to avoid the fragility
oh, cool.

https://codereview.appspot.com/14441074/

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

*** Submitted:

Bootstrap the Juju environment.

Wait until the API server is ready.
Retrieve the API address.

Also added --debug support (logging).

Tests: `make check`.

QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'

- run `.venv/bin/python juju-quickstart -e local`
   -> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
   -> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
   -> the debug messages are shown, the env is bootstrapped,
      the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
   -? like above but this time no debug messages are shown.

R=bac, gary.poster
CC=
https://codereview.appspot.com/14441074

https://codereview.appspot.com/14441074/diff/5001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/14441074/diff/5001/quickstart/app.py#newcode49
quickstart/app.py:49: retcode, output, error = utils.call('juju',
'status', '-e', env_name)
On 2013/10/16 15:43:40, gary.poster wrote:
> I'm OK with this for expediency, but wouldn't the API (Kapil's
library) be
> better in a revision?

> [later] Oh, it's YAML, yeah? I guess that's OK. I think it would be
nice to
> add a comment to that effect, but others might disagree. Just a
suggestion.

Good suggestion. As discussed, I will explicitly add "--format yaml"
here.

https://codereview.appspot.com/14441074/

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

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 2013-10-15 15:31:12 +0000
+++ quickstart/app.py 2013-10-16 15:23:20 +0000
@@ -16,6 +16,10 @@
1616
17"""Juju Quickstart base application functions."""17"""Juju Quickstart base application functions."""
1818
19import json
20
21from quickstart import utils
22
1923
20class ProgramExit(Exception):24class ProgramExit(Exception):
21 """An error occurred while setting up the Juju environment.25 """An error occurred while setting up the Juju environment.
@@ -34,9 +38,39 @@
34def bootstrap(env_name):38def bootstrap(env_name):
35 """Bootstrap the Juju environment with the given name.39 """Bootstrap the Juju environment with the given name.
3640
37 Return the environment API address (e.g. "api.example.com:17070").41 Return when the bootstrap node is ready.
38 Raise a ProgramExit if any error occurs in the bootstrap process.42 Raise a ProgramExit if any error occurs in the bootstrap process.
39 Otherwise return when the environment is bootstrapped and the API server43 """
40 is ready to accept connections.44 retcode, _, error = utils.call('juju', 'bootstrap', '-e', env_name)
41 """45 if retcode:
42 # TODO: everything!46 raise ProgramExit(error)
47 # Call "juju status" multiple times until the bootstrap node is ready.
48 for _ in range(5):
49 retcode, output, error = utils.call('juju', 'status', '-e', env_name)
50 if retcode:
51 continue
52 # Ensure the state server is up and the agent is started.
53 try:
54 agent_state = utils.parse_status_output(output)
55 except ValueError:
56 continue
57 if agent_state == 'started':
58 return
59 details = ''.join(filter(None, [output, error])).strip()
60 raise ProgramExit('the state server is not ready:\n{}'.format(details))
61
62
63def get_api_url(env_name):
64 """Return a Juju API URL for the given environment name.
65
66 Use the Juju CLI in a subprocess in order to retrieve the API addresses.
67 Return the complete URL, e.g. "wss://api.example.com:17070".
68 """
69 retcode, output, error = utils.call(
70 'juju', 'api-endpoints', '-e', env_name, '--format', 'json')
71 if retcode:
72 raise ProgramExit(error)
73 # Assuming there is always at least one API address, grab the first one
74 # from the JSON output.
75 api_address = json.loads(output)[0]
76 return 'wss://{}'.format(api_address)
4377
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2013-10-15 15:40:31 +0000
+++ quickstart/manage.py 2013-10-16 15:23:20 +0000
@@ -18,6 +18,7 @@
1818
19from __future__ import print_function19from __future__ import print_function
20import argparse20import argparse
21import logging
21import os22import os
2223
23import quickstart24import quickstart
@@ -54,7 +55,8 @@
54 'It is possible to specify the environment name by either:\n'55 'It is possible to specify the environment name by either:\n'
55 ' - passing the -e or --environment argument;\n'56 ' - passing the -e or --environment argument;\n'
56 ' - setting the JUJU_ENV environment variable;\n'57 ' - setting the JUJU_ENV environment variable;\n'
57 ' - using "juju switch" to select the default environment to use.'58 ' - using "juju switch" to select the default environment;\n'
59 ' - setting the default environment in {}.'.format(env_file)
58 )60 )
59 # Validate the environment file.61 # Validate the environment file.
60 try:62 try:
@@ -70,8 +72,25 @@
70 options.env_type = env_type72 options.env_type = env_type
7173
7274
75def _configure_logging(level):
76 """Set up the application logging."""
77 root = logging.getLogger()
78 # Remove any previous handler on the root logger.
79 for handler in root.handlers[:]:
80 root.removeHandler(handler)
81 logging.basicConfig(
82 level=level,
83 format=(
84 '%(asctime)s %(levelname)s '
85 '%(module)s@%(funcName)s:%(lineno)d '
86 '%(message)s'
87 ),
88 datefmt='%H:%M:%S',
89 )
90
91
73def setup():92def setup():
74 """Set up the application options.93 """Set up the application options and logger.
7594
76 Return the options as a namespace containing the followin attributes:95 Return the options as a namespace containing the followin attributes:
77 - admin_secret: the password to use to access the Juju API;96 - admin_secret: the password to use to access the Juju API;
@@ -97,6 +116,8 @@
97 help='the path to the Juju environments YAML file (%(default)s)')116 help='the path to the Juju environments YAML file (%(default)s)')
98 parser.add_argument(117 parser.add_argument(
99 '--version', action='version', version='%(prog)s {}'.format(version))118 '--version', action='version', version='%(prog)s {}'.format(version))
119 parser.add_argument(
120 '--debug', action='store_true', help='turn debug mode on')
100 # This is required by juju-core: see "juju help plugins".121 # This is required by juju-core: see "juju help plugins".
101 parser.add_argument(122 parser.add_argument(
102 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,123 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,
@@ -105,6 +126,8 @@
105 options = parser.parse_args()126 options = parser.parse_args()
106 # Validate and process the provided arguments.127 # Validate and process the provided arguments.
107 _validate_env(options, parser)128 _validate_env(options, parser)
129 # Set up logging.
130 _configure_logging(logging.DEBUG if options.debug else logging.INFO)
108 return options131 return options
109132
110133
@@ -114,3 +137,6 @@
114 print('bootstrapping the {} environment (type: {})'.format(137 print('bootstrapping the {} environment (type: {})'.format(
115 options.env_name, options.env_type))138 options.env_name, options.env_type))
116 app.bootstrap(options.env_name)139 app.bootstrap(options.env_name)
140 print('retrieving the Juju API address')
141 api_url = app.get_api_url(options.env_name)
142 print('connecting to {}'.format(api_url))
117143
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2013-10-15 18:25:57 +0000
+++ quickstart/tests/helpers.py 2013-10-16 15:23:20 +0000
@@ -20,21 +20,35 @@
20import os20import os
21import tempfile21import tempfile
2222
23import mock
23import yaml24import yaml
2425
2526
26class ValueErrorTestsMixin(object):27class CallTestsMixin(object):
27 """Set up some base methods for testing functions raising ValueErrors."""28 """Easily use the quickstart.utils.call function."""
2829
29 @contextmanager30 def patch_call(self, retcode, output='', error=''):
30 def assert_value_error(self, error):31 """Patch the quickstart.utils.call function."""
31 """Ensure a ValueError is raised in the context block.32 mock_call = mock.Mock(return_value=(retcode, output, error))
3233 return mock.patch('quickstart.utils.call', mock_call)
33 Also check that the exception includes the expected error message.34
34 """35 def patch_multiple_calls(self, side_effect):
35 with self.assertRaises(ValueError) as context_manager:36 """Patch multiple subsequent quickstart.utils.call calls."""
36 yield37 mock_call = mock.Mock(side_effect=side_effect)
37 self.assertEqual(error, str(context_manager.exception))38 return mock.patch('quickstart.utils.call', mock_call)
39
40
41@contextmanager
42def assert_logs(messages, level='debug'):
43 """Ensure the given messages are logged using the given log level.
44
45 Use this function as a context manager: the code executed in the context
46 block must add the expected log entries.
47 """
48 with mock.patch('logging.{}'.format(level.lower())) as mock_log:
49 yield
50 expected_calls = [mock.call(message) for message in messages]
51 mock_log.assert_has_calls(expected_calls)
3852
3953
40class EnvFileTestsMixin(object):54class EnvFileTestsMixin(object):
@@ -58,3 +72,17 @@
58 env_file.write(contents)72 env_file.write(contents)
59 env_file.close()73 env_file.close()
60 return env_file.name74 return env_file.name
75
76
77class ValueErrorTestsMixin(object):
78 """Set up some base methods for testing functions raising ValueErrors."""
79
80 @contextmanager
81 def assert_value_error(self, error):
82 """Ensure a ValueError is raised in the context block.
83
84 Also check that the exception includes the expected error message.
85 """
86 with self.assertRaises(ValueError) as context_manager:
87 yield
88 self.assertEqual(error, str(context_manager.exception))
6189
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2013-10-15 15:31:32 +0000
+++ quickstart/tests/test_app.py 2013-10-16 15:23:20 +0000
@@ -16,9 +16,15 @@
1616
17"""Tests for the Juju Quickstart base application functions."""17"""Tests for the Juju Quickstart base application functions."""
1818
19from contextlib import contextmanager
20import json
19import unittest21import unittest
2022
23import mock
24import yaml
25
21from quickstart import app26from quickstart import app
27from quickstart.tests import helpers
2228
2329
24class TestProgramExit(unittest.TestCase):30class TestProgramExit(unittest.TestCase):
@@ -27,3 +33,152 @@
27 # The error is properly represented as a string.33 # The error is properly represented as a string.
28 exception = app.ProgramExit('bad wolf')34 exception = app.ProgramExit('bad wolf')
29 self.assertEqual('juju-quickstart: error: bad wolf', str(exception))35 self.assertEqual('juju-quickstart: error: bad wolf', str(exception))
36
37
38class ProgramExitTestsMixin(object):
39 """Set up some base methods for testing functions raising ProgramExit."""
40
41 @contextmanager
42 def assert_program_exit(self, error):
43 """Ensure a ProgramExit is raised in the context block.
44
45 Also check that the exception includes the expected error message.
46 """
47 with self.assertRaises(app.ProgramExit) as context_manager:
48 yield
49 expected = 'juju-quickstart: error: {}'.format(error)
50 self.assertEqual(expected, str(context_manager.exception))
51
52
53class TestBootstrap(
54 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
55
56 env_name = 'ec2'
57
58 def make_status_output(self, agent_state):
59 """Create and return a YAML status output."""
60 return yaml.safe_dump({
61 'machines': {'0': {'agent-state': agent_state}},
62 })
63
64 def make_status_calls(self, number):
65 """Return a list containing the given number of status calls."""
66 return [
67 mock.call('juju', 'status', '-e', self.env_name)
68 for _ in range(number)
69 ]
70
71 def assert_status_retried(self, side_effects):
72 """Ensure the "juju status" command is retried several times.
73
74 Receive the list of side effects the mock status call will return.
75 """
76 with self.patch_multiple_calls(side_effects) as mock_call:
77 app.bootstrap(self.env_name)
78 mock_call.assert_has_calls([
79 mock.call('juju', 'bootstrap', '-e', self.env_name),
80 ] + self.make_status_calls(5))
81
82 def test_success(self):
83 # The environment is successfully bootstrapped.
84 side_effects = [
85 (0, '', ''), # Add a bootstrap call.
86 (0, self.make_status_output('started'), ''), # Add a status call.
87 ]
88 with self.patch_multiple_calls(side_effects) as mock_call:
89 app.bootstrap(self.env_name)
90 mock_call.assert_has_calls([
91 mock.call('juju', 'bootstrap', '-e', self.env_name),
92 ] + self.make_status_calls(1))
93
94 def test_bootstrap_failure(self):
95 # A ProgramExit is raised if an error occurs while bootstrapping.
96 with self.patch_call(1, error='bad wolf') as mock_call:
97 with self.assert_program_exit('bad wolf'):
98 app.bootstrap(self.env_name)
99 mock_call.assert_called_once_with(
100 'juju', 'bootstrap', '-e', self.env_name)
101
102 def test_status_retry_error(self):
103 # Before raising a ProgramExit, the functions tries to call
104 # "juju status" multiple times if it exits with an error.
105 side_effects = [
106 (0, '', ''), # Add the bootstrap call.
107 # Add four status calls with a non-zero exit code.
108 (1, '', 'these'),
109 (2, '', 'are'),
110 (3, '', 'the'),
111 (4, '', 'voyages'),
112 # Add a final valid status call.
113 (0, self.make_status_output('started'), ''),
114 ]
115 self.assert_status_retried(side_effects)
116
117 def test_status_retry_invalid_output(self):
118 # Before raising a ProgramExit, the functions tries to call
119 # "juju status" multiple times if its output is not well formed or if
120 # the agent is not started.
121 side_effects = [
122 (0, '', ''), # Add the bootstrap call.
123 (0, '', ''), # Add the first status call: no output.
124 (0, ':', ''), # Add the second status call: not YAML.
125 (0, 'just-a-string', ''), # Add the third status call: bad YAML.
126 # Add the fourth status call: the agent is still pending.
127 (0, self.make_status_output('pending'), ''),
128 # Add a final valid status call.
129 (0, self.make_status_output('started'), ''),
130 ]
131 self.assert_status_retried(side_effects)
132
133 def test_status_retry_both(self):
134 # Before raising a ProgramExit, the functions tries to call
135 # "juju status" multiple times in any case.
136 side_effects = [
137 (0, '', ''), # Add the bootstrap call.
138 (1, '', 'error'), # Add the first status call: error.
139 (2, '', 'another error'), # Add the second status call: error.
140 # Add the third status call: the agent is still pending.
141 (0, self.make_status_output('pending'), ''),
142 (0, 'just-a-string', ''), # Add the fourth status call: bad YAML.
143 # Add a final valid status call.
144 (0, self.make_status_output('started'), ''),
145 ]
146 self.assert_status_retried(side_effects)
147
148 def test_status_failure(self):
149 # A ProgramExit is raised if "juju status" keeps failing.
150 status_side_effects = [
151 (i, 'output #{}\n'.format(i), 'error #{}\n'.format(i))
152 for i in range(5)
153 ]
154 side_effects = [(0, '', '')] + status_side_effects
155 expected = 'the state server is not ready:\noutput #4\nerror #4'
156 with self.patch_multiple_calls(side_effects) as mock_call:
157 with self.assert_program_exit(expected):
158 app.bootstrap(self.env_name)
159 mock_call.assert_has_calls([
160 mock.call('juju', 'bootstrap', '-e', self.env_name),
161 ] + self.make_status_calls(5))
162
163
164class TestGetApiUrl(
165 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
166
167 env_name = 'ec2'
168
169 def test_success(self):
170 # The API URL is correctly returned.
171 api_addresses = json.dumps(['api.example.com:17070', 'not-today'])
172 with self.patch_call(0, output=api_addresses) as mock_call:
173 api_url = app.get_api_url(self.env_name)
174 self.assertEqual('wss://api.example.com:17070', api_url)
175 mock_call.assert_called_once_with(
176 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json')
177
178 def test_failure(self):
179 # A ProgramExit is raised if an error occurs retrieving the API URL.
180 with self.patch_call(1, error='bad wolf') as mock_call:
181 with self.assert_program_exit('bad wolf'):
182 app.get_api_url(self.env_name)
183 mock_call.assert_called_once_with(
184 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json')
30185
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2013-10-15 18:25:57 +0000
+++ quickstart/tests/test_manage.py 2013-10-16 15:23:20 +0000
@@ -17,6 +17,7 @@
17"""Tests for the Juju Quickstart management infrastructure."""17"""Tests for the Juju Quickstart management infrastructure."""
1818
19import argparse19import argparse
20import logging
20import os21import os
21import unittest22import unittest
2223
@@ -122,16 +123,17 @@
122 path = 'quickstart.manage.utils.get_default_env_name'123 path = 'quickstart.manage.utils.get_default_env_name'
123 return mock.patch(path, mock_get_default_env_name)124 return mock.patch(path, mock_get_default_env_name)
124125
125 def call_setup(self, args, env_name=None):126 def call_setup(self, args, env_name=None, exit_called=True):
126 """Call the setup function simulating the given args and env name.127 """Call the setup function simulating the given args and env name.
127128
128 Also ensure the program exits without errors.129 Also ensure the program exits without errors if exit_called is True.
129 """130 """
130 with mock.patch('sys.argv', ['juju-quickstart'] + args):131 with mock.patch('sys.argv', ['juju-quickstart'] + args):
131 with mock.patch('sys.exit') as mock_exit:132 with mock.patch('sys.exit') as mock_exit:
132 with self.patch_get_default_env_name(env_name):133 with self.patch_get_default_env_name(env_name):
133 manage.setup()134 manage.setup()
134 mock_exit.assert_called_once_with(0)135 if exit_called:
136 mock_exit.assert_called_once_with(0)
135137
136 def test_help(self):138 def test_help(self):
137 # The program help message is properly formatted.139 # The program help message is properly formatted.
@@ -170,3 +172,15 @@
170 self.call_setup(['--version'])172 self.call_setup(['--version'])
171 expected = 'juju-quickstart {}\n'.format(quickstart.get_version())173 expected = 'juju-quickstart {}\n'.format(quickstart.get_version())
172 mock_stderr.write.assert_called_once_with(expected)174 mock_stderr.write.assert_called_once_with(expected)
175
176 def test_configure_logging(self):
177 # Logging is properly set up at the info level.
178 logger = logging.getLogger()
179 self.call_setup([], 'ec2', exit_called=False)
180 self.assertEqual(logging.INFO, logger.level)
181
182 def test_configure_logging_debug(self):
183 # Logging is properly set up at the debug level.
184 logger = logging.getLogger()
185 self.call_setup(['--debug'], 'ec2', exit_called=False)
186 self.assertEqual(logging.DEBUG, logger.level)
173187
=== modified file 'quickstart/tests/test_utils.py'
--- quickstart/tests/test_utils.py 2013-10-15 18:25:57 +0000
+++ quickstart/tests/test_utils.py 2013-10-16 15:23:20 +0000
@@ -61,13 +61,17 @@
61 'no-such-command: [Errno 2] No such file or directory',61 'no-such-command: [Errno 2] No such file or directory',
62 error)62 error)
6363
6464 def test_logging(self):
65class TestGetDefaultEnvName(unittest.TestCase):65 # The command line call and the results are properly logged.
6666 expected_messages = (
67 def patch_call(self, retcode, output='', error=''):67 "running the following: echo 'we are the borg!'",
68 """Patch the quickstart.utils.call function."""68 r"retcode: 0 | output: 'we are the borg!\n' | error: ''",
69 mock_call = mock.Mock(return_value=(retcode, output, error))69 )
70 return mock.patch('quickstart.utils.call', mock_call)70 with helpers.assert_logs(expected_messages):
71 utils.call('echo', 'we are the borg!')
72
73
74class TestGetDefaultEnvName(helpers.CallTestsMixin, unittest.TestCase):
7175
72 def test_environment_variable(self):76 def test_environment_variable(self):
73 # The environment name is successfully returned if JUJU_ENV is set.77 # The environment name is successfully returned if JUJU_ENV is set.
@@ -176,3 +180,39 @@
176 env_type, admin_secret = utils.parse_env_file(env_file, 'aws')180 env_type, admin_secret = utils.parse_env_file(env_file, 'aws')
177 self.assertEqual('ec2', env_type)181 self.assertEqual('ec2', env_type)
178 self.assertEqual('Secret!', admin_secret)182 self.assertEqual('Secret!', admin_secret)
183
184
185class TestParseStatusOutput(helpers.ValueErrorTestsMixin, unittest.TestCase):
186
187 def test_invalid_yaml(self):
188 # A ValueError is raised if the output is not a valid YAML.
189 with self.assertRaises(ValueError) as context_manager:
190 utils.parse_status_output(':')
191 expected = 'unable to parse the output'
192 self.assertIn(expected, str(context_manager.exception))
193
194 def test_invalid_yaml_contents(self):
195 # A ValueError is raised if the output is not well formed.
196 with self.assert_value_error('invalid YAML contents: a-string'):
197 utils.parse_status_output('a-string')
198
199 def test_no_env(self):
200 # A ValueError is raised if the agent-state is not found in the YAML.
201 data = {
202 'machines': {
203 '0': {'agent-version': '1.17.0.1'},
204 },
205 }
206 expected = 'agent state not found in {}'.format(str(data))
207 with self.assert_value_error(expected):
208 utils.parse_status_output(yaml.safe_dump(data))
209
210 def test_success(self):
211 # The agent state is correctly returned.
212 output = yaml.safe_dump({
213 'machines': {
214 '0': {'agent-version': '1.17.0.1', 'agent-state': 'started'},
215 },
216 })
217 agent_state = utils.parse_status_output(output)
218 self.assertEqual('started', agent_state)
179219
=== modified file 'quickstart/utils.py'
--- quickstart/utils.py 2013-10-15 15:47:31 +0000
+++ quickstart/utils.py 2013-10-16 15:23:20 +0000
@@ -18,6 +18,8 @@
1818
19import re19import re
20import os20import os
21import logging
22import pipes
21import subprocess23import subprocess
2224
23import yaml25import yaml
@@ -34,6 +36,8 @@
34 Return a tuple containing the subprocess return code, output and error.36 Return a tuple containing the subprocess return code, output and error.
35 """37 """
36 pipe = subprocess.PIPE38 pipe = subprocess.PIPE
39 cmdline = ' '.join(map(pipes.quote, args))
40 logging.debug('running the following: {}'.format(cmdline))
37 try:41 try:
38 process = subprocess.Popen(args, stdout=pipe, stderr=pipe)42 process = subprocess.Popen(args, stdout=pipe, stderr=pipe)
39 except OSError as err:43 except OSError as err:
@@ -41,20 +45,32 @@
41 # found in the PATH.45 # found in the PATH.
42 return 127, '', '{}: {}'.format(args[0], err)46 return 127, '', '{}: {}'.format(args[0], err)
43 output, error = process.communicate()47 output, error = process.communicate()
44 return process.poll(), output, error48 retcode = process.poll()
49 logging.debug('retcode: {} | output: {!r} | error: {!r}'.format(
50 retcode, output, error))
51 return retcode, output, error
4552
4653
47def get_default_env_name():54def get_default_env_name():
48 """Return the current Juju environment name.55 """Return the current Juju environment name.
4956
50 The environment name can be set either by setting the JUJU_ENV environment57 The environment name can be set either by
51 variable or by using "juju switch". The former overrides the latter.58 - setting the JUJU_ENV environment variable;
59 - using "juju switch my-env-name";
60 - setting the default environment in the environments.yaml file.
61 The former overrides the latter.
5262
53 Return None if a default environment is not found.63 Return None if a default environment is not found.
54 """64 """
55 env_name = os.getenv('JUJU_ENV', '').strip()65 env_name = os.getenv('JUJU_ENV', '').strip()
56 if env_name:66 if env_name:
57 return env_name67 return env_name
68 # XXX 2013-10-16 frankban bug=1193244:
69 # Switch to using "juju switch --raw" in order to avoid the fragility
70 # of parsing the command output.
71 # The "juju switch" command parses ~/.juju/current-environment file. If the
72 # environment name is not found there, then it tries to retrieve the name
73 # from the "default" section of the ~/.juju/environments.yaml file.
58 retcode, output, _ = call('juju', 'switch')74 retcode, output, _ = call('juju', 'switch')
59 if retcode:75 if retcode:
60 return None76 return None
@@ -64,7 +80,7 @@
6480
6581
66def parse_env_file(env_file, env_name):82def parse_env_file(env_file, env_name):
67 """Parse the provided Juju environment.yaml file.83 """Parse the provided Juju environments.yaml file.
6884
69 Return a tuple containing the provider type and the admin secret associated85 Return a tuple containing the provider type and the admin secret associated
70 with the given environment name.86 with the given environment name.
@@ -108,3 +124,22 @@
108 msg = '{} admin secret not found in {}'.format(env_name, env_file)124 msg = '{} admin secret not found in {}'.format(env_name, env_file)
109 raise ValueError(msg)125 raise ValueError(msg)
110 return env_type, admin_secret126 return env_type, admin_secret
127
128
129def parse_status_output(output):
130 """Parse the output of juju status.
131
132 Return the agent state.
133 Raise a ValueError if the agent state cannot be retrieved.
134 """
135 try:
136 status = yaml.safe_load(output)
137 except Exception as err:
138 raise ValueError('unable to parse the output: {}'.format(err))
139 try:
140 state = status.get('machines', {}).get('0', {}).get('agent-state')
141 except AttributeError as err:
142 raise ValueError('invalid YAML contents: {}'.format(status))
143 if state is None:
144 raise ValueError('agent state not found in {}'.format(status))
145 return state

Subscribers

People subscribed via source and target branches