Merge lp:~thedac/charm-helpers/apparmor into lp:charm-helpers

Proposed by David Ames
Status: Merged
Merged at revision: 559
Proposed branch: lp:~thedac/charm-helpers/apparmor
Merge into: lp:charm-helpers
Diff against target: 214 lines (+177/-1)
2 files modified
charmhelpers/contrib/openstack/context.py (+91/-1)
tests/contrib/openstack/test_os_contexts.py (+86/-0)
To merge this branch: bzr merge lp:~thedac/charm-helpers/apparmor
Reviewer Review Type Date Requested Status
James Page Approve
Review via email: mp+290096@code.launchpad.net

Description of the change

Apparmor class for OpenStack charms

The class will validate aa-profile-mode config settings and either set
the profile to enforce, complain or disable mode.

To post a comment you must log in.
lp:~thedac/charm-helpers/apparmor updated
556. By David Ames

Fix call to manually_disable_aa_profile()

557. By David Ames

Move mitaka check into base AA class

Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

See my inline comments. They don't alter functionality, but change an appArmorContext.ctxt to a property that is set only once regardless of whether returned in the __call__() or directly by instance.ctxt. However, it's a style thing only.

Otherwise, good to go.

lp:~thedac/charm-helpers/apparmor updated
558. By David Ames

Set self.ctxt as a property

Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

Just a tiny inline comment.

lp:~thedac/charm-helpers/apparmor updated
559. By David Ames

Use self() rather than self.__call__()

Revision history for this message
James Page (james-page) :
review: Needs Fixing
Revision history for this message
David Ames (thedac) wrote :

I'll address the comments.

lp:~thedac/charm-helpers/apparmor updated
560. By David Ames

Remove python3 setting, add comments for manual disable

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

LGTM but does need a rebase prior to merge - please land away!

review: Approve
lp:~thedac/charm-helpers/apparmor updated
561. By David Ames

Merge upstream

Revision history for this message
David Ames (thedac) wrote :

Rebased. Merging.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/openstack/context.py'
2--- charmhelpers/contrib/openstack/context.py 2016-04-01 10:00:31 +0000
3+++ charmhelpers/contrib/openstack/context.py 2016-04-04 18:08:06 +0000
4@@ -20,7 +20,7 @@
5 import re
6 import time
7 from base64 import b64decode
8-from subprocess import check_call
9+from subprocess import check_call, CalledProcessError
10
11 import six
12 import yaml
13@@ -45,6 +45,7 @@
14 INFO,
15 WARNING,
16 ERROR,
17+ status_set,
18 )
19
20 from charmhelpers.core.sysctl import create as sysctl_create
21@@ -1491,3 +1492,92 @@
22 """
23 def __call__(self):
24 return {'use_internal_endpoints': config('use-internal-endpoints')}
25+
26+
27+class AppArmorContext(OSContextGenerator):
28+ """Base class for apparmor contexts."""
29+
30+ def __init__(self):
31+ self._ctxt = None
32+ self.aa_profile = None
33+ self.aa_utils_packages = ['apparmor-utils']
34+
35+ @property
36+ def ctxt(self):
37+ if self._ctxt is not None:
38+ return self._ctxt
39+ self._ctxt = self._determine_ctxt()
40+ return self._ctxt
41+
42+ def _determine_ctxt(self):
43+ """
44+ Validate aa-profile-mode settings is disable, enforce, or complain.
45+
46+ :return ctxt: Dictionary of the apparmor profile or None
47+ """
48+ if config('aa-profile-mode') in ['disable', 'enforce', 'complain']:
49+ ctxt = {'aa-profile-mode': config('aa-profile-mode')}
50+ else:
51+ ctxt = None
52+ return ctxt
53+
54+ def __call__(self):
55+ return self.ctxt
56+
57+ def install_aa_utils(self):
58+ """
59+ Install packages required for apparmor configuration.
60+ """
61+ log("Installing apparmor utils.")
62+ ensure_packages(self.aa_utils_packages)
63+
64+ def manually_disable_aa_profile(self):
65+ """
66+ Manually disable an apparmor profile.
67+
68+ If aa-profile-mode is set to disabled (default) this is required as the
69+ template has been written but apparmor is yet unaware of the profile
70+ and aa-disable aa-profile fails. Without this the profile would kick
71+ into enforce mode on the next service restart.
72+
73+ """
74+ profile_path = '/etc/apparmor.d'
75+ disable_path = '/etc/apparmor.d/disable'
76+ if not os.path.lexists(os.path.join(disable_path, self.aa_profile)):
77+ os.symlink(os.path.join(profile_path, self.aa_profile),
78+ os.path.join(disable_path, self.aa_profile))
79+
80+ def setup_aa_profile(self):
81+ """
82+ Setup an apparmor profile.
83+ The ctxt dictionary will contain the apparmor profile mode and
84+ the apparmor profile name.
85+ Makes calls out to aa-disable, aa-complain, or aa-enforce to setup
86+ the apparmor profile.
87+ """
88+ self()
89+ if not self.ctxt:
90+ log("Not enabling apparmor Profile")
91+ return
92+ self.install_aa_utils()
93+ cmd = ['aa-{}'.format(self.ctxt['aa-profile-mode'])]
94+ cmd.append(self.ctxt['aa-profile'])
95+ log("Setting up the apparmor profile for {} in {} mode."
96+ "".format(self.ctxt['aa-profile'], self.ctxt['aa-profile-mode']))
97+ try:
98+ check_call(cmd)
99+ except CalledProcessError as e:
100+ # If aa-profile-mode is set to disabled (default) manual
101+ # disabling is required as the template has been written but
102+ # apparmor is yet unaware of the profile and aa-disable aa-profile
103+ # fails. If aa-disable learns to read profile files first this can
104+ # be removed.
105+ if self.ctxt['aa-profile-mode'] == 'disable':
106+ log("Manually disabling the apparmor profile for {}."
107+ "".format(self.ctxt['aa-profile']))
108+ self.manually_disable_aa_profile()
109+ return
110+ status_set('blocked', "Apparmor profile {} failed to be set to {}."
111+ "".format(self.ctxt['aa-profile'],
112+ self.ctxt['aa-profile-mode']))
113+ raise e
114
115=== modified file 'tests/contrib/openstack/test_os_contexts.py'
116--- tests/contrib/openstack/test_os_contexts.py 2016-04-01 10:15:57 +0000
117+++ tests/contrib/openstack/test_os_contexts.py 2016-04-04 18:08:06 +0000
118@@ -84,6 +84,21 @@
119 return None
120 return sorted(self.relation_data[relation_id].keys())
121
122+
123+class FakeAppArmorContext(context.AppArmorContext):
124+
125+ def __init__(self):
126+ super(FakeAppArmorContext, self).__init__()
127+ self.aa_profile = 'fake-aa-profile'
128+
129+ def __call__(self):
130+ super(FakeAppArmorContext, self).__call__()
131+ if not self.ctxt:
132+ return self.ctxt
133+ self._ctxt.update({'aa-profile': self.aa_profile})
134+ return self.ctxt
135+
136+
137 SHARED_DB_RELATION = {
138 'db_host': 'dbserver.local',
139 'password': 'foo'
140@@ -2892,3 +2907,74 @@
141 config = {'use-internal-endpoints': True}
142 self.config.side_effect = fake_config(config)
143 self.assertTrue(ctxt()['use_internal_endpoints'])
144+
145+ def test_apparmor_context_call_not_valid(self):
146+ ''' Tests for the apparmor context'''
147+ mock_aa_object = context.AppArmorContext()
148+ # Test with invalid config
149+ self.config.return_value = 'NOTVALID'
150+ self.assertEquals(mock_aa_object.__call__(), None)
151+
152+ def test_apparmor_context_call_complain(self):
153+ ''' Tests for the apparmor context'''
154+ mock_aa_object = context.AppArmorContext()
155+ # Test complain mode
156+ self.config.return_value = 'complain'
157+ self.assertEquals(mock_aa_object.__call__(),
158+ {'aa-profile-mode': 'complain'})
159+
160+ def test_apparmor_context_call_enforce(self):
161+ ''' Tests for the apparmor context'''
162+ mock_aa_object = context.AppArmorContext()
163+ # Test enforce mode
164+ self.config.return_value = 'enforce'
165+ self.assertEquals(mock_aa_object.__call__(),
166+ {'aa-profile-mode': 'enforce'})
167+
168+ def test_apparmor_context_call_disable(self):
169+ ''' Tests for the apparmor context'''
170+ mock_aa_object = context.AppArmorContext()
171+ # Test complain mode
172+ self.config.return_value = 'disable'
173+ self.assertEquals(mock_aa_object.__call__(),
174+ {'aa-profile-mode': 'disable'})
175+
176+ def test_apparmor_setup_complain(self):
177+ ''' Tests for the apparmor setup'''
178+ AA = FakeAppArmorContext()
179+ AA.install_aa_utils = MagicMock()
180+ AA.manually_disable_aa_profile = MagicMock()
181+ # Test complain mode
182+ self.config.return_value = 'complain'
183+ AA.setup_aa_profile()
184+ AA.install_aa_utils.assert_called_with()
185+ self.check_call.assert_called_with(['aa-complain', 'fake-aa-profile'])
186+ self.assertFalse(AA.manually_disable_aa_profile.called)
187+
188+ def test_apparmor_setup_enforce(self):
189+ ''' Tests for the apparmor setup'''
190+ AA = FakeAppArmorContext()
191+ AA.install_aa_utils = MagicMock()
192+ AA.manually_disable_aa_profile = MagicMock()
193+ # Test enforce mode
194+ self.config.return_value = 'enforce'
195+ AA.setup_aa_profile()
196+ self.check_call.assert_called_with(['aa-enforce', 'fake-aa-profile'])
197+ self.assertFalse(AA.manually_disable_aa_profile.called)
198+
199+ def test_apparmor_setup_disable(self):
200+ ''' Tests for the apparmor setup'''
201+ AA = FakeAppArmorContext()
202+ AA.install_aa_utils = MagicMock()
203+ AA.manually_disable_aa_profile = MagicMock()
204+ # Test disable mode
205+ self.config.return_value = 'disable'
206+ AA.setup_aa_profile()
207+ self.check_call.assert_called_with(['aa-disable', 'fake-aa-profile'])
208+ self.assertFalse(AA.manually_disable_aa_profile.called)
209+ # Test failed to disable
210+ from subprocess import CalledProcessError
211+ self.check_call.side_effect = CalledProcessError(0, 0, 0)
212+ AA.setup_aa_profile()
213+ self.check_call.assert_called_with(['aa-disable', 'fake-aa-profile'])
214+ AA.manually_disable_aa_profile.assert_called_with()

Subscribers

People subscribed via source and target branches