Merge lp:~james-page/charm-helpers/inteli-restart into lp:charm-helpers

Proposed by James Page
Status: Merged
Merged at revision: 24
Proposed branch: lp:~james-page/charm-helpers/inteli-restart
Merge into: lp:charm-helpers
Diff against target: 165 lines (+143/-0)
2 files modified
charmhelpers/core/host.py (+43/-0)
tests/core/test_host.py (+100/-0)
To merge this branch: bzr merge lp:~james-page/charm-helpers/inteli-restart
Reviewer Review Type Date Requested Status
Adam Gandelman (community) Approve
James Page Needs Resubmitting
Review via email: mp+168734@code.launchpad.net

Description of the change

Inteligent restarts of services based on configuration file changes

Example:

@inteli_restart({'/etc/ceph/ceph.conf': [ 'ceph-mon', 'ceph-osd' ]})
def ceph_client_changed():
   ...

In the above example, ceph-mon and ceph-osd would be restarted if the configuration file /etc/ceph/ceph.conf is changed by the hook being executed.

This was missed in the initial sync from openstack-charm-helpers as it landed quite late in to the quantum-gateway charm.

To post a comment you must log in.
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Not sure where, but any chance this can live somewhere out of hahelpers? I'd like to be able to use this without carrying the rest of the hahelpers. Would it make sense to add this to charmhelpers.core.host?

Revision history for this message
Matthew Wedgwood (mew) wrote :

This sounds like an excellent addition to core.host. Could we also give it a more meaningful name, like 'restart_on_change'?

Revision history for this message
James Page (james-page) wrote :

Fine with me - I'll refactor

25. By James Page

Initial integration into core.host with first set of tests

26. By James Page

Revert changes to utils.py

Revision history for this message
James Page (james-page) wrote :

OK _ how does that look?

I can expand the test cases a bit as well to ensure that service restarts are only called once even if multiple files change for example

Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Looks good, couple of small things:

- White space in tests, comments in restart_on_change() are missing indents.
- Definitely no blocker but test_file_hash_exist could mock out the hashlib stuff so the test isn't testing actual md5 calculation, just that the hexdigest() is being returned correctly to caller.

27. By James Page

Sortout indent on comment

28. By James Page

Tidy up lint

Revision history for this message
James Page (james-page) wrote :

Tidied whitespace and other lint errors/warnings as well

29. By James Page

Patchout hashlib, add multiservice test

30. By James Page

Check restart calls a different way

