Merge lp:~whitmo/charms/trusty/cloudfoundry/cf-disco-check into lp:~cf-charmers/charms/trusty/cloudfoundry/trunk

Proposed by Whit Morriss
Status: Needs review
Proposed branch: lp:~whitmo/charms/trusty/cloudfoundry/cf-disco-check
Merge into: lp:~cf-charmers/charms/trusty/cloudfoundry/trunk
Diff against target: 471 lines (+230/-22)
9 files modified
cfdeploy (+3/-1)
cloudfoundry/health_checks.py (+11/-0)
cloudfoundry/jobs.py (+8/-1)
cloudfoundry/releases.py (+4/-1)
cloudfoundry/services.py (+13/-4)
cloudfoundry/tasks.py (+80/-5)
cloudfoundry/utils.py (+14/-7)
reconciler/app.py (+6/-3)
tests/test_tasks.py (+91/-0)
To merge this branch: bzr merge lp:~whitmo/charms/trusty/cloudfoundry/cf-disco-check
Reviewer Review Type Date Requested Status
Cory Johns feedback Pending
Review via email: mp+244644@code.launchpad.net

Description of the change

Basic implementation of health check using healthz & varz endpoint. Only applied to DEA node currently.

To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) :
184. By Whit Morriss

fix typo

185. By Whit Morriss

fix tests for chmod

186. By Whit Morriss

fix path for disco install

187. By Whit Morriss

merge

Unmerged revisions

187. By Whit Morriss

merge

186. By Whit Morriss

fix path for disco install

185. By Whit Morriss

fix tests for chmod

184. By Whit Morriss

fix typo

183. By Whit Morriss

make disco executable

182. By Whit Morriss

move disco install to data_ready

181. By Whit Morriss

clean up chaining

180. By Whit Morriss

refactor config to not include component type. other tweaks

179. By Whit Morriss

initial declaration of healthz for DEA

178. By Whit Morriss

