Merge lp:~junaidali/charms/trusty/plumgrid-director/liberty into lp:~plumgrid-team/charms/trusty/plumgrid-director/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
Reviewer Review Type Date Requested Status
Bilal Baqar Approve
Review via email: mp+292151@code.launchpad.net
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
=== added directory 'bin'
=== added file 'bin/charm_helpers_sync.py'
--- bin/charm_helpers_sync.py 1970-01-01 00:00:00 +0000
+++ bin/charm_helpers_sync.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,253 @@
1#!/usr/bin/python
2
3# Copyright 2014-2015 Canonical Limited.
4#
5# This file is part of charm-helpers.
6#
7# charm-helpers is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3 as
9# published by the Free Software Foundation.
10#
11# charm-helpers is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
19# Authors:
20# Adam Gandelman <adamg@ubuntu.com>
21
22import logging
23import optparse
24import os
25import subprocess
26import shutil
27import sys
28import tempfile
29import yaml
30from fnmatch import fnmatch
31
32import six
33
34CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
35
36
37def parse_config(conf_file):
38 if not os.path.isfile(conf_file):
39 logging.error('Invalid config file: %s.' % conf_file)
40 return False
41 return yaml.load(open(conf_file).read())
42
43
44def clone_helpers(work_dir, branch):
45 dest = os.path.join(work_dir, 'charm-helpers')
46 logging.info('Checking out %s to %s.' % (branch, dest))
47 cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
48 subprocess.check_call(cmd)
49 return dest
50
51
52def _module_path(module):
53 return os.path.join(*module.split('.'))
54
55
56def _src_path(src, module):
57 return os.path.join(src, 'charmhelpers', _module_path(module))
58
59
60def _dest_path(dest, module):
61 return os.path.join(dest, _module_path(module))
62
63
64def _is_pyfile(path):
65 return os.path.isfile(path + '.py')
66
67
68def ensure_init(path):
69 '''
70 ensure directories leading up to path are importable, omitting
71 parent directory, eg path='/hooks/helpers/foo'/:
72 hooks/
73 hooks/helpers/__init__.py
74 hooks/helpers/foo/__init__.py
75 '''
76 for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
77 _i = os.path.join(d, '__init__.py')
78 if not os.path.exists(_i):
79 logging.info('Adding missing __init__.py: %s' % _i)
80 open(_i, 'wb').close()
81
82
83def sync_pyfile(src, dest):
84 src = src + '.py'
85 src_dir = os.path.dirname(src)
86 logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
87 if not os.path.exists(dest):
88 os.makedirs(dest)
89 shutil.copy(src, dest)
90 if os.path.isfile(os.path.join(src_dir, '__init__.py')):
91 shutil.copy(os.path.join(src_dir, '__init__.py'),
92 dest)
93 ensure_init(dest)
94
95
96def get_filter(opts=None):
97 opts = opts or []
98 if 'inc=*' in opts:
99 # do not filter any files, include everything
100 return None
101
102 def _filter(dir, ls):
103 incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
104 _filter = []
105 for f in ls:
106 _f = os.path.join(dir, f)
107
108 if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
109 if True not in [fnmatch(_f, inc) for inc in incs]:
110 logging.debug('Not syncing %s, does not match include '
111 'filters (%s)' % (_f, incs))
112 _filter.append(f)
113 else:
114 logging.debug('Including file, which matches include '
115 'filters (%s): %s' % (incs, _f))
116 elif (os.path.isfile(_f) and not _f.endswith('.py')):
117 logging.debug('Not syncing file: %s' % f)
118 _filter.append(f)
119 elif (os.path.isdir(_f) and not
120 os.path.isfile(os.path.join(_f, '__init__.py'))):
121 logging.debug('Not syncing directory: %s' % f)
122 _filter.append(f)
123 return _filter
124 return _filter
125
126
127def sync_directory(src, dest, opts=None):
128 if os.path.exists(dest):
129 logging.debug('Removing existing directory: %s' % dest)
130 shutil.rmtree(dest)
131 logging.info('Syncing directory: %s -> %s.' % (src, dest))
132
133 shutil.copytree(src, dest, ignore=get_filter(opts))
134 ensure_init(dest)
135
136
137def sync(src, dest, module, opts=None):
138
139 # Sync charmhelpers/__init__.py for bootstrap code.
140 sync_pyfile(_src_path(src, '__init__'), dest)
141
142 # Sync other __init__.py files in the path leading to module.
143 m = []
144 steps = module.split('.')[:-1]
145 while steps:
146 m.append(steps.pop(0))
147 init = '.'.join(m + ['__init__'])
148 sync_pyfile(_src_path(src, init),
149 os.path.dirname(_dest_path(dest, init)))
150
151 # Sync the module, or maybe a .py file.
152 if os.path.isdir(_src_path(src, module)):
153 sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
154 elif _is_pyfile(_src_path(src, module)):
155 sync_pyfile(_src_path(src, module),
156 os.path.dirname(_dest_path(dest, module)))
157 else:
158 logging.warn('Could not sync: %s. Neither a pyfile or directory, '
159 'does it even exist?' % module)
160
161
162def parse_sync_options(options):
163 if not options:
164 return []
165 return options.split(',')
166
167
168def extract_options(inc, global_options=None):
169 global_options = global_options or []
170 if global_options and isinstance(global_options, six.string_types):
171 global_options = [global_options]
172 if '|' not in inc:
173 return (inc, global_options)
174 inc, opts = inc.split('|')
175 return (inc, parse_sync_options(opts) + global_options)
176
177
178def sync_helpers(include, src, dest, options=None):
179 if not os.path.isdir(dest):
180 os.makedirs(dest)
181
182 global_options = parse_sync_options(options)
183
184 for inc in include:
185 if isinstance(inc, str):
186 inc, opts = extract_options(inc, global_options)
187 sync(src, dest, inc, opts)
188 elif isinstance(inc, dict):
189 # could also do nested dicts here.
190 for k, v in six.iteritems(inc):
191 if isinstance(v, list):
192 for m in v:
193 inc, opts = extract_options(m, global_options)
194 sync(src, dest, '%s.%s' % (k, inc), opts)
195
196if __name__ == '__main__':
197 parser = optparse.OptionParser()
198 parser.add_option('-c', '--config', action='store', dest='config',
199 default=None, help='helper config file')
200 parser.add_option('-D', '--debug', action='store_true', dest='debug',
201 default=False, help='debug')
202 parser.add_option('-b', '--branch', action='store', dest='branch',
203 help='charm-helpers bzr branch (overrides config)')
204 parser.add_option('-d', '--destination', action='store', dest='dest_dir',
205 help='sync destination dir (overrides config)')
206 (opts, args) = parser.parse_args()
207
208 if opts.debug:
209 logging.basicConfig(level=logging.DEBUG)
210 else:
211 logging.basicConfig(level=logging.INFO)
212
213 if opts.config:
214 logging.info('Loading charm helper config from %s.' % opts.config)
215 config = parse_config(opts.config)
216 if not config:
217 logging.error('Could not parse config from %s.' % opts.config)
218 sys.exit(1)
219 else:
220 config = {}
221
222 if 'branch' not in config:
223 config['branch'] = CHARM_HELPERS_BRANCH
224 if opts.branch:
225 config['branch'] = opts.branch
226 if opts.dest_dir:
227 config['destination'] = opts.dest_dir
228
229 if 'destination' not in config:
230 logging.error('No destination dir. specified as option or config.')
231 sys.exit(1)
232
233 if 'include' not in config:
234 if not args:
235 logging.error('No modules to sync specified as option or config.')
236 sys.exit(1)
237 config['include'] = []
238 [config['include'].append(a) for a in args]
239
240 sync_options = None
241 if 'options' in config:
242 sync_options = config['options']
243 tmpd = tempfile.mkdtemp()
244 try:
245 checkout = clone_helpers(tmpd, config['branch'])
246 sync_helpers(config['include'], checkout, config['destination'],
247 options=sync_options)
248 except Exception as e:
249 logging.error("Could not sync: %s" % e)
250 raise e
251 finally:
252 logging.debug('Cleaning up %s' % tmpd)
253 shutil.rmtree(tmpd)
0254
=== modified file 'hooks/charmhelpers/contrib/amulet/deployment.py'
--- hooks/charmhelpers/contrib/amulet/deployment.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/amulet/deployment.py 2016-04-24 06:22:47 +0000
@@ -51,7 +51,8 @@
51 if 'units' not in this_service:51 if 'units' not in this_service:
52 this_service['units'] = 152 this_service['units'] = 1
5353
54 self.d.add(this_service['name'], units=this_service['units'])54 self.d.add(this_service['name'], units=this_service['units'],
55 constraints=this_service.get('constraints'))
5556
56 for svc in other_services:57 for svc in other_services:
57 if 'location' in svc:58 if 'location' in svc:
@@ -64,7 +65,8 @@
64 if 'units' not in svc:65 if 'units' not in svc:
65 svc['units'] = 166 svc['units'] = 1
6667
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])68 self.d.add(svc['name'], charm=branch_location, units=svc['units'],
69 constraints=svc.get('constraints'))
6870
69 def _add_relations(self, relations):71 def _add_relations(self, relations):
70 """Add all of the relations for the services."""72 """Add all of the relations for the services."""
7173
=== modified file 'hooks/charmhelpers/contrib/amulet/utils.py'
--- hooks/charmhelpers/contrib/amulet/utils.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/amulet/utils.py 2016-04-24 06:22:47 +0000
@@ -14,17 +14,25 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
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/>.
1616
17import amulet
18import ConfigParser
19import distro_info
20import io17import io
18import json
21import logging19import logging
22import os20import os
23import re21import re
24import six22import socket
23import subprocess
25import sys24import sys
26import time25import time
27import urlparse26import uuid
27
28import amulet
29import distro_info
30import six
31from six.moves import configparser
32if six.PY3:
33 from urllib import parse as urlparse
34else:
35 import urlparse
2836
2937
30class AmuletUtils(object):38class AmuletUtils(object):
@@ -108,7 +116,7 @@
108 # /!\ DEPRECATION WARNING (beisner):116 # /!\ DEPRECATION WARNING (beisner):
109 # New and existing tests should be rewritten to use117 # New and existing tests should be rewritten to use
110 # validate_services_by_name() as it is aware of init systems.118 # validate_services_by_name() as it is aware of init systems.
111 self.log.warn('/!\\ DEPRECATION WARNING: use '119 self.log.warn('DEPRECATION WARNING: use '
112 'validate_services_by_name instead of validate_services '120 'validate_services_by_name instead of validate_services '
113 'due to init system differences.')121 'due to init system differences.')
114122
@@ -142,19 +150,23 @@
142150
143 for service_name in services_list:151 for service_name in services_list:
144 if (self.ubuntu_releases.index(release) >= systemd_switch or152 if (self.ubuntu_releases.index(release) >= systemd_switch or
145 service_name == "rabbitmq-server"):153 service_name in ['rabbitmq-server', 'apache2']):
146 # init is systemd154 # init is systemd (or regular sysv)
147 cmd = 'sudo service {} status'.format(service_name)155 cmd = 'sudo service {} status'.format(service_name)
156 output, code = sentry_unit.run(cmd)
157 service_running = code == 0
148 elif self.ubuntu_releases.index(release) < systemd_switch:158 elif self.ubuntu_releases.index(release) < systemd_switch:
149 # init is upstart159 # init is upstart
150 cmd = 'sudo status {}'.format(service_name)160 cmd = 'sudo status {}'.format(service_name)
161 output, code = sentry_unit.run(cmd)
162 service_running = code == 0 and "start/running" in output
151163
152 output, code = sentry_unit.run(cmd)
153 self.log.debug('{} `{}` returned '164 self.log.debug('{} `{}` returned '
154 '{}'.format(sentry_unit.info['unit_name'],165 '{}'.format(sentry_unit.info['unit_name'],
155 cmd, code))166 cmd, code))
156 if code != 0:167 if not service_running:
157 return "command `{}` returned {}".format(cmd, str(code))168 return u"command `{}` returned {} {}".format(
169 cmd, output, str(code))
158 return None170 return None
159171
160 def _get_config(self, unit, filename):172 def _get_config(self, unit, filename):
@@ -164,7 +176,7 @@
164 # NOTE(beisner): by default, ConfigParser does not handle options176 # NOTE(beisner): by default, ConfigParser does not handle options
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.
166 # https://bugs.python.org/issue7005178 # https://bugs.python.org/issue7005
167 config = ConfigParser.ConfigParser(allow_no_value=True)179 config = configparser.ConfigParser(allow_no_value=True)
168 config.readfp(io.StringIO(file_contents))180 config.readfp(io.StringIO(file_contents))
169 return config181 return config
170182
@@ -259,33 +271,52 @@
259 """Get last modification time of directory."""271 """Get last modification time of directory."""
260 return sentry_unit.directory_stat(directory)['mtime']272 return sentry_unit.directory_stat(directory)['mtime']
261273
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):
263 """Get process' start time.275 """Get start time of a process based on the last modification time
264276 of the /proc/pid directory.
265 Determine start time of the process based on the last modification277
266 time of the /proc/pid directory. If pgrep_full is True, the process278 :sentry_unit: The sentry unit to check for the service on
267 name is matched against the full command line.279 :service: service name to look for in process table
268 """280 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
269 if pgrep_full:281 :returns: epoch time of service process start
270 cmd = 'pgrep -o -f {}'.format(service)282 :param commands: list of bash commands
271 else:283 :param sentry_units: list of sentry unit pointers
272 cmd = 'pgrep -o {}'.format(service)284 :returns: None if successful; Failure message otherwise
273 cmd = cmd + ' | grep -v pgrep || exit 0'285 """
274 cmd_out = sentry_unit.run(cmd)286 if pgrep_full is not None:
275 self.log.debug('CMDout: ' + str(cmd_out))287 # /!\ DEPRECATION WARNING (beisner):
276 if cmd_out[0]:288 # No longer implemented, as pidof is now used instead of pgrep.
277 self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))289 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
278 proc_dir = '/proc/{}'.format(cmd_out[0].strip())290 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
279 return self._get_dir_mtime(sentry_unit, proc_dir)291 'longer implemented re: lp 1474030.')
292
293 pid_list = self.get_process_id_list(sentry_unit, service)
294 pid = pid_list[0]
295 proc_dir = '/proc/{}'.format(pid)
296 self.log.debug('Pid for {} on {}: {}'.format(
297 service, sentry_unit.info['unit_name'], pid))
298
299 return self._get_dir_mtime(sentry_unit, proc_dir)
280300
281 def service_restarted(self, sentry_unit, service, filename,301 def service_restarted(self, sentry_unit, service, filename,
282 pgrep_full=False, sleep_time=20):302 pgrep_full=None, sleep_time=20):
283 """Check if service was restarted.303 """Check if service was restarted.
284304
285 Compare a service's start time vs a file's last modification time305 Compare a service's start time vs a file's last modification time
286 (such as a config file for that service) to determine if the service306 (such as a config file for that service) to determine if the service
287 has been restarted.307 has been restarted.
288 """308 """
309 # /!\ DEPRECATION WARNING (beisner):
310 # This method is prone to races in that no before-time is known.
311 # Use validate_service_config_changed instead.
312
313 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
314 # used instead of pgrep. pgrep_full is still passed through to ensure
315 # deprecation WARNS. lp1474030
316 self.log.warn('DEPRECATION WARNING: use '
317 'validate_service_config_changed instead of '
318 'service_restarted due to known races.')
319
289 time.sleep(sleep_time)320 time.sleep(sleep_time)
290 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=321 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
291 self._get_file_mtime(sentry_unit, filename)):322 self._get_file_mtime(sentry_unit, filename)):
@@ -294,78 +325,122 @@
294 return False325 return False
295326
296 def service_restarted_since(self, sentry_unit, mtime, service,327 def service_restarted_since(self, sentry_unit, mtime, service,
297 pgrep_full=False, sleep_time=20,328 pgrep_full=None, sleep_time=20,
298 retry_count=2):329 retry_count=30, retry_sleep_time=10):
299 """Check if service was been started after a given time.330 """Check if service was been started after a given time.
300331
301 Args:332 Args:
302 sentry_unit (sentry): The sentry unit to check for the service on333 sentry_unit (sentry): The sentry unit to check for the service on
303 mtime (float): The epoch time to check against334 mtime (float): The epoch time to check against
304 service (string): service name to look for in process table335 service (string): service name to look for in process table
305 pgrep_full (boolean): Use full command line search mode with pgrep336 pgrep_full: [Deprecated] Use full command line search mode with pgrep
306 sleep_time (int): Seconds to sleep before looking for process337 sleep_time (int): Initial sleep time (s) before looking for file
307 retry_count (int): If service is not found, how many times to retry338 retry_sleep_time (int): Time (s) to sleep between retries
339 retry_count (int): If file is not found, how many times to retry
308340
309 Returns:341 Returns:
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,
311 False if service is older than mtime or if service was343 False if service is older than mtime or if service was
312 not found.344 not found.
313 """345 """
314 self.log.debug('Checking %s restarted since %s' % (service, mtime))346 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
347 # used instead of pgrep. pgrep_full is still passed through to ensure
348 # deprecation WARNS. lp1474030
349
350 unit_name = sentry_unit.info['unit_name']
351 self.log.debug('Checking that %s service restarted since %s on '
352 '%s' % (service, mtime, unit_name))
315 time.sleep(sleep_time)353 time.sleep(sleep_time)
316 proc_start_time = self._get_proc_start_time(sentry_unit, service,354 proc_start_time = None
317 pgrep_full)355 tries = 0
318 while retry_count > 0 and not proc_start_time:356 while tries <= retry_count and not proc_start_time:
319 self.log.debug('No pid file found for service %s, will retry %i '357 try:
320 'more times' % (service, retry_count))358 proc_start_time = self._get_proc_start_time(sentry_unit,
321 time.sleep(30)359 service,
322 proc_start_time = self._get_proc_start_time(sentry_unit, service,360 pgrep_full)
323 pgrep_full)361 self.log.debug('Attempt {} to get {} proc start time on {} '
324 retry_count = retry_count - 1362 'OK'.format(tries, service, unit_name))
363 except IOError as e:
364 # NOTE(beisner) - race avoidance, proc may not exist yet.
365 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
366 self.log.debug('Attempt {} to get {} proc start time on {} '
367 'failed\n{}'.format(tries, service,
368 unit_name, e))
369 time.sleep(retry_sleep_time)
370 tries += 1
325371
326 if not proc_start_time:372 if not proc_start_time:
327 self.log.warn('No proc start time found, assuming service did '373 self.log.warn('No proc start time found, assuming service did '
328 'not start')374 'not start')
329 return False375 return False
330 if proc_start_time >= mtime:376 if proc_start_time >= mtime:
331 self.log.debug('proc start time is newer than provided mtime'377 self.log.debug('Proc start time is newer than provided mtime'
332 '(%s >= %s)' % (proc_start_time, mtime))378 '(%s >= %s) on %s (OK)' % (proc_start_time,
379 mtime, unit_name))
333 return True380 return True
334 else:381 else:
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 '
336 '(%s), service did not restart' % (proc_start_time,383 '(%s) on %s, service did not '
337 mtime))384 'restart' % (proc_start_time, mtime, unit_name))
338 return False385 return False
339386
340 def config_updated_since(self, sentry_unit, filename, mtime,387 def config_updated_since(self, sentry_unit, filename, mtime,
341 sleep_time=20):388 sleep_time=20, retry_count=30,
389 retry_sleep_time=10):
342 """Check if file was modified after a given time.390 """Check if file was modified after a given time.
343391
344 Args:392 Args:
345 sentry_unit (sentry): The sentry unit to check the file mtime on393 sentry_unit (sentry): The sentry unit to check the file mtime on
346 filename (string): The file to check mtime of394 filename (string): The file to check mtime of
347 mtime (float): The epoch time to check against395 mtime (float): The epoch time to check against
348 sleep_time (int): Seconds to sleep before looking for process396 sleep_time (int): Initial sleep time (s) before looking for file
397 retry_sleep_time (int): Time (s) to sleep between retries
398 retry_count (int): If file is not found, how many times to retry
349399
350 Returns:400 Returns:
351 bool: True if file was modified more recently than mtime, False if401 bool: True if file was modified more recently than mtime, False if
352 file was modified before mtime,402 file was modified before mtime, or if file not found.
353 """403 """
354 self.log.debug('Checking %s updated since %s' % (filename, mtime))404 unit_name = sentry_unit.info['unit_name']
405 self.log.debug('Checking that %s updated since %s on '
406 '%s' % (filename, mtime, unit_name))
355 time.sleep(sleep_time)407 time.sleep(sleep_time)
356 file_mtime = self._get_file_mtime(sentry_unit, filename)408 file_mtime = None
409 tries = 0
410 while tries <= retry_count and not file_mtime:
411 try:
412 file_mtime = self._get_file_mtime(sentry_unit, filename)
413 self.log.debug('Attempt {} to get {} file mtime on {} '
414 'OK'.format(tries, filename, unit_name))
415 except IOError as e:
416 # NOTE(beisner) - race avoidance, file may not exist yet.
417 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
418 self.log.debug('Attempt {} to get {} file mtime on {} '
419 'failed\n{}'.format(tries, filename,
420 unit_name, e))
421 time.sleep(retry_sleep_time)
422 tries += 1
423
424 if not file_mtime:
425 self.log.warn('Could not determine file mtime, assuming '
426 'file does not exist')
427 return False
428
357 if file_mtime >= mtime:429 if file_mtime >= mtime:
358 self.log.debug('File mtime is newer than provided mtime '430 self.log.debug('File mtime is newer than provided mtime '
359 '(%s >= %s)' % (file_mtime, mtime))431 '(%s >= %s) on %s (OK)' % (file_mtime,
432 mtime, unit_name))
360 return True433 return True
361 else:434 else:
362 self.log.warn('File mtime %s is older than provided mtime %s'435 self.log.warn('File mtime is older than provided mtime'
363 % (file_mtime, mtime))436 '(%s < on %s) on %s' % (file_mtime,
437 mtime, unit_name))
364 return False438 return False
365439
366 def validate_service_config_changed(self, sentry_unit, mtime, service,440 def validate_service_config_changed(self, sentry_unit, mtime, service,
367 filename, pgrep_full=False,441 filename, pgrep_full=None,
368 sleep_time=20, retry_count=2):442 sleep_time=20, retry_count=30,
443 retry_sleep_time=10):
369 """Check service and file were updated after mtime444 """Check service and file were updated after mtime
370445
371 Args:446 Args:
@@ -373,9 +448,10 @@
373 mtime (float): The epoch time to check against448 mtime (float): The epoch time to check against
374 service (string): service name to look for in process table449 service (string): service name to look for in process table
375 filename (string): The file to check mtime of450 filename (string): The file to check mtime of
376 pgrep_full (boolean): Use full command line search mode with pgrep451 pgrep_full: [Deprecated] Use full command line search mode with pgrep
377 sleep_time (int): Seconds to sleep before looking for process452 sleep_time (int): Initial sleep in seconds to pass to test helpers
378 retry_count (int): If service is not found, how many times to retry453 retry_count (int): If service is not found, how many times to retry
454 retry_sleep_time (int): Time in seconds to wait between retries
379455
380 Typical Usage:456 Typical Usage:
381 u = OpenStackAmuletUtils(ERROR)457 u = OpenStackAmuletUtils(ERROR)
@@ -392,15 +468,27 @@
392 mtime, False if service is older than mtime or if service was468 mtime, False if service is older than mtime or if service was
393 not found or if filename was modified before mtime.469 not found or if filename was modified before mtime.
394 """470 """
395 self.log.debug('Checking %s restarted since %s' % (service, mtime))471
396 time.sleep(sleep_time)472 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
397 service_restart = self.service_restarted_since(sentry_unit, mtime,473 # used instead of pgrep. pgrep_full is still passed through to ensure
398 service,474 # deprecation WARNS. lp1474030
399 pgrep_full=pgrep_full,475
400 sleep_time=0,476 service_restart = self.service_restarted_since(
401 retry_count=retry_count)477 sentry_unit, mtime,
402 config_update = self.config_updated_since(sentry_unit, filename, mtime,478 service,
403 sleep_time=0)479 pgrep_full=pgrep_full,
480 sleep_time=sleep_time,
481 retry_count=retry_count,
482 retry_sleep_time=retry_sleep_time)
483
484 config_update = self.config_updated_since(
485 sentry_unit,
486 filename,
487 mtime,
488 sleep_time=sleep_time,
489 retry_count=retry_count,
490 retry_sleep_time=retry_sleep_time)
491
404 return service_restart and config_update492 return service_restart and config_update
405493
406 def get_sentry_time(self, sentry_unit):494 def get_sentry_time(self, sentry_unit):
@@ -418,7 +506,6 @@
418 """Return a list of all Ubuntu releases in order of release."""506 """Return a list of all Ubuntu releases in order of release."""
419 _d = distro_info.UbuntuDistroInfo()507 _d = distro_info.UbuntuDistroInfo()
420 _release_list = _d.all508 _release_list = _d.all
421 self.log.debug('Ubuntu release list: {}'.format(_release_list))
422 return _release_list509 return _release_list
423510
424 def file_to_url(self, file_rel_path):511 def file_to_url(self, file_rel_path):
@@ -450,15 +537,20 @@
450 cmd, code, output))537 cmd, code, output))
451 return None538 return None
452539
453 def get_process_id_list(self, sentry_unit, process_name):540 def get_process_id_list(self, sentry_unit, process_name,
541 expect_success=True):
454 """Get a list of process ID(s) from a single sentry juju unit542 """Get a list of process ID(s) from a single sentry juju unit
455 for a single process name.543 for a single process name.
456544
457 :param sentry_unit: Pointer to amulet sentry instance (juju unit)545 :param sentry_unit: Amulet sentry instance (juju unit)
458 :param process_name: Process name546 :param process_name: Process name
547 :param expect_success: If False, expect the PID to be missing,
548 raise if it is present.
459 :returns: List of process IDs549 :returns: List of process IDs
460 """550 """
461 cmd = 'pidof {}'.format(process_name)551 cmd = 'pidof -x {}'.format(process_name)
552 if not expect_success:
553 cmd += " || exit 0 && exit 1"
462 output, code = sentry_unit.run(cmd)554 output, code = sentry_unit.run(cmd)
463 if code != 0:555 if code != 0:
464 msg = ('{} `{}` returned {} '556 msg = ('{} `{}` returned {} '
@@ -467,14 +559,23 @@
467 amulet.raise_status(amulet.FAIL, msg=msg)559 amulet.raise_status(amulet.FAIL, msg=msg)
468 return str(output).split()560 return str(output).split()
469561
470 def get_unit_process_ids(self, unit_processes):562 def get_unit_process_ids(self, unit_processes, expect_success=True):
471 """Construct a dict containing unit sentries, process names, and563 """Construct a dict containing unit sentries, process names, and
472 process IDs."""564 process IDs.
565
566 :param unit_processes: A dictionary of Amulet sentry instance
567 to list of process names.
568 :param expect_success: if False expect the processes to not be
569 running, raise if they are.
570 :returns: Dictionary of Amulet sentry instance to dictionary
571 of process names to PIDs.
572 """
473 pid_dict = {}573 pid_dict = {}
474 for sentry_unit, process_list in unit_processes.iteritems():574 for sentry_unit, process_list in six.iteritems(unit_processes):
475 pid_dict[sentry_unit] = {}575 pid_dict[sentry_unit] = {}
476 for process in process_list:576 for process in process_list:
477 pids = self.get_process_id_list(sentry_unit, process)577 pids = self.get_process_id_list(
578 sentry_unit, process, expect_success=expect_success)
478 pid_dict[sentry_unit].update({process: pids})579 pid_dict[sentry_unit].update({process: pids})
479 return pid_dict580 return pid_dict
480581
@@ -488,7 +589,7 @@
488 return ('Unit count mismatch. expected, actual: {}, '589 return ('Unit count mismatch. expected, actual: {}, '
489 '{} '.format(len(expected), len(actual)))590 '{} '.format(len(expected), len(actual)))
490591
491 for (e_sentry, e_proc_names) in expected.iteritems():592 for (e_sentry, e_proc_names) in six.iteritems(expected):
492 e_sentry_name = e_sentry.info['unit_name']593 e_sentry_name = e_sentry.info['unit_name']
493 if e_sentry in actual.keys():594 if e_sentry in actual.keys():
494 a_proc_names = actual[e_sentry]595 a_proc_names = actual[e_sentry]
@@ -500,22 +601,40 @@
500 return ('Process name count mismatch. expected, actual: {}, '601 return ('Process name count mismatch. expected, actual: {}, '
501 '{}'.format(len(expected), len(actual)))602 '{}'.format(len(expected), len(actual)))
502603
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 \
504 zip(e_proc_names.items(), a_proc_names.items()):605 zip(e_proc_names.items(), a_proc_names.items()):
505 if e_proc_name != a_proc_name:606 if e_proc_name != a_proc_name:
506 return ('Process name mismatch. expected, actual: {}, '607 return ('Process name mismatch. expected, actual: {}, '
507 '{}'.format(e_proc_name, a_proc_name))608 '{}'.format(e_proc_name, a_proc_name))
508609
509 a_pids_length = len(a_pids)610 a_pids_length = len(a_pids)
510 if e_pids_length != a_pids_length:611 fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
511 return ('PID count mismatch. {} ({}) expected, actual: '
512 '{}, {} ({})'.format(e_sentry_name, e_proc_name,612 '{}, {} ({})'.format(e_sentry_name, e_proc_name,
513 e_pids_length, a_pids_length,613 e_pids, a_pids_length,
514 a_pids))614 a_pids))
615
616 # If expected is a list, ensure at least one PID quantity match
617 if isinstance(e_pids, list) and \
618 a_pids_length not in e_pids:
619 return fail_msg
620 # If expected is not bool and not list,
621 # ensure PID quantities match
622 elif not isinstance(e_pids, bool) and \
623 not isinstance(e_pids, list) and \
624 a_pids_length != e_pids:
625 return fail_msg
626 # If expected is bool True, ensure 1 or more PIDs exist
627 elif isinstance(e_pids, bool) and \
628 e_pids is True and a_pids_length < 1:
629 return fail_msg
630 # If expected is bool False, ensure 0 PIDs exist
631 elif isinstance(e_pids, bool) and \
632 e_pids is False and a_pids_length != 0:
633 return fail_msg
515 else:634 else:
516 self.log.debug('PID check OK: {} {} {}: '635 self.log.debug('PID check OK: {} {} {}: '
517 '{}'.format(e_sentry_name, e_proc_name,636 '{}'.format(e_sentry_name, e_proc_name,
518 e_pids_length, a_pids))637 e_pids, a_pids))
519 return None638 return None
520639
521 def validate_list_of_identical_dicts(self, list_of_dicts):640 def validate_list_of_identical_dicts(self, list_of_dicts):
@@ -531,3 +650,180 @@
531 return 'Dicts within list are not identical'650 return 'Dicts within list are not identical'
532651
533 return None652 return None
653
654 def validate_sectionless_conf(self, file_contents, expected):
655 """A crude conf parser. Useful to inspect configuration files which
656 do not have section headers (as would be necessary in order to use
657 the configparser). Such as openstack-dashboard or rabbitmq confs."""
658 for line in file_contents.split('\n'):
659 if '=' in line:
660 args = line.split('=')
661 if len(args) <= 1:
662 continue
663 key = args[0].strip()
664 value = args[1].strip()
665 if key in expected.keys():
666 if expected[key] != value:
667 msg = ('Config mismatch. Expected, actual: {}, '
668 '{}'.format(expected[key], value))
669 amulet.raise_status(amulet.FAIL, msg=msg)
670
671 def get_unit_hostnames(self, units):
672 """Return a dict of juju unit names to hostnames."""
673 host_names = {}
674 for unit in units:
675 host_names[unit.info['unit_name']] = \
676 str(unit.file_contents('/etc/hostname').strip())
677 self.log.debug('Unit host names: {}'.format(host_names))
678 return host_names
679
680 def run_cmd_unit(self, sentry_unit, cmd):
681 """Run a command on a unit, return the output and exit code."""
682 output, code = sentry_unit.run(cmd)
683 if code == 0:
684 self.log.debug('{} `{}` command returned {} '
685 '(OK)'.format(sentry_unit.info['unit_name'],
686 cmd, code))
687 else:
688 msg = ('{} `{}` command returned {} '
689 '{}'.format(sentry_unit.info['unit_name'],
690 cmd, code, output))
691 amulet.raise_status(amulet.FAIL, msg=msg)
692 return str(output), code
693
694 def file_exists_on_unit(self, sentry_unit, file_name):
695 """Check if a file exists on a unit."""
696 try:
697 sentry_unit.file_stat(file_name)
698 return True
699 except IOError:
700 return False
701 except Exception as e:
702 msg = 'Error checking file {}: {}'.format(file_name, e)
703 amulet.raise_status(amulet.FAIL, msg=msg)
704
705 def file_contents_safe(self, sentry_unit, file_name,
706 max_wait=60, fatal=False):
707 """Get file contents from a sentry unit. Wrap amulet file_contents
708 with retry logic to address races where a file checks as existing,
709 but no longer exists by the time file_contents is called.
710 Return None if file not found. Optionally raise if fatal is True."""
711 unit_name = sentry_unit.info['unit_name']
712 file_contents = False
713 tries = 0
714 while not file_contents and tries < (max_wait / 4):
715 try:
716 file_contents = sentry_unit.file_contents(file_name)
717 except IOError:
718 self.log.debug('Attempt {} to open file {} from {} '
719 'failed'.format(tries, file_name,
720 unit_name))
721 time.sleep(4)
722 tries += 1
723
724 if file_contents:
725 return file_contents
726 elif not fatal:
727 return None
728 elif fatal:
729 msg = 'Failed to get file contents from unit.'
730 amulet.raise_status(amulet.FAIL, msg)
731
732 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
733 """Open a TCP socket to check for a listening sevice on a host.
734
735 :param host: host name or IP address, default to localhost
736 :param port: TCP port number, default to 22
737 :param timeout: Connect timeout, default to 15 seconds
738 :returns: True if successful, False if connect failed
739 """
740
741 # Resolve host name if possible
742 try:
743 connect_host = socket.gethostbyname(host)
744 host_human = "{} ({})".format(connect_host, host)
745 except socket.error as e:
746 self.log.warn('Unable to resolve address: '
747 '{} ({}) Trying anyway!'.format(host, e))
748 connect_host = host
749 host_human = connect_host
750
751 # Attempt socket connection
752 try:
753 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
754 knock.settimeout(timeout)
755 knock.connect((connect_host, port))
756 knock.close()
757 self.log.debug('Socket connect OK for host '
758 '{} on port {}.'.format(host_human, port))
759 return True
760 except socket.error as e:
761 self.log.debug('Socket connect FAIL for'
762 ' {} port {} ({})'.format(host_human, port, e))
763 return False
764
765 def port_knock_units(self, sentry_units, port=22,
766 timeout=15, expect_success=True):
767 """Open a TCP socket to check for a listening sevice on each
768 listed juju unit.
769
770 :param sentry_units: list of sentry unit pointers
771 :param port: TCP port number, default to 22
772 :param timeout: Connect timeout, default to 15 seconds
773 :expect_success: True by default, set False to invert logic
774 :returns: None if successful, Failure message otherwise
775 """
776 for unit in sentry_units:
777 host = unit.info['public-address']
778 connected = self.port_knock_tcp(host, port, timeout)
779 if not connected and expect_success:
780 return 'Socket connect failed.'
781 elif connected and not expect_success:
782 return 'Socket connected unexpectedly.'
783
784 def get_uuid_epoch_stamp(self):
785 """Returns a stamp string based on uuid4 and epoch time. Useful in
786 generating test messages which need to be unique-ish."""
787 return '[{}-{}]'.format(uuid.uuid4(), time.time())
788
789# amulet juju action helpers:
790 def run_action(self, unit_sentry, action,
791 _check_output=subprocess.check_output,
792 params=None):
793 """Run the named action on a given unit sentry.
794
795 params a dict of parameters to use
796 _check_output parameter is used for dependency injection.
797
798 @return action_id.
799 """
800 unit_id = unit_sentry.info["unit_name"]
801 command = ["juju", "action", "do", "--format=json", unit_id, action]
802 if params is not None:
803 for key, value in params.iteritems():
804 command.append("{}={}".format(key, value))
805 self.log.info("Running command: %s\n" % " ".join(command))
806 output = _check_output(command, universal_newlines=True)
807 data = json.loads(output)
808 action_id = data[u'Action queued with id']
809 return action_id
810
811 def wait_on_action(self, action_id, _check_output=subprocess.check_output):
812 """Wait for a given action, returning if it completed or not.
813
814 _check_output parameter is used for dependency injection.
815 """
816 command = ["juju", "action", "fetch", "--format=json", "--wait=0",
817 action_id]
818 output = _check_output(command, universal_newlines=True)
819 data = json.loads(output)
820 return data.get(u"status") == "completed"
821
822 def status_get(self, unit):
823 """Return the current service status of this unit."""
824 raw_status, return_code = unit.run(
825 "status-get --format=json --include-data")
826 if return_code != 0:
827 return ("unknown", "")
828 status = json.loads(raw_status)
829 return (status["status"], status["message"])
534830
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-04-24 06:22:47 +0000
@@ -148,6 +148,13 @@
148 self.description = description148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)149 self.check_cmd = self._locate_cmd(check_cmd)
150150
151 def _get_check_filename(self):
152 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
153
154 def _get_service_filename(self, hostname):
155 return os.path.join(NRPE.nagios_exportdir,
156 'service__{}_{}.cfg'.format(hostname, self.command))
157
151 def _locate_cmd(self, check_cmd):158 def _locate_cmd(self, check_cmd):
152 search_path = (159 search_path = (
153 '/usr/lib/nagios/plugins',160 '/usr/lib/nagios/plugins',
@@ -163,9 +170,21 @@
163 log('Check command not found: {}'.format(parts[0]))170 log('Check command not found: {}'.format(parts[0]))
164 return ''171 return ''
165172
173 def _remove_service_files(self):
174 if not os.path.exists(NRPE.nagios_exportdir):
175 return
176 for f in os.listdir(NRPE.nagios_exportdir):
177 if f.endswith('_{}.cfg'.format(self.command)):
178 os.remove(os.path.join(NRPE.nagios_exportdir, f))
179
180 def remove(self, hostname):
181 nrpe_check_file = self._get_check_filename()
182 if os.path.exists(nrpe_check_file):
183 os.remove(nrpe_check_file)
184 self._remove_service_files()
185
166 def write(self, nagios_context, hostname, nagios_servicegroups):186 def write(self, nagios_context, hostname, nagios_servicegroups):
167 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(187 nrpe_check_file = self._get_check_filename()
168 self.command)
169 with open(nrpe_check_file, 'w') as nrpe_check_config:188 with open(nrpe_check_file, 'w') as nrpe_check_config:
170 nrpe_check_config.write("# check {}\n".format(self.shortname))189 nrpe_check_config.write("# check {}\n".format(self.shortname))
171 nrpe_check_config.write("command[{}]={}\n".format(190 nrpe_check_config.write("command[{}]={}\n".format(
@@ -180,9 +199,7 @@
180199
181 def write_service_config(self, nagios_context, hostname,200 def write_service_config(self, nagios_context, hostname,
182 nagios_servicegroups):201 nagios_servicegroups):
183 for f in os.listdir(NRPE.nagios_exportdir):202 self._remove_service_files()
184 if re.search('.*{}.cfg'.format(self.command), f):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186203
187 templ_vars = {204 templ_vars = {
188 'nagios_hostname': hostname,205 'nagios_hostname': hostname,
@@ -192,8 +209,7 @@
192 'command': self.command,209 'command': self.command,
193 }210 }
194 nrpe_service_text = Check.service_template.format(**templ_vars)211 nrpe_service_text = Check.service_template.format(**templ_vars)
195 nrpe_service_file = '{}/service__{}_{}.cfg'.format(212 nrpe_service_file = self._get_service_filename(hostname)
196 NRPE.nagios_exportdir, hostname, self.command)
197 with open(nrpe_service_file, 'w') as nrpe_service_config:213 with open(nrpe_service_file, 'w') as nrpe_service_config:
198 nrpe_service_config.write(str(nrpe_service_text))214 nrpe_service_config.write(str(nrpe_service_text))
199215
@@ -218,12 +234,32 @@
218 if hostname:234 if hostname:
219 self.hostname = hostname235 self.hostname = hostname
220 else:236 else:
221 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)237 nagios_hostname = get_nagios_hostname()
238 if nagios_hostname:
239 self.hostname = nagios_hostname
240 else:
241 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
222 self.checks = []242 self.checks = []
223243
224 def add_check(self, *args, **kwargs):244 def add_check(self, *args, **kwargs):
225 self.checks.append(Check(*args, **kwargs))245 self.checks.append(Check(*args, **kwargs))
226246
247 def remove_check(self, *args, **kwargs):
248 if kwargs.get('shortname') is None:
249 raise ValueError('shortname of check must be specified')
250
251 # Use sensible defaults if they're not specified - these are not
252 # actually used during removal, but they're required for constructing
253 # the Check object; check_disk is chosen because it's part of the
254 # nagios-plugins-basic package.
255 if kwargs.get('check_cmd') is None:
256 kwargs['check_cmd'] = 'check_disk'
257 if kwargs.get('description') is None:
258 kwargs['description'] = ''
259
260 check = Check(*args, **kwargs)
261 check.remove(self.hostname)
262
227 def write(self):263 def write(self):
228 try:264 try:
229 nagios_uid = pwd.getpwnam('nagios').pw_uid265 nagios_uid = pwd.getpwnam('nagios').pw_uid
@@ -260,7 +296,7 @@
260 :param str relation_name: Name of relation nrpe sub joined to296 :param str relation_name: Name of relation nrpe sub joined to
261 """297 """
262 for rel in relations_of_type(relation_name):298 for rel in relations_of_type(relation_name):
263 if 'nagios_hostname' in rel:299 if 'nagios_host_context' in rel:
264 return rel['nagios_host_context']300 return rel['nagios_host_context']
265301
266302
@@ -301,11 +337,13 @@
301 upstart_init = '/etc/init/%s.conf' % svc337 upstart_init = '/etc/init/%s.conf' % svc
302 sysv_init = '/etc/init.d/%s' % svc338 sysv_init = '/etc/init.d/%s' % svc
303 if os.path.exists(upstart_init):339 if os.path.exists(upstart_init):
304 nrpe.add_check(340 # Don't add a check for these services from neutron-gateway
305 shortname=svc,341 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
306 description='process check {%s}' % unit_name,342 nrpe.add_check(
307 check_cmd='check_upstart_job %s' % svc343 shortname=svc,
308 )344 description='process check {%s}' % unit_name,
345 check_cmd='check_upstart_job %s' % svc
346 )
309 elif os.path.exists(sysv_init):347 elif os.path.exists(sysv_init):
310 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc348 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
311 cron_file = ('*/5 * * * * root '349 cron_file = ('*/5 * * * * root '
312350
=== added directory 'hooks/charmhelpers/contrib/hardening'
=== added file 'hooks/charmhelpers/contrib/hardening/__init__.py'
--- hooks/charmhelpers/contrib/hardening/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,15 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added directory 'hooks/charmhelpers/contrib/hardening/apache'
=== added file 'hooks/charmhelpers/contrib/hardening/apache/__init__.py'
--- hooks/charmhelpers/contrib/hardening/apache/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
020
=== added directory 'hooks/charmhelpers/contrib/hardening/apache/checks'
=== added file 'hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.apache.checks import config
22
23
24def run_apache_checks():
25 log("Starting Apache hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("Apache hardening checks complete.", level=DEBUG)
032
=== added file 'hooks/charmhelpers/contrib/hardening/apache/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/apache/checks/config.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/checks/config.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,100 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import re
19import subprocess
20
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO,
25)
26from charmhelpers.contrib.hardening.audits.file import (
27 FilePermissionAudit,
28 DirectoryPermissionAudit,
29 NoReadWriteForOther,
30 TemplatedFile,
31)
32from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
33from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
34from charmhelpers.contrib.hardening import utils
35
36
37def get_audits():
38 """Get Apache hardening config audits.
39
40 :returns: dictionary of audits
41 """
42 if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
43 log("Apache server does not appear to be installed on this node - "
44 "skipping apache hardening", level=INFO)
45 return []
46
47 context = ApacheConfContext()
48 settings = utils.get_settings('apache')
49 audits = [
50 FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root',
51 group='root', mode=0o0640),
52
53 TemplatedFile(os.path.join(settings['common']['apache_dir'],
54 'mods-available/alias.conf'),
55 context,
56 TEMPLATES_DIR,
57 mode=0o0755,
58 user='root',
59 service_actions=[{'service': 'apache2',
60 'actions': ['restart']}]),
61
62 TemplatedFile(os.path.join(settings['common']['apache_dir'],
63 'conf-enabled/hardening.conf'),
64 context,
65 TEMPLATES_DIR,
66 mode=0o0640,
67 user='root',
68 service_actions=[{'service': 'apache2',
69 'actions': ['restart']}]),
70
71 DirectoryPermissionAudit(settings['common']['apache_dir'],
72 user='root',
73 group='root',
74 mode=0o640),
75
76 DisabledModuleAudit(settings['hardening']['modules_to_disable']),
77
78 NoReadWriteForOther(settings['common']['apache_dir']),
79 ]
80
81 return audits
82
83
84class ApacheConfContext(object):
85 """Defines the set of key/value pairs to set in a apache config file.
86
87 This context, when called, will return a dictionary containing the
88 key/value pairs of setting to specify in the
89 /etc/apache/conf-enabled/hardening.conf file.
90 """
91 def __call__(self):
92 settings = utils.get_settings('apache')
93 ctxt = settings['hardening']
94
95 out = subprocess.check_output(['apache2', '-v'])
96 ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
97 out).group(1)
98 ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
99 ctxt['traceenable'] = settings['hardening']['traceenable']
100 return ctxt
0101
=== added directory 'hooks/charmhelpers/contrib/hardening/audits'
=== added file 'hooks/charmhelpers/contrib/hardening/audits/__init__.py'
--- hooks/charmhelpers/contrib/hardening/audits/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,63 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17
18class BaseAudit(object): # NO-QA
19 """Base class for hardening checks.
20
21 The lifecycle of a hardening check is to first check to see if the system
22 is in compliance for the specified check. If it is not in compliance, the
23 check method will return a value which will be supplied to the.
24 """
25 def __init__(self, *args, **kwargs):
26 self.unless = kwargs.get('unless', None)
27 super(BaseAudit, self).__init__()
28
29 def ensure_compliance(self):
30 """Checks to see if the current hardening check is in compliance or
31 not.
32
33 If the check that is performed is not in compliance, then an exception
34 should be raised.
35 """
36 pass
37
38 def _take_action(self):
39 """Determines whether to perform the action or not.
40
41 Checks whether or not an action should be taken. This is determined by
42 the truthy value for the unless parameter. If unless is a callback
43 method, it will be invoked with no parameters in order to determine
44 whether or not the action should be taken. Otherwise, the truthy value
45 of the unless attribute will determine if the action should be
46 performed.
47 """
48 # Do the action if there isn't an unless override.
49 if self.unless is None:
50 return True
51
52 # Invoke the callback if there is one.
53 if hasattr(self.unless, '__call__'):
54 results = self.unless()
55 if results:
56 return False
57 else:
58 return True
59
60 if self.unless:
61 return False
62 else:
63 return True
064
=== added file 'hooks/charmhelpers/contrib/hardening/audits/apache.py'
--- hooks/charmhelpers/contrib/hardening/audits/apache.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/apache.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,100 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import re
18import subprocess
19
20from six import string_types
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO,
25 ERROR,
26)
27
28from charmhelpers.contrib.hardening.audits import BaseAudit
29
30
31class DisabledModuleAudit(BaseAudit):
32 """Audits Apache2 modules.
33
34 Determines if the apache2 modules are enabled. If the modules are enabled
35 then they are removed in the ensure_compliance.
36 """
37 def __init__(self, modules):
38 if modules is None:
39 self.modules = []
40 elif isinstance(modules, string_types):
41 self.modules = [modules]
42 else:
43 self.modules = modules
44
45 def ensure_compliance(self):
46 """Ensures that the modules are not loaded."""
47 if not self.modules:
48 return
49
50 try:
51 loaded_modules = self._get_loaded_modules()
52 non_compliant_modules = []
53 for module in self.modules:
54 if module in loaded_modules:
55 log("Module '%s' is enabled but should not be." %
56 (module), level=INFO)
57 non_compliant_modules.append(module)
58
59 if len(non_compliant_modules) == 0:
60 return
61
62 for module in non_compliant_modules:
63 self._disable_module(module)
64 self._restart_apache()
65 except subprocess.CalledProcessError as e:
66 log('Error occurred auditing apache module compliance. '
67 'This may have been already reported. '
68 'Output is: %s' % e.output, level=ERROR)
69
70 @staticmethod
71 def _get_loaded_modules():
72 """Returns the modules which are enabled in Apache."""
73 output = subprocess.check_output(['apache2ctl', '-M'])
74 modules = []
75 for line in output.strip().split():
76 # Each line of the enabled module output looks like:
77 # module_name (static|shared)
78 # Plus a header line at the top of the output which is stripped
79 # out by the regex.
80 matcher = re.search(r'^ (\S*)', line)
81 if matcher:
82 modules.append(matcher.group(1))
83 return modules
84
85 @staticmethod
86 def _disable_module(module):
87 """Disables the specified module in Apache."""
88 try:
89 subprocess.check_call(['a2dismod', module])
90 except subprocess.CalledProcessError as e:
91 # Note: catch error here to allow the attempt of disabling
92 # multiple modules in one go rather than failing after the
93 # first module fails.
94 log('Error occurred disabling module %s. '
95 'Output is: %s' % (module, e.output), level=ERROR)
96
97 @staticmethod
98 def _restart_apache():
99 """Restarts the apache process"""
100 subprocess.check_output(['service', 'apache2', 'restart'])
0101
=== added file 'hooks/charmhelpers/contrib/hardening/audits/apt.py'
--- hooks/charmhelpers/contrib/hardening/audits/apt.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/apt.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,105 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from __future__ import absolute_import # required for external apt import
18from apt import apt_pkg
19from six import string_types
20
21from charmhelpers.fetch import (
22 apt_cache,
23 apt_purge
24)
25from charmhelpers.core.hookenv import (
26 log,
27 DEBUG,
28 WARNING,
29)
30from charmhelpers.contrib.hardening.audits import BaseAudit
31
32
33class AptConfig(BaseAudit):
34
35 def __init__(self, config, **kwargs):
36 self.config = config
37
38 def verify_config(self):
39 apt_pkg.init()
40 for cfg in self.config:
41 value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
42 if value and value != cfg['expected']:
43 log("APT config '%s' has unexpected value '%s' "
44 "(expected='%s')" %
45 (cfg['key'], value, cfg['expected']), level=WARNING)
46
47 def ensure_compliance(self):
48 self.verify_config()
49
50
51class RestrictedPackages(BaseAudit):
52 """Class used to audit restricted packages on the system."""
53
54 def __init__(self, pkgs, **kwargs):
55 super(RestrictedPackages, self).__init__(**kwargs)
56 if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
57 self.pkgs = [pkgs]
58 else:
59 self.pkgs = pkgs
60
61 def ensure_compliance(self):
62 cache = apt_cache()
63
64 for p in self.pkgs:
65 if p not in cache:
66 continue
67
68 pkg = cache[p]
69 if not self.is_virtual_package(pkg):
70 if not pkg.current_ver:
71 log("Package '%s' is not installed." % pkg.name,
72 level=DEBUG)
73 continue
74 else:
75 log("Restricted package '%s' is installed" % pkg.name,
76 level=WARNING)
77 self.delete_package(cache, pkg)
78 else:
79 log("Checking restricted virtual package '%s' provides" %
80 pkg.name, level=DEBUG)
81 self.delete_package(cache, pkg)
82
83 def delete_package(self, cache, pkg):
84 """Deletes the package from the system.
85
86 Deletes the package form the system, properly handling virtual
87 packages.
88
89 :param cache: the apt cache
90 :param pkg: the package to remove
91 """
92 if self.is_virtual_package(pkg):
93 log("Package '%s' appears to be virtual - purging provides" %
94 pkg.name, level=DEBUG)
95 for _p in pkg.provides_list:
96 self.delete_package(cache, _p[2].parent_pkg)
97 elif not pkg.current_ver:
98 log("Package '%s' not installed" % pkg.name, level=DEBUG)
99 return
100 else:
101 log("Purging package '%s'" % pkg.name, level=DEBUG)
102 apt_purge(pkg.name)
103
104 def is_virtual_package(self, pkg):
105 return pkg.has_provides and not pkg.has_versions
0106
=== added file 'hooks/charmhelpers/contrib/hardening/audits/file.py'
--- hooks/charmhelpers/contrib/hardening/audits/file.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/file.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,552 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import grp
18import os
19import pwd
20import re
21
22from subprocess import (
23 CalledProcessError,
24 check_output,
25 check_call,
26)
27from traceback import format_exc
28from six import string_types
29from stat import (
30 S_ISGID,
31 S_ISUID
32)
33
34from charmhelpers.core.hookenv import (
35 log,
36 DEBUG,
37 INFO,
38 WARNING,
39 ERROR,
40)
41from charmhelpers.core import unitdata
42from charmhelpers.core.host import file_hash
43from charmhelpers.contrib.hardening.audits import BaseAudit
44from charmhelpers.contrib.hardening.templating import (
45 get_template_path,
46 render_and_write,
47)
48from charmhelpers.contrib.hardening import utils
49
50
51class BaseFileAudit(BaseAudit):
52 """Base class for file audits.
53
54 Provides api stubs for compliance check flow that must be used by any class
55 that implemented this one.
56 """
57
58 def __init__(self, paths, always_comply=False, *args, **kwargs):
59 """
60 :param paths: string path of list of paths of files we want to apply
61 compliance checks are criteria to.
62 :param always_comply: if true compliance criteria is always applied
63 else compliance is skipped for non-existent
64 paths.
65 """
66 super(BaseFileAudit, self).__init__(*args, **kwargs)
67 self.always_comply = always_comply
68 if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
69 self.paths = [paths]
70 else:
71 self.paths = paths
72
73 def ensure_compliance(self):
74 """Ensure that the all registered files comply to registered criteria.
75 """
76 for p in self.paths:
77 if os.path.exists(p):
78 if self.is_compliant(p):
79 continue
80
81 log('File %s is not in compliance.' % p, level=INFO)
82 else:
83 if not self.always_comply:
84 log("Non-existent path '%s' - skipping compliance check"
85 % (p), level=INFO)
86 continue
87
88 if self._take_action():
89 log("Applying compliance criteria to '%s'" % (p), level=INFO)
90 self.comply(p)
91
92 def is_compliant(self, path):
93 """Audits the path to see if it is compliance.
94
95 :param path: the path to the file that should be checked.
96 """
97 raise NotImplementedError
98
99 def comply(self, path):
100 """Enforces the compliance of a path.
101
102 :param path: the path to the file that should be enforced.
103 """
104 raise NotImplementedError
105
106 @classmethod
107 def _get_stat(cls, path):
108 """Returns the Posix st_stat information for the specified file path.
109
110 :param path: the path to get the st_stat information for.
111 :returns: an st_stat object for the path or None if the path doesn't
112 exist.
113 """
114 return os.stat(path)
115
116
117class FilePermissionAudit(BaseFileAudit):
118 """Implements an audit for file permissions and ownership for a user.
119
120 This class implements functionality that ensures that a specific user/group
121 will own the file(s) specified and that the permissions specified are
122 applied properly to the file.
123 """
124 def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
125 self.user = user
126 self.group = group
127 self.mode = mode
128 super(FilePermissionAudit, self).__init__(paths, user, group, mode,
129 **kwargs)
130
131 @property
132 def user(self):
133 return self._user
134
135 @user.setter
136 def user(self, name):
137 try:
138 user = pwd.getpwnam(name)
139 except KeyError:
140 log('Unknown user %s' % name, level=ERROR)
141 user = None
142 self._user = user
143
144 @property
145 def group(self):
146 return self._group
147
148 @group.setter
149 def group(self, name):
150 try:
151 group = None
152 if name:
153 group = grp.getgrnam(name)
154 else:
155 group = grp.getgrgid(self.user.pw_gid)
156 except KeyError:
157 log('Unknown group %s' % name, level=ERROR)
158 self._group = group
159
160 def is_compliant(self, path):
161 """Checks if the path is in compliance.
162
163 Used to determine if the path specified meets the necessary
164 requirements to be in compliance with the check itself.
165
166 :param path: the file path to check
167 :returns: True if the path is compliant, False otherwise.
168 """
169 stat = self._get_stat(path)
170 user = self.user
171 group = self.group
172
173 compliant = True
174 if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
175 log('File %s is not owned by %s:%s.' % (path, user.pw_name,
176 group.gr_name),
177 level=INFO)
178 compliant = False
179
180 # POSIX refers to the st_mode bits as corresponding to both the
181 # file type and file permission bits, where the least significant 12
182 # bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
183 # file permission bits (8-0)
184 perms = stat.st_mode & 0o7777
185 if perms != self.mode:
186 log('File %s has incorrect permissions, currently set to %s' %
187 (path, oct(stat.st_mode & 0o7777)), level=INFO)
188 compliant = False
189
190 return compliant
191
192 def comply(self, path):
193 """Issues a chown and chmod to the file paths specified."""
194 utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
195 self.mode)
196
197
198class DirectoryPermissionAudit(FilePermissionAudit):
199 """Performs a permission check for the specified directory path."""
200
201 def __init__(self, paths, user, group=None, mode=0o600,
202 recursive=True, **kwargs):
203 super(DirectoryPermissionAudit, self).__init__(paths, user, group,
204 mode, **kwargs)
205 self.recursive = recursive
206
207 def is_compliant(self, path):
208 """Checks if the directory is compliant.
209
210 Used to determine if the path specified and all of its children
211 directories are in compliance with the check itself.
212
213 :param path: the directory path to check
214 :returns: True if the directory tree is compliant, otherwise False.
215 """
216 if not os.path.isdir(path):
217 log('Path specified %s is not a directory.' % path, level=ERROR)
218 raise ValueError("%s is not a directory." % path)
219
220 if not self.recursive:
221 return super(DirectoryPermissionAudit, self).is_compliant(path)
222
223 compliant = True
224 for root, dirs, _ in os.walk(path):
225 if len(dirs) > 0:
226 continue
227
228 if not super(DirectoryPermissionAudit, self).is_compliant(root):
229 compliant = False
230 continue
231
232 return compliant
233
234 def comply(self, path):
235 for root, dirs, _ in os.walk(path):
236 if len(dirs) > 0:
237 super(DirectoryPermissionAudit, self).comply(root)
238
239
240class ReadOnly(BaseFileAudit):
241 """Audits that files and folders are read only."""
242 def __init__(self, paths, *args, **kwargs):
243 super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
244
245 def is_compliant(self, path):
246 try:
247 output = check_output(['find', path, '-perm', '-go+w',
248 '-type', 'f']).strip()
249
250 # The find above will find any files which have permission sets
251 # which allow too broad of write access. As such, the path is
252 # compliant if there is no output.
253 if output:
254 return False
255
256 return True
257 except CalledProcessError as e:
258 log('Error occurred checking finding writable files for %s. '
259 'Error information is: command %s failed with returncode '
260 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
261 format_exc(e)), level=ERROR)
262 return False
263
264 def comply(self, path):
265 try:
266 check_output(['chmod', 'go-w', '-R', path])
267 except CalledProcessError as e:
268 log('Error occurred removing writeable permissions for %s. '
269 'Error information is: command %s failed with returncode '
270 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
271 format_exc(e)), level=ERROR)
272
273
274class NoReadWriteForOther(BaseFileAudit):
275 """Ensures that the files found under the base path are readable or
276 writable by anyone other than the owner or the group.
277 """
278 def __init__(self, paths):
279 super(NoReadWriteForOther, self).__init__(paths)
280
281 def is_compliant(self, path):
282 try:
283 cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
284 '-perm', '-o+w', '-type', 'f']
285 output = check_output(cmd).strip()
286
287 # The find above here will find any files which have read or
288 # write permissions for other, meaning there is too broad of access
289 # to read/write the file. As such, the path is compliant if there's
290 # no output.
291 if output:
292 return False
293
294 return True
295 except CalledProcessError as e:
296 log('Error occurred while finding files which are readable or '
297 'writable to the world in %s. '
298 'Command output is: %s.' % (path, e.output), level=ERROR)
299
300 def comply(self, path):
301 try:
302 check_output(['chmod', '-R', 'o-rw', path])
303 except CalledProcessError as e:
304 log('Error occurred attempting to change modes of files under '
305 'path %s. Output of command is: %s' % (path, e.output))
306
307
308class NoSUIDSGIDAudit(BaseFileAudit):
309 """Audits that specified files do not have SUID/SGID bits set."""
310 def __init__(self, paths, *args, **kwargs):
311 super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
312
313 def is_compliant(self, path):
314 stat = self._get_stat(path)
315 if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
316 return False
317
318 return True
319
320 def comply(self, path):
321 try:
322 log('Removing suid/sgid from %s.' % path, level=DEBUG)
323 check_output(['chmod', '-s', path])
324 except CalledProcessError as e:
325 log('Error occurred removing suid/sgid from %s.'
326 'Error information is: command %s failed with returncode '
327 '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
328 format_exc(e)), level=ERROR)
329
330
331class TemplatedFile(BaseFileAudit):
332 """The TemplatedFileAudit audits the contents of a templated file.
333
334 This audit renders a file from a template, sets the appropriate file
335 permissions, then generates a hashsum with which to check the content
336 changed.
337 """
338 def __init__(self, path, context, template_dir, mode, user='root',
339 group='root', service_actions=None, **kwargs):
340 self.context = context
341 self.user = user
342 self.group = group
343 self.mode = mode
344 self.template_dir = template_dir
345 self.service_actions = service_actions
346 super(TemplatedFile, self).__init__(paths=path, always_comply=True,
347 **kwargs)
348
349 def is_compliant(self, path):
350 """Determines if the templated file is compliant.
351
352 A templated file is only compliant if it has not changed (as
353 determined by its sha256 hashsum) AND its file permissions are set
354 appropriately.
355
356 :param path: the path to check compliance.
357 """
358 same_templates = self.templates_match(path)
359 same_content = self.contents_match(path)
360 same_permissions = self.permissions_match(path)
361
362 if same_content and same_permissions and same_templates:
363 return True
364
365 return False
366
367 def run_service_actions(self):
368 """Run any actions on services requested."""
369 if not self.service_actions:
370 return
371
372 for svc_action in self.service_actions:
373 name = svc_action['service']
374 actions = svc_action['actions']
375 log("Running service '%s' actions '%s'" % (name, actions),
376 level=DEBUG)
377 for action in actions:
378 cmd = ['service', name, action]
379 try:
380 check_call(cmd)
381 except CalledProcessError as exc:
382 log("Service name='%s' action='%s' failed - %s" %
383 (name, action, exc), level=WARNING)
384
385 def comply(self, path):
386 """Ensures the contents and the permissions of the file.
387
388 :param path: the path to correct
389 """
390 dirname = os.path.dirname(path)
391 if not os.path.exists(dirname):
392 os.makedirs(dirname)
393
394 self.pre_write()
395 render_and_write(self.template_dir, path, self.context())
396 utils.ensure_permissions(path, self.user, self.group, self.mode)
397 self.run_service_actions()
398 self.save_checksum(path)
399 self.post_write()
400
401 def pre_write(self):
402 """Invoked prior to writing the template."""
403 pass
404
405 def post_write(self):
406 """Invoked after writing the template."""
407 pass
408
409 def templates_match(self, path):
410 """Determines if the template files are the same.
411
412 The template file equality is determined by the hashsum of the
413 template files themselves. If there is no hashsum, then the content
414 cannot be sure to be the same so treat it as if they changed.
415 Otherwise, return whether or not the hashsums are the same.
416
417 :param path: the path to check
418 :returns: boolean
419 """
420 template_path = get_template_path(self.template_dir, path)
421 key = 'hardening:template:%s' % template_path
422 template_checksum = file_hash(template_path)
423 kv = unitdata.kv()
424 stored_tmplt_checksum = kv.get(key)
425 if not stored_tmplt_checksum:
426 kv.set(key, template_checksum)
427 kv.flush()
428 log('Saved template checksum for %s.' % template_path,
429 level=DEBUG)
430 # Since we don't have a template checksum, then assume it doesn't
431 # match and return that the template is different.
432 return False
433 elif stored_tmplt_checksum != template_checksum:
434 kv.set(key, template_checksum)
435 kv.flush()
436 log('Updated template checksum for %s.' % template_path,
437 level=DEBUG)
438 return False
439
440 # Here the template hasn't changed based upon the calculated
441 # checksum of the template and what was previously stored.
442 return True
443
444 def contents_match(self, path):
445 """Determines if the file content is the same.
446
447 This is determined by comparing hashsum of the file contents and
448 the saved hashsum. If there is no hashsum, then the content cannot
449 be sure to be the same so treat them as if they are not the same.
450 Otherwise, return True if the hashsums are the same, False if they
451 are not the same.
452
453 :param path: the file to check.
454 """
455 checksum = file_hash(path)
456
457 kv = unitdata.kv()
458 stored_checksum = kv.get('hardening:%s' % path)
459 if not stored_checksum:
460 # If the checksum hasn't been generated, return False to ensure
461 # the file is written and the checksum stored.
462 log('Checksum for %s has not been calculated.' % path, level=DEBUG)
463 return False
464 elif stored_checksum != checksum:
465 log('Checksum mismatch for %s.' % path, level=DEBUG)
466 return False
467
468 return True
469
470 def permissions_match(self, path):
471 """Determines if the file owner and permissions match.
472
473 :param path: the path to check.
474 """
475 audit = FilePermissionAudit(path, self.user, self.group, self.mode)
476 return audit.is_compliant(path)
477
478 def save_checksum(self, path):
479 """Calculates and saves the checksum for the path specified.
480
481 :param path: the path of the file to save the checksum.
482 """
483 checksum = file_hash(path)
484 kv = unitdata.kv()
485 kv.set('hardening:%s' % path, checksum)
486 kv.flush()
487
488
489class DeletedFile(BaseFileAudit):
490 """Audit to ensure that a file is deleted."""
491 def __init__(self, paths):
492 super(DeletedFile, self).__init__(paths)
493
494 def is_compliant(self, path):
495 return not os.path.exists(path)
496
497 def comply(self, path):
498 os.remove(path)
499
500
501class FileContentAudit(BaseFileAudit):
502 """Audit the contents of a file."""
503 def __init__(self, paths, cases, **kwargs):
504 # Cases we expect to pass
505 self.pass_cases = cases.get('pass', [])
506 # Cases we expect to fail
507 self.fail_cases = cases.get('fail', [])
508 super(FileContentAudit, self).__init__(paths, **kwargs)
509
510 def is_compliant(self, path):
511 """
512 Given a set of content matching cases i.e. tuple(regex, bool) where
513 bool value denotes whether or not regex is expected to match, check that
514 all cases match as expected with the contents of the file. Cases can be
515 expected to pass of fail.
516
517 :param path: Path of file to check.
518 :returns: Boolean value representing whether or not all cases are
519 found to be compliant.
520 """
521 log("Auditing contents of file '%s'" % (path), level=DEBUG)
522 with open(path, 'r') as fd:
523 contents = fd.read()
524
525 matches = 0
526 for pattern in self.pass_cases:
527 key = re.compile(pattern, flags=re.MULTILINE)
528 results = re.search(key, contents)
529 if results:
530 matches += 1
531 else:
532 log("Pattern '%s' was expected to pass but instead it failed"
533 % (pattern), level=WARNING)
534
535 for pattern in self.fail_cases:
536 key = re.compile(pattern, flags=re.MULTILINE)
537 results = re.search(key, contents)
538 if not results:
539 matches += 1
540 else:
541 log("Pattern '%s' was expected to fail but instead it passed"
542 % (pattern), level=WARNING)
543
544 total = len(self.pass_cases) + len(self.fail_cases)
545 log("Checked %s cases and %s passed" % (total, matches), level=DEBUG)
546 return matches == total
547
548 def comply(self, *args, **kwargs):
549 """NOOP since we just issue warnings. This is to avoid the
550 NotImplememtedError.
551 """
552 log("Not applying any compliance criteria, only checks.", level=INFO)
0553
=== added file 'hooks/charmhelpers/contrib/hardening/harden.py'
--- hooks/charmhelpers/contrib/hardening/harden.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/harden.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,84 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18
19from collections import OrderedDict
20
21from charmhelpers.core.hookenv import (
22 config,
23 log,
24 DEBUG,
25 WARNING,
26)
27from charmhelpers.contrib.hardening.host.checks import run_os_checks
28from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks
29from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
30from charmhelpers.contrib.hardening.apache.checks import run_apache_checks
31
32
33def harden(overrides=None):
34 """Hardening decorator.
35
36 This is the main entry point for running the hardening stack. In order to
37 run modules of the stack you must add this decorator to charm hook(s) and
38 ensure that your charm config.yaml contains the 'harden' option set to
39 one or more of the supported modules. Setting these will cause the
40 corresponding hardening code to be run when the hook fires.
41
42 This decorator can and should be applied to more than one hook or function
43 such that hardening modules are called multiple times. This is because
44 subsequent calls will perform auditing checks that will report any changes
45 to resources hardened by the first run (and possibly perform compliance
46 actions as a result of any detected infractions).
47
48 :param overrides: Optional list of stack modules used to override those
49 provided with 'harden' config.
50 :returns: Returns value returned by decorated function once executed.
51 """
52 def _harden_inner1(f):
53 log("Hardening function '%s'" % (f.__name__), level=DEBUG)
54
55 def _harden_inner2(*args, **kwargs):
56 RUN_CATALOG = OrderedDict([('os', run_os_checks),
57 ('ssh', run_ssh_checks),
58 ('mysql', run_mysql_checks),
59 ('apache', run_apache_checks)])
60
61 enabled = overrides or (config("harden") or "").split()
62 if enabled:
63 modules_to_run = []
64 # modules will always be performed in the following order
65 for module, func in six.iteritems(RUN_CATALOG):
66 if module in enabled:
67 enabled.remove(module)
68 modules_to_run.append(func)
69
70 if enabled:
71 log("Unknown hardening modules '%s' - ignoring" %
72 (', '.join(enabled)), level=WARNING)
73
74 for hardener in modules_to_run:
75 log("Executing hardening module '%s'" %
76 (hardener.__name__), level=DEBUG)
77 hardener()
78 else:
79 log("No hardening applied to '%s'" % (f.__name__), level=DEBUG)
80
81 return f(*args, **kwargs)
82 return _harden_inner2
83
84 return _harden_inner1
085
=== added directory 'hooks/charmhelpers/contrib/hardening/host'
=== added file 'hooks/charmhelpers/contrib/hardening/host/__init__.py'
--- hooks/charmhelpers/contrib/hardening/host/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
020
=== added directory 'hooks/charmhelpers/contrib/hardening/host/checks'
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,50 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.host.checks import (
22 apt,
23 limits,
24 login,
25 minimize_access,
26 pam,
27 profile,
28 securetty,
29 suid_sgid,
30 sysctl
31)
32
33
34def run_os_checks():
35 log("Starting OS hardening checks.", level=DEBUG)
36 checks = apt.get_audits()
37 checks.extend(limits.get_audits())
38 checks.extend(login.get_audits())
39 checks.extend(minimize_access.get_audits())
40 checks.extend(pam.get_audits())
41 checks.extend(profile.get_audits())
42 checks.extend(securetty.get_audits())
43 checks.extend(suid_sgid.get_audits())
44 checks.extend(sysctl.get_audits())
45
46 for check in checks:
47 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
48 check.ensure_compliance()
49
50 log("OS hardening checks complete.", level=DEBUG)
051
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/apt.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/apt.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/apt.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,39 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.utils import get_settings
18from charmhelpers.contrib.hardening.audits.apt import (
19 AptConfig,
20 RestrictedPackages,
21)
22
23
24def get_audits():
25 """Get OS hardening apt audits.
26
27 :returns: dictionary of audits
28 """
29 audits = [AptConfig([{'key': 'APT::Get::AllowUnauthenticated',
30 'expected': 'false'}])]
31
32 settings = get_settings('os')
33 clean_packages = settings['security']['packages_clean']
34 if clean_packages:
35 security_packages = settings['security']['packages_list']
36 if security_packages:
37 audits.append(RestrictedPackages(security_packages))
38
39 return audits
040
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/limits.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/limits.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/limits.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,55 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import (
18 DirectoryPermissionAudit,
19 TemplatedFile,
20)
21from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
22from charmhelpers.contrib.hardening import utils
23
24
25def get_audits():
26 """Get OS hardening security limits audits.
27
28 :returns: dictionary of audits
29 """
30 audits = []
31 settings = utils.get_settings('os')
32
33 # Ensure that the /etc/security/limits.d directory is only writable
34 # by the root user, but others can execute and read.
35 audits.append(DirectoryPermissionAudit('/etc/security/limits.d',
36 user='root', group='root',
37 mode=0o755))
38
39 # If core dumps are not enabled, then don't allow core dumps to be
40 # created as they may contain sensitive information.
41 if not settings['security']['kernel_enable_core_dump']:
42 audits.append(TemplatedFile('/etc/security/limits.d/10.hardcore.conf',
43 SecurityLimitsContext(),
44 template_dir=TEMPLATES_DIR,
45 user='root', group='root', mode=0o0440))
46 return audits
47
48
49class SecurityLimitsContext(object):
50
51 def __call__(self):
52 settings = utils.get_settings('os')
53 ctxt = {'disable_core_dump':
54 not settings['security']['kernel_enable_core_dump']}
55 return ctxt
056
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/login.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/login.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/login.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,67 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from six import string_types
18
19from charmhelpers.contrib.hardening.audits.file import TemplatedFile
20from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
21from charmhelpers.contrib.hardening import utils
22
23
24def get_audits():
25 """Get OS hardening login.defs audits.
26
27 :returns: dictionary of audits
28 """
29 audits = [TemplatedFile('/etc/login.defs', LoginContext(),
30 template_dir=TEMPLATES_DIR,
31 user='root', group='root', mode=0o0444)]
32 return audits
33
34
35class LoginContext(object):
36
37 def __call__(self):
38 settings = utils.get_settings('os')
39
40 # Octal numbers in yaml end up being turned into decimal,
41 # so check if the umask is entered as a string (e.g. '027')
42 # or as an octal umask as we know it (e.g. 002). If its not
43 # a string assume it to be octal and turn it into an octal
44 # string.
45 umask = settings['environment']['umask']
46 if not isinstance(umask, string_types):
47 umask = '%s' % oct(umask)
48
49 ctxt = {
50 'additional_user_paths':
51 settings['environment']['extra_user_paths'],
52 'umask': umask,
53 'pwd_max_age': settings['auth']['pw_max_age'],
54 'pwd_min_age': settings['auth']['pw_min_age'],
55 'uid_min': settings['auth']['uid_min'],
56 'sys_uid_min': settings['auth']['sys_uid_min'],
57 'sys_uid_max': settings['auth']['sys_uid_max'],
58 'gid_min': settings['auth']['gid_min'],
59 'sys_gid_min': settings['auth']['sys_gid_min'],
60 'sys_gid_max': settings['auth']['sys_gid_max'],
61 'login_retries': settings['auth']['retries'],
62 'login_timeout': settings['auth']['timeout'],
63 'chfn_restrict': settings['auth']['chfn_restrict'],
64 'allow_login_without_home': settings['auth']['allow_homeless']
65 }
66
67 return ctxt
068
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,52 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import (
18 FilePermissionAudit,
19 ReadOnly,
20)
21from charmhelpers.contrib.hardening import utils
22
23
24def get_audits():
25 """Get OS hardening access audits.
26
27 :returns: dictionary of audits
28 """
29 audits = []
30 settings = utils.get_settings('os')
31
32 # Remove write permissions from $PATH folders for all regular users.
33 # This prevents changing system-wide commands from normal users.
34 path_folders = {'/usr/local/sbin',
35 '/usr/local/bin',
36 '/usr/sbin',
37 '/usr/bin',
38 '/bin'}
39 extra_user_paths = settings['environment']['extra_user_paths']
40 path_folders.update(extra_user_paths)
41 audits.append(ReadOnly(path_folders))
42
43 # Only allow the root user to have access to the shadow file.
44 audits.append(FilePermissionAudit('/etc/shadow', 'root', 'root', 0o0600))
45
46 if 'change_user' not in settings['security']['users_allow']:
47 # su should only be accessible to user and group root, unless it is
48 # expressly defined to allow users to change to root via the
49 # security_users_allow config option.
50 audits.append(FilePermissionAudit('/bin/su', 'root', 'root', 0o750))
51
52 return audits
053
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/pam.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/pam.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/pam.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,134 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from subprocess import (
18 check_output,
19 CalledProcessError,
20)
21
22from charmhelpers.core.hookenv import (
23 log,
24 DEBUG,
25 ERROR,
26)
27from charmhelpers.fetch import (
28 apt_install,
29 apt_purge,
30 apt_update,
31)
32from charmhelpers.contrib.hardening.audits.file import (
33 TemplatedFile,
34 DeletedFile,
35)
36from charmhelpers.contrib.hardening import utils
37from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
38
39
40def get_audits():
41 """Get OS hardening PAM authentication audits.
42
43 :returns: dictionary of audits
44 """
45 audits = []
46
47 settings = utils.get_settings('os')
48
49 if settings['auth']['pam_passwdqc_enable']:
50 audits.append(PasswdqcPAM('/etc/passwdqc.conf'))
51
52 if settings['auth']['retries']:
53 audits.append(Tally2PAM('/usr/share/pam-configs/tally2'))
54 else:
55 audits.append(DeletedFile('/usr/share/pam-configs/tally2'))
56
57 return audits
58
59
60class PasswdqcPAMContext(object):
61
62 def __call__(self):
63 ctxt = {}
64 settings = utils.get_settings('os')
65
66 ctxt['auth_pam_passwdqc_options'] = \
67 settings['auth']['pam_passwdqc_options']
68
69 return ctxt
70
71
72class PasswdqcPAM(TemplatedFile):
73 """The PAM Audit verifies the linux PAM settings."""
74 def __init__(self, path):
75 super(PasswdqcPAM, self).__init__(path=path,
76 template_dir=TEMPLATES_DIR,
77 context=PasswdqcPAMContext(),
78 user='root',
79 group='root',
80 mode=0o0640)
81
82 def pre_write(self):
83 # Always remove?
84 for pkg in ['libpam-ccreds', 'libpam-cracklib']:
85 log("Purging package '%s'" % pkg, level=DEBUG),
86 apt_purge(pkg)
87
88 apt_update(fatal=True)
89 for pkg in ['libpam-passwdqc']:
90 log("Installing package '%s'" % pkg, level=DEBUG),
91 apt_install(pkg)
92
93 def post_write(self):
94 """Updates the PAM configuration after the file has been written"""
95 try:
96 check_output(['pam-auth-update', '--package'])
97 except CalledProcessError as e:
98 log('Error calling pam-auth-update: %s' % e, level=ERROR)
99
100
101class Tally2PAMContext(object):
102
103 def __call__(self):
104 ctxt = {}
105 settings = utils.get_settings('os')
106
107 ctxt['auth_lockout_time'] = settings['auth']['lockout_time']
108 ctxt['auth_retries'] = settings['auth']['retries']
109
110 return ctxt
111
112
113class Tally2PAM(TemplatedFile):
114 """The PAM Audit verifies the linux PAM settings."""
115 def __init__(self, path):
116 super(Tally2PAM, self).__init__(path=path,
117 template_dir=TEMPLATES_DIR,
118 context=Tally2PAMContext(),
119 user='root',
120 group='root',
121 mode=0o0640)
122
123 def pre_write(self):
124 # Always remove?
125 apt_purge('libpam-ccreds')
126 apt_update(fatal=True)
127 apt_install('libpam-modules')
128
129 def post_write(self):
130 """Updates the PAM configuration after the file has been written"""
131 try:
132 check_output(['pam-auth-update', '--package'])
133 except CalledProcessError as e:
134 log('Error calling pam-auth-update: %s' % e, level=ERROR)
0135
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/profile.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/profile.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/profile.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,45 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19from charmhelpers.contrib.hardening import utils
20
21
22def get_audits():
23 """Get OS hardening profile audits.
24
25 :returns: dictionary of audits
26 """
27 audits = []
28
29 settings = utils.get_settings('os')
30
31 # If core dumps are not enabled, then don't allow core dumps to be
32 # created as they may contain sensitive information.
33 if not settings['security']['kernel_enable_core_dump']:
34 audits.append(TemplatedFile('/etc/profile.d/pinerolo_profile.sh',
35 ProfileContext(),
36 template_dir=TEMPLATES_DIR,
37 mode=0o0755, user='root', group='root'))
38 return audits
39
40
41class ProfileContext(object):
42
43 def __call__(self):
44 ctxt = {}
45 return ctxt
046
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/securetty.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,39 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.contrib.hardening.audits.file import TemplatedFile
18from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
19from charmhelpers.contrib.hardening import utils
20
21
22def get_audits():
23 """Get OS hardening Secure TTY audits.
24
25 :returns: dictionary of audits
26 """
27 audits = []
28 audits.append(TemplatedFile('/etc/securetty', SecureTTYContext(),
29 template_dir=TEMPLATES_DIR,
30 mode=0o0400, user='root', group='root'))
31 return audits
32
33
34class SecureTTYContext(object):
35
36 def __call__(self):
37 settings = utils.get_settings('os')
38 ctxt = {'ttys': settings['auth']['root_ttys']}
39 return ctxt
040
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,131 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import subprocess
18
19from charmhelpers.core.hookenv import (
20 log,
21 INFO,
22)
23from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit
24from charmhelpers.contrib.hardening import utils
25
26
27BLACKLIST = ['/usr/bin/rcp', '/usr/bin/rlogin', '/usr/bin/rsh',
28 '/usr/libexec/openssh/ssh-keysign',
29 '/usr/lib/openssh/ssh-keysign',
30 '/sbin/netreport',
31 '/usr/sbin/usernetctl',
32 '/usr/sbin/userisdnctl',
33 '/usr/sbin/pppd',
34 '/usr/bin/lockfile',
35 '/usr/bin/mail-lock',
36 '/usr/bin/mail-unlock',
37 '/usr/bin/mail-touchlock',
38 '/usr/bin/dotlockfile',
39 '/usr/bin/arping',
40 '/usr/sbin/uuidd',
41 '/usr/bin/mtr',
42 '/usr/lib/evolution/camel-lock-helper-1.2',
43 '/usr/lib/pt_chown',
44 '/usr/lib/eject/dmcrypt-get-device',
45 '/usr/lib/mc/cons.saver']
46
47WHITELIST = ['/bin/mount', '/bin/ping', '/bin/su', '/bin/umount',
48 '/sbin/pam_timestamp_check', '/sbin/unix_chkpwd', '/usr/bin/at',
49 '/usr/bin/gpasswd', '/usr/bin/locate', '/usr/bin/newgrp',
50 '/usr/bin/passwd', '/usr/bin/ssh-agent',
51 '/usr/libexec/utempter/utempter', '/usr/sbin/lockdev',
52 '/usr/sbin/sendmail.sendmail', '/usr/bin/expiry',
53 '/bin/ping6', '/usr/bin/traceroute6.iputils',
54 '/sbin/mount.nfs', '/sbin/umount.nfs',
55 '/sbin/mount.nfs4', '/sbin/umount.nfs4',
56 '/usr/bin/crontab',
57 '/usr/bin/wall', '/usr/bin/write',
58 '/usr/bin/screen',
59 '/usr/bin/mlocate',
60 '/usr/bin/chage', '/usr/bin/chfn', '/usr/bin/chsh',
61 '/bin/fusermount',
62 '/usr/bin/pkexec',
63 '/usr/bin/sudo', '/usr/bin/sudoedit',
64 '/usr/sbin/postdrop', '/usr/sbin/postqueue',
65 '/usr/sbin/suexec',
66 '/usr/lib/squid/ncsa_auth', '/usr/lib/squid/pam_auth',
67 '/usr/kerberos/bin/ksu',
68 '/usr/sbin/ccreds_validate',
69 '/usr/bin/Xorg',
70 '/usr/bin/X',
71 '/usr/lib/dbus-1.0/dbus-daemon-launch-helper',
72 '/usr/lib/vte/gnome-pty-helper',
73 '/usr/lib/libvte9/gnome-pty-helper',
74 '/usr/lib/libvte-2.90-9/gnome-pty-helper']
75
76
77def get_audits():
78 """Get OS hardening suid/sgid audits.
79
80 :returns: dictionary of audits
81 """
82 checks = []
83 settings = utils.get_settings('os')
84 if not settings['security']['suid_sgid_enforce']:
85 log("Skipping suid/sgid hardening", level=INFO)
86 return checks
87
88 # Build the blacklist and whitelist of files for suid/sgid checks.
89 # There are a total of 4 lists:
90 # 1. the system blacklist
91 # 2. the system whitelist
92 # 3. the user blacklist
93 # 4. the user whitelist
94 #
95 # The blacklist is the set of paths which should NOT have the suid/sgid bit
96 # set and the whitelist is the set of paths which MAY have the suid/sgid
97 # bit setl. The user whitelist/blacklist effectively override the system
98 # whitelist/blacklist.
99 u_b = settings['security']['suid_sgid_blacklist']
100 u_w = settings['security']['suid_sgid_whitelist']
101
102 blacklist = set(BLACKLIST) - set(u_w + u_b)
103 whitelist = set(WHITELIST) - set(u_b + u_w)
104
105 checks.append(NoSUIDSGIDAudit(blacklist))
106
107 dry_run = settings['security']['suid_sgid_dry_run_on_unknown']
108
109 if settings['security']['suid_sgid_remove_from_unknown'] or dry_run:
110 # If the policy is a dry_run (e.g. complain only) or remove unknown
111 # suid/sgid bits then find all of the paths which have the suid/sgid
112 # bit set and then remove the whitelisted paths.
113 root_path = settings['environment']['root_path']
114 unknown_paths = find_paths_with_suid_sgid(root_path) - set(whitelist)
115 checks.append(NoSUIDSGIDAudit(unknown_paths, unless=dry_run))
116
117 return checks
118
119
120def find_paths_with_suid_sgid(root_path):
121 """Finds all paths/files which have an suid/sgid bit enabled.
122
123 Starting with the root_path, this will recursively find all paths which
124 have an suid or sgid bit set.
125 """
126 cmd = ['find', root_path, '-perm', '-4000', '-o', '-perm', '-2000',
127 '-type', 'f', '!', '-path', '/proc/*', '-print']
128
129 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
130 out, _ = p.communicate()
131 return set(out.split('\n'))
0132
=== added file 'hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,211 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import platform
19import re
20import six
21import subprocess
22
23from charmhelpers.core.hookenv import (
24 log,
25 INFO,
26 WARNING,
27)
28from charmhelpers.contrib.hardening import utils
29from charmhelpers.contrib.hardening.audits.file import (
30 FilePermissionAudit,
31 TemplatedFile,
32)
33from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
34
35
36SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s
37net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s
38net.ipv4.conf.all.rp_filter=1
39net.ipv4.conf.default.rp_filter=1
40net.ipv4.icmp_echo_ignore_broadcasts=1
41net.ipv4.icmp_ignore_bogus_error_responses=1
42net.ipv4.icmp_ratelimit=100
43net.ipv4.icmp_ratemask=88089
44net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s
45net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s
46net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s
47net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s
48net.ipv4.tcp_rfc1337=1
49net.ipv4.tcp_syncookies=1
50net.ipv4.conf.all.shared_media=1
51net.ipv4.conf.default.shared_media=1
52net.ipv4.conf.all.accept_source_route=0
53net.ipv4.conf.default.accept_source_route=0
54net.ipv4.conf.all.accept_redirects=0
55net.ipv4.conf.default.accept_redirects=0
56net.ipv6.conf.all.accept_redirects=0
57net.ipv6.conf.default.accept_redirects=0
58net.ipv4.conf.all.secure_redirects=0
59net.ipv4.conf.default.secure_redirects=0
60net.ipv4.conf.all.send_redirects=0
61net.ipv4.conf.default.send_redirects=0
62net.ipv4.conf.all.log_martians=0
63net.ipv6.conf.default.router_solicitations=0
64net.ipv6.conf.default.accept_ra_rtr_pref=0
65net.ipv6.conf.default.accept_ra_pinfo=0
66net.ipv6.conf.default.accept_ra_defrtr=0
67net.ipv6.conf.default.autoconf=0
68net.ipv6.conf.default.dad_transmits=0
69net.ipv6.conf.default.max_addresses=1
70net.ipv6.conf.all.accept_ra=0
71net.ipv6.conf.default.accept_ra=0
72kernel.modules_disabled=%(kernel_modules_disabled)s
73kernel.sysrq=%(kernel_sysrq)s
74fs.suid_dumpable=%(fs_suid_dumpable)s
75kernel.randomize_va_space=2
76"""
77
78
79def get_audits():
80 """Get OS hardening sysctl audits.
81
82 :returns: dictionary of audits
83 """
84 audits = []
85 settings = utils.get_settings('os')
86
87 # Apply the sysctl settings which are configured to be applied.
88 audits.append(SysctlConf())
89 # Make sure that only root has access to the sysctl.conf file, and
90 # that it is read-only.
91 audits.append(FilePermissionAudit('/etc/sysctl.conf',
92 user='root',
93 group='root', mode=0o0440))
94 # If module loading is not enabled, then ensure that the modules
95 # file has the appropriate permissions and rebuild the initramfs
96 if not settings['security']['kernel_enable_module_loading']:
97 audits.append(ModulesTemplate())
98
99 return audits
100
101
102class ModulesContext(object):
103
104 def __call__(self):
105 settings = utils.get_settings('os')
106 with open('/proc/cpuinfo', 'r') as fd:
107 cpuinfo = fd.readlines()
108
109 for line in cpuinfo:
110 match = re.search(r"^vendor_id\s+:\s+(.+)", line)
111 if match:
112 vendor = match.group(1)
113
114 if vendor == "GenuineIntel":
115 vendor = "intel"
116 elif vendor == "AuthenticAMD":
117 vendor = "amd"
118
119 ctxt = {'arch': platform.processor(),
120 'cpuVendor': vendor,
121 'desktop_enable': settings['general']['desktop_enable']}
122
123 return ctxt
124
125
126class ModulesTemplate(object):
127
128 def __init__(self):
129 super(ModulesTemplate, self).__init__('/etc/initramfs-tools/modules',
130 ModulesContext(),
131 templates_dir=TEMPLATES_DIR,
132 user='root', group='root',
133 mode=0o0440)
134
135 def post_write(self):
136 subprocess.check_call(['update-initramfs', '-u'])
137
138
139class SysCtlHardeningContext(object):
140 def __call__(self):
141 settings = utils.get_settings('os')
142 ctxt = {'sysctl': {}}
143
144 log("Applying sysctl settings", level=INFO)
145 extras = {'net_ipv4_ip_forward': 0,
146 'net_ipv6_conf_all_forwarding': 0,
147 'net_ipv6_conf_all_disable_ipv6': 1,
148 'net_ipv4_tcp_timestamps': 0,
149 'net_ipv4_conf_all_arp_ignore': 0,
150 'net_ipv4_conf_all_arp_announce': 0,
151 'kernel_sysrq': 0,
152 'fs_suid_dumpable': 0,
153 'kernel_modules_disabled': 1}
154
155 if settings['sysctl']['ipv6_enable']:
156 extras['net_ipv6_conf_all_disable_ipv6'] = 0
157
158 if settings['sysctl']['forwarding']:
159 extras['net_ipv4_ip_forward'] = 1
160 extras['net_ipv6_conf_all_forwarding'] = 1
161
162 if settings['sysctl']['arp_restricted']:
163 extras['net_ipv4_conf_all_arp_ignore'] = 1
164 extras['net_ipv4_conf_all_arp_announce'] = 2
165
166 if settings['security']['kernel_enable_module_loading']:
167 extras['kernel_modules_disabled'] = 0
168
169 if settings['sysctl']['kernel_enable_sysrq']:
170 sysrq_val = settings['sysctl']['kernel_secure_sysrq']
171 extras['kernel_sysrq'] = sysrq_val
172
173 if settings['security']['kernel_enable_core_dump']:
174 extras['fs_suid_dumpable'] = 1
175
176 settings.update(extras)
177 for d in (SYSCTL_DEFAULTS % settings).split():
178 d = d.strip().partition('=')
179 key = d[0].strip()
180 path = os.path.join('/proc/sys', key.replace('.', '/'))
181 if not os.path.exists(path):
182 log("Skipping '%s' since '%s' does not exist" % (key, path),
183 level=WARNING)
184 continue
185
186 ctxt['sysctl'][key] = d[2] or None
187
188 # Translate for python3
189 return {'sysctl_settings':
190 [(k, v) for k, v in six.iteritems(ctxt['sysctl'])]}
191
192
193class SysctlConf(TemplatedFile):
194 """An audit check for sysctl settings."""
195 def __init__(self):
196 self.conffile = '/etc/sysctl.d/99-juju-hardening.conf'
197 super(SysctlConf, self).__init__(self.conffile,
198 SysCtlHardeningContext(),
199 template_dir=TEMPLATES_DIR,
200 user='root', group='root',
201 mode=0o0440)
202
203 def post_write(self):
204 try:
205 subprocess.check_call(['sysctl', '-p', self.conffile])
206 except subprocess.CalledProcessError as e:
207 # NOTE: on some systems if sysctl cannot apply all settings it
208 # will return non-zero as well.
209 log("sysctl command returned an error (maybe some "
210 "keys could not be set) - %s" % (e),
211 level=WARNING)
0212
=== added directory 'hooks/charmhelpers/contrib/hardening/mysql'
=== added file 'hooks/charmhelpers/contrib/hardening/mysql/__init__.py'
--- hooks/charmhelpers/contrib/hardening/mysql/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
020
=== added directory 'hooks/charmhelpers/contrib/hardening/mysql/checks'
=== added file 'hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.mysql.checks import config
22
23
24def run_mysql_checks():
25 log("Starting MySQL hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("MySQL hardening checks complete.", level=DEBUG)
032
=== added file 'hooks/charmhelpers/contrib/hardening/mysql/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,89 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18import subprocess
19
20from charmhelpers.core.hookenv import (
21 log,
22 WARNING,
23)
24from charmhelpers.contrib.hardening.audits.file import (
25 FilePermissionAudit,
26 DirectoryPermissionAudit,
27 TemplatedFile,
28)
29from charmhelpers.contrib.hardening.mysql import TEMPLATES_DIR
30from charmhelpers.contrib.hardening import utils
31
32
33def get_audits():
34 """Get MySQL hardening config audits.
35
36 :returns: dictionary of audits
37 """
38 if subprocess.call(['which', 'mysql'], stdout=subprocess.PIPE) != 0:
39 log("MySQL does not appear to be installed on this node - "
40 "skipping mysql hardening", level=WARNING)
41 return []
42
43 settings = utils.get_settings('mysql')
44 hardening_settings = settings['hardening']
45 my_cnf = hardening_settings['mysql-conf']
46
47 audits = [
48 FilePermissionAudit(paths=[my_cnf], user='root',
49 group='root', mode=0o0600),
50
51 TemplatedFile(hardening_settings['hardening-conf'],
52 MySQLConfContext(),
53 TEMPLATES_DIR,
54 mode=0o0750,
55 user='mysql',
56 group='root',
57 service_actions=[{'service': 'mysql',
58 'actions': ['restart']}]),
59
60 # MySQL and Percona charms do not allow configuration of the
61 # data directory, so use the default.
62 DirectoryPermissionAudit('/var/lib/mysql',
63 user='mysql',
64 group='mysql',
65 recursive=False,
66 mode=0o755),
67
68 DirectoryPermissionAudit('/etc/mysql',
69 user='root',
70 group='root',
71 recursive=False,
72 mode=0o700),
73 ]
74
75 return audits
76
77
78class MySQLConfContext(object):
79 """Defines the set of key/value pairs to set in a mysql config file.
80
81 This context, when called, will return a dictionary containing the
82 key/value pairs of setting to specify in the
83 /etc/mysql/conf.d/hardening.cnf file.
84 """
85 def __call__(self):
86 settings = utils.get_settings('mysql')
87 # Translate for python3
88 return {'mysql_settings':
89 [(k, v) for k, v in six.iteritems(settings['security'])]}
090
=== added directory 'hooks/charmhelpers/contrib/hardening/ssh'
=== added file 'hooks/charmhelpers/contrib/hardening/ssh/__init__.py'
--- hooks/charmhelpers/contrib/hardening/ssh/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,19 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from os import path
18
19TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')
020
=== added directory 'hooks/charmhelpers/contrib/hardening/ssh/checks'
=== added file 'hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,31 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 log,
19 DEBUG,
20)
21from charmhelpers.contrib.hardening.ssh.checks import config
22
23
24def run_ssh_checks():
25 log("Starting SSH hardening checks.", level=DEBUG)
26 checks = config.get_audits()
27 for check in checks:
28 log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29 check.ensure_compliance()
30
31 log("SSH hardening checks complete.", level=DEBUG)
032
=== added file 'hooks/charmhelpers/contrib/hardening/ssh/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,394 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18
19from charmhelpers.core.hookenv import (
20 log,
21 DEBUG,
22)
23from charmhelpers.fetch import (
24 apt_install,
25 apt_update,
26)
27from charmhelpers.core.host import lsb_release
28from charmhelpers.contrib.hardening.audits.file import (
29 TemplatedFile,
30 FileContentAudit,
31)
32from charmhelpers.contrib.hardening.ssh import TEMPLATES_DIR
33from charmhelpers.contrib.hardening import utils
34
35
36def get_audits():
37 """Get SSH hardening config audits.
38
39 :returns: dictionary of audits
40 """
41 audits = [SSHConfig(), SSHDConfig(), SSHConfigFileContentAudit(),
42 SSHDConfigFileContentAudit()]
43 return audits
44
45
46class SSHConfigContext(object):
47
48 type = 'client'
49
50 def get_macs(self, allow_weak_mac):
51 if allow_weak_mac:
52 weak_macs = 'weak'
53 else:
54 weak_macs = 'default'
55
56 default = 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160'
57 macs = {'default': default,
58 'weak': default + ',hmac-sha1'}
59
60 default = ('hmac-sha2-512-etm@openssh.com,'
61 'hmac-sha2-256-etm@openssh.com,'
62 'hmac-ripemd160-etm@openssh.com,umac-128-etm@openssh.com,'
63 'hmac-sha2-512,hmac-sha2-256,hmac-ripemd160')
64 macs_66 = {'default': default,
65 'weak': default + ',hmac-sha1'}
66
67 # Use newer ciphers on Ubuntu Trusty and above
68 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
69 log("Detected Ubuntu 14.04 or newer, using new macs", level=DEBUG)
70 macs = macs_66
71
72 return macs[weak_macs]
73
74 def get_kexs(self, allow_weak_kex):
75 if allow_weak_kex:
76 weak_kex = 'weak'
77 else:
78 weak_kex = 'default'
79
80 default = 'diffie-hellman-group-exchange-sha256'
81 weak = (default + ',diffie-hellman-group14-sha1,'
82 'diffie-hellman-group-exchange-sha1,'
83 'diffie-hellman-group1-sha1')
84 kex = {'default': default,
85 'weak': weak}
86
87 default = ('curve25519-sha256@libssh.org,'
88 'diffie-hellman-group-exchange-sha256')
89 weak = (default + ',diffie-hellman-group14-sha1,'
90 'diffie-hellman-group-exchange-sha1,'
91 'diffie-hellman-group1-sha1')
92 kex_66 = {'default': default,
93 'weak': weak}
94
95 # Use newer kex on Ubuntu Trusty and above
96 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
97 log('Detected Ubuntu 14.04 or newer, using new key exchange '
98 'algorithms', level=DEBUG)
99 kex = kex_66
100
101 return kex[weak_kex]
102
103 def get_ciphers(self, cbc_required):
104 if cbc_required:
105 weak_ciphers = 'weak'
106 else:
107 weak_ciphers = 'default'
108
109 default = 'aes256-ctr,aes192-ctr,aes128-ctr'
110 cipher = {'default': default,
111 'weak': default + 'aes256-cbc,aes192-cbc,aes128-cbc'}
112
113 default = ('chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,'
114 'aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr')
115 ciphers_66 = {'default': default,
116 'weak': default + ',aes256-cbc,aes192-cbc,aes128-cbc'}
117
118 # Use newer ciphers on ubuntu Trusty and above
119 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
120 log('Detected Ubuntu 14.04 or newer, using new ciphers',
121 level=DEBUG)
122 cipher = ciphers_66
123
124 return cipher[weak_ciphers]
125
126 def __call__(self):
127 settings = utils.get_settings('ssh')
128 if settings['common']['network_ipv6_enable']:
129 addr_family = 'any'
130 else:
131 addr_family = 'inet'
132
133 ctxt = {
134 'addr_family': addr_family,
135 'remote_hosts': settings['common']['remote_hosts'],
136 'password_auth_allowed':
137 settings['client']['password_authentication'],
138 'ports': settings['common']['ports'],
139 'ciphers': self.get_ciphers(settings['client']['cbc_required']),
140 'macs': self.get_macs(settings['client']['weak_hmac']),
141 'kexs': self.get_kexs(settings['client']['weak_kex']),
142 'roaming': settings['client']['roaming'],
143 }
144 return ctxt
145
146
147class SSHConfig(TemplatedFile):
148 def __init__(self):
149 path = '/etc/ssh/ssh_config'
150 super(SSHConfig, self).__init__(path=path,
151 template_dir=TEMPLATES_DIR,
152 context=SSHConfigContext(),
153 user='root',
154 group='root',
155 mode=0o0644)
156
157 def pre_write(self):
158 settings = utils.get_settings('ssh')
159 apt_update(fatal=True)
160 apt_install(settings['client']['package'])
161 if not os.path.exists('/etc/ssh'):
162 os.makedir('/etc/ssh')
163 # NOTE: don't recurse
164 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
165 maxdepth=0)
166
167 def post_write(self):
168 # NOTE: don't recurse
169 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
170 maxdepth=0)
171
172
173class SSHDConfigContext(SSHConfigContext):
174
175 type = 'server'
176
177 def __call__(self):
178 settings = utils.get_settings('ssh')
179 if settings['common']['network_ipv6_enable']:
180 addr_family = 'any'
181 else:
182 addr_family = 'inet'
183
184 ctxt = {
185 'ssh_ip': settings['server']['listen_to'],
186 'password_auth_allowed':
187 settings['server']['password_authentication'],
188 'ports': settings['common']['ports'],
189 'addr_family': addr_family,
190 'ciphers': self.get_ciphers(settings['server']['cbc_required']),
191 'macs': self.get_macs(settings['server']['weak_hmac']),
192 'kexs': self.get_kexs(settings['server']['weak_kex']),
193 'host_key_files': settings['server']['host_key_files'],
194 'allow_root_with_key': settings['server']['allow_root_with_key'],
195 'password_authentication':
196 settings['server']['password_authentication'],
197 'use_priv_sep': settings['server']['use_privilege_separation'],
198 'use_pam': settings['server']['use_pam'],
199 'allow_x11_forwarding': settings['server']['allow_x11_forwarding'],
200 'print_motd': settings['server']['print_motd'],
201 'print_last_log': settings['server']['print_last_log'],
202 'client_alive_interval':
203 settings['server']['alive_interval'],
204 'client_alive_count': settings['server']['alive_count'],
205 'allow_tcp_forwarding': settings['server']['allow_tcp_forwarding'],
206 'allow_agent_forwarding':
207 settings['server']['allow_agent_forwarding'],
208 'deny_users': settings['server']['deny_users'],
209 'allow_users': settings['server']['allow_users'],
210 'deny_groups': settings['server']['deny_groups'],
211 'allow_groups': settings['server']['allow_groups'],
212 'use_dns': settings['server']['use_dns'],
213 'sftp_enable': settings['server']['sftp_enable'],
214 'sftp_group': settings['server']['sftp_group'],
215 'sftp_chroot': settings['server']['sftp_chroot'],
216 'max_auth_tries': settings['server']['max_auth_tries'],
217 'max_sessions': settings['server']['max_sessions'],
218 }
219 return ctxt
220
221
222class SSHDConfig(TemplatedFile):
223 def __init__(self):
224 path = '/etc/ssh/sshd_config'
225 super(SSHDConfig, self).__init__(path=path,
226 template_dir=TEMPLATES_DIR,
227 context=SSHDConfigContext(),
228 user='root',
229 group='root',
230 mode=0o0600,
231 service_actions=[{'service': 'ssh',
232 'actions':
233 ['restart']}])
234
235 def pre_write(self):
236 settings = utils.get_settings('ssh')
237 apt_update(fatal=True)
238 apt_install(settings['server']['package'])
239 if not os.path.exists('/etc/ssh'):
240 os.makedir('/etc/ssh')
241 # NOTE: don't recurse
242 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
243 maxdepth=0)
244
245 def post_write(self):
246 # NOTE: don't recurse
247 utils.ensure_permissions('/etc/ssh', 'root', 'root', 0o0755,
248 maxdepth=0)
249
250
251class SSHConfigFileContentAudit(FileContentAudit):
252 def __init__(self):
253 self.path = '/etc/ssh/ssh_config'
254 super(SSHConfigFileContentAudit, self).__init__(self.path, {})
255
256 def is_compliant(self, *args, **kwargs):
257 self.pass_cases = []
258 self.fail_cases = []
259 settings = utils.get_settings('ssh')
260
261 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
262 if not settings['server']['weak_hmac']:
263 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
264 else:
265 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
266
267 if settings['server']['weak_kex']:
268 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
269 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
270 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
271 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
272 else:
273 self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
274 self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
275
276 if settings['server']['cbc_required']:
277 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
278 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
279 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
280 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
281 else:
282 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
283 self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
284 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
285 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
286 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
287 else:
288 if not settings['client']['weak_hmac']:
289 self.fail_cases.append(r'^MACs.+,hmac-sha1$')
290 else:
291 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
292
293 if settings['client']['weak_kex']:
294 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
295 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
296 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
297 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
298 else:
299 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
300 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
301 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
302 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
303
304 if settings['client']['cbc_required']:
305 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
306 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
307 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
308 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
309 else:
310 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
311 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
312 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
313 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
314
315 if settings['client']['roaming']:
316 self.pass_cases.append(r'^UseRoaming yes$')
317 else:
318 self.fail_cases.append(r'^UseRoaming yes$')
319
320 return super(SSHConfigFileContentAudit, self).is_compliant(*args,
321 **kwargs)
322
323
324class SSHDConfigFileContentAudit(FileContentAudit):
325 def __init__(self):
326 self.path = '/etc/ssh/sshd_config'
327 super(SSHDConfigFileContentAudit, self).__init__(self.path, {})
328
329 def is_compliant(self, *args, **kwargs):
330 self.pass_cases = []
331 self.fail_cases = []
332 settings = utils.get_settings('ssh')
333
334 if lsb_release()['DISTRIB_CODENAME'].lower() >= 'trusty':
335 if not settings['server']['weak_hmac']:
336 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
337 else:
338 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
339
340 if settings['server']['weak_kex']:
341 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
342 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
343 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
344 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
345 else:
346 self.pass_cases.append(r'^KexAlgorithms.+,diffie-hellman-group-exchange-sha256$') # noqa
347 self.fail_cases.append(r'^KexAlgorithms.*diffie-hellman-group14-sha1[,\s]?') # noqa
348
349 if settings['server']['cbc_required']:
350 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
351 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
352 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
353 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
354 else:
355 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
356 self.pass_cases.append(r'^Ciphers\schacha20-poly1305@openssh.com,.+') # noqa
357 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr$')
358 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
359 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
360 else:
361 if not settings['server']['weak_hmac']:
362 self.pass_cases.append(r'^MACs.+,hmac-ripemd160$')
363 else:
364 self.pass_cases.append(r'^MACs.+,hmac-sha1$')
365
366 if settings['server']['weak_kex']:
367 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256[,\s]?') # noqa
368 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
369 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
370 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
371 else:
372 self.pass_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha256$') # noqa
373 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group14-sha1[,\s]?') # noqa
374 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group-exchange-sha1[,\s]?') # noqa
375 self.fail_cases.append(r'^KexAlgorithms\sdiffie-hellman-group1-sha1[,\s]?') # noqa
376
377 if settings['server']['cbc_required']:
378 self.pass_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
379 self.fail_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
380 self.fail_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
381 self.fail_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
382 else:
383 self.fail_cases.append(r'^Ciphers\s.*-cbc[,\s]?')
384 self.pass_cases.append(r'^Ciphers\s.*aes128-ctr[,\s]?')
385 self.pass_cases.append(r'^Ciphers\s.*aes192-ctr[,\s]?')
386 self.pass_cases.append(r'^Ciphers\s.*aes256-ctr[,\s]?')
387
388 if settings['server']['sftp_enable']:
389 self.pass_cases.append(r'^Subsystem\ssftp')
390 else:
391 self.fail_cases.append(r'^Subsystem\ssftp')
392
393 return super(SSHDConfigFileContentAudit, self).is_compliant(*args,
394 **kwargs)
0395
=== added file 'hooks/charmhelpers/contrib/hardening/templating.py'
--- hooks/charmhelpers/contrib/hardening/templating.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/templating.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,71 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18
19from charmhelpers.core.hookenv import (
20 log,
21 DEBUG,
22 WARNING,
23)
24
25try:
26 from jinja2 import FileSystemLoader, Environment
27except ImportError:
28 from charmhelpers.fetch import apt_install
29 from charmhelpers.fetch import apt_update
30 apt_update(fatal=True)
31 apt_install('python-jinja2', fatal=True)
32 from jinja2 import FileSystemLoader, Environment
33
34
35# NOTE: function separated from main rendering code to facilitate easier
36# mocking in unit tests.
37def write(path, data):
38 with open(path, 'wb') as out:
39 out.write(data)
40
41
42def get_template_path(template_dir, path):
43 """Returns the template file which would be used to render the path.
44
45 The path to the template file is returned.
46 :param template_dir: the directory the templates are located in
47 :param path: the file path to be written to.
48 :returns: path to the template file
49 """
50 return os.path.join(template_dir, os.path.basename(path))
51
52
53def render_and_write(template_dir, path, context):
54 """Renders the specified template into the file.
55
56 :param template_dir: the directory to load the template from
57 :param path: the path to write the templated contents to
58 :param context: the parameters to pass to the rendering engine
59 """
60 env = Environment(loader=FileSystemLoader(template_dir))
61 template_file = os.path.basename(path)
62 template = env.get_template(template_file)
63 log('Rendering from template: %s' % template.name, level=DEBUG)
64 rendered_content = template.render(context)
65 if not rendered_content:
66 log("Render returned None - skipping '%s'" % path,
67 level=WARNING)
68 return
69
70 write(path, rendered_content.encode('utf-8').strip())
71 log('Wrote template %s' % path, level=DEBUG)
072
=== added file 'hooks/charmhelpers/contrib/hardening/utils.py'
--- hooks/charmhelpers/contrib/hardening/utils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hardening/utils.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,157 @@
1# Copyright 2016 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import glob
18import grp
19import os
20import pwd
21import six
22import yaml
23
24from charmhelpers.core.hookenv import (
25 log,
26 DEBUG,
27 INFO,
28 WARNING,
29 ERROR,
30)
31
32
33# Global settings cache. Since each hook fire entails a fresh module import it
34# is safe to hold this in memory and not risk missing config changes (since
35# they will result in a new hook fire and thus re-import).
36__SETTINGS__ = {}
37
38
39def _get_defaults(modules):
40 """Load the default config for the provided modules.
41
42 :param modules: stack modules config defaults to lookup.
43 :returns: modules default config dictionary.
44 """
45 default = os.path.join(os.path.dirname(__file__),
46 'defaults/%s.yaml' % (modules))
47 return yaml.safe_load(open(default))
48
49
50def _get_schema(modules):
51 """Load the config schema for the provided modules.
52
53 NOTE: this schema is intended to have 1-1 relationship with they keys in
54 the default config and is used a means to verify valid overrides provided
55 by the user.
56
57 :param modules: stack modules config schema to lookup.
58 :returns: modules default schema dictionary.
59 """
60 schema = os.path.join(os.path.dirname(__file__),
61 'defaults/%s.yaml.schema' % (modules))
62 return yaml.safe_load(open(schema))
63
64
65def _get_user_provided_overrides(modules):
66 """Load user-provided config overrides.
67
68 :param modules: stack modules to lookup in user overrides yaml file.
69 :returns: overrides dictionary.
70 """
71 overrides = os.path.join(os.environ['JUJU_CHARM_DIR'],
72 'hardening.yaml')
73 if os.path.exists(overrides):
74 log("Found user-provided config overrides file '%s'" %
75 (overrides), level=DEBUG)
76 settings = yaml.safe_load(open(overrides))
77 if settings and settings.get(modules):
78 log("Applying '%s' overrides" % (modules), level=DEBUG)
79 return settings.get(modules)
80
81 log("No overrides found for '%s'" % (modules), level=DEBUG)
82 else:
83 log("No hardening config overrides file '%s' found in charm "
84 "root dir" % (overrides), level=DEBUG)
85
86 return {}
87
88
89def _apply_overrides(settings, overrides, schema):
90 """Get overrides config overlayed onto modules defaults.
91
92 :param modules: require stack modules config.
93 :returns: dictionary of modules config with user overrides applied.
94 """
95 if overrides:
96 for k, v in six.iteritems(overrides):
97 if k in schema:
98 if schema[k] is None:
99 settings[k] = v
100 elif type(schema[k]) is dict:
101 settings[k] = _apply_overrides(settings[k], overrides[k],
102 schema[k])
103 else:
104 raise Exception("Unexpected type found in schema '%s'" %
105 type(schema[k]), level=ERROR)
106 else:
107 log("Unknown override key '%s' - ignoring" % (k), level=INFO)
108
109 return settings
110
111
112def get_settings(modules):
113 global __SETTINGS__
114 if modules in __SETTINGS__:
115 return __SETTINGS__[modules]
116
117 schema = _get_schema(modules)
118 settings = _get_defaults(modules)
119 overrides = _get_user_provided_overrides(modules)
120 __SETTINGS__[modules] = _apply_overrides(settings, overrides, schema)
121 return __SETTINGS__[modules]
122
123
124def ensure_permissions(path, user, group, permissions, maxdepth=-1):
125 """Ensure permissions for path.
126
127 If path is a file, apply to file and return. If path is a directory,
128 apply recursively (if required) to directory contents and return.
129
130 :param user: user name
131 :param group: group name
132 :param permissions: octal permissions
133 :param maxdepth: maximum recursion depth. A negative maxdepth allows
134 infinite recursion and maxdepth=0 means no recursion.
135 :returns: None
136 """
137 if not os.path.exists(path):
138 log("File '%s' does not exist - cannot set permissions" % (path),
139 level=WARNING)
140 return
141
142 _user = pwd.getpwnam(user)
143 os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid)
144 os.chmod(path, permissions)
145
146 if maxdepth == 0:
147 log("Max recursion depth reached - skipping further recursion",
148 level=DEBUG)
149 return
150 elif maxdepth > 0:
151 maxdepth -= 1
152
153 if os.path.isdir(path):
154 contents = glob.glob("%s/*" % (path))
155 for c in contents:
156 ensure_permissions(c, user=user, group=group,
157 permissions=permissions, maxdepth=maxdepth)
0158
=== added directory 'hooks/charmhelpers/contrib/mellanox'
=== added file 'hooks/charmhelpers/contrib/mellanox/__init__.py'
=== added file 'hooks/charmhelpers/contrib/mellanox/infiniband.py'
--- hooks/charmhelpers/contrib/mellanox/infiniband.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/mellanox/infiniband.py 2016-04-24 06:22:47 +0000
@@ -0,0 +1,151 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20
21__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
22
23from charmhelpers.fetch import (
24 apt_install,
25 apt_update,
26)
27
28from charmhelpers.core.hookenv import (
29 log,
30 INFO,
31)
32
33try:
34 from netifaces import interfaces as network_interfaces
35except ImportError:
36 apt_install('python-netifaces')
37 from netifaces import interfaces as network_interfaces
38
39import os
40import re
41import subprocess
42
43from charmhelpers.core.kernel import modprobe
44
45REQUIRED_MODULES = (
46 "mlx4_ib",
47 "mlx4_en",
48 "mlx4_core",
49 "ib_ipath",
50 "ib_mthca",
51 "ib_srpt",
52 "ib_srp",
53 "ib_ucm",
54 "ib_isert",
55 "ib_iser",
56 "ib_ipoib",
57 "ib_cm",
58 "ib_uverbs"
59 "ib_umad",
60 "ib_sa",
61 "ib_mad",
62 "ib_core",
63 "ib_addr",
64 "rdma_ucm",
65)
66
67REQUIRED_PACKAGES = (
68 "ibutils",
69 "infiniband-diags",
70 "ibverbs-utils",
71)
72
73IPOIB_DRIVERS = (
74 "ib_ipoib",
75)
76
77ABI_VERSION_FILE = "/sys/class/infiniband_mad/abi_version"
78
79
80class DeviceInfo(object):
81 pass
82
83
84def install_packages():
85 apt_update()
86 apt_install(REQUIRED_PACKAGES, fatal=True)
87
88
89def load_modules():
90 for module in REQUIRED_MODULES:
91 modprobe(module, persist=True)
92
93
94def is_enabled():
95 """Check if infiniband is loaded on the system"""
96 return os.path.exists(ABI_VERSION_FILE)
97
98
99def stat():
100 """Return full output of ibstat"""
101 return subprocess.check_output(["ibstat"])
102
103
104def devices():
105 """Returns a list of IB enabled devices"""
106 return subprocess.check_output(['ibstat', '-l']).splitlines()
107
108
109def device_info(device):
110 """Returns a DeviceInfo object with the current device settings"""
111
112 status = subprocess.check_output([
113 'ibstat', device, '-s']).splitlines()
114
115 regexes = {
116 "CA type: (.*)": "device_type",
117 "Number of ports: (.*)": "num_ports",
118 "Firmware version: (.*)": "fw_ver",
119 "Hardware version: (.*)": "hw_ver",
120 "Node GUID: (.*)": "node_guid",
121 "System image GUID: (.*)": "sys_guid",
122 }
123
124 device = DeviceInfo()
125
126 for line in status:
127 for expression, key in regexes.items():
128 matches = re.search(expression, line)
129 if matches:
130 setattr(device, key, matches.group(1))
131
132 return device
133
134
135def ipoib_interfaces():
136 """Return a list of IPOIB capable ethernet interfaces"""
137 interfaces = []
138
139 for interface in network_interfaces():
140 try:
141 driver = re.search('^driver: (.+)$', subprocess.check_output([
142 'ethtool', '-i',
143 interface]), re.M).group(1)
144
145 if driver in IPOIB_DRIVERS:
146 interfaces.append(interface)
147 except:
148 log("Skipping interface %s" % interface, level=INFO)
149 continue
150
151 return interfaces
0152
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2015-05-19 21:31:00 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2016-04-24 06:22:47 +0000
@@ -23,7 +23,7 @@
23from functools import partial23from functools import partial
2424
25from charmhelpers.core.hookenv import unit_get25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
28 log,28 log,
29 WARNING,29 WARNING,
@@ -32,13 +32,15 @@
32try:32try:
33 import netifaces33 import netifaces
34except ImportError:34except ImportError:
35 apt_install('python-netifaces')35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
36 import netifaces37 import netifaces
3738
38try:39try:
39 import netaddr40 import netaddr
40except ImportError:41except ImportError:
41 apt_install('python-netaddr')42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
42 import netaddr44 import netaddr
4345
4446
@@ -51,7 +53,7 @@
5153
5254
53def no_ip_found_error_out(network):55def no_ip_found_error_out(network):
54 errmsg = ("No IP address found in network: %s" % network)56 errmsg = ("No IP address found in network(s): %s" % network)
55 raise ValueError(errmsg)57 raise ValueError(errmsg)
5658
5759
@@ -59,7 +61,7 @@
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.
6062
61 :param network (str): CIDR presentation format. For example,63 :param network (str): CIDR presentation format. For example,
62 '192.168.1.0/24'.64 '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
63 :param fallback (str): If no address is found, return fallback.65 :param fallback (str): If no address is found, return fallback.
64 :param fatal (boolean): If no address is found, fallback is not66 :param fatal (boolean): If no address is found, fallback is not
65 set and fatal is True then exit(1).67 set and fatal is True then exit(1).
@@ -73,24 +75,26 @@
73 else:75 else:
74 return None76 return None
7577
76 _validate_cidr(network)78 networks = network.split() or [network]
77 network = netaddr.IPNetwork(network)79 for network in networks:
78 for iface in netifaces.interfaces():80 _validate_cidr(network)
79 addresses = netifaces.ifaddresses(iface)81 network = netaddr.IPNetwork(network)
80 if network.version == 4 and netifaces.AF_INET in addresses:82 for iface in netifaces.interfaces():
81 addr = addresses[netifaces.AF_INET][0]['addr']83 addresses = netifaces.ifaddresses(iface)
82 netmask = addresses[netifaces.AF_INET][0]['netmask']84 if network.version == 4 and netifaces.AF_INET in addresses:
83 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))85 addr = addresses[netifaces.AF_INET][0]['addr']
84 if cidr in network:86 netmask = addresses[netifaces.AF_INET][0]['netmask']
85 return str(cidr.ip)87 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
88 if cidr in network:
89 return str(cidr.ip)
8690
87 if network.version == 6 and netifaces.AF_INET6 in addresses:91 if network.version == 6 and netifaces.AF_INET6 in addresses:
88 for addr in addresses[netifaces.AF_INET6]:92 for addr in addresses[netifaces.AF_INET6]:
89 if not addr['addr'].startswith('fe80'):93 if not addr['addr'].startswith('fe80'):
90 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],94 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
91 addr['netmask']))95 addr['netmask']))
92 if cidr in network:96 if cidr in network:
93 return str(cidr.ip)97 return str(cidr.ip)
9498
95 if fallback is not None:99 if fallback is not None:
96 return fallback100 return fallback
@@ -187,6 +191,15 @@
187get_netmask_for_address = partial(_get_for_address, key='netmask')191get_netmask_for_address = partial(_get_for_address, key='netmask')
188192
189193
194def resolve_network_cidr(ip_address):
195 '''
196 Resolves the full address cidr of an ip_address based on
197 configured network interfaces
198 '''
199 netmask = get_netmask_for_address(ip_address)
200 return str(netaddr.IPNetwork("%s/%s" % (ip_address, netmask)).cidr)
201
202
190def format_ipv6_addr(address):203def format_ipv6_addr(address):
191 """If address is IPv6, wrap it in '[]' otherwise return None.204 """If address is IPv6, wrap it in '[]' otherwise return None.
192205
@@ -435,8 +448,12 @@
435448
436 rev = dns.reversename.from_address(address)449 rev = dns.reversename.from_address(address)
437 result = ns_query(rev)450 result = ns_query(rev)
451
438 if not result:452 if not result:
439 return None453 try:
454 result = socket.gethostbyaddr(address)[0]
455 except:
456 return None
440 else:457 else:
441 result = address458 result = address
442459
@@ -448,3 +465,18 @@
448 return result465 return result
449 else:466 else:
450 return result.split('.')[0]467 return result.split('.')[0]
468
469
470def port_has_listener(address, port):
471 """
472 Returns True if the address:port is open and being listened to,
473 else False.
474
475 @param address: an IP address or hostname
476 @param port: integer port
477
478 Note calls 'zc' via a subprocess shell
479 """
480 cmd = ['nc', '-z', address, str(port)]
481 result = subprocess.call(cmd)
482 return not(bool(result))
451483
=== modified file 'hooks/charmhelpers/contrib/network/ovs/__init__.py'
--- hooks/charmhelpers/contrib/network/ovs/__init__.py 2015-05-19 21:31:00 +0000
+++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2016-04-24 06:22:47 +0000
@@ -25,10 +25,14 @@
25)25)
2626
2727
28def add_bridge(name):28def add_bridge(name, datapath_type=None):
29 ''' Add the named bridge to openvswitch '''29 ''' Add the named bridge to openvswitch '''
30 log('Creating bridge {}'.format(name))30 log('Creating bridge {}'.format(name))
31 subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name])31 cmd = ["ovs-vsctl", "--", "--may-exist", "add-br", name]
32 if datapath_type is not None:
33 cmd += ['--', 'set', 'bridge', name,
34 'datapath_type={}'.format(datapath_type)]
35 subprocess.check_call(cmd)
3236
3337
34def del_bridge(name):38def del_bridge(name):
3539
=== modified file 'hooks/charmhelpers/contrib/network/ufw.py'
--- hooks/charmhelpers/contrib/network/ufw.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/network/ufw.py 2016-04-24 06:22:47 +0000
@@ -40,7 +40,9 @@
40import re40import re
41import os41import os
42import subprocess42import subprocess
43
43from charmhelpers.core import hookenv44from charmhelpers.core import hookenv
45from charmhelpers.core.kernel import modprobe, is_module_loaded
4446
45__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"47__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
4648
@@ -82,14 +84,11 @@
82 # do we have IPv6 in the machine?84 # do we have IPv6 in the machine?
83 if os.path.isdir('/proc/sys/net/ipv6'):85 if os.path.isdir('/proc/sys/net/ipv6'):
84 # is ip6tables kernel module loaded?86 # is ip6tables kernel module loaded?
85 lsmod = subprocess.check_output(['lsmod'], universal_newlines=True)87 if not is_module_loaded('ip6_tables'):
86 matches = re.findall('^ip6_tables[ ]+', lsmod, re.M)
87 if len(matches) == 0:
88 # ip6tables support isn't complete, let's try to load it88 # ip6tables support isn't complete, let's try to load it
89 try:89 try:
90 subprocess.check_output(['modprobe', 'ip6_tables'],90 modprobe('ip6_tables')
91 universal_newlines=True)91 # great, we can load the module
92 # great, we could load the module
93 return True92 return True
94 except subprocess.CalledProcessError as ex:93 except subprocess.CalledProcessError as ex:
95 hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,94 hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
9695
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2016-04-24 06:22:47 +0000
@@ -14,12 +14,18 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
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/>.
1616
17import logging
18import re
19import sys
17import six20import six
18from collections import OrderedDict21from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (22from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment23 AmuletDeployment
21)24)
2225
26DEBUG = logging.DEBUG
27ERROR = logging.ERROR
28
2329
24class OpenStackAmuletDeployment(AmuletDeployment):30class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.31 """OpenStack amulet deployment.
@@ -28,9 +34,12 @@
28 that is specifically for use by OpenStack charms.34 that is specifically for use by OpenStack charms.
29 """35 """
3036
31 def __init__(self, series=None, openstack=None, source=None, stable=True):37 def __init__(self, series=None, openstack=None, source=None,
38 stable=True, log_level=DEBUG):
32 """Initialize the deployment environment."""39 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)40 super(OpenStackAmuletDeployment, self).__init__(series)
41 self.log = self.get_logger(level=log_level)
42 self.log.info('OpenStackAmuletDeployment: init')
34 self.openstack = openstack43 self.openstack = openstack
35 self.source = source44 self.source = source
36 self.stable = stable45 self.stable = stable
@@ -38,26 +47,55 @@
38 # out.47 # out.
39 self.current_next = "trusty"48 self.current_next = "trusty"
4049
50 def get_logger(self, name="deployment-logger", level=logging.DEBUG):
51 """Get a logger object that will log to stdout."""
52 log = logging
53 logger = log.getLogger(name)
54 fmt = log.Formatter("%(asctime)s %(funcName)s "
55 "%(levelname)s: %(message)s")
56
57 handler = log.StreamHandler(stream=sys.stdout)
58 handler.setLevel(level)
59 handler.setFormatter(fmt)
60
61 logger.addHandler(handler)
62 logger.setLevel(level)
63
64 return logger
65
41 def _determine_branch_locations(self, other_services):66 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.67 """Determine the branch locations for the other services.
4368
44 Determine if the local branch being tested is derived from its69 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding70 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""71 stable or next branches for the other_services."""
47 base_charms = ['mysql', 'mongodb']72
73 self.log.info('OpenStackAmuletDeployment: determine branch locations')
74
75 # Charms outside the lp:~openstack-charmers namespace
76 base_charms = ['mysql', 'mongodb', 'nrpe']
77
78 # Force these charms to current series even when using an older series.
79 # ie. Use trusty/nrpe even when series is precise, as the P charm
80 # does not possess the necessary external master config and hooks.
81 force_series_current = ['nrpe']
4882
49 if self.series in ['precise', 'trusty']:83 if self.series in ['precise', 'trusty']:
50 base_series = self.series84 base_series = self.series
51 else:85 else:
52 base_series = self.current_next86 base_series = self.current_next
5387
54 if self.stable:88 for svc in other_services:
55 for svc in other_services:89 if svc['name'] in force_series_current:
90 base_series = self.current_next
91 # If a location has been explicitly set, use it
92 if svc.get('location'):
93 continue
94 if self.stable:
56 temp = 'lp:charms/{}/{}'95 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,96 svc['location'] = temp.format(base_series,
58 svc['name'])97 svc['name'])
59 else:98 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:99 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'100 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,101 svc['location'] = temp.format(base_series,
@@ -66,10 +104,13 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'104 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,105 svc['location'] = temp.format(self.current_next,
68 svc['name'])106 svc['name'])
107
69 return other_services108 return other_services
70109
71 def _add_services(self, this_service, other_services):110 def _add_services(self, this_service, other_services):
72 """Add services to the deployment and set openstack-origin/source."""111 """Add services to the deployment and set openstack-origin/source."""
112 self.log.info('OpenStackAmuletDeployment: adding services')
113
73 other_services = self._determine_branch_locations(other_services)114 other_services = self._determine_branch_locations(other_services)
74115
75 super(OpenStackAmuletDeployment, self)._add_services(this_service,116 super(OpenStackAmuletDeployment, self)._add_services(this_service,
@@ -77,29 +118,105 @@
77118
78 services = other_services119 services = other_services
79 services.append(this_service)120 services.append(this_service)
121
122 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',123 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']124 'ceph-osd', 'ceph-radosgw', 'ceph-mon']
82 # Most OpenStack subordinate charms do not expose an origin option125
83 # as that is controlled by the principle.126 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch']127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
128 'openvswitch-odl', 'neutron-api-odl', 'odl-controller',
129 'cinder-backup', 'nexentaedge-data',
130 'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw',
131 'cinder-nexentaedge', 'nexentaedge-mgmt']
85132
86 if self.openstack:133 if self.openstack:
87 for svc in services:134 for svc in services:
88 if svc['name'] not in use_source + ignore:135 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}136 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)137 self.d.configure(svc['name'], config)
91138
92 if self.source:139 if self.source:
93 for svc in services:140 for svc in services:
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:
95 config = {'source': self.source}142 config = {'source': self.source}
96 self.d.configure(svc['name'], config)143 self.d.configure(svc['name'], config)
97144
98 def _configure_services(self, configs):145 def _configure_services(self, configs):
99 """Configure all of the services."""146 """Configure all of the services."""
147 self.log.info('OpenStackAmuletDeployment: configure services')
100 for service, config in six.iteritems(configs):148 for service, config in six.iteritems(configs):
101 self.d.configure(service, config)149 self.d.configure(service, config)
102150
151 def _auto_wait_for_status(self, message=None, exclude_services=None,
152 include_only=None, timeout=1800):
153 """Wait for all units to have a specific extended status, except
154 for any defined as excluded. Unless specified via message, any
155 status containing any case of 'ready' will be considered a match.
156
157 Examples of message usage:
158
159 Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
160 message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
161
162 Wait for all units to reach this status (exact match):
163 message = re.compile('^Unit is ready and clustered$')
164
165 Wait for all units to reach any one of these (exact match):
166 message = re.compile('Unit is ready|OK|Ready')
167
168 Wait for at least one unit to reach this status (exact match):
169 message = {'ready'}
170
171 See Amulet's sentry.wait_for_messages() for message usage detail.
172 https://github.com/juju/amulet/blob/master/amulet/sentry.py
173
174 :param message: Expected status match
175 :param exclude_services: List of juju service names to ignore,
176 not to be used in conjuction with include_only.
177 :param include_only: List of juju service names to exclusively check,
178 not to be used in conjuction with exclude_services.
179 :param timeout: Maximum time in seconds to wait for status match
180 :returns: None. Raises if timeout is hit.
181 """
182 self.log.info('Waiting for extended status on units...')
183
184 all_services = self.d.services.keys()
185
186 if exclude_services and include_only:
187 raise ValueError('exclude_services can not be used '
188 'with include_only')
189
190 if message:
191 if isinstance(message, re._pattern_type):
192 match = message.pattern
193 else:
194 match = message
195
196 self.log.debug('Custom extended status wait match: '
197 '{}'.format(match))
198 else:
199 self.log.debug('Default extended status wait match: contains '
200 'READY (case-insensitive)')
201 message = re.compile('.*ready.*', re.IGNORECASE)
202
203 if exclude_services:
204 self.log.debug('Excluding services from extended status match: '
205 '{}'.format(exclude_services))
206 else:
207 exclude_services = []
208
209 if include_only:
210 services = include_only
211 else:
212 services = list(set(all_services) - set(exclude_services))
213
214 self.log.debug('Waiting up to {}s for extended status on services: '
215 '{}'.format(timeout, services))
216 service_messages = {service: message for service in services}
217 self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
218 self.log.info('OK')
219
103 def _get_openstack_release(self):220 def _get_openstack_release(self):
104 """Get openstack release.221 """Get openstack release.
105222
@@ -111,7 +228,8 @@
111 self.precise_havana, self.precise_icehouse,228 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,229 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,230 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)231 self.wily_liberty, self.trusty_mitaka,
232 self.xenial_mitaka) = range(14)
115233
116 releases = {234 releases = {
117 ('precise', None): self.precise_essex,235 ('precise', None): self.precise_essex,
@@ -123,9 +241,11 @@
123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,241 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,242 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,243 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
244 ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
126 ('utopic', None): self.utopic_juno,245 ('utopic', None): self.utopic_juno,
127 ('vivid', None): self.vivid_kilo,246 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}247 ('wily', None): self.wily_liberty,
248 ('xenial', None): self.xenial_mitaka}
129 return releases[(self.series, self.openstack)]249 return releases[(self.series, self.openstack)]
130250
131 def _get_openstack_release_string(self):251 def _get_openstack_release_string(self):
@@ -142,6 +262,7 @@
142 ('utopic', 'juno'),262 ('utopic', 'juno'),
143 ('vivid', 'kilo'),263 ('vivid', 'kilo'),
144 ('wily', 'liberty'),264 ('wily', 'liberty'),
265 ('xenial', 'mitaka'),
145 ])266 ])
146 if self.openstack:267 if self.openstack:
147 os_origin = self.openstack.split(':')[1]268 os_origin = self.openstack.split(':')[1]
148269
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2016-04-24 06:22:47 +0000
@@ -18,6 +18,7 @@
18import json18import json
19import logging19import logging
20import os20import os
21import re
21import six22import six
22import time23import time
23import urllib24import urllib
@@ -26,7 +27,12 @@
26import glanceclient.v1.client as glance_client27import glanceclient.v1.client as glance_client
27import heatclient.v1.client as heat_client28import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client29import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client30from keystoneclient.auth.identity import v3 as keystone_id_v3
31from keystoneclient import session as keystone_session
32from keystoneclient.v3 import client as keystone_client_v3
33
34import novaclient.client as nova_client
35import pika
30import swiftclient36import swiftclient
3137
32from charmhelpers.contrib.amulet.utils import (38from charmhelpers.contrib.amulet.utils import (
@@ -36,6 +42,8 @@
36DEBUG = logging.DEBUG42DEBUG = logging.DEBUG
37ERROR = logging.ERROR43ERROR = logging.ERROR
3844
45NOVA_CLIENT_VERSION = "2"
46
3947
40class OpenStackAmuletUtils(AmuletUtils):48class OpenStackAmuletUtils(AmuletUtils):
41 """OpenStack amulet utilities.49 """OpenStack amulet utilities.
@@ -137,7 +145,7 @@
137 return "role {} does not exist".format(e['name'])145 return "role {} does not exist".format(e['name'])
138 return ret146 return ret
139147
140 def validate_user_data(self, expected, actual):148 def validate_user_data(self, expected, actual, api_version=None):
141 """Validate user data.149 """Validate user data.
142150
143 Validate a list of actual user data vs a list of expected user151 Validate a list of actual user data vs a list of expected user
@@ -148,10 +156,15 @@
148 for e in expected:156 for e in expected:
149 found = False157 found = False
150 for act in actual:158 for act in actual:
151 a = {'enabled': act.enabled, 'name': act.name,159 if e['name'] == act.name:
152 'email': act.email, 'tenantId': act.tenantId,160 a = {'enabled': act.enabled, 'name': act.name,
153 'id': act.id}161 'email': act.email, 'id': act.id}
154 if e['name'] == a['name']:162 if api_version == 3:
163 a['default_project_id'] = getattr(act,
164 'default_project_id',
165 'none')
166 else:
167 a['tenantId'] = act.tenantId
155 found = True168 found = True
156 ret = self._validate_dict_data(e, a)169 ret = self._validate_dict_data(e, a)
157 if ret:170 if ret:
@@ -186,15 +199,30 @@
186 return cinder_client.Client(username, password, tenant, ept)199 return cinder_client.Client(username, password, tenant, ept)
187200
188 def authenticate_keystone_admin(self, keystone_sentry, user, password,201 def authenticate_keystone_admin(self, keystone_sentry, user, password,
189 tenant):202 tenant=None, api_version=None,
203 keystone_ip=None):
190 """Authenticates admin user with the keystone admin endpoint."""204 """Authenticates admin user with the keystone admin endpoint."""
191 self.log.debug('Authenticating keystone admin...')205 self.log.debug('Authenticating keystone admin...')
192 unit = keystone_sentry206 unit = keystone_sentry
193 service_ip = unit.relation('shared-db',207 if not keystone_ip:
194 'mysql:shared-db')['private-address']208 keystone_ip = unit.relation('shared-db',
195 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))209 'mysql:shared-db')['private-address']
196 return keystone_client.Client(username=user, password=password,210 base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8'))
197 tenant_name=tenant, auth_url=ep)211 if not api_version or api_version == 2:
212 ep = base_ep + "/v2.0"
213 return keystone_client.Client(username=user, password=password,
214 tenant_name=tenant, auth_url=ep)
215 else:
216 ep = base_ep + "/v3"
217 auth = keystone_id_v3.Password(
218 user_domain_name='admin_domain',
219 username=user,
220 password=password,
221 domain_name='admin_domain',
222 auth_url=ep,
223 )
224 sess = keystone_session.Session(auth=auth)
225 return keystone_client_v3.Client(session=sess)
198226
199 def authenticate_keystone_user(self, keystone, user, password, tenant):227 def authenticate_keystone_user(self, keystone, user, password, tenant):
200 """Authenticates a regular user with the keystone public endpoint."""228 """Authenticates a regular user with the keystone public endpoint."""
@@ -223,7 +251,8 @@
223 self.log.debug('Authenticating nova user ({})...'.format(user))251 self.log.debug('Authenticating nova user ({})...'.format(user))
224 ep = keystone.service_catalog.url_for(service_type='identity',252 ep = keystone.service_catalog.url_for(service_type='identity',
225 endpoint_type='publicURL')253 endpoint_type='publicURL')
226 return nova_client.Client(username=user, api_key=password,254 return nova_client.Client(NOVA_CLIENT_VERSION,
255 username=user, api_key=password,
227 project_id=tenant, auth_url=ep)256 project_id=tenant, auth_url=ep)
228257
229 def authenticate_swift_user(self, keystone, user, password, tenant):258 def authenticate_swift_user(self, keystone, user, password, tenant):
@@ -602,3 +631,382 @@
602 self.log.debug('Ceph {} samples (OK): '631 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))632 '{}'.format(sample_type, samples))
604 return None633 return None
634
635 # rabbitmq/amqp specific helpers:
636
637 def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
638 """Wait for rmq units extended status to show cluster readiness,
639 after an optional initial sleep period. Initial sleep is likely
640 necessary to be effective following a config change, as status
641 message may not instantly update to non-ready."""
642
643 if init_sleep:
644 time.sleep(init_sleep)
645
646 message = re.compile('^Unit is ready and clustered$')
647 deployment._auto_wait_for_status(message=message,
648 timeout=timeout,
649 include_only=['rabbitmq-server'])
650
651 def add_rmq_test_user(self, sentry_units,
652 username="testuser1", password="changeme"):
653 """Add a test user via the first rmq juju unit, check connection as
654 the new user against all sentry units.
655
656 :param sentry_units: list of sentry unit pointers
657 :param username: amqp user name, default to testuser1
658 :param password: amqp user password
659 :returns: None if successful. Raise on error.
660 """
661 self.log.debug('Adding rmq user ({})...'.format(username))
662
663 # Check that user does not already exist
664 cmd_user_list = 'rabbitmqctl list_users'
665 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
666 if username in output:
667 self.log.warning('User ({}) already exists, returning '
668 'gracefully.'.format(username))
669 return
670
671 perms = '".*" ".*" ".*"'
672 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
673 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
674
675 # Add user via first unit
676 for cmd in cmds:
677 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
678
679 # Check connection against the other sentry_units
680 self.log.debug('Checking user connect against units...')
681 for sentry_unit in sentry_units:
682 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
683 username=username,
684 password=password)
685 connection.close()
686
687 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
688 """Delete a rabbitmq user via the first rmq juju unit.
689
690 :param sentry_units: list of sentry unit pointers
691 :param username: amqp user name, default to testuser1
692 :param password: amqp user password
693 :returns: None if successful or no such user.
694 """
695 self.log.debug('Deleting rmq user ({})...'.format(username))
696
697 # Check that the user exists
698 cmd_user_list = 'rabbitmqctl list_users'
699 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
700
701 if username not in output:
702 self.log.warning('User ({}) does not exist, returning '
703 'gracefully.'.format(username))
704 return
705
706 # Delete the user
707 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
708 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
709
710 def get_rmq_cluster_status(self, sentry_unit):
711 """Execute rabbitmq cluster status command on a unit and return
712 the full output.
713
714 :param unit: sentry unit
715 :returns: String containing console output of cluster status command
716 """
717 cmd = 'rabbitmqctl cluster_status'
718 output, _ = self.run_cmd_unit(sentry_unit, cmd)
719 self.log.debug('{} cluster_status:\n{}'.format(
720 sentry_unit.info['unit_name'], output))
721 return str(output)
722
723 def get_rmq_cluster_running_nodes(self, sentry_unit):
724 """Parse rabbitmqctl cluster_status output string, return list of
725 running rabbitmq cluster nodes.
726
727 :param unit: sentry unit
728 :returns: List containing node names of running nodes
729 """
730 # NOTE(beisner): rabbitmqctl cluster_status output is not
731 # json-parsable, do string chop foo, then json.loads that.
732 str_stat = self.get_rmq_cluster_status(sentry_unit)
733 if 'running_nodes' in str_stat:
734 pos_start = str_stat.find("{running_nodes,") + 15
735 pos_end = str_stat.find("]},", pos_start) + 1
736 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
737 run_nodes = json.loads(str_run_nodes)
738 return run_nodes
739 else:
740 return []
741
742 def validate_rmq_cluster_running_nodes(self, sentry_units):
743 """Check that all rmq unit hostnames are represented in the
744 cluster_status output of all units.
745
746 :param host_names: dict of juju unit names to host names
747 :param units: list of sentry unit pointers (all rmq units)
748 :returns: None if successful, otherwise return error message
749 """
750 host_names = self.get_unit_hostnames(sentry_units)
751 errors = []
752
753 # Query every unit for cluster_status running nodes
754 for query_unit in sentry_units:
755 query_unit_name = query_unit.info['unit_name']
756 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
757
758 # Confirm that every unit is represented in the queried unit's
759 # cluster_status running nodes output.
760 for validate_unit in sentry_units:
761 val_host_name = host_names[validate_unit.info['unit_name']]
762 val_node_name = 'rabbit@{}'.format(val_host_name)
763
764 if val_node_name not in running_nodes:
765 errors.append('Cluster member check failed on {}: {} not '
766 'in {}\n'.format(query_unit_name,
767 val_node_name,
768 running_nodes))
769 if errors:
770 return ''.join(errors)
771
772 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
773 """Check a single juju rmq unit for ssl and port in the config file."""
774 host = sentry_unit.info['public-address']
775 unit_name = sentry_unit.info['unit_name']
776
777 conf_file = '/etc/rabbitmq/rabbitmq.config'
778 conf_contents = str(self.file_contents_safe(sentry_unit,
779 conf_file, max_wait=16))
780 # Checks
781 conf_ssl = 'ssl' in conf_contents
782 conf_port = str(port) in conf_contents
783
784 # Port explicitly checked in config
785 if port and conf_port and conf_ssl:
786 self.log.debug('SSL is enabled @{}:{} '
787 '({})'.format(host, port, unit_name))
788 return True
789 elif port and not conf_port and conf_ssl:
790 self.log.debug('SSL is enabled @{} but not on port {} '
791 '({})'.format(host, port, unit_name))
792 return False
793 # Port not checked (useful when checking that ssl is disabled)
794 elif not port and conf_ssl:
795 self.log.debug('SSL is enabled @{}:{} '
796 '({})'.format(host, port, unit_name))
797 return True
798 elif not conf_ssl:
799 self.log.debug('SSL not enabled @{}:{} '
800 '({})'.format(host, port, unit_name))
801 return False
802 else:
803 msg = ('Unknown condition when checking SSL status @{}:{} '
804 '({})'.format(host, port, unit_name))
805 amulet.raise_status(amulet.FAIL, msg)
806
807 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
808 """Check that ssl is enabled on rmq juju sentry units.
809
810 :param sentry_units: list of all rmq sentry units
811 :param port: optional ssl port override to validate
812 :returns: None if successful, otherwise return error message
813 """
814 for sentry_unit in sentry_units:
815 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
816 return ('Unexpected condition: ssl is disabled on unit '
817 '({})'.format(sentry_unit.info['unit_name']))
818 return None
819
820 def validate_rmq_ssl_disabled_units(self, sentry_units):
821 """Check that ssl is enabled on listed rmq juju sentry units.
822
823 :param sentry_units: list of all rmq sentry units
824 :returns: True if successful. Raise on error.
825 """
826 for sentry_unit in sentry_units:
827 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
828 return ('Unexpected condition: ssl is enabled on unit '
829 '({})'.format(sentry_unit.info['unit_name']))
830 return None
831
832 def configure_rmq_ssl_on(self, sentry_units, deployment,
833 port=None, max_wait=60):
834 """Turn ssl charm config option on, with optional non-default
835 ssl port specification. Confirm that it is enabled on every
836 unit.
837
838 :param sentry_units: list of sentry units
839 :param deployment: amulet deployment object pointer
840 :param port: amqp port, use defaults if None
841 :param max_wait: maximum time to wait in seconds to confirm
842 :returns: None if successful. Raise on error.
843 """
844 self.log.debug('Setting ssl charm config option: on')
845
846 # Enable RMQ SSL
847 config = {'ssl': 'on'}
848 if port:
849 config['ssl_port'] = port
850
851 deployment.d.configure('rabbitmq-server', config)
852
853 # Wait for unit status
854 self.rmq_wait_for_cluster(deployment)
855
856 # Confirm
857 tries = 0
858 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
859 while ret and tries < (max_wait / 4):
860 time.sleep(4)
861 self.log.debug('Attempt {}: {}'.format(tries, ret))
862 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
863 tries += 1
864
865 if ret:
866 amulet.raise_status(amulet.FAIL, ret)
867
868 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
869 """Turn ssl charm config option off, confirm that it is disabled
870 on every unit.
871
872 :param sentry_units: list of sentry units
873 :param deployment: amulet deployment object pointer
874 :param max_wait: maximum time to wait in seconds to confirm
875 :returns: None if successful. Raise on error.
876 """
877 self.log.debug('Setting ssl charm config option: off')
878
879 # Disable RMQ SSL
880 config = {'ssl': 'off'}
881 deployment.d.configure('rabbitmq-server', config)
882
883 # Wait for unit status
884 self.rmq_wait_for_cluster(deployment)
885
886 # Confirm
887 tries = 0
888 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
889 while ret and tries < (max_wait / 4):
890 time.sleep(4)
891 self.log.debug('Attempt {}: {}'.format(tries, ret))
892 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
893 tries += 1
894
895 if ret:
896 amulet.raise_status(amulet.FAIL, ret)
897
898 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
899 port=None, fatal=True,
900 username="testuser1", password="changeme"):
901 """Establish and return a pika amqp connection to the rabbitmq service
902 running on a rmq juju unit.
903
904 :param sentry_unit: sentry unit pointer
905 :param ssl: boolean, default to False
906 :param port: amqp port, use defaults if None
907 :param fatal: boolean, default to True (raises on connect error)
908 :param username: amqp user name, default to testuser1
909 :param password: amqp user password
910 :returns: pika amqp connection pointer or None if failed and non-fatal
911 """
912 host = sentry_unit.info['public-address']
913 unit_name = sentry_unit.info['unit_name']
914
915 # Default port logic if port is not specified
916 if ssl and not port:
917 port = 5671
918 elif not ssl and not port:
919 port = 5672
920
921 self.log.debug('Connecting to amqp on {}:{} ({}) as '
922 '{}...'.format(host, port, unit_name, username))
923
924 try:
925 credentials = pika.PlainCredentials(username, password)
926 parameters = pika.ConnectionParameters(host=host, port=port,
927 credentials=credentials,
928 ssl=ssl,
929 connection_attempts=3,
930 retry_delay=5,
931 socket_timeout=1)
932 connection = pika.BlockingConnection(parameters)
933 assert connection.server_properties['product'] == 'RabbitMQ'
934 self.log.debug('Connect OK')
935 return connection
936 except Exception as e:
937 msg = ('amqp connection failed to {}:{} as '
938 '{} ({})'.format(host, port, username, str(e)))
939 if fatal:
940 amulet.raise_status(amulet.FAIL, msg)
941 else:
942 self.log.warn(msg)
943 return None
944
945 def publish_amqp_message_by_unit(self, sentry_unit, message,
946 queue="test", ssl=False,
947 username="testuser1",
948 password="changeme",
949 port=None):
950 """Publish an amqp message to a rmq juju unit.
951
952 :param sentry_unit: sentry unit pointer
953 :param message: amqp message string
954 :param queue: message queue, default to test
955 :param username: amqp user name, default to testuser1
956 :param password: amqp user password
957 :param ssl: boolean, default to False
958 :param port: amqp port, use defaults if None
959 :returns: None. Raises exception if publish failed.
960 """
961 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
962 message))
963 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
964 port=port,
965 username=username,
966 password=password)
967
968 # NOTE(beisner): extra debug here re: pika hang potential:
969 # https://github.com/pika/pika/issues/297
970 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
971 self.log.debug('Defining channel...')
972 channel = connection.channel()
973 self.log.debug('Declaring queue...')
974 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
975 self.log.debug('Publishing message...')
976 channel.basic_publish(exchange='', routing_key=queue, body=message)
977 self.log.debug('Closing channel...')
978 channel.close()
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches