Merge lp:~mskalka/juju-ci-tools/assess-destroy-model into lp:juju-ci-tools

Proposed by Michael Skalka
Status: Merged
Merged at revision: 1988
Proposed branch: lp:~mskalka/juju-ci-tools/assess-destroy-model
Merge into: lp:juju-ci-tools
Diff against target: 348 lines (+339/-0)
2 files modified
assess_destroy_model.py (+164/-0)
tests/test_assess_destroy_model.py (+175/-0)
To merge this branch: bzr merge lp:~mskalka/juju-ci-tools/assess-destroy-model
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Aaron Bentley (community) Approve
Review via email: mp+321795@code.launchpad.net

Description of the change

In response to: https://bugs.launchpad.net/juju-ci-tools/+bug/1582021

Adds a test that bootstraps, gets the controller ID, adds a model, destroys it, then gets the controller ID again and asserts the IDs are equal. It will fail if juju can't get the controller ID or if they differ.

Additionally it adds an optional arg to client.destroy_model to destroy a model other than the one it's currently pointed at. The original behavior made it impossible to destroy a model without tracking that model through a client object, which is silly.

Also changes the syntax of client.switch() to default to the client's controller if none is specified. In the past switching back to the default jujupy model used in tests was ambiguous as the model and controller had the same name. Although it wasn't used in this test it was bugging me to no end.

To post a comment you must log in.
1975. By Michael Skalka

docstring edit

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Michael.

I don't think this test is recreating what happened, nor is it verifying how we know juju is correct. I have many thoughts in line, much regarding the extra changes the branch has.

review: Needs Information (code)
1976. By Michael Skalka

backed out client.py changes

1977. By Michael Skalka

restructured test, unittests, backed out client.py changes

Revision history for this message
Michael Skalka (mskalka) wrote :

Curtis,

I backed out the changes made to client.py. I will rework and resubmit those as their own MP.

I changed how the test behaves to better align with the committed bug fix. The test now checks to see if Juju drops its current model when that model is destroyed, and raises errors if it doesn't. Is this more in line with what you had in mind for this test?

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you Michael. This test needs another round of changes. In general never use "assert" and always raise JujuAssertionError when juju fails. Juju errors are being logged as info, but the test will still pass. You might want to consider using
    with logged_exception(logging):
to log exceptions without needing to write log.error.

review: Needs Fixing (code)
1978. By Michael Skalka

changes as per mp comments

1979. By Michael Skalka

added unittest for failure cases

Revision history for this message
Michael Skalka (mskalka) wrote :

Made the requested changes in regards to the asserts. Also added unittests for failure cases where they were missing. I'm not sure what you meant exactly by "Drop this since you didn't write tests for it." but I assumed that was it.

Revision history for this message
Aaron Bentley (abentley) wrote :

get_current_model and get_current_controller appear to reimplement Juju2Backend.get_active_model. If they do not, I recommend explaining the difference in the docstring.

I recommend using --format json or --format yaml in general. It is much more precise than the default textual output. For example, with list-models, parsed['current-model'] gives you the current model.

1980. By Michael Skalka

changes as per mp comments

Revision history for this message
Michael Skalka (mskalka) wrote :

I modified the functions to use --format yaml. I didn't know that was even an option with list-models. It's definitely a lot cleaner, so thank you for the suggestion. As for why I this uses list-models instead of switch, the bug specifies list-models and I wanted to keep it as close to the original case as possible.

Revision history for this message
Aaron Bentley (abentley) wrote :

--format json or --format yaml is an option with basically every query command (show-x, list-x). I prefer json because the parser is much faster (which can affect unit test speed), you don't need to tell it to be safe, and it's part of the standard library. Unfortunately, juju's json output is not human-readable. (You can pretty-print it by piping it to jq, though.)

I still recommend explaining the difference between get_current_model, get_current_controller, and Juju2Backend.get_active_model in the docstring. Otherwise, someone will "helpfully" remove the code duplication sooner or later.

However, this looks like a response to bug #1505504. Cheryl adjusted the scope of bug #1582021 to be just the timing hole: https://bugs.launchpad.net/juju-ci-tools/+bug/1582021/comments/1

Revision history for this message
Michael Skalka (mskalka) wrote :

Hmm. I don't see a report linked for the timing hole, but I'll search through Juju's bug reports. I'm not sure how we could adequately or reliably test for that with the tools we have right now however. Possibly repeatedly adding and deleting a model or series of models in rapid succession and listing each time? I'm happy to update the test if you have a better idea of what we should be attempting.

