Merge lp:~sseman/juju-ci-tools/model-change-watcher-py3 into lp:juju-ci-tools

Proposed by Seman
Status: Merged
Merged at revision: 1797
Proposed branch: lp:~sseman/juju-ci-tools/model-change-watcher-py3
Merge into: lp:juju-ci-tools
Diff against target: 430 lines (+185/-35)
9 files modified
Makefile (+3/-1)
assess_constraints.py (+1/-1)
assess_model_change_watcher.py (+122/-0)
deploy_stack.py (+8/-5)
fakejuju.py (+11/-14)
jujupy.py (+13/-6)
substrate.py (+4/-2)
tests/__init__.py (+19/-5)
utility.py (+4/-1)
To merge this branch: bzr merge lp:~sseman/juju-ci-tools/model-change-watcher-py3
Reviewer Review Type Date Requested Status
Nicholas Skaggs (community) Approve
Aaron Bentley (community) Needs Fixing
Martin Packman (community) Approve
Review via email: mp+312523@code.launchpad.net

Description of the change

This branch adds a test for 2.1 feature "Model changes needs to be in mega-watcher". It basically
   - bootstraps, deploys,
   - start listening to the watcher asynchronously,
   - make a config change,
   - verify the config change event is sent by Juju.

Since this test uses https://github.com/juju/python-libjuju, it runs under Python 3. It also updates some of the shared juju-ci-tools packages so that they work with Python 2 as well as Python 3.

If you have issue installing https://github.com/juju/python-libjuju from the source, just clone the repo and add PYTHONPATH to the lib.

Makefile has been updated to exclude this test from Python 2 lint checks but added another test for Python 3 files.

This is one of the two branches. What is missing from this branch and will address in next branch:
   - Add unit tests
   - Update makefile to run Py2 and Py3 tests.

I ran this test and it is working https://pastebin.canonical.com/172811/

To post a comment you must log in.
1789. By Aaron Bentley

Remove --clean and WaitForSearch.

1790. By Curtis Hovey

Upgrade maas micro version.

1791. By Curtis Hovey

Access container networking on xenial.

1792. By Aaron Bentley

Provide progress output from wait_for.

Revision history for this message
Martin Packman (gz) wrote :

No detailed comments on the asyncio script, but nothing to block landing on. See comments on Python 3 related changes.

review: Needs Fixing
1793. By Andrew James Beach

Merged in fixes to model migration tests.

Revision history for this message
Seman (sseman) wrote :

Thanks for reviewing. Mostly, I updated the code per you suggestion. Some will update on my follow up branch. See inline comments.

Incremental diff https://pastebin.canonical.com/173065/

1794. By Aaron Bentley

Implmement wait_for_version using wait_for.

1795. By Andrew James Beach

Merged in fake_juju_client's new juju_home argument.

Revision history for this message
Martin Packman (gz) wrote :

Thanks! One note over a misunderstanding of a suggestion.

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

> Thanks for reviewing. Mostly, I updated the code per you suggestion. Some will update on my follow up branch. See inline comments.
>> - for model in self.models.values():
>> + for model in list(self.models.values()):
>
> I changed it back. Maybe this is something added for debugging an issue.

I wrote that code for a reason. For you. If you don't understand it, you should ask me.

Specifically, the length of self.models.values changes during iteration. You will see a warning about that when you run the tests. listifying self.models.values fixes that problem.

The length of self.models.values changes because FakeEnvironmentState.destroy_model does:
del self.controller.models[self.name]

review: Needs Fixing
1796. By Seman

Merged.

1797. By Seman

Python 2/3 compatibility update.

Revision history for this message
Seman (sseman) wrote :

I wasn't sure who updated the code, so I removed the change and ran the tests. Since the code is in fakejuju, any issue should be discovered by just running the tests. The test didn't fail but I didn't notice the warning. This code has been reverted.

Incremental diff https://pastebin.canonical.com/173183/

