Merge lp:~cprov/adt-continuous-deployer/better-monitor into lp:adt-continuous-deployer
- better-monitor
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Celso Providelo |
Approved revision: | 48 |
Merged at revision: | 43 |
Proposed branch: | lp:~cprov/adt-continuous-deployer/better-monitor |
Merge into: | lp:adt-continuous-deployer |
Diff against target: |
643 lines (+312/-229) 6 files modified
ci_automation/nova.py (+92/-0) ci_automation/tests/test_branch.py (+6/-6) ci_automation/tests/test_nova.py (+90/-81) ci_automation/utils.py (+30/-0) list.py (+56/-0) monitor.py (+38/-142) |
To merge this branch: | bzr merge lp:~cprov/adt-continuous-deployer/better-monitor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis Ginther | Approve | ||
Para Siva (community) | Approve | ||
Evan (community) | Needs Information | ||
Paul Larson | Approve | ||
Review via email: mp+257315@code.launchpad.net |
Commit message
Refactoring monitor.py and also adding support for basic auto-deployed service listing with list.py script.
Description of the change
Refactoring monitor.py and also adding support for basic service listing via list.py:
{{{
stg-ue-
* mojo-ue-
11a8f85b: Thu Apr 16 22:05:46 2015 (4 units)
* mojo-ue-
e240a00e: Thu Apr 23 18:50:44 2015 (7 units)
* mojo-ue-
c1ec6cc8: Wed Apr 15 19:20:48 2015 (4 units)
* mojo-ue-
13d54727: Wed Apr 15 21:35:30 2015 (2 units)
* mojo-ue-
324e91c7: Thu Apr 23 04:15:42 2015 (2 units)
* mojo-ue-
057b67aa: Thu Apr 23 19:20:44 2015 (2 units)
* mojo-ue-
1aeb45ff: Thu Apr 23 02:10:52 2015 (2 units)
* mojo-ue-
428911cd: Tue Apr 14 15:40:40 2015 (2 units)
}}}
Removal of obsolete deployments still supported by `./monitor.py mojo-ue-
Also fixing some broken and ignored tests from the shortening-
- 45. By Celso Providelo
-
Sorted service list
Does pretty-printing the deployments and purging old deployments really belong in the same program, especially as the algorithm to decide what old environments grows in complexity?
See inline comment.
Para Siva (psivaa) wrote : | # |
An inline comment about a typo for logstash env variable.
Also tested the 'list' option to be working.
- 46. By Celso Providelo
-
Fixing typo.
Celso Providelo (cprov) wrote : | # |
Psivaa, nice catch, typo fixed.
I still have to address Ev's comment/suggestion for preserving the old monitor.py behavior and creating a list.py script for the pretty-print task.
- 47. By Celso Providelo
-
Dedicated script for listing auto-deployed services (list.py), monitor.py preserves previous behaviour.
Celso Providelo (cprov) wrote : | # |
Ev and others,
I've restored monitor.py behaviour (refactored on top of the common nova functions) and introduced list.py for listing auto-deployed services nicely.
Let me know what do you think.
Para Siva (psivaa) wrote : | # |
LGTM now, approving.
A docstring for list.py would be a bonus.
- 48. By Celso Providelo
-
Docstring fixing.
Preview Diff
1 | === added file 'ci_automation/nova.py' | |||
2 | --- ci_automation/nova.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ ci_automation/nova.py 2015-04-27 18:16:41 +0000 | |||
4 | @@ -0,0 +1,92 @@ | |||
5 | 1 | # Copyright (C) 2015 Canonical | ||
6 | 2 | # | ||
7 | 3 | # This program is free software: you can redistribute it and/or modify | ||
8 | 4 | # it under the terms of the GNU General Public License as published by | ||
9 | 5 | # the Free Software Foundation, either version 3 of the License, or | ||
10 | 6 | # (at your option) any later version. | ||
11 | 7 | # | ||
12 | 8 | # This program is distributed in the hope that it will be useful, | ||
13 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
14 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
15 | 11 | # GNU General Public License for more details. | ||
16 | 12 | # | ||
17 | 13 | # You should have received a copy of the GNU General Public License | ||
18 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
19 | 15 | # | ||
20 | 16 | |||
21 | 17 | """CI Automation: nova helpers.""" | ||
22 | 18 | |||
23 | 19 | import io | ||
24 | 20 | import os | ||
25 | 21 | import stat | ||
26 | 22 | import subprocess | ||
27 | 23 | |||
28 | 24 | |||
29 | 25 | def nova_list(): | ||
30 | 26 | """Default 'nova list' accessor.""" | ||
31 | 27 | args = [ | ||
32 | 28 | 'nova', | ||
33 | 29 | 'list', | ||
34 | 30 | '--minimal', | ||
35 | 31 | ] | ||
36 | 32 | |||
37 | 33 | output = subprocess.check_output( | ||
38 | 34 | args, stderr=subprocess.DEVNULL).decode('utf-8').strip() | ||
39 | 35 | |||
40 | 36 | return output | ||
41 | 37 | |||
42 | 38 | |||
43 | 39 | def get_services_map(base_dir, nova_output=None): | ||
44 | 40 | """Return a services map dictionary. | ||
45 | 41 | |||
46 | 42 | 'base_dir' is the path where deployment identifier can be found. | ||
47 | 43 | |||
48 | 44 | If 'nova_output' is not provided `nova_list` accessor is used to get | ||
49 | 45 | this information. | ||
50 | 46 | |||
51 | 47 | Returns a dictionary key-ed by service name (middle term of the mojo | ||
52 | 48 | stage used in the deployment, e.g. 'mojo-ue-core-image-watcher'): | ||
53 | 49 | |||
54 | 50 | { | ||
55 | 51 | 'mojo-ue-core-image-watcher': { | ||
56 | 52 | 'DEADBEEF': { | ||
57 | 53 | 'path': <identifier_path>, | ||
58 | 54 | 'epoch': <identifier_ST_MTIME>, | ||
59 | 55 | 'units': [<unit_name>, <unit_name>, ...], | ||
60 | 56 | }, | ||
61 | 57 | 'ABCD1234': { | ||
62 | 58 | 'path': <identifier_path>, | ||
63 | 59 | 'epoch': <identifier_ST_MTIME>, | ||
64 | 60 | 'units': [<unit_name>, <unit_name>, ...], | ||
65 | 61 | }, | ||
66 | 62 | }, | ||
67 | 63 | } | ||
68 | 64 | |||
69 | 65 | Manually deployed environments, i.e. no identifiers on disk, are ignored. | ||
70 | 66 | """ | ||
71 | 67 | if nova_output is None: | ||
72 | 68 | nova_output = nova_list() | ||
73 | 69 | |||
74 | 70 | services = {} | ||
75 | 71 | |||
76 | 72 | for line in io.StringIO(nova_output).readlines()[3:-1]: | ||
77 | 73 | unit_name = line.split('|')[2].strip() | ||
78 | 74 | service_name = '-'.join(unit_name.split('-')[1:-3]) | ||
79 | 75 | identifier = unit_name.split('-')[-3] | ||
80 | 76 | |||
81 | 77 | path = os.path.join(base_dir, identifier) | ||
82 | 78 | if not os.path.exists(path): | ||
83 | 79 | continue | ||
84 | 80 | |||
85 | 81 | service = services.setdefault(service_name, {}) | ||
86 | 82 | deployment = service.setdefault(identifier, {}) | ||
87 | 83 | units = deployment.setdefault('units', []) | ||
88 | 84 | units.append(unit_name) | ||
89 | 85 | |||
90 | 86 | if deployment.get('path') is None: | ||
91 | 87 | deployment.update({ | ||
92 | 88 | 'path': path, | ||
93 | 89 | 'epoch': os.stat(path)[stat.ST_MTIME], | ||
94 | 90 | }) | ||
95 | 91 | |||
96 | 92 | return services | ||
97 | 0 | 93 | ||
98 | === modified file 'ci_automation/tests/test_branch.py' | |||
99 | --- ci_automation/tests/test_branch.py 2015-04-02 19:29:27 +0000 | |||
100 | +++ ci_automation/tests/test_branch.py 2015-04-27 18:16:41 +0000 | |||
101 | @@ -99,13 +99,13 @@ | |||
102 | 99 | self.assertEqual( | 99 | self.assertEqual( |
103 | 100 | ('\n'.join(['bob/devel\t1', | 100 | ('\n'.join(['bob/devel\t1', |
104 | 101 | 'lp:foo;revno=1\t1']), | 101 | 'lp:foo;revno=1\t1']), |
106 | 102 | '90945bed4b6e27d500ae1ae05c7bba1c'), | 102 | '7ebe5386'), |
107 | 103 | get_identifier(self.branch, stage)) | 103 | get_identifier(self.branch, stage)) |
108 | 104 | self.branch_add_stage('the-builder/devel') | 104 | self.branch_add_stage('the-builder/devel') |
109 | 105 | self.assertEqual( | 105 | self.assertEqual( |
110 | 106 | ('\n'.join(['bob/devel\t1', | 106 | ('\n'.join(['bob/devel\t1', |
111 | 107 | 'lp:foo;revno=1\t1']), | 107 | 'lp:foo;revno=1\t1']), |
113 | 108 | '90945bed4b6e27d500ae1ae05c7bba1c'), | 108 | '7ebe5386'), |
114 | 109 | get_identifier(self.branch, stage)) | 109 | get_identifier(self.branch, stage)) |
115 | 110 | 110 | ||
116 | 111 | def test_get_identifier_spec_changes(self): | 111 | def test_get_identifier_spec_changes(self): |
117 | @@ -115,7 +115,7 @@ | |||
118 | 115 | self.assertEqual( | 115 | self.assertEqual( |
119 | 116 | ('\n'.join(['bob/devel\t1', | 116 | ('\n'.join(['bob/devel\t1', |
120 | 117 | 'lp:foo;revno=1\t1']), | 117 | 'lp:foo;revno=1\t1']), |
122 | 118 | '90945bed4b6e27d500ae1ae05c7bba1c'), | 118 | '7ebe5386'), |
123 | 119 | get_identifier(self.branch, stage)) | 119 | get_identifier(self.branch, stage)) |
124 | 120 | repo_path = os.path.join(self.branch, stage, 'repo') | 120 | repo_path = os.path.join(self.branch, stage, 'repo') |
125 | 121 | with open(repo_path, 'w'): | 121 | with open(repo_path, 'w'): |
126 | @@ -125,7 +125,7 @@ | |||
127 | 125 | self.assertEqual( | 125 | self.assertEqual( |
128 | 126 | ('\n'.join(['bob/devel\t2', | 126 | ('\n'.join(['bob/devel\t2', |
129 | 127 | 'lp:foo;revno=1\t1']), | 127 | 'lp:foo;revno=1\t1']), |
131 | 128 | 'dce1f2c4a1a1634f49a4572fa8630765'), | 128 | 'fd2e0853'), |
132 | 129 | get_identifier(self.branch, stage)) | 129 | get_identifier(self.branch, stage)) |
133 | 130 | 130 | ||
134 | 131 | def test_get_identifier_collect_changes(self): | 131 | def test_get_identifier_collect_changes(self): |
135 | @@ -136,7 +136,7 @@ | |||
136 | 136 | self.assertEqual( | 136 | self.assertEqual( |
137 | 137 | ('\n'.join(['bob/devel\t2', | 137 | ('\n'.join(['bob/devel\t2', |
138 | 138 | 'lp:foo;revno=7\t7']), | 138 | 'lp:foo;revno=7\t7']), |
140 | 139 | 'f4bdf7cb82d6f1a9fc725d1bf70eef75'), | 139 | '77ff4c59'), |
141 | 140 | get_identifier(self.branch, stage)) | 140 | get_identifier(self.branch, stage)) |
142 | 141 | self.branch_update_collect( | 141 | self.branch_update_collect( |
143 | 142 | stage, '\n'.join(['foo lp:~cprov/foo;revno=7', | 142 | stage, '\n'.join(['foo lp:~cprov/foo;revno=7', |
144 | @@ -145,7 +145,7 @@ | |||
145 | 145 | ('\n'.join(['bob/devel\t3', | 145 | ('\n'.join(['bob/devel\t3', |
146 | 146 | 'lp:~cprov/foo;revno=7\t7', | 146 | 'lp:~cprov/foo;revno=7\t7', |
147 | 147 | 'lp:bar;revno=5\t5']), | 147 | 'lp:bar;revno=5\t5']), |
149 | 148 | '261a6c824fa28680e7e0869194a12152'), | 148 | '7a1b03fa'), |
150 | 149 | get_identifier(self.branch, stage)) | 149 | get_identifier(self.branch, stage)) |
151 | 150 | 150 | ||
152 | 151 | 151 | ||
153 | 152 | 152 | ||
154 | === renamed file 'test_monitor.py' => 'ci_automation/tests/test_nova.py' | |||
155 | --- test_monitor.py 2015-04-08 17:59:46 +0000 | |||
156 | +++ ci_automation/tests/test_nova.py 2015-04-27 18:16:41 +0000 | |||
157 | @@ -1,93 +1,102 @@ | |||
159 | 1 | import logging | 1 | #!/usr/bin/env python3 |
160 | 2 | # | ||
161 | 3 | # Copyright (C) 2015 Canonical | ||
162 | 4 | # | ||
163 | 5 | # This program is free software: you can redistribute it and/or modify | ||
164 | 6 | # it under the terms of the GNU General Public License as published by | ||
165 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
166 | 8 | # (at your option) any later version. | ||
167 | 9 | # | ||
168 | 10 | # This program is distributed in the hope that it will be useful, | ||
169 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
170 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
171 | 13 | # GNU General Public License for more details. | ||
172 | 14 | # | ||
173 | 15 | # You should have received a copy of the GNU General Public License | ||
174 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
175 | 17 | # | ||
176 | 18 | |||
177 | 19 | """CI Automation tests for nova helpers.""" | ||
178 | 20 | |||
179 | 2 | import os | 21 | import os |
180 | 3 | import shutil | 22 | import shutil |
181 | 23 | import sys | ||
182 | 4 | import tempfile | 24 | import tempfile |
183 | 5 | import time | 25 | import time |
184 | 6 | import unittest | 26 | import unittest |
185 | 7 | 27 | ||
194 | 8 | from monitor import ( | 28 | |
195 | 9 | get_service_deployments, | 29 | HERE = os.path.abspath(os.path.dirname(__file__)) |
196 | 10 | get_service_info, | 30 | sys.path.append(os.path.join(HERE, '../..')) |
197 | 11 | parse_nova_output, | 31 | |
198 | 12 | ) | 32 | from ci_automation.nova import get_services_map |
199 | 13 | 33 | ||
200 | 14 | 34 | ||
201 | 15 | class TestServiceWatcher(unittest.TestCase): | 35 | class TestNova(unittest.TestCase): |
202 | 36 | |||
203 | 37 | identifiers = ( | ||
204 | 38 | '12345', | ||
205 | 39 | 'abcdef', | ||
206 | 40 | '56789', | ||
207 | 41 | ) | ||
208 | 42 | service_names = ( | ||
209 | 43 | 'mojo-ue-core-result-watcher', | ||
210 | 44 | 'mojo-ue-core-image-watcher', | ||
211 | 45 | 'mojo-ue-core-image-publisher', | ||
212 | 46 | 'mojo-ue-core-image-tester', | ||
213 | 47 | ) | ||
214 | 48 | |||
215 | 16 | def setUp(self): | 49 | def setUp(self): |
216 | 17 | self.prefix = 'juju-mojo-ue' | ||
217 | 18 | self.identifiers = [ | ||
218 | 19 | '12345', | ||
219 | 20 | 'abcdef', | ||
220 | 21 | '56789', | ||
221 | 22 | ] | ||
222 | 23 | self.service_names = [ | ||
223 | 24 | 'core-result-watcher', | ||
224 | 25 | 'core-image-watcher', | ||
225 | 26 | 'core-image-publisher', | ||
226 | 27 | 'core-image-tester', | ||
227 | 28 | ] | ||
228 | 29 | self.base_dir = tempfile.mkdtemp() | 50 | self.base_dir = tempfile.mkdtemp() |
229 | 30 | self.addCleanup(shutil.rmtree, self.base_dir) | 51 | self.addCleanup(shutil.rmtree, self.base_dir) |
233 | 31 | self.unit_name_pattern = '{}-(.*)-(.*)-machine-\d+'.format(self.prefix) | 52 | |
234 | 32 | 53 | def generate_identifiers(self): | |
235 | 33 | # generate some fake 'nova list' output | 54 | """Generate deployment identifiers on disk.""" |
236 | 55 | for ident in self.identifiers: | ||
237 | 56 | # get unique timestamps | ||
238 | 57 | time.sleep(.1) | ||
239 | 58 | os.makedirs(os.path.join(self.base_dir, ident)) | ||
240 | 59 | |||
241 | 60 | def create_output(self): | ||
242 | 61 | """Return some fake 'nova list' output.""" | ||
243 | 34 | count = 0 | 62 | count = 0 |
245 | 35 | self.output = "" | 63 | output = "" |
246 | 36 | for ident in self.identifiers: | 64 | for ident in self.identifiers: |
247 | 37 | count += 1 | 65 | count += 1 |
304 | 38 | 66 | for name in self.service_names: | |
305 | 39 | for service_name in self.service_names: | 67 | output += '| id-{} | juju-{}-{}-machine-0 |\n'.format( |
306 | 40 | self.output = '{}| id-{} | {}-{}-{}-machine-0 |\n'.format( | 68 | count, name, ident) |
307 | 41 | self.output, count, self.prefix, service_name, ident) | 69 | return output |
308 | 42 | 70 | ||
309 | 43 | #self._create_tmp_files() | 71 | def test_service_map_ignores_missing_identifiers(self): |
310 | 44 | 72 | # Deployments with no identifiers on disk are ignored (they were | |
311 | 45 | def _create_tmp_files(self): | 73 | # probably deployed manually with mojo.py, not with cd.py via cron). |
312 | 46 | for ident in self.identifiers: | 74 | service_map = get_services_map( |
313 | 47 | # get unique timestamps | 75 | self.base_dir, nova_output=self.create_output()) |
314 | 48 | time.sleep(2) | 76 | self.assertEqual({}, service_map) |
315 | 49 | os.makedirs(os.path.join(self.base_dir, ident)) | 77 | |
316 | 50 | 78 | def test_service_map(self): | |
317 | 51 | def test_parse_nova_output_valid(self): | 79 | self.generate_identifiers() |
318 | 52 | 80 | service_map = get_services_map( | |
319 | 53 | units = parse_nova_output(self.output, self.service_names[0]) | 81 | self.base_dir, nova_output=self.create_output()) |
320 | 54 | logging.warn("JOE: units: %s", units) | 82 | |
321 | 55 | 83 | # service_map is key-ed by services names. | |
322 | 56 | self.assertEqual(len(self.identifiers), len(units)) | 84 | self.assertEqual( |
323 | 57 | 85 | sorted(self.service_names), sorted(service_map.keys())) | |
324 | 58 | def test_get_service_info_valid(self): | 86 | |
325 | 59 | unit_name = '{}-{}-{}-machine-0'.format( | 87 | # Each service points to a dictionary key-ed by the deployment |
326 | 60 | self.prefix, | 88 | # identifier. |
327 | 61 | self.service_names[0], | 89 | service_contents = service_map['mojo-ue-core-image-watcher'] |
328 | 62 | self.identifiers[0], | 90 | self.assertEqual( |
329 | 63 | ) | 91 | ['56789', 'abcdef'], sorted(service_contents.keys())) |
330 | 64 | 92 | ||
331 | 65 | service_name, identifier = get_service_info(unit_name, | 93 | # Each deployment contains 'path' (identifier file on disk), 'epoch' |
332 | 66 | self.unit_name_pattern) | 94 | # (identifier file mtime) and 'units' (list of full unit names, |
333 | 67 | 95 | # including the bootstrap node). | |
334 | 68 | self.assertEqual(self.service_names[0], service_name) | 96 | deployment_contents = service_contents['abcdef'] |
335 | 69 | self.assertEqual(self.identifiers[0], identifier) | 97 | self.assertEqual( |
336 | 70 | 98 | ['epoch', 'path', 'units'], sorted(deployment_contents.keys())) | |
337 | 71 | def test_get_service_info_invalid(self): | 99 | |
338 | 72 | unit_name = '{}-{}-{}-machine-0'.format( | 100 | |
339 | 73 | self.prefix, | 101 | if __name__ == '__main__': |
340 | 74 | self.service_names[0], | 102 | unittest.main() |
285 | 75 | self.identifiers[0], | ||
286 | 76 | ) | ||
287 | 77 | |||
288 | 78 | service_name, identifier = get_service_info("junk-%s" % unit_name, | ||
289 | 79 | '') | ||
290 | 80 | |||
291 | 81 | self.assertEqual(None, service_name) | ||
292 | 82 | self.assertEqual(None, identifier) | ||
293 | 83 | |||
294 | 84 | def test_get_service_deployments(self): | ||
295 | 85 | service_deployments = get_service_deployments(self.service_names[0], | ||
296 | 86 | self.output, | ||
297 | 87 | self.unit_name_pattern) | ||
298 | 88 | |||
299 | 89 | self.assertEqual(1, len(service_deployments)) | ||
300 | 90 | for key in service_deployments: | ||
301 | 91 | self.assertEqual(len(self.identifiers), | ||
302 | 92 | len(service_deployments[key]), | ||
303 | 93 | "mismatch for '{}'".format(key)) | ||
341 | 94 | 103 | ||
342 | === modified file 'ci_automation/utils.py' | |||
343 | --- ci_automation/utils.py 2015-04-02 19:29:27 +0000 | |||
344 | +++ ci_automation/utils.py 2015-04-27 18:16:41 +0000 | |||
345 | @@ -16,6 +16,8 @@ | |||
346 | 16 | 16 | ||
347 | 17 | """CI Automation: miscelaneous utilities.""" | 17 | """CI Automation: miscelaneous utilities.""" |
348 | 18 | 18 | ||
349 | 19 | import logging | ||
350 | 20 | import os | ||
351 | 19 | import subprocess | 21 | import subprocess |
352 | 20 | import textwrap | 22 | import textwrap |
353 | 21 | 23 | ||
354 | @@ -61,3 +63,31 @@ | |||
355 | 61 | {}""" | 63 | {}""" |
356 | 62 | ''').format(stdout.decode(), stderr.decode()) | 64 | ''').format(stdout.decode(), stderr.decode()) |
357 | 63 | ) | 65 | ) |
358 | 66 | |||
359 | 67 | |||
360 | 68 | def setup_logger(): | ||
361 | 69 | """Return a configured logger instance. | ||
362 | 70 | |||
363 | 71 | Defaults to INFO level and if 'CI_LOGSTASH_HOST' environment variable | ||
364 | 72 | is set and python-logstash is reachable, adds a corresponding 'logstash' | ||
365 | 73 | handler. | ||
366 | 74 | """ | ||
367 | 75 | logging.basicConfig( | ||
368 | 76 | format='%(asctime)s %(name)s %(levelname)s: %(message)s') | ||
369 | 77 | logger = logging.getLogger() | ||
370 | 78 | logger.setLevel(logging.INFO) | ||
371 | 79 | |||
372 | 80 | # Optional remote logging via logstash. | ||
373 | 81 | logstash_host = os.environ.get('CI_LOGSTASH_HOST') | ||
374 | 82 | if logstash_host is not None: | ||
375 | 83 | try: | ||
376 | 84 | import logstash | ||
377 | 85 | except ImportError: | ||
378 | 86 | print('Follow the README instructions for installing ' | ||
379 | 87 | 'python-logstash') | ||
380 | 88 | return 1 | ||
381 | 89 | else: | ||
382 | 90 | logger.addHandler( | ||
383 | 91 | logstash.LogstashHandler(logstash_host, 5959, 1)) | ||
384 | 92 | |||
385 | 93 | return logger | ||
386 | 64 | 94 | ||
387 | === added file 'list.py' | |||
388 | --- list.py 1970-01-01 00:00:00 +0000 | |||
389 | +++ list.py 2015-04-27 18:16:41 +0000 | |||
390 | @@ -0,0 +1,56 @@ | |||
391 | 1 | #!/usr/bin/env python3 | ||
392 | 2 | # | ||
393 | 3 | # Copyright (C) 2015 Canonical | ||
394 | 4 | # | ||
395 | 5 | # This program is free software: you can redistribute it and/or modify | ||
396 | 6 | # it under the terms of the GNU General Public License as published by | ||
397 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
398 | 8 | # (at your option) any later version. | ||
399 | 9 | # | ||
400 | 10 | # This program is distributed in the hope that it will be useful, | ||
401 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
402 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
403 | 13 | # GNU General Public License for more details. | ||
404 | 14 | # | ||
405 | 15 | # You should have received a copy of the GNU General Public License | ||
406 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
407 | 17 | # | ||
408 | 18 | """CI Automation: script for listing auto-deployed services.""" | ||
409 | 19 | |||
410 | 20 | import argparse | ||
411 | 21 | import datetime | ||
412 | 22 | import os | ||
413 | 23 | import sys | ||
414 | 24 | |||
415 | 25 | |||
416 | 26 | from ci_automation.nova import get_services_map | ||
417 | 27 | from ci_automation.utils import setup_logger | ||
418 | 28 | |||
419 | 29 | |||
420 | 30 | def main(): | ||
421 | 31 | parser = argparse.ArgumentParser( | ||
422 | 32 | description="Check the number of deployments for a service") | ||
423 | 33 | parser.add_argument('--base-dir', '-b', | ||
424 | 34 | default=os.path.expanduser("~/ci-cd-identifiers"), | ||
425 | 35 | help="The directory where identifiers are stored " | ||
426 | 36 | " (eg. ~/ci-cd-identifiers)") | ||
427 | 37 | args = parser.parse_args() | ||
428 | 38 | logger = setup_logger() | ||
429 | 39 | |||
430 | 40 | services = get_services_map(args.base_dir) | ||
431 | 41 | sorted_services = sorted(services.items(), key=lambda s: s[0]) | ||
432 | 42 | for srv, deployments in sorted_services: | ||
433 | 43 | print('* {}:'.format(srv)) | ||
434 | 44 | sorted_deployments = sorted( | ||
435 | 45 | deployments.items(), key=lambda d: d[1]['epoch'], reverse=True) | ||
436 | 46 | for ident, contents in sorted_deployments: | ||
437 | 47 | date_created = datetime.datetime.utcfromtimestamp( | ||
438 | 48 | contents['epoch']).ctime() | ||
439 | 49 | print('\t{}: {} ({} units)'.format( | ||
440 | 50 | ident, date_created, len(contents['units']))) | ||
441 | 51 | |||
442 | 52 | return 0 | ||
443 | 53 | |||
444 | 54 | |||
445 | 55 | if __name__ == "__main__": | ||
446 | 56 | sys.exit(main()) | ||
447 | 0 | 57 | ||
448 | === modified file 'monitor.py' | |||
449 | --- monitor.py 2015-04-10 20:20:17 +0000 | |||
450 | +++ monitor.py 2015-04-27 18:16:41 +0000 | |||
451 | @@ -15,155 +15,51 @@ | |||
452 | 15 | # You should have received a copy of the GNU General Public License | 15 | # You should have received a copy of the GNU General Public License |
453 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
454 | 17 | # | 17 | # |
455 | 18 | """CI Automation: monitoring (purging obsolete deployments) services.""" | ||
456 | 18 | 19 | ||
457 | 19 | import argparse | 20 | import argparse |
458 | 20 | import subprocess | ||
459 | 21 | import io | ||
460 | 22 | import logging | ||
461 | 23 | import operator | ||
462 | 24 | import os | 21 | import os |
463 | 25 | import re | ||
464 | 26 | import sys | 22 | import sys |
465 | 27 | 23 | ||
481 | 28 | from stat import ST_MTIME | 24 | |
482 | 29 | 25 | from ci_automation.nova import get_services_map | |
483 | 30 | 26 | from ci_automation.utils import setup_logger | |
484 | 31 | def parse_args(): | 27 | |
485 | 32 | desc = "Check the number of deployments for a service" | 28 | |
486 | 33 | parser = argparse.ArgumentParser(description=desc) | 29 | def main(): |
487 | 34 | parser.add_argument('--base-dir', '-b', | 30 | parser = argparse.ArgumentParser( |
488 | 35 | default=os.path.expanduser("~/ci-cd-identifiers"), | 31 | description="Check the number of deployments for a service") |
489 | 36 | help="The directory where identifiers are stored " | 32 | parser.add_argument( |
490 | 37 | " (eg. ~/ci-cd-identifiers)") | 33 | '--base-dir', '-b', |
491 | 38 | parser.add_argument('--max-deployments', '-m', type=int, default=2, | 34 | default=os.path.expanduser("~/ci-cd-identifiers"), |
492 | 39 | help="The maximum number of deployments to keep.") | 35 | help="The directory where identifiers are stored " |
493 | 40 | parser.add_argument('stage_dirname', | 36 | " (eg. ~/ci-cd-identifiers)") |
494 | 41 | help="the mojo stage directory name (eg. " | 37 | parser.add_argument( |
495 | 42 | "mojo-ue-core-image-tester") | 38 | '--max-deployments', '-m', type=int, default=2, |
496 | 39 | help="The maximum number of deployments to keep.") | ||
497 | 40 | parser.add_argument( | ||
498 | 41 | 'stage_dirname', | ||
499 | 42 | help="the mojo stage directory name (eg. mojo-ue-core-image-tester") | ||
500 | 43 | 43 | ||
501 | 44 | args = parser.parse_args() | 44 | args = parser.parse_args() |
624 | 45 | 45 | logger = setup_logger() | |
625 | 46 | return args | 46 | |
626 | 47 | 47 | deployments = get_services_map(args.base_dir).get(args.stage_dirname) | |
627 | 48 | 48 | if deployments is None: | |
628 | 49 | def get_service_info(unit_name, unit_name_pattern): | 49 | print('No deployments found!') |
629 | 50 | 50 | return | |
630 | 51 | unit_name_regex = re.compile(unit_name_pattern) | 51 | |
631 | 52 | match = unit_name_regex.match(unit_name) | 52 | sorted_deployments = sorted( |
632 | 53 | stage_dirname = None | 53 | deployments.items(), key=lambda d: d[1]['epoch'], reverse=True) |
633 | 54 | identifier = None | 54 | for identifier, _ in sorted_deployments[args.max_deployments:]: |
634 | 55 | 55 | extra = { | |
635 | 56 | if match and len(match.groups()) == 2: | 56 | "service": "ci/cd", |
636 | 57 | stage_dirname = match.group(1) | 57 | "deployment_status": "DESTROYED", |
637 | 58 | identifier = match.group(2) | 58 | "deployment_name": args.stage_dirname, |
638 | 59 | 59 | "deployment_identifier": identifier, | |
639 | 60 | return (stage_dirname, identifier) | 60 | } |
640 | 61 | 61 | logger.info( | |
641 | 62 | 62 | "Destroying deployment %s", identifier, extra=extra) | |
520 | 63 | def nova_list(): | ||
521 | 64 | args = [ | ||
522 | 65 | 'nova', | ||
523 | 66 | 'list', | ||
524 | 67 | '--minimal', | ||
525 | 68 | ] | ||
526 | 69 | |||
527 | 70 | output = subprocess.check_output( | ||
528 | 71 | args, stderr=subprocess.DEVNULL).decode('utf-8').strip() | ||
529 | 72 | |||
530 | 73 | return output | ||
531 | 74 | |||
532 | 75 | |||
533 | 76 | def parse_nova_output(output, stage_dirname): | ||
534 | 77 | units = [] | ||
535 | 78 | buf = io.StringIO(output) | ||
536 | 79 | for line in buf.readlines(): | ||
537 | 80 | # skip everything but the instance entries | ||
538 | 81 | if stage_dirname not in line: | ||
539 | 82 | continue | ||
540 | 83 | data = line.split('|') | ||
541 | 84 | instance_id = data[1].strip() | ||
542 | 85 | unit_name = data[2].strip() | ||
543 | 86 | units.append({'id': instance_id, 'name': unit_name}) | ||
544 | 87 | |||
545 | 88 | return units | ||
546 | 89 | |||
547 | 90 | |||
548 | 91 | def check_deployments(service_deployments, max_deployments, base_dir, logger): | ||
549 | 92 | """ | ||
550 | 93 | :param service_deployments: dictionary of the form: | ||
551 | 94 | {'stage_dirname': ['identifier_1', 'identifier_2', ...]} | ||
552 | 95 | """ | ||
553 | 96 | for key in service_deployments: | ||
554 | 97 | if len(service_deployments[key]) > max_deployments: | ||
555 | 98 | data = [] | ||
556 | 99 | for identifier in service_deployments[key]: | ||
557 | 100 | path = os.path.join(base_dir, identifier) | ||
558 | 101 | # Manually deployed environments (i.e. no identifiers | ||
559 | 102 | # recorded on disk) are ignored and left alone. | ||
560 | 103 | if not os.path.exists(path): | ||
561 | 104 | continue | ||
562 | 105 | data.append((identifier, os.stat(path)[ST_MTIME], path)) | ||
563 | 106 | data = sorted(data, key=operator.itemgetter(1), reverse=True) | ||
564 | 107 | for identifier, _, _ in data[max_deployments:]: | ||
565 | 108 | extra = { | ||
566 | 109 | "service": "ci/cd", | ||
567 | 110 | "deployment_status": "DESTROYED", | ||
568 | 111 | "deployment_name": key, | ||
569 | 112 | "deployment_identifier": identifier, | ||
570 | 113 | } | ||
571 | 114 | logger.info( | ||
572 | 115 | "Destroying deployment %s", identifier, extra=extra) | ||
573 | 116 | |||
574 | 117 | |||
575 | 118 | def get_service_deployments(stage_dirname, output, unit_name_pattern): | ||
576 | 119 | service_deployments = {stage_dirname: []} | ||
577 | 120 | |||
578 | 121 | units = parse_nova_output(output, stage_dirname) | ||
579 | 122 | |||
580 | 123 | for unit in units: | ||
581 | 124 | service_name, identifier = get_service_info(unit['name'], | ||
582 | 125 | unit_name_pattern) | ||
583 | 126 | # skip unparsable unit names | ||
584 | 127 | if service_name is None or identifier is None: | ||
585 | 128 | continue | ||
586 | 129 | |||
587 | 130 | service_deployments[stage_dirname].append(identifier) | ||
588 | 131 | |||
589 | 132 | return service_deployments | ||
590 | 133 | |||
591 | 134 | |||
592 | 135 | def main(): | ||
593 | 136 | args = parse_args() | ||
594 | 137 | |||
595 | 138 | base_dir = args.base_dir | ||
596 | 139 | max_deployments = args.max_deployments | ||
597 | 140 | stage_dirname = args.stage_dirname | ||
598 | 141 | |||
599 | 142 | unit_name_pattern = 'juju-({})-(.*)-machine-\d+'.format(stage_dirname) | ||
600 | 143 | |||
601 | 144 | logging.basicConfig( | ||
602 | 145 | format='%(asctime)s %(name)s %(levelname)s: %(message)s') | ||
603 | 146 | logger = logging.getLogger() | ||
604 | 147 | logger.setLevel(logging.INFO) | ||
605 | 148 | |||
606 | 149 | # Optional remote logging via logstash. | ||
607 | 150 | logstash_host = os.environ.get('CI_LOGSTASH_HOST') | ||
608 | 151 | if logstash_host is not None: | ||
609 | 152 | try: | ||
610 | 153 | import logstash | ||
611 | 154 | except ImportError: | ||
612 | 155 | print('Follow the README instructions for installing ' | ||
613 | 156 | 'python-logstash') | ||
614 | 157 | return 1 | ||
615 | 158 | else: | ||
616 | 159 | logger.addHandler( | ||
617 | 160 | logstash.LogstashHandler(logstash_host, 5959, 1)) | ||
618 | 161 | |||
619 | 162 | output = nova_list() | ||
620 | 163 | service_deployments = get_service_deployments( | ||
621 | 164 | stage_dirname, output, unit_name_pattern) | ||
622 | 165 | check_deployments( | ||
623 | 166 | service_deployments, max_deployments, base_dir, logger) | ||
642 | 167 | 63 | ||
643 | 168 | return 0 | 64 | return 0 |
644 | 169 | 65 |
Nice, this will be really useful