Merge lp:~gandelman-a/charm-helpers/filter_req_packages into lp:charm-helpers

Proposed by Adam Gandelman
Status: Merged
Merged at revision: 25
Proposed branch: lp:~gandelman-a/charm-helpers/filter_req_packages
Merge into: lp:charm-helpers
Diff against target: 123 lines (+81/-0)
2 files modified
charmhelpers/core/host.py (+26/-0)
tests/core/test_host.py (+55/-0)
To merge this branch: bzr merge lp:~gandelman-a/charm-helpers/filter_req_packages
Reviewer Review Type Date Requested Status
Matthew Wedgwood (community) Approve
James Page Approve
Adam Gandelman (community) Needs Resubmitting
Review via email: mp+168163@code.launchpad.net

Description of the change

We run into instances where we've deployed a charm and a week later trigger some relation event that calls 'apt-get install' In that weeks time, a newer package has been published to the Ubuntu archive, causing apt-get to block on local config changes. To help keep these types of hooks idempotent, it would be helpful to only attempt installation of packages that require it, eg:

CORE_PACKAGES = ['mysql', 'apache2']
apt_update()
apt_install(filter_required_packages(CORE_PACKAGES))

Thought about adding a call to filter_required_packages() to apt_install(), but decided against it--with the correct options passed, apt_install() should be able to be used to upgrade packages as well as install

To post a comment you must log in.
Revision history for this message
Matthew Wedgwood (mew) wrote :

I think this is a good addition, but I have some (perhaps nitpicky) problems with it as-is:

1. The function name is a little opaque. "Required" is a little vague. Also, you're actually filtering the packages that aren't required. Perhaps filter_installed_packages would be ok?

2. The function assumes (by throwing an exception) that the package cache is up-to-date. It makes sense to me simply to filter the packages that are already installed, returning everything else. That way you could run 'apt-get update' later if an un-installed packaged required a new repo. This should still fit your use case, as an exception would be thrown by apt_install when the package wasn't found.

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

Matt- Agree with both points. I had proposed this at one point but we never actually started using it, so there I'm open to modifying the exception handling here. Thanks for the feedback.

23. By Adam Gandelman

Do not raise if package unavailable.

24. By Adam Gandelman

Rename filter_required_packages -> filter_installed_packages.

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

LGTM

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

Awesome, +1

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 18:13:28 +0000
4@@ -5,6 +5,7 @@
5 # Nick Moffitt <nick.moffitt@canonical.com>
6 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
7
8+import apt_pkg
9 import os
10 import pwd
11 import grp
12@@ -132,6 +133,22 @@
13 **kwargs)
14
15
16+def filter_installed_packages(packages):
17+ """Returns a list of packages that require installation"""
18+ apt_pkg.init()
19+ cache = apt_pkg.Cache()
20+ _pkgs = []
21+ for package in packages:
22+ try:
23+ p = cache[package]
24+ p.current_ver or _pkgs.append(package)
25+ except KeyError:
26+ log('Package {} has no installation candidate.'.format(package),
27+ level='WARNING')
28+ _pkgs.append(package)
29+ return _pkgs
30+
31+
32 def apt_install(packages, options=None, fatal=False):
33 """Install one or more packages"""
34 options = options or []
35@@ -150,6 +167,15 @@
36 subprocess.call(cmd)
37
38
39+def apt_update(fatal=False):
40+ """Update local apt cache"""
41+ cmd = ['apt-get', 'update']
42+ if fatal:
43+ subprocess.check_call(cmd)
44+ else:
45+ subprocess.call(cmd)
46+
47+
48 def mount(device, mountpoint, options=None, persist=False):
49 '''Mount a filesystem'''
50 cmd_args = ['mount']
51
52=== modified file 'tests/core/test_host.py'
53--- tests/core/test_host.py 2013-06-10 15:59:37 +0000
54+++ tests/core/test_host.py 2013-06-11 18:13:28 +0000
55@@ -17,6 +17,30 @@
56 """rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
57 """).strip().split('\n')
58
59+FAKE_APT_CACHE = {
60+ # an installed package
61+ 'vim': {
62+ 'current_ver': '2:7.3.547-6ubuntu5'
63+ },
64+ # a uninstalled installation candidate
65+ 'emacs': {
66+ }
67+}
68+
69+def fake_apt_cache():
70+ def _get(package):
71+ pkg = MagicMock()
72+ if package not in FAKE_APT_CACHE:
73+ raise KeyError
74+ pkg.name = package
75+ if 'current_ver' in FAKE_APT_CACHE[package]:
76+ pkg.current_ver = FAKE_APT_CACHE[package]['current_ver']
77+ else:
78+ pkg.current_ver = None
79+ return pkg
80+ cache = MagicMock()
81+ cache.__getitem__.side_effect = _get
82+ return cache
83
84 @contextmanager
85 def patch_open():
86@@ -485,6 +509,37 @@
87 check_call.assert_called_with(['apt-get', '-y', '--foo', '--bar',
88 'install', 'foo', 'bar'])
89
90+ @patch('subprocess.check_call')
91+ def test_apt_update_fatal(self, check_call):
92+ host.apt_update(fatal=True)
93+ check_call.assert_called_with(['apt-get', 'update'])
94+
95+ @patch('subprocess.call')
96+ def test_apt_update_nonfatal(self, call):
97+ host.apt_update()
98+ call.assert_called_with(['apt-get', 'update'])
99+
100+ @patch('apt_pkg.Cache')
101+ def test_filter_packages_missing(self, cache):
102+ cache.side_effect = fake_apt_cache
103+ result = host.filter_installed_packages(['vim', 'emacs'])
104+ self.assertEquals(result, ['emacs'])
105+
106+ @patch('apt_pkg.Cache')
107+ def test_filter_packages_none_missing(self, cache):
108+ cache.side_effect = fake_apt_cache
109+ result = host.filter_installed_packages(['vim'])
110+ self.assertEquals(result, [])
111+
112+ @patch.object(host, 'log')
113+ @patch('apt_pkg.Cache')
114+ def test_filter_packages_not_available(self, cache, log):
115+ cache.side_effect = fake_apt_cache
116+ result = host.filter_installed_packages(['vim', 'joe'])
117+ self.assertEquals(result, ['joe'])
118+ log.assert_called_with('Package joe has no installation candidate.',
119+ level='WARNING')
120+
121 @patch('subprocess.check_output')
122 @patch.object(host, 'log')
123 def test_mounts_a_device(self, log, check_output):

Subscribers

People subscribed via source and target branches