__init__.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """
  2. Health checks for OpenShift clusters.
  3. """
  4. import operator
  5. import os
  6. from abc import ABCMeta, abstractmethod, abstractproperty
  7. from importlib import import_module
  8. from ansible.module_utils import six
  9. from ansible.module_utils.six.moves import reduce # pylint: disable=import-error,redefined-builtin
  10. from ansible.plugins.filter.core import to_bool as ansible_to_bool
  11. class OpenShiftCheckException(Exception):
  12. """Raised when a check encounters a failure condition."""
  13. def __init__(self, name, msg=None):
  14. # msg is for the message the user will see when this is raised.
  15. # name is for test code to identify the error without looking at msg text.
  16. if msg is None: # for parameter backward compatibility
  17. msg = name
  18. name = self.__class__.__name__
  19. self.name = name
  20. super(OpenShiftCheckException, self).__init__(msg)
  21. class OpenShiftCheckExceptionList(OpenShiftCheckException):
  22. """A container for multiple logging errors that may be detected in one check."""
  23. def __init__(self, errors):
  24. self.errors = errors
  25. super(OpenShiftCheckExceptionList, self).__init__(
  26. 'OpenShiftCheckExceptionList',
  27. '\n'.join(str(msg) for msg in errors)
  28. )
  29. # make iterable
  30. def __getitem__(self, index):
  31. return self.errors[index]
  32. @six.add_metaclass(ABCMeta)
  33. class OpenShiftCheck(object):
  34. """
  35. A base class for defining checks for an OpenShift cluster environment.
  36. Expect optional params: method execute_module, dict task_vars, and string tmp.
  37. execute_module is expected to have a signature compatible with _execute_module
  38. from ansible plugins/action/__init__.py, e.g.:
  39. def execute_module(module_name=None, module_args=None, tmp=None, task_vars=None, *args):
  40. This is stored so that it can be invoked in subclasses via check.execute_module("name", args)
  41. which provides the check's stored task_vars and tmp.
  42. """
  43. def __init__(self, execute_module=None, task_vars=None, tmp=None):
  44. self._execute_module = execute_module
  45. self.task_vars = task_vars or {}
  46. self.tmp = tmp
  47. # set to True when the check changes the host, for accurate total "changed" count
  48. self.changed = False
  49. @abstractproperty
  50. def name(self):
  51. """The name of this check, usually derived from the class name."""
  52. return "openshift_check"
  53. @property
  54. def tags(self):
  55. """A list of tags that this check satisfy.
  56. Tags are used to reference multiple checks with a single '@tagname'
  57. special check name.
  58. """
  59. return []
  60. @staticmethod
  61. def is_active():
  62. """Returns true if this check applies to the ansible-playbook run."""
  63. return True
  64. @abstractmethod
  65. def run(self):
  66. """Executes a check, normally implemented as a module."""
  67. return {}
  68. @classmethod
  69. def subclasses(cls):
  70. """Returns a generator of subclasses of this class and its subclasses."""
  71. # AUDIT: no-member makes sense due to this having a metaclass
  72. for subclass in cls.__subclasses__(): # pylint: disable=no-member
  73. yield subclass
  74. for subclass in subclass.subclasses():
  75. yield subclass
  76. def execute_module(self, module_name=None, module_args=None):
  77. """Invoke an Ansible module from a check.
  78. Invoke stored _execute_module, normally copied from the action
  79. plugin, with its params and the task_vars and tmp given at
  80. check initialization. No positional parameters beyond these
  81. are specified. If it's necessary to specify any of the other
  82. parameters to _execute_module then that should just be invoked
  83. directly (with awareness of changes in method signature per
  84. Ansible version).
  85. So e.g. check.execute_module("foo", dict(arg1=...))
  86. Return: result hash from module execution.
  87. """
  88. if self._execute_module is None:
  89. raise NotImplementedError(
  90. self.__class__.__name__ +
  91. " invoked execute_module without providing the method at initialization."
  92. )
  93. return self._execute_module(module_name, module_args, self.tmp, self.task_vars)
  94. def get_var(self, *keys, **kwargs):
  95. """Get deeply nested values from task_vars.
  96. Ansible task_vars structures are Python dicts, often mapping strings to
  97. other dicts. This helper makes it easier to get a nested value, raising
  98. OpenShiftCheckException when a key is not found.
  99. Keyword args:
  100. default:
  101. On missing key, return this as default value instead of raising exception.
  102. convert:
  103. Supply a function to apply to normalize the value before returning it.
  104. None is the default (return as-is).
  105. This function should raise ValueError if the user has provided a value
  106. that cannot be converted, or OpenShiftCheckException if some other
  107. problem needs to be described to the user.
  108. """
  109. if len(keys) == 1:
  110. keys = keys[0].split(".")
  111. try:
  112. value = reduce(operator.getitem, keys, self.task_vars)
  113. except (KeyError, TypeError):
  114. if "default" not in kwargs:
  115. raise OpenShiftCheckException(
  116. "This check expects the '{}' inventory variable to be defined\n"
  117. "in order to proceed, but it is undefined. There may be a bug\n"
  118. "in Ansible, the checks, or their dependencies."
  119. "".format(".".join(map(str, keys)))
  120. )
  121. value = kwargs["default"]
  122. convert = kwargs.get("convert", None)
  123. try:
  124. if convert is None:
  125. return value
  126. elif convert is bool: # interpret bool as Ansible does, instead of python truthiness
  127. return ansible_to_bool(value)
  128. else:
  129. return convert(value)
  130. except ValueError as error: # user error in specifying value
  131. raise OpenShiftCheckException(
  132. 'Cannot convert inventory variable to expected type:\n'
  133. ' "{var}={value}"\n'
  134. '{error}'.format(var=".".join(keys), value=value, error=error)
  135. )
  136. except OpenShiftCheckException: # some other check-specific problem
  137. raise
  138. except Exception as error: # probably a bug in the function
  139. raise OpenShiftCheckException(
  140. 'There is a bug in this check. While trying to convert variable \n'
  141. ' "{var}={value}"\n'
  142. 'the given converter cannot be used or failed unexpectedly:\n'
  143. '{error}'.format(var=".".join(keys), value=value, error=error)
  144. )
  145. @staticmethod
  146. def get_major_minor_version(openshift_image_tag):
  147. """Parse and return the deployed version of OpenShift as a tuple."""
  148. if openshift_image_tag and openshift_image_tag[0] == 'v':
  149. openshift_image_tag = openshift_image_tag[1:]
  150. # map major release versions across releases
  151. # to a common major version
  152. openshift_major_release_version = {
  153. "1": "3",
  154. }
  155. components = openshift_image_tag.split(".")
  156. if not components or len(components) < 2:
  157. msg = "An invalid version of OpenShift was found for this host: {}"
  158. raise OpenShiftCheckException(msg.format(openshift_image_tag))
  159. if components[0] in openshift_major_release_version:
  160. components[0] = openshift_major_release_version[components[0]]
  161. components = tuple(int(x) for x in components[:2])
  162. return components
  163. def find_ansible_mount(self, path):
  164. """Return the mount point for path from ansible_mounts."""
  165. # reorganize list of mounts into dict by path
  166. mount_for_path = {
  167. mount['mount']: mount
  168. for mount
  169. in self.get_var('ansible_mounts')
  170. }
  171. # NOTE: including base cases '/' and '' to ensure the loop ends
  172. mount_targets = set(mount_for_path.keys()) | {'/', ''}
  173. mount_point = path
  174. while mount_point not in mount_targets:
  175. mount_point = os.path.dirname(mount_point)
  176. try:
  177. return mount_for_path[mount_point]
  178. except KeyError:
  179. known_mounts = ', '.join('"{}"'.format(mount) for mount in sorted(mount_for_path))
  180. raise OpenShiftCheckException(
  181. 'Unable to determine mount point for path "{}".\n'
  182. 'Known mount points: {}.'.format(path, known_mounts or 'none')
  183. )
  184. LOADER_EXCLUDES = (
  185. "__init__.py",
  186. "mixins.py",
  187. "logging.py",
  188. )
  189. def load_checks(path=None, subpkg=""):
  190. """Dynamically import all check modules for the side effect of registering checks."""
  191. if path is None:
  192. path = os.path.dirname(__file__)
  193. modules = []
  194. for name in os.listdir(path):
  195. if os.path.isdir(os.path.join(path, name)):
  196. modules = modules + load_checks(os.path.join(path, name), subpkg + "." + name)
  197. continue
  198. if name.endswith(".py") and name not in LOADER_EXCLUDES:
  199. modules.append(import_module(__package__ + subpkg + "." + name[:-3]))
  200. return modules