Merge lp:~bac/charms/oneiric/buildbot-master/bbm-pst into lp:~yellow/charms/oneiric/buildbot-master/trunk

Proposed by Brad Crittenden
Status: Merged
Approved by: Graham Binns
Approved revision: 37
Merged at revision: 37
Proposed branch: lp:~bac/charms/oneiric/buildbot-master/bbm-pst
Merge into: lp:~yellow/charms/oneiric/buildbot-master/trunk
Diff against target: 748 lines (+107/-429)
8 files modified
hooks/buildbot-relation-changed (+1/-1)
hooks/config-changed (+15/-11)
hooks/helpers.py (+13/-237)
hooks/install (+66/-6)
hooks/local.py (+5/-3)
hooks/start (+2/-1)
hooks/stop (+3/-2)
hooks/tests.py (+2/-168)
To merge this branch: bzr merge lp:~bac/charms/oneiric/buildbot-master/bbm-pst
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+95279@code.launchpad.net

Description of the change

Move generic, non-juju-related methods out of helpers and into a new package python-shell-toolbox in the ~yellow PPA.

The install hook must now add the apt repository for the PPA, install the package, and ensure local and helpers are not imported until that is done since they depend on shelltoolbox. The other hooks can just import and use shelltoolbox normally since we can assume the install hook did its job.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Approved based on conversation with Brad on G+.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/buildbot-relation-changed'
2--- hooks/buildbot-relation-changed 2012-02-13 12:26:29 +0000
3+++ hooks/buildbot-relation-changed 2012-02-29 22:49:25 +0000
4@@ -4,6 +4,7 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 import os
8+from shelltoolbox import su
9
10 from helpers import (
11 log,
12@@ -11,7 +12,6 @@
13 log_exit,
14 relation_get,
15 relation_set,
16- su,
17 )
18 from local import (
19 buildbot_reconfig,
20
21=== modified file 'hooks/config-changed'
22--- hooks/config-changed 2012-02-23 21:50:41 +0000
23+++ hooks/config-changed 2012-02-29 22:49:25 +0000
24@@ -7,23 +7,24 @@
25 import json
26 import os
27 import os.path
28+from shelltoolbox import (
29+ apt_get_install,
30+ command,
31+ DictDiffer,
32+ get_user_ids,
33+ install_extra_repositories,
34+ run,
35+ su,
36+ )
37 import shutil
38 import subprocess
39 import sys
40
41 from helpers import (
42- apt_get_install,
43- cd,
44- command,
45- DictDiffer,
46 get_config,
47- get_user_ids,
48- install_extra_repository,
49 log,
50 log_entry,
51 log_exit,
52- run,
53- su,
54 )
55 from local import (
56 buildbot_create,
57@@ -31,8 +32,6 @@
58 config_json,
59 fetch_history,
60 generate_string,
61- get_bucket,
62- get_key,
63 get_wrapper_cfg_path,
64 put_history,
65 slave_json,
66@@ -75,7 +74,12 @@
67 added_or_changed = diff.added_or_changed
68
69 if extra_repo and 'extra-repository' in added_or_changed:
70- install_extra_repository(extra_repo)
71+ try:
72+ install_extra_repositories(extra_repo)
73+ except subprocess.CalledProcessError as e:
74+ log('Error adding repository: ' + extra_repo)
75+ log(e)
76+ raise
77 restart_required = True
78 if extra_pkgs and 'extra-packages' in added_or_changed:
79 apt_get_install(
80
81=== modified file 'hooks/helpers.py'
82--- hooks/helpers.py 2012-02-14 17:00:53 +0000
83+++ hooks/helpers.py 2012-02-29 22:49:25 +0000
84@@ -5,37 +5,24 @@
85
86 __metaclass__ = type
87 __all__ = [
88- 'apt_get_install',
89- 'cd',
90- 'command',
91- 'DictDiffer',
92 'get_config',
93- 'get_user_ids',
94- 'get_user_home',
95- 'get_value_from_line',
96- 'grep',
97- 'install_extra_repository',
98 'log',
99 'log_entry',
100 'log_exit',
101- 'run',
102 'relation_get',
103 'relation_set',
104- 'su',
105 'unit_info',
106 ]
107
108 from collections import namedtuple
109-from contextlib import contextmanager
110 import json
111 import operator
112-import os
113-import pwd
114-import re
115-import subprocess
116-import sys
117+from shelltoolbox import (
118+ command,
119+ run,
120+ script_name,
121+ )
122 import tempfile
123-from textwrap import dedent
124 import time
125 import urllib2
126 import yaml
127@@ -43,49 +30,15 @@
128
129 Env = namedtuple('Env', 'uid gid home')
130
131-
132-def run(*args):
133- """Run the command with the given arguments.
134-
135- The first argument is the path to the command to run, subsequent arguments
136- are command-line arguments to be passed.
137- """
138- process = subprocess.Popen(args, stdout=subprocess.PIPE,
139- stderr=subprocess.PIPE, close_fds=True)
140- stdout, stderr = process.communicate()
141- if process.returncode:
142- raise subprocess.CalledProcessError(
143- process.returncode, repr(args), output=stdout+stderr)
144- return stdout
145-
146-
147-def command(*base_args):
148- """Return a callable that will run the given command with any arguments.
149-
150- The first argument is the path to the command to run, subsequent arguments
151- are command-line arguments to "bake into" the returned callable.
152-
153- The callable runs the given executable and also takes arguments that will
154- be appeneded to the "baked in" arguments.
155-
156- For example, this code will list a file named "foo" (if it exists):
157-
158- ls_foo = command('/bin/ls', 'foo')
159- ls_foo()
160-
161- While this invocation will list "foo" and "bar" (assuming they exist):
162-
163- ls_foo('bar')
164- """
165- def callable_command(*args):
166- all_args = base_args + args
167- return run(*all_args)
168-
169- return callable_command
170-
171-
172 log = command('juju-log')
173-apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
174+
175+
176+def log_entry():
177+ log("--> Entering {}".format(script_name()))
178+
179+
180+def log_exit():
181+ log("<-- Exiting {}".format(script_name()))
182
183
184 def get_config():
185@@ -93,16 +46,6 @@
186 return json.loads(config_get())
187
188
189-def install_extra_repository(extra_repository):
190- try:
191- run('apt-add-repository', extra_repository)
192- run('apt-get', 'update')
193- except subprocess.CalledProcessError as e:
194- log('Error adding repository: ' + extra_repository)
195- log(e)
196- raise
197-
198-
199 def relation_get(*args):
200 cmd = command('relation-get')
201 return cmd(*args).strip()
202@@ -114,96 +57,6 @@
203 return cmd(*args)
204
205
206-def grep(content, filename):
207- with open(filename) as f:
208- for line in f:
209- if re.match(content, line):
210- return line.strip()
211-
212-
213-def get_value_from_line(line):
214- return line.split('=')[1].strip('"\' ')
215-
216-
217-def script_name():
218- return os.path.basename(sys.argv[0])
219-
220-
221-def log_entry():
222- log("<-- Entering {}".format(script_name()))
223-
224-
225-def log_exit():
226- log("--> Exiting {}".format(script_name()))
227-
228-
229-class DictDiffer:
230- """
231- Calculate the difference between two dictionaries as:
232- (1) items added
233- (2) items removed
234- (3) keys same in both but changed values
235- (4) keys same in both and unchanged values
236- """
237-
238- # Based on answer by hughdbrown at:
239- # http://stackoverflow.com/questions/1165352
240-
241- def __init__(self, current_dict, past_dict):
242- self.current_dict = current_dict
243- self.past_dict = past_dict
244- self.set_current = set(current_dict)
245- self.set_past = set(past_dict)
246- self.intersect = self.set_current.intersection(self.set_past)
247-
248- @property
249- def added(self):
250- return self.set_current - self.intersect
251-
252- @property
253- def removed(self):
254- return self.set_past - self.intersect
255-
256- @property
257- def changed(self):
258- return set(key for key in self.intersect
259- if self.past_dict[key] != self.current_dict[key])
260- @property
261- def unchanged(self):
262- return set(key for key in self.intersect
263- if self.past_dict[key] == self.current_dict[key])
264- @property
265- def modified(self):
266- return self.current_dict != self.past_dict
267-
268- @property
269- def added_or_changed(self):
270- return self.added.union(self.changed)
271-
272- def _changes(self, keys):
273- new = {}
274- old = {}
275- for k in keys:
276- new[k] = self.current_dict.get(k)
277- old[k] = self.past_dict.get(k)
278- return "%s -> %s" % (old, new)
279-
280- def __str__(self):
281- if self.modified:
282- s = dedent("""\
283- added: %s
284- removed: %s
285- changed: %s
286- unchanged: %s""") % (
287- self._changes(self.added),
288- self._changes(self.removed),
289- self._changes(self.changed),
290- list(self.unchanged))
291- else:
292- s = "no changes"
293- return s
294-
295-
296 def make_charm_config_file(charm_config):
297 charm_config_file = tempfile.NamedTemporaryFile()
298 charm_config_file.write(yaml.dump(charm_config))
299@@ -312,80 +165,3 @@
300 if time.time() - start_time >= timeout:
301 raise RuntimeError('timeout waiting for contents of ' + url)
302 time.sleep(0.1)
303-
304-
305-class Serializer:
306- """Handle JSON (de)serialization."""
307-
308- def __init__(self, path, default=None, serialize=None, deserialize=None):
309- self.path = path
310- self.default = default or {}
311- self.serialize = serialize or json.dump
312- self.deserialize = deserialize or json.load
313-
314- def exists(self):
315- return os.path.exists(self.path)
316-
317- def get(self):
318- if self.exists():
319- with open(self.path) as f:
320- return self.deserialize(f)
321- return self.default
322-
323- def set(self, data):
324- with open(self.path, 'w') as f:
325- self.serialize(data, f)
326-
327-
328-@contextmanager
329-def cd(directory):
330- """A context manager to temporary change current working dir, e.g.::
331-
332- >>> import os
333- >>> os.chdir('/tmp')
334- >>> with cd('/bin'): print os.getcwd()
335- /bin
336- >>> os.getcwd()
337- '/tmp'
338- """
339- cwd = os.getcwd()
340- os.chdir(directory)
341- yield
342- os.chdir(cwd)
343-
344-
345-def get_user_ids(user):
346- """Return the uid and gid of given `user`, e.g.::
347-
348- >>> get_user_ids('root')
349- (0, 0)
350- """
351- userdata = pwd.getpwnam(user)
352- return userdata.pw_uid, userdata.pw_gid
353-
354-
355-def get_user_home(user):
356- """Return the home directory of the given `user`.
357-
358- >>> get_user_home('root')
359- '/root'
360- """
361- return pwd.getpwnam(user).pw_dir
362-
363-
364-@contextmanager
365-def su(user):
366- """A context manager to temporary run the script as a different user."""
367- uid, gid = get_user_ids(user)
368- os.setegid(gid)
369- os.seteuid(uid)
370- current_home = os.getenv('HOME')
371- home = get_user_home(user)
372- os.environ['HOME'] = home
373- try:
374- yield Env(uid, gid, home)
375- finally:
376- os.setegid(os.getgid())
377- os.seteuid(os.getuid())
378- if current_home is not None:
379- os.environ['HOME'] = current_home
380
381=== modified file 'hooks/install'
382--- hooks/install 2012-02-22 19:42:50 +0000
383+++ hooks/install 2012-02-29 22:49:25 +0000
384@@ -5,15 +5,75 @@
385
386 import os
387 import shutil
388-from subprocess import CalledProcessError
389+import subprocess
390+
391+
392+def run(*args):
393+ """Run the command with the given arguments.
394+
395+ The first argument is the path to the command to run, subsequent arguments
396+ are command-line arguments to be passed.
397+ """
398+ process = subprocess.Popen(args, stdout=subprocess.PIPE,
399+ stderr=subprocess.PIPE, close_fds=True)
400+ stdout, stderr = process.communicate()
401+ if process.returncode:
402+ raise subprocess.CalledProcessError(
403+ process.returncode, repr(args), output=stdout+stderr)
404+ return stdout
405+
406+
407+def command(*base_args):
408+ """Return a callable that will run the given command with any arguments.
409+
410+ The first argument is the path to the command to run, subsequent arguments
411+ are command-line arguments to "bake into" the returned callable.
412+
413+ The callable runs the given executable and also takes arguments that will
414+ be appeneded to the "baked in" arguments.
415+
416+ For example, this code will list a file named "foo" (if it exists):
417+
418+ ls_foo = command('/bin/ls', 'foo')
419+ ls_foo()
420+
421+ While this invocation will list "foo" and "bar" (assuming they exist):
422+
423+ ls_foo('bar')
424+ """
425+ def callable_command(*args):
426+ all_args = base_args + args
427+ return run(*all_args)
428+
429+ return callable_command
430+
431+
432+log = command('juju-log')
433+
434+
435+def install_extra_repository(extra_repository):
436+ try:
437+ run('apt-add-repository', extra_repository)
438+ run('apt-get', 'update')
439+ except subprocess.CalledProcessError as e:
440+ log('Error adding repository: ' + extra_repository)
441+ log(e)
442+ raise
443+
444+
445+def install_packages():
446+ apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
447+ apt_get_install('bzr', 'python-boto')
448+ install_extra_repository('ppa:yellow/ppa')
449+ apt_get_install('python-shell-toolbox')
450+
451+
452+install_packages()
453
454 from helpers import (
455- apt_get_install,
456 get_config,
457- log,
458 log_entry,
459 log_exit,
460- run,
461 )
462 from local import (
463 config_json,
464@@ -22,13 +82,12 @@
465
466
467 def cleanup(buildbot_dir):
468- apt_get_install('bzr', 'python-boto')
469 # Since we may be installing into a pre-existing service, ensure the
470 # buildbot directory is removed.
471 if os.path.exists(buildbot_dir):
472 try:
473 run('buildbot', 'stop', buildbot_dir)
474- except (CalledProcessError, OSError):
475+ except (subprocess.CalledProcessError, OSError):
476 # This usually happens because buildbot hasn't been
477 # installed yet, or that it wasn't running; just
478 # ignore the error.
479@@ -52,6 +111,7 @@
480
481
482 if __name__ == '__main__':
483+
484 log_entry()
485 try:
486 main()
487
488=== modified file 'hooks/local.py'
489--- hooks/local.py 2012-02-23 21:50:41 +0000
490+++ hooks/local.py 2012-02-29 22:49:25 +0000
491@@ -26,14 +26,16 @@
492 import subprocess
493 import uuid
494
495-from helpers import (
496+from shelltoolbox import (
497 cd,
498- get_config,
499- log,
500 run,
501 Serializer,
502 su,
503 )
504+from helpers import (
505+ get_config,
506+ log,
507+ )
508
509
510 HTTP_PORT_PROTOCOL = '8010/TCP'
511
512=== modified file 'hooks/start'
513--- hooks/start 2012-02-22 19:42:50 +0000
514+++ hooks/start 2012-02-29 22:49:25 +0000
515@@ -3,10 +3,11 @@
516 # Copyright 2012 Canonical Ltd. This software is licensed under the
517 # GNU Affero General Public License version 3 (see the file LICENSE).
518
519+from shelltoolbox import run
520+
521 from helpers import (
522 log_entry,
523 log_exit,
524- run,
525 )
526 from local import HTTP_PORT_PROTOCOL
527
528
529=== modified file 'hooks/stop'
530--- hooks/stop 2012-02-22 19:42:50 +0000
531+++ hooks/stop 2012-02-29 22:49:25 +0000
532@@ -3,12 +3,13 @@
533 # Copyright 2012 Canonical Ltd. This software is licensed under the
534 # GNU Affero General Public License version 3 (see the file LICENSE).
535
536+from shelltoolbox import run
537+
538 from helpers import (
539- get_config,
540+ get_config
541 log,
542 log_entry,
543 log_exit,
544- run,
545 )
546 from local import (
547 HTTP_PORT_PROTOCOL,
548
549=== modified file 'hooks/tests.py'
550--- hooks/tests.py 2012-02-23 16:45:54 +0000
551+++ hooks/tests.py 2012-02-29 22:49:25 +0000
552@@ -12,14 +12,14 @@
553 import tempfile
554 import unittest
555
556-from helpers import (
557+from shelltoolbox import (
558 cd,
559 command,
560 DictDiffer,
561 run,
562 su,
563- unit_info,
564 )
565+from helpers import unit_info
566 from local import (
567 get_bucket,
568 get_key,
569@@ -28,95 +28,6 @@
570 )
571
572
573-class TestRun(unittest.TestCase):
574-
575- def testSimpleCommand(self):
576- # Running a simple command (ls) works and running the command
577- # produces a string.
578- self.assertIsInstance(run('/bin/ls'), str)
579-
580- def testStdoutReturned(self):
581- # Running a simple command (ls) works and running the command
582- # produces a string.
583- self.assertIn('Usage:', run('/bin/ls', '--help'))
584-
585- def testCalledProcessErrorRaised(self):
586- # If an error occurs a CalledProcessError is raised with the return
587- # code, command executed, and the output of the command.
588- with self.assertRaises(CalledProcessError) as info:
589- run('ls', '--not a valid switch')
590- exception = info.exception
591- self.assertEqual(2, exception.returncode)
592- self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
593- self.assertIn('unrecognized option', exception.output)
594-
595-
596-class TestCommand(unittest.TestCase):
597-
598- def testSimpleCommand(self):
599- # Creating a simple command (ls) works and running the command
600- # produces a string.
601- ls = command('/bin/ls')
602- self.assertIsInstance(ls(), str)
603-
604- def testArguments(self):
605- # Arguments can be passed to commands.
606- ls = command('/bin/ls')
607- self.assertIn('Usage:', ls('--help'))
608-
609- def testMissingExecutable(self):
610- # If the command does not exist, an OSError (No such file or
611- # directory) is raised.
612- bad = command('this command does not exist')
613- with self.assertRaises(OSError) as info:
614- bad()
615- self.assertEqual(2, info.exception.errno)
616-
617- def testError(self):
618- # If the command returns a non-zero exit code, an exception is raised.
619- ls = command('/bin/ls')
620- with self.assertRaises(CalledProcessError):
621- ls('--not a valid switch')
622-
623- def testBakedInArguments(self):
624- # Arguments can be passed when creating the command as well as when
625- # executing it.
626- ll = command('/bin/ls', '-l')
627- self.assertIn('rw', ll()) # Assumes a file is r/w in the pwd.
628- self.assertIn('Usage:', ll('--help'))
629-
630- def testQuoting(self):
631- # There is no need to quote special shell characters in commands.
632- ls = command('/bin/ls')
633- ls('--help', '>')
634-
635-
636-class TestDictDiffer(unittest.TestCase):
637-
638- def testStr(self):
639- a = dict(cow='moo', pig='oink')
640- b = dict(cow='moo', pig='oinkoink', horse='nay')
641- diff = DictDiffer(b, a)
642- s = str(diff)
643- self.assertIn("added: {'horse': None} -> {'horse': 'nay'}", s)
644- self.assertIn("removed: {} -> {}", s)
645- self.assertIn("changed: {'pig': 'oink'} -> {'pig': 'oinkoink'}", s)
646- self.assertIn("unchanged: ['cow']", s)
647-
648- def testStrUnmodified(self):
649- a = dict(cow='moo', pig='oink')
650- diff = DictDiffer(a, a)
651- s = str(diff)
652- self.assertEquals('no changes', s)
653-
654- def testAddedOrChanged(self):
655- a = dict(cow='moo', pig='oink')
656- b = dict(cow='moo', pig='oinkoink', horse='nay')
657- diff = DictDiffer(b, a)
658- expected = set(['horse', 'pig'])
659- self.assertEquals(expected, diff.added_or_changed)
660-
661-
662 class TestUnit_info(unittest.TestCase):
663
664 def make_data(self, state='started'):
665@@ -153,83 +64,6 @@
666 self.assertNotEqual('started', state)
667
668
669-current_euid = os.geteuid()
670-current_egid = os.getegid()
671-current_home = os.environ['HOME']
672-example_euid = current_euid + 1
673-example_egid = current_egid + 1
674-example_home = '/var/lib/example'
675-userinfo = {'example_user': dict(
676- ids=(example_euid, example_egid), home=example_home)}
677-effective_values = dict(uid=current_euid, gid=current_egid)
678-
679-
680-def stub_os_seteuid(value):
681- effective_values['uid'] = value
682-
683-
684-def stub_os_setegid(value):
685- effective_values['gid'] = value
686-
687-
688-class TestSuContextManager(unittest.TestCase):
689-
690- def setUp(self):
691- import helpers
692- self.os_seteuid = os.seteuid
693- self.os_setegid = os.setegid
694- self.helpers_get_user_ids = helpers.get_user_ids
695- self.helpers_get_user_home = helpers.get_user_home
696- os.seteuid = stub_os_seteuid
697- os.setegid = stub_os_setegid
698- helpers.get_user_ids = lambda user: userinfo[user]['ids']
699- helpers.get_user_home = lambda user: userinfo[user]['home']
700-
701- def tearDown(self):
702- import helpers
703- os.seteuid = self.os_seteuid
704- os.setegid = self.os_setegid
705- helpers.get_user_ids = self.helpers_get_user_ids
706- helpers.get_user_home = self.helpers_get_user_home
707-
708- def testChange(self):
709- with su('example_user'):
710- self.assertEqual(example_euid, effective_values['uid'])
711- self.assertEqual(example_egid, effective_values['gid'])
712- self.assertEqual(example_home, os.environ['HOME'])
713-
714- def testEnvironment(self):
715- with su('example_user') as e:
716- self.assertEqual(example_euid, e.uid)
717- self.assertEqual(example_egid, e.gid)
718- self.assertEqual(example_home, e.home)
719-
720- def testRevert(self):
721- with su('example_user'):
722- pass
723- self.assertEqual(current_euid, effective_values['uid'])
724- self.assertEqual(current_egid, effective_values['gid'])
725- self.assertEqual(current_home, os.environ['HOME'])
726-
727- def testRevertAfterFailure(self):
728- try:
729- with su('example_user'):
730- raise RuntimeError()
731- except RuntimeError:
732- self.assertEqual(current_euid, effective_values['uid'])
733- self.assertEqual(current_egid, effective_values['gid'])
734- self.assertEqual(current_home, os.environ['HOME'])
735-
736-
737-class TestCdContextManager(unittest.TestCase):
738- def test_cd(self):
739- curdir = os.getcwd()
740- self.assertNotEqual('/var', curdir)
741- with cd('/var'):
742- self.assertEqual('/var', os.getcwd())
743- self.assertEqual(curdir, os.getcwd())
744-
745-
746 class StubBucket:
747 """Stub S3 Bucket."""
748 def __init__(self, name):

Subscribers

People subscribed via source and target branches

to all changes: