Merge lp:~mskalka/juju-ci-tools/better-expose-test into lp:juju-ci-tools

Proposed by Michael Skalka
Status: Merged
Merged at revision: 1942
Proposed branch: lp:~mskalka/juju-ci-tools/better-expose-test
Merge into: lp:juju-ci-tools
Diff against target: 564 lines (+208/-123)
4 files modified
assess_network_health.py (+117/-55)
jujupy/client.py (+29/-0)
jujupy/tests/test_client.py (+24/-0)
tests/test_assess_network_health.py (+38/-68)
To merge this branch: bzr merge lp:~mskalka/juju-ci-tools/better-expose-test
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+320409@code.launchpad.net

Description of the change

Fixes the expose test to actually test for exposed units. If there are exposed units in the deployment this runs through and deploys a network-health charm under every application, aliased as network-health-<application name> and exposes the network-health charms whos parent charm is also exposed. Then it hits each of the network-health charms with curl to its public address on port 8039 and collects the results. Finally if the expose test is run it cleans up the deployed charms to keep the goal of running under an existing deployment.

This also adds the WaitApplicationNotPresent class into jujupy, allowing jujupy to wait for an application to be removed before continuing.

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

moved early expose test return to start of function

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

Thank you. We need a test for WaitApplicationNotPresent added to test_client(). I have a question inline about ensure_exposed.

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

WaitApplicationNotPresent unittest

1946. By Michael Skalka

cleanup, logging

1947. By Michael Skalka

changed expose test logic to always run

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

Thank you. We agreed you will add a comment to setup_expose_test() to explain why the network-health charms are removed and redeployed. You can merge once the comment is added.

review: Approve (code)
1948. By Michael Skalka

