Merge lp:~junaidali/charms/trusty/plumgrid-director/pg-restart into lp:~plumgrid-team/charms/trusty/plumgrid-director/trunk
- Trusty Tahr (14.04)
- pg-restart
- Merge into trunk
Proposed by
Junaid Ali
Status: | Merged |
---|---|
Merged at revision: | 32 |
Proposed branch: | lp:~junaidali/charms/trusty/plumgrid-director/pg-restart |
Merge into: | lp:~plumgrid-team/charms/trusty/plumgrid-director/trunk |
Diff against target: |
6142 lines (+47/-5762) 52 files modified
charm-helpers-sync.yaml (+6/-1) config.yaml (+8/-0) hooks/charmhelpers/contrib/ansible/__init__.py (+0/-254) hooks/charmhelpers/contrib/benchmark/__init__.py (+0/-126) hooks/charmhelpers/contrib/charmhelpers/__init__.py (+0/-208) hooks/charmhelpers/contrib/charmsupport/__init__.py (+0/-15) hooks/charmhelpers/contrib/charmsupport/nrpe.py (+0/-398) hooks/charmhelpers/contrib/charmsupport/volumes.py (+0/-175) hooks/charmhelpers/contrib/database/mysql.py (+0/-412) hooks/charmhelpers/contrib/hardening/__init__.py (+0/-15) hooks/charmhelpers/contrib/hardening/apache/__init__.py (+0/-19) hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py (+0/-31) hooks/charmhelpers/contrib/hardening/apache/checks/config.py (+0/-100) hooks/charmhelpers/contrib/hardening/audits/__init__.py (+0/-63) hooks/charmhelpers/contrib/hardening/audits/apache.py (+0/-100) hooks/charmhelpers/contrib/hardening/audits/apt.py (+0/-105) hooks/charmhelpers/contrib/hardening/audits/file.py (+0/-552) hooks/charmhelpers/contrib/hardening/harden.py (+0/-84) hooks/charmhelpers/contrib/hardening/host/__init__.py (+0/-19) hooks/charmhelpers/contrib/hardening/host/checks/__init__.py (+0/-50) hooks/charmhelpers/contrib/hardening/host/checks/apt.py (+0/-39) hooks/charmhelpers/contrib/hardening/host/checks/limits.py (+0/-55) hooks/charmhelpers/contrib/hardening/host/checks/login.py (+0/-67) hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py (+0/-52) hooks/charmhelpers/contrib/hardening/host/checks/pam.py (+0/-134) hooks/charmhelpers/contrib/hardening/host/checks/profile.py (+0/-45) hooks/charmhelpers/contrib/hardening/host/checks/securetty.py (+0/-39) hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py (+0/-131) hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py (+0/-211) hooks/charmhelpers/contrib/hardening/mysql/__init__.py (+0/-19) hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py (+0/-31) hooks/charmhelpers/contrib/hardening/mysql/checks/config.py (+0/-89) hooks/charmhelpers/contrib/hardening/ssh/__init__.py (+0/-19) hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py (+0/-31) hooks/charmhelpers/contrib/hardening/ssh/checks/config.py (+0/-394) hooks/charmhelpers/contrib/hardening/templating.py (+0/-71) hooks/charmhelpers/contrib/hardening/utils.py (+0/-157) hooks/charmhelpers/contrib/mellanox/infiniband.py (+0/-151) hooks/charmhelpers/contrib/peerstorage/__init__.py (+0/-269) hooks/charmhelpers/contrib/saltstack/__init__.py (+0/-118) hooks/charmhelpers/contrib/ssl/__init__.py (+0/-94) hooks/charmhelpers/contrib/ssl/service.py (+0/-279) hooks/charmhelpers/contrib/templating/__init__.py (+0/-15) hooks/charmhelpers/contrib/templating/contexts.py (+0/-139) hooks/charmhelpers/contrib/templating/jinja.py (+0/-40) hooks/charmhelpers/contrib/templating/pyformat.py (+0/-29) hooks/charmhelpers/contrib/unison/__init__.py (+0/-313) hooks/pg_dir_hooks.py (+21/-0) hooks/pg_dir_utils.py (+3/-2) metadata.yaml (+2/-0) templates/kilo/nginx.conf (+5/-1) unit_tests/test_pg_dir_hooks.py (+2/-1) |
To merge this branch: | bzr merge lp:~junaidali/charms/trusty/plumgrid-director/pg-restart |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Bilal Baqar | Pending | ||
Review via email: mp+293101@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 33. By Junaid Ali
-
Updated restart_pg function
- 34. By Junaid Ali
-
Updated sleep time after starting libvirt-bin
- 35. By Junaid Ali
-
update sleep time in restart_pg, changes for make sync
- 36. By Junaid Ali
-
nginx changes
- 37. By Junaid Ali
-
director provides a new relations for pg-vip etc
- 38. By Junaid Ali
-
added symlink
- 39. By Junaid Ali
-
leader check for plumgrid-configs relation
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers-sync.yaml' | |||
2 | --- charm-helpers-sync.yaml 2015-07-29 18:07:31 +0000 | |||
3 | +++ charm-helpers-sync.yaml 2016-05-04 13:25:41 +0000 | |||
4 | @@ -3,5 +3,10 @@ | |||
5 | 3 | include: | 3 | include: |
6 | 4 | - core | 4 | - core |
7 | 5 | - fetch | 5 | - fetch |
9 | 6 | - contrib | 6 | - contrib.amulet |
10 | 7 | - contrib.hahelpers | ||
11 | 8 | - contrib.network | ||
12 | 9 | - contrib.openstack | ||
13 | 10 | - contrib.python | ||
14 | 11 | - contrib.storage | ||
15 | 7 | - payload | 12 | - payload |
16 | 8 | 13 | ||
17 | === modified file 'config.yaml' | |||
18 | --- config.yaml 2016-03-24 12:33:25 +0000 | |||
19 | +++ config.yaml 2016-05-04 13:25:41 +0000 | |||
20 | @@ -3,6 +3,14 @@ | |||
21 | 3 | default: 192.168.100.250 | 3 | default: 192.168.100.250 |
22 | 4 | type: string | 4 | type: string |
23 | 5 | description: IP address of the Director's Management interface. Same IP can be used to access PG Console. | 5 | description: IP address of the Director's Management interface. Same IP can be used to access PG Console. |
24 | 6 | plumgrid-username: | ||
25 | 7 | default: plumgrid | ||
26 | 8 | type: string | ||
27 | 9 | description: Username to access PLUMgrid Director | ||
28 | 10 | plumgrid-password: | ||
29 | 11 | default: plumgrid | ||
30 | 12 | type: string | ||
31 | 13 | description: Password to access PLUMgrid Director | ||
32 | 6 | lcm-ssh-key: | 14 | lcm-ssh-key: |
33 | 7 | default: 'null' | 15 | default: 'null' |
34 | 8 | type: string | 16 | type: string |
35 | 9 | 17 | ||
36 | === removed directory 'hooks/charmhelpers/contrib/ansible' | |||
37 | === removed file 'hooks/charmhelpers/contrib/ansible/__init__.py' | |||
38 | --- hooks/charmhelpers/contrib/ansible/__init__.py 2015-07-29 18:07:31 +0000 | |||
39 | +++ hooks/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000 | |||
40 | @@ -1,254 +0,0 @@ | |||
41 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
42 | 2 | # | ||
43 | 3 | # This file is part of charm-helpers. | ||
44 | 4 | # | ||
45 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
46 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
47 | 7 | # published by the Free Software Foundation. | ||
48 | 8 | # | ||
49 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
50 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
51 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
52 | 12 | # GNU Lesser General Public License for more details. | ||
53 | 13 | # | ||
54 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
55 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
56 | 16 | |||
57 | 17 | # Copyright 2013 Canonical Ltd. | ||
58 | 18 | # | ||
59 | 19 | # Authors: | ||
60 | 20 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
61 | 21 | """Charm Helpers ansible - declare the state of your machines. | ||
62 | 22 | |||
63 | 23 | This helper enables you to declare your machine state, rather than | ||
64 | 24 | program it procedurally (and have to test each change to your procedures). | ||
65 | 25 | Your install hook can be as simple as:: | ||
66 | 26 | |||
67 | 27 | {{{ | ||
68 | 28 | import charmhelpers.contrib.ansible | ||
69 | 29 | |||
70 | 30 | |||
71 | 31 | def install(): | ||
72 | 32 | charmhelpers.contrib.ansible.install_ansible_support() | ||
73 | 33 | charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml') | ||
74 | 34 | }}} | ||
75 | 35 | |||
76 | 36 | and won't need to change (nor will its tests) when you change the machine | ||
77 | 37 | state. | ||
78 | 38 | |||
79 | 39 | All of your juju config and relation-data are available as template | ||
80 | 40 | variables within your playbooks and templates. An install playbook looks | ||
81 | 41 | something like:: | ||
82 | 42 | |||
83 | 43 | {{{ | ||
84 | 44 | --- | ||
85 | 45 | - hosts: localhost | ||
86 | 46 | user: root | ||
87 | 47 | |||
88 | 48 | tasks: | ||
89 | 49 | - name: Add private repositories. | ||
90 | 50 | template: | ||
91 | 51 | src: ../templates/private-repositories.list.jinja2 | ||
92 | 52 | dest: /etc/apt/sources.list.d/private.list | ||
93 | 53 | |||
94 | 54 | - name: Update the cache. | ||
95 | 55 | apt: update_cache=yes | ||
96 | 56 | |||
97 | 57 | - name: Install dependencies. | ||
98 | 58 | apt: pkg={{ item }} | ||
99 | 59 | with_items: | ||
100 | 60 | - python-mimeparse | ||
101 | 61 | - python-webob | ||
102 | 62 | - sunburnt | ||
103 | 63 | |||
104 | 64 | - name: Setup groups. | ||
105 | 65 | group: name={{ item.name }} gid={{ item.gid }} | ||
106 | 66 | with_items: | ||
107 | 67 | - { name: 'deploy_user', gid: 1800 } | ||
108 | 68 | - { name: 'service_user', gid: 1500 } | ||
109 | 69 | |||
110 | 70 | ... | ||
111 | 71 | }}} | ||
112 | 72 | |||
113 | 73 | Read more online about `playbooks`_ and standard ansible `modules`_. | ||
114 | 74 | |||
115 | 75 | .. _playbooks: http://www.ansibleworks.com/docs/playbooks.html | ||
116 | 76 | .. _modules: http://www.ansibleworks.com/docs/modules.html | ||
117 | 77 | |||
118 | 78 | A further feature os the ansible hooks is to provide a light weight "action" | ||
119 | 79 | scripting tool. This is a decorator that you apply to a function, and that | ||
120 | 80 | function can now receive cli args, and can pass extra args to the playbook. | ||
121 | 81 | |||
122 | 82 | e.g. | ||
123 | 83 | |||
124 | 84 | |||
125 | 85 | @hooks.action() | ||
126 | 86 | def some_action(amount, force="False"): | ||
127 | 87 | "Usage: some-action AMOUNT [force=True]" # <-- shown on error | ||
128 | 88 | # process the arguments | ||
129 | 89 | # do some calls | ||
130 | 90 | # return extra-vars to be passed to ansible-playbook | ||
131 | 91 | return { | ||
132 | 92 | 'amount': int(amount), | ||
133 | 93 | 'type': force, | ||
134 | 94 | } | ||
135 | 95 | |||
136 | 96 | You can now create a symlink to hooks.py that can be invoked like a hook, but | ||
137 | 97 | with cli params: | ||
138 | 98 | |||
139 | 99 | # link actions/some-action to hooks/hooks.py | ||
140 | 100 | |||
141 | 101 | actions/some-action amount=10 force=true | ||
142 | 102 | |||
143 | 103 | """ | ||
144 | 104 | import os | ||
145 | 105 | import stat | ||
146 | 106 | import subprocess | ||
147 | 107 | import functools | ||
148 | 108 | |||
149 | 109 | import charmhelpers.contrib.templating.contexts | ||
150 | 110 | import charmhelpers.core.host | ||
151 | 111 | import charmhelpers.core.hookenv | ||
152 | 112 | import charmhelpers.fetch | ||
153 | 113 | |||
154 | 114 | |||
155 | 115 | charm_dir = os.environ.get('CHARM_DIR', '') | ||
156 | 116 | ansible_hosts_path = '/etc/ansible/hosts' | ||
157 | 117 | # Ansible will automatically include any vars in the following | ||
158 | 118 | # file in its inventory when run locally. | ||
159 | 119 | ansible_vars_path = '/etc/ansible/host_vars/localhost' | ||
160 | 120 | |||
161 | 121 | |||
162 | 122 | def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'): | ||
163 | 123 | """Installs the ansible package. | ||
164 | 124 | |||
165 | 125 | By default it is installed from the `PPA`_ linked from | ||
166 | 126 | the ansible `website`_ or from a ppa specified by a charm config.. | ||
167 | 127 | |||
168 | 128 | .. _PPA: https://launchpad.net/~rquillo/+archive/ansible | ||
169 | 129 | .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu | ||
170 | 130 | |||
171 | 131 | If from_ppa is empty, you must ensure that the package is available | ||
172 | 132 | from a configured repository. | ||
173 | 133 | """ | ||
174 | 134 | if from_ppa: | ||
175 | 135 | charmhelpers.fetch.add_source(ppa_location) | ||
176 | 136 | charmhelpers.fetch.apt_update(fatal=True) | ||
177 | 137 | charmhelpers.fetch.apt_install('ansible') | ||
178 | 138 | with open(ansible_hosts_path, 'w+') as hosts_file: | ||
179 | 139 | hosts_file.write('localhost ansible_connection=local') | ||
180 | 140 | |||
181 | 141 | |||
182 | 142 | def apply_playbook(playbook, tags=None, extra_vars=None): | ||
183 | 143 | tags = tags or [] | ||
184 | 144 | tags = ",".join(tags) | ||
185 | 145 | charmhelpers.contrib.templating.contexts.juju_state_to_yaml( | ||
186 | 146 | ansible_vars_path, namespace_separator='__', | ||
187 | 147 | allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR)) | ||
188 | 148 | |||
189 | 149 | # we want ansible's log output to be unbuffered | ||
190 | 150 | env = os.environ.copy() | ||
191 | 151 | env['PYTHONUNBUFFERED'] = "1" | ||
192 | 152 | call = [ | ||
193 | 153 | 'ansible-playbook', | ||
194 | 154 | '-c', | ||
195 | 155 | 'local', | ||
196 | 156 | playbook, | ||
197 | 157 | ] | ||
198 | 158 | if tags: | ||
199 | 159 | call.extend(['--tags', '{}'.format(tags)]) | ||
200 | 160 | if extra_vars: | ||
201 | 161 | extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()] | ||
202 | 162 | call.extend(['--extra-vars', " ".join(extra)]) | ||
203 | 163 | subprocess.check_call(call, env=env) | ||
204 | 164 | |||
205 | 165 | |||
206 | 166 | class AnsibleHooks(charmhelpers.core.hookenv.Hooks): | ||
207 | 167 | """Run a playbook with the hook-name as the tag. | ||
208 | 168 | |||
209 | 169 | This helper builds on the standard hookenv.Hooks helper, | ||
210 | 170 | but additionally runs the playbook with the hook-name specified | ||
211 | 171 | using --tags (ie. running all the tasks tagged with the hook-name). | ||
212 | 172 | |||
213 | 173 | Example:: | ||
214 | 174 | |||
215 | 175 | hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml') | ||
216 | 176 | |||
217 | 177 | # All the tasks within my_machine_state.yaml tagged with 'install' | ||
218 | 178 | # will be run automatically after do_custom_work() | ||
219 | 179 | @hooks.hook() | ||
220 | 180 | def install(): | ||
221 | 181 | do_custom_work() | ||
222 | 182 | |||
223 | 183 | # For most of your hooks, you won't need to do anything other | ||
224 | 184 | # than run the tagged tasks for the hook: | ||
225 | 185 | @hooks.hook('config-changed', 'start', 'stop') | ||
226 | 186 | def just_use_playbook(): | ||
227 | 187 | pass | ||
228 | 188 | |||
229 | 189 | # As a convenience, you can avoid the above noop function by specifying | ||
230 | 190 | # the hooks which are handled by ansible-only and they'll be registered | ||
231 | 191 | # for you: | ||
232 | 192 | # hooks = AnsibleHooks( | ||
233 | 193 | # 'playbooks/my_machine_state.yaml', | ||
234 | 194 | # default_hooks=['config-changed', 'start', 'stop']) | ||
235 | 195 | |||
236 | 196 | if __name__ == "__main__": | ||
237 | 197 | # execute a hook based on the name the program is called by | ||
238 | 198 | hooks.execute(sys.argv) | ||
239 | 199 | |||
240 | 200 | """ | ||
241 | 201 | |||
242 | 202 | def __init__(self, playbook_path, default_hooks=None): | ||
243 | 203 | """Register any hooks handled by ansible.""" | ||
244 | 204 | super(AnsibleHooks, self).__init__() | ||
245 | 205 | |||
246 | 206 | self._actions = {} | ||
247 | 207 | self.playbook_path = playbook_path | ||
248 | 208 | |||
249 | 209 | default_hooks = default_hooks or [] | ||
250 | 210 | |||
251 | 211 | def noop(*args, **kwargs): | ||
252 | 212 | pass | ||
253 | 213 | |||
254 | 214 | for hook in default_hooks: | ||
255 | 215 | self.register(hook, noop) | ||
256 | 216 | |||
257 | 217 | def register_action(self, name, function): | ||
258 | 218 | """Register a hook""" | ||
259 | 219 | self._actions[name] = function | ||
260 | 220 | |||
261 | 221 | def execute(self, args): | ||
262 | 222 | """Execute the hook followed by the playbook using the hook as tag.""" | ||
263 | 223 | hook_name = os.path.basename(args[0]) | ||
264 | 224 | extra_vars = None | ||
265 | 225 | if hook_name in self._actions: | ||
266 | 226 | extra_vars = self._actions[hook_name](args[1:]) | ||
267 | 227 | else: | ||
268 | 228 | super(AnsibleHooks, self).execute(args) | ||
269 | 229 | |||
270 | 230 | charmhelpers.contrib.ansible.apply_playbook( | ||
271 | 231 | self.playbook_path, tags=[hook_name], extra_vars=extra_vars) | ||
272 | 232 | |||
273 | 233 | def action(self, *action_names): | ||
274 | 234 | """Decorator, registering them as actions""" | ||
275 | 235 | def action_wrapper(decorated): | ||
276 | 236 | |||
277 | 237 | @functools.wraps(decorated) | ||
278 | 238 | def wrapper(argv): | ||
279 | 239 | kwargs = dict(arg.split('=') for arg in argv) | ||
280 | 240 | try: | ||
281 | 241 | return decorated(**kwargs) | ||
282 | 242 | except TypeError as e: | ||
283 | 243 | if decorated.__doc__: | ||
284 | 244 | e.args += (decorated.__doc__,) | ||
285 | 245 | raise | ||
286 | 246 | |||
287 | 247 | self.register_action(decorated.__name__, wrapper) | ||
288 | 248 | if '_' in decorated.__name__: | ||
289 | 249 | self.register_action( | ||
290 | 250 | decorated.__name__.replace('_', '-'), wrapper) | ||
291 | 251 | |||
292 | 252 | return wrapper | ||
293 | 253 | |||
294 | 254 | return action_wrapper | ||
295 | 255 | 0 | ||
296 | === removed directory 'hooks/charmhelpers/contrib/benchmark' | |||
297 | === removed file 'hooks/charmhelpers/contrib/benchmark/__init__.py' | |||
298 | --- hooks/charmhelpers/contrib/benchmark/__init__.py 2015-07-29 18:07:31 +0000 | |||
299 | +++ hooks/charmhelpers/contrib/benchmark/__init__.py 1970-01-01 00:00:00 +0000 | |||
300 | @@ -1,126 +0,0 @@ | |||
301 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
302 | 2 | # | ||
303 | 3 | # This file is part of charm-helpers. | ||
304 | 4 | # | ||
305 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
306 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
307 | 7 | # published by the Free Software Foundation. | ||
308 | 8 | # | ||
309 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
310 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
311 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
312 | 12 | # GNU Lesser General Public License for more details. | ||
313 | 13 | # | ||
314 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
315 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
316 | 16 | |||
317 | 17 | import subprocess | ||
318 | 18 | import time | ||
319 | 19 | import os | ||
320 | 20 | from distutils.spawn import find_executable | ||
321 | 21 | |||
322 | 22 | from charmhelpers.core.hookenv import ( | ||
323 | 23 | in_relation_hook, | ||
324 | 24 | relation_ids, | ||
325 | 25 | relation_set, | ||
326 | 26 | relation_get, | ||
327 | 27 | ) | ||
328 | 28 | |||
329 | 29 | |||
330 | 30 | def action_set(key, val): | ||
331 | 31 | if find_executable('action-set'): | ||
332 | 32 | action_cmd = ['action-set'] | ||
333 | 33 | |||
334 | 34 | if isinstance(val, dict): | ||
335 | 35 | for k, v in iter(val.items()): | ||
336 | 36 | action_set('%s.%s' % (key, k), v) | ||
337 | 37 | return True | ||
338 | 38 | |||
339 | 39 | action_cmd.append('%s=%s' % (key, val)) | ||
340 | 40 | subprocess.check_call(action_cmd) | ||
341 | 41 | return True | ||
342 | 42 | return False | ||
343 | 43 | |||
344 | 44 | |||
345 | 45 | class Benchmark(): | ||
346 | 46 | """ | ||
347 | 47 | Helper class for the `benchmark` interface. | ||
348 | 48 | |||
349 | 49 | :param list actions: Define the actions that are also benchmarks | ||
350 | 50 | |||
351 | 51 | From inside the benchmark-relation-changed hook, you would | ||
352 | 52 | Benchmark(['memory', 'cpu', 'disk', 'smoke', 'custom']) | ||
353 | 53 | |||
354 | 54 | Examples: | ||
355 | 55 | |||
356 | 56 | siege = Benchmark(['siege']) | ||
357 | 57 | siege.start() | ||
358 | 58 | [... run siege ...] | ||
359 | 59 | # The higher the score, the better the benchmark | ||
360 | 60 | siege.set_composite_score(16.70, 'trans/sec', 'desc') | ||
361 | 61 | siege.finish() | ||
362 | 62 | |||
363 | 63 | |||
364 | 64 | """ | ||
365 | 65 | |||
366 | 66 | BENCHMARK_CONF = '/etc/benchmark.conf' # Replaced in testing | ||
367 | 67 | |||
368 | 68 | required_keys = [ | ||
369 | 69 | 'hostname', | ||
370 | 70 | 'port', | ||
371 | 71 | 'graphite_port', | ||
372 | 72 | 'graphite_endpoint', | ||
373 | 73 | 'api_port' | ||
374 | 74 | ] | ||
375 | 75 | |||
376 | 76 | def __init__(self, benchmarks=None): | ||
377 | 77 | if in_relation_hook(): | ||
378 | 78 | if benchmarks is not None: | ||
379 | 79 | for rid in sorted(relation_ids('benchmark')): | ||
380 | 80 | relation_set(relation_id=rid, relation_settings={ | ||
381 | 81 | 'benchmarks': ",".join(benchmarks) | ||
382 | 82 | }) | ||
383 | 83 | |||
384 | 84 | # Check the relation data | ||
385 | 85 | config = {} | ||
386 | 86 | for key in self.required_keys: | ||
387 | 87 | val = relation_get(key) | ||
388 | 88 | if val is not None: | ||
389 | 89 | config[key] = val | ||
390 | 90 | else: | ||
391 | 91 | # We don't have all of the required keys | ||
392 | 92 | config = {} | ||
393 | 93 | break | ||
394 | 94 | |||
395 | 95 | if len(config): | ||
396 | 96 | with open(self.BENCHMARK_CONF, 'w') as f: | ||
397 | 97 | for key, val in iter(config.items()): | ||
398 | 98 | f.write("%s=%s\n" % (key, val)) | ||
399 | 99 | |||
400 | 100 | @staticmethod | ||
401 | 101 | def start(): | ||
402 | 102 | action_set('meta.start', time.strftime('%Y-%m-%dT%H:%M:%SZ')) | ||
403 | 103 | |||
404 | 104 | """ | ||
405 | 105 | If the collectd charm is also installed, tell it to send a snapshot | ||
406 | 106 | of the current profile data. | ||
407 | 107 | """ | ||
408 | 108 | COLLECT_PROFILE_DATA = '/usr/local/bin/collect-profile-data' | ||
409 | 109 | if os.path.exists(COLLECT_PROFILE_DATA): | ||
410 | 110 | subprocess.check_output([COLLECT_PROFILE_DATA]) | ||
411 | 111 | |||
412 | 112 | @staticmethod | ||
413 | 113 | def finish(): | ||
414 | 114 | action_set('meta.stop', time.strftime('%Y-%m-%dT%H:%M:%SZ')) | ||
415 | 115 | |||
416 | 116 | @staticmethod | ||
417 | 117 | def set_composite_score(value, units, direction='asc'): | ||
418 | 118 | """ | ||
419 | 119 | Set the composite score for a benchmark run. This is a single number | ||
420 | 120 | representative of the benchmark results. This could be the most | ||
421 | 121 | important metric, or an amalgamation of metric scores. | ||
422 | 122 | """ | ||
423 | 123 | return action_set( | ||
424 | 124 | "meta.composite", | ||
425 | 125 | {'value': value, 'units': units, 'direction': direction} | ||
426 | 126 | ) | ||
427 | 127 | 0 | ||
428 | === removed directory 'hooks/charmhelpers/contrib/charmhelpers' | |||
429 | === removed file 'hooks/charmhelpers/contrib/charmhelpers/__init__.py' | |||
430 | --- hooks/charmhelpers/contrib/charmhelpers/__init__.py 2015-07-29 18:07:31 +0000 | |||
431 | +++ hooks/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 | |||
432 | @@ -1,208 +0,0 @@ | |||
433 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
434 | 2 | # | ||
435 | 3 | # This file is part of charm-helpers. | ||
436 | 4 | # | ||
437 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
438 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
439 | 7 | # published by the Free Software Foundation. | ||
440 | 8 | # | ||
441 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
442 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
443 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
444 | 12 | # GNU Lesser General Public License for more details. | ||
445 | 13 | # | ||
446 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
447 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
448 | 16 | |||
449 | 17 | # Copyright 2012 Canonical Ltd. This software is licensed under the | ||
450 | 18 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
451 | 19 | |||
452 | 20 | import warnings | ||
453 | 21 | warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) # noqa | ||
454 | 22 | |||
455 | 23 | import operator | ||
456 | 24 | import tempfile | ||
457 | 25 | import time | ||
458 | 26 | import yaml | ||
459 | 27 | import subprocess | ||
460 | 28 | |||
461 | 29 | import six | ||
462 | 30 | if six.PY3: | ||
463 | 31 | from urllib.request import urlopen | ||
464 | 32 | from urllib.error import (HTTPError, URLError) | ||
465 | 33 | else: | ||
466 | 34 | from urllib2 import (urlopen, HTTPError, URLError) | ||
467 | 35 | |||
468 | 36 | """Helper functions for writing Juju charms in Python.""" | ||
469 | 37 | |||
470 | 38 | __metaclass__ = type | ||
471 | 39 | __all__ = [ | ||
472 | 40 | # 'get_config', # core.hookenv.config() | ||
473 | 41 | # 'log', # core.hookenv.log() | ||
474 | 42 | # 'log_entry', # core.hookenv.log() | ||
475 | 43 | # 'log_exit', # core.hookenv.log() | ||
476 | 44 | # 'relation_get', # core.hookenv.relation_get() | ||
477 | 45 | # 'relation_set', # core.hookenv.relation_set() | ||
478 | 46 | # 'relation_ids', # core.hookenv.relation_ids() | ||
479 | 47 | # 'relation_list', # core.hookenv.relation_units() | ||
480 | 48 | # 'config_get', # core.hookenv.config() | ||
481 | 49 | # 'unit_get', # core.hookenv.unit_get() | ||
482 | 50 | # 'open_port', # core.hookenv.open_port() | ||
483 | 51 | # 'close_port', # core.hookenv.close_port() | ||
484 | 52 | # 'service_control', # core.host.service() | ||
485 | 53 | 'unit_info', # client-side, NOT IMPLEMENTED | ||
486 | 54 | 'wait_for_machine', # client-side, NOT IMPLEMENTED | ||
487 | 55 | 'wait_for_page_contents', # client-side, NOT IMPLEMENTED | ||
488 | 56 | 'wait_for_relation', # client-side, NOT IMPLEMENTED | ||
489 | 57 | 'wait_for_unit', # client-side, NOT IMPLEMENTED | ||
490 | 58 | ] | ||
491 | 59 | |||
492 | 60 | |||
493 | 61 | SLEEP_AMOUNT = 0.1 | ||
494 | 62 | |||
495 | 63 | |||
496 | 64 | # We create a juju_status Command here because it makes testing much, | ||
497 | 65 | # much easier. | ||
498 | 66 | def juju_status(): | ||
499 | 67 | subprocess.check_call(['juju', 'status']) | ||
500 | 68 | |||
501 | 69 | # re-implemented as charmhelpers.fetch.configure_sources() | ||
502 | 70 | # def configure_source(update=False): | ||
503 | 71 | # source = config_get('source') | ||
504 | 72 | # if ((source.startswith('ppa:') or | ||
505 | 73 | # source.startswith('cloud:') or | ||
506 | 74 | # source.startswith('http:'))): | ||
507 | 75 | # run('add-apt-repository', source) | ||
508 | 76 | # if source.startswith("http:"): | ||
509 | 77 | # run('apt-key', 'import', config_get('key')) | ||
510 | 78 | # if update: | ||
511 | 79 | # run('apt-get', 'update') | ||
512 | 80 | |||
513 | 81 | |||
514 | 82 | # DEPRECATED: client-side only | ||
515 | 83 | def make_charm_config_file(charm_config): | ||
516 | 84 | charm_config_file = tempfile.NamedTemporaryFile(mode='w+') | ||
517 | 85 | charm_config_file.write(yaml.dump(charm_config)) | ||
518 | 86 | charm_config_file.flush() | ||
519 | 87 | # The NamedTemporaryFile instance is returned instead of just the name | ||
520 | 88 | # because we want to take advantage of garbage collection-triggered | ||
521 | 89 | # deletion of the temp file when it goes out of scope in the caller. | ||
522 | 90 | return charm_config_file | ||
523 | 91 | |||
524 | 92 | |||
525 | 93 | # DEPRECATED: client-side only | ||
526 | 94 | def unit_info(service_name, item_name, data=None, unit=None): | ||
527 | 95 | if data is None: | ||
528 | 96 | data = yaml.safe_load(juju_status()) | ||
529 | 97 | service = data['services'].get(service_name) | ||
530 | 98 | if service is None: | ||
531 | 99 | # XXX 2012-02-08 gmb: | ||
532 | 100 | # This allows us to cope with the race condition that we | ||
533 | 101 | # have between deploying a service and having it come up in | ||
534 | 102 | # `juju status`. We could probably do with cleaning it up so | ||
535 | 103 | # that it fails a bit more noisily after a while. | ||
536 | 104 | return '' | ||
537 | 105 | units = service['units'] | ||
538 | 106 | if unit is not None: | ||
539 | 107 | item = units[unit][item_name] | ||
540 | 108 | else: | ||
541 | 109 | # It might seem odd to sort the units here, but we do it to | ||
542 | 110 | # ensure that when no unit is specified, the first unit for the | ||
543 | 111 | # service (or at least the one with the lowest number) is the | ||
544 | 112 | # one whose data gets returned. | ||
545 | 113 | sorted_unit_names = sorted(units.keys()) | ||
546 | 114 | item = units[sorted_unit_names[0]][item_name] | ||
547 | 115 | return item | ||
548 | 116 | |||
549 | 117 | |||
550 | 118 | # DEPRECATED: client-side only | ||
551 | 119 | def get_machine_data(): | ||
552 | 120 | return yaml.safe_load(juju_status())['machines'] | ||
553 | 121 | |||
554 | 122 | |||
555 | 123 | # DEPRECATED: client-side only | ||
556 | 124 | def wait_for_machine(num_machines=1, timeout=300): | ||
557 | 125 | """Wait `timeout` seconds for `num_machines` machines to come up. | ||
558 | 126 | |||
559 | 127 | This wait_for... function can be called by other wait_for functions | ||
560 | 128 | whose timeouts might be too short in situations where only a bare | ||
561 | 129 | Juju setup has been bootstrapped. | ||
562 | 130 | |||
563 | 131 | :return: A tuple of (num_machines, time_taken). This is used for | ||
564 | 132 | testing. | ||
565 | 133 | """ | ||
566 | 134 | # You may think this is a hack, and you'd be right. The easiest way | ||
567 | 135 | # to tell what environment we're working in (LXC vs EC2) is to check | ||
568 | 136 | # the dns-name of the first machine. If it's localhost we're in LXC | ||
569 | 137 | # and we can just return here. | ||
570 | 138 | if get_machine_data()[0]['dns-name'] == 'localhost': | ||
571 | 139 | return 1, 0 | ||
572 | 140 | start_time = time.time() | ||
573 | 141 | while True: | ||
574 | 142 | # Drop the first machine, since it's the Zookeeper and that's | ||
575 | 143 | # not a machine that we need to wait for. This will only work | ||
576 | 144 | # for EC2 environments, which is why we return early above if | ||
577 | 145 | # we're in LXC. | ||
578 | 146 | machine_data = get_machine_data() | ||
579 | 147 | non_zookeeper_machines = [ | ||
580 | 148 | machine_data[key] for key in list(machine_data.keys())[1:]] | ||
581 | 149 | if len(non_zookeeper_machines) >= num_machines: | ||
582 | 150 | all_machines_running = True | ||
583 | 151 | for machine in non_zookeeper_machines: | ||
584 | 152 | if machine.get('instance-state') != 'running': | ||
585 | 153 | all_machines_running = False | ||
586 | 154 | break | ||
587 | 155 | if all_machines_running: | ||
588 | 156 | break | ||
589 | 157 | if time.time() - start_time >= timeout: | ||
590 | 158 | raise RuntimeError('timeout waiting for service to start') | ||
591 | 159 | time.sleep(SLEEP_AMOUNT) | ||
592 | 160 | return num_machines, time.time() - start_time | ||
593 | 161 | |||
594 | 162 | |||
595 | 163 | # DEPRECATED: client-side only | ||
596 | 164 | def wait_for_unit(service_name, timeout=480): | ||
597 | 165 | """Wait `timeout` seconds for a given service name to come up.""" | ||
598 | 166 | wait_for_machine(num_machines=1) | ||
599 | 167 | start_time = time.time() | ||
600 | 168 | while True: | ||
601 | 169 | state = unit_info(service_name, 'agent-state') | ||
602 | 170 | if 'error' in state or state == 'started': | ||
603 | 171 | break | ||
604 | 172 | if time.time() - start_time >= timeout: | ||
605 | 173 | raise RuntimeError('timeout waiting for service to start') | ||
606 | 174 | time.sleep(SLEEP_AMOUNT) | ||
607 | 175 | if state != 'started': | ||
608 | 176 | raise RuntimeError('unit did not start, agent-state: ' + state) | ||
609 | 177 | |||
610 | 178 | |||
611 | 179 | # DEPRECATED: client-side only | ||
612 | 180 | def wait_for_relation(service_name, relation_name, timeout=120): | ||
613 | 181 | """Wait `timeout` seconds for a given relation to come up.""" | ||
614 | 182 | start_time = time.time() | ||
615 | 183 | while True: | ||
616 | 184 | relation = unit_info(service_name, 'relations').get(relation_name) | ||
617 | 185 | if relation is not None and relation['state'] == 'up': | ||
618 | 186 | break | ||
619 | 187 | if time.time() - start_time >= timeout: | ||
620 | 188 | raise RuntimeError('timeout waiting for relation to be up') | ||
621 | 189 | time.sleep(SLEEP_AMOUNT) | ||
622 | 190 | |||
623 | 191 | |||
624 | 192 | # DEPRECATED: client-side only | ||
625 | 193 | def wait_for_page_contents(url, contents, timeout=120, validate=None): | ||
626 | 194 | if validate is None: | ||
627 | 195 | validate = operator.contains | ||
628 | 196 | start_time = time.time() | ||
629 | 197 | while True: | ||
630 | 198 | try: | ||
631 | 199 | stream = urlopen(url) | ||
632 | 200 | except (HTTPError, URLError): | ||
633 | 201 | pass | ||
634 | 202 | else: | ||
635 | 203 | page = stream.read() | ||
636 | 204 | if validate(page, contents): | ||
637 | 205 | return page | ||
638 | 206 | if time.time() - start_time >= timeout: | ||
639 | 207 | raise RuntimeError('timeout waiting for contents of ' + url) | ||
640 | 208 | time.sleep(SLEEP_AMOUNT) | ||
641 | 209 | 0 | ||
642 | === removed directory 'hooks/charmhelpers/contrib/charmsupport' | |||
643 | === removed file 'hooks/charmhelpers/contrib/charmsupport/__init__.py' | |||
644 | --- hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-07-29 18:07:31 +0000 | |||
645 | +++ hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 | |||
646 | @@ -1,15 +0,0 @@ | |||
647 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
648 | 2 | # | ||
649 | 3 | # This file is part of charm-helpers. | ||
650 | 4 | # | ||
651 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
652 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
653 | 7 | # published by the Free Software Foundation. | ||
654 | 8 | # | ||
655 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
656 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
657 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
658 | 12 | # GNU Lesser General Public License for more details. | ||
659 | 13 | # | ||
660 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
661 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
662 | 16 | 0 | ||
663 | === removed file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' | |||
664 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-01-30 22:40:26 +0000 | |||
665 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 | |||
666 | @@ -1,398 +0,0 @@ | |||
667 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
668 | 2 | # | ||
669 | 3 | # This file is part of charm-helpers. | ||
670 | 4 | # | ||
671 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
672 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
673 | 7 | # published by the Free Software Foundation. | ||
674 | 8 | # | ||
675 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
676 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
677 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
678 | 12 | # GNU Lesser General Public License for more details. | ||
679 | 13 | # | ||
680 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
681 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
682 | 16 | |||
683 | 17 | """Compatibility with the nrpe-external-master charm""" | ||
684 | 18 | # Copyright 2012 Canonical Ltd. | ||
685 | 19 | # | ||
686 | 20 | # Authors: | ||
687 | 21 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
688 | 22 | |||
689 | 23 | import subprocess | ||
690 | 24 | import pwd | ||
691 | 25 | import grp | ||
692 | 26 | import os | ||
693 | 27 | import glob | ||
694 | 28 | import shutil | ||
695 | 29 | import re | ||
696 | 30 | import shlex | ||
697 | 31 | import yaml | ||
698 | 32 | |||
699 | 33 | from charmhelpers.core.hookenv import ( | ||
700 | 34 | config, | ||
701 | 35 | local_unit, | ||
702 | 36 | log, | ||
703 | 37 | relation_ids, | ||
704 | 38 | relation_set, | ||
705 | 39 | relations_of_type, | ||
706 | 40 | ) | ||
707 | 41 | |||
708 | 42 | from charmhelpers.core.host import service | ||
709 | 43 | |||
710 | 44 | # This module adds compatibility with the nrpe-external-master and plain nrpe | ||
711 | 45 | # subordinate charms. To use it in your charm: | ||
712 | 46 | # | ||
713 | 47 | # 1. Update metadata.yaml | ||
714 | 48 | # | ||
715 | 49 | # provides: | ||
716 | 50 | # (...) | ||
717 | 51 | # nrpe-external-master: | ||
718 | 52 | # interface: nrpe-external-master | ||
719 | 53 | # scope: container | ||
720 | 54 | # | ||
721 | 55 | # and/or | ||
722 | 56 | # | ||
723 | 57 | # provides: | ||
724 | 58 | # (...) | ||
725 | 59 | # local-monitors: | ||
726 | 60 | # interface: local-monitors | ||
727 | 61 | # scope: container | ||
728 | 62 | |||
729 | 63 | # | ||
730 | 64 | # 2. Add the following to config.yaml | ||
731 | 65 | # | ||
732 | 66 | # nagios_context: | ||
733 | 67 | # default: "juju" | ||
734 | 68 | # type: string | ||
735 | 69 | # description: | | ||
736 | 70 | # Used by the nrpe subordinate charms. | ||
737 | 71 | # A string that will be prepended to instance name to set the host name | ||
738 | 72 | # in nagios. So for instance the hostname would be something like: | ||
739 | 73 | # juju-myservice-0 | ||
740 | 74 | # If you're running multiple environments with the same services in them | ||
741 | 75 | # this allows you to differentiate between them. | ||
742 | 76 | # nagios_servicegroups: | ||
743 | 77 | # default: "" | ||
744 | 78 | # type: string | ||
745 | 79 | # description: | | ||
746 | 80 | # A comma-separated list of nagios servicegroups. | ||
747 | 81 | # If left empty, the nagios_context will be used as the servicegroup | ||
748 | 82 | # | ||
749 | 83 | # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master | ||
750 | 84 | # | ||
751 | 85 | # 4. Update your hooks.py with something like this: | ||
752 | 86 | # | ||
753 | 87 | # from charmsupport.nrpe import NRPE | ||
754 | 88 | # (...) | ||
755 | 89 | # def update_nrpe_config(): | ||
756 | 90 | # nrpe_compat = NRPE() | ||
757 | 91 | # nrpe_compat.add_check( | ||
758 | 92 | # shortname = "myservice", | ||
759 | 93 | # description = "Check MyService", | ||
760 | 94 | # check_cmd = "check_http -w 2 -c 10 http://localhost" | ||
761 | 95 | # ) | ||
762 | 96 | # nrpe_compat.add_check( | ||
763 | 97 | # "myservice_other", | ||
764 | 98 | # "Check for widget failures", | ||
765 | 99 | # check_cmd = "/srv/myapp/scripts/widget_check" | ||
766 | 100 | # ) | ||
767 | 101 | # nrpe_compat.write() | ||
768 | 102 | # | ||
769 | 103 | # def config_changed(): | ||
770 | 104 | # (...) | ||
771 | 105 | # update_nrpe_config() | ||
772 | 106 | # | ||
773 | 107 | # def nrpe_external_master_relation_changed(): | ||
774 | 108 | # update_nrpe_config() | ||
775 | 109 | # | ||
776 | 110 | # def local_monitors_relation_changed(): | ||
777 | 111 | # update_nrpe_config() | ||
778 | 112 | # | ||
779 | 113 | # 5. ln -s hooks.py nrpe-external-master-relation-changed | ||
780 | 114 | # ln -s hooks.py local-monitors-relation-changed | ||
781 | 115 | |||
782 | 116 | |||
783 | 117 | class CheckException(Exception): | ||
784 | 118 | pass | ||
785 | 119 | |||
786 | 120 | |||
787 | 121 | class Check(object): | ||
788 | 122 | shortname_re = '[A-Za-z0-9-_]+$' | ||
789 | 123 | service_template = (""" | ||
790 | 124 | #--------------------------------------------------- | ||
791 | 125 | # This file is Juju managed | ||
792 | 126 | #--------------------------------------------------- | ||
793 | 127 | define service {{ | ||
794 | 128 | use active-service | ||
795 | 129 | host_name {nagios_hostname} | ||
796 | 130 | service_description {nagios_hostname}[{shortname}] """ | ||
797 | 131 | """{description} | ||
798 | 132 | check_command check_nrpe!{command} | ||
799 | 133 | servicegroups {nagios_servicegroup} | ||
800 | 134 | }} | ||
801 | 135 | """) | ||
802 | 136 | |||
803 | 137 | def __init__(self, shortname, description, check_cmd): | ||
804 | 138 | super(Check, self).__init__() | ||
805 | 139 | # XXX: could be better to calculate this from the service name | ||
806 | 140 | if not re.match(self.shortname_re, shortname): | ||
807 | 141 | raise CheckException("shortname must match {}".format( | ||
808 | 142 | Check.shortname_re)) | ||
809 | 143 | self.shortname = shortname | ||
810 | 144 | self.command = "check_{}".format(shortname) | ||
811 | 145 | # Note: a set of invalid characters is defined by the | ||
812 | 146 | # Nagios server config | ||
813 | 147 | # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= | ||
814 | 148 | self.description = description | ||
815 | 149 | self.check_cmd = self._locate_cmd(check_cmd) | ||
816 | 150 | |||
817 | 151 | def _get_check_filename(self): | ||
818 | 152 | return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command)) | ||
819 | 153 | |||
820 | 154 | def _get_service_filename(self, hostname): | ||
821 | 155 | return os.path.join(NRPE.nagios_exportdir, | ||
822 | 156 | 'service__{}_{}.cfg'.format(hostname, self.command)) | ||
823 | 157 | |||
824 | 158 | def _locate_cmd(self, check_cmd): | ||
825 | 159 | search_path = ( | ||
826 | 160 | '/usr/lib/nagios/plugins', | ||
827 | 161 | '/usr/local/lib/nagios/plugins', | ||
828 | 162 | ) | ||
829 | 163 | parts = shlex.split(check_cmd) | ||
830 | 164 | for path in search_path: | ||
831 | 165 | if os.path.exists(os.path.join(path, parts[0])): | ||
832 | 166 | command = os.path.join(path, parts[0]) | ||
833 | 167 | if len(parts) > 1: | ||
834 | 168 | command += " " + " ".join(parts[1:]) | ||
835 | 169 | return command | ||
836 | 170 | log('Check command not found: {}'.format(parts[0])) | ||
837 | 171 | return '' | ||
838 | 172 | |||
839 | 173 | def _remove_service_files(self): | ||
840 | 174 | if not os.path.exists(NRPE.nagios_exportdir): | ||
841 | 175 | return | ||
842 | 176 | for f in os.listdir(NRPE.nagios_exportdir): | ||
843 | 177 | if f.endswith('_{}.cfg'.format(self.command)): | ||
844 | 178 | os.remove(os.path.join(NRPE.nagios_exportdir, f)) | ||
845 | 179 | |||
846 | 180 | def remove(self, hostname): | ||
847 | 181 | nrpe_check_file = self._get_check_filename() | ||
848 | 182 | if os.path.exists(nrpe_check_file): | ||
849 | 183 | os.remove(nrpe_check_file) | ||
850 | 184 | self._remove_service_files() | ||
851 | 185 | |||
852 | 186 | def write(self, nagios_context, hostname, nagios_servicegroups): | ||
853 | 187 | nrpe_check_file = self._get_check_filename() | ||
854 | 188 | with open(nrpe_check_file, 'w') as nrpe_check_config: | ||
855 | 189 | nrpe_check_config.write("# check {}\n".format(self.shortname)) | ||
856 | 190 | nrpe_check_config.write("command[{}]={}\n".format( | ||
857 | 191 | self.command, self.check_cmd)) | ||
858 | 192 | |||
859 | 193 | if not os.path.exists(NRPE.nagios_exportdir): | ||
860 | 194 | log('Not writing service config as {} is not accessible'.format( | ||
861 | 195 | NRPE.nagios_exportdir)) | ||
862 | 196 | else: | ||
863 | 197 | self.write_service_config(nagios_context, hostname, | ||
864 | 198 | nagios_servicegroups) | ||
865 | 199 | |||
866 | 200 | def write_service_config(self, nagios_context, hostname, | ||
867 | 201 | nagios_servicegroups): | ||
868 | 202 | self._remove_service_files() | ||
869 | 203 | |||
870 | 204 | templ_vars = { | ||
871 | 205 | 'nagios_hostname': hostname, | ||
872 | 206 | 'nagios_servicegroup': nagios_servicegroups, | ||
873 | 207 | 'description': self.description, | ||
874 | 208 | 'shortname': self.shortname, | ||
875 | 209 | 'command': self.command, | ||
876 | 210 | } | ||
877 | 211 | nrpe_service_text = Check.service_template.format(**templ_vars) | ||
878 | 212 | nrpe_service_file = self._get_service_filename(hostname) | ||
879 | 213 | with open(nrpe_service_file, 'w') as nrpe_service_config: | ||
880 | 214 | nrpe_service_config.write(str(nrpe_service_text)) | ||
881 | 215 | |||
882 | 216 | def run(self): | ||
883 | 217 | subprocess.call(self.check_cmd) | ||
884 | 218 | |||
885 | 219 | |||
886 | 220 | class NRPE(object): | ||
887 | 221 | nagios_logdir = '/var/log/nagios' | ||
888 | 222 | nagios_exportdir = '/var/lib/nagios/export' | ||
889 | 223 | nrpe_confdir = '/etc/nagios/nrpe.d' | ||
890 | 224 | |||
891 | 225 | def __init__(self, hostname=None): | ||
892 | 226 | super(NRPE, self).__init__() | ||
893 | 227 | self.config = config() | ||
894 | 228 | self.nagios_context = self.config['nagios_context'] | ||
895 | 229 | if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: | ||
896 | 230 | self.nagios_servicegroups = self.config['nagios_servicegroups'] | ||
897 | 231 | else: | ||
898 | 232 | self.nagios_servicegroups = self.nagios_context | ||
899 | 233 | self.unit_name = local_unit().replace('/', '-') | ||
900 | 234 | if hostname: | ||
901 | 235 | self.hostname = hostname | ||
902 | 236 | else: | ||
903 | 237 | nagios_hostname = get_nagios_hostname() | ||
904 | 238 | if nagios_hostname: | ||
905 | 239 | self.hostname = nagios_hostname | ||
906 | 240 | else: | ||
907 | 241 | self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) | ||
908 | 242 | self.checks = [] | ||
909 | 243 | |||
910 | 244 | def add_check(self, *args, **kwargs): | ||
911 | 245 | self.checks.append(Check(*args, **kwargs)) | ||
912 | 246 | |||
913 | 247 | def remove_check(self, *args, **kwargs): | ||
914 | 248 | if kwargs.get('shortname') is None: | ||
915 | 249 | raise ValueError('shortname of check must be specified') | ||
916 | 250 | |||
917 | 251 | # Use sensible defaults if they're not specified - these are not | ||
918 | 252 | # actually used during removal, but they're required for constructing | ||
919 | 253 | # the Check object; check_disk is chosen because it's part of the | ||
920 | 254 | # nagios-plugins-basic package. | ||
921 | 255 | if kwargs.get('check_cmd') is None: | ||
922 | 256 | kwargs['check_cmd'] = 'check_disk' | ||
923 | 257 | if kwargs.get('description') is None: | ||
924 | 258 | kwargs['description'] = '' | ||
925 | 259 | |||
926 | 260 | check = Check(*args, **kwargs) | ||
927 | 261 | check.remove(self.hostname) | ||
928 | 262 | |||
929 | 263 | def write(self): | ||
930 | 264 | try: | ||
931 | 265 | nagios_uid = pwd.getpwnam('nagios').pw_uid | ||
932 | 266 | nagios_gid = grp.getgrnam('nagios').gr_gid | ||
933 | 267 | except: | ||
934 | 268 | log("Nagios user not set up, nrpe checks not updated") | ||
935 | 269 | return | ||
936 | 270 | |||
937 | 271 | if not os.path.exists(NRPE.nagios_logdir): | ||
938 | 272 | os.mkdir(NRPE.nagios_logdir) | ||
939 | 273 | os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) | ||
940 | 274 | |||
941 | 275 | nrpe_monitors = {} | ||
942 | 276 | monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} | ||
943 | 277 | for nrpecheck in self.checks: | ||
944 | 278 | nrpecheck.write(self.nagios_context, self.hostname, | ||
945 | 279 | self.nagios_servicegroups) | ||
946 | 280 | nrpe_monitors[nrpecheck.shortname] = { | ||
947 | 281 | "command": nrpecheck.command, | ||
948 | 282 | } | ||
949 | 283 | |||
950 | 284 | service('restart', 'nagios-nrpe-server') | ||
951 | 285 | |||
952 | 286 | monitor_ids = relation_ids("local-monitors") + \ | ||
953 | 287 | relation_ids("nrpe-external-master") | ||
954 | 288 | for rid in monitor_ids: | ||
955 | 289 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) | ||
956 | 290 | |||
957 | 291 | |||
958 | 292 | def get_nagios_hostcontext(relation_name='nrpe-external-master'): | ||
959 | 293 | """ | ||
960 | 294 | Query relation with nrpe subordinate, return the nagios_host_context | ||
961 | 295 | |||
962 | 296 | :param str relation_name: Name of relation nrpe sub joined to | ||
963 | 297 | """ | ||
964 | 298 | for rel in relations_of_type(relation_name): | ||
965 | 299 | if 'nagios_host_context' in rel: | ||
966 | 300 | return rel['nagios_host_context'] | ||
967 | 301 | |||
968 | 302 | |||
969 | 303 | def get_nagios_hostname(relation_name='nrpe-external-master'): | ||
970 | 304 | """ | ||
971 | 305 | Query relation with nrpe subordinate, return the nagios_hostname | ||
972 | 306 | |||
973 | 307 | :param str relation_name: Name of relation nrpe sub joined to | ||
974 | 308 | """ | ||
975 | 309 | for rel in relations_of_type(relation_name): | ||
976 | 310 | if 'nagios_hostname' in rel: | ||
977 | 311 | return rel['nagios_hostname'] | ||
978 | 312 | |||
979 | 313 | |||
980 | 314 | def get_nagios_unit_name(relation_name='nrpe-external-master'): | ||
981 | 315 | """ | ||
982 | 316 | Return the nagios unit name prepended with host_context if needed | ||
983 | 317 | |||
984 | 318 | :param str relation_name: Name of relation nrpe sub joined to | ||
985 | 319 | """ | ||
986 | 320 | host_context = get_nagios_hostcontext(relation_name) | ||
987 | 321 | if host_context: | ||
988 | 322 | unit = "%s:%s" % (host_context, local_unit()) | ||
989 | 323 | else: | ||
990 | 324 | unit = local_unit() | ||
991 | 325 | return unit | ||
992 | 326 | |||
993 | 327 | |||
994 | 328 | def add_init_service_checks(nrpe, services, unit_name): | ||
995 | 329 | """ | ||
996 | 330 | Add checks for each service in list | ||
997 | 331 | |||
998 | 332 | :param NRPE nrpe: NRPE object to add check to | ||
999 | 333 | :param list services: List of services to check | ||
1000 | 334 | :param str unit_name: Unit name to use in check description | ||
1001 | 335 | """ | ||
1002 | 336 | for svc in services: | ||
1003 | 337 | upstart_init = '/etc/init/%s.conf' % svc | ||
1004 | 338 | sysv_init = '/etc/init.d/%s' % svc | ||
1005 | 339 | if os.path.exists(upstart_init): | ||
1006 | 340 | # Don't add a check for these services from neutron-gateway | ||
1007 | 341 | if svc not in ['ext-port', 'os-charm-phy-nic-mtu']: | ||
1008 | 342 | nrpe.add_check( | ||
1009 | 343 | shortname=svc, | ||
1010 | 344 | description='process check {%s}' % unit_name, | ||
1011 | 345 | check_cmd='check_upstart_job %s' % svc | ||
1012 | 346 | ) | ||
1013 | 347 | elif os.path.exists(sysv_init): | ||
1014 | 348 | cronpath = '/etc/cron.d/nagios-service-check-%s' % svc | ||
1015 | 349 | cron_file = ('*/5 * * * * root ' | ||
1016 | 350 | '/usr/local/lib/nagios/plugins/check_exit_status.pl ' | ||
1017 | 351 | '-s /etc/init.d/%s status > ' | ||
1018 | 352 | '/var/lib/nagios/service-check-%s.txt\n' % (svc, | ||
1019 | 353 | svc) | ||
1020 | 354 | ) | ||
1021 | 355 | f = open(cronpath, 'w') | ||
1022 | 356 | f.write(cron_file) | ||
1023 | 357 | f.close() | ||
1024 | 358 | nrpe.add_check( | ||
1025 | 359 | shortname=svc, | ||
1026 | 360 | description='process check {%s}' % unit_name, | ||
1027 | 361 | check_cmd='check_status_file.py -f ' | ||
1028 | 362 | '/var/lib/nagios/service-check-%s.txt' % svc, | ||
1029 | 363 | ) | ||
1030 | 364 | |||
1031 | 365 | |||
1032 | 366 | def copy_nrpe_checks(): | ||
1033 | 367 | """ | ||
1034 | 368 | Copy the nrpe checks into place | ||
1035 | 369 | |||
1036 | 370 | """ | ||
1037 | 371 | NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' | ||
1038 | 372 | nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks', | ||
1039 | 373 | 'charmhelpers', 'contrib', 'openstack', | ||
1040 | 374 | 'files') | ||
1041 | 375 | |||
1042 | 376 | if not os.path.exists(NAGIOS_PLUGINS): | ||
1043 | 377 | os.makedirs(NAGIOS_PLUGINS) | ||
1044 | 378 | for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): | ||
1045 | 379 | if os.path.isfile(fname): | ||
1046 | 380 | shutil.copy2(fname, | ||
1047 | 381 | os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) | ||
1048 | 382 | |||
1049 | 383 | |||
1050 | 384 | def add_haproxy_checks(nrpe, unit_name): | ||
1051 | 385 | """ | ||
1052 | 386 | Add checks for each service in list | ||
1053 | 387 | |||
1054 | 388 | :param NRPE nrpe: NRPE object to add check to | ||
1055 | 389 | :param str unit_name: Unit name to use in check description | ||
1056 | 390 | """ | ||
1057 | 391 | nrpe.add_check( | ||
1058 | 392 | shortname='haproxy_servers', | ||
1059 | 393 | description='Check HAProxy {%s}' % unit_name, | ||
1060 | 394 | check_cmd='check_haproxy.sh') | ||
1061 | 395 | nrpe.add_check( | ||
1062 | 396 | shortname='haproxy_queue', | ||
1063 | 397 | description='Check HAProxy queue depth {%s}' % unit_name, | ||
1064 | 398 | check_cmd='check_haproxy_queue_depth.sh') | ||
1065 | 399 | 0 | ||
1066 | === removed file 'hooks/charmhelpers/contrib/charmsupport/volumes.py' | |||
1067 | --- hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-07-29 18:07:31 +0000 | |||
1068 | +++ hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 | |||
1069 | @@ -1,175 +0,0 @@ | |||
1070 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1071 | 2 | # | ||
1072 | 3 | # This file is part of charm-helpers. | ||
1073 | 4 | # | ||
1074 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1075 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1076 | 7 | # published by the Free Software Foundation. | ||
1077 | 8 | # | ||
1078 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1079 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1080 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1081 | 12 | # GNU Lesser General Public License for more details. | ||
1082 | 13 | # | ||
1083 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1084 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1085 | 16 | |||
1086 | 17 | ''' | ||
1087 | 18 | Functions for managing volumes in juju units. One volume is supported per unit. | ||
1088 | 19 | Subordinates may have their own storage, provided it is on its own partition. | ||
1089 | 20 | |||
1090 | 21 | Configuration stanzas:: | ||
1091 | 22 | |||
1092 | 23 | volume-ephemeral: | ||
1093 | 24 | type: boolean | ||
1094 | 25 | default: true | ||
1095 | 26 | description: > | ||
1096 | 27 | If false, a volume is mounted as sepecified in "volume-map" | ||
1097 | 28 | If true, ephemeral storage will be used, meaning that log data | ||
1098 | 29 | will only exist as long as the machine. YOU HAVE BEEN WARNED. | ||
1099 | 30 | volume-map: | ||
1100 | 31 | type: string | ||
1101 | 32 | default: {} | ||
1102 | 33 | description: > | ||
1103 | 34 | YAML map of units to device names, e.g: | ||
1104 | 35 | "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" | ||
1105 | 36 | Service units will raise a configure-error if volume-ephemeral | ||
1106 | 37 | is 'true' and no volume-map value is set. Use 'juju set' to set a | ||
1107 | 38 | value and 'juju resolved' to complete configuration. | ||
1108 | 39 | |||
1109 | 40 | Usage:: | ||
1110 | 41 | |||
1111 | 42 | from charmsupport.volumes import configure_volume, VolumeConfigurationError | ||
1112 | 43 | from charmsupport.hookenv import log, ERROR | ||
1113 | 44 | def post_mount_hook(): | ||
1114 | 45 | stop_service('myservice') | ||
1115 | 46 | def post_mount_hook(): | ||
1116 | 47 | start_service('myservice') | ||
1117 | 48 | |||
1118 | 49 | if __name__ == '__main__': | ||
1119 | 50 | try: | ||
1120 | 51 | configure_volume(before_change=pre_mount_hook, | ||
1121 | 52 | after_change=post_mount_hook) | ||
1122 | 53 | except VolumeConfigurationError: | ||
1123 | 54 | log('Storage could not be configured', ERROR) | ||
1124 | 55 | |||
1125 | 56 | ''' | ||
1126 | 57 | |||
1127 | 58 | # XXX: Known limitations | ||
1128 | 59 | # - fstab is neither consulted nor updated | ||
1129 | 60 | |||
1130 | 61 | import os | ||
1131 | 62 | from charmhelpers.core import hookenv | ||
1132 | 63 | from charmhelpers.core import host | ||
1133 | 64 | import yaml | ||
1134 | 65 | |||
1135 | 66 | |||
1136 | 67 | MOUNT_BASE = '/srv/juju/volumes' | ||
1137 | 68 | |||
1138 | 69 | |||
1139 | 70 | class VolumeConfigurationError(Exception): | ||
1140 | 71 | '''Volume configuration data is missing or invalid''' | ||
1141 | 72 | pass | ||
1142 | 73 | |||
1143 | 74 | |||
1144 | 75 | def get_config(): | ||
1145 | 76 | '''Gather and sanity-check volume configuration data''' | ||
1146 | 77 | volume_config = {} | ||
1147 | 78 | config = hookenv.config() | ||
1148 | 79 | |||
1149 | 80 | errors = False | ||
1150 | 81 | |||
1151 | 82 | if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): | ||
1152 | 83 | volume_config['ephemeral'] = True | ||
1153 | 84 | else: | ||
1154 | 85 | volume_config['ephemeral'] = False | ||
1155 | 86 | |||
1156 | 87 | try: | ||
1157 | 88 | volume_map = yaml.safe_load(config.get('volume-map', '{}')) | ||
1158 | 89 | except yaml.YAMLError as e: | ||
1159 | 90 | hookenv.log("Error parsing YAML volume-map: {}".format(e), | ||
1160 | 91 | hookenv.ERROR) | ||
1161 | 92 | errors = True | ||
1162 | 93 | if volume_map is None: | ||
1163 | 94 | # probably an empty string | ||
1164 | 95 | volume_map = {} | ||
1165 | 96 | elif not isinstance(volume_map, dict): | ||
1166 | 97 | hookenv.log("Volume-map should be a dictionary, not {}".format( | ||
1167 | 98 | type(volume_map))) | ||
1168 | 99 | errors = True | ||
1169 | 100 | |||
1170 | 101 | volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) | ||
1171 | 102 | if volume_config['device'] and volume_config['ephemeral']: | ||
1172 | 103 | # asked for ephemeral storage but also defined a volume ID | ||
1173 | 104 | hookenv.log('A volume is defined for this unit, but ephemeral ' | ||
1174 | 105 | 'storage was requested', hookenv.ERROR) | ||
1175 | 106 | errors = True | ||
1176 | 107 | elif not volume_config['device'] and not volume_config['ephemeral']: | ||
1177 | 108 | # asked for permanent storage but did not define volume ID | ||
1178 | 109 | hookenv.log('Ephemeral storage was requested, but there is no volume ' | ||
1179 | 110 | 'defined for this unit.', hookenv.ERROR) | ||
1180 | 111 | errors = True | ||
1181 | 112 | |||
1182 | 113 | unit_mount_name = hookenv.local_unit().replace('/', '-') | ||
1183 | 114 | volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) | ||
1184 | 115 | |||
1185 | 116 | if errors: | ||
1186 | 117 | return None | ||
1187 | 118 | return volume_config | ||
1188 | 119 | |||
1189 | 120 | |||
1190 | 121 | def mount_volume(config): | ||
1191 | 122 | if os.path.exists(config['mountpoint']): | ||
1192 | 123 | if not os.path.isdir(config['mountpoint']): | ||
1193 | 124 | hookenv.log('Not a directory: {}'.format(config['mountpoint'])) | ||
1194 | 125 | raise VolumeConfigurationError() | ||
1195 | 126 | else: | ||
1196 | 127 | host.mkdir(config['mountpoint']) | ||
1197 | 128 | if os.path.ismount(config['mountpoint']): | ||
1198 | 129 | unmount_volume(config) | ||
1199 | 130 | if not host.mount(config['device'], config['mountpoint'], persist=True): | ||
1200 | 131 | raise VolumeConfigurationError() | ||
1201 | 132 | |||
1202 | 133 | |||
1203 | 134 | def unmount_volume(config): | ||
1204 | 135 | if os.path.ismount(config['mountpoint']): | ||
1205 | 136 | if not host.umount(config['mountpoint'], persist=True): | ||
1206 | 137 | raise VolumeConfigurationError() | ||
1207 | 138 | |||
1208 | 139 | |||
1209 | 140 | def managed_mounts(): | ||
1210 | 141 | '''List of all mounted managed volumes''' | ||
1211 | 142 | return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) | ||
1212 | 143 | |||
1213 | 144 | |||
1214 | 145 | def configure_volume(before_change=lambda: None, after_change=lambda: None): | ||
1215 | 146 | '''Set up storage (or don't) according to the charm's volume configuration. | ||
1216 | 147 | Returns the mount point or "ephemeral". before_change and after_change | ||
1217 | 148 | are optional functions to be called if the volume configuration changes. | ||
1218 | 149 | ''' | ||
1219 | 150 | |||
1220 | 151 | config = get_config() | ||
1221 | 152 | if not config: | ||
1222 | 153 | hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) | ||
1223 | 154 | raise VolumeConfigurationError() | ||
1224 | 155 | |||
1225 | 156 | if config['ephemeral']: | ||
1226 | 157 | if os.path.ismount(config['mountpoint']): | ||
1227 | 158 | before_change() | ||
1228 | 159 | unmount_volume(config) | ||
1229 | 160 | after_change() | ||
1230 | 161 | return 'ephemeral' | ||
1231 | 162 | else: | ||
1232 | 163 | # persistent storage | ||
1233 | 164 | if os.path.ismount(config['mountpoint']): | ||
1234 | 165 | mounts = dict(managed_mounts()) | ||
1235 | 166 | if mounts.get(config['mountpoint']) != config['device']: | ||
1236 | 167 | before_change() | ||
1237 | 168 | unmount_volume(config) | ||
1238 | 169 | mount_volume(config) | ||
1239 | 170 | after_change() | ||
1240 | 171 | else: | ||
1241 | 172 | before_change() | ||
1242 | 173 | mount_volume(config) | ||
1243 | 174 | after_change() | ||
1244 | 175 | return config['mountpoint'] | ||
1245 | 176 | 0 | ||
1246 | === removed directory 'hooks/charmhelpers/contrib/database' | |||
1247 | === removed file 'hooks/charmhelpers/contrib/database/__init__.py' | |||
1248 | === removed file 'hooks/charmhelpers/contrib/database/mysql.py' | |||
1249 | --- hooks/charmhelpers/contrib/database/mysql.py 2015-07-29 18:07:31 +0000 | |||
1250 | +++ hooks/charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000 | |||
1251 | @@ -1,412 +0,0 @@ | |||
1252 | 1 | """Helper for working with a MySQL database""" | ||
1253 | 2 | import json | ||
1254 | 3 | import re | ||
1255 | 4 | import sys | ||
1256 | 5 | import platform | ||
1257 | 6 | import os | ||
1258 | 7 | import glob | ||
1259 | 8 | |||
1260 | 9 | # from string import upper | ||
1261 | 10 | |||
1262 | 11 | from charmhelpers.core.host import ( | ||
1263 | 12 | mkdir, | ||
1264 | 13 | pwgen, | ||
1265 | 14 | write_file | ||
1266 | 15 | ) | ||
1267 | 16 | from charmhelpers.core.hookenv import ( | ||
1268 | 17 | config as config_get, | ||
1269 | 18 | relation_get, | ||
1270 | 19 | related_units, | ||
1271 | 20 | unit_get, | ||
1272 | 21 | log, | ||
1273 | 22 | DEBUG, | ||
1274 | 23 | INFO, | ||
1275 | 24 | WARNING, | ||
1276 | 25 | ) | ||
1277 | 26 | from charmhelpers.fetch import ( | ||
1278 | 27 | apt_install, | ||
1279 | 28 | apt_update, | ||
1280 | 29 | filter_installed_packages, | ||
1281 | 30 | ) | ||
1282 | 31 | from charmhelpers.contrib.peerstorage import ( | ||
1283 | 32 | peer_store, | ||
1284 | 33 | peer_retrieve, | ||
1285 | 34 | ) | ||
1286 | 35 | from charmhelpers.contrib.network.ip import get_host_ip | ||
1287 | 36 | |||
1288 | 37 | try: | ||
1289 | 38 | import MySQLdb | ||
1290 | 39 | except ImportError: | ||
1291 | 40 | apt_update(fatal=True) | ||
1292 | 41 | apt_install(filter_installed_packages(['python-mysqldb']), fatal=True) | ||
1293 | 42 | import MySQLdb | ||
1294 | 43 | |||
1295 | 44 | |||
1296 | 45 | class MySQLHelper(object): | ||
1297 | 46 | |||
1298 | 47 | def __init__(self, rpasswdf_template, upasswdf_template, host='localhost', | ||
1299 | 48 | migrate_passwd_to_peer_relation=True, | ||
1300 | 49 | delete_ondisk_passwd_file=True): | ||
1301 | 50 | self.host = host | ||
1302 | 51 | # Password file path templates | ||
1303 | 52 | self.root_passwd_file_template = rpasswdf_template | ||
1304 | 53 | self.user_passwd_file_template = upasswdf_template | ||
1305 | 54 | |||
1306 | 55 | self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation | ||
1307 | 56 | # If we migrate we have the option to delete local copy of root passwd | ||
1308 | 57 | self.delete_ondisk_passwd_file = delete_ondisk_passwd_file | ||
1309 | 58 | |||
1310 | 59 | def connect(self, user='root', password=None): | ||
1311 | 60 | log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG) | ||
1312 | 61 | self.connection = MySQLdb.connect(user=user, host=self.host, | ||
1313 | 62 | passwd=password) | ||
1314 | 63 | |||
1315 | 64 | def database_exists(self, db_name): | ||
1316 | 65 | cursor = self.connection.cursor() | ||
1317 | 66 | try: | ||
1318 | 67 | cursor.execute("SHOW DATABASES") | ||
1319 | 68 | databases = [i[0] for i in cursor.fetchall()] | ||
1320 | 69 | finally: | ||
1321 | 70 | cursor.close() | ||
1322 | 71 | |||
1323 | 72 | return db_name in databases | ||
1324 | 73 | |||
1325 | 74 | def create_database(self, db_name): | ||
1326 | 75 | cursor = self.connection.cursor() | ||
1327 | 76 | try: | ||
1328 | 77 | cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8" | ||
1329 | 78 | .format(db_name)) | ||
1330 | 79 | finally: | ||
1331 | 80 | cursor.close() | ||
1332 | 81 | |||
1333 | 82 | def grant_exists(self, db_name, db_user, remote_ip): | ||
1334 | 83 | cursor = self.connection.cursor() | ||
1335 | 84 | priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \ | ||
1336 | 85 | "TO '{}'@'{}'".format(db_name, db_user, remote_ip) | ||
1337 | 86 | try: | ||
1338 | 87 | cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user, | ||
1339 | 88 | remote_ip)) | ||
1340 | 89 | grants = [i[0] for i in cursor.fetchall()] | ||
1341 | 90 | except MySQLdb.OperationalError: | ||
1342 | 91 | return False | ||
1343 | 92 | finally: | ||
1344 | 93 | cursor.close() | ||
1345 | 94 | |||
1346 | 95 | # TODO: review for different grants | ||
1347 | 96 | return priv_string in grants | ||
1348 | 97 | |||
1349 | 98 | def create_grant(self, db_name, db_user, remote_ip, password): | ||
1350 | 99 | cursor = self.connection.cursor() | ||
1351 | 100 | try: | ||
1352 | 101 | # TODO: review for different grants | ||
1353 | 102 | cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' " | ||
1354 | 103 | "IDENTIFIED BY '{}'".format(db_name, | ||
1355 | 104 | db_user, | ||
1356 | 105 | remote_ip, | ||
1357 | 106 | password)) | ||
1358 | 107 | finally: | ||
1359 | 108 | cursor.close() | ||
1360 | 109 | |||
1361 | 110 | def create_admin_grant(self, db_user, remote_ip, password): | ||
1362 | 111 | cursor = self.connection.cursor() | ||
1363 | 112 | try: | ||
1364 | 113 | cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' " | ||
1365 | 114 | "IDENTIFIED BY '{}'".format(db_user, | ||
1366 | 115 | remote_ip, | ||
1367 | 116 | password)) | ||
1368 | 117 | finally: | ||
1369 | 118 | cursor.close() | ||
1370 | 119 | |||
1371 | 120 | def cleanup_grant(self, db_user, remote_ip): | ||
1372 | 121 | cursor = self.connection.cursor() | ||
1373 | 122 | try: | ||
1374 | 123 | cursor.execute("DROP FROM mysql.user WHERE user='{}' " | ||
1375 | 124 | "AND HOST='{}'".format(db_user, | ||
1376 | 125 | remote_ip)) | ||
1377 | 126 | finally: | ||
1378 | 127 | cursor.close() | ||
1379 | 128 | |||
1380 | 129 | def execute(self, sql): | ||
1381 | 130 | """Execute arbitary SQL against the database.""" | ||
1382 | 131 | cursor = self.connection.cursor() | ||
1383 | 132 | try: | ||
1384 | 133 | cursor.execute(sql) | ||
1385 | 134 | finally: | ||
1386 | 135 | cursor.close() | ||
1387 | 136 | |||
1388 | 137 | def migrate_passwords_to_peer_relation(self, excludes=None): | ||
1389 | 138 | """Migrate any passwords storage on disk to cluster peer relation.""" | ||
1390 | 139 | dirname = os.path.dirname(self.root_passwd_file_template) | ||
1391 | 140 | path = os.path.join(dirname, '*.passwd') | ||
1392 | 141 | for f in glob.glob(path): | ||
1393 | 142 | if excludes and f in excludes: | ||
1394 | 143 | log("Excluding %s from peer migration" % (f), level=DEBUG) | ||
1395 | 144 | continue | ||
1396 | 145 | |||
1397 | 146 | key = os.path.basename(f) | ||
1398 | 147 | with open(f, 'r') as passwd: | ||
1399 | 148 | _value = passwd.read().strip() | ||
1400 | 149 | |||
1401 | 150 | try: | ||
1402 | 151 | peer_store(key, _value) | ||
1403 | 152 | |||
1404 | 153 | if self.delete_ondisk_passwd_file: | ||
1405 | 154 | os.unlink(f) | ||
1406 | 155 | except ValueError: | ||
1407 | 156 | # NOTE cluster relation not yet ready - skip for now | ||
1408 | 157 | pass | ||
1409 | 158 | |||
1410 | 159 | def get_mysql_password_on_disk(self, username=None, password=None): | ||
1411 | 160 | """Retrieve, generate or store a mysql password for the provided | ||
1412 | 161 | username on disk.""" | ||
1413 | 162 | if username: | ||
1414 | 163 | template = self.user_passwd_file_template | ||
1415 | 164 | passwd_file = template.format(username) | ||
1416 | 165 | else: | ||
1417 | 166 | passwd_file = self.root_passwd_file_template | ||
1418 | 167 | |||
1419 | 168 | _password = None | ||
1420 | 169 | if os.path.exists(passwd_file): | ||
1421 | 170 | log("Using existing password file '%s'" % passwd_file, level=DEBUG) | ||
1422 | 171 | with open(passwd_file, 'r') as passwd: | ||
1423 | 172 | _password = passwd.read().strip() | ||
1424 | 173 | else: | ||
1425 | 174 | log("Generating new password file '%s'" % passwd_file, level=DEBUG) | ||
1426 | 175 | if not os.path.isdir(os.path.dirname(passwd_file)): | ||
1427 | 176 | # NOTE: need to ensure this is not mysql root dir (which needs | ||
1428 | 177 | # to be mysql readable) | ||
1429 | 178 | mkdir(os.path.dirname(passwd_file), owner='root', group='root', | ||
1430 | 179 | perms=0o770) | ||
1431 | 180 | # Force permissions - for some reason the chmod in makedirs | ||
1432 | 181 | # fails | ||
1433 | 182 | os.chmod(os.path.dirname(passwd_file), 0o770) | ||
1434 | 183 | |||
1435 | 184 | _password = password or pwgen(length=32) | ||
1436 | 185 | write_file(passwd_file, _password, owner='root', group='root', | ||
1437 | 186 | perms=0o660) | ||
1438 | 187 | |||
1439 | 188 | return _password | ||
1440 | 189 | |||
1441 | 190 | def passwd_keys(self, username): | ||
1442 | 191 | """Generator to return keys used to store passwords in peer store. | ||
1443 | 192 | |||
1444 | 193 | NOTE: we support both legacy and new format to support mysql | ||
1445 | 194 | charm prior to refactor. This is necessary to avoid LP 1451890. | ||
1446 | 195 | """ | ||
1447 | 196 | keys = [] | ||
1448 | 197 | if username == 'mysql': | ||
1449 | 198 | log("Bad username '%s'" % (username), level=WARNING) | ||
1450 | 199 | |||
1451 | 200 | if username: | ||
1452 | 201 | # IMPORTANT: *newer* format must be returned first | ||
1453 | 202 | keys.append('mysql-%s.passwd' % (username)) | ||
1454 | 203 | keys.append('%s.passwd' % (username)) | ||
1455 | 204 | else: | ||
1456 | 205 | keys.append('mysql.passwd') | ||
1457 | 206 | |||
1458 | 207 | for key in keys: | ||
1459 | 208 | yield key | ||
1460 | 209 | |||
1461 | 210 | def get_mysql_password(self, username=None, password=None): | ||
1462 | 211 | """Retrieve, generate or store a mysql password for the provided | ||
1463 | 212 | username using peer relation cluster.""" | ||
1464 | 213 | excludes = [] | ||
1465 | 214 | |||
1466 | 215 | # First check peer relation. | ||
1467 | 216 | try: | ||
1468 | 217 | for key in self.passwd_keys(username): | ||
1469 | 218 | _password = peer_retrieve(key) | ||
1470 | 219 | if _password: | ||
1471 | 220 | break | ||
1472 | 221 | |||
1473 | 222 | # If root password available don't update peer relation from local | ||
1474 | 223 | if _password and not username: | ||
1475 | 224 | excludes.append(self.root_passwd_file_template) | ||
1476 | 225 | |||
1477 | 226 | except ValueError: | ||
1478 | 227 | # cluster relation is not yet started; use on-disk | ||
1479 | 228 | _password = None | ||
1480 | 229 | |||
1481 | 230 | # If none available, generate new one | ||
1482 | 231 | if not _password: | ||
1483 | 232 | _password = self.get_mysql_password_on_disk(username, password) | ||
1484 | 233 | |||
1485 | 234 | # Put on wire if required | ||
1486 | 235 | if self.migrate_passwd_to_peer_relation: | ||
1487 | 236 | self.migrate_passwords_to_peer_relation(excludes=excludes) | ||
1488 | 237 | |||
1489 | 238 | return _password | ||
1490 | 239 | |||
1491 | 240 | def get_mysql_root_password(self, password=None): | ||
1492 | 241 | """Retrieve or generate mysql root password for service units.""" | ||
1493 | 242 | return self.get_mysql_password(username=None, password=password) | ||
1494 | 243 | |||
1495 | 244 | def normalize_address(self, hostname): | ||
1496 | 245 | """Ensure that address returned is an IP address (i.e. not fqdn)""" | ||
1497 | 246 | if config_get('prefer-ipv6'): | ||
1498 | 247 | # TODO: add support for ipv6 dns | ||
1499 | 248 | return hostname | ||
1500 | 249 | |||
1501 | 250 | if hostname != unit_get('private-address'): | ||
1502 | 251 | return get_host_ip(hostname, fallback=hostname) | ||
1503 | 252 | |||
1504 | 253 | # Otherwise assume localhost | ||
1505 | 254 | return '127.0.0.1' | ||
1506 | 255 | |||
1507 | 256 | def get_allowed_units(self, database, username, relation_id=None): | ||
1508 | 257 | """Get list of units with access grants for database with username. | ||
1509 | 258 | |||
1510 | 259 | This is typically used to provide shared-db relations with a list of | ||
1511 | 260 | which units have been granted access to the given database. | ||
1512 | 261 | """ | ||
1513 | 262 | self.connect(password=self.get_mysql_root_password()) | ||
1514 | 263 | allowed_units = set() | ||
1515 | 264 | for unit in related_units(relation_id): | ||
1516 | 265 | settings = relation_get(rid=relation_id, unit=unit) | ||
1517 | 266 | # First check for setting with prefix, then without | ||
1518 | 267 | for attr in ["%s_hostname" % (database), 'hostname']: | ||
1519 | 268 | hosts = settings.get(attr, None) | ||
1520 | 269 | if hosts: | ||
1521 | 270 | break | ||
1522 | 271 | |||
1523 | 272 | if hosts: | ||
1524 | 273 | # hostname can be json-encoded list of hostnames | ||
1525 | 274 | try: | ||
1526 | 275 | hosts = json.loads(hosts) | ||
1527 | 276 | except ValueError: | ||
1528 | 277 | hosts = [hosts] | ||
1529 | 278 | else: | ||
1530 | 279 | hosts = [settings['private-address']] | ||
1531 | 280 | |||
1532 | 281 | if hosts: | ||
1533 | 282 | for host in hosts: | ||
1534 | 283 | host = self.normalize_address(host) | ||
1535 | 284 | if self.grant_exists(database, username, host): | ||
1536 | 285 | log("Grant exists for host '%s' on db '%s'" % | ||
1537 | 286 | (host, database), level=DEBUG) | ||
1538 | 287 | if unit not in allowed_units: | ||
1539 | 288 | allowed_units.add(unit) | ||
1540 | 289 | else: | ||
1541 | 290 | log("Grant does NOT exist for host '%s' on db '%s'" % | ||
1542 | 291 | (host, database), level=DEBUG) | ||
1543 | 292 | else: | ||
1544 | 293 | log("No hosts found for grant check", level=INFO) | ||
1545 | 294 | |||
1546 | 295 | return allowed_units | ||
1547 | 296 | |||
1548 | 297 | def configure_db(self, hostname, database, username, admin=False): | ||
1549 | 298 | """Configure access to database for username from hostname.""" | ||
1550 | 299 | self.connect(password=self.get_mysql_root_password()) | ||
1551 | 300 | if not self.database_exists(database): | ||
1552 | 301 | self.create_database(database) | ||
1553 | 302 | |||
1554 | 303 | remote_ip = self.normalize_address(hostname) | ||
1555 | 304 | password = self.get_mysql_password(username) | ||
1556 | 305 | if not self.grant_exists(database, username, remote_ip): | ||
1557 | 306 | if not admin: | ||
1558 | 307 | self.create_grant(database, username, remote_ip, password) | ||
1559 | 308 | else: | ||
1560 | 309 | self.create_admin_grant(username, remote_ip, password) | ||
1561 | 310 | |||
1562 | 311 | return password | ||
1563 | 312 | |||
1564 | 313 | |||
1565 | 314 | class PerconaClusterHelper(object): | ||
1566 | 315 | |||
1567 | 316 | # Going for the biggest page size to avoid wasted bytes. | ||
1568 | 317 | # InnoDB page size is 16MB | ||
1569 | 318 | |||
1570 | 319 | DEFAULT_PAGE_SIZE = 16 * 1024 * 1024 | ||
1571 | 320 | DEFAULT_INNODB_BUFFER_FACTOR = 0.50 | ||
1572 | 321 | |||
1573 | 322 | def human_to_bytes(self, human): | ||
1574 | 323 | """Convert human readable configuration options to bytes.""" | ||
1575 | 324 | num_re = re.compile('^[0-9]+$') | ||
1576 | 325 | if num_re.match(human): | ||
1577 | 326 | return human | ||
1578 | 327 | |||
1579 | 328 | factors = { | ||
1580 | 329 | 'K': 1024, | ||
1581 | 330 | 'M': 1048576, | ||
1582 | 331 | 'G': 1073741824, | ||
1583 | 332 | 'T': 1099511627776 | ||
1584 | 333 | } | ||
1585 | 334 | modifier = human[-1] | ||
1586 | 335 | if modifier in factors: | ||
1587 | 336 | return int(human[:-1]) * factors[modifier] | ||
1588 | 337 | |||
1589 | 338 | if modifier == '%': | ||
1590 | 339 | total_ram = self.human_to_bytes(self.get_mem_total()) | ||
1591 | 340 | if self.is_32bit_system() and total_ram > self.sys_mem_limit(): | ||
1592 | 341 | total_ram = self.sys_mem_limit() | ||
1593 | 342 | factor = int(human[:-1]) * 0.01 | ||
1594 | 343 | pctram = total_ram * factor | ||
1595 | 344 | return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE)) | ||
1596 | 345 | |||
1597 | 346 | raise ValueError("Can only convert K,M,G, or T") | ||
1598 | 347 | |||
1599 | 348 | def is_32bit_system(self): | ||
1600 | 349 | """Determine whether system is 32 or 64 bit.""" | ||
1601 | 350 | try: | ||
1602 | 351 | return sys.maxsize < 2 ** 32 | ||
1603 | 352 | except OverflowError: | ||
1604 | 353 | return False | ||
1605 | 354 | |||
1606 | 355 | def sys_mem_limit(self): | ||
1607 | 356 | """Determine the default memory limit for the current service unit.""" | ||
1608 | 357 | if platform.machine() in ['armv7l']: | ||
1609 | 358 | _mem_limit = self.human_to_bytes('2700M') # experimentally determined | ||
1610 | 359 | else: | ||
1611 | 360 | # Limit for x86 based 32bit systems | ||
1612 | 361 | _mem_limit = self.human_to_bytes('4G') | ||
1613 | 362 | |||
1614 | 363 | return _mem_limit | ||
1615 | 364 | |||
1616 | 365 | def get_mem_total(self): | ||
1617 | 366 | """Calculate the total memory in the current service unit.""" | ||
1618 | 367 | with open('/proc/meminfo') as meminfo_file: | ||
1619 | 368 | for line in meminfo_file: | ||
1620 | 369 | key, mem = line.split(':', 2) | ||
1621 | 370 | if key == 'MemTotal': | ||
1622 | 371 | mtot, modifier = mem.strip().split(' ') | ||
1623 | 372 | return '%s%s' % (mtot, modifier[0].upper()) | ||
1624 | 373 | |||
1625 | 374 | def parse_config(self): | ||
1626 | 375 | """Parse charm configuration and calculate values for config files.""" | ||
1627 | 376 | config = config_get() | ||
1628 | 377 | mysql_config = {} | ||
1629 | 378 | if 'max-connections' in config: | ||
1630 | 379 | mysql_config['max_connections'] = config['max-connections'] | ||
1631 | 380 | |||
1632 | 381 | if 'wait-timeout' in config: | ||
1633 | 382 | mysql_config['wait_timeout'] = config['wait-timeout'] | ||
1634 | 383 | |||
1635 | 384 | if 'innodb-flush-log-at-trx-commit' in config: | ||
1636 | 385 | mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit'] | ||
1637 | 386 | |||
1638 | 387 | # Set a sane default key_buffer size | ||
1639 | 388 | mysql_config['key_buffer'] = self.human_to_bytes('32M') | ||
1640 | 389 | total_memory = self.human_to_bytes(self.get_mem_total()) | ||
1641 | 390 | |||
1642 | 391 | dataset_bytes = config.get('dataset-size', None) | ||
1643 | 392 | innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None) | ||
1644 | 393 | |||
1645 | 394 | if innodb_buffer_pool_size: | ||
1646 | 395 | innodb_buffer_pool_size = self.human_to_bytes( | ||
1647 | 396 | innodb_buffer_pool_size) | ||
1648 | 397 | elif dataset_bytes: | ||
1649 | 398 | log("Option 'dataset-size' has been deprecated, please use" | ||
1650 | 399 | "innodb_buffer_pool_size option instead", level="WARN") | ||
1651 | 400 | innodb_buffer_pool_size = self.human_to_bytes( | ||
1652 | 401 | dataset_bytes) | ||
1653 | 402 | else: | ||
1654 | 403 | innodb_buffer_pool_size = int( | ||
1655 | 404 | total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR) | ||
1656 | 405 | |||
1657 | 406 | if innodb_buffer_pool_size > total_memory: | ||
1658 | 407 | log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format( | ||
1659 | 408 | innodb_buffer_pool_size, | ||
1660 | 409 | total_memory), level='WARN') | ||
1661 | 410 | |||
1662 | 411 | mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size | ||
1663 | 412 | return mysql_config | ||
1664 | 413 | 0 | ||
1665 | === removed directory 'hooks/charmhelpers/contrib/hardening' | |||
1666 | === removed file 'hooks/charmhelpers/contrib/hardening/__init__.py' | |||
1667 | --- hooks/charmhelpers/contrib/hardening/__init__.py 2016-04-22 04:53:43 +0000 | |||
1668 | +++ hooks/charmhelpers/contrib/hardening/__init__.py 1970-01-01 00:00:00 +0000 | |||
1669 | @@ -1,15 +0,0 @@ | |||
1670 | 1 | # Copyright 2016 Canonical Limited. | ||
1671 | 2 | # | ||
1672 | 3 | # This file is part of charm-helpers. | ||
1673 | 4 | # | ||
1674 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1675 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1676 | 7 | # published by the Free Software Foundation. | ||
1677 | 8 | # | ||
1678 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1679 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1680 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1681 | 12 | # GNU Lesser General Public License for more details. | ||
1682 | 13 | # | ||
1683 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1684 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1685 | 16 | 0 | ||
1686 | === removed directory 'hooks/charmhelpers/contrib/hardening/apache' | |||
1687 | === removed file 'hooks/charmhelpers/contrib/hardening/apache/__init__.py' | |||
1688 | --- hooks/charmhelpers/contrib/hardening/apache/__init__.py 2016-04-22 04:53:43 +0000 | |||
1689 | +++ hooks/charmhelpers/contrib/hardening/apache/__init__.py 1970-01-01 00:00:00 +0000 | |||
1690 | @@ -1,19 +0,0 @@ | |||
1691 | 1 | # Copyright 2016 Canonical Limited. | ||
1692 | 2 | # | ||
1693 | 3 | # This file is part of charm-helpers. | ||
1694 | 4 | # | ||
1695 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1696 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1697 | 7 | # published by the Free Software Foundation. | ||
1698 | 8 | # | ||
1699 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1700 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1701 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1702 | 12 | # GNU Lesser General Public License for more details. | ||
1703 | 13 | # | ||
1704 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1705 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1706 | 16 | |||
1707 | 17 | from os import path | ||
1708 | 18 | |||
1709 | 19 | TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates') | ||
1710 | 20 | 0 | ||
1711 | === removed directory 'hooks/charmhelpers/contrib/hardening/apache/checks' | |||
1712 | === removed file 'hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py' | |||
1713 | --- hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 2016-04-22 04:53:43 +0000 | |||
1714 | +++ hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 1970-01-01 00:00:00 +0000 | |||
1715 | @@ -1,31 +0,0 @@ | |||
1716 | 1 | # Copyright 2016 Canonical Limited. | ||
1717 | 2 | # | ||
1718 | 3 | # This file is part of charm-helpers. | ||
1719 | 4 | # | ||
1720 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1721 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1722 | 7 | # published by the Free Software Foundation. | ||
1723 | 8 | # | ||
1724 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1725 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1726 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1727 | 12 | # GNU Lesser General Public License for more details. | ||
1728 | 13 | # | ||
1729 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1730 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1731 | 16 | |||
1732 | 17 | from charmhelpers.core.hookenv import ( | ||
1733 | 18 | log, | ||
1734 | 19 | DEBUG, | ||
1735 | 20 | ) | ||
1736 | 21 | from charmhelpers.contrib.hardening.apache.checks import config | ||
1737 | 22 | |||
1738 | 23 | |||
1739 | 24 | def run_apache_checks(): | ||
1740 | 25 | log("Starting Apache hardening checks.", level=DEBUG) | ||
1741 | 26 | checks = config.get_audits() | ||
1742 | 27 | for check in checks: | ||
1743 | 28 | log("Running '%s' check" % (check.__class__.__name__), level=DEBUG) | ||
1744 | 29 | check.ensure_compliance() | ||
1745 | 30 | |||
1746 | 31 | log("Apache hardening checks complete.", level=DEBUG) | ||
1747 | 32 | 0 | ||
1748 | === removed file 'hooks/charmhelpers/contrib/hardening/apache/checks/config.py' | |||
1749 | --- hooks/charmhelpers/contrib/hardening/apache/checks/config.py 2016-04-22 04:53:43 +0000 | |||
1750 | +++ hooks/charmhelpers/contrib/hardening/apache/checks/config.py 1970-01-01 00:00:00 +0000 | |||
1751 | @@ -1,100 +0,0 @@ | |||
1752 | 1 | # Copyright 2016 Canonical Limited. | ||
1753 | 2 | # | ||
1754 | 3 | # This file is part of charm-helpers. | ||
1755 | 4 | # | ||
1756 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1757 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1758 | 7 | # published by the Free Software Foundation. | ||
1759 | 8 | # | ||
1760 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1761 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1762 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1763 | 12 | # GNU Lesser General Public License for more details. | ||
1764 | 13 | # | ||
1765 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1766 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1767 | 16 | |||
1768 | 17 | import os | ||
1769 | 18 | import re | ||
1770 | 19 | import subprocess | ||
1771 | 20 | |||
1772 | 21 | |||
1773 | 22 | from charmhelpers.core.hookenv import ( | ||
1774 | 23 | log, | ||
1775 | 24 | INFO, | ||
1776 | 25 | ) | ||
1777 | 26 | from charmhelpers.contrib.hardening.audits.file import ( | ||
1778 | 27 | FilePermissionAudit, | ||
1779 | 28 | DirectoryPermissionAudit, | ||
1780 | 29 | NoReadWriteForOther, | ||
1781 | 30 | TemplatedFile, | ||
1782 | 31 | ) | ||
1783 | 32 | from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit | ||
1784 | 33 | from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR | ||
1785 | 34 | from charmhelpers.contrib.hardening import utils | ||
1786 | 35 | |||
1787 | 36 | |||
1788 | 37 | def get_audits(): | ||
1789 | 38 | """Get Apache hardening config audits. | ||
1790 | 39 | |||
1791 | 40 | :returns: dictionary of audits | ||
1792 | 41 | """ | ||
1793 | 42 | if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0: | ||
1794 | 43 | log("Apache server does not appear to be installed on this node - " | ||
1795 | 44 | "skipping apache hardening", level=INFO) | ||
1796 | 45 | return [] | ||
1797 | 46 | |||
1798 | 47 | context = ApacheConfContext() | ||
1799 | 48 | settings = utils.get_settings('apache') | ||
1800 | 49 | audits = [ | ||
1801 | 50 | FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root', | ||
1802 | 51 | group='root', mode=0o0640), | ||
1803 | 52 | |||
1804 | 53 | TemplatedFile(os.path.join(settings['common']['apache_dir'], | ||
1805 | 54 | 'mods-available/alias.conf'), | ||
1806 | 55 | context, | ||
1807 | 56 | TEMPLATES_DIR, | ||
1808 | 57 | mode=0o0755, | ||
1809 | 58 | user='root', | ||
1810 | 59 | service_actions=[{'service': 'apache2', | ||
1811 | 60 | 'actions': ['restart']}]), | ||
1812 | 61 | |||
1813 | 62 | TemplatedFile(os.path.join(settings['common']['apache_dir'], | ||
1814 | 63 | 'conf-enabled/hardening.conf'), | ||
1815 | 64 | context, | ||
1816 | 65 | TEMPLATES_DIR, | ||
1817 | 66 | mode=0o0640, | ||
1818 | 67 | user='root', | ||
1819 | 68 | service_actions=[{'service': 'apache2', | ||
1820 | 69 | 'actions': ['restart']}]), | ||
1821 | 70 | |||
1822 | 71 | DirectoryPermissionAudit(settings['common']['apache_dir'], | ||
1823 | 72 | user='root', | ||
1824 | 73 | group='root', | ||
1825 | 74 | mode=0o640), | ||
1826 | 75 | |||
1827 | 76 | DisabledModuleAudit(settings['hardening']['modules_to_disable']), | ||
1828 | 77 | |||
1829 | 78 | NoReadWriteForOther(settings['common']['apache_dir']), | ||
1830 | 79 | ] | ||
1831 | 80 | |||
1832 | 81 | return audits | ||
1833 | 82 | |||
1834 | 83 | |||
1835 | 84 | class ApacheConfContext(object): | ||
1836 | 85 | """Defines the set of key/value pairs to set in a apache config file. | ||
1837 | 86 | |||
1838 | 87 | This context, when called, will return a dictionary containing the | ||
1839 | 88 | key/value pairs of setting to specify in the | ||
1840 | 89 | /etc/apache/conf-enabled/hardening.conf file. | ||
1841 | 90 | """ | ||
1842 | 91 | def __call__(self): | ||
1843 | 92 | settings = utils.get_settings('apache') | ||
1844 | 93 | ctxt = settings['hardening'] | ||
1845 | 94 | |||
1846 | 95 | out = subprocess.check_output(['apache2', '-v']) | ||
1847 | 96 | ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+', | ||
1848 | 97 | out).group(1) | ||
1849 | 98 | ctxt['apache_icondir'] = '/usr/share/apache2/icons/' | ||
1850 | 99 | ctxt['traceenable'] = settings['hardening']['traceenable'] | ||
1851 | 100 | return ctxt | ||
1852 | 101 | 0 | ||
1853 | === removed directory 'hooks/charmhelpers/contrib/hardening/audits' | |||
1854 | === removed file 'hooks/charmhelpers/contrib/hardening/audits/__init__.py' | |||
1855 | --- hooks/charmhelpers/contrib/hardening/audits/__init__.py 2016-04-22 04:53:43 +0000 | |||
1856 | +++ hooks/charmhelpers/contrib/hardening/audits/__init__.py 1970-01-01 00:00:00 +0000 | |||
1857 | @@ -1,63 +0,0 @@ | |||
1858 | 1 | # Copyright 2016 Canonical Limited. | ||
1859 | 2 | # | ||
1860 | 3 | # This file is part of charm-helpers. | ||
1861 | 4 | # | ||
1862 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1863 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1864 | 7 | # published by the Free Software Foundation. | ||
1865 | 8 | # | ||
1866 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1867 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1868 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1869 | 12 | # GNU Lesser General Public License for more details. | ||
1870 | 13 | # | ||
1871 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1872 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1873 | 16 | |||
1874 | 17 | |||
1875 | 18 | class BaseAudit(object): # NO-QA | ||
1876 | 19 | """Base class for hardening checks. | ||
1877 | 20 | |||
1878 | 21 | The lifecycle of a hardening check is to first check to see if the system | ||
1879 | 22 | is in compliance for the specified check. If it is not in compliance, the | ||
1880 | 23 | check method will return a value which will be supplied to the. | ||
1881 | 24 | """ | ||
1882 | 25 | def __init__(self, *args, **kwargs): | ||
1883 | 26 | self.unless = kwargs.get('unless', None) | ||
1884 | 27 | super(BaseAudit, self).__init__() | ||
1885 | 28 | |||
1886 | 29 | def ensure_compliance(self): | ||
1887 | 30 | """Checks to see if the current hardening check is in compliance or | ||
1888 | 31 | not. | ||
1889 | 32 | |||
1890 | 33 | If the check that is performed is not in compliance, then an exception | ||
1891 | 34 | should be raised. | ||
1892 | 35 | """ | ||
1893 | 36 | pass | ||
1894 | 37 | |||
1895 | 38 | def _take_action(self): | ||
1896 | 39 | """Determines whether to perform the action or not. | ||
1897 | 40 | |||
1898 | 41 | Checks whether or not an action should be taken. This is determined by | ||
1899 | 42 | the truthy value for the unless parameter. If unless is a callback | ||
1900 | 43 | method, it will be invoked with no parameters in order to determine | ||
1901 | 44 | whether or not the action should be taken. Otherwise, the truthy value | ||
1902 | 45 | of the unless attribute will determine if the action should be | ||
1903 | 46 | performed. | ||
1904 | 47 | """ | ||
1905 | 48 | # Do the action if there isn't an unless override. | ||
1906 | 49 | if self.unless is None: | ||
1907 | 50 | return True | ||
1908 | 51 | |||
1909 | 52 | # Invoke the callback if there is one. | ||
1910 | 53 | if hasattr(self.unless, '__call__'): | ||
1911 | 54 | results = self.unless() | ||
1912 | 55 | if results: | ||
1913 | 56 | return False | ||
1914 | 57 | else: | ||
1915 | 58 | return True | ||
1916 | 59 | |||
1917 | 60 | if self.unless: | ||
1918 | 61 | return False | ||
1919 | 62 | else: | ||
1920 | 63 | return True | ||
1921 | 64 | 0 | ||
1922 | === removed file 'hooks/charmhelpers/contrib/hardening/audits/apache.py' | |||
1923 | --- hooks/charmhelpers/contrib/hardening/audits/apache.py 2016-04-22 04:53:43 +0000 | |||
1924 | +++ hooks/charmhelpers/contrib/hardening/audits/apache.py 1970-01-01 00:00:00 +0000 | |||
1925 | @@ -1,100 +0,0 @@ | |||
1926 | 1 | # Copyright 2016 Canonical Limited. | ||
1927 | 2 | # | ||
1928 | 3 | # This file is part of charm-helpers. | ||
1929 | 4 | # | ||
1930 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1931 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1932 | 7 | # published by the Free Software Foundation. | ||
1933 | 8 | # | ||
1934 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1935 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1936 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1937 | 12 | # GNU Lesser General Public License for more details. | ||
1938 | 13 | # | ||
1939 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1940 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1941 | 16 | |||
1942 | 17 | import re | ||
1943 | 18 | import subprocess | ||
1944 | 19 | |||
1945 | 20 | from six import string_types | ||
1946 | 21 | |||
1947 | 22 | from charmhelpers.core.hookenv import ( | ||
1948 | 23 | log, | ||
1949 | 24 | INFO, | ||
1950 | 25 | ERROR, | ||
1951 | 26 | ) | ||
1952 | 27 | |||
1953 | 28 | from charmhelpers.contrib.hardening.audits import BaseAudit | ||
1954 | 29 | |||
1955 | 30 | |||
1956 | 31 | class DisabledModuleAudit(BaseAudit): | ||
1957 | 32 | """Audits Apache2 modules. | ||
1958 | 33 | |||
1959 | 34 | Determines if the apache2 modules are enabled. If the modules are enabled | ||
1960 | 35 | then they are removed in the ensure_compliance. | ||
1961 | 36 | """ | ||
1962 | 37 | def __init__(self, modules): | ||
1963 | 38 | if modules is None: | ||
1964 | 39 | self.modules = [] | ||
1965 | 40 | elif isinstance(modules, string_types): | ||
1966 | 41 | self.modules = [modules] | ||
1967 | 42 | else: | ||
1968 | 43 | self.modules = modules | ||
1969 | 44 | |||
1970 | 45 | def ensure_compliance(self): | ||
1971 | 46 | """Ensures that the modules are not loaded.""" | ||
1972 | 47 | if not self.modules: | ||
1973 | 48 | return | ||
1974 | 49 | |||
1975 | 50 | try: | ||
1976 | 51 | loaded_modules = self._get_loaded_modules() | ||
1977 | 52 | non_compliant_modules = [] | ||
1978 | 53 | for module in self.modules: | ||
1979 | 54 | if module in loaded_modules: | ||
1980 | 55 | log("Module '%s' is enabled but should not be." % | ||
1981 | 56 | (module), level=INFO) | ||
1982 | 57 | non_compliant_modules.append(module) | ||
1983 | 58 | |||
1984 | 59 | if len(non_compliant_modules) == 0: | ||
1985 | 60 | return | ||
1986 | 61 | |||
1987 | 62 | for module in non_compliant_modules: | ||
1988 | 63 | self._disable_module(module) | ||
1989 | 64 | self._restart_apache() | ||
1990 | 65 | except subprocess.CalledProcessError as e: | ||
1991 | 66 | log('Error occurred auditing apache module compliance. ' | ||
1992 | 67 | 'This may have been already reported. ' | ||
1993 | 68 | 'Output is: %s' % e.output, level=ERROR) | ||
1994 | 69 | |||
1995 | 70 | @staticmethod | ||
1996 | 71 | def _get_loaded_modules(): | ||
1997 | 72 | """Returns the modules which are enabled in Apache.""" | ||
1998 | 73 | output = subprocess.check_output(['apache2ctl', '-M']) | ||
1999 | 74 | modules = [] | ||
2000 | 75 | for line in output.strip().split(): | ||
2001 | 76 | # Each line of the enabled module output looks like: | ||
2002 | 77 | # module_name (static|shared) | ||
2003 | 78 | # Plus a header line at the top of the output which is stripped | ||
2004 | 79 | # out by the regex. | ||
2005 | 80 | matcher = re.search(r'^ (\S*)', line) | ||
2006 | 81 | if matcher: | ||
2007 | 82 | modules.append(matcher.group(1)) | ||
2008 | 83 | return modules | ||
2009 | 84 | |||
2010 | 85 | @staticmethod | ||
2011 | 86 | def _disable_module(module): | ||
2012 | 87 | """Disables the specified module in Apache.""" | ||
2013 | 88 | try: | ||
2014 | 89 | subprocess.check_call(['a2dismod', module]) | ||
2015 | 90 | except subprocess.CalledProcessError as e: | ||
2016 | 91 | # Note: catch error here to allow the attempt of disabling | ||
2017 | 92 | # multiple modules in one go rather than failing after the | ||
2018 | 93 | # first module fails. | ||
2019 | 94 | log('Error occurred disabling module %s. ' | ||
2020 | 95 | 'Output is: %s' % (module, e.output), level=ERROR) | ||
2021 | 96 | |||
2022 | 97 | @staticmethod | ||
2023 | 98 | def _restart_apache(): | ||
2024 | 99 | """Restarts the apache process""" | ||
2025 | 100 | subprocess.check_output(['service', 'apache2', 'restart']) | ||
2026 | 101 | 0 | ||
2027 | === removed file 'hooks/charmhelpers/contrib/hardening/audits/apt.py' | |||
2028 | --- hooks/charmhelpers/contrib/hardening/audits/apt.py 2016-04-22 04:53:43 +0000 | |||
2029 | +++ hooks/charmhelpers/contrib/hardening/audits/apt.py 1970-01-01 00:00:00 +0000 | |||
2030 | @@ -1,105 +0,0 @@ | |||
2031 | 1 | # Copyright 2016 Canonical Limited. | ||
2032 | 2 | # | ||
2033 | 3 | # This file is part of charm-helpers. | ||
2034 | 4 | # | ||
2035 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2036 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2037 | 7 | # published by the Free Software Foundation. | ||
2038 | 8 | # | ||
2039 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2040 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2041 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2042 | 12 | # GNU Lesser General Public License for more details. | ||
2043 | 13 | # | ||
2044 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2045 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2046 | 16 | |||
2047 | 17 | from __future__ import absolute_import # required for external apt import | ||
2048 | 18 | from apt import apt_pkg | ||
2049 | 19 | from six import string_types | ||
2050 | 20 | |||
2051 | 21 | from charmhelpers.fetch import ( | ||
2052 | 22 | apt_cache, | ||
2053 | 23 | apt_purge | ||
2054 | 24 | ) | ||
2055 | 25 | from charmhelpers.core.hookenv import ( | ||
2056 | 26 | log, | ||
2057 | 27 | DEBUG, | ||
2058 | 28 | WARNING, | ||
2059 | 29 | ) | ||
2060 | 30 | from charmhelpers.contrib.hardening.audits import BaseAudit | ||
2061 | 31 | |||
2062 | 32 | |||
2063 | 33 | class AptConfig(BaseAudit): | ||
2064 | 34 | |||
2065 | 35 | def __init__(self, config, **kwargs): | ||
2066 | 36 | self.config = config | ||
2067 | 37 | |||
2068 | 38 | def verify_config(self): | ||
2069 | 39 | apt_pkg.init() | ||
2070 | 40 | for cfg in self.config: | ||
2071 | 41 | value = apt_pkg.config.get(cfg['key'], cfg.get('default', '')) | ||
2072 | 42 | if value and value != cfg['expected']: | ||
2073 | 43 | log("APT config '%s' has unexpected value '%s' " | ||
2074 | 44 | "(expected='%s')" % | ||
2075 | 45 | (cfg['key'], value, cfg['expected']), level=WARNING) | ||
2076 | 46 | |||
2077 | 47 | def ensure_compliance(self): | ||
2078 | 48 | self.verify_config() | ||
2079 | 49 | |||
2080 | 50 | |||
2081 | 51 | class RestrictedPackages(BaseAudit): | ||
2082 | 52 | """Class used to audit restricted packages on the system.""" | ||
2083 | 53 | |||
2084 | 54 | def __init__(self, pkgs, **kwargs): | ||
2085 | 55 | super(RestrictedPackages, self).__init__(**kwargs) | ||
2086 | 56 | if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'): | ||
2087 | 57 | self.pkgs = [pkgs] | ||
2088 | 58 | else: | ||
2089 | 59 | self.pkgs = pkgs | ||
2090 | 60 | |||
2091 | 61 | def ensure_compliance(self): | ||
2092 | 62 | cache = apt_cache() | ||
2093 | 63 | |||
2094 | 64 | for p in self.pkgs: | ||
2095 | 65 | if p not in cache: | ||
2096 | 66 | continue | ||
2097 | 67 | |||
2098 | 68 | pkg = cache[p] | ||
2099 | 69 | if not self.is_virtual_package(pkg): | ||
2100 | 70 | if not pkg.current_ver: | ||
2101 | 71 | log("Package '%s' is not installed." % pkg.name, | ||
2102 | 72 | level=DEBUG) | ||
2103 | 73 | continue | ||
2104 | 74 | else: | ||
2105 | 75 | log("Restricted package '%s' is installed" % pkg.name, | ||
2106 | 76 | level=WARNING) | ||
2107 | 77 | self.delete_package(cache, pkg) | ||
2108 | 78 | else: | ||
2109 | 79 | log("Checking restricted virtual package '%s' provides" % | ||
2110 | 80 | pkg.name, level=DEBUG) | ||
2111 | 81 | self.delete_package(cache, pkg) | ||
2112 | 82 | |||
2113 | 83 | def delete_package(self, cache, pkg): | ||
2114 | 84 | """Deletes the package from the system. | ||
2115 | 85 | |||
2116 | 86 | Deletes the package form the system, properly handling virtual | ||
2117 | 87 | packages. | ||
2118 | 88 | |||
2119 | 89 | :param cache: the apt cache | ||
2120 | 90 | :param pkg: the package to remove | ||
2121 | 91 | """ | ||
2122 | 92 | if self.is_virtual_package(pkg): | ||
2123 | 93 | log("Package '%s' appears to be virtual - purging provides" % | ||
2124 | 94 | pkg.name, level=DEBUG) | ||
2125 | 95 | for _p in pkg.provides_list: | ||
2126 | 96 | self.delete_package(cache, _p[2].parent_pkg) | ||
2127 | 97 | elif not pkg.current_ver: | ||
2128 | 98 | log("Package '%s' not installed" % pkg.name, level=DEBUG) | ||
2129 | 99 | return | ||
2130 | 100 | else: | ||
2131 | 101 | log("Purging package '%s'" % pkg.name, level=DEBUG) | ||
2132 | 102 | apt_purge(pkg.name) | ||
2133 | 103 | |||
2134 | 104 | def is_virtual_package(self, pkg): | ||
2135 | 105 | return pkg.has_provides and not pkg.has_versions | ||
2136 | 106 | 0 | ||
2137 | === removed file 'hooks/charmhelpers/contrib/hardening/audits/file.py' | |||
2138 | --- hooks/charmhelpers/contrib/hardening/audits/file.py 2016-04-22 04:53:43 +0000 | |||
2139 | +++ hooks/charmhelpers/contrib/hardening/audits/file.py 1970-01-01 00:00:00 +0000 | |||
2140 | @@ -1,552 +0,0 @@ | |||
2141 | 1 | # Copyright 2016 Canonical Limited. | ||
2142 | 2 | # | ||
2143 | 3 | # This file is part of charm-helpers. | ||
2144 | 4 | # | ||
2145 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2146 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2147 | 7 | # published by the Free Software Foundation. | ||
2148 | 8 | # | ||
2149 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2150 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2151 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2152 | 12 | # GNU Lesser General Public License for more details. | ||
2153 | 13 | # | ||
2154 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2155 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2156 | 16 | |||
2157 | 17 | import grp | ||
2158 | 18 | import os | ||
2159 | 19 | import pwd | ||
2160 | 20 | import re | ||
2161 | 21 | |||
2162 | 22 | from subprocess import ( | ||
2163 | 23 | CalledProcessError, | ||
2164 | 24 | check_output, | ||
2165 | 25 | check_call, | ||
2166 | 26 | ) | ||
2167 | 27 | from traceback import format_exc | ||
2168 | 28 | from six import string_types | ||
2169 | 29 | from stat import ( | ||
2170 | 30 | S_ISGID, | ||
2171 | 31 | S_ISUID | ||
2172 | 32 | ) | ||
2173 | 33 | |||
2174 | 34 | from charmhelpers.core.hookenv import ( | ||
2175 | 35 | log, | ||
2176 | 36 | DEBUG, | ||
2177 | 37 | INFO, | ||
2178 | 38 | WARNING, | ||
2179 | 39 | ERROR, | ||
2180 | 40 | ) | ||
2181 | 41 | from charmhelpers.core import unitdata | ||
2182 | 42 | from charmhelpers.core.host import file_hash | ||
2183 | 43 | from charmhelpers.contrib.hardening.audits import BaseAudit | ||
2184 | 44 | from charmhelpers.contrib.hardening.templating import ( | ||
2185 | 45 | get_template_path, | ||
2186 | 46 | render_and_write, | ||
2187 | 47 | ) | ||
2188 | 48 | from charmhelpers.contrib.hardening import utils | ||
2189 | 49 | |||
2190 | 50 | |||
2191 | 51 | class BaseFileAudit(BaseAudit): | ||
2192 | 52 | """Base class for file audits. | ||
2193 | 53 | |||
2194 | 54 | Provides api stubs for compliance check flow that must be used by any class | ||
2195 | 55 | that implemented this one. | ||
2196 | 56 | """ | ||
2197 | 57 | |||
2198 | 58 | def __init__(self, paths, always_comply=False, *args, **kwargs): | ||
2199 | 59 | """ | ||
2200 | 60 | :param paths: string path of list of paths of files we want to apply | ||
2201 | 61 | compliance checks are criteria to. | ||
2202 | 62 | :param always_comply: if true compliance criteria is always applied | ||
2203 | 63 | else compliance is skipped for non-existent | ||
2204 | 64 | paths. | ||
2205 | 65 | """ | ||
2206 | 66 | super(BaseFileAudit, self).__init__(*args, **kwargs) | ||
2207 | 67 | self.always_comply = always_comply | ||
2208 | 68 | if isinstance(paths, string_types) or not hasattr(paths, '__iter__'): | ||
2209 | 69 | self.paths = [paths] | ||
2210 | 70 | else: | ||
2211 | 71 | self.paths = paths | ||
2212 | 72 | |||
2213 | 73 | def ensure_compliance(self): | ||
2214 | 74 | """Ensure that the all registered files comply to registered criteria. | ||
2215 | 75 | """ | ||
2216 | 76 | for p in self.paths: | ||
2217 | 77 | if os.path.exists(p): | ||
2218 | 78 | if self.is_compliant(p): | ||
2219 | 79 | continue | ||
2220 | 80 | |||
2221 | 81 | log('File %s is not in compliance.' % p, level=INFO) | ||
2222 | 82 | else: | ||
2223 | 83 | if not self.always_comply: | ||
2224 | 84 | log("Non-existent path '%s' - skipping compliance check" | ||
2225 | 85 | % (p), level=INFO) | ||
2226 | 86 | continue | ||
2227 | 87 | |||
2228 | 88 | if self._take_action(): | ||
2229 | 89 | log("Applying compliance criteria to '%s'" % (p), level=INFO) | ||
2230 | 90 | self.comply(p) | ||
2231 | 91 | |||
2232 | 92 | def is_compliant(self, path): | ||
2233 | 93 | """Audits the path to see if it is compliance. | ||
2234 | 94 | |||
2235 | 95 | :param path: the path to the file that should be checked. | ||
2236 | 96 | """ | ||
2237 | 97 | raise NotImplementedError | ||
2238 | 98 | |||
2239 | 99 | def comply(self, path): | ||
2240 | 100 | """Enforces the compliance of a path. | ||
2241 | 101 | |||
2242 | 102 | :param path: the path to the file that should be enforced. | ||
2243 | 103 | """ | ||
2244 | 104 | raise NotImplementedError | ||
2245 | 105 | |||
2246 | 106 | @classmethod | ||
2247 | 107 | def _get_stat(cls, path): | ||
2248 | 108 | """Returns the Posix st_stat information for the specified file path. | ||
2249 | 109 | |||
2250 | 110 | :param path: the path to get the st_stat information for. | ||
2251 | 111 | :returns: an st_stat object for the path or None if the path doesn't | ||
2252 | 112 | exist. | ||
2253 | 113 | """ | ||
2254 | 114 | return os.stat(path) | ||
2255 | 115 | |||
2256 | 116 | |||
2257 | 117 | class FilePermissionAudit(BaseFileAudit): | ||
2258 | 118 | """Implements an audit for file permissions and ownership for a user. | ||
2259 | 119 | |||
2260 | 120 | This class implements functionality that ensures that a specific user/group | ||
2261 | 121 | will own the file(s) specified and that the permissions specified are | ||
2262 | 122 | applied properly to the file. | ||
2263 | 123 | """ | ||
2264 | 124 | def __init__(self, paths, user, group=None, mode=0o600, **kwargs): | ||
2265 | 125 | self.user = user | ||
2266 | 126 | self.group = group | ||
2267 | 127 | self.mode = mode | ||
2268 | 128 | super(FilePermissionAudit, self).__init__(paths, user, group, mode, | ||
2269 | 129 | **kwargs) | ||
2270 | 130 | |||
2271 | 131 | @property | ||
2272 | 132 | def user(self): | ||
2273 | 133 | return self._user | ||
2274 | 134 | |||
2275 | 135 | @user.setter | ||
2276 | 136 | def user(self, name): | ||
2277 | 137 | try: | ||
2278 | 138 | user = pwd.getpwnam(name) | ||
2279 | 139 | except KeyError: | ||
2280 | 140 | log('Unknown user %s' % name, level=ERROR) | ||
2281 | 141 | user = None | ||
2282 | 142 | self._user = user | ||
2283 | 143 | |||
2284 | 144 | @property | ||
2285 | 145 | def group(self): | ||
2286 | 146 | return self._group | ||
2287 | 147 | |||
2288 | 148 | @group.setter | ||
2289 | 149 | def group(self, name): | ||
2290 | 150 | try: | ||
2291 | 151 | group = None | ||
2292 | 152 | if name: | ||
2293 | 153 | group = grp.getgrnam(name) | ||
2294 | 154 | else: | ||
2295 | 155 | group = grp.getgrgid(self.user.pw_gid) | ||
2296 | 156 | except KeyError: | ||
2297 | 157 | log('Unknown group %s' % name, level=ERROR) | ||
2298 | 158 | self._group = group | ||
2299 | 159 | |||
2300 | 160 | def is_compliant(self, path): | ||
2301 | 161 | """Checks if the path is in compliance. | ||
2302 | 162 | |||
2303 | 163 | Used to determine if the path specified meets the necessary | ||
2304 | 164 | requirements to be in compliance with the check itself. | ||
2305 | 165 | |||
2306 | 166 | :param path: the file path to check | ||
2307 | 167 | :returns: True if the path is compliant, False otherwise. | ||
2308 | 168 | """ | ||
2309 | 169 | stat = self._get_stat(path) | ||
2310 | 170 | user = self.user | ||
2311 | 171 | group = self.group | ||
2312 | 172 | |||
2313 | 173 | compliant = True | ||
2314 | 174 | if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid: | ||
2315 | 175 | log('File %s is not owned by %s:%s.' % (path, user.pw_name, | ||
2316 | 176 | group.gr_name), | ||
2317 | 177 | level=INFO) | ||
2318 | 178 | compliant = False | ||
2319 | 179 | |||
2320 | 180 | # POSIX refers to the st_mode bits as corresponding to both the | ||
2321 | 181 | # file type and file permission bits, where the least significant 12 | ||
2322 | 182 | # bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the | ||
2323 | 183 | # file permission bits (8-0) | ||
2324 | 184 | perms = stat.st_mode & 0o7777 | ||
2325 | 185 | if perms != self.mode: | ||
2326 | 186 | log('File %s has incorrect permissions, currently set to %s' % | ||
2327 | 187 | (path, oct(stat.st_mode & 0o7777)), level=INFO) | ||
2328 | 188 | compliant = False | ||
2329 | 189 | |||
2330 | 190 | return compliant | ||
2331 | 191 | |||
2332 | 192 | def comply(self, path): | ||
2333 | 193 | """Issues a chown and chmod to the file paths specified.""" | ||
2334 | 194 | utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name, | ||
2335 | 195 | self.mode) | ||
2336 | 196 | |||
2337 | 197 | |||
2338 | 198 | class DirectoryPermissionAudit(FilePermissionAudit): | ||
2339 | 199 | """Performs a permission check for the specified directory path.""" | ||
2340 | 200 | |||
2341 | 201 | def __init__(self, paths, user, group=None, mode=0o600, | ||
2342 | 202 | recursive=True, **kwargs): | ||
2343 | 203 | super(DirectoryPermissionAudit, self).__init__(paths, user, group, | ||
2344 | 204 | mode, **kwargs) | ||
2345 | 205 | self.recursive = recursive | ||
2346 | 206 | |||
2347 | 207 | def is_compliant(self, path): | ||
2348 | 208 | """Checks if the directory is compliant. | ||
2349 | 209 | |||
2350 | 210 | Used to determine if the path specified and all of its children | ||
2351 | 211 | directories are in compliance with the check itself. | ||
2352 | 212 | |||
2353 | 213 | :param path: the directory path to check | ||
2354 | 214 | :returns: True if the directory tree is compliant, otherwise False. | ||
2355 | 215 | """ | ||
2356 | 216 | if not os.path.isdir(path): | ||
2357 | 217 | log('Path specified %s is not a directory.' % path, level=ERROR) | ||
2358 | 218 | raise ValueError("%s is not a directory." % path) | ||
2359 | 219 | |||
2360 | 220 | if not self.recursive: | ||
2361 | 221 | return super(DirectoryPermissionAudit, self).is_compliant(path) | ||
2362 | 222 | |||
2363 | 223 | compliant = True | ||
2364 | 224 | for root, dirs, _ in os.walk(path): | ||
2365 | 225 | if len(dirs) > 0: | ||
2366 | 226 | continue | ||
2367 | 227 | |||
2368 | 228 | if not super(DirectoryPermissionAudit, self).is_compliant(root): | ||
2369 | 229 | compliant = False | ||
2370 | 230 | continue | ||
2371 | 231 | |||
2372 | 232 | return compliant | ||
2373 | 233 | |||
2374 | 234 | def comply(self, path): | ||
2375 | 235 | for root, dirs, _ in os.walk(path): | ||
2376 | 236 | if len(dirs) > 0: | ||
2377 | 237 | super(DirectoryPermissionAudit, self).comply(root) | ||
2378 | 238 | |||
2379 | 239 | |||
2380 | 240 | class ReadOnly(BaseFileAudit): | ||
2381 | 241 | """Audits that files and folders are read only.""" | ||
2382 | 242 | def __init__(self, paths, *args, **kwargs): | ||
2383 | 243 | super(ReadOnly, self).__init__(paths=paths, *args, **kwargs) | ||
2384 | 244 | |||
2385 | 245 | def is_compliant(self, path): | ||
2386 | 246 | try: | ||
2387 | 247 | output = check_output(['find', path, '-perm', '-go+w', | ||
2388 | 248 | '-type', 'f']).strip() | ||
2389 | 249 | |||
2390 | 250 | # The find above will find any files which have permission sets | ||
2391 | 251 | # which allow too broad of write access. As such, the path is | ||
2392 | 252 | # compliant if there is no output. | ||
2393 | 253 | if output: | ||
2394 | 254 | return False | ||
2395 | 255 | |||
2396 | 256 | return True | ||
2397 | 257 | except CalledProcessError as e: | ||
2398 | 258 | log('Error occurred checking finding writable files for %s. ' | ||
2399 | 259 | 'Error information is: command %s failed with returncode ' | ||
2400 | 260 | '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output, | ||
2401 | 261 | format_exc(e)), level=ERROR) | ||
2402 | 262 | return False | ||
2403 | 263 | |||
2404 | 264 | def comply(self, path): | ||
2405 | 265 | try: | ||
2406 | 266 | check_output(['chmod', 'go-w', '-R', path]) | ||
2407 | 267 | except CalledProcessError as e: | ||
2408 | 268 | log('Error occurred removing writeable permissions for %s. ' | ||
2409 | 269 | 'Error information is: command %s failed with returncode ' | ||
2410 | 270 | '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output, | ||
2411 | 271 | format_exc(e)), level=ERROR) | ||
2412 | 272 | |||
2413 | 273 | |||
2414 | 274 | class NoReadWriteForOther(BaseFileAudit): | ||
2415 | 275 | """Ensures that the files found under the base path are readable or | ||
2416 | 276 | writable by anyone other than the owner or the group. | ||
2417 | 277 | """ | ||
2418 | 278 | def __init__(self, paths): | ||
2419 | 279 | super(NoReadWriteForOther, self).__init__(paths) | ||
2420 | 280 | |||
2421 | 281 | def is_compliant(self, path): | ||
2422 | 282 | try: | ||
2423 | 283 | cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o', | ||
2424 | 284 | '-perm', '-o+w', '-type', 'f'] | ||
2425 | 285 | output = check_output(cmd).strip() | ||
2426 | 286 | |||
2427 | 287 | # The find above here will find any files which have read or | ||
2428 | 288 | # write permissions for other, meaning there is too broad of access | ||
2429 | 289 | # to read/write the file. As such, the path is compliant if there's | ||
2430 | 290 | # no output. | ||
2431 | 291 | if output: | ||
2432 | 292 | return False | ||
2433 | 293 | |||
2434 | 294 | return True | ||
2435 | 295 | except CalledProcessError as e: | ||
2436 | 296 | log('Error occurred while finding files which are readable or ' | ||
2437 | 297 | 'writable to the world in %s. ' | ||
2438 | 298 | 'Command output is: %s.' % (path, e.output), level=ERROR) | ||
2439 | 299 | |||
2440 | 300 | def comply(self, path): | ||
2441 | 301 | try: | ||
2442 | 302 | check_output(['chmod', '-R', 'o-rw', path]) | ||
2443 | 303 | except CalledProcessError as e: | ||
2444 | 304 | log('Error occurred attempting to change modes of files under ' | ||
2445 | 305 | 'path %s. Output of command is: %s' % (path, e.output)) | ||
2446 | 306 | |||
2447 | 307 | |||
2448 | 308 | class NoSUIDSGIDAudit(BaseFileAudit): | ||
2449 | 309 | """Audits that specified files do not have SUID/SGID bits set.""" | ||
2450 | 310 | def __init__(self, paths, *args, **kwargs): | ||
2451 | 311 | super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs) | ||
2452 | 312 | |||
2453 | 313 | def is_compliant(self, path): | ||
2454 | 314 | stat = self._get_stat(path) | ||
2455 | 315 | if (stat.st_mode & (S_ISGID | S_ISUID)) != 0: | ||
2456 | 316 | return False | ||
2457 | 317 | |||
2458 | 318 | return True | ||
2459 | 319 | |||
2460 | 320 | def comply(self, path): | ||
2461 | 321 | try: | ||
2462 | 322 | log('Removing suid/sgid from %s.' % path, level=DEBUG) | ||
2463 | 323 | check_output(['chmod', '-s', path]) | ||
2464 | 324 | except CalledProcessError as e: | ||
2465 | 325 | log('Error occurred removing suid/sgid from %s.' | ||
2466 | 326 | 'Error information is: command %s failed with returncode ' | ||
2467 | 327 | '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output, | ||
2468 | 328 | format_exc(e)), level=ERROR) | ||
2469 | 329 | |||
2470 | 330 | |||
2471 | 331 | class TemplatedFile(BaseFileAudit): | ||
2472 | 332 | """The TemplatedFileAudit audits the contents of a templated file. | ||
2473 | 333 | |||
2474 | 334 | This audit renders a file from a template, sets the appropriate file | ||
2475 | 335 | permissions, then generates a hashsum with which to check the content | ||
2476 | 336 | changed. | ||
2477 | 337 | """ | ||
2478 | 338 | def __init__(self, path, context, template_dir, mode, user='root', | ||
2479 | 339 | group='root', service_actions=None, **kwargs): | ||
2480 | 340 | self.context = context | ||
2481 | 341 | self.user = user | ||
2482 | 342 | self.group = group | ||
2483 | 343 | self.mode = mode | ||
2484 | 344 | self.template_dir = template_dir | ||
2485 | 345 | self.service_actions = service_actions | ||
2486 | 346 | super(TemplatedFile, self).__init__(paths=path, always_comply=True, | ||
2487 | 347 | **kwargs) | ||
2488 | 348 | |||
2489 | 349 | def is_compliant(self, path): | ||
2490 | 350 | """Determines if the templated file is compliant. | ||
2491 | 351 | |||
2492 | 352 | A templated file is only compliant if it has not changed (as | ||
2493 | 353 | determined by its sha256 hashsum) AND its file permissions are set | ||
2494 | 354 | appropriately. | ||
2495 | 355 | |||
2496 | 356 | :param path: the path to check compliance. | ||
2497 | 357 | """ | ||
2498 | 358 | same_templates = self.templates_match(path) | ||
2499 | 359 | same_content = self.contents_match(path) | ||
2500 | 360 | same_permissions = self.permissions_match(path) | ||
2501 | 361 | |||
2502 | 362 | if same_content and same_permissions and same_templates: | ||
2503 | 363 | return True | ||
2504 | 364 | |||
2505 | 365 | return False | ||
2506 | 366 | |||
2507 | 367 | def run_service_actions(self): | ||
2508 | 368 | """Run any actions on services requested.""" | ||
2509 | 369 | if not self.service_actions: | ||
2510 | 370 | return | ||
2511 | 371 | |||
2512 | 372 | for svc_action in self.service_actions: | ||
2513 | 373 | name = svc_action['service'] | ||
2514 | 374 | actions = svc_action['actions'] | ||
2515 | 375 | log("Running service '%s' actions '%s'" % (name, actions), | ||
2516 | 376 | level=DEBUG) | ||
2517 | 377 | for action in actions: | ||
2518 | 378 | cmd = ['service', name, action] | ||
2519 | 379 | try: | ||
2520 | 380 | check_call(cmd) | ||
2521 | 381 | except CalledProcessError as exc: | ||
2522 | 382 | log("Service name='%s' action='%s' failed - %s" % | ||
2523 | 383 | (name, action, exc), level=WARNING) | ||
2524 | 384 | |||
2525 | 385 | def comply(self, path): | ||
2526 | 386 | """Ensures the contents and the permissions of the file. | ||
2527 | 387 | |||
2528 | 388 | :param path: the path to correct | ||
2529 | 389 | """ | ||
2530 | 390 | dirname = os.path.dirname(path) | ||
2531 | 391 | if not os.path.exists(dirname): | ||
2532 | 392 | os.makedirs(dirname) | ||
2533 | 393 | |||
2534 | 394 | self.pre_write() | ||
2535 | 395 | render_and_write(self.template_dir, path, self.context()) | ||
2536 | 396 | utils.ensure_permissions(path, self.user, self.group, self.mode) | ||
2537 | 397 | self.run_service_actions() | ||
2538 | 398 | self.save_checksum(path) | ||
2539 | 399 | self.post_write() | ||
2540 | 400 | |||
2541 | 401 | def pre_write(self): | ||
2542 | 402 | """Invoked prior to writing the template.""" | ||
2543 | 403 | pass | ||
2544 | 404 | |||
2545 | 405 | def post_write(self): | ||
2546 | 406 | """Invoked after writing the template.""" | ||
2547 | 407 | pass | ||
2548 | 408 | |||
2549 | 409 | def templates_match(self, path): | ||
2550 | 410 | """Determines if the template files are the same. | ||
2551 | 411 | |||
2552 | 412 | The template file equality is determined by the hashsum of the | ||
2553 | 413 | template files themselves. If there is no hashsum, then the content | ||
2554 | 414 | cannot be sure to be the same so treat it as if they changed. | ||
2555 | 415 | Otherwise, return whether or not the hashsums are the same. | ||
2556 | 416 | |||
2557 | 417 | :param path: the path to check | ||
2558 | 418 | :returns: boolean | ||
2559 | 419 | """ | ||
2560 | 420 | template_path = get_template_path(self.template_dir, path) | ||
2561 | 421 | key = 'hardening:template:%s' % template_path | ||
2562 | 422 | template_checksum = file_hash(template_path) | ||
2563 | 423 | kv = unitdata.kv() | ||
2564 | 424 | stored_tmplt_checksum = kv.get(key) | ||
2565 | 425 | if not stored_tmplt_checksum: | ||
2566 | 426 | kv.set(key, template_checksum) | ||
2567 | 427 | kv.flush() | ||
2568 | 428 | log('Saved template checksum for %s.' % template_path, | ||
2569 | 429 | level=DEBUG) | ||
2570 | 430 | # Since we don't have a template checksum, then assume it doesn't | ||
2571 | 431 | # match and return that the template is different. | ||
2572 | 432 | return False | ||
2573 | 433 | elif stored_tmplt_checksum != template_checksum: | ||
2574 | 434 | kv.set(key, template_checksum) | ||
2575 | 435 | kv.flush() | ||
2576 | 436 | log('Updated template checksum for %s.' % template_path, | ||
2577 | 437 | level=DEBUG) | ||
2578 | 438 | return False | ||
2579 | 439 | |||
2580 | 440 | # Here the template hasn't changed based upon the calculated | ||
2581 | 441 | # checksum of the template and what was previously stored. | ||
2582 | 442 | return True | ||
2583 | 443 | |||
2584 | 444 | def contents_match(self, path): | ||
2585 | 445 | """Determines if the file content is the same. | ||
2586 | 446 | |||
2587 | 447 | This is determined by comparing hashsum of the file contents and | ||
2588 | 448 | the saved hashsum. If there is no hashsum, then the content cannot | ||
2589 | 449 | be sure to be the same so treat them as if they are not the same. | ||
2590 | 450 | Otherwise, return True if the hashsums are the same, False if they | ||
2591 | 451 | are not the same. | ||
2592 | 452 | |||
2593 | 453 | :param path: the file to check. | ||
2594 | 454 | """ | ||
2595 | 455 | checksum = file_hash(path) | ||
2596 | 456 | |||
2597 | 457 | kv = unitdata.kv() | ||
2598 | 458 | stored_checksum = kv.get('hardening:%s' % path) | ||
2599 | 459 | if not stored_checksum: | ||
2600 | 460 | # If the checksum hasn't been generated, return False to ensure | ||
2601 | 461 | # the file is written and the checksum stored. | ||
2602 | 462 | log('Checksum for %s has not been calculated.' % path, level=DEBUG) | ||
2603 | 463 | return False | ||
2604 | 464 | elif stored_checksum != checksum: | ||
2605 | 465 | log('Checksum mismatch for %s.' % path, level=DEBUG) | ||
2606 | 466 | return False | ||
2607 | 467 | |||
2608 | 468 | return True | ||
2609 | 469 | |||
2610 | 470 | def permissions_match(self, path): | ||
2611 | 471 | """Determines if the file owner and permissions match. | ||
2612 | 472 | |||
2613 | 473 | :param path: the path to check. | ||
2614 | 474 | """ | ||
2615 | 475 | audit = FilePermissionAudit(path, self.user, self.group, self.mode) | ||
2616 | 476 | return audit.is_compliant(path) | ||
2617 | 477 | |||
2618 | 478 | def save_checksum(self, path): | ||
2619 | 479 | """Calculates and saves the checksum for the path specified. | ||
2620 | 480 | |||
2621 | 481 | :param path: the path of the file to save the checksum. | ||
2622 | 482 | """ | ||
2623 | 483 | checksum = file_hash(path) | ||
2624 | 484 | kv = unitdata.kv() | ||
2625 | 485 | kv.set('hardening:%s' % path, checksum) | ||
2626 | 486 | kv.flush() | ||
2627 | 487 | |||
2628 | 488 | |||
2629 | 489 | class DeletedFile(BaseFileAudit): | ||
2630 | 490 | """Audit to ensure that a file is deleted.""" | ||
2631 | 491 | def __init__(self, paths): | ||
2632 | 492 | super(DeletedFile, self).__init__(paths) | ||
2633 | 493 | |||
2634 | 494 | def is_compliant(self, path): | ||
2635 | 495 | return not os.path.exists(path) | ||
2636 | 496 | |||
2637 | 497 | def comply(self, path): | ||
2638 | 498 | os.remove(path) | ||
2639 | 499 | |||
2640 | 500 | |||
2641 | 501 | class FileContentAudit(BaseFileAudit): | ||
2642 | 502 | """Audit the contents of a file.""" | ||
2643 | 503 | def __init__(self, paths, cases, **kwargs): | ||
2644 | 504 | # Cases we expect to pass | ||
2645 | 505 | self.pass_cases = cases.get('pass', []) | ||
2646 | 506 | # Cases we expect to fail | ||
2647 | 507 | self.fail_cases = cases.get('fail', []) | ||
2648 | 508 | super(FileContentAudit, self).__init__(paths, **kwargs) | ||
2649 | 509 | |||
2650 | 510 | def is_compliant(self, path): | ||
2651 | 511 | """ | ||
2652 | 512 | Given a set of content matching cases i.e. tuple(regex, bool) where | ||
2653 | 513 | bool value denotes whether or not regex is expected to match, check that | ||
2654 | 514 | all cases match as expected with the contents of the file. Cases can be | ||
2655 | 515 | expected to pass of fail. | ||
2656 | 516 | |||
2657 | 517 | :param path: Path of file to check. | ||
2658 | 518 | :returns: Boolean value representing whether or not all cases are | ||
2659 | 519 | found to be compliant. | ||
2660 | 520 | """ | ||
2661 | 521 | log("Auditing contents of file '%s'" % (path), level=DEBUG) | ||
2662 | 522 | with open(path, 'r') as fd: | ||
2663 | 523 | contents = fd.read() | ||
2664 | 524 | |||
2665 | 525 | matches = 0 | ||
2666 | 526 | for pattern in self.pass_cases: | ||
2667 | 527 | key = re.compile(pattern, flags=re.MULTILINE) | ||
2668 | 528 | results = re.search(key, contents) | ||
2669 | 529 | if results: | ||
2670 | 530 | matches += 1 | ||
2671 | 531 | else: | ||
2672 | 532 | log("Pattern '%s' was expected to pass but instead it failed" | ||
2673 | 533 | % (pattern), level=WARNING) | ||
2674 | 534 | |||
2675 | 535 | for pattern in self.fail_cases: | ||
2676 | 536 | key = re.compile(pattern, flags=re.MULTILINE) | ||
2677 | 537 | results = re.search(key, contents) | ||
2678 | 538 | if not results: | ||
2679 | 539 | matches += 1 | ||
2680 | 540 | else: | ||
2681 | 541 | log("Pattern '%s' was expected to fail but instead it passed" | ||
2682 | 542 | % (pattern), level=WARNING) | ||
2683 | 543 | |||
2684 | 544 | total = len(self.pass_cases) + len(self.fail_cases) | ||
2685 | 545 | log("Checked %s cases and %s passed" % (total, matches), level=DEBUG) | ||
2686 | 546 | return matches == total | ||
2687 | 547 | |||
2688 | 548 | def comply(self, *args, **kwargs): | ||
2689 | 549 | """NOOP since we just issue warnings. This is to avoid the | ||
2690 | 550 | NotImplememtedError. | ||
2691 | 551 | """ | ||
2692 | 552 | log("Not applying any compliance criteria, only checks.", level=INFO) | ||
2693 | 553 | 0 | ||
2694 | === removed file 'hooks/charmhelpers/contrib/hardening/harden.py' | |||
2695 | --- hooks/charmhelpers/contrib/hardening/harden.py 2016-04-22 04:53:43 +0000 | |||
2696 | +++ hooks/charmhelpers/contrib/hardening/harden.py 1970-01-01 00:00:00 +0000 | |||
2697 | @@ -1,84 +0,0 @@ | |||
2698 | 1 | # Copyright 2016 Canonical Limited. | ||
2699 | 2 | # | ||
2700 | 3 | # This file is part of charm-helpers. | ||
2701 | 4 | # | ||
2702 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2703 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2704 | 7 | # published by the Free Software Foundation. | ||
2705 | 8 | # | ||
2706 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2707 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2708 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2709 | 12 | # GNU Lesser General Public License for more details. | ||
2710 | 13 | # | ||
2711 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2712 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2713 | 16 | |||
2714 | 17 | import six | ||
2715 | 18 | |||
2716 | 19 | from collections import OrderedDict | ||
2717 | 20 | |||
2718 | 21 | from charmhelpers.core.hookenv import ( | ||
2719 | 22 | config, | ||
2720 | 23 | log, | ||
2721 | 24 | DEBUG, | ||
2722 | 25 | WARNING, | ||
2723 | 26 | ) | ||
2724 | 27 | from charmhelpers.contrib.hardening.host.checks import run_os_checks | ||
2725 | 28 | from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks | ||
2726 | 29 | from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks | ||
2727 | 30 | from charmhelpers.contrib.hardening.apache.checks import run_apache_checks | ||
2728 | 31 | |||
2729 | 32 | |||
2730 | 33 | def harden(overrides=None): | ||
2731 | 34 | """Hardening decorator. | ||
2732 | 35 | |||
2733 | 36 | This is the main entry point for running the hardening stack. In order to | ||
2734 | 37 | run modules of the stack you must add this decorator to charm hook(s) and | ||
2735 | 38 | ensure that your charm config.yaml contains the 'harden' option set to | ||
2736 | 39 | one or more of the supported modules. Setting these will cause the | ||
2737 | 40 | corresponding hardening code to be run when the hook fires. | ||
2738 | 41 | |||
2739 | 42 | This decorator can and should be applied to more than one hook or function | ||
2740 | 43 | such that hardening modules are called multiple times. This is because | ||
2741 | 44 | subsequent calls will perform auditing checks that will report any changes | ||
2742 | 45 | to resources hardened by the first run (and possibly perform compliance | ||
2743 | 46 | actions as a result of any detected infractions). | ||
2744 | 47 | |||
2745 | 48 | :param overrides: Optional list of stack modules used to override those | ||
2746 | 49 | provided with 'harden' config. | ||
2747 | 50 | :returns: Returns value returned by decorated function once executed. | ||
2748 | 51 | """ | ||
2749 | 52 | def _harden_inner1(f): | ||
2750 | 53 | log("Hardening function '%s'" % (f.__name__), level=DEBUG) | ||
2751 | 54 | |||
2752 | 55 | def _harden_inner2(*args, **kwargs): | ||
2753 | 56 | RUN_CATALOG = OrderedDict([('os', run_os_checks), | ||
2754 | 57 | ('ssh', run_ssh_checks), | ||
2755 | 58 | ('mysql', run_mysql_checks), | ||
2756 | 59 | ('apache', run_apache_checks)]) | ||
2757 | 60 | |||
2758 | 61 | enabled = overrides or (config("harden") or "").split() | ||
2759 | 62 | if enabled: | ||
2760 | 63 | modules_to_run = [] | ||
2761 | 64 | # modules will always be performed in the following order | ||
2762 | 65 | for module, func in six.iteritems(RUN_CATALOG): | ||
2763 | 66 | if module in enabled: | ||
2764 | 67 | enabled.remove(module) | ||
2765 | 68 | modules_to_run.append(func) | ||
2766 | 69 | |||
2767 | 70 | if enabled: | ||
2768 | 71 | log("Unknown hardening modules '%s' - ignoring" % | ||
2769 | 72 | (', '.join(enabled)), level=WARNING) | ||
2770 | 73 | |||
2771 | 74 | for hardener in modules_to_run: | ||
2772 | 75 | log("Executing hardening module '%s'" % | ||
2773 | 76 | (hardener.__name__), level=DEBUG) | ||
2774 | 77 | hardener() | ||
2775 | 78 | else: | ||
2776 | 79 | log("No hardening applied to '%s'" % (f.__name__), level=DEBUG) | ||
2777 | 80 | |||
2778 | 81 | return f(*args, **kwargs) | ||
2779 | 82 | return _harden_inner2 | ||
2780 | 83 | |||
2781 | 84 | return _harden_inner1 | ||
2782 | 85 | 0 | ||
2783 | === removed directory 'hooks/charmhelpers/contrib/hardening/host' | |||
2784 | === removed file 'hooks/charmhelpers/contrib/hardening/host/__init__.py' | |||
2785 | --- hooks/charmhelpers/contrib/hardening/host/__init__.py 2016-04-22 04:53:43 +0000 | |||
2786 | +++ hooks/charmhelpers/contrib/hardening/host/__init__.py 1970-01-01 00:00:00 +0000 | |||
2787 | @@ -1,19 +0,0 @@ | |||
2788 | 1 | # Copyright 2016 Canonical Limited. | ||
2789 | 2 | # | ||
2790 | 3 | # This file is part of charm-helpers. | ||
2791 | 4 | # | ||
2792 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2793 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2794 | 7 | # published by the Free Software Foundation. | ||
2795 | 8 | # | ||
2796 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2797 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2798 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2799 | 12 | # GNU Lesser General Public License for more details. | ||
2800 | 13 | # | ||
2801 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2802 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2803 | 16 | |||
2804 | 17 | from os import path | ||
2805 | 18 | |||
2806 | 19 | TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates') | ||
2807 | 20 | 0 | ||
2808 | === removed directory 'hooks/charmhelpers/contrib/hardening/host/checks' | |||
2809 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/__init__.py' | |||
2810 | --- hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 2016-04-22 04:53:43 +0000 | |||
2811 | +++ hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 1970-01-01 00:00:00 +0000 | |||
2812 | @@ -1,50 +0,0 @@ | |||
2813 | 1 | # Copyright 2016 Canonical Limited. | ||
2814 | 2 | # | ||
2815 | 3 | # This file is part of charm-helpers. | ||
2816 | 4 | # | ||
2817 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2818 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2819 | 7 | # published by the Free Software Foundation. | ||
2820 | 8 | # | ||
2821 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2822 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2823 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2824 | 12 | # GNU Lesser General Public License for more details. | ||
2825 | 13 | # | ||
2826 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2827 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2828 | 16 | |||
2829 | 17 | from charmhelpers.core.hookenv import ( | ||
2830 | 18 | log, | ||
2831 | 19 | DEBUG, | ||
2832 | 20 | ) | ||
2833 | 21 | from charmhelpers.contrib.hardening.host.checks import ( | ||
2834 | 22 | apt, | ||
2835 | 23 | limits, | ||
2836 | 24 | login, | ||
2837 | 25 | minimize_access, | ||
2838 | 26 | pam, | ||
2839 | 27 | profile, | ||
2840 | 28 | securetty, | ||
2841 | 29 | suid_sgid, | ||
2842 | 30 | sysctl | ||
2843 | 31 | ) | ||
2844 | 32 | |||
2845 | 33 | |||
2846 | 34 | def run_os_checks(): | ||
2847 | 35 | log("Starting OS hardening checks.", level=DEBUG) | ||
2848 | 36 | checks = apt.get_audits() | ||
2849 | 37 | checks.extend(limits.get_audits()) | ||
2850 | 38 | checks.extend(login.get_audits()) | ||
2851 | 39 | checks.extend(minimize_access.get_audits()) | ||
2852 | 40 | checks.extend(pam.get_audits()) | ||
2853 | 41 | checks.extend(profile.get_audits()) | ||
2854 | 42 | checks.extend(securetty.get_audits()) | ||
2855 | 43 | checks.extend(suid_sgid.get_audits()) | ||
2856 | 44 | checks.extend(sysctl.get_audits()) | ||
2857 | 45 | |||
2858 | 46 | for check in checks: | ||
2859 | 47 | log("Running '%s' check" % (check.__class__.__name__), level=DEBUG) | ||
2860 | 48 | check.ensure_compliance() | ||
2861 | 49 | |||
2862 | 50 | log("OS hardening checks complete.", level=DEBUG) | ||
2863 | 51 | 0 | ||
2864 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/apt.py' | |||
2865 | --- hooks/charmhelpers/contrib/hardening/host/checks/apt.py 2016-04-22 04:53:43 +0000 | |||
2866 | +++ hooks/charmhelpers/contrib/hardening/host/checks/apt.py 1970-01-01 00:00:00 +0000 | |||
2867 | @@ -1,39 +0,0 @@ | |||
2868 | 1 | # Copyright 2016 Canonical Limited. | ||
2869 | 2 | # | ||
2870 | 3 | # This file is part of charm-helpers. | ||
2871 | 4 | # | ||
2872 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2873 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2874 | 7 | # published by the Free Software Foundation. | ||
2875 | 8 | # | ||
2876 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2877 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2878 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2879 | 12 | # GNU Lesser General Public License for more details. | ||
2880 | 13 | # | ||
2881 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2882 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2883 | 16 | |||
2884 | 17 | from charmhelpers.contrib.hardening.utils import get_settings | ||
2885 | 18 | from charmhelpers.contrib.hardening.audits.apt import ( | ||
2886 | 19 | AptConfig, | ||
2887 | 20 | RestrictedPackages, | ||
2888 | 21 | ) | ||
2889 | 22 | |||
2890 | 23 | |||
2891 | 24 | def get_audits(): | ||
2892 | 25 | """Get OS hardening apt audits. | ||
2893 | 26 | |||
2894 | 27 | :returns: dictionary of audits | ||
2895 | 28 | """ | ||
2896 | 29 | audits = [AptConfig([{'key': 'APT::Get::AllowUnauthenticated', | ||
2897 | 30 | 'expected': 'false'}])] | ||
2898 | 31 | |||
2899 | 32 | settings = get_settings('os') | ||
2900 | 33 | clean_packages = settings['security']['packages_clean'] | ||
2901 | 34 | if clean_packages: | ||
2902 | 35 | security_packages = settings['security']['packages_list'] | ||
2903 | 36 | if security_packages: | ||
2904 | 37 | audits.append(RestrictedPackages(security_packages)) | ||
2905 | 38 | |||
2906 | 39 | return audits | ||
2907 | 40 | 0 | ||
2908 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/limits.py' | |||
2909 | --- hooks/charmhelpers/contrib/hardening/host/checks/limits.py 2016-04-22 04:53:43 +0000 | |||
2910 | +++ hooks/charmhelpers/contrib/hardening/host/checks/limits.py 1970-01-01 00:00:00 +0000 | |||
2911 | @@ -1,55 +0,0 @@ | |||
2912 | 1 | # Copyright 2016 Canonical Limited. | ||
2913 | 2 | # | ||
2914 | 3 | # This file is part of charm-helpers. | ||
2915 | 4 | # | ||
2916 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2917 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2918 | 7 | # published by the Free Software Foundation. | ||
2919 | 8 | # | ||
2920 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2921 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2922 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2923 | 12 | # GNU Lesser General Public License for more details. | ||
2924 | 13 | # | ||
2925 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2926 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2927 | 16 | |||
2928 | 17 | from charmhelpers.contrib.hardening.audits.file import ( | ||
2929 | 18 | DirectoryPermissionAudit, | ||
2930 | 19 | TemplatedFile, | ||
2931 | 20 | ) | ||
2932 | 21 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
2933 | 22 | from charmhelpers.contrib.hardening import utils | ||
2934 | 23 | |||
2935 | 24 | |||
2936 | 25 | def get_audits(): | ||
2937 | 26 | """Get OS hardening security limits audits. | ||
2938 | 27 | |||
2939 | 28 | :returns: dictionary of audits | ||
2940 | 29 | """ | ||
2941 | 30 | audits = [] | ||
2942 | 31 | settings = utils.get_settings('os') | ||
2943 | 32 | |||
2944 | 33 | # Ensure that the /etc/security/limits.d directory is only writable | ||
2945 | 34 | # by the root user, but others can execute and read. | ||
2946 | 35 | audits.append(DirectoryPermissionAudit('/etc/security/limits.d', | ||
2947 | 36 | user='root', group='root', | ||
2948 | 37 | mode=0o755)) | ||
2949 | 38 | |||
2950 | 39 | # If core dumps are not enabled, then don't allow core dumps to be | ||
2951 | 40 | # created as they may contain sensitive information. | ||
2952 | 41 | if not settings['security']['kernel_enable_core_dump']: | ||
2953 | 42 | audits.append(TemplatedFile('/etc/security/limits.d/10.hardcore.conf', | ||
2954 | 43 | SecurityLimitsContext(), | ||
2955 | 44 | template_dir=TEMPLATES_DIR, | ||
2956 | 45 | user='root', group='root', mode=0o0440)) | ||
2957 | 46 | return audits | ||
2958 | 47 | |||
2959 | 48 | |||
2960 | 49 | class SecurityLimitsContext(object): | ||
2961 | 50 | |||
2962 | 51 | def __call__(self): | ||
2963 | 52 | settings = utils.get_settings('os') | ||
2964 | 53 | ctxt = {'disable_core_dump': | ||
2965 | 54 | not settings['security']['kernel_enable_core_dump']} | ||
2966 | 55 | return ctxt | ||
2967 | 56 | 0 | ||
2968 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/login.py' | |||
2969 | --- hooks/charmhelpers/contrib/hardening/host/checks/login.py 2016-04-22 04:53:43 +0000 | |||
2970 | +++ hooks/charmhelpers/contrib/hardening/host/checks/login.py 1970-01-01 00:00:00 +0000 | |||
2971 | @@ -1,67 +0,0 @@ | |||
2972 | 1 | # Copyright 2016 Canonical Limited. | ||
2973 | 2 | # | ||
2974 | 3 | # This file is part of charm-helpers. | ||
2975 | 4 | # | ||
2976 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2977 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2978 | 7 | # published by the Free Software Foundation. | ||
2979 | 8 | # | ||
2980 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2981 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2982 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2983 | 12 | # GNU Lesser General Public License for more details. | ||
2984 | 13 | # | ||
2985 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2986 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2987 | 16 | |||
2988 | 17 | from six import string_types | ||
2989 | 18 | |||
2990 | 19 | from charmhelpers.contrib.hardening.audits.file import TemplatedFile | ||
2991 | 20 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
2992 | 21 | from charmhelpers.contrib.hardening import utils | ||
2993 | 22 | |||
2994 | 23 | |||
2995 | 24 | def get_audits(): | ||
2996 | 25 | """Get OS hardening login.defs audits. | ||
2997 | 26 | |||
2998 | 27 | :returns: dictionary of audits | ||
2999 | 28 | """ | ||
3000 | 29 | audits = [TemplatedFile('/etc/login.defs', LoginContext(), | ||
3001 | 30 | template_dir=TEMPLATES_DIR, | ||
3002 | 31 | user='root', group='root', mode=0o0444)] | ||
3003 | 32 | return audits | ||
3004 | 33 | |||
3005 | 34 | |||
3006 | 35 | class LoginContext(object): | ||
3007 | 36 | |||
3008 | 37 | def __call__(self): | ||
3009 | 38 | settings = utils.get_settings('os') | ||
3010 | 39 | |||
3011 | 40 | # Octal numbers in yaml end up being turned into decimal, | ||
3012 | 41 | # so check if the umask is entered as a string (e.g. '027') | ||
3013 | 42 | # or as an octal umask as we know it (e.g. 002). If its not | ||
3014 | 43 | # a string assume it to be octal and turn it into an octal | ||
3015 | 44 | # string. | ||
3016 | 45 | umask = settings['environment']['umask'] | ||
3017 | 46 | if not isinstance(umask, string_types): | ||
3018 | 47 | umask = '%s' % oct(umask) | ||
3019 | 48 | |||
3020 | 49 | ctxt = { | ||
3021 | 50 | 'additional_user_paths': | ||
3022 | 51 | settings['environment']['extra_user_paths'], | ||
3023 | 52 | 'umask': umask, | ||
3024 | 53 | 'pwd_max_age': settings['auth']['pw_max_age'], | ||
3025 | 54 | 'pwd_min_age': settings['auth']['pw_min_age'], | ||
3026 | 55 | 'uid_min': settings['auth']['uid_min'], | ||
3027 | 56 | 'sys_uid_min': settings['auth']['sys_uid_min'], | ||
3028 | 57 | 'sys_uid_max': settings['auth']['sys_uid_max'], | ||
3029 | 58 | 'gid_min': settings['auth']['gid_min'], | ||
3030 | 59 | 'sys_gid_min': settings['auth']['sys_gid_min'], | ||
3031 | 60 | 'sys_gid_max': settings['auth']['sys_gid_max'], | ||
3032 | 61 | 'login_retries': settings['auth']['retries'], | ||
3033 | 62 | 'login_timeout': settings['auth']['timeout'], | ||
3034 | 63 | 'chfn_restrict': settings['auth']['chfn_restrict'], | ||
3035 | 64 | 'allow_login_without_home': settings['auth']['allow_homeless'] | ||
3036 | 65 | } | ||
3037 | 66 | |||
3038 | 67 | return ctxt | ||
3039 | 68 | 0 | ||
3040 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py' | |||
3041 | --- hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 2016-04-22 04:53:43 +0000 | |||
3042 | +++ hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 1970-01-01 00:00:00 +0000 | |||
3043 | @@ -1,52 +0,0 @@ | |||
3044 | 1 | # Copyright 2016 Canonical Limited. | ||
3045 | 2 | # | ||
3046 | 3 | # This file is part of charm-helpers. | ||
3047 | 4 | # | ||
3048 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3049 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3050 | 7 | # published by the Free Software Foundation. | ||
3051 | 8 | # | ||
3052 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3053 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3054 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3055 | 12 | # GNU Lesser General Public License for more details. | ||
3056 | 13 | # | ||
3057 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3058 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3059 | 16 | |||
3060 | 17 | from charmhelpers.contrib.hardening.audits.file import ( | ||
3061 | 18 | FilePermissionAudit, | ||
3062 | 19 | ReadOnly, | ||
3063 | 20 | ) | ||
3064 | 21 | from charmhelpers.contrib.hardening import utils | ||
3065 | 22 | |||
3066 | 23 | |||
3067 | 24 | def get_audits(): | ||
3068 | 25 | """Get OS hardening access audits. | ||
3069 | 26 | |||
3070 | 27 | :returns: dictionary of audits | ||
3071 | 28 | """ | ||
3072 | 29 | audits = [] | ||
3073 | 30 | settings = utils.get_settings('os') | ||
3074 | 31 | |||
3075 | 32 | # Remove write permissions from $PATH folders for all regular users. | ||
3076 | 33 | # This prevents changing system-wide commands from normal users. | ||
3077 | 34 | path_folders = {'/usr/local/sbin', | ||
3078 | 35 | '/usr/local/bin', | ||
3079 | 36 | '/usr/sbin', | ||
3080 | 37 | '/usr/bin', | ||
3081 | 38 | '/bin'} | ||
3082 | 39 | extra_user_paths = settings['environment']['extra_user_paths'] | ||
3083 | 40 | path_folders.update(extra_user_paths) | ||
3084 | 41 | audits.append(ReadOnly(path_folders)) | ||
3085 | 42 | |||
3086 | 43 | # Only allow the root user to have access to the shadow file. | ||
3087 | 44 | audits.append(FilePermissionAudit('/etc/shadow', 'root', 'root', 0o0600)) | ||
3088 | 45 | |||
3089 | 46 | if 'change_user' not in settings['security']['users_allow']: | ||
3090 | 47 | # su should only be accessible to user and group root, unless it is | ||
3091 | 48 | # expressly defined to allow users to change to root via the | ||
3092 | 49 | # security_users_allow config option. | ||
3093 | 50 | audits.append(FilePermissionAudit('/bin/su', 'root', 'root', 0o750)) | ||
3094 | 51 | |||
3095 | 52 | return audits | ||
3096 | 53 | 0 | ||
3097 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/pam.py' | |||
3098 | --- hooks/charmhelpers/contrib/hardening/host/checks/pam.py 2016-04-22 04:53:43 +0000 | |||
3099 | +++ hooks/charmhelpers/contrib/hardening/host/checks/pam.py 1970-01-01 00:00:00 +0000 | |||
3100 | @@ -1,134 +0,0 @@ | |||
3101 | 1 | # Copyright 2016 Canonical Limited. | ||
3102 | 2 | # | ||
3103 | 3 | # This file is part of charm-helpers. | ||
3104 | 4 | # | ||
3105 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3106 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3107 | 7 | # published by the Free Software Foundation. | ||
3108 | 8 | # | ||
3109 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3110 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3111 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3112 | 12 | # GNU Lesser General Public License for more details. | ||
3113 | 13 | # | ||
3114 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3115 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3116 | 16 | |||
3117 | 17 | from subprocess import ( | ||
3118 | 18 | check_output, | ||
3119 | 19 | CalledProcessError, | ||
3120 | 20 | ) | ||
3121 | 21 | |||
3122 | 22 | from charmhelpers.core.hookenv import ( | ||
3123 | 23 | log, | ||
3124 | 24 | DEBUG, | ||
3125 | 25 | ERROR, | ||
3126 | 26 | ) | ||
3127 | 27 | from charmhelpers.fetch import ( | ||
3128 | 28 | apt_install, | ||
3129 | 29 | apt_purge, | ||
3130 | 30 | apt_update, | ||
3131 | 31 | ) | ||
3132 | 32 | from charmhelpers.contrib.hardening.audits.file import ( | ||
3133 | 33 | TemplatedFile, | ||
3134 | 34 | DeletedFile, | ||
3135 | 35 | ) | ||
3136 | 36 | from charmhelpers.contrib.hardening import utils | ||
3137 | 37 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
3138 | 38 | |||
3139 | 39 | |||
3140 | 40 | def get_audits(): | ||
3141 | 41 | """Get OS hardening PAM authentication audits. | ||
3142 | 42 | |||
3143 | 43 | :returns: dictionary of audits | ||
3144 | 44 | """ | ||
3145 | 45 | audits = [] | ||
3146 | 46 | |||
3147 | 47 | settings = utils.get_settings('os') | ||
3148 | 48 | |||
3149 | 49 | if settings['auth']['pam_passwdqc_enable']: | ||
3150 | 50 | audits.append(PasswdqcPAM('/etc/passwdqc.conf')) | ||
3151 | 51 | |||
3152 | 52 | if settings['auth']['retries']: | ||
3153 | 53 | audits.append(Tally2PAM('/usr/share/pam-configs/tally2')) | ||
3154 | 54 | else: | ||
3155 | 55 | audits.append(DeletedFile('/usr/share/pam-configs/tally2')) | ||
3156 | 56 | |||
3157 | 57 | return audits | ||
3158 | 58 | |||
3159 | 59 | |||
3160 | 60 | class PasswdqcPAMContext(object): | ||
3161 | 61 | |||
3162 | 62 | def __call__(self): | ||
3163 | 63 | ctxt = {} | ||
3164 | 64 | settings = utils.get_settings('os') | ||
3165 | 65 | |||
3166 | 66 | ctxt['auth_pam_passwdqc_options'] = \ | ||
3167 | 67 | settings['auth']['pam_passwdqc_options'] | ||
3168 | 68 | |||
3169 | 69 | return ctxt | ||
3170 | 70 | |||
3171 | 71 | |||
3172 | 72 | class PasswdqcPAM(TemplatedFile): | ||
3173 | 73 | """The PAM Audit verifies the linux PAM settings.""" | ||
3174 | 74 | def __init__(self, path): | ||
3175 | 75 | super(PasswdqcPAM, self).__init__(path=path, | ||
3176 | 76 | template_dir=TEMPLATES_DIR, | ||
3177 | 77 | context=PasswdqcPAMContext(), | ||
3178 | 78 | user='root', | ||
3179 | 79 | group='root', | ||
3180 | 80 | mode=0o0640) | ||
3181 | 81 | |||
3182 | 82 | def pre_write(self): | ||
3183 | 83 | # Always remove? | ||
3184 | 84 | for pkg in ['libpam-ccreds', 'libpam-cracklib']: | ||
3185 | 85 | log("Purging package '%s'" % pkg, level=DEBUG), | ||
3186 | 86 | apt_purge(pkg) | ||
3187 | 87 | |||
3188 | 88 | apt_update(fatal=True) | ||
3189 | 89 | for pkg in ['libpam-passwdqc']: | ||
3190 | 90 | log("Installing package '%s'" % pkg, level=DEBUG), | ||
3191 | 91 | apt_install(pkg) | ||
3192 | 92 | |||
3193 | 93 | def post_write(self): | ||
3194 | 94 | """Updates the PAM configuration after the file has been written""" | ||
3195 | 95 | try: | ||
3196 | 96 | check_output(['pam-auth-update', '--package']) | ||
3197 | 97 | except CalledProcessError as e: | ||
3198 | 98 | log('Error calling pam-auth-update: %s' % e, level=ERROR) | ||
3199 | 99 | |||
3200 | 100 | |||
3201 | 101 | class Tally2PAMContext(object): | ||
3202 | 102 | |||
3203 | 103 | def __call__(self): | ||
3204 | 104 | ctxt = {} | ||
3205 | 105 | settings = utils.get_settings('os') | ||
3206 | 106 | |||
3207 | 107 | ctxt['auth_lockout_time'] = settings['auth']['lockout_time'] | ||
3208 | 108 | ctxt['auth_retries'] = settings['auth']['retries'] | ||
3209 | 109 | |||
3210 | 110 | return ctxt | ||
3211 | 111 | |||
3212 | 112 | |||
3213 | 113 | class Tally2PAM(TemplatedFile): | ||
3214 | 114 | """The PAM Audit verifies the linux PAM settings.""" | ||
3215 | 115 | def __init__(self, path): | ||
3216 | 116 | super(Tally2PAM, self).__init__(path=path, | ||
3217 | 117 | template_dir=TEMPLATES_DIR, | ||
3218 | 118 | context=Tally2PAMContext(), | ||
3219 | 119 | user='root', | ||
3220 | 120 | group='root', | ||
3221 | 121 | mode=0o0640) | ||
3222 | 122 | |||
3223 | 123 | def pre_write(self): | ||
3224 | 124 | # Always remove? | ||
3225 | 125 | apt_purge('libpam-ccreds') | ||
3226 | 126 | apt_update(fatal=True) | ||
3227 | 127 | apt_install('libpam-modules') | ||
3228 | 128 | |||
3229 | 129 | def post_write(self): | ||
3230 | 130 | """Updates the PAM configuration after the file has been written""" | ||
3231 | 131 | try: | ||
3232 | 132 | check_output(['pam-auth-update', '--package']) | ||
3233 | 133 | except CalledProcessError as e: | ||
3234 | 134 | log('Error calling pam-auth-update: %s' % e, level=ERROR) | ||
3235 | 135 | 0 | ||
3236 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/profile.py' | |||
3237 | --- hooks/charmhelpers/contrib/hardening/host/checks/profile.py 2016-04-22 04:53:43 +0000 | |||
3238 | +++ hooks/charmhelpers/contrib/hardening/host/checks/profile.py 1970-01-01 00:00:00 +0000 | |||
3239 | @@ -1,45 +0,0 @@ | |||
3240 | 1 | # Copyright 2016 Canonical Limited. | ||
3241 | 2 | # | ||
3242 | 3 | # This file is part of charm-helpers. | ||
3243 | 4 | # | ||
3244 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3245 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3246 | 7 | # published by the Free Software Foundation. | ||
3247 | 8 | # | ||
3248 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3249 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3250 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3251 | 12 | # GNU Lesser General Public License for more details. | ||
3252 | 13 | # | ||
3253 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3254 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3255 | 16 | |||
3256 | 17 | from charmhelpers.contrib.hardening.audits.file import TemplatedFile | ||
3257 | 18 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
3258 | 19 | from charmhelpers.contrib.hardening import utils | ||
3259 | 20 | |||
3260 | 21 | |||
3261 | 22 | def get_audits(): | ||
3262 | 23 | """Get OS hardening profile audits. | ||
3263 | 24 | |||
3264 | 25 | :returns: dictionary of audits | ||
3265 | 26 | """ | ||
3266 | 27 | audits = [] | ||
3267 | 28 | |||
3268 | 29 | settings = utils.get_settings('os') | ||
3269 | 30 | |||
3270 | 31 | # If core dumps are not enabled, then don't allow core dumps to be | ||
3271 | 32 | # created as they may contain sensitive information. | ||
3272 | 33 | if not settings['security']['kernel_enable_core_dump']: | ||
3273 | 34 | audits.append(TemplatedFile('/etc/profile.d/pinerolo_profile.sh', | ||
3274 | 35 | ProfileContext(), | ||
3275 | 36 | template_dir=TEMPLATES_DIR, | ||
3276 | 37 | mode=0o0755, user='root', group='root')) | ||
3277 | 38 | return audits | ||
3278 | 39 | |||
3279 | 40 | |||
3280 | 41 | class ProfileContext(object): | ||
3281 | 42 | |||
3282 | 43 | def __call__(self): | ||
3283 | 44 | ctxt = {} | ||
3284 | 45 | return ctxt | ||
3285 | 46 | 0 | ||
3286 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/securetty.py' | |||
3287 | --- hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 2016-04-22 04:53:43 +0000 | |||
3288 | +++ hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 1970-01-01 00:00:00 +0000 | |||
3289 | @@ -1,39 +0,0 @@ | |||
3290 | 1 | # Copyright 2016 Canonical Limited. | ||
3291 | 2 | # | ||
3292 | 3 | # This file is part of charm-helpers. | ||
3293 | 4 | # | ||
3294 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3295 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3296 | 7 | # published by the Free Software Foundation. | ||
3297 | 8 | # | ||
3298 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3299 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3300 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3301 | 12 | # GNU Lesser General Public License for more details. | ||
3302 | 13 | # | ||
3303 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3304 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3305 | 16 | |||
3306 | 17 | from charmhelpers.contrib.hardening.audits.file import TemplatedFile | ||
3307 | 18 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
3308 | 19 | from charmhelpers.contrib.hardening import utils | ||
3309 | 20 | |||
3310 | 21 | |||
3311 | 22 | def get_audits(): | ||
3312 | 23 | """Get OS hardening Secure TTY audits. | ||
3313 | 24 | |||
3314 | 25 | :returns: dictionary of audits | ||
3315 | 26 | """ | ||
3316 | 27 | audits = [] | ||
3317 | 28 | audits.append(TemplatedFile('/etc/securetty', SecureTTYContext(), | ||
3318 | 29 | template_dir=TEMPLATES_DIR, | ||
3319 | 30 | mode=0o0400, user='root', group='root')) | ||
3320 | 31 | return audits | ||
3321 | 32 | |||
3322 | 33 | |||
3323 | 34 | class SecureTTYContext(object): | ||
3324 | 35 | |||
3325 | 36 | def __call__(self): | ||
3326 | 37 | settings = utils.get_settings('os') | ||
3327 | 38 | ctxt = {'ttys': settings['auth']['root_ttys']} | ||
3328 | 39 | return ctxt | ||
3329 | 40 | 0 | ||
3330 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py' | |||
3331 | --- hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 2016-04-22 04:53:43 +0000 | |||
3332 | +++ hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 1970-01-01 00:00:00 +0000 | |||
3333 | @@ -1,131 +0,0 @@ | |||
3334 | 1 | # Copyright 2016 Canonical Limited. | ||
3335 | 2 | # | ||
3336 | 3 | # This file is part of charm-helpers. | ||
3337 | 4 | # | ||
3338 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3339 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3340 | 7 | # published by the Free Software Foundation. | ||
3341 | 8 | # | ||
3342 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3343 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3344 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3345 | 12 | # GNU Lesser General Public License for more details. | ||
3346 | 13 | # | ||
3347 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3348 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3349 | 16 | |||
3350 | 17 | import subprocess | ||
3351 | 18 | |||
3352 | 19 | from charmhelpers.core.hookenv import ( | ||
3353 | 20 | log, | ||
3354 | 21 | INFO, | ||
3355 | 22 | ) | ||
3356 | 23 | from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit | ||
3357 | 24 | from charmhelpers.contrib.hardening import utils | ||
3358 | 25 | |||
3359 | 26 | |||
3360 | 27 | BLACKLIST = ['/usr/bin/rcp', '/usr/bin/rlogin', '/usr/bin/rsh', | ||
3361 | 28 | '/usr/libexec/openssh/ssh-keysign', | ||
3362 | 29 | '/usr/lib/openssh/ssh-keysign', | ||
3363 | 30 | '/sbin/netreport', | ||
3364 | 31 | '/usr/sbin/usernetctl', | ||
3365 | 32 | '/usr/sbin/userisdnctl', | ||
3366 | 33 | '/usr/sbin/pppd', | ||
3367 | 34 | '/usr/bin/lockfile', | ||
3368 | 35 | '/usr/bin/mail-lock', | ||
3369 | 36 | '/usr/bin/mail-unlock', | ||
3370 | 37 | '/usr/bin/mail-touchlock', | ||
3371 | 38 | '/usr/bin/dotlockfile', | ||
3372 | 39 | '/usr/bin/arping', | ||
3373 | 40 | '/usr/sbin/uuidd', | ||
3374 | 41 | '/usr/bin/mtr', | ||
3375 | 42 | '/usr/lib/evolution/camel-lock-helper-1.2', | ||
3376 | 43 | '/usr/lib/pt_chown', | ||
3377 | 44 | '/usr/lib/eject/dmcrypt-get-device', | ||
3378 | 45 | '/usr/lib/mc/cons.saver'] | ||
3379 | 46 | |||
3380 | 47 | WHITELIST = ['/bin/mount', '/bin/ping', '/bin/su', '/bin/umount', | ||
3381 | 48 | '/sbin/pam_timestamp_check', '/sbin/unix_chkpwd', '/usr/bin/at', | ||
3382 | 49 | '/usr/bin/gpasswd', '/usr/bin/locate', '/usr/bin/newgrp', | ||
3383 | 50 | '/usr/bin/passwd', '/usr/bin/ssh-agent', | ||
3384 | 51 | '/usr/libexec/utempter/utempter', '/usr/sbin/lockdev', | ||
3385 | 52 | '/usr/sbin/sendmail.sendmail', '/usr/bin/expiry', | ||
3386 | 53 | '/bin/ping6', '/usr/bin/traceroute6.iputils', | ||
3387 | 54 | '/sbin/mount.nfs', '/sbin/umount.nfs', | ||
3388 | 55 | '/sbin/mount.nfs4', '/sbin/umount.nfs4', | ||
3389 | 56 | '/usr/bin/crontab', | ||
3390 | 57 | '/usr/bin/wall', '/usr/bin/write', | ||
3391 | 58 | '/usr/bin/screen', | ||
3392 | 59 | '/usr/bin/mlocate', | ||
3393 | 60 | '/usr/bin/chage', '/usr/bin/chfn', '/usr/bin/chsh', | ||
3394 | 61 | '/bin/fusermount', | ||
3395 | 62 | '/usr/bin/pkexec', | ||
3396 | 63 | '/usr/bin/sudo', '/usr/bin/sudoedit', | ||
3397 | 64 | '/usr/sbin/postdrop', '/usr/sbin/postqueue', | ||
3398 | 65 | '/usr/sbin/suexec', | ||
3399 | 66 | '/usr/lib/squid/ncsa_auth', '/usr/lib/squid/pam_auth', | ||
3400 | 67 | '/usr/kerberos/bin/ksu', | ||
3401 | 68 | '/usr/sbin/ccreds_validate', | ||
3402 | 69 | '/usr/bin/Xorg', | ||
3403 | 70 | '/usr/bin/X', | ||
3404 | 71 | '/usr/lib/dbus-1.0/dbus-daemon-launch-helper', | ||
3405 | 72 | '/usr/lib/vte/gnome-pty-helper', | ||
3406 | 73 | '/usr/lib/libvte9/gnome-pty-helper', | ||
3407 | 74 | '/usr/lib/libvte-2.90-9/gnome-pty-helper'] | ||
3408 | 75 | |||
3409 | 76 | |||
3410 | 77 | def get_audits(): | ||
3411 | 78 | """Get OS hardening suid/sgid audits. | ||
3412 | 79 | |||
3413 | 80 | :returns: dictionary of audits | ||
3414 | 81 | """ | ||
3415 | 82 | checks = [] | ||
3416 | 83 | settings = utils.get_settings('os') | ||
3417 | 84 | if not settings['security']['suid_sgid_enforce']: | ||
3418 | 85 | log("Skipping suid/sgid hardening", level=INFO) | ||
3419 | 86 | return checks | ||
3420 | 87 | |||
3421 | 88 | # Build the blacklist and whitelist of files for suid/sgid checks. | ||
3422 | 89 | # There are a total of 4 lists: | ||
3423 | 90 | # 1. the system blacklist | ||
3424 | 91 | # 2. the system whitelist | ||
3425 | 92 | # 3. the user blacklist | ||
3426 | 93 | # 4. the user whitelist | ||
3427 | 94 | # | ||
3428 | 95 | # The blacklist is the set of paths which should NOT have the suid/sgid bit | ||
3429 | 96 | # set and the whitelist is the set of paths which MAY have the suid/sgid | ||
3430 | 97 | # bit setl. The user whitelist/blacklist effectively override the system | ||
3431 | 98 | # whitelist/blacklist. | ||
3432 | 99 | u_b = settings['security']['suid_sgid_blacklist'] | ||
3433 | 100 | u_w = settings['security']['suid_sgid_whitelist'] | ||
3434 | 101 | |||
3435 | 102 | blacklist = set(BLACKLIST) - set(u_w + u_b) | ||
3436 | 103 | whitelist = set(WHITELIST) - set(u_b + u_w) | ||
3437 | 104 | |||
3438 | 105 | checks.append(NoSUIDSGIDAudit(blacklist)) | ||
3439 | 106 | |||
3440 | 107 | dry_run = settings['security']['suid_sgid_dry_run_on_unknown'] | ||
3441 | 108 | |||
3442 | 109 | if settings['security']['suid_sgid_remove_from_unknown'] or dry_run: | ||
3443 | 110 | # If the policy is a dry_run (e.g. complain only) or remove unknown | ||
3444 | 111 | # suid/sgid bits then find all of the paths which have the suid/sgid | ||
3445 | 112 | # bit set and then remove the whitelisted paths. | ||
3446 | 113 | root_path = settings['environment']['root_path'] | ||
3447 | 114 | unknown_paths = find_paths_with_suid_sgid(root_path) - set(whitelist) | ||
3448 | 115 | checks.append(NoSUIDSGIDAudit(unknown_paths, unless=dry_run)) | ||
3449 | 116 | |||
3450 | 117 | return checks | ||
3451 | 118 | |||
3452 | 119 | |||
3453 | 120 | def find_paths_with_suid_sgid(root_path): | ||
3454 | 121 | """Finds all paths/files which have an suid/sgid bit enabled. | ||
3455 | 122 | |||
3456 | 123 | Starting with the root_path, this will recursively find all paths which | ||
3457 | 124 | have an suid or sgid bit set. | ||
3458 | 125 | """ | ||
3459 | 126 | cmd = ['find', root_path, '-perm', '-4000', '-o', '-perm', '-2000', | ||
3460 | 127 | '-type', 'f', '!', '-path', '/proc/*', '-print'] | ||
3461 | 128 | |||
3462 | 129 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
3463 | 130 | out, _ = p.communicate() | ||
3464 | 131 | return set(out.split('\n')) | ||
3465 | 132 | 0 | ||
3466 | === removed file 'hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py' | |||
3467 | --- hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 2016-04-22 04:53:43 +0000 | |||
3468 | +++ hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 1970-01-01 00:00:00 +0000 | |||
3469 | @@ -1,211 +0,0 @@ | |||
3470 | 1 | # Copyright 2016 Canonical Limited. | ||
3471 | 2 | # | ||
3472 | 3 | # This file is part of charm-helpers. | ||
3473 | 4 | # | ||
3474 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3475 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3476 | 7 | # published by the Free Software Foundation. | ||
3477 | 8 | # | ||
3478 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3479 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3480 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3481 | 12 | # GNU Lesser General Public License for more details. | ||
3482 | 13 | # | ||
3483 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3484 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3485 | 16 | |||
3486 | 17 | import os | ||
3487 | 18 | import platform | ||
3488 | 19 | import re | ||
3489 | 20 | import six | ||
3490 | 21 | import subprocess | ||
3491 | 22 | |||
3492 | 23 | from charmhelpers.core.hookenv import ( | ||
3493 | 24 | log, | ||
3494 | 25 | INFO, | ||
3495 | 26 | WARNING, | ||
3496 | 27 | ) | ||
3497 | 28 | from charmhelpers.contrib.hardening import utils | ||
3498 | 29 | from charmhelpers.contrib.hardening.audits.file import ( | ||
3499 | 30 | FilePermissionAudit, | ||
3500 | 31 | TemplatedFile, | ||
3501 | 32 | ) | ||
3502 | 33 | from charmhelpers.contrib.hardening.host import TEMPLATES_DIR | ||
3503 | 34 | |||
3504 | 35 | |||
3505 | 36 | SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s | ||
3506 | 37 | net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s | ||
3507 | 38 | net.ipv4.conf.all.rp_filter=1 | ||
3508 | 39 | net.ipv4.conf.default.rp_filter=1 | ||
3509 | 40 | net.ipv4.icmp_echo_ignore_broadcasts=1 | ||
3510 | 41 | net.ipv4.icmp_ignore_bogus_error_responses=1 | ||
3511 | 42 | net.ipv4.icmp_ratelimit=100 | ||
3512 | 43 | net.ipv4.icmp_ratemask=88089 | ||
3513 | 44 | net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s | ||
3514 | 45 | net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s | ||
3515 | 46 | net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s | ||
3516 | 47 | net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s | ||
3517 | 48 | net.ipv4.tcp_rfc1337=1 | ||
3518 | 49 | net.ipv4.tcp_syncookies=1 | ||
3519 | 50 | net.ipv4.conf.all.shared_media=1 | ||
3520 | 51 | net.ipv4.conf.default.shared_media=1 | ||
3521 | 52 | net.ipv4.conf.all.accept_source_route=0 | ||
3522 | 53 | net.ipv4.conf.default.accept_source_route=0 | ||
3523 | 54 | net.ipv4.conf.all.accept_redirects=0 | ||
3524 | 55 | net.ipv4.conf.default.accept_redirects=0 | ||
3525 | 56 | net.ipv6.conf.all.accept_redirects=0 | ||
3526 | 57 | net.ipv6.conf.default.accept_redirects=0 | ||
3527 | 58 | net.ipv4.conf.all.secure_redirects=0 | ||
3528 | 59 | net.ipv4.conf.default.secure_redirects=0 | ||
3529 | 60 | net.ipv4.conf.all.send_redirects=0 | ||
3530 | 61 | net.ipv4.conf.default.send_redirects=0 | ||
3531 | 62 | net.ipv4.conf.all.log_martians=0 | ||
3532 | 63 | net.ipv6.conf.default.router_solicitations=0 | ||
3533 | 64 | net.ipv6.conf.default.accept_ra_rtr_pref=0 | ||
3534 | 65 | net.ipv6.conf.default.accept_ra_pinfo=0 | ||
3535 | 66 | net.ipv6.conf.default.accept_ra_defrtr=0 | ||
3536 | 67 | net.ipv6.conf.default.autoconf=0 | ||
3537 | 68 | net.ipv6.conf.default.dad_transmits=0 | ||
3538 | 69 | net.ipv6.conf.default.max_addresses=1 | ||
3539 | 70 | net.ipv6.conf.all.accept_ra=0 | ||
3540 | 71 | net.ipv6.conf.default.accept_ra=0 | ||
3541 | 72 | kernel.modules_disabled=%(kernel_modules_disabled)s | ||
3542 | 73 | kernel.sysrq=%(kernel_sysrq)s | ||
3543 | 74 | fs.suid_dumpable=%(fs_suid_dumpable)s | ||
3544 | 75 | kernel.randomize_va_space=2 | ||
3545 | 76 | """ | ||
3546 | 77 | |||
3547 | 78 | |||
3548 | 79 | def get_audits(): | ||
3549 | 80 | """Get OS hardening sysctl audits. | ||
3550 | 81 | |||
3551 | 82 | :returns: dictionary of audits | ||
3552 | 83 | """ | ||
3553 | 84 | audits = [] | ||
3554 | 85 | settings = utils.get_settings('os') | ||
3555 | 86 | |||
3556 | 87 | # Apply the sysctl settings which are configured to be applied. | ||
3557 | 88 | audits.append(SysctlConf()) | ||
3558 | 89 | # Make sure that only root has access to the sysctl.conf file, and | ||
3559 | 90 | # that it is read-only. | ||
3560 | 91 | audits.append(FilePermissionAudit('/etc/sysctl.conf', | ||
3561 | 92 | user='root', | ||
3562 | 93 | group='root', mode=0o0440)) | ||
3563 | 94 | # If module loading is not enabled, then ensure that the modules | ||
3564 | 95 | # file has the appropriate permissions and rebuild the initramfs | ||
3565 | 96 | if not settings['security']['kernel_enable_module_loading']: | ||
3566 | 97 | audits.append(ModulesTemplate()) | ||
3567 | 98 | |||
3568 | 99 | return audits | ||
3569 | 100 | |||
3570 | 101 | |||
3571 | 102 | class ModulesContext(object): | ||
3572 | 103 | |||
3573 | 104 | def __call__(self): | ||
3574 | 105 | settings = utils.get_settings('os') | ||
3575 | 106 | with open('/proc/cpuinfo', 'r') as fd: | ||
3576 | 107 | cpuinfo = fd.readlines() | ||
3577 | 108 | |||
3578 | 109 | for line in cpuinfo: | ||
3579 | 110 | match = re.search(r"^vendor_id\s+:\s+(.+)", line) | ||
3580 | 111 | if match: | ||
3581 | 112 | vendor = match.group(1) | ||
3582 | 113 | |||
3583 | 114 | if vendor == "GenuineIntel": | ||
3584 | 115 | vendor = "intel" | ||
3585 | 116 | elif vendor == "AuthenticAMD": | ||
3586 | 117 | vendor = "amd" | ||
3587 | 118 | |||
3588 | 119 | ctxt = {'arch': platform.processor(), | ||
3589 | 120 | 'cpuVendor': vendor, | ||
3590 | 121 | 'desktop_enable': settings['general']['desktop_enable']} | ||
3591 | 122 | |||
3592 | 123 | return ctxt | ||
3593 | 124 | |||
3594 | 125 | |||
3595 | 126 | class ModulesTemplate(object): | ||
3596 | 127 | |||
3597 | 128 | def __init__(self): | ||
3598 | 129 | super(ModulesTemplate, self).__init__('/etc/initramfs-tools/modules', | ||
3599 | 130 | ModulesContext(), | ||
3600 | 131 | templates_dir=TEMPLATES_DIR, | ||
3601 | 132 | user='root', group='root', | ||
3602 | 133 | mode=0o0440) | ||
3603 | 134 | |||
3604 | 135 | def post_write(self): | ||
3605 | 136 | subprocess.check_call(['update-initramfs', '-u']) | ||
3606 | 137 | |||
3607 | 138 | |||
3608 | 139 | class SysCtlHardeningContext(object): | ||
3609 | 140 | def __call__(self): | ||
3610 | 141 | settings = utils.get_settings('os') | ||
3611 | 142 | ctxt = {'sysctl': {}} | ||
3612 | 143 | |||
3613 | 144 | log("Applying sysctl settings", level=INFO) | ||
3614 | 145 | extras = {'net_ipv4_ip_forward': 0, | ||
3615 | 146 | 'net_ipv6_conf_all_forwarding': 0, | ||
3616 | 147 | 'net_ipv6_conf_all_disable_ipv6': 1, | ||
3617 | 148 | 'net_ipv4_tcp_timestamps': 0, | ||
3618 | 149 | 'net_ipv4_conf_all_arp_ignore': 0, | ||
3619 | 150 | 'net_ipv4_conf_all_arp_announce': 0, | ||
3620 | 151 | 'kernel_sysrq': 0, | ||
3621 | 152 | 'fs_suid_dumpable': 0, | ||
3622 | 153 | 'kernel_modules_disabled': 1} | ||
3623 | 154 | |||
3624 | 155 | if settings['sysctl']['ipv6_enable']: | ||
3625 | 156 | extras['net_ipv6_conf_all_disable_ipv6'] = 0 | ||
3626 | 157 | |||
3627 | 158 | if settings['sysctl']['forwarding']: | ||
3628 | 159 | extras['net_ipv4_ip_forward'] = 1 | ||
3629 | 160 | extras['net_ipv6_conf_all_forwarding'] = 1 | ||
3630 | 161 | |||
3631 | 162 | if settings['sysctl']['arp_restricted']: | ||
3632 | 163 | extras['net_ipv4_conf_all_arp_ignore'] = 1 | ||
3633 | 164 | extras['net_ipv4_conf_all_arp_announce'] = 2 | ||
3634 | 165 | |||
3635 | 166 | if settings['security']['kernel_enable_module_loading']: | ||
3636 | 167 | extras['kernel_modules_disabled'] = 0 | ||
3637 | 168 | |||
3638 | 169 | if settings['sysctl']['kernel_enable_sysrq']: | ||
3639 | 170 | sysrq_val = settings['sysctl']['kernel_secure_sysrq'] | ||
3640 | 171 | extras['kernel_sysrq'] = sysrq_val | ||
3641 | 172 | |||
3642 | 173 | if settings['security']['kernel_enable_core_dump']: | ||
3643 | 174 | extras['fs_suid_dumpable'] = 1 | ||
3644 | 175 | |||
3645 | 176 | settings.update(extras) | ||
3646 | 177 | for d in (SYSCTL_DEFAULTS % settings).split(): | ||
3647 | 178 | d = d.strip().partition('=') | ||
3648 | 179 | key = d[0].strip() | ||
3649 | 180 | path = os.path.join('/proc/sys', key.replace('.', '/')) | ||
3650 | 181 | if not os.path.exists(path): | ||
3651 | 182 | log("Skipping '%s' since '%s' does not exist" % (key, path), | ||
3652 | 183 | level=WARNING) | ||
3653 | 184 | continue | ||
3654 | 185 | |||
3655 | 186 | ctxt['sysctl'][key] = d[2] or None | ||
3656 | 187 | |||
3657 | 188 | # Translate for python3 | ||
3658 | 189 | return {'sysctl_settings': | ||
3659 | 190 | [(k, v) for k, v in six.iteritems(ctxt['sysctl'])]} | ||
3660 | 191 | |||
3661 | 192 | |||
3662 | 193 | class SysctlConf(TemplatedFile): | ||
3663 | 194 | """An audit check for sysctl settings.""" | ||
3664 | 195 | def __init__(self): | ||
3665 | 196 | self.conffile = '/etc/sysctl.d/99-juju-hardening.conf' | ||
3666 | 197 | super(SysctlConf, self).__init__(self.conffile, | ||
3667 | 198 | SysCtlHardeningContext(), | ||
3668 | 199 | template_dir=TEMPLATES_DIR, | ||
3669 | 200 | user='root', group='root', | ||
3670 | 201 | mode=0o0440) | ||
3671 | 202 | |||
3672 | 203 | def post_write(self): | ||
3673 | 204 | try: | ||
3674 | 205 | subprocess.check_call(['sysctl', '-p', self.conffile]) | ||
3675 | 206 | except subprocess.CalledProcessError as e: | ||
3676 | 207 | # NOTE: on some systems if sysctl cannot apply all settings it | ||
3677 | 208 | # will return non-zero as well. | ||
3678 | 209 | log("sysctl command returned an error (maybe some " | ||
3679 | 210 | "keys could not be set) - %s" % (e), | ||
3680 | 211 | level=WARNING) | ||
3681 | 212 | 0 | ||
3682 | === removed directory 'hooks/charmhelpers/contrib/hardening/mysql' | |||
3683 | === removed file 'hooks/charmhelpers/contrib/hardening/mysql/__init__.py' | |||
3684 | --- hooks/charmhelpers/contrib/hardening/mysql/__init__.py 2016-04-22 04:53:43 +0000 | |||
3685 | +++ hooks/charmhelpers/contrib/hardening/mysql/__init__.py 1970-01-01 00:00:00 +0000 | |||
3686 | @@ -1,19 +0,0 @@ | |||
3687 | 1 | # Copyright 2016 Canonical Limited. | ||
3688 | 2 | # | ||
3689 | 3 | # This file is part of charm-helpers. | ||
3690 | 4 | # | ||
3691 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3692 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3693 | 7 | # published by the Free Software Foundation. | ||
3694 | 8 | # | ||
3695 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3696 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3697 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3698 | 12 | # GNU Lesser General Public License for more details. | ||
3699 | 13 | # | ||
3700 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3701 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3702 | 16 | |||
3703 | 17 | from os import path | ||
3704 | 18 | |||
3705 | 19 | TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates') | ||
3706 | 20 | 0 | ||
3707 | === removed directory 'hooks/charmhelpers/contrib/hardening/mysql/checks' | |||
3708 | === removed file 'hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py' | |||
3709 | --- hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 2016-04-22 04:53:43 +0000 | |||
3710 | +++ hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 1970-01-01 00:00:00 +0000 | |||
3711 | @@ -1,31 +0,0 @@ | |||
3712 | 1 | # Copyright 2016 Canonical Limited. | ||
3713 | 2 | # | ||
3714 | 3 | # This file is part of charm-helpers. | ||
3715 | 4 | # | ||
3716 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3717 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3718 | 7 | # published by the Free Software Foundation. | ||
3719 | 8 | # | ||
3720 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3721 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3722 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3723 | 12 | # GNU Lesser General Public License for more details. | ||
3724 | 13 | # | ||
3725 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3726 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3727 | 16 | |||
3728 | 17 | from charmhelpers.core.hookenv import ( | ||
3729 | 18 | log, | ||
3730 | 19 | DEBUG, | ||
3731 | 20 | ) | ||
3732 | 21 | from charmhelpers.contrib.hardening.mysql.checks import config | ||
3733 | 22 | |||
3734 | 23 | |||
3735 | 24 | def run_mysql_checks(): | ||
3736 | 25 | log("Starting MySQL hardening checks.", level=DEBUG) | ||
3737 | 26 | checks = config.get_audits() | ||
3738 | 27 | for check in checks: | ||
3739 | 28 | log("Running '%s' check" % (check.__class__.__name__), level=DEBUG) | ||
3740 | 29 | check.ensure_compliance() | ||
3741 | 30 | |||
3742 | 31 | log("MySQL hardening checks complete.", level=DEBUG) | ||
3743 | 32 | 0 | ||
3744 | === removed file 'hooks/charmhelpers/contrib/hardening/mysql/checks/config.py' | |||
3745 | --- hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 2016-04-22 04:53:43 +0000 | |||
3746 | +++ hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 1970-01-01 00:00:00 +0000 | |||
3747 | @@ -1,89 +0,0 @@ | |||
3748 | 1 | # Copyright 2016 Canonical Limited. | ||
3749 | 2 | # | ||
3750 | 3 | # This file is part of charm-helpers. | ||
3751 | 4 | # | ||
3752 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3753 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3754 | 7 | # published by the Free Software Foundation. | ||
3755 | 8 | # | ||
3756 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3757 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3758 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3759 | 12 | # GNU Lesser General Public License for more details. | ||
3760 | 13 | # | ||
3761 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3762 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3763 | 16 | |||
3764 | 17 | import six | ||
3765 | 18 | import subprocess | ||
3766 | 19 | |||
3767 | 20 | from charmhelpers.core.hookenv import ( | ||
3768 | 21 | log, | ||
3769 | 22 | WARNING, | ||
3770 | 23 | ) | ||
3771 | 24 | from charmhelpers.contrib.hardening.audits.file import ( | ||
3772 | 25 | FilePermissionAudit, | ||
3773 | 26 | DirectoryPermissionAudit, | ||
3774 | 27 | TemplatedFile, | ||
3775 | 28 | ) | ||
3776 | 29 | from charmhelpers.contrib.hardening.mysql import TEMPLATES_DIR | ||
3777 | 30 | from charmhelpers.contrib.hardening import utils | ||
3778 | 31 | |||
3779 | 32 | |||
3780 | 33 | def get_audits(): | ||
3781 | 34 | """Get MySQL hardening config audits. | ||
3782 | 35 | |||
3783 | 36 | :returns: dictionary of audits | ||
3784 | 37 | """ | ||
3785 | 38 | if subprocess.call(['which', 'mysql'], stdout=subprocess.PIPE) != 0: | ||
3786 | 39 | log("MySQL does not appear to be installed on this node - " | ||
3787 | 40 | "skipping mysql hardening", level=WARNING) | ||
3788 | 41 | return [] | ||
3789 | 42 | |||
3790 | 43 | settings = utils.get_settings('mysql') | ||
3791 | 44 | hardening_settings = settings['hardening'] | ||
3792 | 45 | my_cnf = hardening_settings['mysql-conf'] | ||
3793 | 46 | |||
3794 | 47 | audits = [ | ||
3795 | 48 | FilePermissionAudit(paths=[my_cnf], user='root', | ||
3796 | 49 | group='root', mode=0o0600), | ||
3797 | 50 | |||
3798 | 51 | TemplatedFile(hardening_settings['hardening-conf'], | ||
3799 | 52 | MySQLConfContext(), | ||
3800 | 53 | TEMPLATES_DIR, | ||
3801 | 54 | mode=0o0750, | ||
3802 | 55 | user='mysql', | ||
3803 | 56 | group='root', | ||
3804 | 57 | service_actions=[{'service': 'mysql', | ||
3805 | 58 | 'actions': ['restart']}]), | ||
3806 | 59 | |||
3807 | 60 | # MySQL and Percona charms do not allow configuration of the | ||
3808 | 61 | # data directory, so use the default. | ||
3809 | 62 | DirectoryPermissionAudit('/var/lib/mysql', | ||
3810 | 63 | user='mysql', | ||
3811 | 64 | group='mysql', | ||
3812 | 65 | recursive=False, | ||
3813 | 66 | mode=0o755), | ||
3814 | 67 | |||
3815 | 68 | DirectoryPermissionAudit('/etc/mysql', | ||
3816 | 69 | user='root', | ||
3817 | 70 | group='root', | ||
3818 | 71 | recursive=False, | ||
3819 | 72 | mode=0o700), | ||
3820 | 73 | ] | ||
3821 | 74 | |||
3822 | 75 | return audits | ||
3823 | 76 | |||
3824 | 77 | |||
3825 | 78 | class MySQLConfContext(object): | ||
3826 | 79 | """Defines the set of key/value pairs to set in a mysql config file. | ||
3827 | 80 | |||
3828 | 81 | This context, when called, will return a dictionary containing the | ||
3829 | 82 | key/value pairs of setting to specify in the | ||
3830 | 83 | /etc/mysql/conf.d/hardening.cnf file. | ||
3831 | 84 | """ | ||
3832 | 85 | def __call__(self): | ||
3833 | 86 | settings = utils.get_settings('mysql') | ||
3834 | 87 | # Translate for python3 | ||
3835 | 88 | return {'mysql_settings': | ||
3836 | 89 | [(k, v) for k, v in six.iteritems(settings['security'])]} | ||
3837 | 90 | 0 | ||
3838 | === removed directory 'hooks/charmhelpers/contrib/hardening/ssh' | |||
3839 | === removed file 'hooks/charmhelpers/contrib/hardening/ssh/__init__.py' | |||
3840 | --- hooks/charmhelpers/contrib/hardening/ssh/__init__.py 2016-04-22 04:53:43 +0000 | |||
3841 | +++ hooks/charmhelpers/contrib/hardening/ssh/__init__.py 1970-01-01 00:00:00 +0000 | |||
3842 | @@ -1,19 +0,0 @@ | |||
3843 | 1 | # Copyright 2016 Canonical Limited. | ||
3844 | 2 | # | ||
3845 | 3 | # This file is part of charm-helpers. | ||
3846 | 4 | # | ||
3847 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3848 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3849 | 7 | # published by the Free Software Foundation. | ||
3850 | 8 | # | ||
3851 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3852 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3853 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3854 | 12 | # GNU Lesser General Public License for more details. | ||
3855 | 13 | # | ||
3856 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3857 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3858 | 16 | |||
3859 | 17 | from os import path | ||
3860 | 18 | |||
3861 | 19 | TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates') | ||
3862 | 20 | 0 | ||
3863 | === removed directory 'hooks/charmhelpers/contrib/hardening/ssh/checks' | |||
3864 | === removed file 'hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py' | |||
3865 | --- hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 2016-04-22 04:53:43 +0000 | |||
3866 | +++ hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 1970-01-01 00:00:00 +0000 | |||
3867 | @@ -1,31 +0,0 @@ | |||
3868 | 1 | # Copyright 2016 Canonical Limited. | ||
3869 | 2 | # | ||
3870 | 3 | # This file is part of charm-helpers. | ||
3871 | 4 | # | ||
3872 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3873 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3874 | 7 | # published by the Free Software Foundation. | ||
3875 | 8 | # | ||
3876 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3877 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3878 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3879 | 12 | # GNU Lesser General Public License for more details. | ||
3880 | 13 | # | ||
3881 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3882 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3883 | 16 | |||
3884 | 17 | from charmhelpers.core.hookenv import ( | ||
3885 | 18 | log, | ||
3886 | 19 | DEBUG, | ||
3887 | 20 | ) | ||
3888 | 21 | from charmhelpers.contrib.hardening.ssh.checks import config | ||
3889 | 22 | |||
3890 | 23 | |||
3891 | 24 | def run_ssh_checks(): | ||
3892 | 25 | log("Starting SSH hardening checks.", level=DEBUG) | ||
3893 | 26 | checks = config.get_audits() | ||
3894 | 27 | for check in checks: | ||
3895 | 28 | log("Running '%s' check" % (check.__class__.__name__), level=DEBUG) | ||
3896 | 29 | check.ensure_compliance() | ||
3897 | 30 | |||
3898 | 31 | log("SSH hardening checks complete.", level=DEBUG) | ||
3899 | 32 | 0 | ||
3900 | === removed file 'hooks/charmhelpers/contrib/hardening/ssh/checks/config.py' | |||
3901 | --- hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 2016-04-22 04:53:43 +0000 | |||
3902 | +++ hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 1970-01-01 00:00:00 +0000 | |||
3903 | @@ -1,394 +0,0 @@ | |||
3904 | 1 | # Copyright 2016 Canonical Limited. | ||
3905 | 2 | # | ||
3906 | 3 | # This file is part of charm-helpers. | ||
3907 | 4 | # | ||
3908 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
3909 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
3910 | 7 | # published by the Free Software Foundation. | ||
3911 | 8 | # | ||
3912 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
3913 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3914 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3915 | 12 | # GNU Lesser General Public License for more details. | ||
3916 | 13 | # | ||
3917 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3918 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3919 | 16 | |||
3920 | 17 | import os | ||
3921 | 18 | |||
3922 | 19 | from charmhelpers.core.hookenv import ( | ||
3923 | 20 | log, | ||
3924 | 21 | DEBUG, | ||
3925 | 22 | ) | ||
3926 | 23 | from charmhelpers.fetch import ( | ||
3927 | 24 | apt_install, | ||
3928 | 25 | apt_update, | ||
3929 | 26 | ) | ||
3930 | 27 | from charmhelpers.core.host import lsb_release | ||
3931 | 28 | from charmhelpers.contrib.hardening.audits.file import ( | ||
3932 | 29 | TemplatedFile, | ||
3933 | 30 | FileContentAudit, | ||
3934 | 31 | ) | ||
3935 | 32 | from charmhelpers.contrib.hardening.ssh import TEMPLATES_DIR | ||
3936 | 33 | from charmhelpers.contrib.hardening import utils | ||
3937 | 34 | |||
3938 | 35 | |||
3939 | 36 | def get_audits(): | ||
3940 | 37 | """Get SSH hardening config audits. | ||
3941 | 38 | |||
3942 | 39 | :returns: dictionary of audits | ||
3943 | 40 | """ | ||
3944 | 41 | audits = [SSHConfig(), SSHDConfig(), SSHConfigFileContentAudit(), | ||
3945 | 42 | SSHDConfigFileContentAudit()] | ||
3946 | 43 | return audits | ||
3947 | 44 | |||
3948 | 45 | |||
3949 | 46 | class SSHConfigContext(object): | ||
3950 | 47 | |||
3951 | 48 | type = 'client' | ||
3952 | 49 | |||
3953 | 50 | def get_macs(self, allow_weak_mac): | ||
3954 | 51 | if allow_weak_mac: | ||
3955 | 52 | weak_macs = 'weak' | ||
3956 | 53 | else: | ||
3957 | 54 | weak_macs = 'default' | ||
3958 | 55 | |||
3959 | 56 | default = 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160' | ||
3960 | 57 | macs = {'default': default, | ||
3961 | 58 | 'weak': default + ',hmac-sha1'} | ||
3962 | 59 | |||
3963 | 60 | default = ('hmac-sha2-512-etm@openssh.com,' | ||
3964 | 61 | 'hmac-sha2-256-etm@openssh.com,' | ||
3965 | 62 | 'hmac-ripemd160-etm@openssh.com,umac-128-etm@openssh.com,' | ||
3966 | 63 | 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160') | ||
3967 | 64 | macs_66 = {'default': default, | ||
3968 | 65 | 'weak': default + ',hmac-sha1'} | ||
3969 | 66 | |||
3970 | 67 | # Use newer ciphers on Ubuntu Trusty and above | ||
3971 | 68 | if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty': | ||
3972 | 69 | log("Detected Ubuntu 14.04 or newer, using new macs", level=DEBUG) | ||
3973 | 70 | macs = macs_66 | ||
3974 | 71 | |||
3975 | 72 | return macs[weak_macs] | ||
3976 | 73 | |||
3977 | 74 | def get_kexs(self, allow_weak_kex): | ||
3978 | 75 | if allow_weak_kex: | ||
3979 | 76 | weak_kex = 'weak' | ||
3980 | 77 | else: | ||
3981 | 78 | weak_kex = 'default' | ||
3982 | 79 | |||
3983 | 80 | default = 'diffie-hellman-group-exchange-sha256' | ||
3984 | 81 | weak = (default + ',diffie-hellman-group14-sha1,' | ||
3985 | 82 | 'diffie-hellman-group-exchange-sha1,' | ||
3986 | 83 | 'diffie-hellman-group1-sha1') | ||
3987 | 84 | kex = {'default': default, | ||
3988 | 85 | 'weak': weak} | ||
3989 | 86 | |||
3990 | 87 | default = ('curve25519-sha256@libssh.org,' | ||
3991 | 88 | 'diffie-hellman-group-exchange-sha256') | ||
3992 | 89 | weak = (default + ',diffie-hellman-group14-sha1,' | ||
3993 | 90 | 'diffie-hellman-group-exchange-sha1,' | ||
3994 | 91 | 'diffie-hellman-group1-sha1') | ||
3995 | 92 | kex_66 = {'default': default, | ||
3996 | 93 | 'weak': weak} | ||
3997 | 94 | |||
3998 | 95 | # Use newer kex on Ubuntu Trusty and above | ||
3999 | 96 | if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty': | ||
4000 | 97 | log('Detected Ubuntu 14.04 or newer, using new key exchange ' | ||
4001 | 98 | 'algorithms', level=DEBUG) | ||
4002 | 99 | kex = kex_66 | ||
4003 | 100 | |||
4004 | 101 | return kex[weak_kex] | ||
4005 | 102 | |||
4006 | 103 | def get_ciphers(self, cbc_required): | ||
4007 | 104 | if cbc_required: | ||
4008 | 105 | weak_ciphers = 'weak' | ||
4009 | 106 | else: | ||
4010 | 107 | weak_ciphers = 'default' | ||
4011 | 108 | |||
4012 | 109 | default = 'aes256-ctr,aes192-ctr,aes128-ctr' | ||
4013 | 110 | cipher = {'default': default, | ||
4014 | 111 | 'weak': default + 'aes256-cbc,aes192-cbc,aes128-cbc'} | ||
4015 | 112 | |||
4016 | 113 | default = ('chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,' | ||
4017 | 114 | 'aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr') | ||
4018 | 115 | ciphers_66 = {'default': default, | ||
4019 | 116 | 'weak': default + ',aes256-cbc,aes192-cbc,aes128-cbc'} | ||
4020 | 117 | |||
4021 | 118 | # Use newer ciphers on ubuntu Trusty and above | ||
4022 | 119 | if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty': | ||
4023 | 120 | log('Detected Ubuntu 14.04 or newer, using new ciphers', | ||
4024 | 121 | level=DEBUG) | ||
4025 | 122 | cipher = ciphers_66 | ||
4026 | 123 | |||
4027 | 124 | return cipher[weak_ciphers] | ||
4028 | 125 | |||
4029 | 126 | def __call__(self): | ||
4030 | 127 | settings = utils.get_settings('ssh') | ||
4031 | 128 | if settings['common']['network_ipv6_enable']: | ||
4032 | 129 | addr_family = 'any' | ||
4033 | 130 | else: | ||
4034 | 131 | addr_family = 'inet' | ||
4035 | 132 | |||
4036 | 133 | ctxt = { | ||
4037 | 134 | 'addr_family': addr_family, | ||
4038 | 135 | 'remote_hosts': settings['common']['remote_hosts'], | ||
4039 | 136 | 'password_auth_allowed': | ||
4040 | 137 | settings['client']['password_authentication'], | ||
4041 | 138 | 'ports': settings['common']['ports'], | ||
4042 | 139 | 'ciphers': self.get_ciphers(settings['client']['cbc_required']), | ||
4043 | 140 | 'macs': self.get_macs(settings['client']['weak_hmac']), | ||
4044 | 141 | 'kexs': self.get_kexs(settings['client']['weak_kex']), | ||
4045 | 142 | 'roaming': settings['client']['roaming'], | ||
4046 | 143 | } | ||
4047 | 144 | return ctxt | ||
4048 | 145 | |||
4049 | 146 | |||
4050 | 147 | class SSHConfig(TemplatedFile): | ||
4051 | 148 | def __init__(self): | ||
4052 | 149 | path = '/etc/ssh/ssh_config' | ||
4053 | 150 | super(SSHConfig, self).__init__(path=path, | ||
4054 | 151 | template_dir=TEMPLATES_DIR, | ||
4055 | 152 | context=SSHConfigContext(), | ||
4056 | 153 | user='root', | ||
4057 | 154 | group='root', | ||
4058 | 155 | mode=0o0644) | ||
4059 | 156 | |||
4060 | 157 | def pre_write(self): | ||
4061 | 158 | settings = utils.get_settings('ssh') | ||
4062 | 159 | apt_update(fatal=True) | ||
4063 | 160 | apt_install(settings['client']['package']) | ||
4064 | 161 | if not os.path.exists('/etc/ssh'): | ||
4065 | 162 | os.makedir('/etc/ssh') | ||
4066 | 163 | # NOTE: don't recurse | ||
4067 | 164 | utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755, | ||
4068 | 165 | maxdepth=0) | ||
4069 | 166 | |||
4070 | 167 | def post_write(self): | ||
4071 | 168 | # NOTE: don't recurse | ||
4072 | 169 | utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755, | ||
4073 | 170 | maxdepth=0) | ||
4074 | 171 | |||
4075 | 172 | |||
4076 | 173 | class SSHDConfigContext(SSHConfigContext): | ||
4077 | 174 | |||
4078 | 175 | type = 'server' | ||
4079 | 176 | |||
4080 | 177 | def __call__(self): | ||
4081 | 178 | settings = utils.get_settings('ssh') | ||
4082 | 179 | if settings['common']['network_ipv6_enable']: | ||
4083 | 180 | addr_family = 'any' | ||
4084 | 181 | else: | ||
4085 | 182 | addr_family = 'inet' | ||
4086 | 183 | |||
4087 | 184 | ctxt = { | ||
4088 | 185 | 'ssh_ip': settings['server']['listen_to'], | ||
4089 | 186 | 'password_auth_allowed': | ||
4090 | 187 | settings['server']['password_authentication'], | ||
4091 | 188 | 'ports': settings['common']['ports'], | ||
4092 | 189 | 'addr_family': addr_family, | ||
4093 | 190 | 'ciphers': self.get_ciphers(settings['server']['cbc_required']), | ||
4094 | 191 | 'macs': self.get_macs(settings['server']['weak_hmac']), | ||
4095 | 192 | 'kexs': self.get_kexs(settings['server']['weak_kex']), | ||
4096 | 193 | 'host_key_files': settings['server']['host_key_files'], | ||
4097 | 194 | 'allow_root_with_key': settings['server']['allow_root_with_key'], | ||
4098 | 195 | 'password_authentication': | ||
4099 | 196 | settings['server']['password_authentication'], | ||
4100 | 197 | 'use_priv_sep': settings['server']['use_privilege_separation'], | ||
4101 | 198 | 'use_pam': settings['server']['use_pam'], | ||
4102 | 199 | 'allow_x11_forwarding': settings['server']['allow_x11_forwarding'], | ||
4103 | 200 | 'print_motd': settings['server']['print_motd'], | ||
4104 | 201 | 'print_last_log': settings['server']['print_last_log'], | ||
4105 | 202 | 'client_alive_interval': | ||
4106 | 203 | settings['server']['alive_interval'], | ||
4107 | 204 | 'client_alive_count': settings['server']['alive_count'], | ||
4108 | 205 | 'allow_tcp_forwarding': settings['server']['allow_tcp_forwarding'], | ||
4109 | 206 | 'allow_agent_forwarding': | ||
4110 | 207 | settings['server']['allow_agent_forwarding'], | ||
4111 | 208 | 'deny_users': settings['server']['deny_users'], | ||
4112 | 209 | 'allow_users': settings['server']['allow_users'], | ||
4113 | 210 | 'deny_groups': settings['server']['deny_groups'], | ||
4114 | 211 | 'allow_groups': settings['server']['allow_groups'], | ||
4115 | 212 | 'use_dns': settings['server']['use_dns'], | ||
4116 | 213 | 'sftp_enable': settings['server']['sftp_enable'], | ||
4117 | 214 | 'sftp_group': settings['server']['sftp_group'], | ||
4118 | 215 | 'sftp_chroot': settings['server']['sftp_chroot'], | ||
4119 | 216 | 'max_auth_tries': settings['server']['max_auth_tries'], | ||
4120 | 217 | 'max_sessions': settings['server']['max_sessions'], | ||
4121 | 218 | } | ||
4122 | 219 | return ctxt | ||
4123 | 220 | |||
4124 | 221 | |||
4125 | 222 | class SSHDConfig(TemplatedFile): | ||
4126 | 223 | def __init__(self): | ||
4127 | 224 | path = '/etc/ssh/sshd_config' | ||
4128 | 225 | super(SSHDConfig, self).__init__(path=path, | ||
4129 | 226 | template_dir=TEMPLATES_DIR, | ||
4130 | 227 | context=SSHDConfigContext(), | ||
4131 | 228 | user='root', | ||
4132 | 229 | group='root', | ||
4133 | 230 | mode=0o0600, | ||
4134 | 231 | service_actions=[{'service': 'ssh', | ||
4135 | 232 | 'actions': | ||
4136 | 233 | ['restart']}]) | ||
4137 | 234 | |||
4138 | 235 | def pre_write(self): | ||
4139 | 236 | settings = utils.get_settings('ssh') | ||
4140 | 237 | apt_update(fatal=True) | ||
4141 | 238 | apt_install(settings['server']['package']) | ||
4142 | 239 | if not os.path.exists('/etc/ssh'): | ||
4143 | 240 | os.makedir('/etc/ssh') | ||
4144 | 241 | # NOTE: don't recurse | ||
4145 | 242 | utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755, | ||
4146 | 243 | maxdepth=0) | ||
4147 | 244 | |||
4148 | 245 | def post_write(self): | ||
4149 | 246 | # NOTE: don't recurse | ||
4150 | 247 | utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755, | ||
4151 | 248 | maxdepth=0) | ||
4152 | 249 | |||
4153 | 250 | |||
4154 | 251 | class SSHConfigFileContentAudit(FileContentAudit): | ||
4155 | 252 | def __init__(self): | ||
4156 | 253 | self.path = '/etc/ssh/ssh_config' | ||
4157 | 254 | super(SSHConfigFileContentAudit, self).__init__(self.path, {}) | ||
4158 | 255 | |||
4159 | 256 | def is_compliant(self, *args, **kwargs): | ||
4160 | 257 | self.pass_cases = [] | ||
4161 | 258 | self.fail_cases = [] | ||
4162 | 259 | settings = utils.get_settings('ssh') | ||
4163 | 260 | |||
4164 | 261 | if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty': | ||
4165 | 262 | if not settings['server']['weak_hmac']: | ||
4166 | 263 | self.pass_cases.append(r'^MACs.+,hmac-ripemd160$') | ||
4167 | 264 | else: | ||
4168 | 265 | self.pass_cases.append(r'^MACs.+,hmac-sha1$') | ||
4169 | 266 | |||
4170 | 267 | if settings['server']['weak_kex']: | ||
4171 | 268 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa | ||
4172 | 269 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4173 | 270 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4174 | 271 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4175 | 272 | else: | ||
4176 | 273 | self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa | ||
4177 | 274 | self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa | ||
4178 | 275 | |||
4179 | 276 | if settings['server']['cbc_required']: | ||
4180 | 277 | self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4181 | 278 | self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4182 | 279 | self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4183 | 280 | self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4184 | 281 | else: | ||
4185 | 282 | self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4186 | 283 | self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa | ||
4187 | 284 | self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$') | ||
4188 | 285 | self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4189 | 286 | self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4190 | 287 | else: | ||
4191 | 288 | if not settings['client']['weak_hmac']: | ||
4192 | 289 | self.fail_cases.append(r'^MACs.+,hmac-sha1$') | ||
4193 | 290 | else: | ||
4194 | 291 | self.pass_cases.append(r'^MACs.+,hmac-sha1$') | ||
4195 | 292 | |||
4196 | 293 | if settings['client']['weak_kex']: | ||
4197 | 294 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa | ||
4198 | 295 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4199 | 296 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4200 | 297 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4201 | 298 | else: | ||
4202 | 299 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa | ||
4203 | 300 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4204 | 301 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4205 | 302 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4206 | 303 | |||
4207 | 304 | if settings['client']['cbc_required']: | ||
4208 | 305 | self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4209 | 306 | self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4210 | 307 | self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4211 | 308 | self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4212 | 309 | else: | ||
4213 | 310 | self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4214 | 311 | self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4215 | 312 | self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4216 | 313 | self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4217 | 314 | |||
4218 | 315 | if settings['client']['roaming']: | ||
4219 | 316 | self.pass_cases.append(r'^UseRoaming yes$') | ||
4220 | 317 | else: | ||
4221 | 318 | self.fail_cases.append(r'^UseRoaming yes$') | ||
4222 | 319 | |||
4223 | 320 | return super(SSHConfigFileContentAudit, self).is_compliant(*args, | ||
4224 | 321 | **kwargs) | ||
4225 | 322 | |||
4226 | 323 | |||
4227 | 324 | class SSHDConfigFileContentAudit(FileContentAudit): | ||
4228 | 325 | def __init__(self): | ||
4229 | 326 | self.path = '/etc/ssh/sshd_config' | ||
4230 | 327 | super(SSHDConfigFileContentAudit, self).__init__(self.path, {}) | ||
4231 | 328 | |||
4232 | 329 | def is_compliant(self, *args, **kwargs): | ||
4233 | 330 | self.pass_cases = [] | ||
4234 | 331 | self.fail_cases = [] | ||
4235 | 332 | settings = utils.get_settings('ssh') | ||
4236 | 333 | |||
4237 | 334 | if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty': | ||
4238 | 335 | if not settings['server']['weak_hmac']: | ||
4239 | 336 | self.pass_cases.append(r'^MACs.+,hmac-ripemd160$') | ||
4240 | 337 | else: | ||
4241 | 338 | self.pass_cases.append(r'^MACs.+,hmac-sha1$') | ||
4242 | 339 | |||
4243 | 340 | if settings['server']['weak_kex']: | ||
4244 | 341 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa | ||
4245 | 342 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4246 | 343 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4247 | 344 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4248 | 345 | else: | ||
4249 | 346 | self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa | ||
4250 | 347 | self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa | ||
4251 | 348 | |||
4252 | 349 | if settings['server']['cbc_required']: | ||
4253 | 350 | self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4254 | 351 | self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4255 | 352 | self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4256 | 353 | self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4257 | 354 | else: | ||
4258 | 355 | self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4259 | 356 | self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa | ||
4260 | 357 | self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$') | ||
4261 | 358 | self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4262 | 359 | self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4263 | 360 | else: | ||
4264 | 361 | if not settings['server']['weak_hmac']: | ||
4265 | 362 | self.pass_cases.append(r'^MACs.+,hmac-ripemd160$') | ||
4266 | 363 | else: | ||
4267 | 364 | self.pass_cases.append(r'^MACs.+,hmac-sha1$') | ||
4268 | 365 | |||
4269 | 366 | if settings['server']['weak_kex']: | ||
4270 | 367 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa | ||
4271 | 368 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4272 | 369 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4273 | 370 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4274 | 371 | else: | ||
4275 | 372 | self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa | ||
4276 | 373 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa | ||
4277 | 374 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa | ||
4278 | 375 | self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa | ||
4279 | 376 | |||
4280 | 377 | if settings['server']['cbc_required']: | ||
4281 | 378 | self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4282 | 379 | self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4283 | 380 | self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4284 | 381 | self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4285 | 382 | else: | ||
4286 | 383 | self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?') | ||
4287 | 384 | self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?') | ||
4288 | 385 | self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?') | ||
4289 | 386 | self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?') | ||
4290 | 387 | |||
4291 | 388 | if settings['server']['sftp_enable']: | ||
4292 | 389 | self.pass_cases.append(r'^Subsystem\ssftp') | ||
4293 | 390 | else: | ||
4294 | 391 | self.fail_cases.append(r'^Subsystem\ssftp') | ||
4295 | 392 | |||
4296 | 393 | return super(SSHDConfigFileContentAudit, self).is_compliant(*args, | ||
4297 | 394 | **kwargs) | ||
4298 | 395 | 0 | ||
4299 | === removed file 'hooks/charmhelpers/contrib/hardening/templating.py' | |||
4300 | --- hooks/charmhelpers/contrib/hardening/templating.py 2016-04-22 04:53:43 +0000 | |||
4301 | +++ hooks/charmhelpers/contrib/hardening/templating.py 1970-01-01 00:00:00 +0000 | |||
4302 | @@ -1,71 +0,0 @@ | |||
4303 | 1 | # Copyright 2016 Canonical Limited. | ||
4304 | 2 | # | ||
4305 | 3 | # This file is part of charm-helpers. | ||
4306 | 4 | # | ||
4307 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
4308 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
4309 | 7 | # published by the Free Software Foundation. | ||
4310 | 8 | # | ||
4311 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
4312 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4313 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4314 | 12 | # GNU Lesser General Public License for more details. | ||
4315 | 13 | # | ||
4316 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4317 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
4318 | 16 | |||
4319 | 17 | import os | ||
4320 | 18 | |||
4321 | 19 | from charmhelpers.core.hookenv import ( | ||
4322 | 20 | log, | ||
4323 | 21 | DEBUG, | ||
4324 | 22 | WARNING, | ||
4325 | 23 | ) | ||
4326 | 24 | |||
4327 | 25 | try: | ||
4328 | 26 | from jinja2 import FileSystemLoader, Environment | ||
4329 | 27 | except ImportError: | ||
4330 | 28 | from charmhelpers.fetch import apt_install | ||
4331 | 29 | from charmhelpers.fetch import apt_update | ||
4332 | 30 | apt_update(fatal=True) | ||
4333 | 31 | apt_install('python-jinja2', fatal=True) | ||
4334 | 32 | from jinja2 import FileSystemLoader, Environment | ||
4335 | 33 | |||
4336 | 34 | |||
4337 | 35 | # NOTE: function separated from main rendering code to facilitate easier | ||
4338 | 36 | # mocking in unit tests. | ||
4339 | 37 | def write(path, data): | ||
4340 | 38 | with open(path, 'wb') as out: | ||
4341 | 39 | out.write(data) | ||
4342 | 40 | |||
4343 | 41 | |||
4344 | 42 | def get_template_path(template_dir, path): | ||
4345 | 43 | """Returns the template file which would be used to render the path. | ||
4346 | 44 | |||
4347 | 45 | The path to the template file is returned. | ||
4348 | 46 | :param template_dir: the directory the templates are located in | ||
4349 | 47 | :param path: the file path to be written to. | ||
4350 | 48 | :returns: path to the template file | ||
4351 | 49 | """ | ||
4352 | 50 | return os.path.join(template_dir, os.path.basename(path)) | ||
4353 | 51 | |||
4354 | 52 | |||
4355 | 53 | def render_and_write(template_dir, path, context): | ||
4356 | 54 | """Renders the specified template into the file. | ||
4357 | 55 | |||
4358 | 56 | :param template_dir: the directory to load the template from | ||
4359 | 57 | :param path: the path to write the templated contents to | ||
4360 | 58 | :param context: the parameters to pass to the rendering engine | ||
4361 | 59 | """ | ||
4362 | 60 | env = Environment(loader=FileSystemLoader(template_dir)) | ||
4363 | 61 | template_file = os.path.basename(path) | ||
4364 | 62 | template = env.get_template(template_file) | ||
4365 | 63 | log('Rendering from template: %s' % template.name, level=DEBUG) | ||
4366 | 64 | rendered_content = template.render(context) | ||
4367 | 65 | if not rendered_content: | ||
4368 | 66 | log("Render returned None - skipping '%s'" % path, | ||
4369 | 67 | level=WARNING) | ||
4370 | 68 | return | ||
4371 | 69 | |||
4372 | 70 | write(path, rendered_content.encode('utf-8').strip()) | ||
4373 | 71 | log('Wrote template %s' % path, level=DEBUG) | ||
4374 | 72 | 0 | ||
4375 | === removed file 'hooks/charmhelpers/contrib/hardening/utils.py' | |||
4376 | --- hooks/charmhelpers/contrib/hardening/utils.py 2016-04-22 04:53:43 +0000 | |||
4377 | +++ hooks/charmhelpers/contrib/hardening/utils.py 1970-01-01 00:00:00 +0000 | |||
4378 | @@ -1,157 +0,0 @@ | |||
4379 | 1 | # Copyright 2016 Canonical Limited. | ||
4380 | 2 | # | ||
4381 | 3 | # This file is part of charm-helpers. | ||
4382 | 4 | # | ||
4383 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
4384 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
4385 | 7 | # published by the Free Software Foundation. | ||
4386 | 8 | # | ||
4387 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
4388 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4389 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4390 | 12 | # GNU Lesser General Public License for more details. | ||
4391 | 13 | # | ||
4392 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4393 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
4394 | 16 | |||
4395 | 17 | import glob | ||
4396 | 18 | import grp | ||
4397 | 19 | import os | ||
4398 | 20 | import pwd | ||
4399 | 21 | import six | ||
4400 | 22 | import yaml | ||
4401 | 23 | |||
4402 | 24 | from charmhelpers.core.hookenv import ( | ||
4403 | 25 | log, | ||
4404 | 26 | DEBUG, | ||
4405 | 27 | INFO, | ||
4406 | 28 | WARNING, | ||
4407 | 29 | ERROR, | ||
4408 | 30 | ) | ||
4409 | 31 | |||
4410 | 32 | |||
4411 | 33 | # Global settings cache. Since each hook fire entails a fresh module import it | ||
4412 | 34 | # is safe to hold this in memory and not risk missing config changes (since | ||
4413 | 35 | # they will result in a new hook fire and thus re-import). | ||
4414 | 36 | __SETTINGS__ = {} | ||
4415 | 37 | |||
4416 | 38 | |||
4417 | 39 | def _get_defaults(modules): | ||
4418 | 40 | """Load the default config for the provided modules. | ||
4419 | 41 | |||
4420 | 42 | :param modules: stack modules config defaults to lookup. | ||
4421 | 43 | :returns: modules default config dictionary. | ||
4422 | 44 | """ | ||
4423 | 45 | default = os.path.join(os.path.dirname(__file__), | ||
4424 | 46 | 'defaults/%s.yaml' % (modules)) | ||
4425 | 47 | return yaml.safe_load(open(default)) | ||
4426 | 48 | |||
4427 | 49 | |||
4428 | 50 | def _get_schema(modules): | ||
4429 | 51 | """Load the config schema for the provided modules. | ||
4430 | 52 | |||
4431 | 53 | NOTE: this schema is intended to have 1-1 relationship with they keys in | ||
4432 | 54 | the default config and is used a means to verify valid overrides provided | ||
4433 | 55 | by the user. | ||
4434 | 56 | |||
4435 | 57 | :param modules: stack modules config schema to lookup. | ||
4436 | 58 | :returns: modules default schema dictionary. | ||
4437 | 59 | """ | ||
4438 | 60 | schema = os.path.join(os.path.dirname(__file__), | ||
4439 | 61 | 'defaults/%s.yaml.schema' % (modules)) | ||
4440 | 62 | return yaml.safe_load(open(schema)) | ||
4441 | 63 | |||
4442 | 64 | |||
4443 | 65 | def _get_user_provided_overrides(modules): | ||
4444 | 66 | """Load user-provided config overrides. | ||
4445 | 67 | |||
4446 | 68 | :param modules: stack modules to lookup in user overrides yaml file. | ||
4447 | 69 | :returns: overrides dictionary. | ||
4448 | 70 | """ | ||
4449 | 71 | overrides = os.path.join(os.environ['JUJU_CHARM_DIR'], | ||
4450 | 72 | 'hardening.yaml') | ||
4451 | 73 | if os.path.exists(overrides): | ||
4452 | 74 | log("Found user-provided config overrides file '%s'" % | ||
4453 | 75 | (overrides), level=DEBUG) | ||
4454 | 76 | settings = yaml.safe_load(open(overrides)) | ||
4455 | 77 | if settings and settings.get(modules): | ||
4456 | 78 | log("Applying '%s' overrides" % (modules), level=DEBUG) | ||
4457 | 79 | return settings.get(modules) | ||
4458 | 80 | |||
4459 | 81 | log("No overrides found for '%s'" % (modules), level=DEBUG) | ||
4460 | 82 | else: | ||
4461 | 83 | log("No hardening config overrides file '%s' found in charm " | ||
4462 | 84 | "root dir" % (overrides), level=DEBUG) | ||
4463 | 85 | |||
4464 | 86 | return {} | ||
4465 | 87 | |||
4466 | 88 | |||
4467 | 89 | def _apply_overrides(settings, overrides, schema): | ||
4468 | 90 | """Get overrides config overlayed onto modules defaults. | ||
4469 | 91 | |||
4470 | 92 | :param modules: require stack modules config. | ||
4471 | 93 | :returns: dictionary of modules config with user overrides applied. | ||
4472 | 94 | """ | ||
4473 | 95 | if overrides: | ||
4474 | 96 | for k, v in six.iteritems(overrides): | ||
4475 | 97 | if k in schema: | ||
4476 | 98 | if schema[k] is None: | ||
4477 | 99 | settings[k] = v | ||
4478 | 100 | elif type(schema[k]) is dict: | ||
4479 | 101 | settings[k] = _apply_overrides(settings[k], overrides[k], | ||
4480 | 102 | schema[k]) | ||
4481 | 103 | else: | ||
4482 | 104 | raise Exception("Unexpected type found in schema '%s'" % | ||
4483 | 105 | type(schema[k]), level=ERROR) | ||
4484 | 106 | else: | ||
4485 | 107 | log("Unknown override key '%s' - ignoring" % (k), level=INFO) | ||
4486 | 108 | |||
4487 | 109 | return settings | ||
4488 | 110 | |||
4489 | 111 | |||
4490 | 112 | def get_settings(modules): | ||
4491 | 113 | global __SETTINGS__ | ||
4492 | 114 | if modules in __SETTINGS__: | ||
4493 | 115 | return __SETTINGS__[modules] | ||
4494 | 116 | |||
4495 | 117 | schema = _get_schema(modules) | ||
4496 | 118 | settings = _get_defaults(modules) | ||
4497 | 119 | overrides = _get_user_provided_overrides(modules) | ||
4498 | 120 | __SETTINGS__[modules] = _apply_overrides(settings, overrides, schema) | ||
4499 | 121 | return __SETTINGS__[modules] | ||
4500 | 122 | |||
4501 | 123 | |||
4502 | 124 | def ensure_permissions(path, user, group, permissions, maxdepth=-1): | ||
4503 | 125 | """Ensure permissions for path. | ||
4504 | 126 | |||
4505 | 127 | If path is a file, apply to file and return. If path is a directory, | ||
4506 | 128 | apply recursively (if required) to directory contents and return. | ||
4507 | 129 | |||
4508 | 130 | :param user: user name | ||
4509 | 131 | :param group: group name | ||
4510 | 132 | :param permissions: octal permissions | ||
4511 | 133 | :param maxdepth: maximum recursion depth. A negative maxdepth allows | ||
4512 | 134 | infinite recursion and maxdepth=0 means no recursion. | ||
4513 | 135 | :returns: None | ||
4514 | 136 | """ | ||
4515 | 137 | if not os.path.exists(path): | ||
4516 | 138 | log("File '%s' does not exist - cannot set permissions" % (path), | ||
4517 | 139 | level=WARNING) | ||
4518 | 140 | return | ||
4519 | 141 | |||
4520 | 142 | _user = pwd.getpwnam(user) | ||
4521 | 143 | os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid) | ||
4522 | 144 | os.chmod(path, permissions) | ||
4523 | 145 | |||
4524 | 146 | if maxdepth == 0: | ||
4525 | 147 | log("Max recursion depth reached - skipping further recursion", | ||
4526 | 148 | level=DEBUG) | ||
4527 | 149 | return | ||
4528 | 150 | elif maxdepth > 0: | ||
4529 | 151 | maxdepth -= 1 | ||
4530 | 152 | |||
4531 | 153 | if os.path.isdir(path): | ||
4532 | 154 | contents = glob.glob("%s/*" % (path)) | ||
4533 | 155 | for c in contents: | ||
4534 | 156 | ensure_permissions(c, user=user, group=group, | ||
4535 | 157 | permissions=permissions, maxdepth=maxdepth) | ||
4536 | 158 | 0 | ||
4537 | === removed directory 'hooks/charmhelpers/contrib/mellanox' | |||
4538 | === removed file 'hooks/charmhelpers/contrib/mellanox/__init__.py' | |||
4539 | === removed file 'hooks/charmhelpers/contrib/mellanox/infiniband.py' | |||
4540 | --- hooks/charmhelpers/contrib/mellanox/infiniband.py 2016-01-30 22:41:50 +0000 | |||
4541 | +++ hooks/charmhelpers/contrib/mellanox/infiniband.py 1970-01-01 00:00:00 +0000 | |||
4542 | @@ -1,151 +0,0 @@ | |||
4543 | 1 | #!/usr/bin/env python | ||
4544 | 2 | # -*- coding: utf-8 -*- | ||
4545 | 3 | |||
4546 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
4547 | 5 | # | ||
4548 | 6 | # This file is part of charm-helpers. | ||
4549 | 7 | # | ||
4550 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
4551 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
4552 | 10 | # published by the Free Software Foundation. | ||
4553 | 11 | # | ||
4554 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
4555 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4556 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4557 | 15 | # GNU Lesser General Public License for more details. | ||
4558 | 16 | # | ||
4559 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
4560 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
4561 | 19 | |||
4562 | 20 | |||
4563 | 21 | __author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" | ||
4564 | 22 | |||
4565 | 23 | from charmhelpers.fetch import ( | ||
4566 | 24 | apt_install, | ||
4567 | 25 | apt_update, | ||
4568 | 26 | ) | ||
4569 | 27 | |||
4570 | 28 | from charmhelpers.core.hookenv import ( | ||
4571 | 29 | log, | ||
4572 | 30 | INFO, | ||
4573 | 31 | ) | ||
4574 | 32 | |||
4575 | 33 | try: | ||
4576 | 34 | from netifaces import interfaces as network_interfaces | ||
4577 | 35 | except ImportError: | ||
4578 | 36 | apt_install('python-netifaces') | ||
4579 | 37 | from netifaces import interfaces as network_interfaces | ||
4580 | 38 | |||
4581 | 39 | import os | ||
4582 | 40 | import re | ||
4583 | 41 | import subprocess | ||
4584 | 42 | |||
4585 | 43 | from charmhelpers.core.kernel import modprobe | ||
4586 | 44 | |||
4587 | 45 | REQUIRED_MODULES = ( | ||
4588 | 46 | "mlx4_ib", | ||
4589 | 47 | "mlx4_en", | ||
4590 | 48 | "mlx4_core", | ||
4591 | 49 | "ib_ipath", | ||
4592 | 50 | "ib_mthca", | ||
4593 | 51 | "ib_srpt", | ||
4594 | 52 | "ib_srp", | ||
4595 | 53 | "ib_ucm", | ||
4596 | 54 | "ib_isert", | ||
4597 | 55 | "ib_iser", | ||
4598 | 56 | "ib_ipoib", | ||
4599 | 57 | "ib_cm", | ||
4600 | 58 | "ib_uverbs" | ||
4601 | 59 | "ib_umad", | ||
4602 | 60 | "ib_sa", | ||
4603 | 61 | "ib_mad", | ||
4604 | 62 | "ib_core", | ||
4605 | 63 | "ib_addr", | ||
4606 | 64 | "rdma_ucm", | ||
4607 | 65 | ) | ||
4608 | 66 | |||
4609 | 67 | REQUIRED_PACKAGES = ( | ||
4610 | 68 | "ibutils", | ||
4611 | 69 | "infiniband-diags", | ||
4612 | 70 | "ibverbs-utils", | ||
4613 | 71 | ) | ||
4614 | 72 | |||
4615 | 73 | IPOIB_DRIVERS = ( | ||
4616 | 74 | "ib_ipoib", | ||
4617 | 75 | ) | ||
4618 | 76 | |||
4619 | 77 | ABI_VERSION_FILE = "/sys/class/infiniband_mad/abi_version" | ||
4620 | 78 | |||
4621 | 79 | |||
4622 | 80 | class DeviceInfo(object): | ||
4623 | 81 | pass | ||
4624 | 82 | |||
4625 | 83 | |||
4626 | 84 | def install_packages(): | ||
4627 | 85 | apt_update() | ||
4628 | 86 | apt_install(REQUIRED_PACKAGES, fatal=True) | ||
4629 | 87 | |||
4630 | 88 | |||
4631 | 89 | def load_modules(): | ||
4632 | 90 | for module in REQUIRED_MODULES: | ||
4633 | 91 | modprobe(module, persist=True) | ||
4634 | 92 | |||
4635 | 93 | |||
4636 | 94 | def is_enabled(): | ||
4637 | 95 | """Check if infiniband is loaded on the system""" | ||
4638 | 96 | return os.path.exists(ABI_VERSION_FILE) | ||
4639 | 97 | |||
4640 | 98 | |||
4641 | 99 | def stat(): | ||
4642 | 100 | """Return full output of ibstat""" | ||
4643 | 101 | return subprocess.check_output(["ibstat"]) | ||
4644 | 102 | |||
4645 | 103 | |||
4646 | 104 | def devices(): | ||
4647 | 105 | """Returns a list of IB enabled devices""" | ||
4648 | 106 | return subprocess.check_output(['ibstat', '-l']).splitlines() | ||
4649 | 107 | |||
4650 | 108 | |||
4651 | 109 | def device_info(device): | ||
4652 | 110 | """Returns a DeviceInfo object with the current device settings""" | ||
4653 | 111 | |||
4654 | 112 | status = subprocess.check_output([ | ||
4655 | 113 | 'ibstat', device, '-s']).splitlines() | ||
4656 | 114 | |||
4657 | 115 | regexes = { | ||
4658 | 116 | "CA type: (.*)": "device_type", | ||
4659 | 117 | "Number of ports: (.*)": "num_ports", | ||
4660 | 118 | "Firmware version: (.*)": "fw_ver", | ||
4661 | 119 | "Hardware version: (.*)": "hw_ver", | ||
4662 | 120 | "Node GUID: (.*)": "node_guid", | ||
4663 | 121 | "System image GUID: (.*)": "sys_guid", | ||
4664 | 122 | } | ||
4665 | 123 | |||
4666 | 124 | device = DeviceInfo() | ||
4667 | 125 | |||
4668 | 126 | for line in status: | ||
4669 | 127 | for expression, key in regexes.items(): | ||
4670 | 128 | matches = re.search(expression, line) | ||
4671 | 129 | if matches: | ||
4672 | 130 | setattr(device, key, matches.group(1)) | ||
4673 | 131 | |||
4674 | 132 | return device | ||
4675 | 133 | |||
4676 | 134 | |||
4677 | 135 | def ipoib_interfaces(): | ||
4678 | 136 | """Return a list of IPOIB capable ethernet interfaces""" | ||
4679 | 137 | interfaces = [] | ||
4680 | 138 | |||
4681 | 139 | for interface in network_interfaces(): | ||
4682 | 140 | try: | ||
4683 | 141 | driver = re.search('^driver: (.+)$', subprocess.check_output([ | ||
4684 | 142 | 'ethtool', '-i', | ||
4685 | 143 | interface]), re.M).group(1) | ||
4686 | 144 | |||
4687 | 145 | if driver in IPOIB_DRIVERS: | ||
4688 | 146 | interfaces.append(interface) | ||
4689 | 147 | except: | ||
4690 | 148 | log("Skipping interface %s" % interface, level=INFO) | ||
4691 | 149 | continue | ||
4692 | 150 | |||
4693 | 151 | return interfaces | ||
4694 | 152 | 0 | ||
4695 | === removed directory 'hooks/charmhelpers/contrib/peerstorage' | |||
4696 | === removed file 'hooks/charmhelpers/contrib/peerstorage/__init__.py' | |||
4697 | --- hooks/charmhelpers/contrib/peerstorage/__init__.py 2016-01-30 22:40:26 +0000 | |||
4698 | +++ hooks/charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000 | |||
4699 | @@ -1,269 +0,0 @@ | |||
4700 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
4701 | 2 | # | ||
4702 | 3 | # This file is part of charm-helpers. | ||
4703 | 4 | # | ||
4704 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
4705 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
4706 | 7 | # published by the Free Software Foundation. | ||
4707 | 8 | # | ||
4708 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
4709 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4710 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4711 | 12 | # GNU Lesser General Public License for more details. | ||
4712 | 13 | # | ||
4713 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4714 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
4715 | 16 | |||
4716 | 17 | import json | ||
4717 | 18 | import six | ||
4718 | 19 | |||
4719 | 20 | from charmhelpers.core.hookenv import relation_id as current_relation_id | ||
4720 | 21 | from charmhelpers.core.hookenv import ( | ||
4721 | 22 | is_relation_made, | ||
4722 | 23 | relation_ids, | ||
4723 | 24 | relation_get as _relation_get, | ||
4724 | 25 | local_unit, | ||
4725 | 26 | relation_set as _relation_set, | ||
4726 | 27 | leader_get as _leader_get, | ||
4727 | 28 | leader_set, | ||
4728 | 29 | is_leader, | ||
4729 | 30 | ) | ||
4730 | 31 | |||
4731 | 32 | |||
4732 | 33 | """ | ||
4733 | 34 | This helper provides functions to support use of a peer relation | ||
4734 | 35 | for basic key/value storage, with the added benefit that all storage | ||
4735 | 36 | can be replicated across peer units. | ||
4736 | 37 | |||
4737 | 38 | Requirement to use: | ||
4738 | 39 | |||
4739 | 40 | To use this, the "peer_echo()" method has to be called form the peer | ||
4740 | 41 | relation's relation-changed hook: | ||
4741 | 42 | |||
4742 | 43 | @hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name | ||
4743 | 44 | def cluster_relation_changed(): | ||
4744 | 45 | peer_echo() | ||
4745 | 46 | |||
4746 | 47 | Once this is done, you can use peer storage from anywhere: | ||
4747 | 48 | |||
4748 | 49 | @hooks.hook("some-hook") | ||
4749 | 50 | def some_hook(): | ||
4750 | 51 | # You can store and retrieve key/values this way: | ||
4751 | 52 | if is_relation_made("cluster"): # from charmhelpers.core.hookenv | ||
4752 | 53 | # There are peers available so we can work with peer storage | ||
4753 | 54 | peer_store("mykey", "myvalue") | ||
4754 | 55 | value = peer_retrieve("mykey") | ||
4755 | 56 | print value | ||
4756 | 57 | else: | ||
4757 | 58 | print "No peers joind the relation, cannot share key/values :(" | ||
4758 | 59 | """ | ||
4759 | 60 | |||
4760 | 61 | |||
4761 | 62 | def leader_get(attribute=None, rid=None): | ||
4762 | 63 | """Wrapper to ensure that settings are migrated from the peer relation. | ||
4763 | 64 | |||
4764 | 65 | This is to support upgrading an environment that does not support | ||
4765 | 66 | Juju leadership election to one that does. | ||
4766 | 67 | |||
4767 | 68 | If a setting is not extant in the leader-get but is on the relation-get | ||
4768 | 69 | peer rel, it is migrated and marked as such so that it is not re-migrated. | ||
4769 | 70 | """ | ||
4770 | 71 | migration_key = '__leader_get_migrated_settings__' | ||
4771 | 72 | if not is_leader(): | ||
4772 | 73 | return _leader_get(attribute=attribute) | ||
4773 | 74 | |||
4774 | 75 | settings_migrated = False | ||
4775 | 76 | leader_settings = _leader_get(attribute=attribute) | ||
4776 | 77 | previously_migrated = _leader_get(attribute=migration_key) | ||
4777 | 78 | |||
4778 | 79 | if previously_migrated: | ||
4779 | 80 | migrated = set(json.loads(previously_migrated)) | ||
4780 | 81 | else: | ||
4781 | 82 | migrated = set([]) | ||
4782 | 83 | |||
4783 | 84 | try: | ||
4784 | 85 | if migration_key in leader_settings: | ||
4785 | 86 | del leader_settings[migration_key] | ||
4786 | 87 | except TypeError: | ||
4787 | 88 | pass | ||
4788 | 89 | |||
4789 | 90 | if attribute: | ||
4790 | 91 | if attribute in migrated: | ||
4791 | 92 | return leader_settings | ||
4792 | 93 | |||
4793 | 94 | # If attribute not present in leader db, check if this unit has set | ||
4794 | 95 | # the attribute in the peer relation | ||
4795 | 96 | if not leader_settings: | ||
4796 | 97 | peer_setting = _relation_get(attribute=attribute, unit=local_unit(), | ||
4797 | 98 | rid=rid) | ||
4798 | 99 | if peer_setting: | ||
4799 | 100 | leader_set(settings={attribute: peer_setting}) | ||
4800 | 101 | leader_settings = peer_setting | ||
4801 | 102 | |||
4802 | 103 | if leader_settings: | ||
4803 | 104 | settings_migrated = True | ||
4804 | 105 | migrated.add(attribute) | ||
4805 | 106 | else: | ||
4806 | 107 | r_settings = _relation_get(unit=local_unit(), rid=rid) | ||
4807 | 108 | if r_settings: | ||
4808 | 109 | for key in set(r_settings.keys()).difference(migrated): | ||
4809 | 110 | # Leader setting wins | ||
4810 | 111 | if not leader_settings.get(key): | ||
4811 | 112 | leader_settings[key] = r_settings[key] | ||
4812 | 113 | |||
4813 | 114 | settings_migrated = True | ||
4814 | 115 | migrated.add(key) | ||
4815 | 116 | |||
4816 | 117 | if settings_migrated: | ||
4817 | 118 | leader_set(**leader_settings) | ||
4818 | 119 | |||
4819 | 120 | if migrated and settings_migrated: | ||
4820 | 121 | migrated = json.dumps(list(migrated)) | ||
4821 | 122 | leader_set(settings={migration_key: migrated}) | ||
4822 | 123 | |||
4823 | 124 | return leader_settings | ||
4824 | 125 | |||
4825 | 126 | |||
4826 | 127 | def relation_set(relation_id=None, relation_settings=None, **kwargs): | ||
4827 | 128 | """Attempt to use leader-set if supported in the current version of Juju, | ||
4828 | 129 | otherwise falls back on relation-set. | ||
4829 | 130 | |||
4830 | 131 | Note that we only attempt to use leader-set if the provided relation_id is | ||
4831 | 132 | a peer relation id or no relation id is provided (in which case we assume | ||
4832 | 133 | we are within the peer relation context). | ||
4833 | 134 | """ | ||
4834 | 135 | try: | ||
4835 | 136 | if relation_id in relation_ids('cluster'): | ||
4836 | 137 | return leader_set(settings=relation_settings, **kwargs) | ||
4837 | 138 | else: | ||
4838 | 139 | raise NotImplementedError | ||
4839 | 140 | except NotImplementedError: | ||
4840 | 141 | return _relation_set(relation_id=relation_id, | ||
4841 | 142 | relation_settings=relation_settings, **kwargs) | ||
4842 | 143 | |||
4843 | 144 | |||
4844 | 145 | def relation_get(attribute=None, unit=None, rid=None): | ||
4845 | 146 | """Attempt to use leader-get if supported in the current version of Juju, | ||
4846 | 147 | otherwise falls back on relation-get. | ||
4847 | 148 | |||
4848 | 149 | Note that we only attempt to use leader-get if the provided rid is a peer | ||
4849 | 150 | relation id or no relation id is provided (in which case we assume we are | ||
4850 | 151 | within the peer relation context). | ||
4851 | 152 | """ | ||
4852 | 153 | try: | ||
4853 | 154 | if rid in relation_ids('cluster'): | ||
4854 | 155 | return leader_get(attribute, rid) | ||
4855 | 156 | else: | ||
4856 | 157 | raise NotImplementedError | ||
4857 | 158 | except NotImplementedError: | ||
4858 | 159 | return _relation_get(attribute=attribute, rid=rid, unit=unit) | ||
4859 | 160 | |||
4860 | 161 | |||
4861 | 162 | def peer_retrieve(key, relation_name='cluster'): | ||
4862 | 163 | """Retrieve a named key from peer relation `relation_name`.""" | ||
4863 | 164 | cluster_rels = relation_ids(relation_name) | ||
4864 | 165 | if len(cluster_rels) > 0: | ||
4865 | 166 | cluster_rid = cluster_rels[0] | ||
4866 | 167 | return relation_get(attribute=key, rid=cluster_rid, | ||
4867 | 168 | unit=local_unit()) | ||
4868 | 169 | else: | ||
4869 | 170 | raise ValueError('Unable to detect' | ||
4870 | 171 | 'peer relation {}'.format(relation_name)) | ||
4871 | 172 | |||
4872 | 173 | |||
4873 | 174 | def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_', | ||
4874 | 175 | inc_list=None, exc_list=None): | ||
4875 | 176 | """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """ | ||
4876 | 177 | inc_list = inc_list if inc_list else [] | ||
4877 | 178 | exc_list = exc_list if exc_list else [] | ||
4878 | 179 | peerdb_settings = peer_retrieve('-', relation_name=relation_name) | ||
4879 | 180 | matched = {} | ||
4880 | 181 | if peerdb_settings is None: | ||
4881 | 182 | return matched | ||
4882 | 183 | for k, v in peerdb_settings.items(): | ||
4883 | 184 | full_prefix = prefix + delimiter | ||
4884 | 185 | if k.startswith(full_prefix): | ||
4885 | 186 | new_key = k.replace(full_prefix, '') | ||
4886 | 187 | if new_key in exc_list: | ||
4887 | 188 | continue | ||
4888 | 189 | if new_key in inc_list or len(inc_list) == 0: | ||
4889 | 190 | matched[new_key] = v | ||
4890 | 191 | return matched | ||
4891 | 192 | |||
4892 | 193 | |||
4893 | 194 | def peer_store(key, value, relation_name='cluster'): | ||
4894 | 195 | """Store the key/value pair on the named peer relation `relation_name`.""" | ||
4895 | 196 | cluster_rels = relation_ids(relation_name) | ||
4896 | 197 | if len(cluster_rels) > 0: | ||
4897 | 198 | cluster_rid = cluster_rels[0] | ||
4898 | 199 | relation_set(relation_id=cluster_rid, | ||
4899 | 200 | relation_settings={key: value}) | ||
4900 | 201 | else: | ||
4901 | 202 | raise ValueError('Unable to detect ' | ||
4902 | 203 | 'peer relation {}'.format(relation_name)) | ||
4903 | 204 | |||
4904 | 205 | |||
4905 | 206 | def peer_echo(includes=None, force=False): | ||
4906 | 207 | """Echo filtered attributes back onto the same relation for storage. | ||
4907 | 208 | |||
4908 | 209 | This is a requirement to use the peerstorage module - it needs to be called | ||
4909 | 210 | from the peer relation's changed hook. | ||
4910 | 211 | |||
4911 | 212 | If Juju leader support exists this will be a noop unless force is True. | ||
4912 | 213 | """ | ||
4913 | 214 | try: | ||
4914 | 215 | is_leader() | ||
4915 | 216 | except NotImplementedError: | ||
4916 | 217 | pass | ||
4917 | 218 | else: | ||
4918 | 219 | if not force: | ||
4919 | 220 | return # NOOP if leader-election is supported | ||
4920 | 221 | |||
4921 | 222 | # Use original non-leader calls | ||
4922 | 223 | relation_get = _relation_get | ||
4923 | 224 | relation_set = _relation_set | ||
4924 | 225 | |||
4925 | 226 | rdata = relation_get() | ||
4926 | 227 | echo_data = {} | ||
4927 | 228 | if includes is None: | ||
4928 | 229 | echo_data = rdata.copy() | ||
4929 | 230 | for ex in ['private-address', 'public-address']: | ||
4930 | 231 | if ex in echo_data: | ||
4931 | 232 | echo_data.pop(ex) | ||
4932 | 233 | else: | ||
4933 | 234 | for attribute, value in six.iteritems(rdata): | ||
4934 | 235 | for include in includes: | ||
4935 | 236 | if include in attribute: | ||
4936 | 237 | echo_data[attribute] = value | ||
4937 | 238 | if len(echo_data) > 0: | ||
4938 | 239 | relation_set(relation_settings=echo_data) | ||
4939 | 240 | |||
4940 | 241 | |||
4941 | 242 | def peer_store_and_set(relation_id=None, peer_relation_name='cluster', | ||
4942 | 243 | peer_store_fatal=False, relation_settings=None, | ||
4943 | 244 | delimiter='_', **kwargs): | ||
4944 | 245 | """Store passed-in arguments both in argument relation and in peer storage. | ||
4945 | 246 | |||
4946 | 247 | It functions like doing relation_set() and peer_store() at the same time, | ||
4947 | 248 | with the same data. | ||
4948 | 249 | |||
4949 | 250 | @param relation_id: the id of the relation to store the data on. Defaults | ||
4950 | 251 | to the current relation. | ||
4951 | 252 | @param peer_store_fatal: Set to True, the function will raise an exception | ||
4952 | 253 | should the peer sotrage not be avialable.""" | ||
4953 | 254 | |||
4954 | 255 | relation_settings = relation_settings if relation_settings else {} | ||
4955 | 256 | relation_set(relation_id=relation_id, | ||
4956 | 257 | relation_settings=relation_settings, | ||
4957 | 258 | **kwargs) | ||
4958 | 259 | if is_relation_made(peer_relation_name): | ||
4959 | 260 | for key, value in six.iteritems(dict(list(kwargs.items()) + | ||
4960 | 261 | list(relation_settings.items()))): | ||
4961 | 262 | key_prefix = relation_id or current_relation_id() | ||
4962 | 263 | peer_store(key_prefix + delimiter + key, | ||
4963 | 264 | value, | ||
4964 | 265 | relation_name=peer_relation_name) | ||
4965 | 266 | else: | ||
4966 | 267 | if peer_store_fatal: | ||
4967 | 268 | raise ValueError('Unable to detect ' | ||
4968 | 269 | 'peer relation {}'.format(peer_relation_name)) | ||
4969 | 270 | 0 | ||
4970 | === removed directory 'hooks/charmhelpers/contrib/saltstack' | |||
4971 | === removed file 'hooks/charmhelpers/contrib/saltstack/__init__.py' | |||
4972 | --- hooks/charmhelpers/contrib/saltstack/__init__.py 2015-07-29 18:07:31 +0000 | |||
4973 | +++ hooks/charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000 | |||
4974 | @@ -1,118 +0,0 @@ | |||
4975 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
4976 | 2 | # | ||
4977 | 3 | # This file is part of charm-helpers. | ||
4978 | 4 | # | ||
4979 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
4980 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
4981 | 7 | # published by the Free Software Foundation. | ||
4982 | 8 | # | ||
4983 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
4984 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4985 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4986 | 12 | # GNU Lesser General Public License for more details. | ||
4987 | 13 | # | ||
4988 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
4989 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
4990 | 16 | |||
4991 | 17 | """Charm Helpers saltstack - declare the state of your machines. | ||
4992 | 18 | |||
4993 | 19 | This helper enables you to declare your machine state, rather than | ||
4994 | 20 | program it procedurally (and have to test each change to your procedures). | ||
4995 | 21 | Your install hook can be as simple as:: | ||
4996 | 22 | |||
4997 | 23 | {{{ | ||
4998 | 24 | from charmhelpers.contrib.saltstack import ( | ||
4999 | 25 | install_salt_support, | ||
5000 | 26 | update_machine_state, |
The diff has been truncated for viewing.