Merge lp:~junaidali/charms/trusty/neutron-api-plumgrid/latest-ini into lp:~plumgrid-team/charms/trusty/neutron-api-plumgrid/trunk

Proposed by Junaid Ali
Status: Merged
Merged at revision: 20
Proposed branch: lp:~junaidali/charms/trusty/neutron-api-plumgrid/latest-ini
Merge into: lp:~plumgrid-team/charms/trusty/neutron-api-plumgrid/trunk
Diff against target: 6412 lines (+124/-5807)
56 files modified
charm-helpers-sync.yaml (+6/-1)
config.yaml (+14/-15)
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/neutron_plumgrid_context.py (+37/-11)
hooks/neutron_plumgrid_hooks.py (+7/-0)
hooks/neutron_plumgrid_utils.py (+26/-3)
metadata.yaml (+2/-0)
templates/kilo/pgrc (+8/-0)
templates/kilo/plumgrid.ini (+6/-0)
templates/kilo/plumlib.ini (+4/-1)
unit_tests/test_neutron_plumgrid_plugin_context.py (+11/-18)
unit_tests/test_neutron_plumgrid_plugin_utils.py (+3/-1)
To merge this branch: bzr merge lp:~junaidali/charms/trusty/neutron-api-plumgrid/latest-ini
Reviewer Review Type Date Requested Status
Bilal Baqar Pending
Review via email: mp+293151@code.launchpad.net
To post a comment you must log in.
21. By Junaid Ali

l2VTEP and ECMP support plus unit_test fix

22. By Junaid Ali

Added pgrc file (ECMP support)

23. By Junaid Ali

changing legacy-mode's default value to True

24. By Junaid Ali

added relation with director to extract pg vip etc.

25. By Junaid Ali

added symlink

26. By Junaid Ali

fixing unit_test

27. By Junaid Ali

Updating functions names and code comments

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: