diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..137847c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +SOURCES/freeipa-healthcheck-0.3.tar.gz diff --git a/.ipa-healthcheck.metadata b/.ipa-healthcheck.metadata new file mode 100644 index 0000000..8b355c4 --- /dev/null +++ b/.ipa-healthcheck.metadata @@ -0,0 +1 @@ +d06ba28575381405cf0e303f6ab388484c768899 SOURCES/freeipa-healthcheck-0.3.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 new file mode 100644 index 0000000..ed363bc --- /dev/null +++ b/SOURCES/0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch @@ -0,0 +1,26 @@ +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 + available + +We won't be executing make check because the dependencies aren't +available due to IDM being in a module. +--- + setup.py | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/setup.py b/setup.py +index 801323f..c3cd215 100644 +--- a/setup.py ++++ b/setup.py +@@ -60,6 +60,5 @@ setup( + 'Programming Language :: Python :: 3.6', + ], + python_requires='!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', +- setup_requires=['pytest-runner',], + tests_require=['pytest',], + ) +-- +2.17.2 + diff --git a/SOURCES/0002-Change-DNA-no-range-from-SUCCESS-to-WARNING.patch b/SOURCES/0002-Change-DNA-no-range-from-SUCCESS-to-WARNING.patch new file mode 100644 index 0000000..1cbb6e9 --- /dev/null +++ b/SOURCES/0002-Change-DNA-no-range-from-SUCCESS-to-WARNING.patch @@ -0,0 +1,50 @@ +From acd807b6e054b06b7b6fafe86741e00bae7d2527 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 30 Jul 2019 17:25:30 -0400 +Subject: [PATCH] Change DNA no range from SUCCESS to WARNING + +Elevate the not set version to WARNING so admins can be aware in +advance that with no range then uid/gid cannot be allocated from +this master. This is particularly important if the only master with +a range defined is retired. + +https://github.com/freeipa/freeipa-healthcheck/issues/60 +--- + src/ipahealthcheck/ipa/dna.py | 6 ++++-- + tests/test_ipa_dna.py | 2 +- + 2 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/src/ipahealthcheck/ipa/dna.py b/src/ipahealthcheck/ipa/dna.py +index 9dd2ffa..4d85057 100644 +--- a/src/ipahealthcheck/ipa/dna.py ++++ b/src/ipahealthcheck/ipa/dna.py +@@ -41,9 +41,11 @@ class IPADNARangeCheck(IPAPlugin): + next_start=next_start or 0, + next_max=next_max or 0) + else: +- yield Result(self, constants.SUCCESS, ++ yield Result(self, constants.WARNING, + range_start=0, + range_max=0, + next_start=0, + next_max=0, +- msg='No range defined') ++ msg='No DNA range defined. If no masters define a ' ++ 'range then users and groups cannot be ' ++ 'created.') +diff --git a/tests/test_ipa_dna.py b/tests/test_ipa_dna.py +index 0ff7dd4..3d5dd3e 100644 +--- a/tests/test_ipa_dna.py ++++ b/tests/test_ipa_dna.py +@@ -66,7 +66,7 @@ class TestDNARange(BaseTest): + assert len(self.results) == 1 + + result = self.results.results[0] +- assert result.result == constants.SUCCESS ++ assert result.result == constants.WARNING + assert result.source == 'ipahealthcheck.ipa.dna' + assert result.check == 'IPADNARangeCheck' + assert result.kw.get('range_start') == 0 +-- +2.20.1 + diff --git a/SOURCES/0003-Always-initialize-AD-roles-even-if-the-IPA-API-is-in.patch b/SOURCES/0003-Always-initialize-AD-roles-even-if-the-IPA-API-is-in.patch new file mode 100644 index 0000000..e585c76 --- /dev/null +++ b/SOURCES/0003-Always-initialize-AD-roles-even-if-the-IPA-API-is-in.patch @@ -0,0 +1,45 @@ +From 5fb1b2049889705d2cda60d745be5b1dacb23146 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 6 Aug 2019 18:00:50 +0000 +Subject: [PATCH 3/8] Always initialize AD roles even if the IPA API is + initialized + +The setting of these roles in the registry were guarded by whether +the IPA API was initialized. If another plugin intialized the API +then these values weren't being set, effectively disabling the +AD Trust checks. + +https://github.com/freeipa/freeipa-healthcheck/issues/62 +--- + src/ipahealthcheck/ipa/plugin.py | 14 ++++++-------- + 1 file changed, 6 insertions(+), 8 deletions(-) + +diff --git a/src/ipahealthcheck/ipa/plugin.py b/src/ipahealthcheck/ipa/plugin.py +index a73a7df..c4cef9b 100644 +--- a/src/ipahealthcheck/ipa/plugin.py ++++ b/src/ipahealthcheck/ipa/plugin.py +@@ -40,15 +40,13 @@ class IPARegistry(Registry): + def initialize(self, framework): + installutils.check_server_configuration() + +- if api.isdone('finalize'): +- return +- +- if not api.isdone('bootstrap'): +- api.bootstrap(in_server=True, +- context='ipahealthcheck', +- log=None) + if not api.isdone('finalize'): +- api.finalize() ++ if not api.isdone('bootstrap'): ++ api.bootstrap(in_server=True, ++ context='ipahealthcheck', ++ log=None) ++ if not api.isdone('finalize'): ++ api.finalize() + + if not api.Backend.ldap2.isconnected(): + try: +-- +2.20.1 + diff --git a/SOURCES/0004-Create-a-default-set-of-mock-to-always-apply-to-all-.patch b/SOURCES/0004-Create-a-default-set-of-mock-to-always-apply-to-all-.patch new file mode 100644 index 0000000..a8db556 --- /dev/null +++ b/SOURCES/0004-Create-a-default-set-of-mock-to-always-apply-to-all-.patch @@ -0,0 +1,449 @@ +From 14c7619284c5d29507b74d87c01b7c2d362be5c5 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 6 Aug 2019 16:00:24 -0400 +Subject: [PATCH 4/8] Create a default set of mock to always apply to all test + cases + +There are some common cases related to initialization where it makes +sense to centralize them in the base class. +--- + tests/base.py | 15 ++++++++++++- + tests/test_dogtag_ca.py | 2 -- + tests/test_dogtag_connectivity.py | 2 -- + tests/test_ds_replication.py | 7 +----- + tests/test_ipa_agent.py | 2 -- + tests/test_ipa_certfile_expiration.py | 2 -- + tests/test_ipa_certmonger_ca.py | 2 -- + tests/test_ipa_dna.py | 7 +----- + tests/test_ipa_expiration.py | 2 -- + tests/test_ipa_nssdb.py | 2 -- + tests/test_ipa_nssvalidation.py | 2 -- + tests/test_ipa_opensslvalidation.py | 2 -- + tests/test_ipa_revocation.py | 2 -- + tests/test_ipa_roles.py | 12 +--------- + tests/test_ipa_topology.py | 6 ----- + tests/test_ipa_tracking.py | 2 -- + tests/test_ipa_trust.py | 32 --------------------------- + tests/test_meta_services.py | 6 ----- + 18 files changed, 17 insertions(+), 90 deletions(-) + +diff --git a/tests/base.py b/tests/base.py +index 9a54941..8b9e37c 100644 +--- a/tests/base.py ++++ b/tests/base.py +@@ -17,18 +17,31 @@ class BaseTest(TestCase): + + If a test needs a particular value then it will need to use + @patch individually. ++ ++ A default set of Mock patches is set because they apply to all or ++ nearly all test cases. + """ ++ default_patches = { ++ 'ipaserver.install.installutils.check_server_configuration': ++ mock.Mock(return_value=None), ++ } ++ patches = {} + + def setup_class(self): + # collect the list of patches to be applied for this class of + # tests ++ self.default_patches.update(self.patches) ++ + self.applied_patches = [ +- mock.patch(patch, data) for patch, data in self.patches.items() ++ mock.patch(patch, data) for patch, data in ++ self.default_patches.items() + ] + + for patch in self.applied_patches: + patch.start() + ++ self.results = None ++ + def teardown_class(self): + mock.patch.stopall() + +diff --git a/tests/test_dogtag_ca.py b/tests/test_dogtag_ca.py +index 5c7faed..b5c5351 100644 +--- a/tests/test_dogtag_ca.py ++++ b/tests/test_dogtag_ca.py +@@ -37,8 +37,6 @@ class mock_CertDB: + + class TestCACerts(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), + 'ipaserver.install.krainstance.KRAInstance': +diff --git a/tests/test_dogtag_connectivity.py b/tests/test_dogtag_connectivity.py +index ad57bdf..544e325 100644 +--- a/tests/test_dogtag_connectivity.py ++++ b/tests/test_dogtag_connectivity.py +@@ -14,8 +14,6 @@ from ipalib.errors import CertificateOperationError + + class TestCAConnectivity(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), + } +diff --git a/tests/test_ds_replication.py b/tests/test_ds_replication.py +index dd2bfc9..b6b3652 100644 +--- a/tests/test_ds_replication.py ++++ b/tests/test_ds_replication.py +@@ -4,7 +4,7 @@ + + import pytest + from base import BaseTest +-from unittest.mock import Mock, patch ++from unittest.mock import patch + from util import capture_results, m_api + + from ipahealthcheck.core import config, constants +@@ -37,11 +37,6 @@ class mock_ldap: + + + class TestReplicationConflicts(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + @pytest.mark.skipif(NUM_VERSION < 40790, + reason="no way of currently testing this") + @patch('ipapython.ipaldap.LDAPClient.from_realm') +diff --git a/tests/test_ipa_agent.py b/tests/test_ipa_agent.py +index f614bb1..c58c7a6 100644 +--- a/tests/test_ipa_agent.py ++++ b/tests/test_ipa_agent.py +@@ -56,8 +56,6 @@ class mock_ldap_conn: + class TestNSSAgent(BaseTest): + cert = IPACertificate() + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + 'ipaserver.install.cainstance.CAInstance': +diff --git a/tests/test_ipa_certfile_expiration.py b/tests/test_ipa_certfile_expiration.py +index 61cb29b..d5601c5 100644 +--- a/tests/test_ipa_certfile_expiration.py ++++ b/tests/test_ipa_certfile_expiration.py +@@ -24,8 +24,6 @@ class IPACertificate: + + class TestIPACertificateFile(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipahealthcheck.ipa.certs.get_expected_requests': + Mock(return_value=get_expected_requests()), + 'ipalib.install.certmonger._cm_dbus_object': +diff --git a/tests/test_ipa_certmonger_ca.py b/tests/test_ipa_certmonger_ca.py +index ac56a60..4eec1ba 100644 +--- a/tests/test_ipa_certmonger_ca.py ++++ b/tests/test_ipa_certmonger_ca.py +@@ -12,8 +12,6 @@ from unittest.mock import Mock, patch + + class TestCertmonger(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), + } +diff --git a/tests/test_ipa_dna.py b/tests/test_ipa_dna.py +index 3d5dd3e..5450642 100644 +--- a/tests/test_ipa_dna.py ++++ b/tests/test_ipa_dna.py +@@ -3,7 +3,7 @@ + # + + from base import BaseTest +-from unittest.mock import Mock, patch ++from unittest.mock import patch + from util import capture_results + + from ipahealthcheck.core import config, constants +@@ -27,11 +27,6 @@ class mock_ReplicationManager: + + + class TestDNARange(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + @patch('ipaserver.install.replication.ReplicationManager') + def test_dnarange_set(self, mock_manager): + mock_manager.return_value = mock_ReplicationManager(start=1, max=100) +diff --git a/tests/test_ipa_expiration.py b/tests/test_ipa_expiration.py +index f29f319..4c177f8 100644 +--- a/tests/test_ipa_expiration.py ++++ b/tests/test_ipa_expiration.py +@@ -17,8 +17,6 @@ from datetime import datetime, timedelta, timezone + + class TestExpiration(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipahealthcheck.ipa.certs.get_expected_requests': + Mock(return_value=get_expected_requests()), + 'ipalib.install.certmonger._cm_dbus_object': +diff --git a/tests/test_ipa_nssdb.py b/tests/test_ipa_nssdb.py +index 67b9a55..7d5664e 100644 +--- a/tests/test_ipa_nssdb.py ++++ b/tests/test_ipa_nssdb.py +@@ -28,8 +28,6 @@ def my_unparse_trust_flags(trust_flags): + + class TestNSSDBTrust(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), + 'ipapython.certdb.unparse_trust_flags': +diff --git a/tests/test_ipa_nssvalidation.py b/tests/test_ipa_nssvalidation.py +index ba93ba5..1e567d8 100644 +--- a/tests/test_ipa_nssvalidation.py ++++ b/tests/test_ipa_nssvalidation.py +@@ -19,8 +19,6 @@ class DsInstance: + + class TestNSSValidation(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipahealthcheck.ipa.certs.get_dogtag_cert_password': + Mock(return_value='foo'), + 'ipaserver.install.dsinstance.DsInstance': +diff --git a/tests/test_ipa_opensslvalidation.py b/tests/test_ipa_opensslvalidation.py +index 74751e3..0d334cd 100644 +--- a/tests/test_ipa_opensslvalidation.py ++++ b/tests/test_ipa_opensslvalidation.py +@@ -14,8 +14,6 @@ from ipapython.ipautil import _RunResult + + class TestOpenSSLValidation(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), + } +diff --git a/tests/test_ipa_revocation.py b/tests/test_ipa_revocation.py +index 3d1ea84..39cf3e7 100644 +--- a/tests/test_ipa_revocation.py ++++ b/tests/test_ipa_revocation.py +@@ -23,8 +23,6 @@ class IPACertificate: + + class TestRevocation(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipaserver.install.certs.is_ipa_issued_cert': + Mock(return_value=True), + 'ipalib.x509.load_certificate_from_file': +diff --git a/tests/test_ipa_roles.py b/tests/test_ipa_roles.py +index 35c7a1d..453db06 100644 +--- a/tests/test_ipa_roles.py ++++ b/tests/test_ipa_roles.py +@@ -3,7 +3,7 @@ + # + + from base import BaseTest +-from unittest.mock import Mock, patch ++from unittest.mock import patch + from util import capture_results, CAInstance + from util import m_api + +@@ -14,11 +14,6 @@ from ipahealthcheck.ipa.roles import (IPACRLManagerCheck, + + + class TestCRLManagerRole(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + @patch('ipaserver.install.cainstance.CAInstance') + def test_not_crlmanager(self, mock_ca): + mock_ca.return_value = CAInstance(crlgen=False) +@@ -57,11 +52,6 @@ class TestCRLManagerRole(BaseTest): + + + class TestRenewalMaster(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_renewal_master_not_set(self): + framework = object() + registry.initialize(framework) +diff --git a/tests/test_ipa_topology.py b/tests/test_ipa_topology.py +index ebf7657..a4ff6d9 100644 +--- a/tests/test_ipa_topology.py ++++ b/tests/test_ipa_topology.py +@@ -5,7 +5,6 @@ + from util import capture_results + from util import m_api + from base import BaseTest +-from unittest.mock import Mock + + from ipahealthcheck.core import config, constants + from ipahealthcheck.ipa.plugin import registry +@@ -13,11 +12,6 @@ from ipahealthcheck.ipa.topology import IPATopologyDomainCheck + + + class TestTopology(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_topology_ok(self): + m_api.Command.topologysuffix_verify.side_effect = [ + { +diff --git a/tests/test_ipa_tracking.py b/tests/test_ipa_tracking.py +index abcb9af..c40d8f4 100644 +--- a/tests/test_ipa_tracking.py ++++ b/tests/test_ipa_tracking.py +@@ -15,8 +15,6 @@ from mock_certmonger import get_expected_requests, set_requests + + class TestTracking(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ipahealthcheck.ipa.certs.get_expected_requests': + Mock(return_value=get_expected_requests()), + 'ipalib.install.certmonger._cm_dbus_object': +diff --git a/tests/test_ipa_trust.py b/tests/test_ipa_trust.py +index 2dcc4b3..0a1d58c 100644 +--- a/tests/test_ipa_trust.py ++++ b/tests/test_ipa_trust.py +@@ -102,11 +102,6 @@ class SSSDConfig(): + + + class TestTrustAgent(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_no_trust_agent(self): + framework = object() + registry.initialize(framework) +@@ -182,11 +177,6 @@ class TestTrustAgent(BaseTest): + + + class TestTrustDomains(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_no_trust_agent(self): + framework = object() + registry.initialize(framework) +@@ -371,11 +361,6 @@ class TestTrustDomains(BaseTest): + + + class TestTrustCatalog(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_no_trust_agent(self): + framework = object() + registry.initialize(framework) +@@ -477,8 +462,6 @@ class TestTrustCatalog(BaseTest): + + class Testsidgen(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -562,8 +545,6 @@ class Testsidgen(BaseTest): + + class TestTrustAgentMember(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -642,8 +623,6 @@ class TestTrustAgentMember(BaseTest): + + class TestControllerPrincipal(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -724,8 +703,6 @@ class TestControllerPrincipal(BaseTest): + + class TestControllerService(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -798,8 +775,6 @@ class TestControllerService(BaseTest): + + class TestControllerGroupSID(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -877,8 +852,6 @@ class TestControllerGroupSID(BaseTest): + + class TestControllerConf(BaseTest): + patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), + 'ldap.initialize': + Mock(return_value=mock_ldap_conn()), + } +@@ -922,11 +895,6 @@ class TestControllerConf(BaseTest): + + + class TestPackageCheck(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_agent_with_package(self): + # Note that this test assumes the import is installed + framework = object() +diff --git a/tests/test_meta_services.py b/tests/test_meta_services.py +index 0cb8f03..6fbea13 100644 +--- a/tests/test_meta_services.py ++++ b/tests/test_meta_services.py +@@ -7,15 +7,9 @@ from base import BaseTest + + from ipahealthcheck.ipa.plugin import registry + from ipahealthcheck.meta.services import httpd +-from unittest.mock import Mock + + + class TestServices(BaseTest): +- patches = { +- 'ipaserver.install.installutils.check_server_configuration': +- Mock(return_value=None), +- } +- + def test_simple_service(self): + """ + Test a service. It was chosen at random. +-- +2.20.1 + diff --git a/SOURCES/0005-Mock-the-AD-trust-roles.patch b/SOURCES/0005-Mock-the-AD-trust-roles.patch new file mode 100644 index 0000000..910c6e2 --- /dev/null +++ b/SOURCES/0005-Mock-the-AD-trust-roles.patch @@ -0,0 +1,99 @@ +From 0727c05df8e2b9cd6977bf076e88e5da0fd573a6 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 6 Aug 2019 16:04:40 -0400 +Subject: [PATCH 5/8] Mock the AD trust roles + +The actual values are set directly in the registry but these classes +provide just enough implementation to allow the code to initialize. +--- + src/ipahealthcheck/ipa/plugin.py | 4 +++- + tests/base.py | 5 +++++ + tests/util.py | 27 +++++++++++++++++++++++++++ + 3 files changed, 35 insertions(+), 1 deletion(-) + +diff --git a/src/ipahealthcheck/ipa/plugin.py b/src/ipahealthcheck/ipa/plugin.py +index c4cef9b..bd95c16 100644 +--- a/src/ipahealthcheck/ipa/plugin.py ++++ b/src/ipahealthcheck/ipa/plugin.py +@@ -12,7 +12,6 @@ from ipaserver.install import cainstance + from ipaserver.install import dsinstance + from ipaserver.install import httpinstance + from ipaserver.install import installutils +-from ipaserver.servroles import ADtrustBasedRole, ServiceBasedRole + + from ipahealthcheck.core.plugin import Plugin, Registry + +@@ -38,6 +37,9 @@ class IPARegistry(Registry): + self.trust_controller = False + + def initialize(self, framework): ++ # deferred import for mock ++ from ipaserver.servroles import ADtrustBasedRole, ServiceBasedRole ++ + installutils.check_server_configuration() + + if not api.isdone('finalize'): +diff --git a/tests/base.py b/tests/base.py +index 8b9e37c..d1d2442 100644 +--- a/tests/base.py ++++ b/tests/base.py +@@ -3,6 +3,7 @@ + # + from unittest import mock, TestCase + from util import no_exceptions ++from util import ADtrustBasedRole, ServiceBasedRole + + + class BaseTest(TestCase): +@@ -24,6 +25,10 @@ class BaseTest(TestCase): + default_patches = { + 'ipaserver.install.installutils.check_server_configuration': + mock.Mock(return_value=None), ++ 'ipaserver.servroles.ServiceBasedRole': ++ mock.Mock(return_value=ServiceBasedRole()), ++ 'ipaserver.servroles.ADtrustBasedRole': ++ mock.Mock(return_value=ADtrustBasedRole()), + } + patches = {} + +diff --git a/tests/util.py b/tests/util.py +index 603185f..5bd592e 100644 +--- a/tests/util.py ++++ b/tests/util.py +@@ -76,6 +76,33 @@ class KRAInstance: + return self.installed + + ++class ServiceBasedRole: ++ """A bare-bones role override ++ ++ This is just enough to satisfy the initialization code so ++ the AD Trust status can be determined. It will always default ++ to false and the registry should be overridden directly in the ++ test cases. ++ """ ++ def __init__(self, attr_name=None, name=None, component_services=None): ++ pass ++ ++ def status(self, api_instance, server=None, attrs_list=("*",)): ++ return [dict()] ++ ++ ++class ADtrustBasedRole(ServiceBasedRole): ++ """A bare-bones role override ++ ++ This is just enough to satisfy the initialization code so ++ the AD Trust status can be determined. It will always default ++ to false and the registry should be overridden directly in the ++ test cases. ++ """ ++ def __init__(self, attr_name=None, name=None): ++ pass ++ ++ + # Mock api. This file needs to be imported before anything that would + # import ipalib.api in order for it to be replaced properly. + +-- +2.20.1 + diff --git a/SOURCES/0006-Force-the-KRA-to-be-off-during-NSS-db-trust-tests.patch b/SOURCES/0006-Force-the-KRA-to-be-off-during-NSS-db-trust-tests.patch new file mode 100644 index 0000000..7a095de --- /dev/null +++ b/SOURCES/0006-Force-the-KRA-to-be-off-during-NSS-db-trust-tests.patch @@ -0,0 +1,36 @@ +From ae70afc781098b628b40894f9e0e841d3ba5b585 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 6 Aug 2019 16:45:46 -0400 +Subject: [PATCH 6/8] Force the KRA to be off during NSS db trust tests + +This avoids the side-effect of something else enabling the KRA and that +bleeding into the test. +--- + tests/test_ipa_nssdb.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/tests/test_ipa_nssdb.py b/tests/test_ipa_nssdb.py +index 7d5664e..590401c 100644 +--- a/tests/test_ipa_nssdb.py ++++ b/tests/test_ipa_nssdb.py +@@ -2,7 +2,7 @@ + # Copyright (C) 2019 FreeIPA Contributors see COPYING for license + # + +-from util import capture_results, CAInstance ++from util import capture_results, CAInstance, KRAInstance + from base import BaseTest + from ipahealthcheck.core import config, constants + from ipahealthcheck.ipa.plugin import registry +@@ -30,6 +30,8 @@ class TestNSSDBTrust(BaseTest): + patches = { + 'ipaserver.install.cainstance.CAInstance': + Mock(return_value=CAInstance()), ++ 'ipaserver.install.krainstance.KRAInstance': ++ Mock(return_value=KRAInstance(False)), + 'ipapython.certdb.unparse_trust_flags': + Mock(side_effect=my_unparse_trust_flags), + } +-- +2.20.1 + diff --git a/SOURCES/0007-Lookup-AD-user-by-SID-and-not-by-hardcoded-username.patch b/SOURCES/0007-Lookup-AD-user-by-SID-and-not-by-hardcoded-username.patch new file mode 100644 index 0000000..acc0071 --- /dev/null +++ b/SOURCES/0007-Lookup-AD-user-by-SID-and-not-by-hardcoded-username.patch @@ -0,0 +1,298 @@ +From d6d06a3a22d441fbfb208eeb63caae7aafd434b4 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Thu, 8 Aug 2019 18:46:32 +0000 +Subject: [PATCH 7/8] Lookup AD user by SID and not by hardcoded username + +Looking up the user user as Administrator@REALM is not +portable because the administrator login name may be +localized. +--- + .travis.yml | 2 +- + src/ipahealthcheck/ipa/plugin.py | 8 ++++ + src/ipahealthcheck/ipa/trust.py | 44 +++++++++++++++---- + tests/test_ipa_trust.py | 72 ++++++++++++++++++-------------- + 4 files changed, 85 insertions(+), 41 deletions(-) + +diff --git a/.travis.yml b/.travis.yml +index 16722fe..25dbf1e 100644 +--- a/.travis.yml ++++ b/.travis.yml +@@ -19,4 +19,4 @@ install: + + script: + - tox -epep8,flake8 +- - docker run -v ${TRAVIS_BUILD_DIR}:/root/src/ fedora:29 /bin/sh -c "dnf -y install freeipa-server tox python3-pytest; cd /root/src; tox -epy3" ++ - docker run -v ${TRAVIS_BUILD_DIR}:/root/src/ fedora:29 /bin/sh -c "dnf -y install freeipa-server freeipa-server-trust-ad tox python3-pytest; cd /root/src; tox -epy3" +diff --git a/src/ipahealthcheck/ipa/plugin.py b/src/ipahealthcheck/ipa/plugin.py +index bd95c16..34cb8c9 100644 +--- a/src/ipahealthcheck/ipa/plugin.py ++++ b/src/ipahealthcheck/ipa/plugin.py +@@ -57,6 +57,14 @@ class IPARegistry(Registry): + logging.debug('Failed to connect to LDAP: %s', e) + return + ++ # This package is pulled in when the trust package is installed ++ # and is required to lookup trust users. If this is not installed ++ # then it can be inferred that trust is not enabled. ++ try: ++ import pysss_nss_idmap # noqa: F401 ++ except ImportError: ++ return ++ + roles = ( + ADtrustBasedRole(u"ad_trust_agent_server", + u"AD trust agent"), +diff --git a/src/ipahealthcheck/ipa/trust.py b/src/ipahealthcheck/ipa/trust.py +index 2da20c0..6c5978d 100644 +--- a/src/ipahealthcheck/ipa/trust.py ++++ b/src/ipahealthcheck/ipa/trust.py +@@ -16,6 +16,12 @@ from ipaplatform.paths import paths + from ipapython import ipautil + from ipapython.dn import DN + ++try: ++ import pysss_nss_idmap ++except ImportError: ++ # agent and controller will be set to False in init, all tests will ++ # be skipped ++ pass + try: + from ipaserver.masters import ENABLED_SERVICE + except ImportError: +@@ -33,13 +39,19 @@ def get_trust_domains(): + Get the list of AD trust domains from IPA + + The caller is expected to catch any exceptions. ++ ++ Each entry is a dictionary representating an AD domain. + """ + result = api.Command.trust_find() + results = result['result'] + trust_domains = [] + for result in results: + if result.get('trusttype')[0] == 'Active Directory domain': +- trust_domains.append(result.get('cn')[0]) ++ domain = dict() ++ domain['domain'] = result.get('cn')[0] ++ domain['domainsid'] = result.get('ipanttrusteddomainsid')[0] ++ domain['netbios'] = result.get('ipantflatname')[0] ++ trust_domains.append(domain) + return trust_domains + + +@@ -123,14 +135,17 @@ class IPATrustDomainsCheck(IPAPlugin): + if 'implicit_files' in sssd_domains: + sssd_domains.remove('implicit_files') + ++ trust_domains = [] + try: +- trust_domains = get_trust_domains() ++ domains = get_trust_domains() + except Exception as e: + yield Result(self, constants.WARNING, + key='trust-find', + error=str(e), + msg='Execution of {key} failed: {error}') +- trust_domains = [] ++ else: ++ for entry in domains: ++ trust_domains.append(entry.get('domain')) + + if api.env.domain in sssd_domains: + sssd_domains.remove(api.env.domain) +@@ -204,17 +219,28 @@ class IPATrustCatalogCheck(IPAPlugin): + msg='Execution of {key} failed: {error}') + trust_domains = [] + +- for domain in trust_domains: ++ for trust_domain in trust_domains: ++ sid = trust_domain.get('domainsid') + try: +- ipautil.run(['/bin/id', "Administrator@%s" % domain], +- capture_output=True) ++ id = pysss_nss_idmap.getnamebysid(sid + '-500') + except Exception as e: +- yield Result(self, constants.WARNING, +- key='/bin/id', ++ yield Result(self, constants.ERROR, ++ key=sid, + error=str(e), +- msg='Execution of {key} failed: {error}') ++ msg='Look up of{key} failed: {error}') + continue + ++ if not id: ++ yield Result(self, constants.WARNING, ++ key=sid, ++ error='returned nothing', ++ msg='Look up of {key} {error}') ++ else: ++ yield Result(self, constants.SUCCESS, ++ key='Domain Security Identifier', ++ sid=sid) ++ ++ domain = trust_domain.get('domain') + args = [paths.SSSCTL, "domain-status", domain, "--active-server"] + try: + result = ipautil.run(args, capture_output=True) +diff --git a/tests/test_ipa_trust.py b/tests/test_ipa_trust.py +index 0a1d58c..89d3bff 100644 +--- a/tests/test_ipa_trust.py ++++ b/tests/test_ipa_trust.py +@@ -261,12 +261,14 @@ class TestTrustDomains(BaseTest): + { + 'cn': ['ad.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-abc'], ++ 'trusttype': ['Active Directory domain'], + }, + { + 'cn': ['child.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-def'], ++ 'trusttype': ['Active Directory domain'], + }, + ] + }] +@@ -324,12 +326,14 @@ class TestTrustDomains(BaseTest): + { + 'cn': ['ad.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-abc'], ++ 'trusttype': ['Active Directory domain'], + }, + { + 'cn': ['child.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-def'], ++ 'trusttype': ['Active Directory domain'], + }, + ] + }] +@@ -373,41 +377,27 @@ class TestTrustCatalog(BaseTest): + # Zero because the call was skipped altogether + assert len(self.results) == 0 + ++ @patch('pysss_nss_idmap.getnamebysid') + @patch('ipapython.ipautil.run') +- def test_trust_catalog_ok(self, mock_run): ++ def test_trust_catalog_ok(self, mock_run, mock_getnamebysid): + # id Administrator@ad.example +- idresult = namedtuple('run', ['returncode', 'error_log']) +- idresult.returncode = 0 +- idresult.error_log = '' +- idresult.output = '797600500(administrator@ad.example),' \ +- '1797600520(group policy creator owners@ad.example),' \ +- '1797600519(enterprise admins@ad.example),' \ +- '1797600512(domain admins@ad.example),' \ +- '1797600518(schema admins@ad.example)' \ +- ',1797600513(domain users@ad.example)\n' + dsresult = namedtuple('run', ['returncode', 'error_log']) + dsresult.returncode = 0 + dsresult.error_log = '' + dsresult.output = 'Active servers:\nAD Global Catalog: ' \ + 'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \ + 'IPA: master.ipa.vm\n\n' +- # id Administrator@client.example +- id2result = namedtuple('run', ['returncode', 'error_log']) +- id2result.returncode = 0 +- id2result.error_log = '' +- id2result.output = '797600500(administrator@client.example),' \ +- '1797600520(group policy creator owners@client.example),' \ +- '1797600519(enterprise admins@client.example),' \ +- '1797600512(domain admins@client.example),' \ +- '1797600518(schema admins@client.example)' \ +- ',1797600513(domain users@client.example)\n' + ds2result = namedtuple('run', ['returncode', 'error_log']) + ds2result.returncode = 0 + ds2result.error_log = '' + ds2result.output = 'Active servers:\nAD Global Catalog: ' \ + 'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \ + +- mock_run.side_effect = [idresult, dsresult, id2result, ds2result] ++ mock_run.side_effect = [dsresult, ds2result] ++ mock_getnamebysid.side_effect = [ ++ {'S-1-5-21-abc-500': {'name': 'admin@ad.example', 'type': 3}}, ++ {'S-1-5-21-def-500': {'name': 'admin@child.example', 'type': 3}} ++ ] + + # get_trust_domains() + m_api.Command.trust_find.side_effect = [{ +@@ -415,12 +405,14 @@ class TestTrustCatalog(BaseTest): + { + 'cn': ['ad.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-abc'], ++ 'trusttype': ['Active Directory domain'], + }, + { + 'cn': ['child.example'], + 'ipantflatname': ['ADROOT'], +- "trusttype": ["Active Directory domain"], ++ 'ipanttrusteddomainsid': ['S-1-5-21-def'], ++ 'trusttype': ['Active Directory domain'], + }, + ] + }] +@@ -433,31 +425,49 @@ class TestTrustCatalog(BaseTest): + f.config = config.Config() + self.results = capture_results(f) + +- assert len(self.results) == 4 ++ assert len(self.results) == 6 + + result = self.results.results[0] + assert result.result == constants.SUCCESS + assert result.source == 'ipahealthcheck.ipa.trust' + assert result.check == 'IPATrustCatalogCheck' +- assert result.kw.get('key') == 'AD Global Catalog' ++ assert result.kw.get('key') == 'Domain Security Identifier' ++ assert result.kw.get('sid') == 'S-1-5-21-abc' + + result = self.results.results[1] + assert result.result == constants.SUCCESS + assert result.source == 'ipahealthcheck.ipa.trust' + assert result.check == 'IPATrustCatalogCheck' +- assert result.kw.get('key') == 'AD Domain Controller' ++ assert result.kw.get('key') == 'AD Global Catalog' ++ assert result.kw.get('domain') == 'ad.example' + + result = self.results.results[2] + assert result.result == constants.SUCCESS + assert result.source == 'ipahealthcheck.ipa.trust' + assert result.check == 'IPATrustCatalogCheck' ++ assert result.kw.get('key') == 'AD Domain Controller' ++ assert result.kw.get('domain') == 'ad.example' ++ ++ result = self.results.results[3] ++ assert result.result == constants.SUCCESS ++ assert result.source == 'ipahealthcheck.ipa.trust' ++ assert result.check == 'IPATrustCatalogCheck' ++ assert result.kw.get('key') == 'Domain Security Identifier' ++ assert result.kw.get('sid') == 'S-1-5-21-def' ++ ++ result = self.results.results[4] ++ assert result.result == constants.SUCCESS ++ assert result.source == 'ipahealthcheck.ipa.trust' ++ assert result.check == 'IPATrustCatalogCheck' + assert result.kw.get('key') == 'AD Global Catalog' ++ assert result.kw.get('domain') == 'child.example' + +- result = self.results.results[1] ++ result = self.results.results[5] + assert result.result == constants.SUCCESS + assert result.source == 'ipahealthcheck.ipa.trust' + assert result.check == 'IPATrustCatalogCheck' + assert result.kw.get('key') == 'AD Domain Controller' ++ assert result.kw.get('domain') == 'child.example' + + + class Testsidgen(BaseTest): +-- +2.20.1 + diff --git a/SOURCES/0008-Rename-misspelled-sslctl-to-sssctl-for-the-SSSD-cont.patch b/SOURCES/0008-Rename-misspelled-sslctl-to-sssctl-for-the-SSSD-cont.patch new file mode 100644 index 0000000..43ed20d --- /dev/null +++ b/SOURCES/0008-Rename-misspelled-sslctl-to-sssctl-for-the-SSSD-cont.patch @@ -0,0 +1,53 @@ +From c3586321aa82bd4e12e70c8463b85c2e3888c510 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Fri, 9 Aug 2019 09:34:14 -0400 +Subject: [PATCH 8/8] Rename misspelled sslctl to sssctl for the SSSD control + script + +--- + src/ipahealthcheck/ipa/trust.py | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/src/ipahealthcheck/ipa/trust.py b/src/ipahealthcheck/ipa/trust.py +index 6c5978d..cb18378 100644 +--- a/src/ipahealthcheck/ipa/trust.py ++++ b/src/ipahealthcheck/ipa/trust.py +@@ -127,9 +127,9 @@ class IPATrustDomainsCheck(IPAPlugin): + if result.returncode != 0: + yield Result(self, constants.ERROR, + key='domain_list_error', +- sslctl=paths.SSSCTL, ++ sssctl=paths.SSSCTL, + error=result.error_log, +- msg='Execution of {sslctl} failed: {error}') ++ msg='Execution of {sssctl} failed: {error}') + return + sssd_domains = result.output.strip().split('\n') + if 'implicit_files' in sssd_domains: +@@ -152,8 +152,8 @@ class IPATrustDomainsCheck(IPAPlugin): + else: + yield Result(self, constants.ERROR, + key=api.env.domain, +- sslctl=paths.SSSCTL, +- msg='{key} not in {sslctl} domain-list') ++ sssctl=paths.SSSCTL, ++ msg='{key} not in {sssctl} domain-list') + + trust_domains_out = ', '.join(trust_domains) + sssd_domains_out = ', '.join(sssd_domains) +@@ -161,10 +161,10 @@ class IPATrustDomainsCheck(IPAPlugin): + if set(trust_domains).symmetric_difference(set(sssd_domains)): + yield Result(self, constants.ERROR, + key='domain-list', +- sslctl=paths.SSSCTL, ++ sssctl=paths.SSSCTL, + sssd_domains=sssd_domains_out, + trust_domains=trust_domains_out, +- msg='{sslctl} {key} reports mismatch: ' ++ msg='{sssctl} {key} reports mismatch: ' + 'sssd domains {sssd_domains} ' + 'trust domains {trust_domains}') + else: +-- +2.20.1 + diff --git a/SOURCES/ipahealthcheck.conf b/SOURCES/ipahealthcheck.conf new file mode 100644 index 0000000..ab109a1 --- /dev/null +++ b/SOURCES/ipahealthcheck.conf @@ -0,0 +1 @@ +[default] diff --git a/SPECS/ipa-healthcheck.spec b/SPECS/ipa-healthcheck.spec new file mode 100644 index 0000000..6ccacf0 --- /dev/null +++ b/SPECS/ipa-healthcheck.spec @@ -0,0 +1,138 @@ +%global project freeipa +%global shortname healthcheck +%global longname ipa%{shortname} +%global debug_package %{nil} +%global python3dir %{_builddir}/python3-%{name}-%{version}-%{release} +%{!?python3_sitelib: %global python3_sitelib %(%{__python3} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} + + +Name: ipa-healthcheck +Version: 0.3 +Release: 4%{?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 +Source1: %{longname}.conf + +Patch0001: 0001-Remove-requirement-for-pytest-runner-since-PyPI-isn-.patch +Patch0002: 0002-Change-DNA-no-range-from-SUCCESS-to-WARNING.patch +Patch0003: 0003-Always-initialize-AD-roles-even-if-the-IPA-API-is-in.patch +Patch0004: 0004-Create-a-default-set-of-mock-to-always-apply-to-all-.patch +Patch0005: 0005-Mock-the-AD-trust-roles.patch +Patch0006: 0006-Force-the-KRA-to-be-off-during-NSS-db-trust-tests.patch +Patch0007: 0007-Lookup-AD-user-by-SID-and-not-by-hardcoded-username.patch +Patch0008: 0008-Rename-misspelled-sslctl-to-sssctl-for-the-SSSD-cont.patch + +Requires: ipa-server +Requires: python3-ipalib +Requires: python3-ipaserver +# cronie-anacron provides anacron +Requires: anacron +Requires: logrotate +Requires(post): systemd-units +BuildRequires: python3-devel +BuildRequires: systemd-devel +%{?systemd_requires} + + +%description +The FreeIPA health check tool provides a set of checks to +proactively detect defects in a FreeIPA cluster. + + +%prep +%autosetup -p1 -n %{project}-%{shortname}-%{version} + + +%build +%py3_build + + +%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 + +mkdir -p %{buildroot}/%{_localstatedir}/log/ipa/%{shortname} + +mkdir -p %{buildroot}/%{_mandir}/man1 +mkdir -p %{buildroot}/%{_mandir}/man5 +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/ + + +%post +%systemd_post ipa-%{shortname}.service + + +%preun +%systemd_preun ipa-%{shortname}.service + + +%postun +%systemd_postun_with_restart ipa-%{shortname}.service + + +%files +%{!?_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 +%config(noreplace) %{_sysconfdir}/logrotate.d/%{longname} +%{python3_sitelib}/%{longname}/ +%{python3_sitelib}/%{longname}-%{version}-*.egg-info/ +%{python3_sitelib}/%{longname}-%{version}-*-nspkg.pth +%{_unitdir}/* +%{_libexecdir}/* +%{_mandir}/man1/* +%{_mandir}/man5/* + + +%changelog +* 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 + +* Tue Apr 30 2019 François Cami - 0.2-2 +- Fix changelog + +* Thu Apr 25 2019 Rob Crittenden - 0.2-1 +- Update to upstream 0.2 + +* Thu Apr 4 2019 François Cami - 0.1-2 +- Explicitly list dependencies + +* Tue Apr 2 2019 François Cami - 0.1-1 +- Initial package import