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
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2013-06-11 14:09:19 +0000
+++ charmhelpers/core/host.py 2013-06-11 21:49:25 +0000
@@ -9,6 +9,7 @@
9import pwd9import pwd
10import grp10import grp
11import subprocess11import subprocess
12import hashlib
1213
13from hookenv import log, execution_environment14from hookenv import log, execution_environment
1415
@@ -188,3 +189,45 @@
188 system_mounts = [m[1::-1] for m in [l.strip().split()189 system_mounts = [m[1::-1] for m in [l.strip().split()
189 for l in f.readlines()]]190 for l in f.readlines()]]
190 return system_mounts191 return system_mounts
192
193
194def file_hash(path):
195 ''' Generate a md5 hash of the contents of 'path' or None if not found '''
196 if os.path.exists(path):
197 h = hashlib.md5()
198 with open(path, 'r') as source:
199 h.update(source.read()) # IGNORE:E1101 - it does have update
200 return h.hexdigest()
201 else:
202 return None
203
204
205def restart_on_change(restart_map):
206 ''' Restart services based on configuration files changing
207
208 This function is used a decorator, for example
209
210 @restart_on_change({
211 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
212 })
213 def ceph_client_changed():
214 ...
215
216 In this example, the cinder-api and cinder-volume services
217 would be restarted if /etc/ceph/ceph.conf is changed by the
218 ceph_client_changed function.
219 '''
220 def wrap(f):
221 def wrapped_f(*args):
222 checksums = {}
223 for path in restart_map:
224 checksums[path] = file_hash(path)
225 f(*args)
226 restarts = []
227 for path in restart_map:
228 if checksums[path] != file_hash(path):
229 restarts += restart_map[path]
230 for service_name in list(set(restarts)):
231 service('restart', service_name)
232 return wrapped_f
233 return wrap
191234
=== modified file 'tests/core/test_host.py'
--- tests/core/test_host.py 2013-06-10 15:59:37 +0000
+++ tests/core/test_host.py 2013-06-11 21:49:25 +0000
@@ -561,3 +561,103 @@
561 ['/dev/pts', 'devpts']561 ['/dev/pts', 'devpts']
562 ])562 ])
563 mock_open.assert_called_with('/proc/mounts')563 mock_open.assert_called_with('/proc/mounts')
564
565 _hash_files = {
566 '/etc/exists.conf': 'lots of nice ceph configuration',
567 '/etc/missing.conf': None
568 }
569
570 @patch('hashlib.md5')
571 @patch('os.path.exists')
572 def test_file_hash_exists(self, exists, md5):
573 filename = '/etc/exists.conf'
574 exists.side_effect = [True]
575 m = md5()
576 m.hexdigest.return_value = self._hash_files[filename]
577 with patch_open() as (mock_open, mock_file):
578 mock_file.read.return_value = self._hash_files[filename]
579 result = host.file_hash(filename)
580 self.assertEqual(result, self._hash_files[filename])
581
582 @patch('os.path.exists')
583 def test_file_hash_missing(self, exists):
584 filename = '/etc/missing.conf'
585 exists.side_effect = [False]
586 with patch_open() as (mock_open, mock_file):
587 mock_file.read.return_value = self._hash_files[filename]
588 result = host.file_hash(filename)
589 self.assertEqual(result, None)
590
591 @patch.object(host, 'service')
592 @patch('os.path.exists')
593 def test_restart_no_changes(self, exists, service):
594 file_name = '/etc/missing.conf'
595 restart_map = {
596 file_name: ['test-service']
597 }
598 exists.side_effect = [False, False]
599
600 @host.restart_on_change(restart_map)
601 def make_no_changes():
602 pass
603
604 make_no_changes()
605
606 assert not service.called
607
608 exists.assert_has_calls([
609 call(file_name),
610 ])
611
612 @patch.object(host, 'service')
613 @patch('os.path.exists')
614 def test_restart_on_change(self, exists, service):
615 file_name = '/etc/missing.conf'
616 restart_map = {
617 file_name: ['test-service']
618 }
619 exists.side_effect = [False, True]
620
621 @host.restart_on_change(restart_map)
622 def make_some_changes(mock_file):
623 mock_file.read.return_value = "newstuff"
624
625 with patch_open() as (mock_open, mock_file):
626 make_some_changes(mock_file)
627
628 for service_name in restart_map[file_name]:
629 service.assert_called_with('restart', service_name)
630
631 exists.assert_has_calls([
632 call(file_name),
633 ])
634
635 @patch.object(host, 'service')
636 @patch('os.path.exists')
637 def test_multiservice_restart_on_change(self, exists, service):
638 file_name_one = '/etc/missing.conf'
639 file_name_two = '/etc/exists.conf'
640 restart_map = {
641 file_name_one: ['test-service'],
642 file_name_two: ['test-service', 'test-service2']
643 }
644 exists.side_effect = [False, True, True, True]
645
646 @host.restart_on_change(restart_map)
647 def make_some_changes():
648 pass
649
650 with patch_open() as (mock_open, mock_file):
651 mock_file.read.side_effect = ['exists', 'missing', 'exists2']
652 make_some_changes()
653
654 # Restart should only happen once per service
655 service.assert_has_calls([
656 call('restart', 'test-service2'),
657 call('restart', 'test-service')
658 ])
659
660 exists.assert_has_calls([
661 call(file_name_one),
662 call(file_name_two)
663 ])

Subscribers

People subscribed via source and target branches