Revision history for this message
Nicholas Skaggs (nskaggs) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2016-11-18 20:05:05 +0000
3+++ Makefile 2016-12-09 00:09:01 +0000
4@@ -1,8 +1,10 @@
5 p=test*.py
6+py3="assess_model_change_watcher.py"
7 test:
8 TMPDIR=/tmp python -m unittest discover -vv ./tests -p "$(p)"
9 lint:
10- flake8 $$(find -name '*.py') --builtins xrange,basestring
11+ python3 -m flake8 --builtins xrange,basestring $(py3)
12+ flake8 $$(find -name '*.py') --builtins xrange,basestring --exclude $(py3)
13 cover:
14 python -m coverage run --source="./" --omit "./tests/*" -m unittest discover -vv ./tests
15 python -m coverage report
16
17=== modified file 'assess_constraints.py'
18--- assess_constraints.py 2016-10-22 02:03:03 +0000
19+++ assess_constraints.py 2016-12-09 00:09:01 +0000
20@@ -219,7 +219,7 @@
21 status = client.get_status()
22 app_data = status.get_applications()[application]
23 machines = [unit_data['machine'] for unit_data in
24- app_data['units'].itervalues()]
25+ app_data['units'].values()]
26 return machines
27
28
29
30=== added file 'assess_model_change_watcher.py'
31--- assess_model_change_watcher.py 1970-01-01 00:00:00 +0000
32+++ assess_model_change_watcher.py 2016-12-09 00:09:01 +0000
33@@ -0,0 +1,122 @@
34+#!/usr/bin/env python3
35+
36+import argparse
37+import asyncio
38+import logging
39+import os
40+import sys
41+
42+from juju.client.connection import Connection
43+from juju.client import watcher
44+
45+from deploy_stack import (
46+ BootstrapManager,
47+ until_timeout,
48+ )
49+from jujucharm import local_charm_path
50+from utility import (
51+ add_basic_testing_arguments,
52+ configure_logging,
53+ JujuAssertionError,
54+ )
55+
56+
57+log = logging.getLogger("assess_model_change_watcher")
58+TOKEN = "1234asdf"
59+
60+
61+def is_config_change_in_event(event):
62+ message = _get_message(event)
63+ return all([
64+ "application" in event,
65+ "change" in event,
66+ is_in_dict("config", {"token": TOKEN}, message),
67+ ])
68+
69+
70+def _get_message(event):
71+ for message in event:
72+ if isinstance(message, dict):
73+ return message
74+ return None
75+
76+
77+def is_in_dict(key, value, items):
78+ return items.get(key) == value
79+
80+
81+async def listen_to_watcher(event_found, conn, future):
82+ logging.info("Starting to listen for the watcher.")
83+ all_watcher = watcher.AllWatcher()
84+ all_watcher.connect(conn)
85+ for _ in until_timeout(120):
86+ logging.info("Listening for events...")
87+ change = await all_watcher.Next()
88+ for delta in change.deltas:
89+ logging.info("Event received: {}".format(str(delta.deltas)))
90+ if event_found(delta.deltas) is True:
91+ await all_watcher.Stop()
92+ await conn.close()
93+ logging.info("Event found: {}".format(str(delta.deltas)))
94+ future.set_result(True)
95+ return
96+
97+ await all_watcher.Stop()
98+ await conn.close()
99+ logging.warning("Event not found.")
100+ future.set_result(False)
101+
102+
103+def run_listener(client, event, juju_bin):
104+ logging.info("Running listener.")
105+ loop = asyncio.get_event_loop()
106+ future = asyncio.Future()
107+
108+ logging.info("Connect to the current model.")
109+ os.environ['JUJU_DATA'] = client.env.juju_home
110+ os.environ['PATH'] = "{}{}{}".format(
111+ juju_bin, os.pathsep, os.environ.get('PATH', ''))
112+ conn = loop.run_until_complete(Connection.connect_current())
113+ logging.info("Connected to the current model.")
114+
115+ asyncio.ensure_future(listen_to_watcher(event, conn, future))
116+ return loop, future
117+
118+
119+def assess_model_change_watcher(client, charm_series, juju_bin):
120+ charm = local_charm_path(
121+ charm='dummy-source', juju_ver=client.version, series=charm_series,
122+ platform='ubuntu')
123+ client.deploy(charm)
124+ client.wait_for_started()
125+
126+ loop, future = run_listener(client, is_config_change_in_event, juju_bin)
127+
128+ logging.info("Making config change.")
129+ client.set_config('dummy-source', {'token': TOKEN})
130+
131+ loop.run_until_complete(future)
132+ result = future.result()
133+ if result is not True:
134+ raise JujuAssertionError("Config change event was not sent.")
135+ loop.close()
136+
137+
138+def parse_args(argv):
139+ parser = argparse.ArgumentParser(description="Assess config change.")
140+ add_basic_testing_arguments(parser)
141+ return parser.parse_args(argv)
142+
143+
144+def main(argv=None):
145+ args = parse_args(argv)
146+ configure_logging(args.verbose)
147+ bs_manager = BootstrapManager.from_args(args)
148+ with bs_manager.booted_context(args.upload_tools):
149+ assess_model_change_watcher(
150+ bs_manager.client, bs_manager.series, args.juju_bin)
151+ return 0
152+
153+
154+if __name__ == '__main__':
155+ sys.exit(main())
156
157=== modified file 'deploy_stack.py'
158--- deploy_stack.py 2016-12-01 16:39:37 +0000
159+++ deploy_stack.py 2016-12-09 00:09:01 +0000
160@@ -3,10 +3,13 @@
161
162
163 from argparse import ArgumentParser
164-from contextlib import (
165- contextmanager,
166- nested,
167-)
168+from contextlib import contextmanager
169+try:
170+ from contextlib import nested
171+except ImportError:
172+ from contextlib import ExitStack as nested
173+
174+
175 from datetime import (
176 datetime,
177 )
178@@ -274,7 +277,7 @@
179 if machine_id not in machines:
180 machines[machine_id] = remote_from_address(address)
181 # Update remote machines in place with real addresses if substrate needs.
182- resolve_remote_dns_names(client.env, machines.itervalues())
183+ resolve_remote_dns_names(client.env, machines.values())
184 return machines
185
186
187
188=== modified file 'fakejuju.py'
189--- fakejuju.py 2016-12-08 15:12:32 +0000
190+++ fakejuju.py 2016-12-09 00:09:01 +0000
191@@ -21,14 +21,11 @@
192
193 __metaclass__ = type
194
195-
196-def check_juju_output(func):
197- def wrapper(*args, **kwargs):
198- result = func(*args, **kwargs)
199- if 'service' in result:
200- raise AssertionError('Result contained service')
201- return result
202- return wrapper
203+# Python 2 and 3 compatibility
204+try:
205+ argtype = basestring
206+except NameError:
207+ argtype = str
208
209
210 class ControllerOperation(Exception):
211@@ -106,7 +103,7 @@
212 self.state = 'registered'
213
214 def destroy(self, kill=False):
215- for model in self.models.values():
216+ for model in list(self.models.values()):
217 model.destroy_model()
218 self.models.clear()
219 if kill:
220@@ -146,7 +143,7 @@
221
222 def add_machine(self, host_name=None, machine_id=None):
223 if machine_id is None:
224- machine_id = str(self.machine_id_iter.next())
225+ machine_id = str(next(self.machine_id_iter))
226 self.machines.add(machine_id)
227 if host_name is None:
228 host_name = '{}.example.com'.format(machine_id)
229@@ -732,13 +729,14 @@
230 if model is not None:
231 full_args.extend(['-m', model])
232 full_args.extend(args)
233- self.log.log(level, ' '.join(full_args))
234+ self.log.log(level, u' '.join(full_args))
235
236 def juju(self, command, args, used_feature_flags,
237 juju_home, model=None, check=True, timeout=None, extra_env=None):
238 if 'service' in command:
239 raise Exception('Command names must not contain "service".')
240- if isinstance(args, basestring):
241+
242+ if isinstance(args, argtype):
243 args = (args,)
244 self._log_command(command, args, model)
245 if model is not None:
246@@ -867,7 +865,6 @@
247 self.juju(command, args, used_feature_flags,
248 juju_home, model, timeout=timeout)
249
250- @check_juju_output
251 def get_juju_output(self, command, args, used_feature_flags, juju_home,
252 model=None, timeout=None, user_name=None,
253 merge_stderr=False):
254@@ -910,7 +907,7 @@
255 status_dict = model_state.get_status_dict()
256 # Parsing JSON is much faster than parsing YAML, and JSON is a
257 # subset of YAML, so emit JSON.
258- return json.dumps(status_dict)
259+ return json.dumps(status_dict).encode('utf-8')
260 if command == 'create-backup':
261 self.controller_state.require_controller('backup', model)
262 return 'juju-backup-0.tar.gz'
263
264=== modified file 'jujupy.py'
265--- jujupy.py 2016-12-08 08:22:52 +0000
266+++ jujupy.py 2016-12-09 00:09:01 +0000
267@@ -55,6 +55,12 @@
268
269 __metaclass__ = type
270
271+# Python 2 and 3 compatibility
272+try:
273+ argtype = basestring
274+except NameError:
275+ argtype = str
276+
277 AGENTS_READY = set(['started', 'idle'])
278 WIN_JUJU_CMD = os.path.join('\\', 'Progra~2', 'Juju', 'juju.exe')
279
280@@ -251,11 +257,11 @@
281
282
283 @contextmanager
284-def temp_yaml_file(yaml_dict):
285+def temp_yaml_file(yaml_dict, encoding="utf-8"):
286 temp_file = NamedTemporaryFile(suffix='.yaml', delete=False)
287 try:
288 with temp_file:
289- yaml.safe_dump(yaml_dict, temp_file)
290+ yaml.safe_dump(yaml_dict, temp_file, encoding=encoding)
291 yield temp_file.name
292 finally:
293 os.unlink(temp_file.name)
294@@ -1129,7 +1135,7 @@
295
296 # If args is a string, make it a tuple. This makes writing commands
297 # with one argument a bit nicer.
298- if isinstance(args, basestring):
299+ if isinstance(args, argtype):
300 args = (args,)
301 # we split the command here so that the caller can control where the -m
302 # model flag goes. Everything in the command string is put before the
303@@ -1276,7 +1282,7 @@
304 enforced.
305 """
306 version = EnvJujuClient.get_version(juju_path)
307- client_class = get_client_class(version)
308+ client_class = get_client_class(str(version))
309 if config is None:
310 env = client_class.config_class('', {})
311 else:
312@@ -1385,7 +1391,8 @@
313 """
314 if juju_path is None:
315 juju_path = 'juju'
316- return subprocess.check_output((juju_path, '--version')).strip()
317+ version = subprocess.check_output((juju_path, '--version')).strip()
318+ return version.decode("utf-8")
319
320 def check_timeouts(self):
321 return self._backend._check_timeouts()
322@@ -1799,7 +1806,7 @@
323 return self.status_class.from_text(
324 self.get_juju_output(
325 self._show_status, '--format', 'yaml',
326- controller=controller))
327+ controller=controller).decode('utf-8'))
328 except subprocess.CalledProcessError:
329 pass
330 raise StatusTimeout(
331
332=== modified file 'substrate.py'
333--- substrate.py 2016-11-28 22:04:46 +0000
334+++ substrate.py 2016-12-09 00:09:01 +0000
335@@ -6,8 +6,10 @@
336 import os
337 import subprocess
338 from time import sleep
339-import urlparse
340-
341+try:
342+ import urlparse
343+except ImportError:
344+ import urllib.parse as parser
345 from boto import ec2
346 from boto.exception import EC2ResponseError
347
348
349=== modified file 'tests/__init__.py'
350--- tests/__init__.py 2016-11-21 16:30:01 +0000
351+++ tests/__init__.py 2016-12-09 00:09:01 +0000
352@@ -5,12 +5,20 @@
353 import errno
354 import logging
355 import os
356-import StringIO
357+import io
358+try:
359+ from StringIO import StringIO
360+except ImportError:
361+ from io import StringIO
362 import subprocess
363+import sys
364 from tempfile import NamedTemporaryFile
365 import unittest
366
367-from mock import patch
368+try:
369+ from mock import patch
370+except ImportError:
371+ from unittest.mock import patch
372 import yaml
373
374 import utility
375@@ -18,7 +26,10 @@
376
377 @contextmanager
378 def stdout_guard():
379- stdout = StringIO.StringIO()
380+ if isinstance(sys.stdout, io.TextIOWrapper):
381+ stdout = io.StringIO()
382+ else:
383+ stdout = io.BytesIO()
384 with patch('sys.stdout', stdout):
385 yield
386 if stdout.getvalue() != '':
387@@ -103,7 +114,7 @@
388 log = logging.getLogger()
389 testcase.addCleanup(setattr, log, 'handlers', log.handlers)
390 log.handlers = []
391- testcase.log_stream = StringIO.StringIO()
392+ testcase.log_stream = StringIO()
393 handler = logging.StreamHandler(testcase.log_stream)
394 handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
395 log.addHandler(handler)
396@@ -118,7 +129,10 @@
397
398 @contextmanager
399 def parse_error(test_case):
400- stderr = StringIO.StringIO()
401+ if isinstance(sys.stdout, io.TextIOWrapper):
402+ stderr = io.StringIO()
403+ else:
404+ stderr = io.BytesIO()
405 with test_case.assertRaises(SystemExit):
406 with patch('sys.stderr', stderr):
407 yield stderr
408
409=== modified file 'utility.py'
410--- utility.py 2016-11-18 20:26:28 +0000
411+++ utility.py 2016-12-09 00:09:01 +0000
412@@ -88,6 +88,9 @@
413 def now():
414 return datetime.now()
415
416+ def __next__(self):
417+ self.next()
418+
419 def next(self):
420 elapsed = self.now() - self.start
421 remaining = self.timeout - elapsed.total_seconds()
422@@ -190,7 +193,7 @@
423 else:
424 continue
425 conn = socket.socket(*addrinfo[0][:3])
426- conn.settimeout(max(remaining, 5))
427+ conn.settimeout(max(remaining or 0, 5))
428 try:
429 conn.connect(sockaddr)
430 except socket.timeout:

Subscribers

People subscribed via source and target branches