Merge ~mthaddon/juju-upgrader/+git/juju-upgrader:juju-statistics into juju-upgrader:master

Proposed by Tom Haddon
Status: Merged
Approved by: Tom Haddon
Approved revision: c38721719d6b67bf51f69c0cb30d2a75fb468540
Merged at revision: 62695b0894f71e62fbfaa461eafc22fbe9d5ba4c
Proposed branch: ~mthaddon/juju-upgrader/+git/juju-upgrader:juju-statistics
Merge into: juju-upgrader:master
Diff against target: 545 lines (+533/-0)
2 files modified
juju_statistics.py (+79/-0)
unit_tests/test_juju_statistics.py (+454/-0)
Reviewer Review Type Date Requested Status
Joel Sing (community) +1 Approve
Stuart Bishop (community) Approve
Review via email: mp+335695@code.launchpad.net

Commit message

Add juju_statistics with unit tests

Description of the change

Add juju_statistics with unit tests

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Stuart Bishop (stub) wrote :

All looks good. Some taste things, not necessarily recommended to take on board.

review: Approve
Revision history for this message
Joel Sing (jsing) wrote :

LGTM with some minor comments.

review: Approve (+1)
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 62695b0894f71e62fbfaa461eafc22fbe9d5ba4c

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/juju_statistics.py b/juju_statistics.py
2new file mode 100644
3index 0000000..12bde95
4--- /dev/null
5+++ b/juju_statistics.py
6@@ -0,0 +1,79 @@
7+# Copyright (c) 2017, 2018 Canonical Ltd
8+# License: GPLv3
9+# Authors: Paul Gear <paul.gear@canonical.com>
10+# Tom Haddon <tom.haddon@canonical.com>
11+
12+import yaml
13+
14+import juju_utils
15+
16+
17+class Statistics:
18+
19+ def __init__(self):
20+ self.totals = {}
21+ self.summary = {}
22+
23+ def has_stats(self):
24+ return len(self.totals) > 0 or len(self.summary) > 0
25+
26+ def add_model(self, model):
27+ self._increment_data('models')
28+
29+ # count model breakdowns
30+ self._increment_summary('life', 'model', juju_utils.get_model_life(model))
31+ self._increment_summary('status', 'model', juju_utils.get_model_state(model))
32+
33+ def add_status(self, status):
34+ # count model versions
35+ self._increment_summary('version', 'model', juju_utils.get_model_status_version(status))
36+
37+ # count machines, incl. agent status & version
38+ machines = status.get('machines', [])
39+ self._increment_data('machines', len(machines))
40+ for m in machines:
41+ self._increment_status_sections('machines', machines[m])
42+
43+ # count applications & units
44+ apps = status.get('applications', [])
45+ self._increment_data('applications', len(apps))
46+ for a in apps:
47+ # primary units
48+ units = apps[a].get('units', [])
49+ self._increment_data('units', len(units))
50+ for u in units:
51+ self._increment_status_sections('units', units[u])
52+
53+ # subordinate units
54+ subordinates = units[u].get('subordinates', [])
55+ self._increment_data('units', len(subordinates))
56+ for s in subordinates:
57+ self._increment_status_sections('units', subordinates[s])
58+
59+ def _increment_status_sections(self, section, obj):
60+ juju_status = obj.get('juju-status', {})
61+ self._increment_summary('life', section, juju_status.get('life', None))
62+ self._increment_summary('status', section, juju_status.get('current', None))
63+ self._increment_summary('version', section, juju_status.get('version', None))
64+
65+ def _increment_data(self, key, i=1):
66+ self._increment_dict(self.totals, key, i)
67+
68+ def _increment_summary(self, section, key, val, i=1):
69+ if val is None:
70+ return
71+ self.summary.setdefault(section, {})
72+ self.summary[section].setdefault(key, {})
73+ self._increment_dict(self.summary[section][key], val, i)
74+
75+ def _increment_dict(self, obj, key, val):
76+ if key not in obj:
77+ obj[key] = val
78+ else:
79+ obj[key] += val
80+
81+ def report(self):
82+ output = ['']
83+ output.append(yaml.dump(self.totals, default_flow_style=False))
84+ output.append(yaml.dump(self.summary, default_flow_style=False))
85+ return "\n".join(output)
86diff --git a/unit_tests/test_juju_statistics.py b/unit_tests/test_juju_statistics.py
87new file mode 100644
88index 0000000..fa16ffd
89--- /dev/null
90+++ b/unit_tests/test_juju_statistics.py
91@@ -0,0 +1,454 @@
92+#!/usr/bin/python3
93+
94+# Copyright (c) 2018 Canonical Ltd
95+# License: GPLv3
96+# Author: Tom Haddon <tom.haddon@canonical.com>
97+#
98+# Unit tests for juju_statistics.py.
99+#
100+
101+import textwrap
102+import unittest
103+
104+import juju_statistics
105+
106+
107+class TestJujuStatistics(unittest.TestCase):
108+
109+ def setUp(self):
110+ self.stats = juju_statistics.Statistics()
111+
112+ def test___init__(self):
113+ self.assertEqual(self.stats.totals, {})
114+ self.assertEqual(self.stats.summary, {})
115+
116+ def test_has_stats(self):
117+ # No stats for a newly created Statistics object.
118+ self.assertEqual(len(self.stats.totals), 0)
119+ self.assertEqual(len(self.stats.summary), 0)
120+ self.assertFalse(self.stats.has_stats())
121+ # Now confirm if we populate either data or summary that we have
122+ # stats.
123+ self.stats.totals = {'key': 'value'}
124+ self.assertTrue(self.stats.has_stats())
125+ self.stats.totals = {}
126+ self.assertFalse(self.stats.has_stats())
127+ self.stats.summary = {'key': 'value'}
128+ self.assertTrue(self.stats.has_stats())
129+ self.stats.summary = {}
130+ self.assertFalse(self.stats.has_stats())
131+
132+ def test_add_model(self):
133+ test_model_obj1 = {
134+ 'life': 'alive',
135+ 'status': {'current': 'available'},
136+ }
137+ self.stats.add_model(test_model_obj1)
138+ self.assertEqual(self.stats.totals, {'models': 1})
139+ expected_summary = {
140+ 'life': {
141+ 'model': {
142+ 'alive': 1,
143+ }
144+ },
145+ 'status': {
146+ 'model': {
147+ 'available': 1,
148+ }
149+ }
150+ }
151+ self.assertEqual(self.stats.summary, expected_summary)
152+ test_model_obj2 = {
153+ 'life': 'alive',
154+ 'status': {'current': 'idle'},
155+ }
156+ self.stats.add_model(test_model_obj2)
157+ self.assertEqual(self.stats.totals, {'models': 2})
158+ expected_summary = {
159+ 'life': {
160+ 'model': {
161+ 'alive': 2,
162+ }
163+ },
164+ 'status': {
165+ 'model': {
166+ 'available': 1,
167+ 'idle': 1,
168+ }
169+ }
170+ }
171+ self.assertEqual(self.stats.summary, expected_summary)
172+
173+ def test_add_status(self):
174+ self.stats.add_status({})
175+ self.assertEqual(self.stats.totals, {'applications': 0, 'machines': 0})
176+ self.assertEqual(self.stats.summary, {})
177+ test_status_obj = {
178+ "model": {
179+ "name": "mojo-dot-canonical-dot-com",
180+ "controller": "prodstack",
181+ "cloud": "prodstack",
182+ "region": "prodstack",
183+ "version": "2.2.8",
184+ },
185+ "machines": {
186+ "0": {
187+ "juju-status": {
188+ "current": "started",
189+ "since": "02 Jan 2018 20:23:12Z",
190+ "version": "2.2.8",
191+ },
192+ "dns-name": "162.213.33.51",
193+ "ip-addresses": ["162.213.33.51", "10.0.0.10"],
194+ "instance-id": "68da26e2-9593-4ca7-9718-cda2dff6ceb1",
195+ "machine-status": {
196+ "current": "running",
197+ "message": "ACTIVE",
198+ "since": "30 Sep 2016 17:04:30Z",
199+ },
200+ "series": "xenial",
201+ "hardware": "arch=amd64 cores=1 mem=2048M root-disk=10240M availability-zone=nova",
202+ }
203+ },
204+ "applications": {
205+ "apache2": {
206+ "charm": "local:xenial/apache2-0",
207+ "series": "xenial",
208+ "os": "ubuntu",
209+ "charm-origin": "local",
210+ "charm-name": "apache2",
211+ "charm-rev": 0,
212+ "exposed": True,
213+ "application-status": {
214+ "current": "unknown",
215+ "since": "30 Sep 2016 17:07:54Z",
216+ },
217+ "relations": {
218+ "juju-info": ["content-fetcher", "livepatch", "landscape"],
219+ "nrpe-external-master": ["nrpe"],
220+ },
221+ "units": {
222+ "apache2/0": {
223+ "workload-status": {
224+ "current": "unknown",
225+ "since": "30 Sep 2016 17:07:54Z",
226+ },
227+ "juju-status": {
228+ "current": "idle",
229+ "since": "04 Jan 2018 09:10:28Z",
230+ "version": "2.2.8",
231+ },
232+ "leader": True,
233+ "machine": "0",
234+ "open-ports": ["80/tcp", "443/tcp"],
235+ "public-address": "162.213.33.51",
236+ "subordinates": {
237+ "content-fetcher/0": {
238+ "workload-status": {
239+ "current": "unknown",
240+ "since": "30 Sep 2016 17:28:40Z",
241+ },
242+ "juju-status": {
243+ "current": "idle",
244+ "since": "04 Jan 2018 09:15:47Z",
245+ "version": "2.2.8",
246+ },
247+ "leader": True,
248+ "upgrading-from": "local:xenial/content-fetcher-0",
249+ "public-address": "162.213.33.51",
250+ },
251+ "livepatch/0": {
252+ "workload-status": {
253+ "current": "active",
254+ "message": "Effective kernel 4.4.0-103-generic",
255+ "since": "04 Jan 2018 09:14:48Z",
256+ },
257+ "juju-status": {
258+ "current": "idle",
259+ "since": "04 Jan 2018 09:14:48Z",
260+ "version": "2.2.8",
261+ },
262+ "leader": True,
263+ "upgrading-from": "local:xenial/livepatch-5",
264+ "public-address": "162.213.33.51",
265+ },
266+ "landscape/1": {
267+ "workload-status": {
268+ "current": "active",
269+ "message": "System successfully registered",
270+ "since": "02 Jan 2018 20:24:58Z",
271+ },
272+ "juju-status": {
273+ "current": "idle",
274+ "since": "04 Jan 2018 09:10:41Z",
275+ "version": "2.2.8",
276+ },
277+ "leader": True,
278+ "upgrading-from": "local:xenial/landscape-client-23",
279+ "public-address": "162.213.33.51",
280+ },
281+ "nrpe/0": {
282+ "workload-status": {
283+ "current": "unknown",
284+ "since": "30 Sep 2016 17:29:04Z",
285+ },
286+ "juju-status": {
287+ "current": "idle",
288+ "since": "04 Jan 2018 09:16:01Z",
289+ "version": "2.2.8",
290+ },
291+ "leader": True,
292+ "upgrading-from": "local:xenial/nrpe-0",
293+ "public-address": "162.213.33.51",
294+ }
295+ }
296+ }
297+ }
298+ },
299+ "content-fetcher": {
300+ "charm": "local:xenial/content-fetcher-0",
301+ "series": "xenial",
302+ "os": "ubuntu",
303+ "charm-origin": "local",
304+ "charm-name": "content-fetcher",
305+ "charm-rev": 0,
306+ "exposed": False,
307+ "application-status": {
308+ "current": "unknown",
309+ "since": "30 Sep 2016 17:28:40Z",
310+ },
311+ "relations": {
312+ "general-info": ["apache2"],
313+ },
314+ "subordinate-to": ["apache2"],
315+ },
316+ "livepatch": {
317+ "charm": "local:xenial/livepatch-5",
318+ "series": "xenial",
319+ "os": "ubuntu",
320+ "charm-origin": "local",
321+ "charm-name": "livepatch",
322+ "charm-rev": 5,
323+ "exposed": False,
324+ "application-status": {
325+ "current": "active",
326+ "message": "Effective kernel 4.4.0-103-generic",
327+ "since": "04 Jan 2018 09:14:48Z",
328+ },
329+ "relations": {
330+ "general-info": ["apache2"],
331+ },
332+ "subordinate-to": ["apache2"],
333+ "version": "1.2.31",
334+ },
335+ "landscape": {
336+ "charm": "local:xenial/landscape-client-23",
337+ "series": "xenial",
338+ "os": "ubuntu",
339+ "charm-origin": "local",
340+ "charm-name": "landscape-client",
341+ "charm-rev": 23,
342+ "exposed": False,
343+ "application-status": {
344+ "current": "active",
345+ "message": "System successfully registered",
346+ "since": "02 Jan 2018 20:24:58Z",
347+ },
348+ "relations": {
349+ "container": ["apache2"],
350+ },
351+ "subordinate-to": ["apache2"],
352+ },
353+ "nrpe": {
354+ "charm": "local:xenial/nrpe-0",
355+ "series": "xenial",
356+ "os": "ubuntu",
357+ "charm-origin": "local",
358+ "charm-name": "nrpe",
359+ "charm-rev": 0,
360+ "exposed": False,
361+ "application-status": {
362+ "current": "unknown",
363+ "since": "30 Sep 2016 17:29:04Z",
364+ },
365+ "relations": {
366+ "nrpe-external-master": ["apache2"],
367+ },
368+ "subordinate-to": ["apache2"],
369+ },
370+ "ubuntu": {
371+ "charm": "cs:ubuntu-10",
372+ "series": "xenial",
373+ "os": "ubuntu",
374+ "charm-origin": "jujucharms",
375+ "charm-name": "ubuntu",
376+ "charm-rev": 10,
377+ "exposed": False,
378+ "application-status": {
379+ "current": "waiting",
380+ "message": "waiting for machine",
381+ "since": "24 Apr 2017 17:41:29Z",
382+ }
383+ }
384+ }
385+ }
386+ self.stats.add_status(test_status_obj)
387+ self.assertEqual(self.stats.totals, {'units': 5, 'applications': 6, 'machines': 1})
388+ expected_summary = {
389+ 'status': {
390+ 'units': {
391+ 'idle': 5,
392+ },
393+ 'machines': {
394+ 'started': 1,
395+ }
396+ },
397+ 'version': {
398+ 'model': {
399+ '2.2.8': 1,
400+ },
401+ 'units': {
402+ '2.2.8': 5,
403+ },
404+ 'machines': {
405+ '2.2.8': 1,
406+ }
407+ }
408+ }
409+ self.assertEqual(self.stats.summary, expected_summary)
410+
411+ def test__increment_dict(self):
412+ test_dict = {}
413+ self.stats._increment_dict(test_dict, 'models', 1)
414+ self.assertEqual(test_dict, {'models': 1})
415+ self.stats._increment_dict(test_dict, 'models', 10)
416+ self.assertEqual(test_dict, {'models': 11})
417+
418+ def test__increment_data(self):
419+ self.stats._increment_data('models')
420+ self.assertEqual(self.stats.totals, {'models': 1})
421+ self.stats._increment_data('models')
422+ self.assertEqual(self.stats.totals, {'models': 2})
423+ self.stats._increment_data('units', 10)
424+ self.assertEqual(self.stats.totals, {'models': 2, 'units': 10})
425+ self.stats._increment_data('units', 2)
426+ self.assertEqual(self.stats.totals, {'models': 2, 'units': 12})
427+
428+ def test__increment_summary(self):
429+ self.stats._increment_summary('life', 'machines', 'alive')
430+ self.assertEqual(self.stats.summary, {'life': {'machines': {'alive': 1}}})
431+ self.stats._increment_summary('life', 'machines', 'alive')
432+ self.assertEqual(self.stats.summary, {'life': {'machines': {'alive': 2}}})
433+ self.stats._increment_summary('life', 'machines', 'alive', 10)
434+ self.assertEqual(self.stats.summary, {'life': {'machines': {'alive': 12}}})
435+ self.stats._increment_summary('life', 'machines', None)
436+ self.assertEqual(self.stats.summary, {'life': {'machines': {'alive': 12}}})
437+ self.stats._increment_summary('life', 'machines', 'dead')
438+ self.assertEqual(self.stats.summary, {'life': {'machines': {'alive': 12, 'dead': 1}}})
439+
440+ def test__increment_status_sections(self):
441+ test_machine_status_obj1 = {
442+ 'juju-status': {
443+ 'life': 'alive',
444+ 'version': '2.2.8',
445+ }
446+ }
447+ self.stats._increment_status_sections('machines', test_machine_status_obj1)
448+ expected_summary = {
449+ 'life': {
450+ 'machines': {
451+ 'alive': 1,
452+ }
453+ },
454+ 'version': {
455+ 'machines': {
456+ '2.2.8': 1,
457+ }
458+ }
459+ }
460+ self.assertEqual(self.stats.summary, expected_summary)
461+ test_machine_status_obj2 = {
462+ 'juju-status': {
463+ 'life': 'dead',
464+ 'version': '2.2.8',
465+ }
466+ }
467+ self.stats._increment_status_sections('machines', test_machine_status_obj2)
468+ expected_summary = {
469+ 'life': {
470+ 'machines': {
471+ 'alive': 1,
472+ 'dead': 1,
473+ }
474+ },
475+ 'version': {
476+ 'machines': {
477+ '2.2.8': 2,
478+ }
479+ }
480+ }
481+ self.assertEqual(self.stats.summary, expected_summary)
482+
483+ def test_report(self):
484+ self.stats.totals = {
485+ 'applications': 940,
486+ 'machines': 559,
487+ 'models': 117,
488+ 'units': 2494,
489+ }
490+ self.stats.summary = {
491+ 'life': {
492+ 'model': {
493+ 'alive': 177,
494+ }
495+ },
496+ 'status': {
497+ 'machines': {
498+ 'started': 559,
499+ },
500+ 'model': {
501+ 'available': 177,
502+ },
503+ 'units': {
504+ 'executing': 13,
505+ 'idle': 2481,
506+ },
507+ 'version': {
508+ 'machines': {
509+ '2.2.8': 559,
510+ },
511+ 'model': {
512+ '2.2.8': 177,
513+ },
514+ 'units': {
515+ '2.2.8': 2494,
516+ }
517+ }
518+ }
519+ }
520+ expected_report = textwrap.dedent("""
521+ applications: 940
522+ machines: 559
523+ models: 117
524+ units: 2494
525+
526+ life:
527+ model:
528+ alive: 177
529+ status:
530+ machines:
531+ started: 559
532+ model:
533+ available: 177
534+ units:
535+ executing: 13
536+ idle: 2481
537+ version:
538+ machines:
539+ 2.2.8: 559
540+ model:
541+ 2.2.8: 177
542+ units:
543+ 2.2.8: 2494
544+ """)
545+ self.assertEqual(self.stats.report(), expected_report)

Subscribers

People subscribed via source and target branches