Revision history for this message
Aaron Bentley (abentley) wrote :

> Hmm. I don't see a report linked for the timing hole

The report of the timing hole is bug #1582021 itself.

It is the fact that "list-models" errors with "model not found". It is not erroring because the current-model is wrong. It is erroring because "we first get all models, then query each one for more details"

"bigdata" was in the first list, but was not accessible when queried, producing the "model not found" error.

A correct test for this would be something like:
juju bootstrap --default-model foobar
juju destroy-model foobar
# Avoid bug 1505504.
juju switch controller
# If we have a timing hole, this fails with "model not found"
juju list-models

This test should be run against 2.0-beta7 (or earlier) to prove that it fails, with 2.0-beta8 to prove that it fails even with bug #1505504 fixed, and then with current juju to prove that it passes.

1981. By Michael Skalka

cleanup

1982. By Michael Skalka

more cleanup as per mp

1983. By Michael Skalka

lint

Revision history for this message
Michael Skalka (mskalka) wrote :

Ok. I restructured the test a little to reflect what you think is the proper procedure and swapped to json at your recommendation.

Now it:
boostraps and stores that as a ModelClient object
adds a model which is captured as a new ModelClient object
destroys that model using that object
lists model so we know we avoid bug 1505504
switches back to the original model using our first client object
runs list-model on that client object to try to catch that timing hole

I confirmed the test fails on 2.0-beta8; it throws a called process error on list-models which is the expected outcome. It passes on current juju versions.

Revision history for this message
Aaron Bentley (abentley) wrote :

This looks good, though please update the error message as noted below. Also, Curtis should re-review before you merge to make sure his concerns are dealt with.

A few notes about testing:
assertRaisesRegexp (aka assertRaisesRegex in Python3) is much nicer than assertRaises followed by checking message content.

Also, assertIn should be used in preference to assertTrue(x in y).

review: Approve
1984. By Michael Skalka

unittest changes as per mp comments

1985. By Michael Skalka

