Merge lp:~junaidali/charms/trusty/plumgrid-director/pg-restart into lp:~plumgrid-team/charms/trusty/plumgrid-director/trunk

Proposed by Junaid Ali
Status: Merged
Merged at revision: 32
Proposed branch: lp:~junaidali/charms/trusty/plumgrid-director/pg-restart
Merge into: lp:~plumgrid-team/charms/trusty/plumgrid-director/trunk
Diff against target: 6142 lines (+47/-5762)
52 files modified
charm-helpers-sync.yaml (+6/-1)
config.yaml (+8/-0)
hooks/charmhelpers/contrib/ansible/__init__.py (+0/-254)
hooks/charmhelpers/contrib/benchmark/__init__.py (+0/-126)
hooks/charmhelpers/contrib/charmhelpers/__init__.py (+0/-208)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+0/-15)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+0/-398)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+0/-175)
hooks/charmhelpers/contrib/database/mysql.py (+0/-412)
hooks/charmhelpers/contrib/hardening/__init__.py (+0/-15)
hooks/charmhelpers/contrib/hardening/apache/__init__.py (+0/-19)
hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py (+0/-31)
hooks/charmhelpers/contrib/hardening/apache/checks/config.py (+0/-100)
hooks/charmhelpers/contrib/hardening/audits/__init__.py (+0/-63)
hooks/charmhelpers/contrib/hardening/audits/apache.py (+0/-100)
hooks/charmhelpers/contrib/hardening/audits/apt.py (+0/-105)
hooks/charmhelpers/contrib/hardening/audits/file.py (+0/-552)
hooks/charmhelpers/contrib/hardening/harden.py (+0/-84)
hooks/charmhelpers/contrib/hardening/host/__init__.py (+0/-19)
hooks/charmhelpers/contrib/hardening/host/checks/__init__.py (+0/-50)
hooks/charmhelpers/contrib/hardening/host/checks/apt.py (+0/-39)
hooks/charmhelpers/contrib/hardening/host/checks/limits.py (+0/-55)
hooks/charmhelpers/contrib/hardening/host/checks/login.py (+0/-67)
hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py (+0/-52)
hooks/charmhelpers/contrib/hardening/host/checks/pam.py (+0/-134)
hooks/charmhelpers/contrib/hardening/host/checks/profile.py (+0/-45)
hooks/charmhelpers/contrib/hardening/host/checks/securetty.py (+0/-39)
hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py (+0/-131)
hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py (+0/-211)
hooks/charmhelpers/contrib/hardening/mysql/__init__.py (+0/-19)
hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py (+0/-31)
hooks/charmhelpers/contrib/hardening/mysql/checks/config.py (+0/-89)
hooks/charmhelpers/contrib/hardening/ssh/__init__.py (+0/-19)
hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py (+0/-31)
hooks/charmhelpers/contrib/hardening/ssh/checks/config.py (+0/-394)
hooks/charmhelpers/contrib/hardening/templating.py (+0/-71)
hooks/charmhelpers/contrib/hardening/utils.py (+0/-157)
hooks/charmhelpers/contrib/mellanox/infiniband.py (+0/-151)
hooks/charmhelpers/contrib/peerstorage/__init__.py (+0/-269)
hooks/charmhelpers/contrib/saltstack/__init__.py (+0/-118)
hooks/charmhelpers/contrib/ssl/__init__.py (+0/-94)
hooks/charmhelpers/contrib/ssl/service.py (+0/-279)
hooks/charmhelpers/contrib/templating/__init__.py (+0/-15)
hooks/charmhelpers/contrib/templating/contexts.py (+0/-139)
hooks/charmhelpers/contrib/templating/jinja.py (+0/-40)
hooks/charmhelpers/contrib/templating/pyformat.py (+0/-29)
hooks/charmhelpers/contrib/unison/__init__.py (+0/-313)
hooks/pg_dir_hooks.py (+21/-0)
hooks/pg_dir_utils.py (+3/-2)
metadata.yaml (+2/-0)
templates/kilo/nginx.conf (+5/-1)
unit_tests/test_pg_dir_hooks.py (+2/-1)
To merge this branch: bzr merge lp:~junaidali/charms/trusty/plumgrid-director/pg-restart
Reviewer Review Type Date Requested Status
Bilal Baqar Pending
Review via email: mp+293101@code.launchpad.net
To post a comment you must log in.
33. By Junaid Ali

Updated restart_pg function

34. By Junaid Ali

Updated sleep time after starting libvirt-bin

35. By Junaid Ali

update sleep time in restart_pg, changes for make sync

36. By Junaid Ali

nginx changes

37. By Junaid Ali

director provides a new relations for pg-vip etc

38. By Junaid Ali

added symlink

39. By Junaid Ali

