summaryrefslogtreecommitdiffstats
path: root/roles/openshift_logging/library/openshift_logging_facts.py
blob: f810ae39c6261e0b601d7483ab3bb5f9f556c4aa (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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
'''
---
module: openshift_logging_facts
version_added: ""
short_description: Gather facts about the OpenShift logging stack
description:
  - Determine the current facts about the OpenShift logging stack (e.g. cluster size)
options:
author: Red Hat, Inc
'''

import copy
import json

# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
from subprocess import *   # noqa: F402,F403

# ignore pylint errors related to the module_utils import
# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
from ansible.module_utils.basic import *   # noqa: F402,F403

import yaml

EXAMPLES = """
- action: opneshift_logging_facts
"""

RETURN = """
"""

DEFAULT_OC_OPTIONS = ["-o", "json"]

# constants used for various labels and selectors
COMPONENT_KEY = "component"
LOGGING_INFRA_KEY = "logging-infra"

# selectors for filtering resources
DS_FLUENTD_SELECTOR = LOGGING_INFRA_KEY + "=" + "fluentd"
LOGGING_SELECTOR = LOGGING_INFRA_KEY + "=" + "support"
ROUTE_SELECTOR = "component=support,logging-infra=support,provider=openshift"
COMPONENTS = ["kibana", "curator", "elasticsearch", "fluentd", "kibana_ops", "curator_ops", "elasticsearch_ops"]


class OCBaseCommand(object):
    ''' The base class used to query openshift '''

    def __init__(self, binary, kubeconfig, namespace):
        ''' the init method of OCBaseCommand class '''
        self.binary = binary
        self.kubeconfig = kubeconfig
        self.user = self.get_system_admin(self.kubeconfig)
        self.namespace = namespace

    # pylint: disable=no-self-use
    def get_system_admin(self, kubeconfig):
        ''' Retrieves the system admin '''
        with open(kubeconfig, 'r') as kubeconfig_file:
            config = yaml.load(kubeconfig_file)
            for user in config["users"]:
                if user["name"].startswith("system:admin"):
                    return user["name"]
        raise Exception("Unable to find system:admin in: " + kubeconfig)

    # pylint: disable=too-many-arguments, dangerous-default-value
    def oc_command(self, sub, kind, namespace=None, name=None, add_options=None):
        ''' Wrapper method for the "oc" command '''
        cmd = [self.binary, sub, kind]
        if name is not None:
            cmd = cmd + [name]
        if namespace is not None:
            cmd = cmd + ["-n", namespace]
        if add_options is None:
            add_options = []
        cmd = cmd + ["--user=" + self.user, "--config=" + self.kubeconfig] + DEFAULT_OC_OPTIONS + add_options
        try:
            process = Popen(cmd, stdout=PIPE, stderr=PIPE)   # noqa: F405
            out, err = process.communicate(cmd)
            if len(err) > 0:
                if 'not found' in err:
                    return {'items': []}
                if 'No resources found' in err:
                    return {'items': []}
                raise Exception(err)
        except Exception as excp:
            err = "There was an exception trying to run the command '" + " ".join(cmd) + "' " + str(excp)
            raise Exception(err)

        return json.loads(out)


class OpenshiftLoggingFacts(OCBaseCommand):
    ''' The class structure for holding the OpenshiftLogging Facts'''
    name = "facts"

    def __init__(self, logger, binary, kubeconfig, namespace):
        ''' The init method for OpenshiftLoggingFacts '''
        super(OpenshiftLoggingFacts, self).__init__(binary, kubeconfig, namespace)
        self.logger = logger
        self.facts = dict()

    def default_keys_for(self, kind):
        ''' Sets the default key values for kind '''
        for comp in COMPONENTS:
            self.add_facts_for(comp, kind)

    def add_facts_for(self, comp, kind, name=None, facts=None):
        ''' Add facts for the provided kind '''
        if comp not in self.facts:
            self.facts[comp] = dict()
        if kind not in self.facts[comp]:
            self.facts[comp][kind] = dict()
        if name:
            self.facts[comp][kind][name] = facts

    def facts_for_routes(self, namespace):
        ''' Gathers facts for Routes in logging namespace '''
        self.default_keys_for("routes")
        route_list = self.oc_command("get", "routes", namespace=namespace, add_options=["-l", ROUTE_SELECTOR])
        if len(route_list["items"]) == 0:
            return None
        for route in route_list["items"]:
            name = route["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None:
                self.add_facts_for(comp, "routes", name, dict(host=route["spec"]["host"]))
        self.facts["agl_namespace"] = namespace

    def facts_for_daemonsets(self, namespace):
        ''' Gathers facts for Daemonsets in logging namespace '''
        self.default_keys_for("daemonsets")
        ds_list = self.oc_command("get", "daemonsets", namespace=namespace,
                                  add_options=["-l", LOGGING_INFRA_KEY + "=fluentd"])
        if len(ds_list["items"]) == 0:
            return
        for ds_item in ds_list["items"]:
            name = ds_item["metadata"]["name"]
            comp = self.comp(name)
            spec = ds_item["spec"]["template"]["spec"]
            container = spec["containers"][0]
            result = dict(
                selector=ds_item["spec"]["selector"],
                image=container["image"],
                resources=container["resources"],
                nodeSelector=spec["nodeSelector"],
                serviceAccount=spec["serviceAccount"],
                terminationGracePeriodSeconds=spec["terminationGracePeriodSeconds"]
            )
            self.add_facts_for(comp, "daemonsets", name, result)

    def facts_for_pvcs(self, namespace):
        ''' Gathers facts for PVCS in logging namespace'''
        self.default_keys_for("pvcs")
        pvclist = self.oc_command("get", "pvc", namespace=namespace, add_options=["-l", LOGGING_INFRA_KEY])
        if len(pvclist["items"]) == 0:
            return
        for pvc in pvclist["items"]:
            name = pvc["metadata"]["name"]
            comp = self.comp(name)
            self.add_facts_for(comp, "pvcs", name, dict())

    def facts_for_deploymentconfigs(self, namespace):
        ''' Gathers facts for DeploymentConfigs in logging namespace '''
        self.default_keys_for("deploymentconfigs")
        dclist = self.oc_command("get", "deploymentconfigs", namespace=namespace, add_options=["-l", LOGGING_INFRA_KEY])
        if len(dclist["items"]) == 0:
            return
        dcs = dclist["items"]
        for dc_item in dcs:
            name = dc_item["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None:
                spec = dc_item["spec"]["template"]["spec"]
                facts = dict(
                    name=name,
                    selector=dc_item["spec"]["selector"],
                    replicas=dc_item["spec"]["replicas"],
                    serviceAccount=spec["serviceAccount"],
                    containers=dict(),
                    volumes=dict()
                )
                if "nodeSelector" in spec:
                    facts["nodeSelector"] = spec["nodeSelector"]
                if "supplementalGroups" in spec["securityContext"]:
                    facts["storageGroups"] = spec["securityContext"]["supplementalGroups"]
                facts["spec"] = spec
                if "volumes" in spec:
                    for vol in spec["volumes"]:
                        clone = copy.deepcopy(vol)
                        clone.pop("name", None)
                        facts["volumes"][vol["name"]] = clone
                for container in spec["containers"]:
                    facts["containers"][container["name"]] = container
                self.add_facts_for(comp, "deploymentconfigs", name, facts)

    def facts_for_services(self, namespace):
        ''' Gathers facts for services in logging namespace '''
        self.default_keys_for("services")
        servicelist = self.oc_command("get", "services", namespace=namespace, add_options=["-l", LOGGING_SELECTOR])
        if len(servicelist["items"]) == 0:
            return
        for service in servicelist["items"]:
            name = service["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None:
                self.add_facts_for(comp, "services", name, dict())

    # pylint: disable=too-many-arguments
    def facts_from_configmap(self, comp, kind, name, config_key, yaml_file=None):
        '''Extracts facts in logging namespace from configmap'''
        if yaml_file is not None:
            config_facts = yaml.load(yaml_file)
            self.facts[comp][kind][name][config_key] = config_facts
            self.facts[comp][kind][name]["raw"] = yaml_file

    def facts_for_configmaps(self, namespace):
        ''' Gathers facts for configmaps in logging namespace '''
        self.default_keys_for("configmaps")
        a_list = self.oc_command("get", "configmaps", namespace=namespace)
        if len(a_list["items"]) == 0:
            return
        for item in a_list["items"]:
            name = item["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None:
                self.add_facts_for(comp, "configmaps", name, dict(item["data"]))
                if comp in ["elasticsearch", "elasticsearch_ops"]:
                    for config_key in item["data"]:
                        self.facts_from_configmap(comp, "configmaps", name, config_key, item["data"][config_key])

    def facts_for_oauthclients(self, namespace):
        ''' Gathers facts for oauthclients used with logging '''
        self.default_keys_for("oauthclients")
        a_list = self.oc_command("get", "oauthclients", namespace=namespace, add_options=["-l", LOGGING_SELECTOR])
        if len(a_list["items"]) == 0:
            return
        for item in a_list["items"]:
            name = item["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None:
                result = dict(
                    redirectURIs=item["redirectURIs"]
                )
                self.add_facts_for(comp, "oauthclients", name, result)

    def facts_for_secrets(self, namespace):
        ''' Gathers facts for secrets in the logging namespace '''
        self.default_keys_for("secrets")
        a_list = self.oc_command("get", "secrets", namespace=namespace)
        if len(a_list["items"]) == 0:
            return
        for item in a_list["items"]:
            name = item["metadata"]["name"]
            comp = self.comp(name)
            if comp is not None and item["type"] == "Opaque":
                result = dict(
                    keys=item["data"].keys()
                )
                self.add_facts_for(comp, "secrets", name, result)

    def facts_for_sccs(self):
        ''' Gathers facts for SCCs used with logging '''
        self.default_keys_for("sccs")
        scc = self.oc_command("get", "scc", name="privileged")
        if len(scc["users"]) == 0:
            return
        for item in scc["users"]:
            comp = self.comp(item)
            if comp is not None:
                self.add_facts_for(comp, "sccs", "privileged", dict())

    def facts_for_clusterrolebindings(self, namespace):
        ''' Gathers ClusterRoleBindings used with logging '''
        self.default_keys_for("clusterrolebindings")
        role = self.oc_command("get", "clusterrolebindings", name="cluster-readers")
        if "subjects" not in role or len(role["subjects"]) == 0:
            return
        for item in role["subjects"]:
            comp = self.comp(item["name"])
            if comp is not None and namespace == item["namespace"]:
                self.add_facts_for(comp, "clusterrolebindings", "cluster-readers", dict())

# this needs to end up nested under the service account...
    def facts_for_rolebindings(self, namespace):
        ''' Gathers facts for RoleBindings used with logging '''
        self.default_keys_for("rolebindings")
        role = self.oc_command("get", "rolebindings", namespace=namespace, name="logging-elasticsearch-view-role")
        if "subjects" not in role or len(role["subjects"]) == 0:
            return
        for item in role["subjects"]:
            comp = self.comp(item["name"])
            if comp is not None and namespace == item.get("namespace"):
                self.add_facts_for(comp, "rolebindings", "logging-elasticsearch-view-role", dict())

    # pylint: disable=no-self-use, too-many-return-statements
    def comp(self, name):
        ''' Does a comparison to evaluate the logging component '''
        if name.startswith("logging-curator-ops"):
            return "curator_ops"
        elif name.startswith("logging-kibana-ops") or name.startswith("kibana-ops"):
            return "kibana_ops"
        elif name.startswith("logging-es-ops") or name.startswith("logging-elasticsearch-ops"):
            return "elasticsearch_ops"
        elif name.startswith("logging-curator"):
            return "curator"
        elif name.startswith("logging-kibana") or name.startswith("kibana"):
            return "kibana"
        elif name.startswith("logging-es") or name.startswith("logging-elasticsearch"):
            return "elasticsearch"
        elif name.startswith("logging-fluentd") or name.endswith("aggregated-logging-fluentd"):
            return "fluentd"
        else:
            return None

    def build_facts(self):
        ''' Builds the logging facts and returns them '''
        self.facts_for_routes(self.namespace)
        self.facts_for_daemonsets(self.namespace)
        self.facts_for_deploymentconfigs(self.namespace)
        self.facts_for_services(self.namespace)
        self.facts_for_configmaps(self.namespace)
        self.facts_for_sccs()
        self.facts_for_oauthclients(self.namespace)
        self.facts_for_clusterrolebindings(self.namespace)
        self.facts_for_rolebindings(self.namespace)
        self.facts_for_secrets(self.namespace)
        self.facts_for_pvcs(self.namespace)

        return self.facts


def main():
    ''' The main method '''
    module = AnsibleModule(   # noqa: F405
        argument_spec=dict(
            admin_kubeconfig={"default": "/etc/origin/master/admin.kubeconfig", "type": "str"},
            oc_bin={"required": True, "type": "str"},
            openshift_logging_namespace={"required": True, "type": "str"}
        ),
        supports_check_mode=False
    )
    try:
        cmd = OpenshiftLoggingFacts(module, module.params['oc_bin'], module.params['admin_kubeconfig'],
                                    module.params['openshift_logging_namespace'])
        module.exit_json(
            ansible_facts={"openshift_logging_facts": cmd.build_facts()}
        )
    # ignore broad-except error to avoid stack trace to ansible user
    # pylint: disable=broad-except
    except Exception as error:
        module.fail_json(msg=str(error))


if __name__ == '__main__':
    main()