Revision history for this message
James Page (james-page) :
review: Needs Resubmitting
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/core/host.py'
2--- charmhelpers/core/host.py 2013-06-11 14:09:19 +0000
3+++ charmhelpers/core/host.py 2013-06-11 21:49:25 +0000
4@@ -9,6 +9,7 @@
5 import pwd
6 import grp
7 import subprocess
8+import hashlib
9
10 from hookenv import log, execution_environment
11
12@@ -188,3 +189,45 @@
13 system_mounts = [m[1::-1] for m in [l.strip().split()
14 for l in f.readlines()]]
15 return system_mounts
16+
17+
18+def file_hash(path):
19+ ''' Generate a md5 hash of the contents of 'path' or None if not found '''
20+ if os.path.exists(path):
21+ h = hashlib.md5()
22+ with open(path, 'r') as source:
23+ h.update(source.read()) # IGNORE:E1101 - it does have update
24+ return h.hexdigest()
25+ else:
26+ return None
27+
28+
29+def restart_on_change(restart_map):
30+ ''' Restart services based on configuration files changing
31+
32+ This function is used a decorator, for example
33+
34+ @restart_on_change({
35+ '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
36+ })
37+ def ceph_client_changed():
38+ ...
39+
40+ In this example, the cinder-api and cinder-volume services
41+ would be restarted if /etc/ceph/ceph.conf is changed by the
42+ ceph_client_changed function.
43+ '''
44+ def wrap(f):
45+ def wrapped_f(*args):
46+ checksums = {}
47+ for path in restart_map:
48+ checksums[path] = file_hash(path)
49+ f(*args)
50+ restarts = []
51+ for path in restart_map:
52+ if checksums[path] != file_hash(path):
53+ restarts += restart_map[path]
54+ for service_name in list(set(restarts)):
55+ service('restart', service_name)
56+ return wrapped_f
57+ return wrap
58
59=== modified file 'tests/core/test_host.py'
60--- tests/core/test_host.py 2013-06-10 15:59:37 +0000
61+++ tests/core/test_host.py 2013-06-11 21:49:25 +0000
62@@ -561,3 +561,103 @@
63 ['/dev/pts', 'devpts']
64 ])
65 mock_open.assert_called_with('/proc/mounts')
66+
67+ _hash_files = {
68+ '/etc/exists.conf': 'lots of nice ceph configuration',
69+ '/etc/missing.conf': None
70+ }
71+
72+ @patch('hashlib.md5')
73+ @patch('os.path.exists')
74+ def test_file_hash_exists(self, exists, md5):
75+ filename = '/etc/exists.conf'
76+ exists.side_effect = [True]
77+ m = md5()
78+ m.hexdigest.return_value = self._hash_files[filename]
79+ with patch_open() as (mock_open, mock_file):
80+ mock_file.read.return_value = self._hash_files[filename]
81+ result = host.file_hash(filename)
82+ self.assertEqual(result, self._hash_files[filename])
83+
84+ @patch('os.path.exists')
85+ def test_file_hash_missing(self, exists):
86+ filename = '/etc/missing.conf'
87+ exists.side_effect = [False]
88+ with patch_open() as (mock_open, mock_file):
89+ mock_file.read.return_value = self._hash_files[filename]
90+ result = host.file_hash(filename)
91+ self.assertEqual(result, None)
92+
93+ @patch.object(host, 'service')
94+ @patch('os.path.exists')
95+ def test_restart_no_changes(self, exists, service):
96+ file_name = '/etc/missing.conf'
97+ restart_map = {
98+ file_name: ['test-service']
99+ }
100+ exists.side_effect = [False, False]
101+
102+ @host.restart_on_change(restart_map)
103+ def make_no_changes():
104+ pass
105+
106+ make_no_changes()
107+
108+ assert not service.called
109+
110+ exists.assert_has_calls([
111+ call(file_name),
112+ ])
113+
114+ @patch.object(host, 'service')
115+ @patch('os.path.exists')
116+ def test_restart_on_change(self, exists, service):
117+ file_name = '/etc/missing.conf'
118+ restart_map = {
119+ file_name: ['test-service']
120+ }
121+ exists.side_effect = [False, True]
122+
123+ @host.restart_on_change(restart_map)
124+ def make_some_changes(mock_file):
125+ mock_file.read.return_value = "newstuff"
126+
127+ with patch_open() as (mock_open, mock_file):
128+ make_some_changes(mock_file)
129+
130+ for service_name in restart_map[file_name]:
131+ service.assert_called_with('restart', service_name)
132+
133+ exists.assert_has_calls([
134+ call(file_name),
135+ ])
136+
137+ @patch.object(host, 'service')
138+ @patch('os.path.exists')
139+ def test_multiservice_restart_on_change(self, exists, service):
140+ file_name_one = '/etc/missing.conf'
141+ file_name_two = '/etc/exists.conf'
142+ restart_map = {
143+ file_name_one: ['test-service'],
144+ file_name_two: ['test-service', 'test-service2']
145+ }
146+ exists.side_effect = [False, True, True, True]
147+
148+ @host.restart_on_change(restart_map)
149+ def make_some_changes():
150+ pass
151+
152+ with patch_open() as (mock_open, mock_file):
153+ mock_file.read.side_effect = ['exists', 'missing', 'exists2']
154+ make_some_changes()
155+
156+ # Restart should only happen once per service
157+ service.assert_has_calls([
158+ call('restart', 'test-service2'),
159+ call('restart', 'test-service')
160+ ])
161+
162+ exists.assert_has_calls([
163+ call(file_name_one),
164+ call(file_name_two)
165+ ])

Subscribers

People subscribed via source and target branches