From d4c6c5fd1e8ff2c14e06c592ec92d1560ed78c96 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Jan 21 2020 21:12:37 +0000 Subject: import ipa-healthcheck-0.4-3.module+el8.2.0+5089+8260dc50 --- diff --git a/.gitignore b/.gitignore index 9c36803..7218656 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/freeipa-healthcheck-0.2.tar.gz +SOURCES/freeipa-healthcheck-0.4.tar.gz diff --git a/.ipa-healthcheck.metadata b/.ipa-healthcheck.metadata index 2a0255c..f0c76cd 100644 --- a/.ipa-healthcheck.metadata +++ b/.ipa-healthcheck.metadata @@ -1 +1 @@ -2c1398586b861f598c4836eeed2ee30a629cedb1 SOURCES/freeipa-healthcheck-0.2.tar.gz +2e61604c36f6f793612b1e3bb3f5e78df1cb58ac SOURCES/freeipa-healthcheck-0.4.tar.gz diff --git a/SOURCES/0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch b/SOURCES/0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch index 8a3a8f2..ed363bc 100644 --- a/SOURCES/0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch +++ b/SOURCES/0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch @@ -1,4 +1,4 @@ -From bf2363355b4fec7a47ad9a02d011701ecabd523a Mon Sep 17 00:00:00 2001 +From 611a7d51ac6b49770cdc0da02d101023a4a49536 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 3 May 2019 10:10:57 -0400 Subject: [PATCH] Remove requirement for pytest-runner since PyPI isn't @@ -11,10 +11,10 @@ available due to IDM being in a module. 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py -index 4b3ca3b..6c941f8 100644 +index 801323f..c3cd215 100644 --- a/setup.py +++ b/setup.py -@@ -58,6 +58,5 @@ setup( +@@ -60,6 +60,5 @@ setup( 'Programming Language :: Python :: 3.6', ], python_requires='!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', diff --git a/SOURCES/0002-Move-main-to-run_healthcheck-for-abstraction-purpose.patch b/SOURCES/0002-Move-main-to-run_healthcheck-for-abstraction-purpose.patch new file mode 100644 index 0000000..c14ea28 --- /dev/null +++ b/SOURCES/0002-Move-main-to-run_healthcheck-for-abstraction-purpose.patch @@ -0,0 +1,106 @@ +From eb87d277442dd1a1076ba9ae74c18ace8cc7dda8 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 11 Nov 2019 10:29:53 -0500 +Subject: [PATCH 2/5] Move main to run_healthcheck for abstraction purposes + +Take as arguments the entry point name and the configuration +file. +--- + src/ipahealthcheck/core/config.py | 4 ++-- + src/ipahealthcheck/core/main.py | 37 +++++++++++++++++-------------- + 2 files changed, 22 insertions(+), 19 deletions(-) + +diff --git a/src/ipahealthcheck/core/config.py b/src/ipahealthcheck/core/config.py +index 46da507..0cb72b7 100644 +--- a/src/ipahealthcheck/core/config.py ++++ b/src/ipahealthcheck/core/config.py +@@ -5,7 +5,7 @@ + import logging + from configparser import ConfigParser, ParsingError + +-from ipahealthcheck.core.constants import CONFIG_FILE, CONFIG_SECTION ++from ipahealthcheck.core.constants import CONFIG_SECTION + from ipahealthcheck.core.constants import DEFAULT_CONFIG + + logger = logging.getLogger() +@@ -70,7 +70,7 @@ class Config: + self.__d[key] = d[key] + + +-def read_config(config_file=CONFIG_FILE): ++def read_config(config_file): + """ + Simple configuration file reader + +diff --git a/src/ipahealthcheck/core/main.py b/src/ipahealthcheck/core/main.py +index b3fbe6a..f59a8a8 100644 +--- a/src/ipahealthcheck/core/main.py ++++ b/src/ipahealthcheck/core/main.py +@@ -28,11 +28,14 @@ else: + from ipahealthcheck.meta.services import ServiceCheck + + +-def find_registries(): +- return { +- ep.name: ep.resolve() +- for ep in pkg_resources.iter_entry_points('ipahealthcheck.registry') +- } ++def find_registries(entry_points): ++ registries = {} ++ for entry_point in entry_points: ++ registries.update({ ++ ep.name: ep.resolve() ++ for ep in pkg_resources.iter_entry_points(entry_point) ++ }) ++ return registries + + + def find_plugins(name, registry): +@@ -194,9 +197,7 @@ def limit_results(results, source, check): + return new_results + + +-def main(): +- environ["KRB5_CLIENT_KTNAME"] = "/etc/krb5.keytab" +- environ["KRB5CCNAME"] = "MEMORY:" ++def run_healthcheck(entry_points, configfile): + framework = object() + plugins = [] + output = constants.DEFAULT_OUTPUT +@@ -208,17 +209,11 @@ def main(): + if options.debug: + logger.setLevel(logging.DEBUG) + +- config = read_config() ++ config = read_config(configfile) + if config is None: + sys.exit(1) + +- if not ( +- options.source or options.list_sources +- ) and not is_ipa_configured(): +- logging.error("IPA is not configured on this system.") +- sys.exit(1) +- +- for name, registry in find_registries().items(): ++ for name, registry in find_registries(entry_points).items(): + try: + registry.initialize(framework) + except Exception as e: +@@ -283,4 +278,12 @@ def main(): + return_value = 1 + break + +- sys.exit(return_value) ++ return return_value ++ ++ ++def main(): ++ environ["KRB5_CLIENT_KTNAME"] = "/etc/krb5.keytab" ++ environ["KRB5CCNAME"] = "MEMORY:" ++ ++ sys.exit(run_healthcheck(['ipahealthcheck.registry'], ++ constants.CONFIG_FILE)) +-- +2.20.1 + diff --git a/SOURCES/0003-Abstract-ServiceCheck-to-not-be-IPA-specific.patch b/SOURCES/0003-Abstract-ServiceCheck-to-not-be-IPA-specific.patch new file mode 100644 index 0000000..1ae8db6 --- /dev/null +++ b/SOURCES/0003-Abstract-ServiceCheck-to-not-be-IPA-specific.patch @@ -0,0 +1,187 @@ +From c40e32b9d0ac49806b8336bd5065350574d29672 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 18 Nov 2019 12:51:27 -0500 +Subject: [PATCH 3/5] Abstract ServiceCheck to not be IPA-specific + +It is up to the implementor to check for services running for now. +--- + src/ipahealthcheck/core/main.py | 9 +-------- + src/ipahealthcheck/core/service.py | 10 ++++++++++ + src/ipahealthcheck/meta/services.py | 29 +++++++++++++++-------------- + 3 files changed, 26 insertions(+), 22 deletions(-) + create mode 100644 src/ipahealthcheck/core/service.py + +diff --git a/src/ipahealthcheck/core/main.py b/src/ipahealthcheck/core/main.py +index f59a8a8..2b818d4 100644 +--- a/src/ipahealthcheck/core/main.py ++++ b/src/ipahealthcheck/core/main.py +@@ -15,18 +15,11 @@ from ipahealthcheck.core.config import read_config + from ipahealthcheck.core.plugin import Result, Results, json_to_results + from ipahealthcheck.core.output import output_registry + from ipahealthcheck.core import constants ++from ipahealthcheck.core.service import ServiceCheck + + logging.basicConfig(format='%(message)s') + logger = logging.getLogger() + +-try: +- from ipaserver.install.installutils import is_ipa_configured +-except ImportError: +- logging.error("IPA server packages are not installed on this system.") +- sys.exit(1) +-else: +- from ipahealthcheck.meta.services import ServiceCheck +- + + def find_registries(entry_points): + registries = {} +diff --git a/src/ipahealthcheck/core/service.py b/src/ipahealthcheck/core/service.py +new file mode 100644 +index 0000000..f9e2645 +--- /dev/null ++++ b/src/ipahealthcheck/core/service.py +@@ -0,0 +1,10 @@ ++# ++# Copyright (C) 2019 FreeIPA Contributors see COPYING for license ++# ++ ++from ipahealthcheck.core.plugin import Plugin ++ ++ ++class ServiceCheck(Plugin): ++ def check(self, instance=''): ++ raise NotImplementedError +diff --git a/src/ipahealthcheck/meta/services.py b/src/ipahealthcheck/meta/services.py +index d375066..a987108 100644 +--- a/src/ipahealthcheck/meta/services.py ++++ b/src/ipahealthcheck/meta/services.py +@@ -6,7 +6,8 @@ import logging + + from ipahealthcheck.core import constants + from ipahealthcheck.core.plugin import Result, duration +-from ipahealthcheck.meta.plugin import Plugin, registry ++from ipahealthcheck.core.service import ServiceCheck ++from ipahealthcheck.meta.plugin import registry + try: + from ipapython.ipaldap import realm_to_serverid + except ImportError: +@@ -20,7 +21,7 @@ from ipaserver.install import cainstance + logger = logging.getLogger() + + +-class ServiceCheck(Plugin): ++class IPAServiceCheck(ServiceCheck): + @duration + def check(self, instance=''): + try: +@@ -47,7 +48,7 @@ class ServiceCheck(Plugin): + + + @registry +-class certmonger(ServiceCheck): ++class certmonger(IPAServiceCheck): + def check(self): + self.service_name = 'certmonger' + +@@ -55,7 +56,7 @@ class certmonger(ServiceCheck): + + + @registry +-class dirsrv(ServiceCheck): ++class dirsrv(IPAServiceCheck): + def check(self): + self.service_name = 'dirsrv' + +@@ -63,7 +64,7 @@ class dirsrv(ServiceCheck): + + + @registry +-class gssproxy(ServiceCheck): ++class gssproxy(IPAServiceCheck): + def check(self): + self.service_name = 'gssproxy' + +@@ -71,7 +72,7 @@ class gssproxy(ServiceCheck): + + + @registry +-class httpd(ServiceCheck): ++class httpd(IPAServiceCheck): + def check(self): + self.service_name = 'httpd' + +@@ -79,7 +80,7 @@ class httpd(ServiceCheck): + + + @registry +-class ipa_custodia(ServiceCheck): ++class ipa_custodia(IPAServiceCheck): + def check(self): + self.service_name = 'ipa-custodia' + +@@ -87,7 +88,7 @@ class ipa_custodia(ServiceCheck): + + + @registry +-class ipa_dnskeysyncd(ServiceCheck): ++class ipa_dnskeysyncd(IPAServiceCheck): + def check(self): + self.service_name = 'ipa-dnskeysyncd' + +@@ -98,7 +99,7 @@ class ipa_dnskeysyncd(ServiceCheck): + + + @registry +-class ipa_otpd(ServiceCheck): ++class ipa_otpd(IPAServiceCheck): + def check(self): + self.service_name = 'ipa-otpd' + +@@ -106,7 +107,7 @@ class ipa_otpd(ServiceCheck): + + + @registry +-class kadmin(ServiceCheck): ++class kadmin(IPAServiceCheck): + def check(self): + self.service_name = 'kadmin' + +@@ -114,7 +115,7 @@ class kadmin(ServiceCheck): + + + @registry +-class krb5kdc(ServiceCheck): ++class krb5kdc(IPAServiceCheck): + def check(self): + self.service_name = 'krb5kdc' + +@@ -122,7 +123,7 @@ class krb5kdc(ServiceCheck): + + + @registry +-class named(ServiceCheck): ++class named(IPAServiceCheck): + def check(self): + self.service_name = 'named' + +@@ -133,7 +134,7 @@ class named(ServiceCheck): + + + @registry +-class pki_tomcatd(ServiceCheck): ++class pki_tomcatd(IPAServiceCheck): + def check(self): + self.service_name = 'pki_tomcatd' + +@@ -145,7 +146,7 @@ class pki_tomcatd(ServiceCheck): + + + @registry +-class sssd(ServiceCheck): ++class sssd(IPAServiceCheck): + def check(self): + self.service_name = 'sssd' + +-- +2.20.1 + diff --git a/SOURCES/0004-Move-the-abstracted-plugin-runner-code-into-a-separa.patch b/SOURCES/0004-Move-the-abstracted-plugin-runner-code-into-a-separa.patch new file mode 100644 index 0000000..a27845e --- /dev/null +++ b/SOURCES/0004-Move-the-abstracted-plugin-runner-code-into-a-separa.patch @@ -0,0 +1,579 @@ +From 952fc6f6dee99523360c9826ad865086cd31474f Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 18 Nov 2019 14:33:32 -0500 +Subject: [PATCH 4/5] Move the abstracted plugin runner code into a separate + file + +This way main.py contains just the ipa-healthcheck main code +and not the rest of the abstracted code. + +Also replace sys.exit(1) with return 1. +--- + src/ipahealthcheck/core/core.py | 272 ++++++++++++++++++++++++++++++++ + src/ipahealthcheck/core/main.py | 267 +------------------------------ + 2 files changed, 273 insertions(+), 266 deletions(-) + create mode 100644 src/ipahealthcheck/core/core.py + +diff --git a/src/ipahealthcheck/core/core.py b/src/ipahealthcheck/core/core.py +new file mode 100644 +index 0000000..182eac3 +--- /dev/null ++++ b/src/ipahealthcheck/core/core.py +@@ -0,0 +1,272 @@ ++# ++# Copyright (C) 2019 FreeIPA Contributors see COPYING for license ++# ++ ++import argparse ++import json ++import logging ++import pkg_resources ++ ++from datetime import datetime ++ ++from ipahealthcheck.core.config import read_config ++from ipahealthcheck.core.plugin import Result, Results, json_to_results ++from ipahealthcheck.core.output import output_registry ++from ipahealthcheck.core import constants ++from ipahealthcheck.core.service import ServiceCheck ++ ++logging.basicConfig(format='%(message)s') ++logger = logging.getLogger() ++ ++ ++def find_registries(entry_points): ++ registries = {} ++ for entry_point in entry_points: ++ registries.update({ ++ ep.name: ep.resolve() ++ for ep in pkg_resources.iter_entry_points(entry_point) ++ }) ++ return registries ++ ++ ++def find_plugins(name, registry): ++ for ep in pkg_resources.iter_entry_points(name): ++ # load module ++ ep.load() ++ return registry.get_plugins() ++ ++ ++def run_plugin(plugin, available=()): ++ # manually calculate duration when we create results of our own ++ start = datetime.utcnow() ++ try: ++ for result in plugin.check(): ++ if result is None: ++ # Treat no result as success, fudge start time ++ result = Result(plugin, constants.SUCCESS, start=start) ++ yield result ++ except Exception as e: ++ logger.debug('Exception raised: %s', e) ++ yield Result(plugin, constants.CRITICAL, exception=str(e), ++ start=start) ++ ++ ++def source_or_check_matches(plugin, source, check): ++ """Determine whether a given a plugin matches if a source ++ and optional check are provided. ++ """ ++ if source is not None and plugin.__module__ != source: ++ return False ++ ++ if check and plugin.__class__.__name__ != check: ++ return False ++ ++ return True ++ ++ ++def run_service_plugins(plugins, config, source, check): ++ """Execute plugins with the base class of ServiceCheck ++ ++ This is a specialized check to use systemd to determine ++ if a service is running or not. ++ """ ++ results = Results() ++ available = [] ++ ++ for plugin in plugins: ++ if not isinstance(plugin, ServiceCheck): ++ continue ++ ++ logger.debug('Calling check %s', plugin) ++ for result in plugin.check(): ++ # always run the service checks so dependencies work ++ if result is not None and result.result == constants.SUCCESS: ++ available.append(plugin.service.service_name) ++ if not source_or_check_matches(plugin, source, check): ++ continue ++ if result is not None: ++ results.add(result) ++ ++ return results, set(available) ++ ++ ++def run_plugins(plugins, config, available, source, check): ++ """Execute plugins without the base class of ServiceCheck ++ ++ These are the remaining, non-service checking checks ++ that do validation for various parts of a system. ++ """ ++ results = Results() ++ ++ for plugin in plugins: ++ if isinstance(plugin, ServiceCheck): ++ continue ++ ++ if not source_or_check_matches(plugin, source, check): ++ continue ++ ++ logger.debug('Calling check %s' % plugin) ++ plugin.config = config ++ if not set(plugin.requires).issubset(available): ++ logger.debug('Skipping %s:%s because %s service(s) not running', ++ plugin.__class__.__module__, ++ plugin.__class__.__name__, ++ ', '.join(set(plugin.requires) - available)) ++ # Not providing a Result in this case because if a required ++ # service isn't available then this could generate a lot of ++ # false positives. ++ else: ++ for result in run_plugin(plugin, available): ++ results.add(result) ++ ++ return results ++ ++ ++def list_sources(plugins): ++ """Print list of all sources and checks""" ++ source = None ++ for plugin in plugins: ++ if source != plugin.__class__.__module__: ++ print(plugin.__class__.__module__) ++ source = plugin.__class__.__module__ ++ print(" ", plugin.__class__.__name__) ++ ++ return 0 ++ ++ ++def parse_options(output_registry): ++ output_names = [plugin.__name__.lower() for ++ plugin in output_registry.plugins] ++ parser = argparse.ArgumentParser() ++ parser.add_argument('--debug', dest='debug', action='store_true', ++ default=False, help='Include debug output') ++ parser.add_argument('--list-sources', dest='list_sources', ++ action='store_true', default=False, ++ help='List all available sources') ++ parser.add_argument('--source', dest='source', ++ default=None, ++ help='Source of checks, e.g. ipahealthcheck.foo.bar') ++ parser.add_argument('--check', dest='check', ++ default=None, ++ help='Check to execute, e.g. BazCheck') ++ parser.add_argument('--output-type', dest='output', choices=output_names, ++ default='json', help='Output method') ++ parser.add_argument('--output-file', dest='outfile', default=None, ++ help='File to store output') ++ parser.add_argument('--input-file', dest='infile', ++ help='File to read as input') ++ parser.add_argument('--failures-only', dest='failures_only', ++ action='store_true', default=False, ++ help='Exclude SUCCESS results on output') ++ parser.add_argument('--severity', dest='severity', action="append", ++ help='Include only the selected severity(s)', ++ choices=[key for key in constants._nameToLevel]) ++ for plugin in output_registry.plugins: ++ onelinedoc = plugin.__doc__.split('\n\n', 1)[0].strip() ++ group = parser.add_argument_group(plugin.__name__.lower(), ++ onelinedoc) ++ for option in plugin.options: ++ group.add_argument(option[0], **option[1]) ++ ++ options = parser.parse_args() ++ ++ # Validation ++ if options.check and not options.source: ++ print("--source is required when --check is used") ++ return 1 ++ ++ return options ++ ++ ++def limit_results(results, source, check): ++ """Return ony those results which match source and/or check""" ++ new_results = Results() ++ for result in results.results: ++ if result.source == source: ++ if check is None or result.check == check: ++ new_results.add(result) ++ return new_results ++ ++ ++def run_healthcheck(entry_points, configfile): ++ framework = object() ++ plugins = [] ++ output = constants.DEFAULT_OUTPUT ++ ++ logger.setLevel(logging.INFO) ++ ++ options = parse_options(output_registry) ++ ++ if options.debug: ++ logger.setLevel(logging.DEBUG) ++ ++ config = read_config(configfile) ++ if config is None: ++ return 1 ++ ++ for name, registry in find_registries(entry_points).items(): ++ try: ++ registry.initialize(framework) ++ except Exception as e: ++ print("Unable to initialize %s: %s" % (name, e)) ++ return 1 ++ for plugin in find_plugins(name, registry): ++ plugins.append(plugin) ++ ++ for out in output_registry.plugins: ++ if out.__name__.lower() == options.output: ++ output = out(options) ++ ++ if options.list_sources: ++ return list_sources(plugins) ++ ++ if options.infile: ++ try: ++ with open(options.infile, 'r') as f: ++ raw_data = f.read() ++ ++ json_data = json.loads(raw_data) ++ results = json_to_results(json_data) ++ available = () ++ except Exception as e: ++ print("Unable to import '%s': %s" % (options.infile, e)) ++ return 1 ++ if options.source: ++ results = limit_results(results, options.source, options.check) ++ else: ++ results, available = run_service_plugins(plugins, config, ++ options.source, ++ options.check) ++ results.extend(run_plugins(plugins, config, available, ++ options.source, options.check)) ++ ++ if options.source and len(results.results) == 0: ++ for plugin in plugins: ++ if not source_or_check_matches(plugin, options.source, ++ options.check): ++ continue ++ ++ if not set(plugin.requires).issubset(available): ++ print("Source '%s' is missing one or more requirements '%s'" % ++ (options.source, ', '.join(plugin.requires))) ++ return 1 ++ ++ if options.check: ++ print("Check '%s' not found in Source '%s'" % ++ (options.check, options.source)) ++ else: ++ print("Source '%s' not found" % options.source) ++ return 1 ++ ++ try: ++ output.render(results) ++ except Exception as e: ++ logger.error('Output raised %s: %s', e.__class__.__name__, e) ++ ++ return_value = 0 ++ for result in results.results: ++ if result.result != constants.SUCCESS: ++ return_value = 1 ++ break ++ ++ return return_value +diff --git a/src/ipahealthcheck/core/main.py b/src/ipahealthcheck/core/main.py +index 2b818d4..d9e85d7 100644 +--- a/src/ipahealthcheck/core/main.py ++++ b/src/ipahealthcheck/core/main.py +@@ -2,276 +2,11 @@ + # Copyright (C) 2019 FreeIPA Contributors see COPYING for license + # + +-import argparse +-import json +-import logging + from os import environ +-import pkg_resources + import sys + +-from datetime import datetime +- +-from ipahealthcheck.core.config import read_config +-from ipahealthcheck.core.plugin import Result, Results, json_to_results +-from ipahealthcheck.core.output import output_registry + from ipahealthcheck.core import constants +-from ipahealthcheck.core.service import ServiceCheck +- +-logging.basicConfig(format='%(message)s') +-logger = logging.getLogger() +- +- +-def find_registries(entry_points): +- registries = {} +- for entry_point in entry_points: +- registries.update({ +- ep.name: ep.resolve() +- for ep in pkg_resources.iter_entry_points(entry_point) +- }) +- return registries +- +- +-def find_plugins(name, registry): +- for ep in pkg_resources.iter_entry_points(name): +- # load module +- ep.load() +- return registry.get_plugins() +- +- +-def run_plugin(plugin, available=()): +- # manually calculate duration when we create results of our own +- start = datetime.utcnow() +- try: +- for result in plugin.check(): +- if result is None: +- # Treat no result as success, fudge start time +- result = Result(plugin, constants.SUCCESS, start=start) +- yield result +- except Exception as e: +- logger.debug('Exception raised: %s', e) +- yield Result(plugin, constants.CRITICAL, exception=str(e), +- start=start) +- +- +-def source_or_check_matches(plugin, source, check): +- """Determine whether a given a plugin matches if a source +- and optional check are provided. +- """ +- if source is not None and plugin.__module__ != source: +- return False +- +- if check and plugin.__class__.__name__ != check: +- return False +- +- return True +- +- +-def run_service_plugins(plugins, config, source, check): +- """Execute plugins with the base class of ServiceCheck +- +- This is a specialized check to use systemd to determine +- if a service is running or not. +- """ +- results = Results() +- available = [] +- +- for plugin in plugins: +- if not isinstance(plugin, ServiceCheck): +- continue +- +- logger.debug('Calling check %s', plugin) +- for result in plugin.check(): +- # always run the service checks so dependencies work +- if result is not None and result.result == constants.SUCCESS: +- available.append(plugin.service.service_name) +- if not source_or_check_matches(plugin, source, check): +- continue +- if result is not None: +- results.add(result) +- +- return results, set(available) +- +- +-def run_plugins(plugins, config, available, source, check): +- """Execute plugins without the base class of ServiceCheck +- +- These are the remaining, non-service checking checks +- that do validation for various parts of a system. +- """ +- results = Results() +- +- for plugin in plugins: +- if isinstance(plugin, ServiceCheck): +- continue +- +- if not source_or_check_matches(plugin, source, check): +- continue +- +- logger.debug('Calling check %s' % plugin) +- plugin.config = config +- if not set(plugin.requires).issubset(available): +- logger.debug('Skipping %s:%s because %s service(s) not running', +- plugin.__class__.__module__, +- plugin.__class__.__name__, +- ', '.join(set(plugin.requires) - available)) +- # Not providing a Result in this case because if a required +- # service isn't available then this could generate a lot of +- # false positives. +- else: +- for result in run_plugin(plugin, available): +- results.add(result) +- +- return results +- +- +-def list_sources(plugins): +- """Print list of all sources and checks""" +- source = None +- for plugin in plugins: +- if source != plugin.__class__.__module__: +- print(plugin.__class__.__module__) +- source = plugin.__class__.__module__ +- print(" ", plugin.__class__.__name__) +- +- return 0 +- +- +-def parse_options(output_registry): +- output_names = [plugin.__name__.lower() for +- plugin in output_registry.plugins] +- parser = argparse.ArgumentParser() +- parser.add_argument('--debug', dest='debug', action='store_true', +- default=False, help='Include debug output') +- parser.add_argument('--list-sources', dest='list_sources', +- action='store_true', default=False, +- help='List all available sources') +- parser.add_argument('--source', dest='source', +- default=None, +- help='Source of checks, e.g. ipahealthcheck.foo.bar') +- parser.add_argument('--check', dest='check', +- default=None, +- help='Check to execute, e.g. BazCheck') +- parser.add_argument('--output-type', dest='output', choices=output_names, +- default='json', help='Output method') +- parser.add_argument('--output-file', dest='outfile', default=None, +- help='File to store output') +- parser.add_argument('--input-file', dest='infile', +- help='File to read as input') +- parser.add_argument('--failures-only', dest='failures_only', +- action='store_true', default=False, +- help='Exclude SUCCESS results on output') +- parser.add_argument('--severity', dest='severity', action="append", +- help='Include only the selected severity(s)', +- choices=[key for key in constants._nameToLevel]) +- for plugin in output_registry.plugins: +- onelinedoc = plugin.__doc__.split('\n\n', 1)[0].strip() +- group = parser.add_argument_group(plugin.__name__.lower(), +- onelinedoc) +- for option in plugin.options: +- group.add_argument(option[0], **option[1]) +- +- options = parser.parse_args() +- +- # Validation +- if options.check and not options.source: +- print("--source is required when --check is used") +- sys.exit(1) +- +- return options +- +- +-def limit_results(results, source, check): +- """Return ony those results which match source and/or check""" +- new_results = Results() +- for result in results.results: +- if result.source == source: +- if check is None or result.check == check: +- new_results.add(result) +- return new_results +- +- +-def run_healthcheck(entry_points, configfile): +- framework = object() +- plugins = [] +- output = constants.DEFAULT_OUTPUT +- +- logger.setLevel(logging.INFO) +- +- options = parse_options(output_registry) +- +- if options.debug: +- logger.setLevel(logging.DEBUG) +- +- config = read_config(configfile) +- if config is None: +- sys.exit(1) +- +- for name, registry in find_registries(entry_points).items(): +- try: +- registry.initialize(framework) +- except Exception as e: +- print("Unable to initialize %s: %s" % (name, e)) +- sys.exit(1) +- for plugin in find_plugins(name, registry): +- plugins.append(plugin) +- +- for out in output_registry.plugins: +- if out.__name__.lower() == options.output: +- output = out(options) +- +- if options.list_sources: +- return list_sources(plugins) +- +- if options.infile: +- try: +- with open(options.infile, 'r') as f: +- raw_data = f.read() +- +- json_data = json.loads(raw_data) +- results = json_to_results(json_data) +- available = () +- except Exception as e: +- print("Unable to import '%s': %s" % (options.infile, e)) +- sys.exit(1) +- if options.source: +- results = limit_results(results, options.source, options.check) +- else: +- results, available = run_service_plugins(plugins, config, +- options.source, +- options.check) +- results.extend(run_plugins(plugins, config, available, +- options.source, options.check)) +- +- if options.source and len(results.results) == 0: +- for plugin in plugins: +- if not source_or_check_matches(plugin, options.source, +- options.check): +- continue +- +- if not set(plugin.requires).issubset(available): +- print("Source '%s' is missing one or more requirements '%s'" % +- (options.source, ', '.join(plugin.requires))) +- sys.exit(1) +- +- if options.check: +- print("Check '%s' not found in Source '%s'" % +- (options.check, options.source)) +- else: +- print("Source '%s' not found" % options.source) +- sys.exit(1) +- +- try: +- output.render(results) +- except Exception as e: +- logger.error('Output raised %s: %s', e.__class__.__name__, e) +- +- return_value = 0 +- for result in results.results: +- if result.result != constants.SUCCESS: +- return_value = 1 +- break +- +- return return_value ++from ipahealthcheck.core.core import run_healthcheck + + + def main(): +-- +2.20.1 + diff --git a/SOURCES/0005-Convert-running-healthchecks-into-a-class-and-add-pr.patch b/SOURCES/0005-Convert-running-healthchecks-into-a-class-and-add-pr.patch new file mode 100644 index 0000000..7a071e4 --- /dev/null +++ b/SOURCES/0005-Convert-running-healthchecks-into-a-class-and-add-pr.patch @@ -0,0 +1,228 @@ +From af5c169151f0697d34954ac5c9dd0686dc73ef3f Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 18 Nov 2019 15:13:44 -0500 +Subject: [PATCH 5/5] Convert running healthchecks into a class and add + pre-check + +Add a pre-check so that dependencies can be checked per product. +In the case of IPA verify that the server is installed otherwise +bail out. +--- + src/ipahealthcheck/core/core.py | 168 +++++++++++++++++--------------- + src/ipahealthcheck/core/main.py | 17 +++- + 2 files changed, 104 insertions(+), 81 deletions(-) + +diff --git a/src/ipahealthcheck/core/core.py b/src/ipahealthcheck/core/core.py +index 182eac3..aef6197 100644 +--- a/src/ipahealthcheck/core/core.py ++++ b/src/ipahealthcheck/core/core.py +@@ -188,85 +188,97 @@ def limit_results(results, source, check): + return new_results + + +-def run_healthcheck(entry_points, configfile): +- framework = object() +- plugins = [] +- output = constants.DEFAULT_OUTPUT +- +- logger.setLevel(logging.INFO) +- +- options = parse_options(output_registry) +- +- if options.debug: +- logger.setLevel(logging.DEBUG) +- +- config = read_config(configfile) +- if config is None: +- return 1 +- +- for name, registry in find_registries(entry_points).items(): +- try: +- registry.initialize(framework) +- except Exception as e: +- print("Unable to initialize %s: %s" % (name, e)) +- return 1 +- for plugin in find_plugins(name, registry): +- plugins.append(plugin) +- +- for out in output_registry.plugins: +- if out.__name__.lower() == options.output: +- output = out(options) +- +- if options.list_sources: +- return list_sources(plugins) +- +- if options.infile: +- try: +- with open(options.infile, 'r') as f: +- raw_data = f.read() +- +- json_data = json.loads(raw_data) +- results = json_to_results(json_data) +- available = () +- except Exception as e: +- print("Unable to import '%s': %s" % (options.infile, e)) ++class RunChecks: ++ def __init__(self, entry_points, configfile): ++ self.entry_points = entry_points ++ self.configfile = configfile ++ ++ def pre_check(self): ++ pass ++ ++ def run_healthcheck(self): ++ framework = object() ++ plugins = [] ++ output = constants.DEFAULT_OUTPUT ++ ++ logger.setLevel(logging.INFO) ++ ++ options = parse_options(output_registry) ++ ++ if options.debug: ++ logger.setLevel(logging.DEBUG) ++ ++ config = read_config(self.configfile) ++ if config is None: + return 1 +- if options.source: +- results = limit_results(results, options.source, options.check) +- else: +- results, available = run_service_plugins(plugins, config, +- options.source, +- options.check) +- results.extend(run_plugins(plugins, config, available, +- options.source, options.check)) +- +- if options.source and len(results.results) == 0: +- for plugin in plugins: +- if not source_or_check_matches(plugin, options.source, +- options.check): +- continue + +- if not set(plugin.requires).issubset(available): +- print("Source '%s' is missing one or more requirements '%s'" % +- (options.source, ', '.join(plugin.requires))) ++ rval = self.pre_check() ++ if rval is not None: ++ return rval ++ ++ for name, registry in find_registries(self.entry_points).items(): ++ try: ++ registry.initialize(framework) ++ except Exception as e: ++ print("Unable to initialize %s: %s" % (name, e)) + return 1 +- +- if options.check: +- print("Check '%s' not found in Source '%s'" % +- (options.check, options.source)) ++ for plugin in find_plugins(name, registry): ++ plugins.append(plugin) ++ ++ for out in output_registry.plugins: ++ if out.__name__.lower() == options.output: ++ output = out(options) ++ ++ if options.list_sources: ++ return list_sources(plugins) ++ ++ if options.infile: ++ try: ++ with open(options.infile, 'r') as f: ++ raw_data = f.read() ++ ++ json_data = json.loads(raw_data) ++ results = json_to_results(json_data) ++ available = () ++ except Exception as e: ++ print("Unable to import '%s': %s" % (options.infile, e)) ++ return 1 ++ if options.source: ++ results = limit_results(results, options.source, options.check) + else: +- print("Source '%s' not found" % options.source) +- return 1 +- +- try: +- output.render(results) +- except Exception as e: +- logger.error('Output raised %s: %s', e.__class__.__name__, e) +- +- return_value = 0 +- for result in results.results: +- if result.result != constants.SUCCESS: +- return_value = 1 +- break +- +- return return_value ++ results, available = run_service_plugins(plugins, config, ++ options.source, ++ options.check) ++ results.extend(run_plugins(plugins, config, available, ++ options.source, options.check)) ++ ++ if options.source and len(results.results) == 0: ++ for plugin in plugins: ++ if not source_or_check_matches(plugin, options.source, ++ options.check): ++ continue ++ ++ if not set(plugin.requires).issubset(available): ++ print("Source '%s' is missing one or more requirements '%s'" % ++ (options.source, ', '.join(plugin.requires))) ++ return 1 ++ ++ if options.check: ++ print("Check '%s' not found in Source '%s'" % ++ (options.check, options.source)) ++ else: ++ print("Source '%s' not found" % options.source) ++ return 1 ++ ++ try: ++ output.render(results) ++ except Exception as e: ++ logger.error('Output raised %s: %s', e.__class__.__name__, e) ++ ++ return_value = 0 ++ for result in results.results: ++ if result.result != constants.SUCCESS: ++ return_value = 1 ++ break ++ ++ return return_value +diff --git a/src/ipahealthcheck/core/main.py b/src/ipahealthcheck/core/main.py +index d9e85d7..63a1de6 100644 +--- a/src/ipahealthcheck/core/main.py ++++ b/src/ipahealthcheck/core/main.py +@@ -6,12 +6,23 @@ from os import environ + import sys + + from ipahealthcheck.core import constants +-from ipahealthcheck.core.core import run_healthcheck ++from ipahealthcheck.core.core import RunChecks ++ ++from ipaserver.install.installutils import is_ipa_configured ++ ++ ++class IPAChecks(RunChecks): ++ def pre_check(self): ++ if not is_ipa_configured(): ++ print("IPA is not configured") ++ return 1 + + + def main(): + environ["KRB5_CLIENT_KTNAME"] = "/etc/krb5.keytab" + environ["KRB5CCNAME"] = "MEMORY:" + +- sys.exit(run_healthcheck(['ipahealthcheck.registry'], +- constants.CONFIG_FILE)) ++ ipachecks = IPAChecks(['ipahealthcheck.registry', ++ 'pkihealthcheck.registry'], ++ constants.CONFIG_FILE) ++ sys.exit(ipachecks.run_healthcheck()) +-- +2.20.1 + diff --git a/SPECS/ipa-healthcheck.spec b/SPECS/ipa-healthcheck.spec index 1a66f7b..8c1c027 100644 --- a/SPECS/ipa-healthcheck.spec +++ b/SPECS/ipa-healthcheck.spec @@ -7,29 +7,46 @@ Name: ipa-healthcheck -Version: 0.2 +Version: 0.4 Release: 3%{?dist} Summary: Health check tool for IdM BuildArch: noarch License: GPLv3 -URL: https://github.com/%{project}/%{name} -Source0: https://github.com/%{project}/%{name}/archive/release-%{version}.tar.gz#/%{project}-%{shortname}-%{version}.tar.gz +URL: https://github.com/%{project}/freeipa-healthcheck +Source0: https://github.com/%{project}/%{name}/archive/%{version}.tar.gz#/%{project}-%{shortname}-%{version}.tar.gz Source1: %{longname}.conf + +Patch0001: 0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch +Patch0002: 0002-Move-main-to-run_healthcheck-for-abstraction-purpose.patch +Patch0003: 0003-Abstract-ServiceCheck-to-not-be-IPA-specific.patch +Patch0004: 0004-Move-the-abstracted-plugin-runner-code-into-a-separa.patch +Patch0005: 0005-Convert-running-healthchecks-into-a-class-and-add-pr.patch + Requires: ipa-server Requires: python3-ipalib Requires: python3-ipaserver +# cronie-anacron provides anacron +Requires: anacron +Requires: logrotate Requires(post): systemd-units +Requires: %{name}-core = %{version}-%{release} BuildRequires: python3-devel BuildRequires: systemd-devel %{?systemd_requires} -Patch: 0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch - %description The FreeIPA health check tool provides a set of checks to proactively detect defects in a FreeIPA cluster. +%package -n %{name}-core +Summary: Core plugin system for healthcheck +# to allow package downgrades where this subpackage doesn't exist +Obsoletes: %{name} < 0.4 + +%description -n %{name}-core +Core files + %prep %autosetup -p1 -n %{project}-%{shortname}-%{version} @@ -41,42 +58,54 @@ proactively detect defects in a FreeIPA cluster. %install %py3_install + mkdir -p %{buildroot}%{_sysconfdir}/%{longname} install -m644 %{SOURCE1} %{buildroot}%{_sysconfdir}/%{longname} + mkdir -p %{buildroot}/%{_unitdir} +install -p -m644 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.service %{buildroot}%{_unitdir} +install -p -m644 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.timer %{buildroot}%{_unitdir} + +mkdir -p %{buildroot}/%{_libexecdir}/ipa +install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.sh %{buildroot}%{_libexecdir}/ipa/ + +mkdir -p %{buildroot}%{_sysconfdir}/logrotate.d +install -p -m644 %{_builddir}/%{project}-%{shortname}-%{version}/logrotate/%{longname} %{buildroot}%{_sysconfdir}/logrotate.d -install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.service %{buildroot}%{_unitdir} -install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.timer %{buildroot}%{_unitdir} -mkdir -p %{buildroot}/%{_libexecdir} -install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/systemd/ipa-%{shortname}.sh %{buildroot}%{_libexecdir}/ +mkdir -p %{buildroot}/%{_localstatedir}/log/ipa/%{shortname} mkdir -p %{buildroot}/%{_mandir}/man1 mkdir -p %{buildroot}/%{_mandir}/man5 -install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/man/man1/ipa-%{shortname}.1 %{buildroot}%{_mandir}/man1/ -install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/man/man5/%{longname}.conf.5 %{buildroot}%{_mandir}/man5/ - -#%check -#%{__python3} setup.py test +install -p -m644 %{_builddir}/%{project}-%{shortname}-%{version}/man/man1/ipa-%{shortname}.1 %{buildroot}%{_mandir}/man1/ +install -p -m644 %{_builddir}/%{project}-%{shortname}-%{version}/man/man5/%{longname}.conf.5 %{buildroot}%{_mandir}/man5/ +(cd %{buildroot}/%{python3_sitelib}/ipahealthcheck && find . -type f | \ + grep -v '^./core' | \ + grep -v 'opt-1' | \ + sed -e 's,\.py.*$,.*,g' | sort -u | \ + sed -e 's,\./,%%{python3_sitelib}/ipahealthcheck/,g' ) >healthcheck.list %post %systemd_post ipa-%{shortname}.service + %preun %systemd_preun ipa-%{shortname}.service + %postun %systemd_postun_with_restart ipa-%{shortname}.service -%files +%files -f healthcheck.list %{!?_licensedir:%global license %%doc} %license COPYING %doc README.md %{_bindir}/ipa-%{shortname} %dir %{_sysconfdir}/%{longname} +%dir %{_localstatedir}/log/ipa/%{shortname} %config(noreplace) %{_sysconfdir}/%{longname}/%{longname}.conf -%{python3_sitelib}/%{longname}/ +%config(noreplace) %{_sysconfdir}/logrotate.d/%{longname} %{python3_sitelib}/%{longname}-%{version}-*.egg-info/ %{python3_sitelib}/%{longname}-%{version}-*-nspkg.pth %{_unitdir}/* @@ -84,8 +113,46 @@ install -p -m755 %{_builddir}/%{project}-%{shortname}-%{version}/man/man5/%{long %{_mandir}/man1/* %{_mandir}/man5/* +%files -n %{name}-core +%{!?_licensedir:%global license %%doc} +%license COPYING +%doc README.md +%{python3_sitelib}/%{longname}/core/ + %changelog +* Thu Dec 5 2019 Rob Crittenden - 0.4-3 +- Add Obsoletes to core subpackage (#1780121) + +* Mon Dec 2 2019 Rob Crittenden - 0.4-2 +- Abstract processing so core package is standalone (#1771710) + +* Mon Dec 2 2019 Rob Crittenden - 0.4-1 +- Rebase to upstream 0.4 (#1770346) +- Create subpackage to split out core processing (#1771710) +- Correct URL (#1773512) +- Errors not translated to strings (#1752849) +- JSON output not indented by default (#1729043) +- Add dependencies to checks to avoid false-positives (#1727900) +- Verify expected DNS records (#1695125) + +* Mon Aug 12 2019 Rob Crittenden - 0.3-4 +- Lookup AD user by SID and not by hardcoded username (#1739500) + +* Thu Aug 8 2019 Rob Crittenden - 0.3-3 +- The AD trust agent and controller are not being initialized (#1738314) + +* Mon Aug 5 2019 Rob Crittenden - 0.3-2 +- Change DNA plugin to return WARNING if no range is set (#1737492) + +* Mon Jul 29 2019 François Cami - 0.3-1 +- Update to upstream 0.3 (#1701351) +- Add logrotate configs + depend on anacron and logrotate (#1729207) + +* Thu Jul 11 2019 François Cami - 0.2-4 +- Fix ipa-healthcheck.sh installation path (rhbz#1729188) +- Create and own log directory (rhbz#1729188) + * Tue Apr 30 2019 François Cami - 0.2-3 - Add python3-lib389 to BRs