added comment to setup_expose test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'assess_network_health.py'
2--- assess_network_health.py 2017-03-17 14:19:36 +0000
3+++ assess_network_health.py 2017-03-21 16:28:48 +0000
4@@ -15,7 +15,10 @@
5 from collections import defaultdict
6
7 from jujupy import (
8- client_for_existing,
9+ client_for_existing
10+ )
11+from jujupy.client import (
12+ WaitApplicationNotPresent
13 )
14 from deploy_stack import (
15 BootstrapManager
16@@ -36,6 +39,8 @@
17
18 NO_EXPOSED_UNITS = 'No exposed units'
19
20+PORT = 8039
21+
22
23 class AssessNetworkHealth:
24
25@@ -47,6 +52,7 @@
26 args.temp_env_name)
27 self.expose_client = None
28 self.existing_series = set([])
29+ self.expose_test_charms = set([])
30
31 def assess_network_health(self, client, bundle=None, target_model=None,
32 reboot=False, series=None, maas=None):
33@@ -102,15 +108,14 @@
34 json.dumps(vis_result,
35 indent=4,
36 sort_keys=True)))
37- exp_result = None
38- if not target_model:
39- exp_result = self.ensure_exposed(client, series)
40- log.info('{0}Exposure '
41- 'result:\n {1}'.format(reboot_msg,
42- json.dumps(exp_result,
43- indent=4,
44- sort_keys=True)) or
45- NO_EXPOSED_UNITS)
46+
47+ exp_result = self.ensure_exposed(client, series)
48+ log.info('{0}Exposure '
49+ 'result:\n {1}'.format(reboot_msg,
50+ json.dumps(exp_result,
51+ indent=4,
52+ sort_keys=True)) or
53+ NO_EXPOSED_UNITS)
54 log.info('Tests complete.')
55 return self.parse_final_results(vis_result, int_result,
56 exp_result)
57@@ -194,14 +199,15 @@
58
59 def cleanup(self, client):
60 log.info('Cleaning up deployed test charms and models.')
61+ if self.expose_test_charms:
62+ for charm in self.expose_test_charms:
63+ client.remove_service(charm)
64+ return
65 for series in self.existing_series:
66 client.remove_service('network-health-{}'.format(series))
67- if 'exposetest' in client.get_models().keys():
68- client.get_models()['exposetest'].destroy_model()
69- log.info('Cleanup complete.')
70
71 def get_unit_info(self, client):
72- """Gets the machine or container interface and dns info.
73+ """Gets the machine or container interface info.
74
75 :param client: Client to get results from
76 :return: Dict of machine results as
77@@ -294,10 +300,13 @@
78 for ip in target_ips:
79 result[app][unit][ip] = False
80 pattern = r"(pass)"
81- out = client.run(['curl {}:80'.format(ip)],
82+ log.info('Attempting to contact {}:{} '
83+ 'from {}'.format(ip, PORT, unit))
84+ out = client.run(['curl {}:{}'.format(ip, PORT)],
85 units=[unit])
86 match = re.search(pattern, json.dumps(out[0]))
87 if match:
88+ log.info('pass')
89 result[app][unit][ip] = True
90 return result
91
92@@ -308,38 +317,92 @@
93 :return: Exposure test results in dict by pass/fail
94 """
95 log.info('Starting test of exposed units.')
96+
97 apps = client.get_status().get_applications()
98- targets = self.parse_targets(client.get_status())
99- exposed = [app for app, e in apps.items() if e.get('exposed') is True]
100+ exposed = [app for app, e in apps.items() if e.get('exposed')
101+ is True and 'network-health' not in app]
102 if len(exposed) is 0:
103- log.info('No exposed units, aboring test.')
104- return None
105- new_client = self.setup_expose_test(client, series)
106+ nh_only = True
107+ log.info('No exposed units, testing with network-health '
108+ 'charms only.')
109+ else:
110+ nh_only = False
111+ self.setup_expose_test(client, series, exposed)
112+
113 service_results = {}
114- for service, units in targets.items():
115- service_results[service] = self.ping_units(new_client,
116- 'network-health/0',
117- units)
118+ for unit, info in client.get_status().iter_units():
119+ ip = info['public-address']
120+ if nh_only and 'network-health' in unit:
121+ service_results[unit] = self.curl(ip)
122+ elif not nh_only and 'network-health' not in unit:
123+ service_results[unit] = self.curl(ip)
124 log.info(service_results)
125 return self.parse_expose_results(service_results, exposed)
126
127- def setup_expose_test(self, client, series):
128- """Sets up new model to run exposure test.
129-
130- :param client: The juju client in use
131- :return: New juju client object
132- """
133- if not self.expose_client:
134- new_client = client.add_model('exposetest')
135- new_client.deploy('ubuntu', series=series)
136- new_client.deploy('~juju-qa/network-health', series=series)
137- new_client.wait_for_started()
138- new_client.wait_for_workloads()
139- new_client.juju('add-relation', ('ubuntu', 'network-health'))
140- new_client.wait_for_subordinate_units('ubuntu', 'network-health')
141- self.expose_client = new_client
142-
143- return self.expose_client
144+ def curl(self, ip):
145+ log.info('Attempting to curl unit at {}:{}'.format(ip, PORT))
146+ try:
147+ out = subprocess.check_output(
148+ 'curl {}:{} -m 5'.format(ip, PORT), shell=True)
149+ except subprocess.CalledProcessError as e:
150+ out = ''
151+ log.warning('Curl failed for error:\n{}'.format(e))
152+ log.info('Got: "{}" from unit at {}:{}'.format(out, ip, PORT))
153+ if 'pass' in out:
154+ return True
155+ return False
156+
157+ def setup_expose_test(self, client, series, exposed):
158+ """Sets up the expose test using aliased NH charms.
159+
160+ :param client: juju client object used in the test.
161+ :param series: Charm series
162+ :param exposed: List of exposed charms
163+ """
164+
165+ log.info('Removing previous network-health charms')
166+
167+ """
168+ This is done to work with the beahvior used in other network-health
169+ tests to circumvent Juju's lack of support for multi-series charms.
170+ If a multi-series subordinate is deployed under one of its available
171+ series, then a second copy of that charm in a differnt series cannot
172+ also be deployed. Subsequently, when we deploy the NH charms for the
173+ above tests, the series is appended to the end of the charm. In order
174+ for the expose test to work properly the NH charm has to be exposed,
175+ which in Juju means all of the NH charms under that alias or none.
176+ So if there are existing exposed units, the test redeploys an aliased
177+ NH charm under each so that it can expose them individually, ensuring
178+ valid test results.
179+ On the subject of speed, since the deps in network-health's wheelhouse
180+ have already been built on the target machine or container, this is a
181+ relatively fast process at ~30 seconds for large(6+ charm) deployments.
182+ """
183+ for series in self.existing_series:
184+ alias = 'network-health-{}'.format(series)
185+ client.remove_service(alias)
186+ for series in self.existing_series:
187+ alias = 'network-health-{}'.format(series)
188+ client.wait_for(WaitApplicationNotPresent(alias))
189+ log.info('Deploying aliased network-health charms')
190+ apps = client.get_status().get_applications()
191+ for app, info in apps.items():
192+ if 'network-health' not in app:
193+ alias = 'network-health-{}'.format(app)
194+ client.deploy('~juju-qa/network-health', alias=alias,
195+ series=info['series'])
196+ try:
197+ client.juju('add-relation', (app, alias))
198+ self.expose_test_charms.add(alias)
199+ except subprocess.CalledProcessError as e:
200+ log.warning('Could not relate {}, {} due to '
201+ 'error:\n{}'.format(app, alias, e))
202+ for app in apps.keys():
203+ if 'network-health' not in app:
204+ client.wait_for_subordinate_units(
205+ app, 'network-health-{}'.format(app))
206+ for app in exposed:
207+ client.juju('expose', ('network-health-{}'.format(app)))
208
209 def parse_expose_results(self, service_results, exposed):
210 """Parses expose test results into dict of pass/fail.
211@@ -347,21 +410,19 @@
212 :param service_results: Raw results from expose test
213 :return: Parsed results dict
214 """
215- result = {'fail': (),
216- 'pass': ()}
217- for service, results in service_results.items():
218- # If we could connect but shouldn't, fail
219- if 'True' in results and service not in exposed:
220- result['fail'] = result['fail'] + (service,)
221- # If we could connect but should, pass
222- elif 'True' in results and service in exposed:
223- result['pass'] = result['pass'] + (service,)
224- # If we couldn't connect and shouldn't, pass
225- elif 'False' in results and service not in exposed:
226- result['pass'] = result['pass'] + (service,)
227+ results = {'fail': (),
228+ 'pass': ()}
229+ for unit, result in service_results.items():
230+ app = unit.split('/')[0]
231+ if app in exposed and result:
232+ results['pass'] += (unit,)
233+ elif app in exposed and not result:
234+ results['fail'] += (unit,)
235+ elif app not in exposed and result:
236+ results['fail'] += (unit,)
237 else:
238- result['fail'] = result['fail'] + (service,)
239- return result
240+ results['pass'] += (unit,)
241+ return results
242
243 def parse_final_results(self, visibility, internet, exposed):
244 """Parses test results and raises an error if any failed.
245@@ -572,6 +633,7 @@
246 reboot=args.reboot)
247 finally:
248 test.cleanup(client)
249+ log.info('Cleanup complete.')
250 return 0
251
252
253
254=== modified file 'jujupy/client.py'
255--- jujupy/client.py 2017-03-15 20:09:30 +0000
256+++ jujupy/client.py 2017-03-21 16:28:48 +0000
257@@ -1338,6 +1338,35 @@
258 self.machine)
259
260
261+class WaitApplicationNotPresent(BaseCondition):
262+ """Condition satisfied when a given machine is not present."""
263+
264+ def __init__(self, application, timeout=300):
265+ super(WaitApplicationNotPresent, self).__init__(timeout)
266+ self.application = application
267+
268+ def __eq__(self, other):
269+ if not type(self) is type(other):
270+ return False
271+ if self.timeout != other.timeout:
272+ return False
273+ if self.application != other.application:
274+ return False
275+ return True
276+
277+ def __ne__(self, other):
278+ return not self.__eq__(other)
279+
280+ def iter_blocking_state(self, status):
281+ for application in status.get_applications().keys():
282+ if application == self.application:
283+ yield application, 'still-present'
284+
285+ def do_raise(self, model_name, status):
286+ raise Exception("Timed out waiting for application "
287+ "removal {}".format(self.application))
288+
289+
290 class MachineDown(BaseCondition):
291 """Condition satisfied when a given machine is down."""
292
293
294=== modified file 'jujupy/tests/test_client.py'
295--- jujupy/tests/test_client.py 2017-03-15 20:09:30 +0000
296+++ jujupy/tests/test_client.py 2017-03-21 16:28:48 +0000
297@@ -86,6 +86,7 @@
298 uniquify_local,
299 UnitError,
300 WaitMachineNotPresent,
301+ WaitApplicationNotPresent
302 )
303 from jujupy.fake import (
304 get_user_register_command_info,
305@@ -370,6 +371,29 @@
306 not_present.do_raise('', None)
307
308
309+class TestWaitApplicationNotPresent(ClientTest):
310+
311+ def test_iter_blocking_state(self):
312+ not_present = WaitApplicationNotPresent('foo')
313+ client = fake_juju_client()
314+ client.bootstrap()
315+ self.assertItemsEqual(
316+ [], not_present.iter_blocking_state(client.get_status()))
317+ client.deploy('foo')
318+ self.assertItemsEqual(
319+ [('foo', 'still-present')],
320+ not_present.iter_blocking_state(client.get_status()))
321+ client.remove_service('foo')
322+ self.assertItemsEqual(
323+ [], not_present.iter_blocking_state(client.get_status()))
324+
325+ def test_do_raise(self):
326+ not_present = WaitApplicationNotPresent('foo')
327+ with self.assertRaisesRegexp(
328+ Exception, 'Timed out waiting for application removal foo'):
329+ not_present.do_raise('', None)
330+
331+
332 class TestMachineDown(ClientTest):
333
334 def test_iter_blocking_state(self):
335
336=== modified file 'tests/test_assess_network_health.py'
337--- tests/test_assess_network_health.py 2017-03-16 20:51:05 +0000
338+++ tests/test_assess_network_health.py 2017-03-21 16:28:48 +0000
339@@ -2,7 +2,6 @@
340 import json
341 import StringIO
342 import logging
343-import argparse
344 import copy
345 from textwrap import dedent
346 from datetime import (
347@@ -28,9 +27,7 @@
348 parse_args,
349 _setup_spaces
350 )
351-from utility import (
352- add_basic_testing_arguments
353- )
354+
355
356 apps = {'foo':
357 {'charm-name': 'foo',
358@@ -157,11 +154,6 @@
359
360 class TestAssessNetworkHealth(TestCase):
361
362- def parse_args(self, args):
363- parser = argparse.ArgumentParser()
364- add_basic_testing_arguments(parser)
365- return parser.parse_args(args)
366-
367 def test_setup_testing_environment(self):
368
369 def setup_iteration(bundle, target_model, series):
370@@ -176,7 +168,7 @@
371 net_health.setup_testing_environment(mock_client, bundle,
372 target_model, series)
373 return mock_client
374- args = self.parse_args([])
375+ args = parse_args([])
376 net_health = AssessNetworkHealth(args)
377 client = setup_iteration(bundle=None, target_model=None, series=series)
378 self.assertEqual(
379@@ -232,7 +224,7 @@
380
381 def test_connect_to_existing_model_when_different(self):
382 model = {'bar': 'baz'}
383- args = self.parse_args([])
384+ args = parse_args([])
385 net_health = AssessNetworkHealth(args)
386 client = Mock(spec=["juju", "show_model", "switch"])
387 with patch.object(client, 'show_model', return_value=model):
388@@ -242,7 +234,7 @@
389
390 def test_connect_to_existing_model_when_same(self):
391 model = {'foo': 'baz'}
392- args = self.parse_args([])
393+ args = parse_args([])
394 net_health = AssessNetworkHealth(args)
395 client = Mock(spec=["juju", "show_model", "switch"])
396 with patch.object(client, 'show_model', return_value=model):
397@@ -254,7 +246,7 @@
398 pass
399
400 def test_neighbor_visibility(self):
401- args = self.parse_args([])
402+ args = parse_args([])
403 net_health = AssessNetworkHealth(args)
404 client = Mock(wraps=fake_juju_client())
405 client.bootstrap()
406@@ -272,7 +264,7 @@
407 self.assertEqual(expected, out)
408
409 def test_internet_connection(self):
410- args = self.parse_args([])
411+ args = parse_args([])
412 net_health = AssessNetworkHealth(args)
413 client = fake_juju_client()
414 client.bootstrap()
415@@ -293,27 +285,30 @@
416 self.assertEqual(expected, out)
417
418 def test_ensure_exposed(self):
419- args = self.parse_args([])
420+ args = parse_args([])
421 net_health = AssessNetworkHealth(args)
422- client = Mock(wraps=fake_juju_client())
423- client.bootstrap()
424- new_client = fake_juju_client()
425- new_client.bootstrap()
426- new_client._backend.set_action_result('network-health/0', 'ping',
427- ping_result)
428- new_client.deploy('ubuntu', num=2, series='trusty')
429- new_client.deploy('network-health', series='trusty')
430- now = datetime.now() + timedelta(days=1)
431- with patch('utility.until_timeout.now', return_value=now):
432- with patch.object(client, 'get_status', return_value=status):
433- with patch('assess_network_health.AssessNetworkHealth.'
434- 'setup_expose_test', return_value=new_client):
435- out = net_health.ensure_exposed(client, series)
436- expected = {'fail': (), 'pass': ('ubuntu',)}
437- self.assertEqual(out, expected)
438+ mock_client = Mock(spec=["juju", "deploy",
439+ "wait_for_subordinate_units",
440+ "get_status"])
441+ mock_client.get_status.return_value = status
442+ mock_client.series = 'xenial'
443+ mock_client.version = '2.2'
444+ with patch('subprocess.check_output', return_value='pass'):
445+ net_health.ensure_exposed(mock_client, series)
446+ self.assertEqual(
447+ [call.get_status(),
448+ call.get_status(),
449+ call.deploy('~juju-qa/network-health',
450+ alias='network-health-ubuntu', series='trusty'),
451+ call.juju('add-relation', ('ubuntu', 'network-health-ubuntu')),
452+ call.wait_for_subordinate_units('ubuntu',
453+ 'network-health-ubuntu'),
454+ call.juju('expose', 'network-health-ubuntu'),
455+ call.get_status()],
456+ mock_client.mock_calls)
457
458 def test_dummy_deployment(self):
459- args = self.parse_args([])
460+ args = parse_args([])
461 net_health = AssessNetworkHealth(args)
462 client = Mock(wraps=fake_juju_client())
463 client.bootstrap()
464@@ -321,38 +316,13 @@
465 client.deploy.assert_called_once_with('ubuntu', num=2, series='trusty')
466
467 def test_bundle_deployment(self):
468- args = self.parse_args([])
469+ args = parse_args([])
470 net_health = AssessNetworkHealth(args)
471 client = Mock(wraps=fake_juju_client())
472 client.bootstrap()
473 net_health.setup_bundle_deployment(client, bundle_string)
474 client.deploy_bundle.assert_called_once_with(bundle_string)
475
476- def test_setup_expose_test(self):
477- args = self.parse_args([])
478- net_health = AssessNetworkHealth(args)
479- mock_client = Mock(spec=["juju", "wait_for_started",
480- "wait_for_workloads", "deploy",
481- "get_juju_output",
482- "wait_for_subordinate_units",
483- "get_status", "deploy_bundle", "add_model"
484- ])
485- mock_client.series = 'trusty'
486- mock_client.version = '2.2'
487- net_health.setup_expose_test(mock_client, series)
488- self.assertEqual(
489- [call.add_model('exposetest'),
490- call.add_model().deploy('ubuntu', series='trusty'),
491- call.add_model().deploy('~juju-qa/network-health',
492- series='trusty'),
493- call.add_model().wait_for_started(),
494- call.add_model().wait_for_workloads(),
495- call.add_model().juju('add-relation', ('ubuntu',
496- 'network-health')),
497- call.add_model().wait_for_subordinate_units('ubuntu',
498- 'network-health')],
499- mock_client.mock_calls)
500-
501 def test_setup_spaces_existing_spaces(self):
502 existing_spaces = maas_spaces
503 new_spaces = _setup_spaces(bundle_yaml, existing_spaces)
504@@ -374,18 +344,18 @@
505 self.assertEqual(expected_spaces, new_spaces)
506
507 def test_parse_expose_results(self):
508- args = self.parse_args([])
509+ args = parse_args([])
510 net_health = AssessNetworkHealth(args)
511 exposed = ['bar', 'baz']
512- service_results = {'foo': "{'foo/0': 'True'}",
513- 'bar': "{'bar/0': 'False'}",
514- 'baz': "{'baz/0': 'True'}"}
515- expected = {"fail": ('foo', 'bar'), "pass": ('baz',)}
516+ service_results = {'foo': False,
517+ 'bar': True,
518+ 'baz': False}
519+ expected = {'fail': ('baz',), 'pass': ('foo', 'bar')}
520 result = net_health.parse_expose_results(service_results, exposed)
521 self.assertEqual(expected, result)
522
523 def test_parse_final_results_with_fail(self):
524- args = self.parse_args([])
525+ args = parse_args([])
526 net_health = AssessNetworkHealth(args)
527 visible = {"bar/0": {"foo": {"foo/0": False, "foo/1": True}}}
528 internet = {"0": False, "1": True}
529@@ -400,7 +370,7 @@
530 self.assertTrue(line in error_strings)
531
532 def test_parse_final_results_without_fail(self):
533- args = self.parse_args([])
534+ args = parse_args([])
535 net_health = AssessNetworkHealth(args)
536 visible = {"bar/0": {"foo": {"foo/0": True, "foo/1": True}}}
537 internet = {"0": True, "1": True}
538@@ -408,7 +378,7 @@
539 net_health.parse_final_results(visible, internet, exposed)
540
541 def test_ping_units(self):
542- args = self.parse_args([])
543+ args = parse_args([])
544 net_health = AssessNetworkHealth(args)
545 client = fake_juju_client()
546 client.bootstrap()
547@@ -419,7 +389,7 @@
548 self.assertEqual(out, result['results']['results'])
549
550 def test_to_json(self):
551- args = self.parse_args([])
552+ args = parse_args([])
553 net_health = AssessNetworkHealth(args)
554 expected = '("foo"=("foo/0"="1.1.1.1","foo/1"="1.1.1.2"))'
555 targets = {'foo': {'foo/0': '1.1.1.1', 'foo/1': '1.1.1.2'}}
556@@ -427,7 +397,7 @@
557 self.assertEqual(expected, json_like)
558
559 def test_parse_targets(self):
560- args = self.parse_args([])
561+ args = parse_args([])
562 net_health = AssessNetworkHealth(args)
563 client = fake_juju_client()
564 client.bootstrap()

Subscribers

People subscribed via source and target branches