Merge lp:~ev/ubuntu-ci-services-itself/better-structure-and-logging-part2 into lp:ubuntu-ci-services-itself

Proposed by Evan
Status: Work in progress
Proposed branch: lp:~ev/ubuntu-ci-services-itself/better-structure-and-logging-part2
Merge into: lp:ubuntu-ci-services-itself
Prerequisite: lp:~ev/ubuntu-ci-services-itself/better-structure-and-logging
Diff against target: 485 lines (+202/-93) (has conflicts)
8 files modified
branch-source-builder/bsbuilder/run_worker.py (+2/-4)
charms/precise/rabbitmq-worker/config.yaml (+2/-2)
charms/precise/rabbitmq-worker/hooks/hooks.py (+125/-78)
charms/precise/rabbitmq-worker/hooks/test_hooks.py (+34/-0)
ci-utils/ci_utils/logger.py (+34/-0)
juju-deployer/image-builder.yaml.tmpl (+1/-1)
juju-deployer/test-runner.yaml.tmpl (+1/-1)
test_runner/tstrun/run_worker.py (+3/-7)
Text conflict in charms/precise/rabbitmq-worker/hooks/hooks.py
To merge this branch: bzr merge lp:~ev/ubuntu-ci-services-itself/better-structure-and-logging-part2
Reviewer Review Type Date Requested Status
Canonical CI Engineering Pending
Review via email: mp+210064@code.launchpad.net

Description of the change

WIP.

To post a comment you must log in.
314. By Evan

Fix references to uid.

315. By Evan

Fix reference to user and group on config object.

316. By Evan

Fix tarball extraction code and set dir permissions appropriately.

317. By Evan

Create the config dir. Support upgrading the charm by reinstalling it. Log the error when the charm fails.

318. By Evan

Kill globals.

319. By Evan

Add a test for create_dirs in the rabbitmq-worker charm.

320. By Evan

Oops. Don't always return non-zero.

321. By Evan

Fix writing of upstart job and add a test for it.

322. By Evan

Don't collide on names with the logging module.

323. By Evan

Export, not env. The former ensures the variable gets passed through to run_worker.

Unmerged revisions

323. By Evan

Export, not env. The former ensures the variable gets passed through to run_worker.

322. By Evan

Don't collide on names with the logging module.

321. By Evan

Fix writing of upstart job and add a test for it.

320. By Evan

Oops. Don't always return non-zero.

319. By Evan

Add a test for create_dirs in the rabbitmq-worker charm.

318. By Evan

Kill globals.

317. By Evan

Create the config dir. Support upgrading the charm by reinstalling it. Log the error when the charm fails.

316. By Evan

Fix tarball extraction code and set dir permissions appropriately.

315. By Evan

Fix reference to user and group on config object.

314. By Evan

