Browse Source

update sleep time in restart_pg, changes for make sync

Junaid Ali 3 years ago
parent
commit
86037a00c8
50 changed files with 9 additions and 5760 deletions
  1. 6
    1
      charm-helpers-sync.yaml
  2. 0
    254
      hooks/charmhelpers/contrib/ansible/__init__.py
  3. 0
    126
      hooks/charmhelpers/contrib/benchmark/__init__.py
  4. 0
    208
      hooks/charmhelpers/contrib/charmhelpers/__init__.py
  5. 0
    15
      hooks/charmhelpers/contrib/charmsupport/__init__.py
  6. 0
    398
      hooks/charmhelpers/contrib/charmsupport/nrpe.py
  7. 0
    175
      hooks/charmhelpers/contrib/charmsupport/volumes.py
  8. 0
    0
      hooks/charmhelpers/contrib/database/__init__.py
  9. 0
    412
      hooks/charmhelpers/contrib/database/mysql.py
  10. 0
    15
      hooks/charmhelpers/contrib/hardening/__init__.py
  11. 0
    19
      hooks/charmhelpers/contrib/hardening/apache/__init__.py
  12. 0
    31
      hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py
  13. 0
    100
      hooks/charmhelpers/contrib/hardening/apache/checks/config.py
  14. 0
    63
      hooks/charmhelpers/contrib/hardening/audits/__init__.py
  15. 0
    100
      hooks/charmhelpers/contrib/hardening/audits/apache.py
  16. 0
    105
      hooks/charmhelpers/contrib/hardening/audits/apt.py
  17. 0
    552
      hooks/charmhelpers/contrib/hardening/audits/file.py
  18. 0
    84
      hooks/charmhelpers/contrib/hardening/harden.py
  19. 0
    19
      hooks/charmhelpers/contrib/hardening/host/__init__.py
  20. 0
    50
      hooks/charmhelpers/contrib/hardening/host/checks/__init__.py
  21. 0
    39
      hooks/charmhelpers/contrib/hardening/host/checks/apt.py
  22. 0
    55
      hooks/charmhelpers/contrib/hardening/host/checks/limits.py
  23. 0
    67
      hooks/charmhelpers/contrib/hardening/host/checks/login.py
  24. 0
    52
      hooks/charmhelpers/contrib/hardening/host/checks/minimize_access.py
  25. 0
    134
      hooks/charmhelpers/contrib/hardening/host/checks/pam.py
  26. 0
    45
      hooks/charmhelpers/contrib/hardening/host/checks/profile.py
  27. 0
    39
      hooks/charmhelpers/contrib/hardening/host/checks/securetty.py
  28. 0
    131
      hooks/charmhelpers/contrib/hardening/host/checks/suid_sgid.py
  29. 0
    211
      hooks/charmhelpers/contrib/hardening/host/checks/sysctl.py
  30. 0
    19
      hooks/charmhelpers/contrib/hardening/mysql/__init__.py
  31. 0
    31
      hooks/charmhelpers/contrib/hardening/mysql/checks/__init__.py
  32. 0
    89
      hooks/charmhelpers/contrib/hardening/mysql/checks/config.py
  33. 0
    19
      hooks/charmhelpers/contrib/hardening/ssh/__init__.py
  34. 0
    31
      hooks/charmhelpers/contrib/hardening/ssh/checks/__init__.py
  35. 0
    394
      hooks/charmhelpers/contrib/hardening/ssh/checks/config.py
  36. 0
    71
      hooks/charmhelpers/contrib/hardening/templating.py
  37. 0
    157
      hooks/charmhelpers/contrib/hardening/utils.py
  38. 0
    0
      hooks/charmhelpers/contrib/mellanox/__init__.py
  39. 0
    151
      hooks/charmhelpers/contrib/mellanox/infiniband.py
  40. 0
    269
      hooks/charmhelpers/contrib/peerstorage/__init__.py
  41. 0
    118
      hooks/charmhelpers/contrib/saltstack/__init__.py
  42. 0
    94
      hooks/charmhelpers/contrib/ssl/__init__.py
  43. 0
    279
      hooks/charmhelpers/contrib/ssl/service.py
  44. 0
    15
      hooks/charmhelpers/contrib/templating/__init__.py
  45. 0
    139
      hooks/charmhelpers/contrib/templating/contexts.py
  46. 0
    40
      hooks/charmhelpers/contrib/templating/jinja.py
  47. 0
    29
      hooks/charmhelpers/contrib/templating/pyformat.py
  48. 0
    313
      hooks/charmhelpers/contrib/unison/__init__.py
  49. 1
    1
      hooks/pg_gw_utils.py
  50. 2
    1
      unit_tests/test_pg_gw_hooks.py

+ 6
- 1
charm-helpers-sync.yaml View File

@@ -3,5 +3,10 @@ destination: hooks/charmhelpers
3 3
 include:
4 4
     - core
5 5
     - fetch
6
-    - contrib
6
+    - contrib.amulet
7
+    - contrib.hahelpers
8
+    - contrib.network
9
+    - contrib.openstack
10
+    - contrib.python
11
+    - contrib.storage
7 12
     - payload

+ 0
- 254
hooks/charmhelpers/contrib/ansible/__init__.py View File