leader check for plumgrid-configs relation

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charm-helpers-sync.yaml'
--- charm-helpers-sync.yaml 2015-07-29 18:07:31 +0000
+++ charm-helpers-sync.yaml 2016-05-04 13:25:41 +0000
@@ -3,5 +3,10 @@
3include:3include:
4 - core4 - core
5 - fetch5 - fetch
6 - contrib6 - contrib.amulet
7 - contrib.hahelpers
8 - contrib.network
9 - contrib.openstack
10 - contrib.python
11 - contrib.storage
7 - payload12 - payload
813
=== modified file 'config.yaml'
--- config.yaml 2016-03-24 12:33:25 +0000
+++ config.yaml 2016-05-04 13:25:41 +0000
@@ -3,6 +3,14 @@
3 default: 192.168.100.2503 default: 192.168.100.250
4 type: string4 type: string
5 description: IP address of the Director's Management interface. Same IP can be used to access PG Console.5 description: IP address of the Director's Management interface. Same IP can be used to access PG Console.
6 plumgrid-username:
7 default: plumgrid
8 type: string
9 description: Username to access PLUMgrid Director
10 plumgrid-password:
11 default: plumgrid
12 type: string
13 description: Password to access PLUMgrid Director
6 lcm-ssh-key:14 lcm-ssh-key:
7 default: 'null'15 default: 'null'
8 type: string16 type: string
917
=== removed directory 'hooks/charmhelpers/contrib/ansible'
=== removed file 'hooks/charmhelpers/contrib/ansible/__init__.py'
--- hooks/charmhelpers/contrib/ansible/__init__.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,254 +0,0 @@
1# Copyright 2014-2015 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# Copyright 2013 Canonical Ltd.
18#
19# Authors:
20# Charm Helpers Developers <juju@lists.ubuntu.com>
21"""Charm Helpers ansible - declare the state of your machines.
22
23This helper enables you to declare your machine state, rather than
24program it procedurally (and have to test each change to your procedures).
25Your install hook can be as simple as::
26
27 {{{
28 import charmhelpers.contrib.ansible
29
30
31 def install():
32 charmhelpers.contrib.ansible.install_ansible_support()
33 charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
34 }}}
35
36and won't need to change (nor will its tests) when you change the machine
37state.
38
39All of your juju config and relation-data are available as template
40variables within your playbooks and templates. An install playbook looks
41something like::
42
43 {{{
44 ---
45 - hosts: localhost
46 user: root
47
48 tasks:
49 - name: Add private repositories.
50 template:
51 src: ../templates/private-repositories.list.jinja2
52 dest: /etc/apt/sources.list.d/private.list
53
54 - name: Update the cache.
55 apt: update_cache=yes
56
57 - name: Install dependencies.
58 apt: pkg={{ item }}
59 with_items:
60 - python-mimeparse
61 - python-webob
62 - sunburnt
63
64 - name: Setup groups.
65 group: name={{ item.name }} gid={{ item.gid }}
66 with_items:
67 - { name: 'deploy_user', gid: 1800 }
68 - { name: 'service_user', gid: 1500 }
69
70 ...
71 }}}
72
73Read more online about `playbooks`_ and standard ansible `modules`_.
74
75.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
76.. _modules: http://www.ansibleworks.com/docs/modules.html
77
78A further feature os the ansible hooks is to provide a light weight "action"
79scripting tool. This is a decorator that you apply to a function, and that
80function can now receive cli args, and can pass extra args to the playbook.
81
82e.g.
83
84
85@hooks.action()
86def some_action(amount, force="False"):
87 "Usage: some-action AMOUNT [force=True]" # <-- shown on error
88 # process the arguments
89 # do some calls
90 # return extra-vars to be passed to ansible-playbook
91 return {
92 'amount': int(amount),
93 'type': force,
94 }
95
96You can now create a symlink to hooks.py that can be invoked like a hook, but
97with cli params:
98
99# link actions/some-action to hooks/hooks.py
100
101actions/some-action amount=10 force=true
102
103"""
104import os
105import stat
106import subprocess
107import functools
108
109import charmhelpers.contrib.templating.contexts
110import charmhelpers.core.host
111import charmhelpers.core.hookenv
112import charmhelpers.fetch
113
114
115charm_dir = os.environ.get('CHARM_DIR', '')
116ansible_hosts_path = '/etc/ansible/hosts'
117# Ansible will automatically include any vars in the following
118# file in its inventory when run locally.
119ansible_vars_path = '/etc/ansible/host_vars/localhost'
120
121
122def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
123 """Installs the ansible package.
124
125 By default it is installed from the `PPA`_ linked from
126 the ansible `website`_ or from a ppa specified by a charm config..
127
128 .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
129 .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
130
131 If from_ppa is empty, you must ensure that the package is available
132 from a configured repository.
133 """
134 if from_ppa:
135 charmhelpers.fetch.add_source(ppa_location)
136 charmhelpers.fetch.apt_update(fatal=True)
137 charmhelpers.fetch.apt_install('ansible')
138 with open(ansible_hosts_path, 'w+') as hosts_file:
139 hosts_file.write('localhost ansible_connection=local')
140
141
142def apply_playbook(playbook, tags=None, extra_vars=None):
143 tags = tags or []
144 tags = ",".join(tags)
145 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
146 ansible_vars_path, namespace_separator='__',
147 allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
148
149 # we want ansible's log output to be unbuffered
150 env = os.environ.copy()
151 env['PYTHONUNBUFFERED'] = "1"
152 call = [
153 'ansible-playbook',
154 '-c',
155 'local',
156 playbook,
157 ]
158 if tags:
159 call.extend(['--tags', '{}'.format(tags)])
160 if extra_vars:
161 extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
162 call.extend(['--extra-vars', " ".join(extra)])
163 subprocess.check_call(call, env=env)
164
165
166class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
167 """Run a playbook with the hook-name as the tag.
168
169 This helper builds on the standard hookenv.Hooks helper,
170 but additionally runs the playbook with the hook-name specified
171 using --tags (ie. running all the tasks tagged with the hook-name).
172
173 Example::
174
175 hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
176
177 # All the tasks within my_machine_state.yaml tagged with 'install'
178 # will be run automatically after do_custom_work()
179 @hooks.hook()
180 def install():
181 do_custom_work()
182
183 # For most of your hooks, you won't need to do anything other
184 # than run the tagged tasks for the hook:
185 @hooks.hook('config-changed', 'start', 'stop')
186 def just_use_playbook():
187 pass
188
189 # As a convenience, you can avoid the above noop function by specifying
190 # the hooks which are handled by ansible-only and they'll be registered
191 # for you:
192 # hooks = AnsibleHooks(
193 # 'playbooks/my_machine_state.yaml',
194 # default_hooks=['config-changed', 'start', 'stop'])
195
196 if __name__ == "__main__":
197 # execute a hook based on the name the program is called by
198 hooks.execute(sys.argv)
199
200 """
201
202 def __init__(self, playbook_path, default_hooks=None):
203 """Register any hooks handled by ansible."""
204 super(AnsibleHooks, self).__init__()
205
206 self._actions = {}
207 self.playbook_path = playbook_path
208
209 default_hooks = default_hooks or []
210
211 def noop(*args, **kwargs):
212 pass
213
214 for hook in default_hooks:
215 self.register(hook, noop)
216
217 def register_action(self, name, function):
218 """Register a hook"""
219 self._actions[name] = function
220
221 def execute(self, args):
222 """Execute the hook followed by the playbook using the hook as tag."""
223 hook_name = os.path.basename(args[0])
224 extra_vars = None
225 if hook_name in self._actions:
226 extra_vars = self._actions[hook_name](args[1:])
227 else:
228 super(AnsibleHooks, self).execute(args)
229
230 charmhelpers.contrib.ansible.apply_playbook(
231 self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
232
233 def action(self, *action_names):
234 """Decorator, registering them as actions"""
235 def action_wrapper(decorated):
236
237 @functools.wraps(decorated)
238 def wrapper(argv):
239 kwargs = dict(arg.split('=') for arg in argv)
240 try:
241 return decorated(**kwargs)
242 except TypeError as e:
243 if decorated.__doc__:
244 e.args += (decorated.__doc__,)
245 raise
246
247 self.register_action(decorated.__name__, wrapper)
248 if '_' in decorated.__name__:
249 self.register_action(
250 decorated.__name__.replace('_', '-'), wrapper)
251
252 return wrapper
253
254 return action_wrapper
2550
=== removed directory 'hooks/charmhelpers/contrib/benchmark'
=== removed file 'hooks/charmhelpers/contrib/benchmark/__init__.py'
--- hooks/charmhelpers/contrib/benchmark/__init__.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/benchmark/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,126 +0,0 @@
1# Copyright 2014-2015 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
18import time
19import os
20from distutils.spawn import find_executable
21
22from charmhelpers.core.hookenv import (
23 in_relation_hook,
24 relation_ids,
25 relation_set,
26 relation_get,
27)
28
29
30def action_set(key, val):
31 if find_executable('action-set'):
32 action_cmd = ['action-set']
33
34 if isinstance(val, dict):
35 for k, v in iter(val.items()):
36 action_set('%s.%s' % (key, k), v)
37 return True
38
39 action_cmd.append('%s=%s' % (key, val))
40 subprocess.check_call(action_cmd)
41 return True
42 return False
43
44
45class Benchmark():
46 """
47 Helper class for the `benchmark` interface.
48
49 :param list actions: Define the actions that are also benchmarks
50
51 From inside the benchmark-relation-changed hook, you would
52 Benchmark(['memory', 'cpu', 'disk', 'smoke', 'custom'])
53
54 Examples:
55
56 siege = Benchmark(['siege'])
57 siege.start()
58 [... run siege ...]
59 # The higher the score, the better the benchmark
60 siege.set_composite_score(16.70, 'trans/sec', 'desc')
61 siege.finish()
62
63
64 """
65
66 BENCHMARK_CONF = '/etc/benchmark.conf' # Replaced in testing
67
68 required_keys = [
69 'hostname',
70 'port',
71 'graphite_port',
72 'graphite_endpoint',
73 'api_port'
74 ]
75
76 def __init__(self, benchmarks=None):
77 if in_relation_hook():
78 if benchmarks is not None:
79 for rid in sorted(relation_ids('benchmark')):
80 relation_set(relation_id=rid, relation_settings={
81 'benchmarks': ",".join(benchmarks)
82 })
83
84 # Check the relation data
85 config = {}
86 for key in self.required_keys:
87 val = relation_get(key)
88 if val is not None:
89 config[key] = val
90 else:
91 # We don't have all of the required keys
92 config = {}
93 break
94
95 if len(config):
96 with open(self.BENCHMARK_CONF, 'w') as f:
97 for key, val in iter(config.items()):
98 f.write("%s=%s\n" % (key, val))
99
100 @staticmethod
101 def start():
102 action_set('meta.start', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
103
104 """
105 If the collectd charm is also installed, tell it to send a snapshot
106 of the current profile data.
107 """
108 COLLECT_PROFILE_DATA = '/usr/local/bin/collect-profile-data'
109 if os.path.exists(COLLECT_PROFILE_DATA):
110 subprocess.check_output([COLLECT_PROFILE_DATA])
111
112 @staticmethod
113 def finish():
114 action_set('meta.stop', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
115
116 @staticmethod
117 def set_composite_score(value, units, direction='asc'):
118 """
119 Set the composite score for a benchmark run. This is a single number
120 representative of the benchmark results. This could be the most
121 important metric, or an amalgamation of metric scores.
122 """
123 return action_set(
124 "meta.composite",
125 {'value': value, 'units': units, 'direction': direction}
126 )
1270
=== removed directory 'hooks/charmhelpers/contrib/charmhelpers'
=== removed file 'hooks/charmhelpers/contrib/charmhelpers/__init__.py'
--- hooks/charmhelpers/contrib/charmhelpers/__init__.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,208 +0,0 @@
1# Copyright 2014-2015 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# Copyright 2012 Canonical Ltd. This software is licensed under the
18# GNU Affero General Public License version 3 (see the file LICENSE).
19
20import warnings
21warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) # noqa
22
23import operator
24import tempfile
25import time
26import yaml
27import subprocess
28
29import six
30if six.PY3:
31 from urllib.request import urlopen
32 from urllib.error import (HTTPError, URLError)
33else:
34 from urllib2 import (urlopen, HTTPError, URLError)
35
36"""Helper functions for writing Juju charms in Python."""
37
38__metaclass__ = type
39__all__ = [
40 # 'get_config', # core.hookenv.config()
41 # 'log', # core.hookenv.log()
42 # 'log_entry', # core.hookenv.log()
43 # 'log_exit', # core.hookenv.log()
44 # 'relation_get', # core.hookenv.relation_get()
45 # 'relation_set', # core.hookenv.relation_set()
46 # 'relation_ids', # core.hookenv.relation_ids()
47 # 'relation_list', # core.hookenv.relation_units()
48 # 'config_get', # core.hookenv.config()
49 # 'unit_get', # core.hookenv.unit_get()
50 # 'open_port', # core.hookenv.open_port()
51 # 'close_port', # core.hookenv.close_port()
52 # 'service_control', # core.host.service()
53 'unit_info', # client-side, NOT IMPLEMENTED
54 'wait_for_machine', # client-side, NOT IMPLEMENTED
55 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
56 'wait_for_relation', # client-side, NOT IMPLEMENTED
57 'wait_for_unit', # client-side, NOT IMPLEMENTED
58]
59
60
61SLEEP_AMOUNT = 0.1
62
63
64# We create a juju_status Command here because it makes testing much,
65# much easier.
66def juju_status():
67 subprocess.check_call(['juju', 'status'])
68
69# re-implemented as charmhelpers.fetch.configure_sources()
70# def configure_source(update=False):
71# source = config_get('source')
72# if ((source.startswith('ppa:') or
73# source.startswith('cloud:') or
74# source.startswith('http:'))):
75# run('add-apt-repository', source)
76# if source.startswith("http:"):
77# run('apt-key', 'import', config_get('key'))
78# if update:
79# run('apt-get', 'update')
80
81
82# DEPRECATED: client-side only
83def make_charm_config_file(charm_config):
84 charm_config_file = tempfile.NamedTemporaryFile(mode='w+')
85 charm_config_file.write(yaml.dump(charm_config))
86 charm_config_file.flush()
87 # The NamedTemporaryFile instance is returned instead of just the name
88 # because we want to take advantage of garbage collection-triggered
89 # deletion of the temp file when it goes out of scope in the caller.
90 return charm_config_file
91
92
93# DEPRECATED: client-side only
94def unit_info(service_name, item_name, data=None, unit=None):
95 if data is None:
96 data = yaml.safe_load(juju_status())
97 service = data['services'].get(service_name)
98 if service is None:
99 # XXX 2012-02-08 gmb:
100 # This allows us to cope with the race condition that we
101 # have between deploying a service and having it come up in
102 # `juju status`. We could probably do with cleaning it up so
103 # that it fails a bit more noisily after a while.
104 return ''
105 units = service['units']
106 if unit is not None:
107 item = units[unit][item_name]
108 else:
109 # It might seem odd to sort the units here, but we do it to
110 # ensure that when no unit is specified, the first unit for the
111 # service (or at least the one with the lowest number) is the
112 # one whose data gets returned.
113 sorted_unit_names = sorted(units.keys())
114 item = units[sorted_unit_names[0]][item_name]
115 return item
116
117
118# DEPRECATED: client-side only
119def get_machine_data():
120 return yaml.safe_load(juju_status())['machines']
121
122
123# DEPRECATED: client-side only
124def wait_for_machine(num_machines=1, timeout=300):
125 """Wait `timeout` seconds for `num_machines` machines to come up.
126
127 This wait_for... function can be called by other wait_for functions
128 whose timeouts might be too short in situations where only a bare
129 Juju setup has been bootstrapped.
130
131 :return: A tuple of (num_machines, time_taken). This is used for
132 testing.
133 """
134 # You may think this is a hack, and you'd be right. The easiest way
135 # to tell what environment we're working in (LXC vs EC2) is to check
136 # the dns-name of the first machine. If it's localhost we're in LXC
137 # and we can just return here.
138 if get_machine_data()[0]['dns-name'] == 'localhost':
139 return 1, 0
140 start_time = time.time()
141 while True:
142 # Drop the first machine, since it's the Zookeeper and that's
143 # not a machine that we need to wait for. This will only work
144 # for EC2 environments, which is why we return early above if
145 # we're in LXC.
146 machine_data = get_machine_data()
147 non_zookeeper_machines = [
148 machine_data[key] for key in list(machine_data.keys())[1:]]
149 if len(non_zookeeper_machines) >= num_machines:
150 all_machines_running = True
151 for machine in non_zookeeper_machines:
152 if machine.get('instance-state') != 'running':
153 all_machines_running = False
154 break
155 if all_machines_running:
156 break
157 if time.time() - start_time >= timeout:
158 raise RuntimeError('timeout waiting for service to start')
159 time.sleep(SLEEP_AMOUNT)
160 return num_machines, time.time() - start_time
161
162
163# DEPRECATED: client-side only
164def wait_for_unit(service_name, timeout=480):
165 """Wait `timeout` seconds for a given service name to come up."""
166 wait_for_machine(num_machines=1)
167 start_time = time.time()
168 while True:
169 state = unit_info(service_name, 'agent-state')
170 if 'error' in state or state == 'started':
171 break
172 if time.time() - start_time >= timeout:
173 raise RuntimeError('timeout waiting for service to start')
174 time.sleep(SLEEP_AMOUNT)
175 if state != 'started':
176 raise RuntimeError('unit did not start, agent-state: ' + state)
177
178
179# DEPRECATED: client-side only
180def wait_for_relation(service_name, relation_name, timeout=120):
181 """Wait `timeout` seconds for a given relation to come up."""
182 start_time = time.time()
183 while True:
184 relation = unit_info(service_name, 'relations').get(relation_name)
185 if relation is not None and relation['state'] == 'up':
186 break
187 if time.time() - start_time >= timeout:
188 raise RuntimeError('timeout waiting for relation to be up')
189 time.sleep(SLEEP_AMOUNT)
190
191
192# DEPRECATED: client-side only
193def wait_for_page_contents(url, contents, timeout=120, validate=None):
194 if validate is None:
195 validate = operator.contains
196 start_time = time.time()
197 while True:
198 try:
199 stream = urlopen(url)
200 except (HTTPError, URLError):
201 pass
202 else:
203 page = stream.read()
204 if validate(page, contents):
205 return page
206 if time.time() - start_time >= timeout:
207 raise RuntimeError('timeout waiting for contents of ' + url)
208 time.sleep(SLEEP_AMOUNT)
2090
=== removed directory 'hooks/charmhelpers/contrib/charmsupport'
=== removed file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
--- hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
1# Copyright 2014-2015 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/>.
160
=== removed file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-01-30 22:40:26 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
@@ -1,398 +0,0 @@
1# Copyright 2014-2015 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"""Compatibility with the nrpe-external-master charm"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
23import subprocess
24import pwd
25import grp
26import os
27import glob
28import shutil
29import re
30import shlex
31import yaml
32
33from charmhelpers.core.hookenv import (
34 config,
35 local_unit,
36 log,
37 relation_ids,
38 relation_set,
39 relations_of_type,
40)
41
42from charmhelpers.core.host import service
43
44# This module adds compatibility with the nrpe-external-master and plain nrpe
45# subordinate charms. To use it in your charm:
46#
47# 1. Update metadata.yaml
48#
49# provides:
50# (...)
51# nrpe-external-master:
52# interface: nrpe-external-master
53# scope: container
54#
55# and/or
56#
57# provides:
58# (...)
59# local-monitors:
60# interface: local-monitors
61# scope: container
62
63#
64# 2. Add the following to config.yaml
65#
66# nagios_context:
67# default: "juju"
68# type: string
69# description: |
70# Used by the nrpe subordinate charms.
71# A string that will be prepended to instance name to set the host name
72# in nagios. So for instance the hostname would be something like:
73# juju-myservice-0
74# If you're running multiple environments with the same services in them
75# this allows you to differentiate between them.
76# nagios_servicegroups:
77# default: ""
78# type: string
79# description: |
80# A comma-separated list of nagios servicegroups.
81# If left empty, the nagios_context will be used as the servicegroup
82#
83# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84#
85# 4. Update your hooks.py with something like this:
86#
87# from charmsupport.nrpe import NRPE
88# (...)
89# def update_nrpe_config():
90# nrpe_compat = NRPE()
91# nrpe_compat.add_check(
92# shortname = "myservice",
93# description = "Check MyService",
94# check_cmd = "check_http -w 2 -c 10 http://localhost"
95# )
96# nrpe_compat.add_check(
97# "myservice_other",
98# "Check for widget failures",
99# check_cmd = "/srv/myapp/scripts/widget_check"
100# )
101# nrpe_compat.write()
102#
103# def config_changed():
104# (...)
105# update_nrpe_config()
106#
107# def nrpe_external_master_relation_changed():
108# update_nrpe_config()
109#
110# def local_monitors_relation_changed():
111# update_nrpe_config()
112#
113# 5. ln -s hooks.py nrpe-external-master-relation-changed
114# ln -s hooks.py local-monitors-relation-changed
115
116
117class CheckException(Exception):
118 pass
119
120
121class Check(object):
122 shortname_re = '[A-Za-z0-9-_]+$'
123 service_template = ("""
124#---------------------------------------------------
125# This file is Juju managed
126#---------------------------------------------------
127define service {{
128 use active-service
129 host_name {nagios_hostname}
130 service_description {nagios_hostname}[{shortname}] """
131 """{description}
132 check_command check_nrpe!{command}
133 servicegroups {nagios_servicegroup}
134}}
135""")
136
137 def __init__(self, shortname, description, check_cmd):
138 super(Check, self).__init__()
139 # XXX: could be better to calculate this from the service name
140 if not re.match(self.shortname_re, shortname):
141 raise CheckException("shortname must match {}".format(
142 Check.shortname_re))
143 self.shortname = shortname
144 self.command = "check_{}".format(shortname)
145 # Note: a set of invalid characters is defined by the
146 # Nagios server config
147 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)
150
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
158 def _locate_cmd(self, check_cmd):
159 search_path = (
160 '/usr/lib/nagios/plugins',
161 '/usr/local/lib/nagios/plugins',
162 )
163 parts = shlex.split(check_cmd)
164 for path in search_path:
165 if os.path.exists(os.path.join(path, parts[0])):
166 command = os.path.join(path, parts[0])
167 if len(parts) > 1:
168 command += " " + " ".join(parts[1:])
169 return command
170 log('Check command not found: {}'.format(parts[0]))
171 return ''
172
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
186 def write(self, nagios_context, hostname, nagios_servicegroups):
187 nrpe_check_file = self._get_check_filename()
188 with open(nrpe_check_file, 'w') as nrpe_check_config:
189 nrpe_check_config.write("# check {}\n".format(self.shortname))
190 nrpe_check_config.write("command[{}]={}\n".format(
191 self.command, self.check_cmd))
192
193 if not os.path.exists(NRPE.nagios_exportdir):
194 log('Not writing service config as {} is not accessible'.format(
195 NRPE.nagios_exportdir))
196 else:
197 self.write_service_config(nagios_context, hostname,
198 nagios_servicegroups)
199
200 def write_service_config(self, nagios_context, hostname,
201 nagios_servicegroups):
202 self._remove_service_files()
203
204 templ_vars = {
205 'nagios_hostname': hostname,
206 'nagios_servicegroup': nagios_servicegroups,
207 'description': self.description,
208 'shortname': self.shortname,
209 'command': self.command,
210 }
211 nrpe_service_text = Check.service_template.format(**templ_vars)
212 nrpe_service_file = self._get_service_filename(hostname)
213 with open(nrpe_service_file, 'w') as nrpe_service_config:
214 nrpe_service_config.write(str(nrpe_service_text))
215
216 def run(self):
217 subprocess.call(self.check_cmd)
218
219
220class NRPE(object):
221 nagios_logdir = '/var/log/nagios'
222 nagios_exportdir = '/var/lib/nagios/export'
223 nrpe_confdir = '/etc/nagios/nrpe.d'
224
225 def __init__(self, hostname=None):
226 super(NRPE, self).__init__()
227 self.config = config()
228 self.nagios_context = self.config['nagios_context']
229 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
230 self.nagios_servicegroups = self.config['nagios_servicegroups']
231 else:
232 self.nagios_servicegroups = self.nagios_context
233 self.unit_name = local_unit().replace('/', '-')
234 if hostname:
235 self.hostname = hostname
236 else:
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)
242 self.checks = []
243
244 def add_check(self, *args, **kwargs):
245 self.checks.append(Check(*args, **kwargs))
246
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
263 def write(self):
264 try:
265 nagios_uid = pwd.getpwnam('nagios').pw_uid
266 nagios_gid = grp.getgrnam('nagios').gr_gid
267 except:
268 log("Nagios user not set up, nrpe checks not updated")
269 return
270
271 if not os.path.exists(NRPE.nagios_logdir):
272 os.mkdir(NRPE.nagios_logdir)
273 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
274
275 nrpe_monitors = {}
276 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
277 for nrpecheck in self.checks:
278 nrpecheck.write(self.nagios_context, self.hostname,
279 self.nagios_servicegroups)
280 nrpe_monitors[nrpecheck.shortname] = {
281 "command": nrpecheck.command,
282 }
283
284 service('restart', 'nagios-nrpe-server')
285
286 monitor_ids = relation_ids("local-monitors") + \
287 relation_ids("nrpe-external-master")
288 for rid in monitor_ids:
289 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
290
291
292def get_nagios_hostcontext(relation_name='nrpe-external-master'):
293 """
294 Query relation with nrpe subordinate, return the nagios_host_context
295
296 :param str relation_name: Name of relation nrpe sub joined to
297 """
298 for rel in relations_of_type(relation_name):
299 if 'nagios_host_context' in rel:
300 return rel['nagios_host_context']
301
302
303def get_nagios_hostname(relation_name='nrpe-external-master'):
304 """
305 Query relation with nrpe subordinate, return the nagios_hostname
306
307 :param str relation_name: Name of relation nrpe sub joined to
308 """
309 for rel in relations_of_type(relation_name):
310 if 'nagios_hostname' in rel:
311 return rel['nagios_hostname']
312
313
314def get_nagios_unit_name(relation_name='nrpe-external-master'):
315 """
316 Return the nagios unit name prepended with host_context if needed
317
318 :param str relation_name: Name of relation nrpe sub joined to
319 """
320 host_context = get_nagios_hostcontext(relation_name)
321 if host_context:
322 unit = "%s:%s" % (host_context, local_unit())
323 else:
324 unit = local_unit()
325 return unit
326
327
328def add_init_service_checks(nrpe, services, unit_name):
329 """
330 Add checks for each service in list
331
332 :param NRPE nrpe: NRPE object to add check to
333 :param list services: List of services to check
334 :param str unit_name: Unit name to use in check description
335 """
336 for svc in services:
337 upstart_init = '/etc/init/%s.conf' % svc
338 sysv_init = '/etc/init.d/%s' % svc
339 if os.path.exists(upstart_init):
340 # Don't add a check for these services from neutron-gateway
341 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
342 nrpe.add_check(
343 shortname=svc,
344 description='process check {%s}' % unit_name,
345 check_cmd='check_upstart_job %s' % svc
346 )
347 elif os.path.exists(sysv_init):
348 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
349 cron_file = ('*/5 * * * * root '
350 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
351 '-s /etc/init.d/%s status > '
352 '/var/lib/nagios/service-check-%s.txt\n' % (svc,
353 svc)
354 )
355 f = open(cronpath, 'w')
356 f.write(cron_file)
357 f.close()
358 nrpe.add_check(
359 shortname=svc,
360 description='process check {%s}' % unit_name,
361 check_cmd='check_status_file.py -f '
362 '/var/lib/nagios/service-check-%s.txt' % svc,
363 )
364
365
366def copy_nrpe_checks():
367 """
368 Copy the nrpe checks into place
369
370 """
371 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
372 nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
373 'charmhelpers', 'contrib', 'openstack',
374 'files')
375
376 if not os.path.exists(NAGIOS_PLUGINS):
377 os.makedirs(NAGIOS_PLUGINS)
378 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
379 if os.path.isfile(fname):
380 shutil.copy2(fname,
381 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
382
383
384def add_haproxy_checks(nrpe, unit_name):
385 """
386 Add checks for each service in list
387
388 :param NRPE nrpe: NRPE object to add check to
389 :param str unit_name: Unit name to use in check description
390 """
391 nrpe.add_check(
392 shortname='haproxy_servers',
393 description='Check HAProxy {%s}' % unit_name,
394 check_cmd='check_haproxy.sh')
395 nrpe.add_check(
396 shortname='haproxy_queue',
397 description='Check HAProxy queue depth {%s}' % unit_name,
398 check_cmd='check_haproxy_queue_depth.sh')
3990
=== removed file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
--- hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
@@ -1,175 +0,0 @@
1# Copyright 2014-2015 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'''
18Functions for managing volumes in juju units. One volume is supported per unit.
19Subordinates may have their own storage, provided it is on its own partition.
20
21Configuration stanzas::
22
23 volume-ephemeral:
24 type: boolean
25 default: true
26 description: >
27 If false, a volume is mounted as sepecified in "volume-map"
28 If true, ephemeral storage will be used, meaning that log data
29 will only exist as long as the machine. YOU HAVE BEEN WARNED.
30 volume-map:
31 type: string
32 default: {}
33 description: >
34 YAML map of units to device names, e.g:
35 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36 Service units will raise a configure-error if volume-ephemeral
37 is 'true' and no volume-map value is set. Use 'juju set' to set a
38 value and 'juju resolved' to complete configuration.
39
40Usage::
41
42 from charmsupport.volumes import configure_volume, VolumeConfigurationError
43 from charmsupport.hookenv import log, ERROR
44 def post_mount_hook():
45 stop_service('myservice')
46 def post_mount_hook():
47 start_service('myservice')
48
49 if __name__ == '__main__':
50 try:
51 configure_volume(before_change=pre_mount_hook,
52 after_change=post_mount_hook)
53 except VolumeConfigurationError:
54 log('Storage could not be configured', ERROR)
55
56'''
57
58# XXX: Known limitations
59# - fstab is neither consulted nor updated
60
61import os
62from charmhelpers.core import hookenv
63from charmhelpers.core import host
64import yaml
65
66
67MOUNT_BASE = '/srv/juju/volumes'
68
69
70class VolumeConfigurationError(Exception):
71 '''Volume configuration data is missing or invalid'''
72 pass
73
74
75def get_config():
76 '''Gather and sanity-check volume configuration data'''
77 volume_config = {}
78 config = hookenv.config()
79
80 errors = False
81
82 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83 volume_config['ephemeral'] = True
84 else:
85 volume_config['ephemeral'] = False
86
87 try:
88 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89 except yaml.YAMLError as e:
90 hookenv.log("Error parsing YAML volume-map: {}".format(e),
91 hookenv.ERROR)
92 errors = True
93 if volume_map is None:
94 # probably an empty string
95 volume_map = {}
96 elif not isinstance(volume_map, dict):
97 hookenv.log("Volume-map should be a dictionary, not {}".format(
98 type(volume_map)))
99 errors = True
100
101 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102 if volume_config['device'] and volume_config['ephemeral']:
103 # asked for ephemeral storage but also defined a volume ID
104 hookenv.log('A volume is defined for this unit, but ephemeral '
105 'storage was requested', hookenv.ERROR)
106 errors = True
107 elif not volume_config['device'] and not volume_config['ephemeral']:
108 # asked for permanent storage but did not define volume ID
109 hookenv.log('Ephemeral storage was requested, but there is no volume '
110 'defined for this unit.', hookenv.ERROR)
111 errors = True
112
113 unit_mount_name = hookenv.local_unit().replace('/', '-')
114 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
116 if errors:
117 return None
118 return volume_config
119
120
121def mount_volume(config):
122 if os.path.exists(config['mountpoint']):
123 if not os.path.isdir(config['mountpoint']):
124 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125 raise VolumeConfigurationError()
126 else:
127 host.mkdir(config['mountpoint'])
128 if os.path.ismount(config['mountpoint']):
129 unmount_volume(config)
130 if not host.mount(config['device'], config['mountpoint'], persist=True):
131 raise VolumeConfigurationError()
132
133
134def unmount_volume(config):
135 if os.path.ismount(config['mountpoint']):
136 if not host.umount(config['mountpoint'], persist=True):
137 raise VolumeConfigurationError()
138
139
140def managed_mounts():
141 '''List of all mounted managed volumes'''
142 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
144
145def configure_volume(before_change=lambda: None, after_change=lambda: None):
146 '''Set up storage (or don't) according to the charm's volume configuration.
147 Returns the mount point or "ephemeral". before_change and after_change
148 are optional functions to be called if the volume configuration changes.
149 '''
150
151 config = get_config()
152 if not config:
153 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154 raise VolumeConfigurationError()
155
156 if config['ephemeral']:
157 if os.path.ismount(config['mountpoint']):
158 before_change()
159 unmount_volume(config)
160 after_change()
161 return 'ephemeral'
162 else:
163 # persistent storage
164 if os.path.ismount(config['mountpoint']):
165 mounts = dict(managed_mounts())
166 if mounts.get(config['mountpoint']) != config['device']:
167 before_change()
168 unmount_volume(config)
169 mount_volume(config)
170 after_change()
171 else:
172 before_change()
173 mount_volume(config)
174 after_change()
175 return config['mountpoint']
1760
=== removed directory 'hooks/charmhelpers/contrib/database'
=== removed file 'hooks/charmhelpers/contrib/database/__init__.py'
=== removed file 'hooks/charmhelpers/contrib/database/mysql.py'
--- hooks/charmhelpers/contrib/database/mysql.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
@@ -1,412 +0,0 @@
1"""Helper for working with a MySQL database"""
2import json
3import re
4import sys
5import platform
6import os
7import glob
8
9# from string import upper
10
11from charmhelpers.core.host import (
12 mkdir,
13 pwgen,
14 write_file
15)
16from charmhelpers.core.hookenv import (
17 config as config_get,
18 relation_get,
19 related_units,
20 unit_get,
21 log,
22 DEBUG,
23 INFO,
24 WARNING,
25)
26from charmhelpers.fetch import (
27 apt_install,
28 apt_update,
29 filter_installed_packages,
30)
31from charmhelpers.contrib.peerstorage import (
32 peer_store,
33 peer_retrieve,
34)
35from charmhelpers.contrib.network.ip import get_host_ip
36
37try:
38 import MySQLdb
39except ImportError:
40 apt_update(fatal=True)
41 apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
42 import MySQLdb
43
44
45class MySQLHelper(object):
46
47 def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
48 migrate_passwd_to_peer_relation=True,
49 delete_ondisk_passwd_file=True):
50 self.host = host
51 # Password file path templates
52 self.root_passwd_file_template = rpasswdf_template
53 self.user_passwd_file_template = upasswdf_template
54
55 self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation
56 # If we migrate we have the option to delete local copy of root passwd
57 self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
58
59 def connect(self, user='root', password=None):
60 log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
61 self.connection = MySQLdb.connect(user=user, host=self.host,
62 passwd=password)
63
64 def database_exists(self, db_name):
65 cursor = self.connection.cursor()
66 try:
67 cursor.execute("SHOW DATABASES")
68 databases = [i[0] for i in cursor.fetchall()]
69 finally:
70 cursor.close()
71
72 return db_name in databases
73
74 def create_database(self, db_name):
75 cursor = self.connection.cursor()
76 try:
77 cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
78 .format(db_name))
79 finally:
80 cursor.close()
81
82 def grant_exists(self, db_name, db_user, remote_ip):
83 cursor = self.connection.cursor()
84 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
85 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
86 try:
87 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
88 remote_ip))
89 grants = [i[0] for i in cursor.fetchall()]
90 except MySQLdb.OperationalError:
91 return False
92 finally:
93 cursor.close()
94
95 # TODO: review for different grants
96 return priv_string in grants
97
98 def create_grant(self, db_name, db_user, remote_ip, password):
99 cursor = self.connection.cursor()
100 try:
101 # TODO: review for different grants
102 cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
103 "IDENTIFIED BY '{}'".format(db_name,
104 db_user,
105 remote_ip,
106 password))
107 finally:
108 cursor.close()
109
110 def create_admin_grant(self, db_user, remote_ip, password):
111 cursor = self.connection.cursor()
112 try:
113 cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
114 "IDENTIFIED BY '{}'".format(db_user,
115 remote_ip,
116 password))
117 finally:
118 cursor.close()
119
120 def cleanup_grant(self, db_user, remote_ip):
121 cursor = self.connection.cursor()
122 try:
123 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
124 "AND HOST='{}'".format(db_user,
125 remote_ip))
126 finally:
127 cursor.close()
128
129 def execute(self, sql):
130 """Execute arbitary SQL against the database."""
131 cursor = self.connection.cursor()
132 try:
133 cursor.execute(sql)
134 finally:
135 cursor.close()
136
137 def migrate_passwords_to_peer_relation(self, excludes=None):
138 """Migrate any passwords storage on disk to cluster peer relation."""
139 dirname = os.path.dirname(self.root_passwd_file_template)
140 path = os.path.join(dirname, '*.passwd')
141 for f in glob.glob(path):
142 if excludes and f in excludes:
143 log("Excluding %s from peer migration" % (f), level=DEBUG)
144 continue
145
146 key = os.path.basename(f)
147 with open(f, 'r') as passwd:
148 _value = passwd.read().strip()
149
150 try:
151 peer_store(key, _value)
152
153 if self.delete_ondisk_passwd_file:
154 os.unlink(f)
155 except ValueError:
156 # NOTE cluster relation not yet ready - skip for now
157 pass
158
159 def get_mysql_password_on_disk(self, username=None, password=None):
160 """Retrieve, generate or store a mysql password for the provided
161 username on disk."""
162 if username:
163 template = self.user_passwd_file_template
164 passwd_file = template.format(username)
165 else:
166 passwd_file = self.root_passwd_file_template
167
168 _password = None
169 if os.path.exists(passwd_file):
170 log("Using existing password file '%s'" % passwd_file, level=DEBUG)
171 with open(passwd_file, 'r') as passwd:
172 _password = passwd.read().strip()
173 else:
174 log("Generating new password file '%s'" % passwd_file, level=DEBUG)
175 if not os.path.isdir(os.path.dirname(passwd_file)):
176 # NOTE: need to ensure this is not mysql root dir (which needs
177 # to be mysql readable)
178 mkdir(os.path.dirname(passwd_file), owner='root', group='root',
179 perms=0o770)
180 # Force permissions - for some reason the chmod in makedirs
181 # fails
182 os.chmod(os.path.dirname(passwd_file), 0o770)
183
184 _password = password or pwgen(length=32)
185 write_file(passwd_file, _password, owner='root', group='root',
186 perms=0o660)
187
188 return _password
189
190 def passwd_keys(self, username):
191 """Generator to return keys used to store passwords in peer store.
192
193 NOTE: we support both legacy and new format to support mysql
194 charm prior to refactor. This is necessary to avoid LP 1451890.
195 """
196 keys = []
197 if username == 'mysql':
198 log("Bad username '%s'" % (username), level=WARNING)
199
200 if username:
201 # IMPORTANT: *newer* format must be returned first
202 keys.append('mysql-%s.passwd' % (username))
203 keys.append('%s.passwd' % (username))
204 else:
205 keys.append('mysql.passwd')
206
207 for key in keys:
208 yield key
209
210 def get_mysql_password(self, username=None, password=None):
211 """Retrieve, generate or store a mysql password for the provided
212 username using peer relation cluster."""
213 excludes = []
214
215 # First check peer relation.
216 try:
217 for key in self.passwd_keys(username):
218 _password = peer_retrieve(key)
219 if _password:
220 break
221
222 # If root password available don't update peer relation from local
223 if _password and not username:
224 excludes.append(self.root_passwd_file_template)
225
226 except ValueError:
227 # cluster relation is not yet started; use on-disk
228 _password = None
229
230 # If none available, generate new one
231 if not _password:
232 _password = self.get_mysql_password_on_disk(username, password)
233
234 # Put on wire if required
235 if self.migrate_passwd_to_peer_relation:
236 self.migrate_passwords_to_peer_relation(excludes=excludes)
237
238 return _password
239
240 def get_mysql_root_password(self, password=None):
241 """Retrieve or generate mysql root password for service units."""
242 return self.get_mysql_password(username=None, password=password)
243
244 def normalize_address(self, hostname):
245 """Ensure that address returned is an IP address (i.e. not fqdn)"""
246 if config_get('prefer-ipv6'):
247 # TODO: add support for ipv6 dns
248 return hostname
249
250 if hostname != unit_get('private-address'):
251 return get_host_ip(hostname, fallback=hostname)
252
253 # Otherwise assume localhost
254 return '127.0.0.1'
255
256 def get_allowed_units(self, database, username, relation_id=None):
257 """Get list of units with access grants for database with username.
258
259 This is typically used to provide shared-db relations with a list of
260 which units have been granted access to the given database.
261 """
262 self.connect(password=self.get_mysql_root_password())
263 allowed_units = set()
264 for unit in related_units(relation_id):
265 settings = relation_get(rid=relation_id, unit=unit)
266 # First check for setting with prefix, then without
267 for attr in ["%s_hostname" % (database), 'hostname']:
268 hosts = settings.get(attr, None)
269 if hosts:
270 break
271
272 if hosts:
273 # hostname can be json-encoded list of hostnames
274 try:
275 hosts = json.loads(hosts)
276 except ValueError:
277 hosts = [hosts]
278 else:
279 hosts = [settings['private-address']]
280
281 if hosts:
282 for host in hosts:
283 host = self.normalize_address(host)
284 if self.grant_exists(database, username, host):
285 log("Grant exists for host '%s' on db '%s'" %
286 (host, database), level=DEBUG)
287 if unit not in allowed_units:
288 allowed_units.add(unit)
289 else:
290 log("Grant does NOT exist for host '%s' on db '%s'" %
291 (host, database), level=DEBUG)
292 else:
293 log("No hosts found for grant check", level=INFO)
294
295 return allowed_units
296
297 def configure_db(self, hostname, database, username, admin=False):
298 """Configure access to database for username from hostname."""
299 self.connect(password=self.get_mysql_root_password())
300 if not self.database_exists(database):
301 self.create_database(database)
302
303 remote_ip = self.normalize_address(hostname)
304 password = self.get_mysql_password(username)
305 if not self.grant_exists(database, username, remote_ip):
306 if not admin:
307 self.create_grant(database, username, remote_ip, password)
308 else:
309 self.create_admin_grant(username, remote_ip, password)
310
311 return password
312
313
314class PerconaClusterHelper(object):
315
316 # Going for the biggest page size to avoid wasted bytes.
317 # InnoDB page size is 16MB
318
319 DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
320 DEFAULT_INNODB_BUFFER_FACTOR = 0.50
321
322 def human_to_bytes(self, human):
323 """Convert human readable configuration options to bytes."""
324 num_re = re.compile('^[0-9]+$')
325 if num_re.match(human):
326 return human
327
328 factors = {
329 'K': 1024,
330 'M': 1048576,
331 'G': 1073741824,
332 'T': 1099511627776
333 }
334 modifier = human[-1]
335 if modifier in factors:
336 return int(human[:-1]) * factors[modifier]
337
338 if modifier == '%':
339 total_ram = self.human_to_bytes(self.get_mem_total())
340 if self.is_32bit_system() and total_ram > self.sys_mem_limit():
341 total_ram = self.sys_mem_limit()
342 factor = int(human[:-1]) * 0.01
343 pctram = total_ram * factor
344 return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
345
346 raise ValueError("Can only convert K,M,G, or T")
347
348 def is_32bit_system(self):
349 """Determine whether system is 32 or 64 bit."""
350 try:
351 return sys.maxsize < 2 ** 32
352 except OverflowError:
353 return False
354
355 def sys_mem_limit(self):
356 """Determine the default memory limit for the current service unit."""
357 if platform.machine() in ['armv7l']:
358 _mem_limit = self.human_to_bytes('2700M') # experimentally determined
359 else:
360 # Limit for x86 based 32bit systems
361 _mem_limit = self.human_to_bytes('4G')
362
363 return _mem_limit
364
365 def get_mem_total(self):
366 """Calculate the total memory in the current service unit."""
367 with open('/proc/meminfo') as meminfo_file:
368 for line in meminfo_file:
369 key, mem = line.split(':', 2)
370 if key == 'MemTotal':
371 mtot, modifier = mem.strip().split(' ')
372 return '%s%s' % (mtot, modifier[0].upper())
373
374 def parse_config(self):
375 """Parse charm configuration and calculate values for config files."""
376 config = config_get()
377 mysql_config = {}
378 if 'max-connections' in config:
379 mysql_config['max_connections'] = config['max-connections']
380
381 if 'wait-timeout' in config:
382 mysql_config['wait_timeout'] = config['wait-timeout']
383
384 if 'innodb-flush-log-at-trx-commit' in config:
385 mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit']
386
387 # Set a sane default key_buffer size
388 mysql_config['key_buffer'] = self.human_to_bytes('32M')
389 total_memory = self.human_to_bytes(self.get_mem_total())
390
391 dataset_bytes = config.get('dataset-size', None)
392 innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
393
394 if innodb_buffer_pool_size:
395 innodb_buffer_pool_size = self.human_to_bytes(
396 innodb_buffer_pool_size)
397 elif dataset_bytes:
398 log("Option 'dataset-size' has been deprecated, please use"
399 "innodb_buffer_pool_size option instead", level="WARN")
400 innodb_buffer_pool_size = self.human_to_bytes(
401 dataset_bytes)
402 else:
403 innodb_buffer_pool_size = int(
404 total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR)
405
406 if innodb_buffer_pool_size > total_memory:
407 log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
408 innodb_buffer_pool_size,
409 total_memory), level='WARN')
410
411 mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
412 return mysql_config
4130
=== removed directory 'hooks/charmhelpers/contrib/hardening'
=== removed file 'hooks/charmhelpers/contrib/hardening/__init__.py'
--- hooks/charmhelpers/contrib/hardening/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
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/>.
160
=== removed directory 'hooks/charmhelpers/contrib/hardening/apache'
=== removed file 'hooks/charmhelpers/contrib/hardening/apache/__init__.py'
--- hooks/charmhelpers/contrib/hardening/apache/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
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')
200
=== removed directory 'hooks/charmhelpers/contrib/hardening/apache/checks'
=== removed file 'hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
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)
320
=== removed file 'hooks/charmhelpers/contrib/hardening/apache/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/apache/checks/config.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/apache/checks/config.py 1970-01-01 00:00:00 +0000
@@ -1,100 +0,0 @@
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
1010
=== removed directory 'hooks/charmhelpers/contrib/hardening/audits'
=== removed file 'hooks/charmhelpers/contrib/hardening/audits/__init__.py'
--- hooks/charmhelpers/contrib/hardening/audits/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,63 +0,0 @@
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
640
=== removed file 'hooks/charmhelpers/contrib/hardening/audits/apache.py'
--- hooks/charmhelpers/contrib/hardening/audits/apache.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/apache.py 1970-01-01 00:00:00 +0000
@@ -1,100 +0,0 @@
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'])
1010
=== removed file 'hooks/charmhelpers/contrib/hardening/audits/apt.py'
--- hooks/charmhelpers/contrib/hardening/audits/apt.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/apt.py 1970-01-01 00:00:00 +0000
@@ -1,105 +0,0 @@
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
1060
=== removed file 'hooks/charmhelpers/contrib/hardening/audits/file.py'
--- hooks/charmhelpers/contrib/hardening/audits/file.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/audits/file.py 1970-01-01 00:00:00 +0000
@@ -1,552 +0,0 @@
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)
5530
=== removed file 'hooks/charmhelpers/contrib/hardening/harden.py'
--- hooks/charmhelpers/contrib/hardening/harden.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/harden.py 1970-01-01 00:00:00 +0000
@@ -1,84 +0,0 @@
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
850
=== removed directory 'hooks/charmhelpers/contrib/hardening/host'
=== removed file 'hooks/charmhelpers/contrib/hardening/host/__init__.py'
--- hooks/charmhelpers/contrib/hardening/host/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
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')
200
=== removed directory 'hooks/charmhelpers/contrib/hardening/host/checks'
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,50 +0,0 @@
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)
510
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/apt.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/apt.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/apt.py 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
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
400
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/limits.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/limits.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/limits.py 1970-01-01 00:00:00 +0000
@@ -1,55 +0,0 @@
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
560
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/login.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/login.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/login.py 1970-01-01 00:00:00 +0000
@@ -1,67 +0,0 @@
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
680
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py 1970-01-01 00:00:00 +0000
@@ -1,52 +0,0 @@
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
530
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/pam.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/pam.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/pam.py 1970-01-01 00:00:00 +0000
@@ -1,134 +0,0 @@
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)
1350
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/profile.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/profile.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/profile.py 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
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
460
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/securetty.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/securetty.py 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
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
400
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py 1970-01-01 00:00:00 +0000
@@ -1,131 +0,0 @@
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'))
1320
=== removed file 'hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py'
--- hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py 1970-01-01 00:00:00 +0000
@@ -1,211 +0,0 @@
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)
2120
=== removed directory 'hooks/charmhelpers/contrib/hardening/mysql'
=== removed file 'hooks/charmhelpers/contrib/hardening/mysql/__init__.py'
--- hooks/charmhelpers/contrib/hardening/mysql/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
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')
200
=== removed directory 'hooks/charmhelpers/contrib/hardening/mysql/checks'
=== removed file 'hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
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)
320
=== removed file 'hooks/charmhelpers/contrib/hardening/mysql/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/mysql/checks/config.py 1970-01-01 00:00:00 +0000
@@ -1,89 +0,0 @@
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'])]}
900
=== removed directory 'hooks/charmhelpers/contrib/hardening/ssh'
=== removed file 'hooks/charmhelpers/contrib/hardening/ssh/__init__.py'
--- hooks/charmhelpers/contrib/hardening/ssh/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
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')
200
=== removed directory 'hooks/charmhelpers/contrib/hardening/ssh/checks'
=== removed file 'hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py'
--- hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
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)
320
=== removed file 'hooks/charmhelpers/contrib/hardening/ssh/checks/config.py'
--- hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/ssh/checks/config.py 1970-01-01 00:00:00 +0000
@@ -1,394 +0,0 @@
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)
3950
=== removed file 'hooks/charmhelpers/contrib/hardening/templating.py'
--- hooks/charmhelpers/contrib/hardening/templating.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/templating.py 1970-01-01 00:00:00 +0000
@@ -1,71 +0,0 @@
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)
720
=== removed file 'hooks/charmhelpers/contrib/hardening/utils.py'
--- hooks/charmhelpers/contrib/hardening/utils.py 2016-04-22 04:53:43 +0000
+++ hooks/charmhelpers/contrib/hardening/utils.py 1970-01-01 00:00:00 +0000
@@ -1,157 +0,0 @@
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)
1580
=== removed directory 'hooks/charmhelpers/contrib/mellanox'
=== removed file 'hooks/charmhelpers/contrib/mellanox/__init__.py'
=== removed file 'hooks/charmhelpers/contrib/mellanox/infiniband.py'
--- hooks/charmhelpers/contrib/mellanox/infiniband.py 2016-01-30 22:41:50 +0000
+++ hooks/charmhelpers/contrib/mellanox/infiniband.py 1970-01-01 00:00:00 +0000
@@ -1,151 +0,0 @@
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
1520
=== removed directory 'hooks/charmhelpers/contrib/peerstorage'
=== removed file 'hooks/charmhelpers/contrib/peerstorage/__init__.py'
--- hooks/charmhelpers/contrib/peerstorage/__init__.py 2016-01-30 22:40:26 +0000
+++ hooks/charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,269 +0,0 @@
1# Copyright 2014-2015 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 json
18import six
19
20from charmhelpers.core.hookenv import relation_id as current_relation_id
21from charmhelpers.core.hookenv import (
22 is_relation_made,
23 relation_ids,
24 relation_get as _relation_get,
25 local_unit,
26 relation_set as _relation_set,
27 leader_get as _leader_get,
28 leader_set,
29 is_leader,
30)
31
32
33"""
34This helper provides functions to support use of a peer relation
35for basic key/value storage, with the added benefit that all storage
36can be replicated across peer units.
37
38Requirement to use:
39
40To use this, the "peer_echo()" method has to be called form the peer
41relation's relation-changed hook:
42
43@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
44def cluster_relation_changed():
45 peer_echo()
46
47Once this is done, you can use peer storage from anywhere:
48
49@hooks.hook("some-hook")
50def some_hook():
51 # You can store and retrieve key/values this way:
52 if is_relation_made("cluster"): # from charmhelpers.core.hookenv
53 # There are peers available so we can work with peer storage
54 peer_store("mykey", "myvalue")
55 value = peer_retrieve("mykey")
56 print value
57 else:
58 print "No peers joind the relation, cannot share key/values :("
59"""
60
61
62def leader_get(attribute=None, rid=None):
63 """Wrapper to ensure that settings are migrated from the peer relation.
64
65 This is to support upgrading an environment that does not support
66 Juju leadership election to one that does.
67
68 If a setting is not extant in the leader-get but is on the relation-get
69 peer rel, it is migrated and marked as such so that it is not re-migrated.
70 """
71 migration_key = '__leader_get_migrated_settings__'
72 if not is_leader():
73 return _leader_get(attribute=attribute)
74
75 settings_migrated = False
76 leader_settings = _leader_get(attribute=attribute)
77 previously_migrated = _leader_get(attribute=migration_key)
78
79 if previously_migrated:
80 migrated = set(json.loads(previously_migrated))
81 else:
82 migrated = set([])
83
84 try:
85 if migration_key in leader_settings:
86 del leader_settings[migration_key]
87 except TypeError:
88 pass
89
90 if attribute:
91 if attribute in migrated:
92 return leader_settings
93
94 # If attribute not present in leader db, check if this unit has set
95 # the attribute in the peer relation
96 if not leader_settings:
97 peer_setting = _relation_get(attribute=attribute, unit=local_unit(),
98 rid=rid)
99 if peer_setting:
100 leader_set(settings={attribute: peer_setting})
101 leader_settings = peer_setting
102
103 if leader_settings:
104 settings_migrated = True
105 migrated.add(attribute)
106 else:
107 r_settings = _relation_get(unit=local_unit(), rid=rid)
108 if r_settings:
109 for key in set(r_settings.keys()).difference(migrated):
110 # Leader setting wins
111 if not leader_settings.get(key):
112 leader_settings[key] = r_settings[key]
113
114 settings_migrated = True
115 migrated.add(key)
116
117 if settings_migrated:
118 leader_set(**leader_settings)
119
120 if migrated and settings_migrated:
121 migrated = json.dumps(list(migrated))
122 leader_set(settings={migration_key: migrated})
123
124 return leader_settings
125
126
127def relation_set(relation_id=None, relation_settings=None, **kwargs):
128 """Attempt to use leader-set if supported in the current version of Juju,
129 otherwise falls back on relation-set.
130
131 Note that we only attempt to use leader-set if the provided relation_id is
132 a peer relation id or no relation id is provided (in which case we assume
133 we are within the peer relation context).
134 """
135 try:
136 if relation_id in relation_ids('cluster'):
137 return leader_set(settings=relation_settings, **kwargs)
138 else:
139 raise NotImplementedError
140 except NotImplementedError:
141 return _relation_set(relation_id=relation_id,
142 relation_settings=relation_settings, **kwargs)
143
144
145def relation_get(attribute=None, unit=None, rid=None):
146 """Attempt to use leader-get if supported in the current version of Juju,
147 otherwise falls back on relation-get.
148
149 Note that we only attempt to use leader-get if the provided rid is a peer
150 relation id or no relation id is provided (in which case we assume we are
151 within the peer relation context).
152 """
153 try:
154 if rid in relation_ids('cluster'):
155 return leader_get(attribute, rid)
156 else:
157 raise NotImplementedError
158 except NotImplementedError:
159 return _relation_get(attribute=attribute, rid=rid, unit=unit)
160
161
162def peer_retrieve(key, relation_name='cluster'):
163 """Retrieve a named key from peer relation `relation_name`."""
164 cluster_rels = relation_ids(relation_name)
165 if len(cluster_rels) > 0:
166 cluster_rid = cluster_rels[0]
167 return relation_get(attribute=key, rid=cluster_rid,
168 unit=local_unit())
169 else:
170 raise ValueError('Unable to detect'
171 'peer relation {}'.format(relation_name))
172
173
174def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
175 inc_list=None, exc_list=None):
176 """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
177 inc_list = inc_list if inc_list else []
178 exc_list = exc_list if exc_list else []
179 peerdb_settings = peer_retrieve('-', relation_name=relation_name)
180 matched = {}
181 if peerdb_settings is None:
182 return matched
183 for k, v in peerdb_settings.items():
184 full_prefix = prefix + delimiter
185 if k.startswith(full_prefix):
186 new_key = k.replace(full_prefix, '')
187 if new_key in exc_list:
188 continue
189 if new_key in inc_list or len(inc_list) == 0:
190 matched[new_key] = v
191 return matched
192
193
194def peer_store(key, value, relation_name='cluster'):
195 """Store the key/value pair on the named peer relation `relation_name`."""
196 cluster_rels = relation_ids(relation_name)
197 if len(cluster_rels) > 0:
198 cluster_rid = cluster_rels[0]
199 relation_set(relation_id=cluster_rid,
200 relation_settings={key: value})
201 else:
202 raise ValueError('Unable to detect '
203 'peer relation {}'.format(relation_name))
204
205
206def peer_echo(includes=None, force=False):
207 """Echo filtered attributes back onto the same relation for storage.
208
209 This is a requirement to use the peerstorage module - it needs to be called
210 from the peer relation's changed hook.
211
212 If Juju leader support exists this will be a noop unless force is True.
213 """
214 try:
215 is_leader()
216 except NotImplementedError:
217 pass
218 else:
219 if not force:
220 return # NOOP if leader-election is supported
221
222 # Use original non-leader calls
223 relation_get = _relation_get
224 relation_set = _relation_set
225
226 rdata = relation_get()
227 echo_data = {}
228 if includes is None:
229 echo_data = rdata.copy()
230 for ex in ['private-address', 'public-address']:
231 if ex in echo_data:
232 echo_data.pop(ex)
233 else:
234 for attribute, value in six.iteritems(rdata):
235 for include in includes:
236 if include in attribute:
237 echo_data[attribute] = value
238 if len(echo_data) > 0:
239 relation_set(relation_settings=echo_data)
240
241
242def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
243 peer_store_fatal=False, relation_settings=None,
244 delimiter='_', **kwargs):
245 """Store passed-in arguments both in argument relation and in peer storage.
246
247 It functions like doing relation_set() and peer_store() at the same time,
248 with the same data.
249
250 @param relation_id: the id of the relation to store the data on. Defaults
251 to the current relation.
252 @param peer_store_fatal: Set to True, the function will raise an exception
253 should the peer sotrage not be avialable."""
254
255 relation_settings = relation_settings if relation_settings else {}
256 relation_set(relation_id=relation_id,
257 relation_settings=relation_settings,
258 **kwargs)
259 if is_relation_made(peer_relation_name):
260 for key, value in six.iteritems(dict(list(kwargs.items()) +
261 list(relation_settings.items()))):
262 key_prefix = relation_id or current_relation_id()
263 peer_store(key_prefix + delimiter + key,
264 value,
265 relation_name=peer_relation_name)
266 else:
267 if peer_store_fatal:
268 raise ValueError('Unable to detect '
269 'peer relation {}'.format(peer_relation_name))
2700
=== removed directory 'hooks/charmhelpers/contrib/saltstack'
=== removed file 'hooks/charmhelpers/contrib/saltstack/__init__.py'
--- hooks/charmhelpers/contrib/saltstack/__init__.py 2015-07-29 18:07:31 +0000
+++ hooks/charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,118 +0,0 @@
1# Copyright 2014-2015 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"""Charm Helpers saltstack - declare the state of your machines.
18
19This helper enables you to declare your machine state, rather than
20program it procedurally (and have to test each change to your procedures).
21Your install hook can be as simple as::
22
23 {{{
24 from charmhelpers.contrib.saltstack import (
25 install_salt_support,
26 update_machine_state,
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches