openshift_logging_facts.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. '''
  2. ---
  3. module: openshift_logging_facts
  4. version_added: ""
  5. short_description: Gather facts about the OpenShift logging stack
  6. description:
  7. - Determine the current facts about the OpenShift logging stack (e.g. cluster size)
  8. options:
  9. author: Red Hat, Inc
  10. '''
  11. import copy
  12. import json
  13. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  14. from subprocess import * # noqa: F402,F403
  15. # ignore pylint errors related to the module_utils import
  16. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  17. from ansible.module_utils.basic import * # noqa: F402,F403
  18. import yaml
  19. EXAMPLES = """
  20. - action: opneshift_logging_facts
  21. """
  22. RETURN = """
  23. """
  24. DEFAULT_OC_OPTIONS = ["-o", "json"]
  25. # constants used for various labels and selectors
  26. COMPONENT_KEY = "component"
  27. LOGGING_INFRA_KEY = "logging-infra"
  28. # selectors for filtering resources
  29. DS_FLUENTD_SELECTOR = LOGGING_INFRA_KEY + "=" + "fluentd"
  30. LOGGING_SELECTOR = LOGGING_INFRA_KEY + "=" + "support"
  31. ROUTE_SELECTOR = "component=support,logging-infra=support,provider=openshift"
  32. COMPONENTS = ["kibana", "curator", "elasticsearch", "fluentd", "kibana_ops", "curator_ops", "elasticsearch_ops"]
  33. class OCBaseCommand(object):
  34. ''' The base class used to query openshift '''
  35. def __init__(self, binary, kubeconfig, namespace):
  36. ''' the init method of OCBaseCommand class '''
  37. self.binary = binary
  38. self.kubeconfig = kubeconfig
  39. self.user = self.get_system_admin(self.kubeconfig)
  40. self.namespace = namespace
  41. # pylint: disable=no-self-use
  42. def get_system_admin(self, kubeconfig):
  43. ''' Retrieves the system admin '''
  44. with open(kubeconfig, 'r') as kubeconfig_file:
  45. config = yaml.load(kubeconfig_file)
  46. for user in config["users"]:
  47. if user["name"].startswith("system:admin"):
  48. return user["name"]
  49. raise Exception("Unable to find system:admin in: " + kubeconfig)
  50. # pylint: disable=too-many-arguments, dangerous-default-value
  51. def oc_command(self, sub, kind, namespace=None, name=None, add_options=None):
  52. ''' Wrapper method for the "oc" command '''
  53. cmd = [self.binary, sub, kind]
  54. if name is not None:
  55. cmd = cmd + [name]
  56. if namespace is not None:
  57. cmd = cmd + ["-n", namespace]
  58. if add_options is None:
  59. add_options = []
  60. cmd = cmd + ["--user=" + self.user, "--config=" + self.kubeconfig] + DEFAULT_OC_OPTIONS + add_options
  61. try:
  62. process = Popen(cmd, stdout=PIPE, stderr=PIPE) # noqa: F405
  63. out, err = process.communicate(cmd)
  64. if len(err) > 0:
  65. if 'not found' in err:
  66. return {'items': []}
  67. if 'No resources found' in err:
  68. return {'items': []}
  69. raise Exception(err)
  70. except Exception as excp:
  71. err = "There was an exception trying to run the command '" + " ".join(cmd) + "' " + str(excp)
  72. raise Exception(err)
  73. return json.loads(out)
  74. class OpenshiftLoggingFacts(OCBaseCommand):
  75. ''' The class structure for holding the OpenshiftLogging Facts'''
  76. name = "facts"
  77. def __init__(self, logger, binary, kubeconfig, namespace):
  78. ''' The init method for OpenshiftLoggingFacts '''
  79. super(OpenshiftLoggingFacts, self).__init__(binary, kubeconfig, namespace)
  80. self.logger = logger
  81. self.facts = dict()
  82. def default_keys_for(self, kind):
  83. ''' Sets the default key values for kind '''
  84. for comp in COMPONENTS:
  85. self.add_facts_for(comp, kind)
  86. def add_facts_for(self, comp, kind, name=None, facts=None):
  87. ''' Add facts for the provided kind '''
  88. if comp not in self.facts:
  89. self.facts[comp] = dict()
  90. if kind not in self.facts[comp]:
  91. self.facts[comp][kind] = dict()
  92. if name:
  93. self.facts[comp][kind][name] = facts
  94. def facts_for_routes(self, namespace):
  95. ''' Gathers facts for Routes in logging namespace '''
  96. self.default_keys_for("routes")
  97. route_list = self.oc_command("get", "routes", namespace=namespace, add_options=["-l", ROUTE_SELECTOR])
  98. if len(route_list["items"]) == 0:
  99. return None
  100. for route in route_list["items"]:
  101. name = route["metadata"]["name"]
  102. comp = self.comp(name)
  103. if comp is not None:
  104. self.add_facts_for(comp, "routes", name, dict(host=route["spec"]["host"]))
  105. self.facts["agl_namespace"] = namespace
  106. def facts_for_daemonsets(self, namespace):
  107. ''' Gathers facts for Daemonsets in logging namespace '''
  108. self.default_keys_for("daemonsets")
  109. ds_list = self.oc_command("get", "daemonsets", namespace=namespace,
  110. add_options=["-l", LOGGING_INFRA_KEY + "=fluentd"])
  111. if len(ds_list["items"]) == 0:
  112. return
  113. for ds_item in ds_list["items"]:
  114. name = ds_item["metadata"]["name"]
  115. comp = self.comp(name)
  116. spec = ds_item["spec"]["template"]["spec"]
  117. container = spec["containers"][0]
  118. result = dict(
  119. selector=ds_item["spec"]["selector"],
  120. image=container["image"],
  121. resources=container["resources"],
  122. nodeSelector=spec["nodeSelector"],
  123. serviceAccount=spec["serviceAccount"],
  124. terminationGracePeriodSeconds=spec["terminationGracePeriodSeconds"]
  125. )
  126. self.add_facts_for(comp, "daemonsets", name, result)
  127. def facts_for_pvcs(self, namespace):
  128. ''' Gathers facts for PVCS in logging namespace'''
  129. self.default_keys_for("pvcs")
  130. pvclist = self.oc_command("get", "pvc", namespace=namespace, add_options=["-l", LOGGING_INFRA_KEY])
  131. if len(pvclist["items"]) == 0:
  132. return
  133. for pvc in pvclist["items"]:
  134. name = pvc["metadata"]["name"]
  135. comp = self.comp(name)
  136. self.add_facts_for(comp, "pvcs", name, dict())
  137. def facts_for_deploymentconfigs(self, namespace):
  138. ''' Gathers facts for DeploymentConfigs in logging namespace '''
  139. self.default_keys_for("deploymentconfigs")
  140. dclist = self.oc_command("get", "deploymentconfigs", namespace=namespace, add_options=["-l", LOGGING_INFRA_KEY])
  141. if len(dclist["items"]) == 0:
  142. return
  143. dcs = dclist["items"]
  144. for dc_item in dcs:
  145. name = dc_item["metadata"]["name"]
  146. comp = self.comp(name)
  147. if comp is not None:
  148. spec = dc_item["spec"]["template"]["spec"]
  149. facts = dict(
  150. name=name,
  151. selector=dc_item["spec"]["selector"],
  152. replicas=dc_item["spec"]["replicas"],
  153. serviceAccount=spec["serviceAccount"],
  154. containers=dict(),
  155. volumes=dict()
  156. )
  157. if "nodeSelector" in spec:
  158. facts["nodeSelector"] = spec["nodeSelector"]
  159. if "supplementalGroups" in spec["securityContext"]:
  160. facts["storageGroups"] = spec["securityContext"]["supplementalGroups"]
  161. facts["spec"] = spec
  162. if "volumes" in spec:
  163. for vol in spec["volumes"]:
  164. clone = copy.deepcopy(vol)
  165. clone.pop("name", None)
  166. facts["volumes"][vol["name"]] = clone
  167. for container in spec["containers"]:
  168. facts["containers"][container["name"]] = container
  169. self.add_facts_for(comp, "deploymentconfigs", name, facts)
  170. def facts_for_services(self, namespace):
  171. ''' Gathers facts for services in logging namespace '''
  172. self.default_keys_for("services")
  173. servicelist = self.oc_command("get", "services", namespace=namespace, add_options=["-l", LOGGING_SELECTOR])
  174. if len(servicelist["items"]) == 0:
  175. return
  176. for service in servicelist["items"]:
  177. name = service["metadata"]["name"]
  178. comp = self.comp(name)
  179. if comp is not None:
  180. self.add_facts_for(comp, "services", name, dict())
  181. # pylint: disable=too-many-arguments
  182. def facts_from_configmap(self, comp, kind, name, config_key, yaml_file=None):
  183. '''Extracts facts in logging namespace from configmap'''
  184. if yaml_file is not None:
  185. if config_key.endswith(".yml") or config_key.endswith(".yaml"):
  186. config_facts = yaml.load(yaml_file)
  187. self.facts[comp][kind][name][config_key] = config_facts
  188. self.facts[comp][kind][name][config_key]["raw"] = yaml_file
  189. def facts_for_configmaps(self, namespace):
  190. ''' Gathers facts for configmaps in logging namespace '''
  191. self.default_keys_for("configmaps")
  192. a_list = self.oc_command("get", "configmaps", namespace=namespace)
  193. if len(a_list["items"]) == 0:
  194. return
  195. for item in a_list["items"]:
  196. name = item["metadata"]["name"]
  197. comp = self.comp(name)
  198. if comp is not None:
  199. self.add_facts_for(comp, "configmaps", name, dict(item["data"]))
  200. if comp in ["elasticsearch", "elasticsearch_ops"]:
  201. for config_key in item["data"]:
  202. self.facts_from_configmap(comp, "configmaps", name, config_key, item["data"][config_key])
  203. def facts_for_oauthclients(self, namespace):
  204. ''' Gathers facts for oauthclients used with logging '''
  205. self.default_keys_for("oauthclients")
  206. a_list = self.oc_command("get", "oauthclients", namespace=namespace, add_options=["-l", LOGGING_SELECTOR])
  207. if len(a_list["items"]) == 0:
  208. return
  209. for item in a_list["items"]:
  210. name = item["metadata"]["name"]
  211. comp = self.comp(name)
  212. if comp is not None:
  213. result = dict(
  214. redirectURIs=item["redirectURIs"]
  215. )
  216. self.add_facts_for(comp, "oauthclients", name, result)
  217. def facts_for_secrets(self, namespace):
  218. ''' Gathers facts for secrets in the logging namespace '''
  219. self.default_keys_for("secrets")
  220. a_list = self.oc_command("get", "secrets", namespace=namespace)
  221. if len(a_list["items"]) == 0:
  222. return
  223. for item in a_list["items"]:
  224. name = item["metadata"]["name"]
  225. comp = self.comp(name)
  226. if comp is not None and item["type"] == "Opaque":
  227. result = dict(
  228. keys=item["data"].keys()
  229. )
  230. self.add_facts_for(comp, "secrets", name, result)
  231. def facts_for_sccs(self):
  232. ''' Gathers facts for SCCs used with logging '''
  233. self.default_keys_for("sccs")
  234. scc = self.oc_command("get", "securitycontextconstraints.v1.security.openshift.io", name="privileged")
  235. if len(scc["users"]) == 0:
  236. return
  237. for item in scc["users"]:
  238. comp = self.comp(item)
  239. if comp is not None:
  240. self.add_facts_for(comp, "sccs", "privileged", dict())
  241. def facts_for_clusterrolebindings(self, namespace):
  242. ''' Gathers ClusterRoleBindings used with logging '''
  243. self.default_keys_for("clusterrolebindings")
  244. role = self.oc_command("get", "clusterrolebindings", name="cluster-readers")
  245. if "subjects" not in role or len(role["subjects"]) == 0:
  246. return
  247. for item in role["subjects"]:
  248. comp = self.comp(item["name"])
  249. if comp is not None and namespace == item.get("namespace"):
  250. self.add_facts_for(comp, "clusterrolebindings", "cluster-readers", dict())
  251. # this needs to end up nested under the service account...
  252. def facts_for_rolebindings(self, namespace):
  253. ''' Gathers facts for RoleBindings used with logging '''
  254. self.default_keys_for("rolebindings")
  255. role = self.oc_command("get", "rolebindings", namespace=namespace, name="logging-elasticsearch-view-role")
  256. if "subjects" not in role or len(role["subjects"]) == 0:
  257. return
  258. for item in role["subjects"]:
  259. comp = self.comp(item["name"])
  260. if comp is not None and namespace == item.get("namespace"):
  261. self.add_facts_for(comp, "rolebindings", "logging-elasticsearch-view-role", dict())
  262. # pylint: disable=no-self-use, too-many-return-statements
  263. def comp(self, name):
  264. ''' Does a comparison to evaluate the logging component '''
  265. if name.startswith("logging-curator-ops"):
  266. return "curator_ops"
  267. elif name.startswith("logging-kibana-ops") or name.startswith("kibana-ops"):
  268. return "kibana_ops"
  269. elif name.startswith("logging-es-ops") or name.startswith("logging-elasticsearch-ops"):
  270. return "elasticsearch_ops"
  271. elif name.startswith("logging-curator"):
  272. return "curator"
  273. elif name.startswith("logging-kibana") or name.startswith("kibana"):
  274. return "kibana"
  275. elif name.startswith("logging-es") or name.startswith("logging-elasticsearch"):
  276. return "elasticsearch"
  277. elif name.startswith("logging-fluentd") or name.endswith("aggregated-logging-fluentd"):
  278. return "fluentd"
  279. else:
  280. return None
  281. def build_facts(self):
  282. ''' Builds the logging facts and returns them '''
  283. self.facts_for_routes(self.namespace)
  284. self.facts_for_daemonsets(self.namespace)
  285. self.facts_for_deploymentconfigs(self.namespace)
  286. self.facts_for_services(self.namespace)
  287. self.facts_for_configmaps(self.namespace)
  288. self.facts_for_sccs()
  289. self.facts_for_oauthclients(self.namespace)
  290. self.facts_for_clusterrolebindings(self.namespace)
  291. self.facts_for_rolebindings(self.namespace)
  292. self.facts_for_secrets(self.namespace)
  293. self.facts_for_pvcs(self.namespace)
  294. return self.facts
  295. def main():
  296. ''' The main method '''
  297. module = AnsibleModule( # noqa: F405
  298. argument_spec=dict(
  299. admin_kubeconfig={"default": "/etc/origin/master/admin.kubeconfig", "type": "str"},
  300. oc_bin={"required": True, "type": "str"},
  301. openshift_logging_namespace={"required": True, "type": "str"}
  302. ),
  303. supports_check_mode=False
  304. )
  305. try:
  306. cmd = OpenshiftLoggingFacts(module, module.params['oc_bin'], module.params['admin_kubeconfig'],
  307. module.params['openshift_logging_namespace'])
  308. module.exit_json(
  309. ansible_facts={"openshift_logging_facts": cmd.build_facts()}
  310. )
  311. # ignore broad-except error to avoid stack trace to ansible user
  312. # pylint: disable=broad-except
  313. except Exception as error:
  314. module.fail_json(msg=str(error))
  315. if __name__ == '__main__':
  316. main()