@@ -1,254 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-# Copyright 2013 Canonical Ltd.
18
-#
19
-# Authors:
20
-#  Charm Helpers Developers <juju@lists.ubuntu.com>
21
-"""Charm Helpers ansible - declare the state of your machines.
22
-
23
-This helper enables you to declare your machine state, rather than
24
-program it procedurally (and have to test each change to your procedures).
25
-Your install hook can be as simple as::
26
-
27
-    {{{
28
-    import charmhelpers.contrib.ansible
29
-
30
-
31
-    def install():
32
-        charmhelpers.contrib.ansible.install_ansible_support()
33
-        charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
34
-    }}}
35
-
36
-and won't need to change (nor will its tests) when you change the machine
37
-state.
38
-
39
-All of your juju config and relation-data are available as template
40
-variables within your playbooks and templates. An install playbook looks
41
-something like::
42
-
43
-    {{{
44
-    ---
45
-    - hosts: localhost
46
-      user: root
47
-
48
-      tasks:
49
-        - name: Add private repositories.
50
-          template:
51
-            src: ../templates/private-repositories.list.jinja2
52
-            dest: /etc/apt/sources.list.d/private.list
53
-
54
-        - name: Update the cache.
55
-          apt: update_cache=yes
56
-
57
-        - name: Install dependencies.
58
-          apt: pkg={{ item }}
59
-          with_items:
60
-            - python-mimeparse
61
-            - python-webob
62
-            - sunburnt
63
-
64
-        - name: Setup groups.
65
-          group: name={{ item.name }} gid={{ item.gid }}
66
-          with_items:
67
-            - { name: 'deploy_user', gid: 1800 }
68
-            - { name: 'service_user', gid: 1500 }
69
-
70
-      ...
71
-    }}}
72
-
73
-Read more online about `playbooks`_ and standard ansible `modules`_.
74
-
75
-.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
76
-.. _modules: http://www.ansibleworks.com/docs/modules.html
77
-
78
-A further feature os the ansible hooks is to provide a light weight "action"
79
-scripting tool. This is a decorator that you apply to a function, and that
80
-function can now receive cli args, and can pass extra args to the playbook.
81
-
82
-e.g.
83
-
84
-
85
-@hooks.action()
86
-def some_action(amount, force="False"):
87
-    "Usage: some-action AMOUNT [force=True]"  # <-- shown on error
88
-    # process the arguments
89
-    # do some calls
90
-    # return extra-vars to be passed to ansible-playbook
91
-    return {
92
-        'amount': int(amount),
93
-        'type': force,
94
-    }
95
-
96
-You can now create a symlink to hooks.py that can be invoked like a hook, but
97
-with cli params:
98
-
99
-# link actions/some-action to hooks/hooks.py
100
-
101
-actions/some-action amount=10 force=true
102
-
103
-"""
104
-import os
105
-import stat
106
-import subprocess
107
-import functools
108
-
109
-import charmhelpers.contrib.templating.contexts
110
-import charmhelpers.core.host
111
-import charmhelpers.core.hookenv
112
-import charmhelpers.fetch
113
-
114
-
115
-charm_dir = os.environ.get('CHARM_DIR', '')
116
-ansible_hosts_path = '/etc/ansible/hosts'
117
-# Ansible will automatically include any vars in the following
118
-# file in its inventory when run locally.
119
-ansible_vars_path = '/etc/ansible/host_vars/localhost'
120
-
121
-
122
-def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
123
-    """Installs the ansible package.
124
-
125
-    By default it is installed from the `PPA`_ linked from
126
-    the ansible `website`_ or from a ppa specified by a charm config..
127
-
128
-    .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
129
-    .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
130
-
131
-    If from_ppa is empty, you must ensure that the package is available
132
-    from a configured repository.
133
-    """
134
-    if from_ppa:
135
-        charmhelpers.fetch.add_source(ppa_location)
136
-        charmhelpers.fetch.apt_update(fatal=True)
137
-    charmhelpers.fetch.apt_install('ansible')
138
-    with open(ansible_hosts_path, 'w+') as hosts_file:
139
-        hosts_file.write('localhost ansible_connection=local')
140
-
141
-
142
-def apply_playbook(playbook, tags=None, extra_vars=None):
143
-    tags = tags or []
144
-    tags = ",".join(tags)
145
-    charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
146
-        ansible_vars_path, namespace_separator='__',
147
-        allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
148
-
149
-    # we want ansible's log output to be unbuffered
150
-    env = os.environ.copy()
151
-    env['PYTHONUNBUFFERED'] = "1"
152
-    call = [
153
-        'ansible-playbook',
154
-        '-c',
155
-        'local',
156
-        playbook,
157
-    ]
158
-    if tags:
159
-        call.extend(['--tags', '{}'.format(tags)])
160
-    if extra_vars:
161
-        extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
162
-        call.extend(['--extra-vars', " ".join(extra)])
163
-    subprocess.check_call(call, env=env)
164
-
165
-
166
-class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
167
-    """Run a playbook with the hook-name as the tag.
168
-
169
-    This helper builds on the standard hookenv.Hooks helper,
170
-    but additionally runs the playbook with the hook-name specified
171
-    using --tags (ie. running all the tasks tagged with the hook-name).
172
-
173
-    Example::
174
-
175
-        hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
176
-
177
-        # All the tasks within my_machine_state.yaml tagged with 'install'
178
-        # will be run automatically after do_custom_work()
179
-        @hooks.hook()
180
-        def install():
181
-            do_custom_work()
182
-
183
-        # For most of your hooks, you won't need to do anything other
184
-        # than run the tagged tasks for the hook:
185
-        @hooks.hook('config-changed', 'start', 'stop')
186
-        def just_use_playbook():
187
-            pass
188
-
189
-        # As a convenience, you can avoid the above noop function by specifying
190
-        # the hooks which are handled by ansible-only and they'll be registered
191
-        # for you:
192
-        # hooks = AnsibleHooks(
193
-        #     'playbooks/my_machine_state.yaml',
194
-        #     default_hooks=['config-changed', 'start', 'stop'])
195
-
196
-        if __name__ == "__main__":
197
-            # execute a hook based on the name the program is called by
198
-            hooks.execute(sys.argv)
199
-
200
-    """
201
-
202
-    def __init__(self, playbook_path, default_hooks=None):
203
-        """Register any hooks handled by ansible."""
204
-        super(AnsibleHooks, self).__init__()
205
-
206
-        self._actions = {}
207
-        self.playbook_path = playbook_path
208
-
209
-        default_hooks = default_hooks or []
210
-
211
-        def noop(*args, **kwargs):
212
-            pass
213
-
214
-        for hook in default_hooks:
215
-            self.register(hook, noop)
216
-
217
-    def register_action(self, name, function):
218
-        """Register a hook"""
219
-        self._actions[name] = function
220
-
221
-    def execute(self, args):
222
-        """Execute the hook followed by the playbook using the hook as tag."""
223
-        hook_name = os.path.basename(args[0])
224
-        extra_vars = None
225
-        if hook_name in self._actions:
226
-            extra_vars = self._actions[hook_name](args[1:])
227
-        else:
228
-            super(AnsibleHooks, self).execute(args)
229
-
230
-        charmhelpers.contrib.ansible.apply_playbook(
231
-            self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
232
-
233
-    def action(self, *action_names):
234
-        """Decorator, registering them as actions"""
235
-        def action_wrapper(decorated):
236
-
237
-            @functools.wraps(decorated)
238
-            def wrapper(argv):
239
-                kwargs = dict(arg.split('=') for arg in argv)
240
-                try:
241
-                    return decorated(**kwargs)
242
-                except TypeError as e:
243
-                    if decorated.__doc__:
244
-                        e.args += (decorated.__doc__,)
245
-                    raise
246
-
247
-            self.register_action(decorated.__name__, wrapper)
248
-            if '_' in decorated.__name__:
249
-                self.register_action(
250
-                    decorated.__name__.replace('_', '-'), wrapper)
251
-
252
-            return wrapper
253
-
254
-        return action_wrapper

+ 0
- 126
hooks/charmhelpers/contrib/benchmark/__init__.py View File

@@ -1,126 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-import subprocess
18
-import time
19
-import os
20
-from distutils.spawn import find_executable
21
-
22
-from charmhelpers.core.hookenv import (
23
-    in_relation_hook,
24
-    relation_ids,
25
-    relation_set,
26
-    relation_get,
27
-)
28
-
29
-
30
-def action_set(key, val):
31
-    if find_executable('action-set'):
32
-        action_cmd = ['action-set']
33
-
34
-        if isinstance(val, dict):
35
-            for k, v in iter(val.items()):
36
-                action_set('%s.%s' % (key, k), v)
37
-            return True
38
-
39
-        action_cmd.append('%s=%s' % (key, val))
40
-        subprocess.check_call(action_cmd)
41
-        return True
42
-    return False
43
-
44
-
45
-class Benchmark():
46
-    """
47
-    Helper class for the `benchmark` interface.
48
-
49
-    :param list actions: Define the actions that are also benchmarks
50
-
51
-    From inside the benchmark-relation-changed hook, you would
52
-    Benchmark(['memory', 'cpu', 'disk', 'smoke', 'custom'])
53
-
54
-    Examples:
55
-
56
-        siege = Benchmark(['siege'])
57
-        siege.start()
58
-        [... run siege ...]
59
-        # The higher the score, the better the benchmark
60
-        siege.set_composite_score(16.70, 'trans/sec', 'desc')
61
-        siege.finish()
62
-
63
-
64
-    """
65
-
66
-    BENCHMARK_CONF = '/etc/benchmark.conf'  # Replaced in testing
67
-
68
-    required_keys = [
69
-        'hostname',
70
-        'port',
71
-        'graphite_port',
72
-        'graphite_endpoint',
73
-        'api_port'
74
-    ]
75
-
76
-    def __init__(self, benchmarks=None):
77
-        if in_relation_hook():
78
-            if benchmarks is not None:
79
-                for rid in sorted(relation_ids('benchmark')):
80
-                    relation_set(relation_id=rid, relation_settings={
81
-                        'benchmarks': ",".join(benchmarks)
82
-                    })
83
-
84
-            # Check the relation data
85
-            config = {}
86
-            for key in self.required_keys:
87
-                val = relation_get(key)
88
-                if val is not None:
89
-                    config[key] = val
90
-                else:
91
-                    # We don't have all of the required keys
92
-                    config = {}
93
-                    break
94
-
95
-            if len(config):
96
-                with open(self.BENCHMARK_CONF, 'w') as f:
97
-                    for key, val in iter(config.items()):
98
-                        f.write("%s=%s\n" % (key, val))
99
-
100
-    @staticmethod
101
-    def start():
102
-        action_set('meta.start', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
103
-
104
-        """
105
-        If the collectd charm is also installed, tell it to send a snapshot
106
-        of the current profile data.
107
-        """
108
-        COLLECT_PROFILE_DATA = '/usr/local/bin/collect-profile-data'
109
-        if os.path.exists(COLLECT_PROFILE_DATA):
110
-            subprocess.check_output([COLLECT_PROFILE_DATA])
111
-
112
-    @staticmethod
113
-    def finish():
114
-        action_set('meta.stop', time.strftime('%Y-%m-%dT%H:%M:%SZ'))
115
-
116
-    @staticmethod
117
-    def set_composite_score(value, units, direction='asc'):
118
-        """
119
-        Set the composite score for a benchmark run. This is a single number
120
-        representative of the benchmark results. This could be the most
121
-        important metric, or an amalgamation of metric scores.
122
-        """
123
-        return action_set(
124
-            "meta.composite",
125
-            {'value': value, 'units': units, 'direction': direction}
126
-        )

+ 0
- 208
hooks/charmhelpers/contrib/charmhelpers/__init__.py View File

@@ -1,208 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
18
-# GNU Affero General Public License version 3 (see the file LICENSE).
19
-
20
-import warnings
21
-warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning)  # noqa
22
-
23
-import operator
24
-import tempfile
25
-import time
26
-import yaml
27
-import subprocess
28
-
29
-import six
30
-if six.PY3:
31
-    from urllib.request import urlopen
32
-    from urllib.error import (HTTPError, URLError)
33
-else:
34
-    from urllib2 import (urlopen, HTTPError, URLError)
35
-
36
-"""Helper functions for writing Juju charms in Python."""
37
-
38
-__metaclass__ = type
39
-__all__ = [
40
-    # 'get_config',             # core.hookenv.config()
41
-    # 'log',                    # core.hookenv.log()
42
-    # 'log_entry',              # core.hookenv.log()
43
-    # 'log_exit',               # core.hookenv.log()
44
-    # 'relation_get',           # core.hookenv.relation_get()
45
-    # 'relation_set',           # core.hookenv.relation_set()
46
-    # 'relation_ids',           # core.hookenv.relation_ids()
47
-    # 'relation_list',          # core.hookenv.relation_units()
48
-    # 'config_get',             # core.hookenv.config()
49
-    # 'unit_get',               # core.hookenv.unit_get()
50
-    # 'open_port',              # core.hookenv.open_port()
51
-    # 'close_port',             # core.hookenv.close_port()
52
-    # 'service_control',        # core.host.service()
53
-    'unit_info',              # client-side, NOT IMPLEMENTED
54
-    'wait_for_machine',       # client-side, NOT IMPLEMENTED
55
-    'wait_for_page_contents',  # client-side, NOT IMPLEMENTED
56
-    'wait_for_relation',      # client-side, NOT IMPLEMENTED
57
-    'wait_for_unit',          # client-side, NOT IMPLEMENTED
58
-]
59
-
60
-
61
-SLEEP_AMOUNT = 0.1
62
-
63
-
64
-# We create a juju_status Command here because it makes testing much,
65
-# much easier.
66
-def juju_status():
67
-    subprocess.check_call(['juju', 'status'])
68
-
69
-# re-implemented as charmhelpers.fetch.configure_sources()
70
-# def configure_source(update=False):
71
-#    source = config_get('source')
72
-#    if ((source.startswith('ppa:') or
73
-#         source.startswith('cloud:') or
74
-#         source.startswith('http:'))):
75
-#        run('add-apt-repository', source)
76
-#    if source.startswith("http:"):
77
-#        run('apt-key', 'import', config_get('key'))
78
-#    if update:
79
-#        run('apt-get', 'update')
80
-
81
-
82
-# DEPRECATED: client-side only
83
-def make_charm_config_file(charm_config):
84
-    charm_config_file = tempfile.NamedTemporaryFile(mode='w+')
85
-    charm_config_file.write(yaml.dump(charm_config))
86
-    charm_config_file.flush()
87
-    # The NamedTemporaryFile instance is returned instead of just the name
88
-    # because we want to take advantage of garbage collection-triggered
89
-    # deletion of the temp file when it goes out of scope in the caller.
90
-    return charm_config_file
91
-
92
-
93
-# DEPRECATED: client-side only
94
-def unit_info(service_name, item_name, data=None, unit=None):
95
-    if data is None:
96
-        data = yaml.safe_load(juju_status())
97
-    service = data['services'].get(service_name)
98
-    if service is None:
99
-        # XXX 2012-02-08 gmb:
100
-        #     This allows us to cope with the race condition that we
101
-        #     have between deploying a service and having it come up in
102
-        #     `juju status`. We could probably do with cleaning it up so
103
-        #     that it fails a bit more noisily after a while.
104
-        return ''
105
-    units = service['units']
106
-    if unit is not None:
107
-        item = units[unit][item_name]
108
-    else:
109
-        # It might seem odd to sort the units here, but we do it to
110
-        # ensure that when no unit is specified, the first unit for the
111
-        # service (or at least the one with the lowest number) is the
112
-        # one whose data gets returned.
113
-        sorted_unit_names = sorted(units.keys())
114
-        item = units[sorted_unit_names[0]][item_name]
115
-    return item
116
-
117
-
118
-# DEPRECATED: client-side only
119
-def get_machine_data():
120
-    return yaml.safe_load(juju_status())['machines']
121
-
122
-
123
-# DEPRECATED: client-side only
124
-def wait_for_machine(num_machines=1, timeout=300):
125
-    """Wait `timeout` seconds for `num_machines` machines to come up.
126
-
127
-    This wait_for... function can be called by other wait_for functions
128
-    whose timeouts might be too short in situations where only a bare
129
-    Juju setup has been bootstrapped.
130
-
131
-    :return: A tuple of (num_machines, time_taken). This is used for
132
-             testing.
133
-    """
134
-    # You may think this is a hack, and you'd be right. The easiest way
135
-    # to tell what environment we're working in (LXC vs EC2) is to check
136
-    # the dns-name of the first machine. If it's localhost we're in LXC
137
-    # and we can just return here.
138
-    if get_machine_data()[0]['dns-name'] == 'localhost':
139
-        return 1, 0
140
-    start_time = time.time()
141
-    while True:
142
-        # Drop the first machine, since it's the Zookeeper and that's
143
-        # not a machine that we need to wait for. This will only work
144
-        # for EC2 environments, which is why we return early above if
145
-        # we're in LXC.
146
-        machine_data = get_machine_data()
147
-        non_zookeeper_machines = [
148
-            machine_data[key] for key in list(machine_data.keys())[1:]]
149
-        if len(non_zookeeper_machines) >= num_machines:
150
-            all_machines_running = True
151
-            for machine in non_zookeeper_machines:
152
-                if machine.get('instance-state') != 'running':
153
-                    all_machines_running = False
154
-                    break
155
-            if all_machines_running:
156
-                break
157
-        if time.time() - start_time >= timeout:
158
-            raise RuntimeError('timeout waiting for service to start')
159
-        time.sleep(SLEEP_AMOUNT)
160
-    return num_machines, time.time() - start_time
161
-
162
-
163
-# DEPRECATED: client-side only
164
-def wait_for_unit(service_name, timeout=480):
165
-    """Wait `timeout` seconds for a given service name to come up."""
166
-    wait_for_machine(num_machines=1)
167
-    start_time = time.time()
168
-    while True:
169
-        state = unit_info(service_name, 'agent-state')
170
-        if 'error' in state or state == 'started':
171
-            break
172
-        if time.time() - start_time >= timeout:
173
-            raise RuntimeError('timeout waiting for service to start')
174
-        time.sleep(SLEEP_AMOUNT)
175
-    if state != 'started':
176
-        raise RuntimeError('unit did not start, agent-state: ' + state)
177
-
178
-
179
-# DEPRECATED: client-side only
180
-def wait_for_relation(service_name, relation_name, timeout=120):
181
-    """Wait `timeout` seconds for a given relation to come up."""
182
-    start_time = time.time()
183
-    while True:
184
-        relation = unit_info(service_name, 'relations').get(relation_name)
185
-        if relation is not None and relation['state'] == 'up':
186
-            break
187
-        if time.time() - start_time >= timeout:
188
-            raise RuntimeError('timeout waiting for relation to be up')
189
-        time.sleep(SLEEP_AMOUNT)
190
-
191
-
192
-# DEPRECATED: client-side only
193
-def wait_for_page_contents(url, contents, timeout=120, validate=None):
194
-    if validate is None:
195
-        validate = operator.contains
196
-    start_time = time.time()
197
-    while True:
198
-        try:
199
-            stream = urlopen(url)
200
-        except (HTTPError, URLError):
201
-            pass
202
-        else:
203
-            page = stream.read()
204
-            if validate(page, contents):
205
-                return page
206
-        if time.time() - start_time >= timeout:
207
-            raise RuntimeError('timeout waiting for contents of ' + url)
208
-        time.sleep(SLEEP_AMOUNT)

+ 0
- 15
hooks/charmhelpers/contrib/charmsupport/__init__.py View File

@@ -1,15 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.

+ 0
- 398
hooks/charmhelpers/contrib/charmsupport/nrpe.py View File

@@ -1,398 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-"""Compatibility with the nrpe-external-master charm"""
18
-# Copyright 2012 Canonical Ltd.
19
-#
20
-# Authors:
21
-#  Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
-
23
-import subprocess
24
-import pwd
25
-import grp
26
-import os
27
-import glob
28
-import shutil
29
-import re
30
-import shlex
31
-import yaml
32
-
33
-from charmhelpers.core.hookenv import (
34
-    config,
35
-    local_unit,
36
-    log,
37
-    relation_ids,
38
-    relation_set,
39
-    relations_of_type,
40
-)
41
-
42
-from charmhelpers.core.host import service
43
-
44
-# This module adds compatibility with the nrpe-external-master and plain nrpe
45
-# subordinate charms. To use it in your charm:
46
-#
47
-# 1. Update metadata.yaml
48
-#
49
-#   provides:
50
-#     (...)
51
-#     nrpe-external-master:
52
-#       interface: nrpe-external-master
53
-#       scope: container
54
-#
55
-#   and/or
56
-#
57
-#   provides:
58
-#     (...)
59
-#     local-monitors:
60
-#       interface: local-monitors
61
-#       scope: container
62
-
63
-#
64
-# 2. Add the following to config.yaml
65
-#
66
-#    nagios_context:
67
-#      default: "juju"
68
-#      type: string
69
-#      description: |
70
-#        Used by the nrpe subordinate charms.
71
-#        A string that will be prepended to instance name to set the host name
72
-#        in nagios. So for instance the hostname would be something like:
73
-#            juju-myservice-0
74
-#        If you're running multiple environments with the same services in them
75
-#        this allows you to differentiate between them.
76
-#    nagios_servicegroups:
77
-#      default: ""
78
-#      type: string
79
-#      description: |
80
-#        A comma-separated list of nagios servicegroups.
81
-#        If left empty, the nagios_context will be used as the servicegroup
82
-#
83
-# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84
-#
85
-# 4. Update your hooks.py with something like this:
86
-#
87
-#    from charmsupport.nrpe import NRPE
88
-#    (...)
89
-#    def update_nrpe_config():
90
-#        nrpe_compat = NRPE()
91
-#        nrpe_compat.add_check(
92
-#            shortname = "myservice",
93
-#            description = "Check MyService",
94
-#            check_cmd = "check_http -w 2 -c 10 http://localhost"
95
-#            )
96
-#        nrpe_compat.add_check(
97
-#            "myservice_other",
98
-#            "Check for widget failures",
99
-#            check_cmd = "/srv/myapp/scripts/widget_check"
100
-#            )
101
-#        nrpe_compat.write()
102
-#
103
-#    def config_changed():
104
-#        (...)
105
-#        update_nrpe_config()
106
-#
107
-#    def nrpe_external_master_relation_changed():
108
-#        update_nrpe_config()
109
-#
110
-#    def local_monitors_relation_changed():
111
-#        update_nrpe_config()
112
-#
113
-# 5. ln -s hooks.py nrpe-external-master-relation-changed
114
-#    ln -s hooks.py local-monitors-relation-changed
115
-
116
-
117
-class CheckException(Exception):
118
-    pass
119
-
120
-
121
-class Check(object):
122
-    shortname_re = '[A-Za-z0-9-_]+$'
123
-    service_template = ("""
124
-#---------------------------------------------------
125
-# This file is Juju managed
126
-#---------------------------------------------------
127
-define service {{
128
-    use                             active-service
129
-    host_name                       {nagios_hostname}
130
-    service_description             {nagios_hostname}[{shortname}] """
131
-                        """{description}
132
-    check_command                   check_nrpe!{command}
133
-    servicegroups                   {nagios_servicegroup}
134
-}}
135
-""")
136
-
137
-    def __init__(self, shortname, description, check_cmd):
138
-        super(Check, self).__init__()
139
-        # XXX: could be better to calculate this from the service name
140
-        if not re.match(self.shortname_re, shortname):
141
-            raise CheckException("shortname must match {}".format(
142
-                Check.shortname_re))
143
-        self.shortname = shortname
144
-        self.command = "check_{}".format(shortname)
145
-        # Note: a set of invalid characters is defined by the
146
-        # Nagios server config
147
-        # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
148
-        self.description = description
149
-        self.check_cmd = self._locate_cmd(check_cmd)
150
-
151
-    def _get_check_filename(self):
152
-        return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
153
-
154
-    def _get_service_filename(self, hostname):
155
-        return os.path.join(NRPE.nagios_exportdir,
156
-                            'service__{}_{}.cfg'.format(hostname, self.command))
157
-
158
-    def _locate_cmd(self, check_cmd):
159
-        search_path = (
160
-            '/usr/lib/nagios/plugins',
161
-            '/usr/local/lib/nagios/plugins',
162
-        )
163
-        parts = shlex.split(check_cmd)
164
-        for path in search_path:
165
-            if os.path.exists(os.path.join(path, parts[0])):
166
-                command = os.path.join(path, parts[0])
167
-                if len(parts) > 1:
168
-                    command += " " + " ".join(parts[1:])
169
-                return command
170
-        log('Check command not found: {}'.format(parts[0]))
171
-        return ''
172
-
173
-    def _remove_service_files(self):
174
-        if not os.path.exists(NRPE.nagios_exportdir):
175
-            return
176
-        for f in os.listdir(NRPE.nagios_exportdir):
177
-            if f.endswith('_{}.cfg'.format(self.command)):
178
-                os.remove(os.path.join(NRPE.nagios_exportdir, f))
179
-
180
-    def remove(self, hostname):
181
-        nrpe_check_file = self._get_check_filename()
182
-        if os.path.exists(nrpe_check_file):
183
-            os.remove(nrpe_check_file)
184
-        self._remove_service_files()
185
-
186
-    def write(self, nagios_context, hostname, nagios_servicegroups):
187
-        nrpe_check_file = self._get_check_filename()
188
-        with open(nrpe_check_file, 'w') as nrpe_check_config:
189
-            nrpe_check_config.write("# check {}\n".format(self.shortname))
190
-            nrpe_check_config.write("command[{}]={}\n".format(
191
-                self.command, self.check_cmd))
192
-
193
-        if not os.path.exists(NRPE.nagios_exportdir):
194
-            log('Not writing service config as {} is not accessible'.format(
195
-                NRPE.nagios_exportdir))
196
-        else:
197
-            self.write_service_config(nagios_context, hostname,
198
-                                      nagios_servicegroups)
199
-
200
-    def write_service_config(self, nagios_context, hostname,
201
-                             nagios_servicegroups):
202
-        self._remove_service_files()
203
-
204
-        templ_vars = {
205
-            'nagios_hostname': hostname,
206
-            'nagios_servicegroup': nagios_servicegroups,
207
-            'description': self.description,
208
-            'shortname': self.shortname,
209
-            'command': self.command,
210
-        }
211
-        nrpe_service_text = Check.service_template.format(**templ_vars)
212
-        nrpe_service_file = self._get_service_filename(hostname)
213
-        with open(nrpe_service_file, 'w') as nrpe_service_config:
214
-            nrpe_service_config.write(str(nrpe_service_text))
215
-
216
-    def run(self):
217
-        subprocess.call(self.check_cmd)
218
-
219
-
220
-class NRPE(object):
221
-    nagios_logdir = '/var/log/nagios'
222
-    nagios_exportdir = '/var/lib/nagios/export'
223
-    nrpe_confdir = '/etc/nagios/nrpe.d'
224
-
225
-    def __init__(self, hostname=None):
226
-        super(NRPE, self).__init__()
227
-        self.config = config()
228
-        self.nagios_context = self.config['nagios_context']
229
-        if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
230
-            self.nagios_servicegroups = self.config['nagios_servicegroups']
231
-        else:
232
-            self.nagios_servicegroups = self.nagios_context
233
-        self.unit_name = local_unit().replace('/', '-')
234
-        if hostname:
235
-            self.hostname = hostname
236
-        else:
237
-            nagios_hostname = get_nagios_hostname()
238
-            if nagios_hostname:
239
-                self.hostname = nagios_hostname
240
-            else:
241
-                self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
242
-        self.checks = []
243
-
244
-    def add_check(self, *args, **kwargs):
245
-        self.checks.append(Check(*args, **kwargs))
246
-
247
-    def remove_check(self, *args, **kwargs):
248
-        if kwargs.get('shortname') is None:
249
-            raise ValueError('shortname of check must be specified')
250
-
251
-        # Use sensible defaults if they're not specified - these are not
252
-        # actually used during removal, but they're required for constructing
253
-        # the Check object; check_disk is chosen because it's part of the
254
-        # nagios-plugins-basic package.
255
-        if kwargs.get('check_cmd') is None:
256
-            kwargs['check_cmd'] = 'check_disk'
257
-        if kwargs.get('description') is None:
258
-            kwargs['description'] = ''
259
-
260
-        check = Check(*args, **kwargs)
261
-        check.remove(self.hostname)
262
-
263
-    def write(self):
264
-        try:
265
-            nagios_uid = pwd.getpwnam('nagios').pw_uid
266
-            nagios_gid = grp.getgrnam('nagios').gr_gid
267
-        except:
268
-            log("Nagios user not set up, nrpe checks not updated")
269
-            return
270
-
271
-        if not os.path.exists(NRPE.nagios_logdir):
272
-            os.mkdir(NRPE.nagios_logdir)
273
-            os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
274
-
275
-        nrpe_monitors = {}
276
-        monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
277
-        for nrpecheck in self.checks:
278
-            nrpecheck.write(self.nagios_context, self.hostname,
279
-                            self.nagios_servicegroups)
280
-            nrpe_monitors[nrpecheck.shortname] = {
281
-                "command": nrpecheck.command,
282
-            }
283
-
284
-        service('restart', 'nagios-nrpe-server')
285
-
286
-        monitor_ids = relation_ids("local-monitors") + \
287
-            relation_ids("nrpe-external-master")
288
-        for rid in monitor_ids:
289
-            relation_set(relation_id=rid, monitors=yaml.dump(monitors))
290
-
291
-
292
-def get_nagios_hostcontext(relation_name='nrpe-external-master'):
293
-    """
294
-    Query relation with nrpe subordinate, return the nagios_host_context
295
-
296
-    :param str relation_name: Name of relation nrpe sub joined to
297
-    """
298
-    for rel in relations_of_type(relation_name):
299
-        if 'nagios_host_context' in rel:
300
-            return rel['nagios_host_context']
301
-
302
-
303
-def get_nagios_hostname(relation_name='nrpe-external-master'):
304
-    """
305
-    Query relation with nrpe subordinate, return the nagios_hostname
306
-
307
-    :param str relation_name: Name of relation nrpe sub joined to
308
-    """
309
-    for rel in relations_of_type(relation_name):
310
-        if 'nagios_hostname' in rel:
311
-            return rel['nagios_hostname']
312
-
313
-
314
-def get_nagios_unit_name(relation_name='nrpe-external-master'):
315
-    """
316
-    Return the nagios unit name prepended with host_context if needed
317
-
318
-    :param str relation_name: Name of relation nrpe sub joined to
319
-    """
320
-    host_context = get_nagios_hostcontext(relation_name)
321
-    if host_context:
322
-        unit = "%s:%s" % (host_context, local_unit())
323
-    else:
324
-        unit = local_unit()
325
-    return unit
326
-
327
-
328
-def add_init_service_checks(nrpe, services, unit_name):
329
-    """
330
-    Add checks for each service in list
331
-
332
-    :param NRPE nrpe: NRPE object to add check to
333
-    :param list services: List of services to check
334
-    :param str unit_name: Unit name to use in check description
335
-    """
336
-    for svc in services:
337
-        upstart_init = '/etc/init/%s.conf' % svc
338
-        sysv_init = '/etc/init.d/%s' % svc
339
-        if os.path.exists(upstart_init):
340
-            # Don't add a check for these services from neutron-gateway
341
-            if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
342
-                nrpe.add_check(
343
-                    shortname=svc,
344
-                    description='process check {%s}' % unit_name,
345
-                    check_cmd='check_upstart_job %s' % svc
346
-                )
347
-        elif os.path.exists(sysv_init):
348
-            cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
349
-            cron_file = ('*/5 * * * * root '
350
-                         '/usr/local/lib/nagios/plugins/check_exit_status.pl '
351
-                         '-s /etc/init.d/%s status > '
352
-                         '/var/lib/nagios/service-check-%s.txt\n' % (svc,
353
-                                                                     svc)
354
-                         )
355
-            f = open(cronpath, 'w')
356
-            f.write(cron_file)
357
-            f.close()
358
-            nrpe.add_check(
359
-                shortname=svc,
360
-                description='process check {%s}' % unit_name,
361
-                check_cmd='check_status_file.py -f '
362
-                          '/var/lib/nagios/service-check-%s.txt' % svc,
363
-            )
364
-
365
-
366
-def copy_nrpe_checks():
367
-    """
368
-    Copy the nrpe checks into place
369
-
370
-    """
371
-    NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
372
-    nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
373
-                                  'charmhelpers', 'contrib', 'openstack',
374
-                                  'files')
375
-
376
-    if not os.path.exists(NAGIOS_PLUGINS):
377
-        os.makedirs(NAGIOS_PLUGINS)
378
-    for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
379
-        if os.path.isfile(fname):
380
-            shutil.copy2(fname,
381
-                         os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
382
-
383
-
384
-def add_haproxy_checks(nrpe, unit_name):
385
-    """
386
-    Add checks for each service in list
387
-
388
-    :param NRPE nrpe: NRPE object to add check to
389
-    :param str unit_name: Unit name to use in check description
390
-    """
391
-    nrpe.add_check(
392
-        shortname='haproxy_servers',
393
-        description='Check HAProxy {%s}' % unit_name,
394
-        check_cmd='check_haproxy.sh')
395
-    nrpe.add_check(
396
-        shortname='haproxy_queue',
397
-        description='Check HAProxy queue depth {%s}' % unit_name,
398
-        check_cmd='check_haproxy_queue_depth.sh')

+ 0
- 175
hooks/charmhelpers/contrib/charmsupport/volumes.py View File

@@ -1,175 +0,0 @@
1
-# Copyright 2014-2015 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-'''
18
-Functions for managing volumes in juju units. One volume is supported per unit.
19
-Subordinates may have their own storage, provided it is on its own partition.
20
-
21
-Configuration stanzas::
22
-
23
-  volume-ephemeral:
24
-    type: boolean
25
-    default: true
26
-    description: >
27
-      If false, a volume is mounted as sepecified in "volume-map"
28
-      If true, ephemeral storage will be used, meaning that log data
29
-         will only exist as long as the machine. YOU HAVE BEEN WARNED.
30
-  volume-map:
31
-    type: string
32
-    default: {}
33
-    description: >
34
-      YAML map of units to device names, e.g:
35
-        "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36
-      Service units will raise a configure-error if volume-ephemeral
37
-      is 'true' and no volume-map value is set. Use 'juju set' to set a
38
-      value and 'juju resolved' to complete configuration.
39
-
40
-Usage::
41
-
42
-    from charmsupport.volumes import configure_volume, VolumeConfigurationError
43
-    from charmsupport.hookenv import log, ERROR
44
-    def post_mount_hook():
45
-        stop_service('myservice')
46
-    def post_mount_hook():
47
-        start_service('myservice')
48
-
49
-    if __name__ == '__main__':
50
-        try:
51
-            configure_volume(before_change=pre_mount_hook,
52
-                             after_change=post_mount_hook)
53
-        except VolumeConfigurationError:
54
-            log('Storage could not be configured', ERROR)
55
-
56
-'''
57
-
58
-# XXX: Known limitations
59
-# - fstab is neither consulted nor updated
60
-
61
-import os
62
-from charmhelpers.core import hookenv
63
-from charmhelpers.core import host
64
-import yaml
65
-
66
-
67
-MOUNT_BASE = '/srv/juju/volumes'
68
-
69
-
70
-class VolumeConfigurationError(Exception):
71
-    '''Volume configuration data is missing or invalid'''
72
-    pass
73
-
74
-
75
-def get_config():
76
-    '''Gather and sanity-check volume configuration data'''
77
-    volume_config = {}
78
-    config = hookenv.config()
79
-
80
-    errors = False
81
-
82
-    if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83
-        volume_config['ephemeral'] = True
84
-    else:
85
-        volume_config['ephemeral'] = False
86
-
87
-    try:
88
-        volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89
-    except yaml.YAMLError as e:
90
-        hookenv.log("Error parsing YAML volume-map: {}".format(e),
91
-                    hookenv.ERROR)
92
-        errors = True
93
-    if volume_map is None:
94
-        # probably an empty string
95
-        volume_map = {}
96
-    elif not isinstance(volume_map, dict):
97
-        hookenv.log("Volume-map should be a dictionary, not {}".format(
98
-            type(volume_map)))
99
-        errors = True
100
-
101
-    volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102
-    if volume_config['device'] and volume_config['ephemeral']:
103
-        # asked for ephemeral storage but also defined a volume ID
104
-        hookenv.log('A volume is defined for this unit, but ephemeral '
105
-                    'storage was requested', hookenv.ERROR)
106
-        errors = True
107
-    elif not volume_config['device'] and not volume_config['ephemeral']:
108
-        # asked for permanent storage but did not define volume ID
109
-        hookenv.log('Ephemeral storage was requested, but there is no volume '
110
-                    'defined for this unit.', hookenv.ERROR)
111
-        errors = True
112
-
113
-    unit_mount_name = hookenv.local_unit().replace('/', '-')
114
-    volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
-
116
-    if errors:
117
-        return None
118
-    return volume_config
119
-
120
-
121
-def mount_volume(config):
122
-    if os.path.exists(config['mountpoint']):
123
-        if not os.path.isdir(config['mountpoint']):
124
-            hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125
-            raise VolumeConfigurationError()
126
-    else:
127
-        host.mkdir(config['mountpoint'])
128
-    if os.path.ismount(config['mountpoint']):
129
-        unmount_volume(config)
130
-    if not host.mount(config['device'], config['mountpoint'], persist=True):
131
-        raise VolumeConfigurationError()
132
-
133
-
134
-def unmount_volume(config):
135
-    if os.path.ismount(config['mountpoint']):
136
-        if not host.umount(config['mountpoint'], persist=True):
137
-            raise VolumeConfigurationError()
138
-
139
-
140
-def managed_mounts():
141
-    '''List of all mounted managed volumes'''
142
-    return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
-
144
-
145
-def configure_volume(before_change=lambda: None, after_change=lambda: None):
146
-    '''Set up storage (or don't) according to the charm's volume configuration.
147
-       Returns the mount point or "ephemeral". before_change and after_change
148
-       are optional functions to be called if the volume configuration changes.
149
-    '''
150
-
151
-    config = get_config()
152
-    if not config:
153
-        hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154
-        raise VolumeConfigurationError()
155
-
156
-    if config['ephemeral']:
157
-        if os.path.ismount(config['mountpoint']):
158
-            before_change()
159
-            unmount_volume(config)
160
-            after_change()
161
-        return 'ephemeral'
162
-    else:
163
-        # persistent storage
164
-        if os.path.ismount(config['mountpoint']):
165
-            mounts = dict(managed_mounts())
166
-            if mounts.get(config['mountpoint']) != config['device']:
167
-                before_change()
168
-                unmount_volume(config)
169
-                mount_volume(config)
170
-                after_change()
171
-        else:
172
-            before_change()
173
-            mount_volume(config)
174
-            after_change()
175
-        return config['mountpoint']

+ 0
- 0
hooks/charmhelpers/contrib/database/__init__.py View File


+ 0
- 412
hooks/charmhelpers/contrib/database/mysql.py View File

@@ -1,412 +0,0 @@
1
-"""Helper for working with a MySQL database"""
2
-import json
3
-import re
4
-import sys
5
-import platform
6
-import os
7
-import glob
8
-
9
-# from string import upper
10
-
11
-from charmhelpers.core.host import (
12
-    mkdir,
13
-    pwgen,
14
-    write_file
15
-)
16
-from charmhelpers.core.hookenv import (
17
-    config as config_get,
18
-    relation_get,
19
-    related_units,
20
-    unit_get,
21
-    log,
22
-    DEBUG,
23
-    INFO,
24
-    WARNING,
25
-)
26
-from charmhelpers.fetch import (
27
-    apt_install,
28
-    apt_update,
29
-    filter_installed_packages,
30
-)
31
-from charmhelpers.contrib.peerstorage import (
32
-    peer_store,
33
-    peer_retrieve,
34
-)
35
-from charmhelpers.contrib.network.ip import get_host_ip
36
-
37
-try:
38
-    import MySQLdb
39
-except ImportError:
40
-    apt_update(fatal=True)
41
-    apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
42
-    import MySQLdb
43
-
44
-
45
-class MySQLHelper(object):
46
-
47
-    def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
48
-                 migrate_passwd_to_peer_relation=True,
49
-                 delete_ondisk_passwd_file=True):
50
-        self.host = host
51
-        # Password file path templates
52
-        self.root_passwd_file_template = rpasswdf_template
53
-        self.user_passwd_file_template = upasswdf_template
54
-
55
-        self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation
56
-        # If we migrate we have the option to delete local copy of root passwd
57
-        self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
58
-
59
-    def connect(self, user='root', password=None):
60
-        log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
61
-        self.connection = MySQLdb.connect(user=user, host=self.host,
62
-                                          passwd=password)
63
-
64
-    def database_exists(self, db_name):
65
-        cursor = self.connection.cursor()
66
-        try:
67
-            cursor.execute("SHOW DATABASES")
68
-            databases = [i[0] for i in cursor.fetchall()]
69
-        finally:
70
-            cursor.close()
71
-
72
-        return db_name in databases
73
-
74
-    def create_database(self, db_name):
75
-        cursor = self.connection.cursor()
76
-        try:
77
-            cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
78
-                           .format(db_name))
79
-        finally:
80
-            cursor.close()
81
-
82
-    def grant_exists(self, db_name, db_user, remote_ip):
83
-        cursor = self.connection.cursor()
84
-        priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
85
-                      "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
86
-        try:
87
-            cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
88
-                                                              remote_ip))
89
-            grants = [i[0] for i in cursor.fetchall()]
90
-        except MySQLdb.OperationalError:
91
-            return False
92
-        finally:
93
-            cursor.close()
94
-
95
-        # TODO: review for different grants
96
-        return priv_string in grants
97
-
98
-    def create_grant(self, db_name, db_user, remote_ip, password):
99
-        cursor = self.connection.cursor()
100
-        try:
101
-            # TODO: review for different grants
102
-            cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
103
-                           "IDENTIFIED BY '{}'".format(db_name,
104
-                                                       db_user,
105
-                                                       remote_ip,
106
-                                                       password))
107
-        finally:
108
-            cursor.close()
109
-
110
-    def create_admin_grant(self, db_user, remote_ip, password):
111
-        cursor = self.connection.cursor()
112
-        try:
113
-            cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
114
-                           "IDENTIFIED BY '{}'".format(db_user,
115
-                                                       remote_ip,
116
-                                                       password))
117
-        finally:
118
-            cursor.close()
119
-
120
-    def cleanup_grant(self, db_user, remote_ip):
121
-        cursor = self.connection.cursor()
122
-        try:
123
-            cursor.execute("DROP FROM mysql.user WHERE user='{}' "
124
-                           "AND HOST='{}'".format(db_user,
125
-                                                  remote_ip))
126
-        finally:
127
-            cursor.close()
128
-
129
-    def execute(self, sql):
130
-        """Execute arbitary SQL against the database."""
131
-        cursor = self.connection.cursor()
132
-        try:
133
-            cursor.execute(sql)
134
-        finally:
135
-            cursor.close()
136
-
137
-    def migrate_passwords_to_peer_relation(self, excludes=None):
138
-        """Migrate any passwords storage on disk to cluster peer relation."""
139
-        dirname = os.path.dirname(self.root_passwd_file_template)
140
-        path = os.path.join(dirname, '*.passwd')
141
-        for f in glob.glob(path):
142
-            if excludes and f in excludes:
143
-                log("Excluding %s from peer migration" % (f), level=DEBUG)
144
-                continue
145
-
146
-            key = os.path.basename(f)
147
-            with open(f, 'r') as passwd:
148
-                _value = passwd.read().strip()
149
-
150
-            try:
151
-                peer_store(key, _value)
152
-
153
-                if self.delete_ondisk_passwd_file:
154
-                    os.unlink(f)
155
-            except ValueError:
156
-                # NOTE cluster relation not yet ready - skip for now
157
-                pass
158
-
159
-    def get_mysql_password_on_disk(self, username=None, password=None):
160
-        """Retrieve, generate or store a mysql password for the provided
161
-        username on disk."""
162
-        if username:
163
-            template = self.user_passwd_file_template
164
-            passwd_file = template.format(username)
165
-        else:
166
-            passwd_file = self.root_passwd_file_template
167
-
168
-        _password = None
169
-        if os.path.exists(passwd_file):
170
-            log("Using existing password file '%s'" % passwd_file, level=DEBUG)
171
-            with open(passwd_file, 'r') as passwd:
172
-                _password = passwd.read().strip()
173
-        else:
174
-            log("Generating new password file '%s'" % passwd_file, level=DEBUG)
175
-            if not os.path.isdir(os.path.dirname(passwd_file)):
176
-                # NOTE: need to ensure this is not mysql root dir (which needs
177
-                # to be mysql readable)
178
-                mkdir(os.path.dirname(passwd_file), owner='root', group='root',
179
-                      perms=0o770)
180
-                # Force permissions - for some reason the chmod in makedirs
181
-                # fails
182
-                os.chmod(os.path.dirname(passwd_file), 0o770)
183
-
184
-            _password = password or pwgen(length=32)
185
-            write_file(passwd_file, _password, owner='root', group='root',
186
-                       perms=0o660)
187
-
188
-        return _password
189
-
190
-    def passwd_keys(self, username):
191
-        """Generator to return keys used to store passwords in peer store.
192
-
193
-        NOTE: we support both legacy and new format to support mysql
194
-        charm prior to refactor. This is necessary to avoid LP 1451890.
195
-        """
196
-        keys = []
197
-        if username == 'mysql':
198
-            log("Bad username '%s'" % (username), level=WARNING)
199
-
200
-        if username:
201
-            # IMPORTANT: *newer* format must be returned first
202
-            keys.append('mysql-%s.passwd' % (username))
203
-            keys.append('%s.passwd' % (username))
204
-        else:
205
-            keys.append('mysql.passwd')
206
-
207
-        for key in keys:
208
-            yield key
209
-
210
-    def get_mysql_password(self, username=None, password=None):
211
-        """Retrieve, generate or store a mysql password for the provided
212
-        username using peer relation cluster."""
213
-        excludes = []
214
-
215
-        # First check peer relation.
216
-        try:
217
-            for key in self.passwd_keys(username):
218
-                _password = peer_retrieve(key)
219
-                if _password:
220
-                    break
221
-
222
-            # If root password available don't update peer relation from local
223
-            if _password and not username:
224
-                excludes.append(self.root_passwd_file_template)
225
-
226
-        except ValueError:
227
-            # cluster relation is not yet started; use on-disk
228
-            _password = None
229
-
230
-        # If none available, generate new one
231
-        if not _password:
232
-            _password = self.get_mysql_password_on_disk(username, password)
233
-
234
-        # Put on wire if required
235
-        if self.migrate_passwd_to_peer_relation:
236
-            self.migrate_passwords_to_peer_relation(excludes=excludes)
237
-
238
-        return _password
239
-
240
-    def get_mysql_root_password(self, password=None):
241
-        """Retrieve or generate mysql root password for service units."""
242
-        return self.get_mysql_password(username=None, password=password)
243
-
244
-    def normalize_address(self, hostname):
245
-        """Ensure that address returned is an IP address (i.e. not fqdn)"""
246
-        if config_get('prefer-ipv6'):
247
-            # TODO: add support for ipv6 dns
248
-            return hostname
249
-
250
-        if hostname != unit_get('private-address'):
251
-            return get_host_ip(hostname, fallback=hostname)
252
-
253
-        # Otherwise assume localhost
254
-        return '127.0.0.1'
255
-
256
-    def get_allowed_units(self, database, username, relation_id=None):
257
-        """Get list of units with access grants for database with username.
258
-
259
-        This is typically used to provide shared-db relations with a list of
260
-        which units have been granted access to the given database.
261
-        """
262
-        self.connect(password=self.get_mysql_root_password())
263
-        allowed_units = set()
264
-        for unit in related_units(relation_id):
265
-            settings = relation_get(rid=relation_id, unit=unit)
266
-            # First check for setting with prefix, then without
267
-            for attr in ["%s_hostname" % (database), 'hostname']:
268
-                hosts = settings.get(attr, None)
269
-                if hosts:
270
-                    break
271
-
272
-            if hosts:
273
-                # hostname can be json-encoded list of hostnames
274
-                try:
275
-                    hosts = json.loads(hosts)
276
-                except ValueError:
277
-                    hosts = [hosts]
278
-            else:
279
-                hosts = [settings['private-address']]
280
-
281
-            if hosts:
282
-                for host in hosts:
283
-                    host = self.normalize_address(host)
284
-                    if self.grant_exists(database, username, host):
285
-                        log("Grant exists for host '%s' on db '%s'" %
286
-                            (host, database), level=DEBUG)
287
-                        if unit not in allowed_units:
288
-                            allowed_units.add(unit)
289
-                    else:
290
-                        log("Grant does NOT exist for host '%s' on db '%s'" %
291
-                            (host, database), level=DEBUG)
292
-            else:
293
-                log("No hosts found for grant check", level=INFO)
294
-
295
-        return allowed_units
296
-
297
-    def configure_db(self, hostname, database, username, admin=False):
298
-        """Configure access to database for username from hostname."""
299
-        self.connect(password=self.get_mysql_root_password())
300
-        if not self.database_exists(database):
301
-            self.create_database(database)
302
-
303
-        remote_ip = self.normalize_address(hostname)
304
-        password = self.get_mysql_password(username)
305
-        if not self.grant_exists(database, username, remote_ip):
306
-            if not admin:
307
-                self.create_grant(database, username, remote_ip, password)
308
-            else:
309
-                self.create_admin_grant(username, remote_ip, password)
310
-
311
-        return password
312
-
313
-
314
-class PerconaClusterHelper(object):
315
-
316
-    # Going for the biggest page size to avoid wasted bytes.
317
-    # InnoDB page size is 16MB
318
-
319
-    DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
320
-    DEFAULT_INNODB_BUFFER_FACTOR = 0.50
321
-
322
-    def human_to_bytes(self, human):
323
-        """Convert human readable configuration options to bytes."""
324
-        num_re = re.compile('^[0-9]+$')
325
-        if num_re.match(human):
326
-            return human
327
-
328
-        factors = {
329
-            'K': 1024,
330
-            'M': 1048576,
331
-            'G': 1073741824,
332
-            'T': 1099511627776
333
-        }
334
-        modifier = human[-1]
335
-        if modifier in factors:
336
-            return int(human[:-1]) * factors[modifier]
337
-
338
-        if modifier == '%':
339
-            total_ram = self.human_to_bytes(self.get_mem_total())
340
-            if self.is_32bit_system() and total_ram > self.sys_mem_limit():
341
-                total_ram = self.sys_mem_limit()
342
-            factor = int(human[:-1]) * 0.01
343
-            pctram = total_ram * factor
344
-            return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
345
-
346
-        raise ValueError("Can only convert K,M,G, or T")
347
-
348
-    def is_32bit_system(self):
349
-        """Determine whether system is 32 or 64 bit."""
350
-        try:
351
-            return sys.maxsize < 2 ** 32
352
-        except OverflowError:
353
-            return False
354
-
355
-    def sys_mem_limit(self):
356
-        """Determine the default memory limit for the current service unit."""
357
-        if platform.machine() in ['armv7l']:
358
-            _mem_limit = self.human_to_bytes('2700M')  # experimentally determined
359
-        else:
360
-            # Limit for x86 based 32bit systems
361
-            _mem_limit = self.human_to_bytes('4G')
362
-
363
-        return _mem_limit
364
-
365
-    def get_mem_total(self):
366
-        """Calculate the total memory in the current service unit."""
367
-        with open('/proc/meminfo') as meminfo_file:
368
-            for line in meminfo_file:
369
-                key, mem = line.split(':', 2)
370
-                if key == 'MemTotal':
371
-                    mtot, modifier = mem.strip().split(' ')
372
-                    return '%s%s' % (mtot, modifier[0].upper())
373
-
374
-    def parse_config(self):
375
-        """Parse charm configuration and calculate values for config files."""
376
-        config = config_get()
377
-        mysql_config = {}
378
-        if 'max-connections' in config:
379
-            mysql_config['max_connections'] = config['max-connections']
380
-
381
-        if 'wait-timeout' in config:
382
-            mysql_config['wait_timeout'] = config['wait-timeout']
383
-
384
-        if 'innodb-flush-log-at-trx-commit' in config:
385
-            mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit']
386
-
387
-        # Set a sane default key_buffer size
388
-        mysql_config['key_buffer'] = self.human_to_bytes('32M')
389
-        total_memory = self.human_to_bytes(self.get_mem_total())
390
-
391
-        dataset_bytes = config.get('dataset-size', None)
392
-        innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
393
-
394
-        if innodb_buffer_pool_size:
395
-            innodb_buffer_pool_size = self.human_to_bytes(
396
-                innodb_buffer_pool_size)
397
-        elif dataset_bytes:
398
-            log("Option 'dataset-size' has been deprecated, please use"
399
-                "innodb_buffer_pool_size option instead", level="WARN")
400
-            innodb_buffer_pool_size = self.human_to_bytes(
401
-                dataset_bytes)
402
-        else:
403
-            innodb_buffer_pool_size = int(
404
-                total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR)
405
-
406
-        if innodb_buffer_pool_size > total_memory:
407
-            log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
408
-                innodb_buffer_pool_size,
409
-                total_memory), level='WARN')
410
-
411
-        mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
412
-        return mysql_config

+ 0
- 15
hooks/charmhelpers/contrib/hardening/__init__.py View File

@@ -1,15 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.

+ 0
- 19
hooks/charmhelpers/contrib/hardening/apache/__init__.py View File

@@ -1,19 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-from os import path
18
-
19
-TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

+ 0
- 31
hooks/charmhelpers/contrib/hardening/apache/checks/__init__.py View File

@@ -1,31 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-from charmhelpers.core.hookenv import (
18
-    log,
19
-    DEBUG,
20
-)
21
-from charmhelpers.contrib.hardening.apache.checks import config
22
-
23
-
24
-def run_apache_checks():
25
-    log("Starting Apache hardening checks.", level=DEBUG)
26
-    checks = config.get_audits()
27
-    for check in checks:
28
-        log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
29
-        check.ensure_compliance()
30
-
31
-    log("Apache hardening checks complete.", level=DEBUG)

+ 0
- 100
hooks/charmhelpers/contrib/hardening/apache/checks/config.py View File

@@ -1,100 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-import os
18
-import re
19
-import subprocess
20
-
21
-
22
-from charmhelpers.core.hookenv import (
23
-    log,
24
-    INFO,
25
-)
26
-from charmhelpers.contrib.hardening.audits.file import (
27
-    FilePermissionAudit,
28
-    DirectoryPermissionAudit,
29
-    NoReadWriteForOther,
30
-    TemplatedFile,
31
-)
32
-from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
33
-from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
34
-from charmhelpers.contrib.hardening import utils
35
-
36
-
37
-def get_audits():
38
-    """Get Apache hardening config audits.
39
-
40
-    :returns:  dictionary of audits
41
-    """
42
-    if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
43
-        log("Apache server does not appear to be installed on this node - "
44
-            "skipping apache hardening", level=INFO)
45
-        return []
46
-
47
-    context = ApacheConfContext()
48
-    settings = utils.get_settings('apache')
49
-    audits = [
50
-        FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root',
51
-                            group='root', mode=0o0640),
52
-
53
-        TemplatedFile(os.path.join(settings['common']['apache_dir'],
54
-                                   'mods-available/alias.conf'),
55
-                      context,
56
-                      TEMPLATES_DIR,
57
-                      mode=0o0755,
58
-                      user='root',
59
-                      service_actions=[{'service': 'apache2',
60
-                                        'actions': ['restart']}]),
61
-
62
-        TemplatedFile(os.path.join(settings['common']['apache_dir'],
63
-                                   'conf-enabled/hardening.conf'),
64
-                      context,
65
-                      TEMPLATES_DIR,
66
-                      mode=0o0640,
67
-                      user='root',
68
-                      service_actions=[{'service': 'apache2',
69
-                                        'actions': ['restart']}]),
70
-
71
-        DirectoryPermissionAudit(settings['common']['apache_dir'],
72
-                                 user='root',
73
-                                 group='root',
74
-                                 mode=0o640),
75
-
76
-        DisabledModuleAudit(settings['hardening']['modules_to_disable']),
77
-
78
-        NoReadWriteForOther(settings['common']['apache_dir']),
79
-    ]
80
-
81
-    return audits
82
-
83
-
84
-class ApacheConfContext(object):
85
-    """Defines the set of key/value pairs to set in a apache config file.
86
-
87
-    This context, when called, will return a dictionary containing the
88
-    key/value pairs of setting to specify in the
89
-    /etc/apache/conf-enabled/hardening.conf file.
90
-    """
91
-    def __call__(self):
92
-        settings = utils.get_settings('apache')
93
-        ctxt = settings['hardening']
94
-
95
-        out = subprocess.check_output(['apache2', '-v'])
96
-        ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
97
-                                           out).group(1)
98
-        ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
99
-        ctxt['traceenable'] = settings['hardening']['traceenable']
100
-        return ctxt

+ 0
- 63
hooks/charmhelpers/contrib/hardening/audits/__init__.py View File

@@ -1,63 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-
18
-class BaseAudit(object):  # NO-QA
19
-    """Base class for hardening checks.
20
-
21
-    The lifecycle of a hardening check is to first check to see if the system
22
-    is in compliance for the specified check. If it is not in compliance, the
23
-    check method will return a value which will be supplied to the.
24
-    """
25
-    def __init__(self, *args, **kwargs):
26
-        self.unless = kwargs.get('unless', None)
27
-        super(BaseAudit, self).__init__()
28
-
29
-    def ensure_compliance(self):
30
-        """Checks to see if the current hardening check is in compliance or
31
-        not.
32
-
33
-        If the check that is performed is not in compliance, then an exception
34
-        should be raised.
35
-        """
36
-        pass
37
-
38
-    def _take_action(self):
39
-        """Determines whether to perform the action or not.
40
-
41
-        Checks whether or not an action should be taken. This is determined by
42
-        the truthy value for the unless parameter. If unless is a callback
43
-        method, it will be invoked with no parameters in order to determine
44
-        whether or not the action should be taken. Otherwise, the truthy value
45
-        of the unless attribute will determine if the action should be
46
-        performed.
47
-        """
48
-        # Do the action if there isn't an unless override.
49
-        if self.unless is None:
50
-            return True
51
-
52
-        # Invoke the callback if there is one.
53
-        if hasattr(self.unless, '__call__'):
54
-            results = self.unless()
55
-            if results:
56
-                return False
57
-            else:
58
-                return True
59
-
60
-        if self.unless:
61
-            return False
62
-        else:
63
-            return True

+ 0
- 100
hooks/charmhelpers/contrib/hardening/audits/apache.py View File

@@ -1,100 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-import re
18
-import subprocess
19
-
20
-from six import string_types
21
-
22
-from charmhelpers.core.hookenv import (
23
-    log,
24
-    INFO,
25
-    ERROR,
26
-)
27
-
28
-from charmhelpers.contrib.hardening.audits import BaseAudit
29
-
30
-
31
-class DisabledModuleAudit(BaseAudit):
32
-    """Audits Apache2 modules.
33
-
34
-    Determines if the apache2 modules are enabled. If the modules are enabled
35
-    then they are removed in the ensure_compliance.
36
-    """
37
-    def __init__(self, modules):
38
-        if modules is None:
39
-            self.modules = []
40
-        elif isinstance(modules, string_types):
41
-            self.modules = [modules]
42
-        else:
43
-            self.modules = modules
44
-
45
-    def ensure_compliance(self):
46
-        """Ensures that the modules are not loaded."""
47
-        if not self.modules:
48
-            return
49
-
50
-        try:
51
-            loaded_modules = self._get_loaded_modules()
52
-            non_compliant_modules = []
53
-            for module in self.modules:
54
-                if module in loaded_modules:
55
-                    log("Module '%s' is enabled but should not be." %
56
-                        (module), level=INFO)
57
-                    non_compliant_modules.append(module)
58
-
59
-            if len(non_compliant_modules) == 0:
60
-                return
61
-
62
-            for module in non_compliant_modules:
63
-                self._disable_module(module)
64
-            self._restart_apache()
65
-        except subprocess.CalledProcessError as e:
66
-            log('Error occurred auditing apache module compliance. '
67
-                'This may have been already reported. '
68
-                'Output is: %s' % e.output, level=ERROR)
69
-
70
-    @staticmethod
71
-    def _get_loaded_modules():
72
-        """Returns the modules which are enabled in Apache."""
73
-        output = subprocess.check_output(['apache2ctl', '-M'])
74
-        modules = []
75
-        for line in output.strip().split():
76
-            # Each line of the enabled module output looks like:
77
-            #  module_name (static|shared)
78
-            # Plus a header line at the top of the output which is stripped
79
-            # out by the regex.
80
-            matcher = re.search(r'^ (\S*)', line)
81
-            if matcher:
82
-                modules.append(matcher.group(1))
83
-        return modules
84
-
85
-    @staticmethod
86
-    def _disable_module(module):
87
-        """Disables the specified module in Apache."""
88
-        try:
89
-            subprocess.check_call(['a2dismod', module])
90
-        except subprocess.CalledProcessError as e:
91
-            # Note: catch error here to allow the attempt of disabling
92
-            # multiple modules in one go rather than failing after the
93
-            # first module fails.
94
-            log('Error occurred disabling module %s. '
95
-                'Output is: %s' % (module, e.output), level=ERROR)
96
-
97
-    @staticmethod
98
-    def _restart_apache():
99
-        """Restarts the apache process"""
100
-        subprocess.check_output(['service', 'apache2', 'restart'])

+ 0
- 105
hooks/charmhelpers/contrib/hardening/audits/apt.py View File

@@ -1,105 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-from __future__ import absolute_import  # required for external apt import
18
-from apt import apt_pkg
19
-from six import string_types
20
-
21
-from charmhelpers.fetch import (
22
-    apt_cache,
23
-    apt_purge
24
-)
25
-from charmhelpers.core.hookenv import (
26
-    log,
27
-    DEBUG,
28
-    WARNING,
29
-)
30
-from charmhelpers.contrib.hardening.audits import BaseAudit
31
-
32
-
33
-class AptConfig(BaseAudit):
34
-
35
-    def __init__(self, config, **kwargs):
36
-        self.config = config
37
-
38
-    def verify_config(self):
39
-        apt_pkg.init()
40
-        for cfg in self.config:
41
-            value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
42
-            if value and value != cfg['expected']:
43
-                log("APT config '%s' has unexpected value '%s' "
44
-                    "(expected='%s')" %
45
-                    (cfg['key'], value, cfg['expected']), level=WARNING)
46
-
47
-    def ensure_compliance(self):
48
-        self.verify_config()
49
-
50
-
51
-class RestrictedPackages(BaseAudit):
52
-    """Class used to audit restricted packages on the system."""
53
-
54
-    def __init__(self, pkgs, **kwargs):
55
-        super(RestrictedPackages, self).__init__(**kwargs)
56
-        if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
57
-            self.pkgs = [pkgs]
58
-        else:
59
-            self.pkgs = pkgs
60
-
61
-    def ensure_compliance(self):
62
-        cache = apt_cache()
63
-
64
-        for p in self.pkgs:
65
-            if p not in cache:
66
-                continue
67
-
68
-            pkg = cache[p]
69
-            if not self.is_virtual_package(pkg):
70
-                if not pkg.current_ver:
71
-                    log("Package '%s' is not installed." % pkg.name,
72
-                        level=DEBUG)
73
-                    continue
74
-                else:
75
-                    log("Restricted package '%s' is installed" % pkg.name,
76
-                        level=WARNING)
77
-                    self.delete_package(cache, pkg)
78
-            else:
79
-                log("Checking restricted virtual package '%s' provides" %
80
-                    pkg.name, level=DEBUG)
81
-                self.delete_package(cache, pkg)
82
-
83
-    def delete_package(self, cache, pkg):
84
-        """Deletes the package from the system.
85
-
86
-        Deletes the package form the system, properly handling virtual
87
-        packages.
88
-
89
-        :param cache: the apt cache
90
-        :param pkg: the package to remove
91
-        """
92
-        if self.is_virtual_package(pkg):
93
-            log("Package '%s' appears to be virtual - purging provides" %
94
-                pkg.name, level=DEBUG)
95
-            for _p in pkg.provides_list:
96
-                self.delete_package(cache, _p[2].parent_pkg)
97
-        elif not pkg.current_ver:
98
-            log("Package '%s' not installed" % pkg.name, level=DEBUG)
99
-            return
100
-        else:
101
-            log("Purging package '%s'" % pkg.name, level=DEBUG)
102
-            apt_purge(pkg.name)
103
-
104
-    def is_virtual_package(self, pkg):
105
-        return pkg.has_provides and not pkg.has_versions

+ 0
- 552
hooks/charmhelpers/contrib/hardening/audits/file.py View File

@@ -1,552 +0,0 @@
1
-# Copyright 2016 Canonical Limited.
2
-#
3
-# This file is part of charm-helpers.
4
-#
5
-# charm-helpers is free software: you can redistribute it and/or modify
6
-# it under the terms of the GNU Lesser General Public License version 3 as
7
-# published by the Free Software Foundation.
8
-#
9
-# charm-helpers is distributed in the hope that it will be useful,
10
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
-# GNU Lesser General Public License for more details.
13
-#
14
-# You should have received a copy of the GNU Lesser General Public License
15
-# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
-
17
-import grp
18
-import os
19
-import pwd
20
-import re
21
-
22
-from subprocess import (
23
-    CalledProcessError,
24
-    check_output,
25
-    check_call,
26
-)
27
-from traceback import format_exc
28
-from six import string_types
29
-from stat import (
30
-    S_ISGID,
31
-    S_ISUID
32
-)
33
-
34
-from charmhelpers.core.hookenv import (
35
-    log,
36
-    DEBUG,
37
-    INFO,
38
-    WARNING,
39
-    ERROR,
40
-)
41
-from charmhelpers.core import unitdata
42
-from charmhelpers.core.host import file_hash
43
-from charmhelpers.contrib.hardening.audits import BaseAudit
44
-from charmhelpers.contrib.hardening.templating import (
45
-    get_template_path,
46
-    render_and_write,
47
-)
48
-from charmhelpers.contrib.hardening import utils
49
-
50
-
51
-class BaseFileAudit(BaseAudit):
52
-    """Base class for file audits.
53
-
54
-    Provides api stubs for compliance check flow that must be used by any class
55
-    that implemented this one.
56
-    """
57
-
58
-    def __init__(self, paths, always_comply=False, *args, **kwargs):
59
-        """
60
-        :param paths: string path of list of paths of files we want to apply
61
-                      compliance checks are criteria to.
62
-        :param always_comply: if true compliance criteria is always applied
63
-                              else compliance is skipped for non-existent
64
-                              paths.
65
-        """
66
-        super(BaseFileAudit, self).__init__(*args, **kwargs)
67
-        self.always_comply = always_comply
68
-        if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
69
-            self.paths = [paths]
70
-        else:
71
-            self.paths = paths
72
-
73
-    def ensure_compliance(self):
74
-        """Ensure that the all registered files comply to registered criteria.
75
-        """
76
-        for p in self.paths:
77
-            if os.path.exists(p):
78
-                if self.is_compliant(p):
79
-                    continue
80
-
81
-                log('File %s is not in compliance.' % p, level=INFO)
82
-            else:
83
-                if not self.always_comply:
84
-                    log("Non-existent path '%s' - skipping compliance check"
85
-                        % (p), level=INFO)
86
-                    continue
87
-
88
-            if self._take_action():
89
-                log("Applying compliance criteria to '%s'" % (p), level=INFO)
90
-                self.comply(p)
91
-
92
-    def is_compliant(self, path):
93
-        """Audits the path to see if it is compliance.
94
-
95
-        :param path: the path to the file that should be checked.
96
-        """
97
-        raise NotImplementedError
98
-
99
-    def comply(self, path):
100
-        """Enforces the compliance of a path.
101
-
102
-        :param path: the path to the file that should be enforced.
103
-        """
104
-        raise NotImplementedError
105
-
106
-    @classmethod
107
-    def _get_stat(cls, path):
108
-        """Returns the Posix st_stat information for the specified file path.
109
-
110
-        :param path: the path to get the st_stat information for.
111
-        :returns: an st_stat object for the path or None if the path doesn't
112
-                  exist.
113
-        """
114
-        return os.stat(path)
115
-
116
-
117
-class FilePermissionAudit(BaseFileAudit):
118
-    """Implements an audit for file permissions and ownership for a user.
119
-
120
-    This class implements functionality that ensures that a specific user/group
121
-    will own the file(s) specified and that the permissions specified are
122
-    applied properly to the file.
123
-    """
124
-    def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
125
-        self.user = user
126
-        self.group = group
127
-        self.mode = mode
128
-        super(FilePermissionAudit, self).__init__(paths, user, group, mode,
129
-                                                  **kwargs)
130
-
131
-    @property
132
-    def user(self):
133
-        return self._user
134
-
135
-    @user.setter
136
-    def user(self, name):
137
-        try:
138
-            user = pwd.getpwnam(name)
139
-        except KeyError:
140
-            log('Unknown user %s' % name, level=ERROR)
141
-            user = None
142
-        self._user = user
143
-
144
-    @property
145
-    def group(self):
146
-        return self._group
147
-
148
-    @group.setter
149
-    def group(self, name):
150
-        try:
151
-            group = None
152
-            if name:
153
-                group = grp.getgrnam(name)
154
-            else:
155
-                group = grp.getgrgid(self.user.pw_gid)
156
-        except KeyError:
157
-            log('Unknown group %s' % name, level=ERROR)
158
-        self._group = group
159
-
160
-    def is_compliant(self, path):
161
-        """Checks if the path is in compliance.
162
-
163
-        Used to determine if the path specified meets the necessary
164
-        requirements to be in compliance with the check itself.
165
-
166
-        :param path: the file path to check
167
-        :returns: True if the path is compliant, False otherwise.
168
-        """
169
-        stat = self._get_stat(path)
170
-        user = self.user
171
-        group = self.group
172
-
173
-        compliant = True
174
-        if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
175
-            log('File %s is not owned by %s:%s.' % (path, user.pw_name,
176
-                                                    group.gr_name),
177
-                level=INFO)
178
-            compliant = False
179
-
180
-        # POSIX refers to the st_mode bits as corresponding to both the
181
-        # file type and file permission bits, where the least significant 12
182
-        # bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
183
-        # file permission bits (8-0)
184
-        perms = stat.st_mode & 0o7777
185
-        if perms != self.mode:
186
-            log('File %s has incorrect permissions, currently set to %s' %
187
-                (path, oct(stat.st_mode & 0o7777)), level=INFO)
188
-            compliant = False
189
-
190
-        return compliant
191
-
192
-    def comply(self, path):
193
-        """Issues a chown and chmod to the file paths specified."""
194
-        utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
195
-                                 self.mode)
196
-
197
-
198
-class DirectoryPermissionAudit(FilePermissionAudit):
199
-    """Performs a permission check for the  specified directory path."""
200
-
201
-    def __init__(self, paths, user, group=None, mode=0o600,
202
-                 recursive=True, **kwargs):
203
-        super(DirectoryPermissionAudit, self).__init__(paths, user, group,
204
-                                                       mode, **kwargs)
205
-        self.recursive = recursive
206
-
207
-    def is_compliant(self, path):
208
-        """Checks if the directory is compliant.
209
-
210
-        Used to determine if the path specified and all of its children
211
-        directories are in compliance with the check itself.
212
-
213
-        :param path: the directory path to check
214
-        :returns: True if the directory tree is compliant, otherwise False.
215
-        """
216
-        if not os.path.isdir(path):
217
-            log('Path specified %s is not a directory.' % path, level=ERROR)
218
-            raise ValueError("%s is not a directory." % path)
219
-
220
-        if not self.recursive:
221
-            return super(DirectoryPermissionAudit, self).is_compliant(path)
222
-
223
-        compliant = True
224
-        for root, dirs, _ in os.walk(path):
225
-            if len(dirs) > 0:
226
-                continue
227
-
228
-            if not super(DirectoryPermissionAudit, self).is_compliant(root):
229
-                compliant = False
230
-                continue
231
-
232
-        return compliant
233
-
234
-    def comply(self, path):
235
-        for root, dirs, _ in os.walk(path):
236
-            if len(dirs) > 0:
237
-                super(DirectoryPermissionAudit, self).comply(root)
238
-
239
-
240
-class ReadOnly(BaseFileAudit):
241
-    """Audits that files and folders are read only."""
242
-    def __init__(self, paths, *args, **kwargs):
243
-        super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
244
-
245
-    def is_compliant(self, path):
246
-        try:
247
-            output = check_output(['find', path, '-perm', '-go+w',
248
-                                   '-type', 'f']).strip()
249
-
250
-            # The find above will find any files which have permission sets
251
-            # which allow too broad of write access. As such, the path is
252
-            # compliant if there is no output.
253
-            if output:
254
-                return False
255
-
256
-            return True
257
-        except CalledProcessError as e:
258
-            log('Error occurred checking finding writable files for %s. '
259
-                'Error information is: command %s failed with returncode '
260
-                '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
261
-                                           format_exc(e)), level=ERROR)
262
-            return False
263
-
264
-    def comply(self, path):
265
-        try:
266
-            check_output(['chmod', 'go-w', '-R', path])
267
-        except CalledProcessError as e:
268
-            log('Error occurred removing writeable permissions for %s. '
269
-                'Error information is: command %s failed with returncode '
270
-                '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
271
-                                           format_exc(e)), level=ERROR)
272
-
273
-
274
-class NoReadWriteForOther(BaseFileAudit):
275
-    """Ensures that the files found under the base path are readable or
276
-    writable by anyone other than the owner or the group.
277
-    """
278
-    def __init__(self, paths):
279
-        super(NoReadWriteForOther, self).__init__(paths)
280
-
281
-    def is_compliant(self, path):
282
-        try:
283
-            cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
284
-                   '-perm', '-o+w', '-type', 'f']
285
-            output = check_output(cmd).strip()
286
-
287
-            # The find above here will find any files which have read or
288
-            # write permissions for other, meaning there is too broad of access
289
-            # to read/write the file. As such, the path is compliant if there's
290
-            # no output.
291
-            if output:
292
-                return False
293
-
294
-            return True
295
-        except CalledProcessError as e:
296
-            log('Error occurred while finding files which are readable or '
297
-                'writable to the world in %s. '
298
-                'Command output is: %s.' % (path, e.output), level=ERROR)
299
-
300
-    def comply(self, path):
301
-        try:
302
-            check_output(['chmod', '-R', 'o-rw', path])
303
-        except CalledProcessError as e:
304
-            log('Error occurred attempting to change modes of files under '
305
-                'path %s. Output of command is: %s' % (path, e.output))
306
-
307
-
308
-class NoSUIDSGIDAudit(BaseFileAudit):
309
-    """Audits that specified files do not have SUID/SGID bits set."""
310
-    def __init__(self, paths, *args, **kwargs):
311
-        super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
312
-
313
-    def is_compliant(self, path):
314
-        stat = self._get_stat(path)
315
-        if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
316
-            return False
317
-
318
-        return True
319
-
320
-    def comply(self, path):
321
-        try:
322
-            log('Removing suid/sgid from %s.' % path, level=DEBUG)
323
-            check_output(['chmod', '-s', path])
324
-        except CalledProcessError as e:
325
-            log('Error occurred removing suid/sgid from %s.'
326
-                'Error information is: command %s failed with returncode '
327
-                '%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
328
-                                           format_exc(e)), level=ERROR)
329
-
330
-
331
-class TemplatedFile(BaseFileAudit):
332
-    """The TemplatedFileAudit audits the contents of a templated file.
333
-
334
-    This audit renders a file from a template, sets the appropriate file
335
-    permissions, then generates a hashsum with which to check the content
336
-    changed.
337
-    """
338
-    def __init__(self, path, context, template_dir, mode, user='root',
339
-                 group='root', service_actions=None, **kwargs):
340
-        self.context = context
341
-        self.user = user
342
-        self.group = group
343
-        self.mode = mode
344
-        self.template_dir = template_dir
345
-        self.service_actions = service_actions
346
-        super(TemplatedFile, self).__init__(paths=path, always_comply=True,
347
-                                            **kwargs)
348
-
349
-    def is_compliant(self, path):
350
-        """Determines if the templated file is compliant.
351
-
352
-        A templated file is only compliant if it has not changed (as
353
-        determined by its sha256 hashsum) AND its file permissions are set
354
-        appropriately.
355
-
356
-        :param path: the path to check compliance.
357
-        """
358
-        same_templates = self.templates_match(path)
359
-        same_content = self.contents_match(path)
360
-        same_permissions = self.permissions_match(path)
361
-
362
-        if same_content and same_permissions and same_templates:
363
-            return True
364
-
365
-        return False
366
-
367
-    def run_service_actions(self):
368
-        """Run any actions on services requested."""
369
-        if not self.service_actions:
370
-            return
371
-
372
-        for svc_action in self.service_actions:
373
-            name = svc_action['service']
374
-            actions = svc_action['actions']
375
-            log("Running service '%s' actions '%s'" % (name, actions),
376
-                level=DEBUG)
377
-            for action in actions:
378
-                cmd = ['service', name, action]
379
-                try:
380
-                    check_call(cmd)
381
-                except CalledProcessError as exc:
382
-                    log("Service name='%s' action='%s' failed - %s" %
383
-                        (name, action, exc), level=WARNING)
384
-
385
-    def comply(self, path):
386
-        """Ensures the contents and the permissions of the file.
387
-
388
-        :param path: the path to correct
389
-        """
390
-        dirname = os.path.dirname(path)
391
-        if not os.path.exists(dirname):
392
-            os.makedirs(dirname)
393
-
394
-        self.pre_write()
395
-        render_and_write(self.template_dir, path, self.context())
396
-        utils.ensure_permissions(path, self.user, self.group, self.mode)
397
-        self.run_service_actions()
398
-        self.save_checksum(path)
399
-        self.post_write()
400
-
401
-    def pre_write(self):
402
-        """Invoked prior to writing the template."""
403
-        pass
404
-
405
-    def post_write(self):
406
-        """Invoked after writing the template."""
407
-        pass
408
-
409
-    def templates_match(self, path):
410
-        """Determines if the template files are the same.
411
-
412
-        The template file equality is determined by the hashsum of the
413
-        template files themselves. If there is no hashsum, then the content
414
-        cannot be sure to be the same so treat it as if they changed.
415
-        Otherwise, return whether or not the hashsums are the same.
416
-
417
-        :param path: the path to check
418
-        :returns: boolean