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