docstring edit

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'assess_destroy_model.py'
--- assess_destroy_model.py 1970-01-01 00:00:00 +0000
+++ assess_destroy_model.py 2017-04-07 20:13:21 +0000
@@ -0,0 +1,164 @@
1#!/usr/bin/env python
2"""Assess if Juju tracks the model when the current model is destroyed."""
3
4from __future__ import print_function
5
6import argparse
7import logging
8import sys
9import subprocess
10import json
11
12from deploy_stack import (
13 BootstrapManager,
14 )
15from utility import (
16 add_basic_testing_arguments,
17 configure_logging,
18 JujuAssertionError,
19 )
20
21
22__metaclass__ = type
23
24
25log = logging.getLogger("assess_destroy_model")
26
27TEST_MODEL = 'test-tmp-env'
28
29
30def assess_destroy_model(client):
31 """Tests if Juju tracks the model properly through deletion.
32
33 In normal behavior Juju should drop the current model selection if that
34 model is destroyed. This will fail if Juju does not drop it's current
35 selection.
36
37 :param client: Jujupy ModelClient object
38 """
39
40 current_model = get_current_model(client)
41 controller = get_current_controller(client)
42 log.info('Current model: {}'.format(current_model))
43
44 new_client = add_model(client)
45 destroy_model(client, new_client)
46
47 log.info('Juju successfully dropped its current model. '
48 'Switching to {} to complete test'.format(current_model))
49 switch_model(client, current_model, controller)
50
51 log.info('SUCCESS')
52
53
54def add_model(client):
55 """Adds a model to the current juju environment then destroys it.
56
57 Will raise an exception if the Juju does not deselect the current model.
58
59 :param client: Jujupy ModelClient object
60 """
61 log.info('Adding model "{}" to current controller'.format(TEST_MODEL))
62 new_client = client.add_model(TEST_MODEL)
63 new_model = get_current_model(new_client)
64 if new_model == TEST_MODEL:
65 log.info('Current model and newly added model match')
66 else:
67 error = ('Juju failed to switch to new model after creation. '
68 'Expected {} got {}'.format(TEST_MODEL, new_model))
69 raise JujuAssertionError(error)
70 return new_client
71
72
73def destroy_model(client, new_client):
74 log.info('Destroying model "{}"'.format(TEST_MODEL))
75 new_client.destroy_model()
76 new_model = get_current_model(client)
77 if new_model:
78 error = 'Juju failed to unset model after it was destroyed'
79 raise JujuAssertionError(error)
80
81
82def switch_model(client, current_model, current_controller):
83 """Switches back to the old model.
84
85 :param client: Jujupy ModelClient object
86 :param current_model: String name of initial testing model
87 :param current_controller: String name of testing controller
88 """
89 client.switch(model=current_model, controller=current_controller)
90 new_model = get_current_model(client)
91 if new_model == current_model:
92 log.info('Current model and switch target match')
93 else:
94 error = ('Juju failed to switch back to existing model. '
95 'Expected {} got {}'.format(TEST_MODEL, new_model))
96 raise JujuAssertionError(error)
97
98
99def get_current_controller(client):
100 """Gets the current controller from Juju's list-models command.
101
102 :param client: Jujupy ModelClient object
103 :return: String name of current controller
104 """
105 raw = client.get_juju_output('switch', include_e=False).decode('utf-8')
106 raw = raw.split(':')[0]
107 return raw
108
109
110def get_current_model(client):
111 """Gets the current model from Juju's list-models command.
112
113 :param client: Jujupy ModelClient object
114 :return: String name of current model
115 """
116 raw = list_models(client)
117 try:
118 return raw['current-model']
119 except KeyError as e:
120 log.warning('No model is currently selected.')
121 return None
122
123
124def list_models(client):
125 """Helper function to get the output of juju's list-models command.
126
127 Instead of using 'juju switch' or client.backend.get_active_model() we use
128 list-models because that was what the bug report this test was generated
129 around used. It also allows for flexiblity in the future to get more
130 detailed information about the models that Juju thinks it has
131 if we need it.
132
133 :param client: Jujupy ModelClient object
134 :return: Dict of list-models command
135 """
136 try:
137 raw = client.get_juju_output('list-models', '--format', 'json',
138 include_e=False).decode('utf-8')
139 except subprocess.CalledProcessError as e:
140 log.error('Failed to list current models due to error: {}'.format(e))
141 raise e
142 return json.loads(raw)
143
144
145def parse_args(argv):
146 """Parse all arguments."""
147 parser = argparse.ArgumentParser(
148 description='Test if juju drops selection of the current model '
149 'when that model is destroyed.')
150 add_basic_testing_arguments(parser)
151 return parser.parse_args(argv)
152
153
154def main(argv=None):
155 args = parse_args(argv)
156 configure_logging(args.verbose)
157 bs_manager = BootstrapManager.from_args(args)
158 with bs_manager.booted_context(args.upload_tools):
159 assess_destroy_model(bs_manager.client)
160 return 0
161
162
163if __name__ == '__main__':
164 sys.exit(main())
0165
=== added file 'tests/test_assess_destroy_model.py'
--- tests/test_assess_destroy_model.py 1970-01-01 00:00:00 +0000
+++ tests/test_assess_destroy_model.py 2017-04-07 20:13:21 +0000
@@ -0,0 +1,175 @@
1"""Tests for assess_destroy_model module."""
2
3import logging
4import StringIO
5from textwrap import dedent
6
7from mock import (
8 Mock,
9 call,
10 patch,
11 )
12
13from assess_destroy_model import (
14 add_model,
15 destroy_model,
16 switch_model,
17 parse_args,
18 main,
19 )
20from jujupy import (
21 fake_juju_client,
22 )
23from tests import (
24 parse_error,
25 TestCase,
26 )
27from utility import (
28 JujuAssertionError
29 )
30
31list_model_initial = dedent(
32 """{
33 "models": [
34 {"name": "controller",
35 "controller-name": "foo-tmp-env"},
36 {"name": "foo-tmp-env",
37 "controller-name": "foo-tmp-env"}],
38 "current-model": "foo-tmp-env"
39 }""")
40
41list_model_destroy = dedent(
42 """{
43 "models": [
44 {"name": "controller",
45 "controller-name": "foo-tmp-env"},
46 {"name": "foo-tmp-env",
47 "controller-name": "foo-tmp-env"}]
48 }""")
49
50list_model_add = dedent(
51 """{
52 "models": [
53 {"name": "controller",
54 "controller-name": "foo-tmp-env"},
55 {"name": "test-tmp-env",
56 "controller-name": "foo-tmp-env"},
57 {"name": "foo-tmp-env",
58 "controller-name": "foo-tmp-env"}],
59 "current-model": "test-tmp-env"
60 }""")
61
62list_model_switch = dedent(
63 """{
64 "models": [
65 {"name": "controller",
66 "controller-name": "foo-tmp-env"},
67 {"name": "foo-tmp-env",
68 "controller-name": "foo-tmp-env"}],
69 "current-model": "bar-tmp-env"
70 }""")
71
72
73class TestParseArgs(TestCase):
74
75 def test_common_args(self):
76 args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
77 self.assertEqual("an-env", args.env)
78 self.assertEqual("/bin/juju", args.juju_bin)
79 self.assertEqual("/tmp/logs", args.logs)
80 self.assertEqual("an-env-mod", args.temp_env_name)
81 self.assertEqual(False, args.debug)
82
83 def test_help(self):
84 fake_stdout = StringIO.StringIO()
85 with parse_error(self) as fake_stderr:
86 with patch("sys.stdout", fake_stdout):
87 parse_args(["--help"])
88 self.assertEqual("", fake_stderr.getvalue())
89 self.assertNotIn("TODO", fake_stdout.getvalue())
90
91
92class TestMain(TestCase):
93
94 def test_main(self):
95 argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
96 client = Mock(spec=["is_jes_enabled"])
97 with patch("assess_destroy_model.configure_logging",
98 autospec=True) as mock_cl:
99 with patch("assess_destroy_model.BootstrapManager.booted_context",
100 autospec=True) as mock_bc:
101 with patch('deploy_stack.client_from_config',
102 return_value=client) as mock_cfc:
103 with patch("assess_destroy_model.assess_destroy_model",
104 autospec=True) as mock_assess:
105 main(argv)
106 mock_cl.assert_called_once_with(logging.DEBUG)
107 mock_cfc.assert_called_once_with('an-env', "/bin/juju", debug=False,
108 soft_deadline=None)
109 self.assertEqual(mock_bc.call_count, 1)
110 mock_assess.assert_called_once_with(client)
111
112
113class TestAssess(TestCase):
114
115 def test_add_model(self):
116 fake_client = Mock(wraps=fake_juju_client())
117 fake_client.bootstrap()
118 fake_client.get_juju_output.return_value = list_model_add
119 fake_client.add_model.return_value = fake_client
120 add_model(fake_client)
121 self.assertEqual(
122 [call.bootstrap(),
123 call.add_model('test-tmp-env'),
124 call.get_juju_output('list-models', '--format', 'json',
125 include_e=False)],
126 fake_client.mock_calls)
127
128 def test_add_model_fails(self):
129 fake_client = Mock(wraps=fake_juju_client())
130 fake_client.bootstrap()
131 fake_client.get_juju_output.return_value = list_model_initial
132 fake_client.add_model.return_value = fake_client
133 pattern = 'Expected test-tmp-env got foo-tmp-env'
134 with self.assertRaisesRegexp(JujuAssertionError, pattern):
135 add_model(fake_client)
136
137 def test_destroy_model(self):
138 fake_client = Mock(wraps=fake_juju_client())
139 fake_client.bootstrap()
140 fake_client.get_juju_output.return_value = list_model_destroy
141 destroy_model(fake_client, fake_client)
142 self.assertEqual(
143 [call.bootstrap(),
144 call.destroy_model(),
145 call.get_juju_output('list-models', '--format', 'json',
146 include_e=False)],
147 fake_client.mock_calls)
148
149 def test_destroy_model_fails(self):
150 fake_client = Mock(wraps=fake_juju_client())
151 fake_client.bootstrap()
152 fake_client.get_juju_output.return_value = list_model_initial
153 pattern = 'Juju failed to unset model after it was destroyed'
154 with self.assertRaisesRegexp(JujuAssertionError, pattern):
155 destroy_model(fake_client, fake_client)
156
157 def test_switch_model(self):
158 fake_client = Mock(wraps=fake_juju_client())
159 fake_client.bootstrap()
160 fake_client.get_juju_output.return_value = list_model_initial
161 switch_model(fake_client, 'foo-tmp-env', 'foo-tmp-env')
162 self.assertEqual(
163 [call.bootstrap(),
164 call.switch(controller='foo-tmp-env', model='foo-tmp-env'),
165 call.get_juju_output('list-models', '--format', 'json',
166 include_e=False)],
167 fake_client.mock_calls)
168
169 def test_switch_model_fails(self):
170 fake_client = Mock(wraps=fake_juju_client())
171 fake_client.bootstrap()
172 fake_client.get_juju_output.return_value = list_model_switch
173 pattern = 'Expected test-tmp-env got bar-tmp-env'
174 with self.assertRaisesRegexp(JujuAssertionError, pattern):
175 switch_model(fake_client, 'foo-tmp-env', 'test-tmp-env')

Subscribers

People subscribed via source and target branches