Fix references to uid.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'branch-source-builder/bsbuilder/run_worker.py'
2--- branch-source-builder/bsbuilder/run_worker.py 2014-03-06 05:57:13 +0000
3+++ branch-source-builder/bsbuilder/run_worker.py 2014-03-09 17:04:23 +0000
4@@ -15,16 +15,14 @@
5 # along with this program. If not, see <http://www.gnu.org/licenses/>.
6
7 import json
8-import logging
9 import time
10
11 import upload_package
12 import watch_ppa
13
14-from ci_utils import amqp_utils, dump_stack
15+from ci_utils import (amqp_utils, dump_stack, logger)
16
17-logging.basicConfig(level=logging.INFO)
18-log = logging.getLogger(__name__)
19+log = logger.setup_logging('bsb-worker')
20
21 TIME_BETWEEN_CHECKS = 60
22
23
24=== modified file 'charms/precise/rabbitmq-worker/config.yaml'
25--- charms/precise/rabbitmq-worker/config.yaml 2014-02-25 22:29:04 +0000
26+++ charms/precise/rabbitmq-worker/config.yaml 2014-03-09 17:04:23 +0000
27@@ -33,11 +33,11 @@
28 <install_root>/<unit>/unit_config
29 where <unit> is the location the branch is extracted to.
30
31- uid:
32+ user:
33 type: string
34 default: nobody
35 description: User to run service as
36- gid:
37+ group:
38 type: string
39 default: nogroup
40 description: Group to run service as
41
42=== modified file 'charms/precise/rabbitmq-worker/hooks/hooks.py'
43--- charms/precise/rabbitmq-worker/hooks/hooks.py 2014-03-07 20:45:24 +0000
44+++ charms/precise/rabbitmq-worker/hooks/hooks.py 2014-03-09 17:04:23 +0000
45@@ -2,86 +2,90 @@
46
47 import base64
48 import os
49-import json
50 import shutil
51 import subprocess
52 import sys
53 import tempfile
54 import textwrap
55+import tempfile
56 import charmhelpers.fetch
57-
58-
59-def juju_info(msg):
60- subprocess.check_call(['juju-log', '-l', 'INFO', msg])
61- pass
62-
63-
64-def _config():
65- output = subprocess.check_output(['config-get', '--format=json'])
66- return json.loads(output)
67-
68-
69-def _relation_get(key):
70- return subprocess.check_output(['relation-get', key]).strip()
71-
72-
73-def _relation_set(keyvalues, relation_id=None):
74- args = ['relation-set']
75- if relation_id:
76- args.extend(['-r', relation_id])
77- args.extend(["{}={}".format(k, v or '') for k, v in keyvalues.items()])
78- subprocess.check_call(args)
79-
80-
81-def _service_name(config):
82- unit = os.environ['JUJU_UNIT_NAME'].split('/')[0]
83+from charmhelpers.core import (hookenv, host)
84+
85+
86+def _service_name():
87+ unit = hookenv.service_name()
88 for x in (':', '-', '/', '"', "'"):
89 unit = unit.replace(x, '_')
90 return unit
91
92
93-def _service_dir(config):
94- return os.path.join(config['install_root'], _service_name(config))
95+hooks = hookenv.Hooks()
96+
97+root_dir = lambda cfg: os.path.join(cfg['install_root'], _service_name())
98+code_dir = lambda cfg: os.path.join(root_dir(cfg), 'code')
99+conf_dir = lambda cfg: os.path.join(root_dir(cfg), 'conf')
100+logs_dir = lambda cfg: os.path.join(root_dir(cfg), 'logs')
101+vars_dir = lambda cfg: os.path.join(root_dir(cfg), 'var')
102+
103+
104+def create_dirs(config):
105+ """Create the directory structure expected by the deployment.
106+ /srv/rabbitmq_worker/{code,conf,logs,var}/
107+ """
108+ user = config['user']
109+ group = config['group']
110+
111+ host.mkdir(root_dir(config), perms=0755)
112+ host.mkdir(code_dir(config), perms=0755)
113+ host.mkdir(conf_dir(config), perms=0755)
114+ host.mkdir(logs_dir(config), owner=user, group=group, perms=0755)
115+ host.mkdir(vars_dir(config), owner=user, group=group, perms=0755)
116
117
118 def pip_install(package):
119 cmd_line = ['pip', 'install', '-b', '/tmp/']
120- if package.startswith('svn+') or package.startswith('git+') or \
121- package.startswith('hg+') or package.startswith('bzr+'):
122- cmd_line.append('-e')
123+ svn = package.startswith('svn+')
124+ git = package.startswith('git+')
125+ hg = package.startswith('hg+')
126+ bzr = package.startswith('bzr+')
127+
128+ if (svn or git or hg or bzr):
129+ cmd_line.append('-e')
130 cmd_line.append(package)
131- return(subprocess.call(cmd_line))
132+ return subprocess.check_call(cmd_line)
133
134
135 def _install_from_tarball(config):
136- juju_info('grabbing service from tarball...')
137- sdir = _service_dir(config)
138- if os.path.exists(sdir):
139- juju_info('deleting pre-existing service directory: %s' % sdir)
140- shutil.rmtree(sdir)
141- os.mkdir(sdir)
142- cmd = 'curl %s | tar -xzC %s' % (config['tarball'], sdir)
143+ hookenv.log('grabbing service from tarball...')
144+ cdir = code_dir(config)
145+ if os.listdir(cdir):
146+ hookenv.log('deleting pre-existing code directory: %s' % cdir)
147+ shutil.rmtree(cdir)
148+ os.mkdir(cdir)
149+ cmd = 'curl %s | tar -xzC %s' % (config['tarball'], cdir)
150 subprocess.check_call(cmd, shell=True)
151
152
153 def _install_from_bzr(config):
154- juju_info('grabbing service from bzr...')
155-
156- sdir = _service_dir(config)
157- if os.path.exists(sdir):
158- juju_info('deleting pre-existing service directory: %s' % sdir)
159- shutil.rmtree(sdir)
160+ hookenv.log('grabbing service from bzr...')
161+ cdir = code_dir(config)
162+ if os.path.exists(cdir):
163+ hookenv.log('deleting pre-existing code directory: %s' % cdir)
164+ shutil.rmtree(cdir)
165
166 args = ['bzr', 'branch']
167 rev = config.get('revno', '')
168 if rev:
169 args.extend(['-r', rev])
170 args.append(config['branch'])
171- args.append(sdir)
172+ args.append(cdir)
173 subprocess.check_call(args)
174
175
176-def install(config):
177+@hooks.hook()
178+@hooks.hook('upgrade-charm')
179+def install():
180+ config = hookenv.config()
181 charmhelpers.fetch.configure_sources(update=True)
182
183 pkgs = [x for x in config.get('packages', '').split(' ') if x]
184@@ -89,15 +93,16 @@
185 pkgs.append('python-pip')
186 pkgs.append('bzr')
187
188- juju_info('installing apt packages...')
189+ hookenv.log('installing apt packages...')
190 charmhelpers.fetch.apt_install(pkgs)
191
192 pip_pkgs = [x for x in config.get('pip-packages', '').split(' ') if x]
193 if pip_pkgs:
194- juju_info('installing pip packages...')
195+ hookenv.log('installing pip packages...')
196 for package in pip_pkgs:
197 pip_install(package.strip())
198
199+ create_dirs(config)
200 if config.get('vcs') == 'tarball':
201 _install_from_tarball(config)
202 else:
203@@ -105,7 +110,7 @@
204
205 unit_config = config.get('unit-config')
206 if unit_config:
207- with open(os.path.join(_service_dir(config), 'unit_config'), 'w') as f:
208+ with open(os.path.join(conf_dir(config), 'unit_config'), 'w') as f:
209 f.write(base64.b64decode(unit_config))
210
211
212@@ -121,36 +126,63 @@
213 # use sigint so python code just needs to catch KeyboardInterrupt
214 kill signal SIGINT
215
216+ export PYTHONPATH={conf_dir}
217+ export CONFDIR={conf_dir}
218+ export VARDIR={vars_dir}
219+ export LOGDIR={logs_dir}
220+
221 # If the process quits unexpectadly trigger respawn it, unless it
222 # fails 15 times within 5 seconds
223 respawn
224 respawn limit 15 5
225
226- setuid {uid}
227- setgid {gid}
228- chdir {sdir}
229+ setuid {user}
230+ setgid {group}
231+ chdir {code_dir}
232
233 exec {main}
234 ''')
235 params = {
236 'main': config['main'],
237- 'sdir': _service_dir(config),
238- 'uid': config.get('uid', 'nobody'),
239- 'gid': config.get('gid', 'nogroup'),
240+ 'conf_dir': conf_dir(config),
241+ 'code_dir': code_dir(config),
242+ 'vars_dir': vars_dir(config),
243+ 'logs_dir': logs_dir(config),
244+ 'user': config.get('user', 'nobody'),
245+ 'group': config.get('group', 'nogroup'),
246 }
247+<<<<<<< TREE
248 path = '/etc/init/{}.conf'.format(_service_name(config))
249 with tempfile.NamedTemporaryFile('w', delete=False) as f:
250+=======
251+ # Atomic file creation.
252+ with tempfile.NamedTemporaryFile('w', delete=False) as f:
253+>>>>>>> MERGE-SOURCE
254 f.write(template.format(**params))
255+<<<<<<< TREE
256 # Rename to final destination
257 os.rename(f.name, path)
258+=======
259+ # need this so the file is ready for the subprocess call
260+ f.flush()
261+ os.fsync(f.fileno())
262+ os.rename(f.name, '/etc/init/%s.conf' % _service_name())
263+>>>>>>> MERGE-SOURCE
264
265
266 def _create_amqp_config(config):
267+<<<<<<< TREE
268 path = os.path.join(_service_dir(config), 'amqp_config.py')
269 with tempfile.NamedTemporaryFile('w', delete=False) as f:
270+=======
271+ path = os.path.join(conf_dir(config), 'amqp_config.py')
272+ # Atomic file creation.
273+ with tempfile.NamedTemporaryFile('w', delete=False) as f:
274+>>>>>>> MERGE-SOURCE
275 f.write('# DO NOT EDIT. Generated by restish charm hook\n')
276 f.write('AMQP_USER = "%s"\n' % config['amqp-user'])
277 f.write('AMQP_VHOST = "%s"\n' % config['amqp-vhost'])
278+<<<<<<< TREE
279 f.write('AMQP_HOST = "%s"\n' % _relation_get('private-address'))
280 f.write('AMQP_PASSWORD = "%s"\n' % _relation_get('password'))
281 # Now that the file is complete (and closed) rename it to its final
282@@ -168,37 +200,52 @@
283
284
285 def amqp_relation_changed(config):
286+=======
287+ f.write('AMQP_HOST = "%s"\n' % hookenv.relation_get('private-address'))
288+ f.write('AMQP_PASSWORD = "%s"\n' % hookenv.relation_get('password'))
289+ # Rename to final location.
290+ os.rename(f.name, path)
291+
292+
293+@hooks.hook('amqp-relation-joined')
294+def amqp_relation_joined():
295+ config = hookenv.config()
296+ hookenv.relation_set(
297+ relation_settings={
298+ 'username': config['amqp-user'],
299+ 'vhost': config['amqp-vhost'],
300+ }
301+ )
302+
303+
304+@hooks.hook('amqp-relation-changed')
305+def amqp_relation_changed():
306+ config = hookenv.config()
307+>>>>>>> MERGE-SOURCE
308 _create_upstart(config)
309 _create_amqp_config(config)
310 try:
311- subprocess.check_call(['/sbin/restart', _service_name(config)])
312+ subprocess.check_call(['/sbin/restart', _service_name()])
313 except subprocess.CalledProcessError:
314 # if it wasn't running, restart fails, so just try to start
315- subprocess.check_call(['/sbin/start', _service_name(config)])
316-
317-
318-def amqp_relation_broken(config):
319- subprocess.call(['/sbin/stop', _service_name(config)])
320- os.unlink(os.path.join(_service_dir(config), 'amqp_config.py'))
321+ subprocess.check_call(['/sbin/start', _service_name()])
322+
323+
324+@hooks.hook('amqp-relation-broken')
325+def amqp_relation_broken():
326+ config = hookenv.config()
327+ host.service_stop(_service_name())
328+ os.unlink(os.path.join(conf_dir(config), 'amqp_config.py'))
329
330
331 def main():
332- hook = os.path.basename(sys.argv[0])
333- juju_info("Running hook: %s" % hook)
334-
335- hook_py = hook.replace('-', '_')
336- funcs = globals()
337- if hook_py not in funcs:
338- print("Unknown hook: %s" % hook)
339+ try:
340+ hooks.execute(sys.argv)
341+ return 0
342+ except Exception as e:
343+ hookenv.log('hook failed: %s' % str(e))
344 return 1
345
346- config = _config()
347- try:
348- return funcs[hook_py](config)
349- except subprocess.CalledProcessError as e:
350- juju_info('Error running: %s: %s' % (e.cmd, e.output))
351- return e.returncode
352-
353
354 if __name__ == '__main__':
355 exit(main())
356
357=== added file 'charms/precise/rabbitmq-worker/hooks/test_hooks.py'
358--- charms/precise/rabbitmq-worker/hooks/test_hooks.py 1970-01-01 00:00:00 +0000
359+++ charms/precise/rabbitmq-worker/hooks/test_hooks.py 2014-03-09 17:04:23 +0000
360@@ -0,0 +1,34 @@
361+import os
362+import testtools
363+import hooks
364+import mock
365+import ci_utils
366+
367+class T(testtools.TestCase):
368+ # Give it a name.
369+ @mock.patch.dict(os.environ, {'JUJU_UNIT_NAME': 'test_unit'})
370+ # charmhelpers wants to call juju-log.
371+ @mock.patch('subprocess.call')
372+ # You need to be root to chown to a different user/group.
373+ @mock.patch('os.chown')
374+ def test_create_dirs(self, mock_chown, mock_call):
375+ with ci_utils.TmpDir() as d:
376+ cfg = {'install_root': d, 'user': 'nobody', 'group': 'nogroup'}
377+ hooks.create_dirs(cfg)
378+ paths = ('code', 'conf', 'logs', 'var')
379+ for p in map(lambda x: os.path.join(d, 'test_unit', x), paths):
380+ self.assertTrue(os.path.exists(p))
381+
382+ # Give it a name.
383+ @mock.patch.dict(os.environ, {'JUJU_UNIT_NAME': 'test_unit'})
384+ # We cannot rename to /etc/init/...
385+ @mock.patch('os.rename')
386+ def test_create_upstart(self, mock_rename):
387+ with ci_utils.TmpDir() as d:
388+ u = 'nobody'
389+ g = 'nogroup'
390+ p = '/usr/bin/program'
391+ cfg = {'install_root': d, 'user': u, 'group': g, 'main': p}
392+ hooks._create_upstart(cfg)
393+ expected = '/etc/init/test_unit.conf'
394+ self.assertEqual(mock_rename.call_args[0][1], expected)
395
396=== added symlink 'charms/precise/rabbitmq-worker/hooks/upgrade-charm'
397=== target is u'hooks.py'
398=== added file 'ci-utils/ci_utils/logger.py'
399--- ci-utils/ci_utils/logger.py 1970-01-01 00:00:00 +0000
400+++ ci-utils/ci_utils/logger.py 2014-03-09 17:04:23 +0000
401@@ -0,0 +1,34 @@
402+# Ubuntu CI Engine
403+# Copyright 2013, 2014 Canonical Ltd.
404+
405+# This program is free software: you can redistribute it and/or modify it
406+# under the terms of the GNU Affero General Public License version 3, as
407+# published by the Free Software Foundation.
408+
409+# This program is distributed in the hope that it will be useful, but
410+# WITHOUT ANY WARRANTY; without even the implied warranties of
411+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
412+# PURPOSE. See the GNU Affero General Public License for more details.
413+
414+# You should have received a copy of the GNU Affero General Public License
415+# along with this program. If not, see <http://www.gnu.org/licenses/>.
416+
417+import logging
418+import os
419+
420+
421+def setup_logging(name):
422+ fmt = '%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s'
423+ kwargs = {'format': fmt, 'datefmt': '%H:%M:%S', 'level': logging.INFO}
424+ logging.basicConfig(**kwargs)
425+ log = logging.getLogger(name)
426+
427+ if 'LOGDIR' in os.environ:
428+ path = '%s.log' % name
429+ path = os.path.join(os.environ['LOGDIR'], path)
430+ # 10 MB
431+ tenmb = 10 * 1024 * 1024
432+ fh = logging.handlers.RotatingFileHandler(path, maxBytes=tenmb)
433+ log.addHandler(fh)
434+
435+ return log
436
437=== modified file 'juju-deployer/image-builder.yaml.tmpl'
438--- juju-deployer/image-builder.yaml.tmpl 2014-03-09 17:04:23 +0000
439+++ juju-deployer/image-builder.yaml.tmpl 2014-03-09 17:04:23 +0000
440@@ -28,7 +28,7 @@
441 branch: ${CI_BRANCH}
442 tarball: ${CI_PAYLOAD_URL}
443 packages: "qemu-utils python-glanceclient python-swiftclient"
444- uid: root
445+ user: root
446 unit-config: include-base64://configs/unit_config.yaml
447 install_sources: |
448 - ${CI_PPA}
449
450=== modified file 'juju-deployer/test-runner.yaml.tmpl'
451--- juju-deployer/test-runner.yaml.tmpl 2014-03-09 17:04:23 +0000
452+++ juju-deployer/test-runner.yaml.tmpl 2014-03-09 17:04:23 +0000
453@@ -36,7 +36,7 @@
454 tarball: ${CI_PAYLOAD_URL}
455 packages: "python-requests python-novaclient python-swiftclient"
456 unit-config: include-base64://configs/unit_config.yaml
457- uid: root
458+ user: root
459 install_sources: |
460 - ${CI_PPA}
461 install_keys: |
462
463=== modified file 'test_runner/tstrun/run_worker.py'
464--- test_runner/tstrun/run_worker.py 2014-03-06 17:33:33 +0000
465+++ test_runner/tstrun/run_worker.py 2014-03-09 17:04:23 +0000
466@@ -15,16 +15,12 @@
467 # along with this program. If not, see <http://www.gnu.org/licenses/>.
468
469 import json
470-import logging
471-
472-from ci_utils import amqp_utils
473-from ci_utils import dump_stack
474-from ci_utils import data_store
475+from ci_utils import (amqp_utils, dump_stack, data_store, logger)
476 import tstrun
477 from tstrun import testbed
478
479-logging.basicConfig(level=logging.INFO)
480-log = logging.getLogger(__name__)
481+
482+log = logger.setup_logging('test-runner-worker')
483
484
485 def on_message(msg):

Subscribers

People subscribed via source and target branches

to all changes: