openshift_logging_facts.py 15 KB

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