aboutsummaryrefslogtreecommitdiffstats
path: root/charms/trusty/ceilometer/charmhelpers/contrib/hardening/utils.py
blob: a6743a4de9b6449f7a236a05574512e32af38600 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# Copyright 2016 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.

import glob
import grp
import os
import pwd
import six
import yaml

from charmhelpers.core.hookenv import (
    log,
    DEBUG,
    INFO,
    WARNING,
    ERROR,
)


# Global settings cache. Since each hook fire entails a fresh module import it
# is safe to hold this in memory and not risk missing config changes (since
# they will result in a new hook fire and thus re-import).
__SETTINGS__ = {}


def _get_defaults(modules):
    """Load the default config for the provided modules.

    :param modules: stack modules config defaults to lookup.
    :returns: modules default config dictionary.
    """
    default = os.path.join(os.path.dirname(__file__),
                           'defaults/%s.yaml' % (modules))
    return yaml.safe_load(open(default))


def _get_schema(modules):
    """Load the config schema for the provided modules.

    NOTE: this schema is intended to have 1-1 relationship with they keys in
    the default config and is used a means to verify valid overrides provided
    by the user.

    :param modules: stack modules config schema to lookup.
    :returns: modules default schema dictionary.
    """
    schema = os.path.join(os.path.dirname(__file__),
                          'defaults/%s.yaml.schema' % (modules))
    return yaml.safe_load(open(schema))


def _get_user_provided_overrides(modules):
    """Load user-provided config overrides.

    :param modules: stack modules to lookup in user overrides yaml file.
    :returns: overrides dictionary.
    """
    overrides = os.path.join(os.environ['JUJU_CHARM_DIR'],
                             'hardening.yaml')
    if os.path.exists(overrides):
        log("Found user-provided config overrides file '%s'" %
            (overrides), level=DEBUG)
        settings = yaml.safe_load(open(overrides))
        if settings and settings.get(modules):
            log("Applying '%s' overrides" % (modules), level=DEBUG)
            return settings.get(modules)

        log("No overrides found for '%s'" % (modules), level=DEBUG)
    else:
        log("No hardening config overrides file '%s' found in charm "
            "root dir" % (overrides), level=DEBUG)

    return {}


def _apply_overrides(settings, overrides, schema):
    """Get overrides config overlayed onto modules defaults.

    :param modules: require stack modules config.
    :returns: dictionary of modules config with user overrides applied.
    """
    if overrides:
        for k, v in six.iteritems(overrides):
            if k in schema:
                if schema[k] is None:
                    settings[k] = v
                elif type(schema[k]) is dict:
                    settings[k] = _apply_overrides(settings[k], overrides[k],
                                                   schema[k])
                else:
                    raise Exception("Unexpected type found in schema '%s'" %
                                    type(schema[k]), level=ERROR)
            else:
                log("Unknown override key '%s' - ignoring" % (k), level=INFO)

    return settings


def get_settings(modules):
    global __SETTINGS__
    if modules in __SETTINGS__:
        return __SETTINGS__[modules]

    schema = _get_schema(modules)
    settings = _get_defaults(modules)
    overrides = _get_user_provided_overrides(modules)
    __SETTINGS__[modules] = _apply_overrides(settings, overrides, schema)
    return __SETTINGS__[modules]


def ensure_permissions(path, user, group, permissions, maxdepth=-1):
    """Ensure permissions for path.

    If path is a file, apply to file and return. If path is a directory,
    apply recursively (if required) to directory contents and return.

    :param user: user name
    :param group: group name
    :param permissions: octal permissions
    :param maxdepth: maximum recursion depth. A negative maxdepth allows
                     infinite recursion and maxdepth=0 means no recursion.
    :returns: None
    """
    if not os.path.exists(path):
        log("File '%s' does not exist - cannot set permissions" % (path),
            level=WARNING)
        return

    _user = pwd.getpwnam(user)
    os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid)
    os.chmod(path, permissions)

    if maxdepth == 0:
        log("Max recursion depth reached - skipping further recursion",
            level=DEBUG)
        return
    elif maxdepth > 0:
        maxdepth -= 1

    if os.path.isdir(path):
        contents = glob.glob("%s/*" % (path))
        for c in contents:
            ensure_permissions(c, user=user, group=group,
                               permissions=permissions, maxdepth=maxdepth)