extend to use job level health check definitions (untested)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cfdeploy'
2--- cfdeploy 2014-12-03 23:14:23 +0000
3+++ cfdeploy 2014-12-17 16:51:23 +0000
4@@ -19,7 +19,7 @@
5
6
7 def install_python_deps():
8- print "Setting up virutalenv."
9+ print "Setting up virtualenv."
10 subprocess.check_output(['pip', 'install', 'wheel'])
11 for dep in ['progress', 'requests', 'jujuclient']:
12 subprocess.check_output(['pip', 'install',
13@@ -77,6 +77,7 @@
14 parser.add_argument('-l', '--log', action='store_true',
15 help='Write debug log to cfdeploy.log')
16 parser.add_argument('-c', '--constraints')
17+ parser.add_argument('--to')
18 parser.add_argument('-g', '--generate', action='store_true')
19 parser.add_argument('admin_password')
20
21@@ -152,6 +153,7 @@
22 bar.next(message='Deploying Orchestrator')
23 wait_for(30, 5, partial(deploy,
24 constraints=options.constraints,
25+ to=options.to,
26 generate_dependents=options.generate,
27 admin_password=options.admin_password))
28 until(lambda: socket_open(reconciler_endpoint(), 8888),
29
30=== modified file 'cloudfoundry/health_checks.py'
31--- cloudfoundry/health_checks.py 2014-11-20 15:50:36 +0000
32+++ cloudfoundry/health_checks.py 2014-12-17 16:51:23 +0000
33@@ -36,3 +36,14 @@
34 return result
35 else:
36 return dict(result, health='warn', message='Working (%s)' % status['status'])
37+
38+
39+def healthz_check(ctype, service, healthz=tasks.healthz, varz=tasks.varz):
40+ health = healthz(ctype) and 'pass' or 'fail'
41+ result = {
42+ 'name': 'healthz',
43+ 'health': health,
44+ 'message': None,
45+ 'data': varz(ctype),
46+ }
47+ return result
48
49=== modified file 'cloudfoundry/jobs.py'
50--- cloudfoundry/jobs.py 2014-11-20 15:50:36 +0000
51+++ cloudfoundry/jobs.py 2014-12-17 16:51:23 +0000
52@@ -2,6 +2,7 @@
53 from functools import partial
54 from .path import path
55 import yaml
56+import itertools
57
58 from charmhelpers.core import hookenv
59 from charmhelpers.core import services
60@@ -68,7 +69,13 @@
61 service_def = service_data[charm_name]
62 results = []
63 health = 'pass'
64- checks = service_def.get('health', []) + [health_checks.status]
65+ job_health = [job['health'] for job in service_def['jobs'] \
66+ if 'health' in job]
67+ base_checks = service_def.get('health', [])
68+ checks = itertools.chain(base_checks,
69+ itertools.chain(*job_health),
70+ [health_checks.status])
71+
72 for health_check in checks:
73 result = health_check(service_def)
74 if result['health'] == 'fail':
75
76=== modified file 'cloudfoundry/releases.py'
77--- cloudfoundry/releases.py 2014-12-08 19:48:50 +0000
78+++ cloudfoundry/releases.py 2014-12-17 16:51:23 +0000
79@@ -26,9 +26,12 @@
80 ('etcd:client', 'loggregator-trafficcontrol:etcd'),
81 ]
82
83+UTILITIES = {
84+ "cf-disco": "cf-disco-v0.0.1-538768711c7545d87e60ad79ec3568d8c77c7db6"
85+}
86+
87 COMMON_UPGRADES = []
88
89-
90 RELEASES = [
91 {
92 "releases": (190, 190),
93
94=== modified file 'cloudfoundry/services.py'
95--- cloudfoundry/services.py 2014-12-08 23:20:58 +0000
96+++ cloudfoundry/services.py 2014-12-17 16:51:23 +0000
97@@ -3,6 +3,10 @@
98 import tasks
99 import utils
100
101+from functools import partial
102+from .health_checks import healthz_check
103+
104+
105 __all__ = ['SERVICES']
106
107
108@@ -264,7 +268,6 @@
109
110 },
111
112-
113 'dea-v3': {
114 'summary': 'DEA runs CF apps in containers',
115 'description': '',
116@@ -285,9 +288,12 @@
117 contexts.RouterRelation,
118 ],
119 'data_ready': [
120- #tasks.enable_swapaccounting
121+ tasks.install_cfdisco,
122+ tasks.config_cfdisco,
123+ # tasks.enable_swapaccounting,
124 tasks.patch_dea
125- ]
126+ ],
127+ 'health': [partial(healthz_check, "DEA")]
128 },
129 {
130 'job_name': 'dea_logging_agent',
131@@ -297,10 +303,13 @@
132 contexts.EtcdRelation]
133 },
134 {'job_name': 'metron_agent',
135+ 'data_ready': [tasks.install_cfdisco,
136+ tasks.config_cfdisco],
137 'required_data': [contexts.LTCRelation,
138 contexts.NatsRelation,
139 contexts.DopplerRelation,
140- contexts.EtcdRelation]},
141+ contexts.EtcdRelation],
142+ 'health': [partial(healthz_check, "MetronAgent")]},
143 ]
144
145 },
146
147=== modified file 'cloudfoundry/tasks.py'
148--- cloudfoundry/tasks.py 2014-10-03 15:47:17 +0000
149+++ cloudfoundry/tasks.py 2014-12-17 16:51:23 +0000
150@@ -1,12 +1,14 @@
151+import json
152+import logging
153 import os
154 import re
155+import shlex
156 import shutil
157+import stat
158 import subprocess
159-import yaml
160-import stat
161 import tempfile
162 import textwrap
163-import logging
164+import yaml
165 from urlparse import urlparse
166 from functools import partial
167 from charmhelpers.core import host
168@@ -16,6 +18,7 @@
169 from cloudfoundry import contexts
170 from cloudfoundry import templating
171 from cloudfoundry import utils
172+from .releases import UTILITIES
173 from .path import path
174
175 logger = logging.getLogger(__name__)
176@@ -41,7 +44,8 @@
177 enable_monit_http_interface()
178 subprocess.check_call(['gem', 'install', '--no-ri', '--no-rdoc', gem_file])
179 subprocess.check_call([
180- 'pip', 'install', '--use-wheel', '-f', './wheelhouse', '--pre', 'raindance'])
181+ 'pip', 'install', '--use-wheel', '-f', './wheelhouse', '--pre',
182+ 'raindance'])
183
184
185 def install(service_def):
186@@ -99,7 +103,8 @@
187 return
188 from raindance.package import PackageArchive
189 pa = PackageArchive(url)
190- mirror = pa.build_mirror_section(ARTIFACTS_DIR, SOFTWARE, [(version, ARCH)], [job_name])
191+ mirror = pa.build_mirror_section(ARTIFACTS_DIR,
192+ SOFTWARE, [(version, ARCH)], [job_name])
193 for filename in mirror:
194 pass # just need to iterate to force the (lazy) download
195
196@@ -203,6 +208,76 @@
197 subprocess.check_call(['patch', '-s', '-F4'], stdin=patch)
198 os.unlink(fn)
199
200+JUJU_VCAP_BIN = path("/var/vcap/juju/bin")
201+
202+
203+def install_cfdisco(job_name, jujubin=JUJU_VCAP_BIN,
204+ discov=UTILITIES['cf-disco']):
205+ orch = contexts.OrchestratorRelation()
206+
207+ urlbase = path(orch.get_first('artifacts_url'))
208+ url = urlbase / ('cf/utilities/%s' % discov)
209+ sha1 = discov.rsplit("-", 1)[1]
210+
211+ outfile = jujubin / 'cf-disco'
212+
213+ from raindance.package import PackageArchive
214+ verify = PackageArchive.verify_file
215+
216+ if not (outfile.exists() and verify(outfile, sha1)):
217+ jujubin.makedirs_p()
218+ PackageArchive.wget(url, outfile)
219+ assert PackageArchive.verify_file(outfile, sha1), "Bad sha1 for %s" % outfile
220+ outfile.chmod(755)
221+
222+
223+JUJU_VCAP_ETC = path("/var/vcap/juju/etc")
224+
225+
226+def config_cfdisco(job_name, jujuetc=JUJU_VCAP_ETC):
227+ disco_conf = jujuetc / 'disco.json'
228+ if not disco_conf.exists():
229+ nats = contexts.NatsRelation()
230+ nats_info = nats.erb_mapping()
231+ nats_info = dict((k.replace('nats.', ''), v) for k, v in nats_info.items())
232+ nats_info['addr'] = nats.get_first('address')
233+
234+ # @@ fix to use multiple nats servers
235+ nats_uri_tmpt = "nats://{user}:{password}@{addr}:{port}"
236+ nats_uri = nats_uri_tmpt.format(**nats_info)
237+ privip = hookenv.unit_private_ip()
238+
239+ jujuetc.makedirs_p()
240+ disco_conf.write_text(json.dumps(dict(nats_uri=nats_uri,
241+ ip=privip)))
242+
243+
244+def healthz(ctype, exe=JUJU_VCAP_BIN / 'cf-disco',
245+ config=JUJU_VCAP_ETC / 'disco.json'):
246+ conf = json.loads(config.text())
247+ conf['type'] = ctype
248+ tmplt = "{exe} {type} {ip} {nats_uri}"
249+ conf['exe'] = exe
250+ args = shlex.split(tmplt.format(**conf))
251+ try:
252+ subprocess.check_output(args)
253+ return True
254+ except subprocess.CalledProcessError as e:
255+ if e.returncode == 1:
256+ return False
257+ raise
258+
259+
260+def varz(ctype, exe=JUJU_VCAP_BIN / 'cf-disco',
261+ config=JUJU_VCAP_ETC / 'disco.json'):
262+ conf = json.loads(config.text())
263+ conf['type'] = ctype
264+ tmplt = "{exe} --check='varz' {type} {ip} {nats_uri}"
265+ conf['exe'] = exe
266+ args = shlex.split(tmplt.format(**conf))
267+ out = subprocess.check_output(args)
268+ return json.loads(out)
269+
270
271 class JobTemplates(services.ManagerCallback):
272 template_base_dir = TEMPLATES_BASE_DIR
273
274=== modified file 'cloudfoundry/utils.py'
275--- cloudfoundry/utils.py 2014-12-09 22:56:46 +0000
276+++ cloudfoundry/utils.py 2014-12-17 16:51:23 +0000
277@@ -171,6 +171,7 @@
278 def command(*base_args, **kwargs):
279 check = kwargs.pop('check', False)
280 throw = kwargs.pop('throw', True)
281+ combine_output = kwargs.pop('combine_output', False)
282
283 def callable_command(*args, **kws):
284 kwargs.update(kws)
285@@ -182,17 +183,18 @@
286
287 p = subprocess.Popen(all_args,
288 stdout=subprocess.PIPE,
289- stderr=subprocess.STDOUT,
290+ stderr=subprocess.PIPE,
291 **kwargs)
292- output, _ = p.communicate()
293+ output, outerr = p.communicate()
294 ret_code = p.poll()
295- logging.debug('output: %s', output)
296- if check is True:
297- if ret_code != 0 and throw:
298- raise subprocess.CalledProcessError(ret_code, all_args, output=output)
299+ if check and throw and ret_code != 0:
300+ raise subprocess.CalledProcessError(ret_code, all_args, output=output)
301+ elif check and not throw:
302 return ret_code
303+ elif combine_output:
304+ return (output + outerr).strip()
305 else:
306- return output.strip()
307+ return map(str.strip, (output, outerr))
308 return callable_command
309
310
311@@ -407,6 +409,7 @@
312 def deploy(**config):
313 status = get_client().status()
314 constraints = config.pop('constraints', None)
315+ to = config.pop('to', None)
316 if 'cloudfoundry' in status['Services']:
317 return True
318 # create an up to date config
319@@ -427,6 +430,10 @@
320 '--repository=%s' % repo_path]
321 if constraints:
322 args.append('--constraints=%s' % constraints)
323+
324+ if to:
325+ args.append('--to=%s' % to)
326+
327 args.append('local:trusty/cloudfoundry')
328 juju = sh.check('juju', throw=False)
329 if juju(*args) != 0:
330
331=== modified file 'reconciler/app.py'
332--- reconciler/app.py 2014-12-08 19:38:24 +0000
333+++ reconciler/app.py 2014-12-17 16:51:23 +0000
334@@ -102,7 +102,7 @@
335 try:
336 unit_dash = unit_name.replace('/', '-')
337 key_file = path(__file__).dirname() / '..' / 'orchestrator-key'
338- output = subprocess.check_output([
339+ output, outerr = utils.command(
340 'ssh', 'root@{}'.format(unit_addr),
341 '-i', key_file,
342 '-o', 'UserKnownHostsFile=/dev/null',
343@@ -112,12 +112,15 @@
344 'cd $CHARM_DIR',
345 'hooks/health',
346 ]),
347- ], stderr=subprocess.STDOUT)
348+ combine_output=False, check=True)()
349 try:
350 unit.update(yaml.safe_load(output))
351 except (yaml.error.YAMLError, ValueError):
352 unit['health'] = 'fail'
353- unit['state'] = {'message': 'Unable to parse health: {}'.format(output)}
354+ unit['state'] = {
355+ 'message': 'Unable to parse health: '
356+ 'stdout: {}'
357+ 'stderr: {}'.format(output, outerr)}
358 except subprocess.CalledProcessError as e:
359 unit['state'] = {'message': 'Unable to retrieve health: {}'.format(e.output)}
360 if unit_state == 'started':
361
362=== modified file 'tests/test_tasks.py'
363--- tests/test_tasks.py 2014-10-03 15:47:17 +0000
364+++ tests/test_tasks.py 2014-12-17 16:51:23 +0000
365@@ -1,11 +1,20 @@
366 import unittest
367 import mock
368+import contextlib
369+import tempfile
370+import json
371
372+from subprocess import CalledProcessError
373 from charmhelpers.core import services
374 from cloudfoundry.path import path
375 from cloudfoundry import tasks
376
377
378+def patch_set(*methods):
379+ patches = [mock.patch(x) for x in methods]
380+ return contextlib.nested(*patches)
381+
382+
383 class TestTasks(unittest.TestCase):
384 def setUp(self):
385 self.charm_dir_patch = mock.patch(
386@@ -161,3 +170,85 @@
387 output = tasks._enable_swapaccounting(sample)
388 self.assertIn("arg1 cgroup_enable=memory swapaccount=1", output)
389 self.assertIn("recovery arg2 cgroup_enable=memory swapaccount=1", output)
390+
391+ def test_cfdisco_install(self):
392+ patches = ['cloudfoundry.contexts.OrchestratorRelation',
393+ 'raindance.package.PackageArchive.wget',
394+ 'raindance.package.PackageArchive.verify_file',
395+ 'cloudfoundry.path.path.chmod']
396+
397+ bin_d = path(tempfile.mkdtemp(prefix='cf-tests-'))
398+ with patch_set(*patches) as (om, wm, vm, chm):
399+ om().get_first.return_value = 'http://pkg/url'
400+ vm.return_value = True
401+ tasks.install_cfdisco("some_job",
402+ jujubin=bin_d,
403+ discov="bin-version")
404+ assert wm.called
405+ expect = mock.call(path(u'http://pkg/url/utilities/bin-version'),
406+ bin_d / 'cf-disco')
407+ assert wm.call_args == expect
408+ assert vm.called
409+
410+ def test_config_cfdisco(self):
411+ patches = ('cloudfoundry.contexts.NatsRelation',
412+ 'charmhelpers.core.hookenv.unit_private_ip')
413+ with patch_set(*patches) as (cxt, ipm):
414+ cxt().get_first.return_value = "10.0.0.1"
415+ cxt().erb_mapping.return_value = {
416+ 'nats.user': 'user',
417+ 'nats.password': 'pw',
418+ 'nats.port': "1234"
419+ }
420+ local_ip = ipm.return_value = "10.0.0.10"
421+ etc = path(tempfile.mkdtemp(prefix='cf-tests-'))
422+ result = etc / 'disco.json'
423+ tasks.config_cfdisco("JOB", etc)
424+ assert result.exists()
425+ outval = json.loads(result.text())
426+ assert outval['ip'] == local_ip
427+ assert outval['nats_uri'] == 'nats://user:pw@10.0.0.1:1234'
428+
429+ def test_check_healthz_ok(self):
430+ config = path(tempfile.mkstemp()[1])
431+ conf_txt = json.dumps(dict(type='COMP',
432+ ip='10.0.0.1',
433+ nats_uri='NATS_URI'))
434+ config.write_text(conf_txt)
435+ with mock.patch('subprocess.check_call') as cm:
436+ cm.return_value = 0
437+ assert tasks.healthz('CF-THANG', exe='cd', config=config) is True
438+
439+ def test_check_healthz_not_ok(self):
440+ config = path(tempfile.mkstemp()[1])
441+ conf_txt = json.dumps(dict(type='COMP',
442+ ip='10.0.0.1',
443+ nats_uri='NATS_URI'))
444+
445+ config.write_text(conf_txt)
446+ with mock.patch('subprocess.check_call') as cm:
447+ cm.side_effect = CalledProcessError(1, "CMD")
448+ assert tasks.healthz('CF-THANG', exe='cd', config=config) is False
449+
450+ def test_healthz_raises(self):
451+ config = path(tempfile.mkstemp()[1])
452+ conf_txt = json.dumps(dict(type='COMP',
453+ ip='10.0.0.1',
454+ nats_uri='NATS_URI'))
455+ config.write_text(conf_txt)
456+ with mock.patch('subprocess.check_call') as cm:
457+ cm.side_effect = CalledProcessError(2, "CMD")
458+ with self.assertRaises(CalledProcessError):
459+ tasks.healthz('CF-THANG', exe='cd', config=config)
460+
461+ def test_check_varz(self):
462+ config = path(tempfile.mkstemp()[1])
463+ conf_txt = json.dumps(dict(type='COMP',
464+ ip='10.0.0.1',
465+ nats_uri='NATS_URI'))
466+
467+ config.write_text(conf_txt)
468+ with mock.patch('subprocess.check_output') as co:
469+ co.return_value = "{}"
470+ out = tasks.varz('CF-THANG', "exe", config)
471+ assert out == {}

Subscribers

People subscribed via source and target branches