Blame SOURCES/0002-Issue-50701-Add-additional-healthchecks-to-dsconf.patch

8394b4
From 09326585a5561480d44beb508af2cb1da52bfff6 Mon Sep 17 00:00:00 2001
8394b4
From: Mark Reynolds <mreynolds@redhat.com>
8394b4
Date: Mon, 18 Nov 2019 12:02:39 -0500
8394b4
Subject: [PATCH] Issue 50701 - Add additional healthchecks to dsconf
8394b4
8394b4
Description:  New checks and several design changes have been implemented
8394b4
8394b4
  Design changes:
8394b4
    - Moved to a "yield" design, where a lint function can return multiple results
8394b4
    - Revised the lint report so it's easier to read and distiguish between multiple
8394b4
      errors
8394b4
    - Revised most lint errors to include CLI examples on how to fix the issue
8394b4
8394b4
  New Checks:
8394b4
    - Check TLS certs for expired/expiring
8394b4
    - Add RI plugin checks for missing indexes for RI member attributes
8394b4
    - Added Disk Space check
8394b4
    - Add Virtual Attribute index check
8394b4
    - Add replication agmt status check
8394b4
    - Add replication conflict entry check
8394b4
    - File System checks (/etc/revolv.conf, and NSS pin files)
8394b4
    - Replication changelog trimming
8394b4
8394b4
relates: https://pagure.io/389-ds-base/issue/50701
8394b4
8394b4
Reviewed by: firstyear, mhonek, tbordaz, and spichugi (Thanks!!!!)
8394b4
8394b4
add suggested changes
8394b4
8394b4
Improved the replication agreement health checks to use the new
8394b4
state levels (red, amber, green), and we use that to generate
8394b4
different reports.
8394b4
8394b4
Also improved report example autofilling of the values, so the exact
8394b4
commands can be copied and pasted.
8394b4
8394b4
Added a changelog trimming check as well.
8394b4
8394b4
Updated the help section to wanr that htehealthcheck feature should
8394b4
only be run on the local instance
8394b4
8394b4
Moved healthcheck to dsctl and added file permission checks
8394b4
---
8394b4
 src/lib389/cli/dsconf                |   2 -
8394b4
 src/lib389/cli/dsctl                 |  10 +-
8394b4
 src/lib389/lib389/_mapped_object.py  |   6 +-
8394b4
 src/lib389/lib389/agreement.py       |  67 +++++--
8394b4
 src/lib389/lib389/backend.py         | 122 +++++++++---
8394b4
 src/lib389/lib389/cli_base/dsrc.py   |   6 +-
8394b4
 src/lib389/lib389/cli_conf/health.py |  62 ------
8394b4
 src/lib389/lib389/cli_ctl/health.py  | 123 ++++++++++++
8394b4
 src/lib389/lib389/config.py          |  18 +-
8394b4
 src/lib389/lib389/dseldif.py         |  43 +++-
8394b4
 src/lib389/lib389/lint.py            | 287 +++++++++++++++++++++++----
8394b4
 src/lib389/lib389/monitor.py         |  14 ++
8394b4
 src/lib389/lib389/nss_ssl.py         |  35 +++-
8394b4
 src/lib389/lib389/plugins.py         |  46 ++++-
8394b4
 src/lib389/lib389/properties.py      |   1 +
8394b4
 src/lib389/lib389/replica.py         |  70 +++++++
8394b4
 16 files changed, 746 insertions(+), 166 deletions(-)
8394b4
 delete mode 100644 src/lib389/lib389/cli_conf/health.py
8394b4
 create mode 100644 src/lib389/lib389/cli_ctl/health.py
8394b4
8394b4
diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf
8394b4
index 6e3ef19c3..5143756c8 100755
8394b4
--- a/src/lib389/cli/dsconf
8394b4
+++ b/src/lib389/cli/dsconf
8394b4
@@ -21,7 +21,6 @@ from lib389.cli_conf import backend as cli_backend
8394b4
 from lib389.cli_conf import directory_manager as cli_directory_manager
8394b4
 from lib389.cli_conf import plugin as cli_plugin
8394b4
 from lib389.cli_conf import schema as cli_schema
8394b4
-from lib389.cli_conf import health as cli_health
8394b4
 from lib389.cli_conf import monitor as cli_monitor
8394b4
 from lib389.cli_conf import saslmappings as cli_sasl
8394b4
 from lib389.cli_conf import pwpolicy as cli_pwpolicy
8394b4
@@ -80,7 +79,6 @@ cli_backup.create_parser(subparsers)
8394b4
 cli_chaining.create_parser(subparsers)
8394b4
 cli_config.create_parser(subparsers)
8394b4
 cli_directory_manager.create_parsers(subparsers)
8394b4
-cli_health.create_parser(subparsers)
8394b4
 cli_monitor.create_parser(subparsers)
8394b4
 cli_plugin.create_parser(subparsers)
8394b4
 cli_pwpolicy.create_parser(subparsers)
8394b4
diff --git a/src/lib389/cli/dsctl b/src/lib389/cli/dsctl
8394b4
index 31e906b7d..8b86629ac 100755
8394b4
--- a/src/lib389/cli/dsctl
8394b4
+++ b/src/lib389/cli/dsctl
8394b4
@@ -16,14 +16,17 @@ import sys
8394b4
 import signal
8394b4
 import os
8394b4
 from lib389.utils import get_instance_list
8394b4
-from lib389.cli_base import _get_arg, setup_script_logger, disconnect_instance
8394b4
 from lib389 import DirSrv
8394b4
 from lib389.cli_ctl import instance as cli_instance
8394b4
 from lib389.cli_ctl import dbtasks as cli_dbtasks
8394b4
-from lib389.cli_base import disconnect_instance, setup_script_logger
8394b4
-from lib389.cli_base import format_error_to_dict
8394b4
 from lib389.cli_ctl import tls as cli_tls
8394b4
+from lib389.cli_ctl import health as cli_health
8394b4
 from lib389.cli_ctl.instance import instance_remove_all
8394b4
+from lib389.cli_base import (
8394b4
+    _get_arg,
8394b4
+    disconnect_instance,
8394b4
+    setup_script_logger,
8394b4
+    format_error_to_dict)
8394b4
 from lib389._constants import DSRC_CONTAINER
8394b4
 
8394b4
 parser = argparse.ArgumentParser()
8394b4
@@ -54,6 +57,7 @@ if not os.path.exists(DSRC_CONTAINER):
8394b4
     cli_instance.create_parser(subparsers)
8394b4
 cli_dbtasks.create_parser(subparsers)
8394b4
 cli_tls.create_parser(subparsers)
8394b4
+cli_health.create_parser(subparsers)
8394b4
 
8394b4
 argcomplete.autocomplete(parser)
8394b4
 
8394b4
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
8394b4
index e331b3b27..4da112d25 100644
8394b4
--- a/src/lib389/lib389/_mapped_object.py
8394b4
+++ b/src/lib389/lib389/_mapped_object.py
8394b4
@@ -978,9 +978,9 @@ class DSLdapObject(DSLogging):
8394b4
             return None
8394b4
         results = []
8394b4
         for fn in self._lint_functions:
8394b4
-            result = fn()
8394b4
-            if result:
8394b4
-                results.append(result)
8394b4
+            for result in fn():
8394b4
+                if result is not None:
8394b4
+                    results.append(result)
8394b4
         return results
8394b4
 
8394b4
 
8394b4
diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py
8394b4
index a0d4597ec..93fd72895 100644
8394b4
--- a/src/lib389/lib389/agreement.py
8394b4
+++ b/src/lib389/lib389/agreement.py
8394b4
@@ -105,6 +105,9 @@ class Agreement(DSLdapObject):
8394b4
             time.sleep(2)
8394b4
         return (done, error)
8394b4
 
8394b4
+    def get_name(self):
8394b4
+        return self.get_attr_val_utf8_l('cn')
8394b4
+
8394b4
     def get_agmt_maxcsn(self):
8394b4
         """Get the agreement maxcsn from the database RUV entry
8394b4
         :returns: CSN string if found, otherwise None is returned
8394b4
@@ -202,7 +205,7 @@ class Agreement(DSLdapObject):
8394b4
         consumer.close()
8394b4
         return result_msg
8394b4
 
8394b4
-    def get_agmt_status(self, binddn=None, bindpw=None):
8394b4
+    def get_agmt_status(self, binddn=None, bindpw=None, return_json=False):
8394b4
         """Return the status message
8394b4
         :param binddn: Specifies a specific bind DN to use when contacting the remote consumer
8394b4
         :type binddn: str
8394b4
@@ -211,33 +214,55 @@ class Agreement(DSLdapObject):
8394b4
         :returns: A status message about the replication agreement
8394b4
         """
8394b4
         status = "Unknown"
8394b4
-
8394b4
+        con_maxcsn = "Unknown"
8394b4
         try:
8394b4
             agmt_maxcsn = self.get_agmt_maxcsn()
8394b4
+            agmt_status = json.loads(self.get_attr_val_utf8_l(AGMT_UPDATE_STATUS_JSON))
8394b4
             if agmt_maxcsn is not None:
8394b4
-                con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw)
8394b4
-                if con_maxcsn:
8394b4
-                    if agmt_maxcsn == con_maxcsn:
8394b4
-                        status = "In Synchronization"
8394b4
-                    else:
8394b4
-                        # Not in sync - attempt to discover the cause
8394b4
-                        repl_msg = "Unknown"
8394b4
-                        if self.get_attr_val_utf8_l(AGMT_UPDATE_IN_PROGRESS) == 'true':
8394b4
-                            # Replication is on going - this is normal
8394b4
-                            repl_msg = "Replication still in progress"
8394b4
-                        elif "can't contact ldap" in \
8394b4
-                             self.get_attr_val_utf8_l(AGMT_UPDATE_STATUS):
8394b4
-                            # Consumer is down
8394b4
-                            repl_msg = "Consumer can not be contacted"
8394b4
-
8394b4
-                        status = ("Not in Synchronization: supplier " +
8394b4
-                                  "(%s) consumer (%s) Reason(%s)" %
8394b4
-                                  (agmt_maxcsn, con_maxcsn, repl_msg))
8394b4
+                try:
8394b4
+                    con_maxcsn = self.get_consumer_maxcsn(binddn=binddn, bindpw=bindpw)
8394b4
+                    if con_maxcsn:
8394b4
+                        if agmt_maxcsn == con_maxcsn:
8394b4
+                            if return_json:
8394b4
+                                return json.dumps({
8394b4
+                                    'msg': "In Synchronization",
8394b4
+                                    'agmt_maxcsn': agmt_maxcsn,
8394b4
+                                    'con_maxcsn': con_maxcsn,
8394b4
+                                    'state': agmt_status['state'],
8394b4
+                                    'reason': agmt_status['message']
8394b4
+                                })
8394b4
+                            else:
8394b4
+                                return "In Synchronization"
8394b4
+                except:
8394b4
+                    pass
8394b4
+            else:
8394b4
+                agmt_maxcsn = "Unknown"
8394b4
+
8394b4
+            # Not in sync - attempt to discover the cause
8394b4
+            repl_msg = agmt_status['message']
8394b4
+            if self.get_attr_val_utf8_l(AGMT_UPDATE_IN_PROGRESS) == 'true':
8394b4
+                # Replication is on going - this is normal
8394b4
+                repl_msg = "Replication still in progress"
8394b4
+            elif "can't contact ldap" in agmt_status['message']:
8394b4
+                    # Consumer is down
8394b4
+                    repl_msg = "Consumer can not be contacted"
8394b4
+
8394b4
+            if return_json:
8394b4
+                return json.dumps({
8394b4
+                    'msg': "Not in Synchronization",
8394b4
+                    'agmt_maxcsn': agmt_maxcsn,
8394b4
+                    'con_maxcsn': con_maxcsn,
8394b4
+                    'state': agmt_status['state'],
8394b4
+                    'reason': repl_msg
8394b4
+                })
8394b4
+            else:
8394b4
+                return ("Not in Synchronization: supplier " +
8394b4
+                        "(%s) consumer (%s) State (%s) Reason (%s)" %
8394b4
+                        (agmt_maxcsn, con_maxcsn, agmt_status['state'], repl_msg))
8394b4
         except ldap.INVALID_CREDENTIALS as e:
8394b4
             raise(e)
8394b4
         except ldap.LDAPError as e:
8394b4
             raise ValueError(str(e))
8394b4
-        return status
8394b4
 
8394b4
     def get_lag_time(self, suffix, agmt_name, binddn=None, bindpw=None):
8394b4
         """Get the lag time between the supplier and the consumer
8394b4
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
8394b4
index 62fd0ae94..ac2af021c 100644
8394b4
--- a/src/lib389/lib389/backend.py
8394b4
+++ b/src/lib389/lib389/backend.py
8394b4
@@ -7,6 +7,7 @@
8394b4
 # --- END COPYRIGHT BLOCK ---
8394b4
 
8394b4
 from datetime import datetime
8394b4
+import copy
8394b4
 import ldap
8394b4
 from lib389._constants import *
8394b4
 from lib389.properties import *
8394b4
@@ -19,6 +20,8 @@ from lib389._mapped_object import DSLdapObjects, DSLdapObject
8394b4
 from lib389.mappingTree import MappingTrees
8394b4
 from lib389.exceptions import NoSuchEntryError, InvalidArgumentError
8394b4
 from lib389.replica import Replicas
8394b4
+from lib389.cos import (CosTemplates, CosIndirectDefinitions,
8394b4
+                        CosPointerDefinitions, CosClassicDefinitions)
8394b4
 
8394b4
 # We need to be a factor to the backend monitor
8394b4
 from lib389.monitor import MonitorBackend
8394b4
@@ -30,7 +33,7 @@ from lib389.encrypted_attributes import EncryptedAttr, EncryptedAttrs
8394b4
 # This is for sample entry creation.
8394b4
 from lib389.configurations import get_sample_entries
8394b4
 
8394b4
-from lib389.lint import DSBLE0001
8394b4
+from lib389.lint import DSBLE0001, DSBLE0002, DSBLE0003, DSVIRTLE0001
8394b4
 
8394b4
 
8394b4
 class BackendLegacy(object):
8394b4
@@ -410,10 +413,92 @@ class Backend(DSLdapObject):
8394b4
         self._must_attributes = ['nsslapd-suffix', 'cn']
8394b4
         self._create_objectclasses = ['top', 'extensibleObject', BACKEND_OBJECTCLASS_VALUE]
8394b4
         self._protected = False
8394b4
-        self._lint_functions = [self._lint_mappingtree]
8394b4
+        self._lint_functions = [self._lint_mappingtree, self._lint_search, self._lint_virt_attrs]
8394b4
         # Check if a mapping tree for this suffix exists.
8394b4
         self._mts = MappingTrees(self._instance)
8394b4
 
8394b4
+    def _lint_virt_attrs(self):
8394b4
+        """Check if any virtual attribute are incorrectly indexed"""
8394b4
+        indexes = self.get_indexes()
8394b4
+        suffix = self.get_attr_val_utf8('nsslapd-suffix')
8394b4
+
8394b4
+        # First check nsrole
8394b4
+        try:
8394b4
+            indexes.get('nsrole')
8394b4
+            report = copy.deepcopy(DSVIRTLE0001)
8394b4
+            report['detail'] = report['detail'].replace('ATTR', 'nsrole')
8394b4
+            report['fix'] = report['fix'].replace('ATTR', 'nsrole')
8394b4
+            report['fix'] = report['fix'].replace('SUFFIX', suffix)
8394b4
+            report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+            report['items'].append(suffix)
8394b4
+            report['items'].append('nsrole')
8394b4
+            yield report
8394b4
+        except:
8394b4
+            pass
8394b4
+
8394b4
+        # Check COS next
8394b4
+        for cosDefType in [CosIndirectDefinitions, CosPointerDefinitions, CosClassicDefinitions]:
8394b4
+            defs = cosDefType(self._instance, self._dn).list()
8394b4
+            for cosDef in defs:
8394b4
+                attrs = cosDef.get_attr_val_utf8_l("cosAttribute").split()
8394b4
+                for attr in attrs:
8394b4
+                    if attr in ["default", "override", "operational", "operational-default", "merge-schemes"]:
8394b4
+                        # We are at the end, just break out
8394b4
+                        break
8394b4
+                    try:
8394b4
+                        indexes.get(attr)
8394b4
+                        # If we got here there is an index (bad)
8394b4
+                        report = copy.deepcopy(DSVIRTLE0001)
8394b4
+                        report['detail'] = report['detail'].replace('ATTR', attr)
8394b4
+                        report['fix'] = report['fix'].replace('ATTR', attr)
8394b4
+                        report['fix'] = report['fix'].replace('SUFFIX', suffix)
8394b4
+                        report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                        report['items'].append(suffix)
8394b4
+                        report['items'].append("Class Of Service (COS)")
8394b4
+                        report['items'].append("cosAttribute: " + attr)
8394b4
+                        yield report
8394b4
+                    except:
8394b4
+                        # this is what we hope for
8394b4
+                        pass
8394b4
+
8394b4
+    def _lint_search(self):
8394b4
+        """Perform a search and make sure an entry is accessible
8394b4
+        """
8394b4
+        dn = self.get_attr_val_utf8('nsslapd-suffix')
8394b4
+        suffix = DSLdapObject(self._instance, dn=dn)
8394b4
+        try:
8394b4
+            suffix.get_attr_val('objectclass')
8394b4
+        except ldap.NO_SUCH_OBJECT:
8394b4
+            # backend root entry not created yet
8394b4
+            DSBLE0003['items'] = [dn, ]
8394b4
+            yield DSBLE0003
8394b4
+        except ldap.LDAPError as e:
8394b4
+            # Some other error
8394b4
+            DSBLE0002['detail'] = DSBLE0002['detail'].replace('ERROR', str(e))
8394b4
+            DSBLE0002['items'] = [dn, ]
8394b4
+            yield DSBLE0002
8394b4
+
8394b4
+    def _lint_mappingtree(self):
8394b4
+        """Backend lint
8394b4
+
8394b4
+        This should check for:
8394b4
+        * missing mapping tree entries for the backend
8394b4
+        * missing indices if we are local and have log access?
8394b4
+        """
8394b4
+
8394b4
+        # Check for the missing mapping tree.
8394b4
+        suffix = self.get_attr_val_utf8('nsslapd-suffix')
8394b4
+        bename = self.get_attr_val_bytes('cn')
8394b4
+        try:
8394b4
+            mt = self._mts.get(suffix)
8394b4
+            if mt.get_attr_val_bytes('nsslapd-backend') != bename and mt.get_attr_val('nsslapd-state') != ensure_bytes('backend'):
8394b4
+                raise ldap.NO_SUCH_OBJECT("We have a matching suffix, but not a backend or correct database name.")
8394b4
+        except ldap.NO_SUCH_OBJECT:
8394b4
+            result = DSBLE0001
8394b4
+            result['items'] = [bename, ]
8394b4
+            yield result
8394b4
+        return None
8394b4
+
8394b4
     def create_sample_entries(self, version):
8394b4
         """Creates sample entries under nsslapd-suffix value
8394b4
 
8394b4
@@ -552,27 +637,6 @@ class Backend(DSLdapObject):
8394b4
         # Now remove our children, this is all ldbm config
8394b4
         self._instance.delete_branch_s(self._dn, ldap.SCOPE_SUBTREE)
8394b4
 
8394b4
-    def _lint_mappingtree(self):
8394b4
-        """Backend lint
8394b4
-
8394b4
-        This should check for:
8394b4
-        * missing mapping tree entries for the backend
8394b4
-        * missing indices if we are local and have log access?
8394b4
-        """
8394b4
-
8394b4
-        # Check for the missing mapping tree.
8394b4
-        suffix = self.get_attr_val_utf8('nsslapd-suffix')
8394b4
-        bename = self.get_attr_val_bytes('cn')
8394b4
-        try:
8394b4
-            mt = self._mts.get(suffix)
8394b4
-            if mt.get_attr_val_bytes('nsslapd-backend') != bename and mt.get_attr_val('nsslapd-state') != ensure_bytes('backend'):
8394b4
-                raise ldap.NO_SUCH_OBJECT("We have a matching suffix, but not a backend or correct database name.")
8394b4
-        except ldap.NO_SUCH_OBJECT:
8394b4
-            result = DSBLE0001
8394b4
-            result['items'] = [bename, ]
8394b4
-            return result
8394b4
-        return None
8394b4
-
8394b4
     def get_suffix(self):
8394b4
         return self.get_attr_val_utf8_l('nsslapd-suffix')
8394b4
 
8394b4
@@ -753,6 +817,18 @@ class Backend(DSLdapObject):
8394b4
                         break
8394b4
         return subsuffixes
8394b4
 
8394b4
+    def get_cos_indirect_defs(self):
8394b4
+        return CosIndirectDefinitions(self._instance, self._dn).list()
8394b4
+
8394b4
+    def get_cos_pointer_defs(self):
8394b4
+        return CosPointerDefinitions(self._instance, self._dn).list()
8394b4
+
8394b4
+    def get_cos_classic_defs(self):
8394b4
+        return CosClassicDefinitions(self._instance, self._dn).list()
8394b4
+
8394b4
+    def get_cos_templates(self):
8394b4
+        return CosTemplates(self._instance, self._dn).list()
8394b4
+
8394b4
 
8394b4
 class Backends(DSLdapObjects):
8394b4
     """DSLdapObjects that represents DN_LDBM base DN
8394b4
diff --git a/src/lib389/lib389/cli_base/dsrc.py b/src/lib389/lib389/cli_base/dsrc.py
8394b4
index bbd160e8e..20b240df5 100644
8394b4
--- a/src/lib389/lib389/cli_base/dsrc.py
8394b4
+++ b/src/lib389/lib389/cli_base/dsrc.py
8394b4
@@ -41,12 +41,15 @@ def dsrc_arg_concat(args, dsrc_inst):
8394b4
             'uri': args.instance,
8394b4
             'basedn': args.basedn,
8394b4
             'binddn': args.binddn,
8394b4
+            'bindpw': None,
8394b4
             'saslmech': None,
8394b4
             'tls_cacertdir': None,
8394b4
             'tls_cert': None,
8394b4
             'tls_key': None,
8394b4
             'tls_reqcert': ldap.OPT_X_TLS_HARD,
8394b4
             'starttls': args.starttls,
8394b4
+            'prompt': False,
8394b4
+            'pwdfile': None,
8394b4
             'args': {}
8394b4
         }
8394b4
         # Now gather the args
8394b4
@@ -137,7 +140,8 @@ def dsrc_to_ldap(path, instance_name, log):
8394b4
     else:
8394b4
         dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_HARD
8394b4
     dsrc_inst['starttls'] = config.getboolean(instance_name, 'starttls', fallback=False)
8394b4
-
8394b4
+    dsrc_inst['pwdfile'] = None
8394b4
+    dsrc_inst['prompt'] = False
8394b4
     # Now gather the args
8394b4
     dsrc_inst['args'][SER_LDAP_URL] = dsrc_inst['uri']
8394b4
     dsrc_inst['args'][SER_ROOT_DN] = dsrc_inst['binddn']
8394b4
diff --git a/src/lib389/lib389/cli_conf/health.py b/src/lib389/lib389/cli_conf/health.py
8394b4
deleted file mode 100644
8394b4
index 040d85674..000000000
8394b4
--- a/src/lib389/lib389/cli_conf/health.py
8394b4
+++ /dev/null
8394b4
@@ -1,62 +0,0 @@
8394b4
-# --- BEGIN COPYRIGHT BLOCK ---
8394b4
-# Copyright (C) 2016 Red Hat, Inc.
8394b4
-# All rights reserved.
8394b4
-#
8394b4
-# License: GPL (version 3 or any later version).
8394b4
-# See LICENSE for details.
8394b4
-# --- END COPYRIGHT BLOCK ---
8394b4
-
8394b4
-from lib389.backend import Backend, Backends
8394b4
-from lib389.config import Encryption, Config
8394b4
-from lib389 import plugins
8394b4
-
8394b4
-# These get all instances, then check them all.
8394b4
-CHECK_MANY_OBJECTS = [
8394b4
-    Backends,
8394b4
-]
8394b4
-
8394b4
-# These get single instances and check them.
8394b4
-CHECK_OBJECTS = [
8394b4
-    Config,
8394b4
-    Encryption,
8394b4
-    plugins.ReferentialIntegrityPlugin
8394b4
-]
8394b4
-
8394b4
-
8394b4
-def _format_check_output(log, result):
8394b4
-    log.info("==== DS Lint Error: %s ====" % result['dsle'])
8394b4
-    log.info(" Severity: %s " % result['severity'])
8394b4
-    log.info(" Affects:")
8394b4
-    for item in result['items']:
8394b4
-        log.info(" -- %s" % item)
8394b4
-    log.info(" Details:")
8394b4
-    log.info(result['detail'])
8394b4
-    log.info(" Resolution:")
8394b4
-    log.info(result['fix'])
8394b4
-
8394b4
-
8394b4
-def health_check_run(inst, basedn, log, args):
8394b4
-    log.info("Beginning lint report, this could take a while ...")
8394b4
-    report = []
8394b4
-    for lo in CHECK_MANY_OBJECTS:
8394b4
-        log.info("Checking %s ..." % lo.__name__)
8394b4
-        lo_inst = lo(inst)
8394b4
-        for clo in lo_inst.list():
8394b4
-            result = clo.lint()
8394b4
-            if result is not None:
8394b4
-                report += result
8394b4
-    for lo in CHECK_OBJECTS:
8394b4
-        log.info("Checking %s ..." % lo.__name__)
8394b4
-        lo_inst = lo(inst)
8394b4
-        result = lo_inst.lint()
8394b4
-        if result is not None:
8394b4
-            report += result
8394b4
-    log.info("Healthcheck complete!")
8394b4
-    for item in report:
8394b4
-        _format_check_output(log, item)
8394b4
-
8394b4
-
8394b4
-def create_parser(subparsers):
8394b4
-    run_healthcheck_parser = subparsers.add_parser('healthcheck', help="Run a healthcheck report on your Directory Server instance. This is a safe, read only operation.")
8394b4
-    run_healthcheck_parser.set_defaults(func=health_check_run)
8394b4
-
8394b4
diff --git a/src/lib389/lib389/cli_ctl/health.py b/src/lib389/lib389/cli_ctl/health.py
8394b4
new file mode 100644
8394b4
index 000000000..d8f3d732b
8394b4
--- /dev/null
8394b4
+++ b/src/lib389/lib389/cli_ctl/health.py
8394b4
@@ -0,0 +1,123 @@
8394b4
+# --- BEGIN COPYRIGHT BLOCK ---
8394b4
+# Copyright (C) 2016 Red Hat, Inc.
8394b4
+# All rights reserved.
8394b4
+#
8394b4
+# License: GPL (version 3 or any later version).
8394b4
+# See LICENSE for details.
8394b4
+# --- END COPYRIGHT BLOCK ---
8394b4
+
8394b4
+import json
8394b4
+from getpass import getpass
8394b4
+from lib389.cli_base import connect_instance, disconnect_instance, format_error_to_dict
8394b4
+from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
8394b4
+from lib389.backend import Backend, Backends
8394b4
+from lib389.config import Encryption, Config
8394b4
+from lib389.monitor import MonitorDiskSpace
8394b4
+from lib389.replica import Replica, Changelog5
8394b4
+from lib389.nss_ssl import NssSsl
8394b4
+from lib389.dseldif import FSChecks
8394b4
+from lib389 import plugins
8394b4
+from lib389._constants import DSRC_HOME
8394b4
+
8394b4
+# These get all instances, then check them all.
8394b4
+CHECK_MANY_OBJECTS = [
8394b4
+    Backends,
8394b4
+]
8394b4
+
8394b4
+# These get single instances and check them.
8394b4
+CHECK_OBJECTS = [
8394b4
+    Config,
8394b4
+    Encryption,
8394b4
+    FSChecks,
8394b4
+    plugins.ReferentialIntegrityPlugin,
8394b4
+    MonitorDiskSpace,
8394b4
+    Replica,
8394b4
+    Changelog5,
8394b4
+    NssSsl,
8394b4
+]
8394b4
+
8394b4
+
8394b4
+def _format_check_output(log, result, idx):
8394b4
+    log.info("\n\n[{}] DS Lint Error: {}".format(idx, result['dsle']))
8394b4
+    log.info("-" * 80)
8394b4
+    log.info("Severity: %s " % result['severity'])
8394b4
+    log.info("Affects:")
8394b4
+    for item in result['items']:
8394b4
+        log.info(" -- %s" % item)
8394b4
+    log.info("\nDetails:")
8394b4
+    log.info('-----------')
8394b4
+    log.info(result['detail'])
8394b4
+    log.info("\nResolution:")
8394b4
+    log.info('-----------')
8394b4
+    log.info(result['fix'])
8394b4
+
8394b4
+
8394b4
+def health_check_run(inst, log, args):
8394b4
+    """Connect to the local server using LDAPI, and perform various health checks
8394b4
+    """
8394b4
+
8394b4
+    # update the args for connect_instance()
8394b4
+    args.basedn = None
8394b4
+    args.binddn = None
8394b4
+    args.bindpw = None
8394b4
+    args.starttls = None
8394b4
+    args.pwdfile = None
8394b4
+    args.prompt = False
8394b4
+    dsrc_inst = dsrc_to_ldap(DSRC_HOME, args.instance, log.getChild('dsrc'))
8394b4
+    dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
8394b4
+    try:
8394b4
+        inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
8394b4
+    except Exception as e:
8394b4
+        raise ValueError('Failed to connect to Directory Server instance: ' + str(e))
8394b4
+
8394b4
+    if not args.json:
8394b4
+        log.info("Beginning lint report, this could take a while ...")
8394b4
+    report = []
8394b4
+    for lo in CHECK_MANY_OBJECTS:
8394b4
+        if not args.json:
8394b4
+            log.info("Checking %s ..." % lo.__name__)
8394b4
+        lo_inst = lo(inst)
8394b4
+        for clo in lo_inst.list():
8394b4
+            result = clo.lint()
8394b4
+            if result is not None:
8394b4
+                report += result
8394b4
+    for lo in CHECK_OBJECTS:
8394b4
+        if not args.json:
8394b4
+            log.info("Checking %s ..." % lo.__name__)
8394b4
+        lo_inst = lo(inst)
8394b4
+        result = lo_inst.lint()
8394b4
+        if result is not None:
8394b4
+            report += result
8394b4
+    if not args.json:
8394b4
+        log.info("Healthcheck complete.")
8394b4
+    count = len(report)
8394b4
+    if count == 0:
8394b4
+        if not args.json:
8394b4
+            log.info("No issues found.")
8394b4
+        else:
8394b4
+            log.info(json.dumps(report))
8394b4
+    else:
8394b4
+        plural = ""
8394b4
+        if count > 1:
8394b4
+            plural = "s"
8394b4
+        if not args.json:
8394b4
+            log.info("{} Issue{} found!  Generating report ...".format(count, plural))
8394b4
+            idx = 1
8394b4
+            for item in report:
8394b4
+                _format_check_output(log, item, idx)
8394b4
+                idx += 1
8394b4
+            log.info('\n\n===== End Of Report ({} Issue{} found) ====='.format(count, plural))
8394b4
+        else:
8394b4
+            log.info(json.dumps(report))
8394b4
+
8394b4
+    disconnect_instance(inst)
8394b4
+
8394b4
+
8394b4
+def create_parser(subparsers):
8394b4
+    run_healthcheck_parser = subparsers.add_parser('healthcheck', help=
8394b4
+        "Run a healthcheck report on a local Directory Server instance. This "
8394b4
+        "is a safe and read-only operation.  Do not attempt to run this on a "
8394b4
+        "remote Directory Server as this tool needs access to local resources, "
8394b4
+        "otherwise the report may be inaccurate.")
8394b4
+    run_healthcheck_parser.set_defaults(func=health_check_run)
8394b4
+
8394b4
diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py
8394b4
index db5359a68..f71baf2d8 100644
8394b4
--- a/src/lib389/lib389/config.py
8394b4
+++ b/src/lib389/lib389/config.py
8394b4
@@ -16,6 +16,7 @@
8394b4
    DirSrv.backend.methodName()
8394b4
 """
8394b4
 
8394b4
+import copy
8394b4
 import ldap
8394b4
 from lib389._constants import *
8394b4
 from lib389 import Entry
8394b4
@@ -199,17 +200,18 @@ class Config(DSLdapObject):
8394b4
     def _lint_hr_timestamp(self):
8394b4
         hr_timestamp = self.get_attr_val('nsslapd-logging-hr-timestamps-enabled')
8394b4
         if ensure_bytes('on') != hr_timestamp:
8394b4
-            return DSCLE0001
8394b4
-        pass # nsslapd-logging-hr-timestamps-enabled
8394b4
+            report = copy.deepcopy(DSCLE0001)
8394b4
+            report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+            yield report
8394b4
 
8394b4
     def _lint_passwordscheme(self):
8394b4
         allowed_schemes = ['SSHA512', 'PBKDF2_SHA256']
8394b4
         u_password_scheme = self.get_attr_val_utf8('passwordStorageScheme')
8394b4
         u_root_scheme = self.get_attr_val_utf8('nsslapd-rootpwstoragescheme')
8394b4
         if u_root_scheme not in allowed_schemes or u_password_scheme not in allowed_schemes:
8394b4
-            return DSCLE0002
8394b4
-        return None
8394b4
-
8394b4
+            report = copy.deepcopy(DSCLE0002)
8394b4
+            report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+            yield report
8394b4
 
8394b4
 class Encryption(DSLdapObject):
8394b4
     """
8394b4
@@ -237,8 +239,10 @@ class Encryption(DSLdapObject):
8394b4
     def _lint_check_tls_version(self):
8394b4
         tls_min = self.get_attr_val('sslVersionMin')
8394b4
         if tls_min < ensure_bytes('TLS1.1'):
8394b4
-            return DSELE0001
8394b4
-        return None
8394b4
+            report = copy.deepcopy(DSELE0001)
8394b4
+            report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+            yield report
8394b4
+        yield None
8394b4
 
8394b4
     @property
8394b4
     def ciphers(self):
8394b4
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
8394b4
index dfe3b91e2..4155abcdd 100644
8394b4
--- a/src/lib389/lib389/dseldif.py
8394b4
+++ b/src/lib389/lib389/dseldif.py
8394b4
@@ -1,14 +1,17 @@
8394b4
 # --- BEGIN COPYRIGHT BLOCK ---
8394b4
-# Copyright (C) 2017 Red Hat, Inc.
8394b4
+# Copyright (C) 2019 Red Hat, Inc.
8394b4
 # All rights reserved.
8394b4
 #
8394b4
 # License: GPL (version 3 or any later version).
8394b4
 # See LICENSE for details.
8394b4
 # --- END COPYRIGHT BLOCK ---
8394b4
 #
8394b4
+
8394b4
+import copy
8394b4
 import os
8394b4
+from stat import ST_MODE
8394b4
 from lib389.paths import Paths
8394b4
-
8394b4
+from lib389.lint import DSPERMLE0001, DSPERMLE0002
8394b4
 
8394b4
 class DSEldif(object):
8394b4
     """A class for working with dse.ldif file
8394b4
@@ -155,3 +158,39 @@ class DSEldif(object):
8394b4
             self._instance.log.debug("During replace operation: {}".format(e))
8394b4
         self.add(entry_dn, attr, value)
8394b4
         self._update()
8394b4
+
8394b4
+
8394b4
+class FSChecks(object):
8394b4
+    """This is for the healthcheck feature, check commonly used system config files the
8394b4
+    server uses.  This is here for lack of a better place to add this class.
8394b4
+    """
8394b4
+    def __init__(self, dirsrv=None):
8394b4
+        self.dirsrv = dirsrv
8394b4
+        self._certdb = self.dirsrv.get_cert_dir()
8394b4
+        self.ds_files = [
8394b4
+            ('/etc/resolv.conf', '644', DSPERMLE0001),
8394b4
+            (self._certdb + "/pin.txt", '600', DSPERMLE0002),
8394b4
+            (self._certdb + "/pwdfile.txt", '600', DSPERMLE0002),
8394b4
+        ]
8394b4
+        self._lint_functions = [self._lint_file_perms]
8394b4
+
8394b4
+    def lint(self):
8394b4
+        results = []
8394b4
+        for fn in self._lint_functions:
8394b4
+            for result in fn():
8394b4
+                if result is not None:
8394b4
+                    results.append(result)
8394b4
+        return results
8394b4
+
8394b4
+    def _lint_file_perms(self):
8394b4
+        # Check file permissions are correct
8394b4
+        for ds_file in self.ds_files:
8394b4
+            perms = str(oct(os.stat(ds_file[0])[ST_MODE])[-3:])
8394b4
+            if perms != ds_file[1]:
8394b4
+                report = copy.deepcopy(ds_file[2])
8394b4
+                report['items'].append(ds_file[0])
8394b4
+                report['detail'] = report['detail'].replace('FILE', ds_file[0])
8394b4
+                report['detail'] = report['detail'].replace('PERMS', ds_file[1])
8394b4
+                report['fix'] = report['fix'].replace('FILE', ds_file[0])
8394b4
+                report['fix'] = report['fix'].replace('PERMS', ds_file[1])
8394b4
+                yield report
8394b4
diff --git a/src/lib389/lib389/lint.py b/src/lib389/lib389/lint.py
8394b4
index 8c4b4dedc..515711136 100644
8394b4
--- a/src/lib389/lib389/lint.py
8394b4
+++ b/src/lib389/lib389/lint.py
8394b4
@@ -1,5 +1,5 @@
8394b4
 # --- BEGIN COPYRIGHT BLOCK ---
8394b4
-# Copyright (C) 2017 Red Hat, Inc.
8394b4
+# Copyright (C) 2019 Red Hat, Inc.
8394b4
 # All rights reserved.
8394b4
 #
8394b4
 # License: GPL (version 3 or any later version).
8394b4
@@ -10,12 +10,12 @@
8394b4
 # as well as some functions to help process them.
8394b4
 
8394b4
 
8394b4
+# Database checks
8394b4
 DSBLE0001 = {
8394b4
     'dsle': 'DSBLE0001',
8394b4
     'severity': 'MEDIUM',
8394b4
     'items' : [],
8394b4
-    'detail' : """
8394b4
-This backend may be missing the correct mapping tree references. Mapping Trees allow
8394b4
+    'detail' : """This backend may be missing the correct mapping tree references. Mapping Trees allow
8394b4
 the directory server to determine which backend an operation is routed to in the
8394b4
 abscence of other information. This is extremely important for correct functioning
8394b4
 of LDAP ADD for example.
8394b4
@@ -31,20 +31,35 @@ objectClass: top
8394b4
 objectClass: extensibleObject
8394b4
 objectClass: nsMappingTree
8394b4
 
8394b4
-    """,
8394b4
-    'fix' : """
8394b4
-Either you need to create the mapping tree, or you need to repair the related
8394b4
+""",
8394b4
+    'fix' : """Either you need to create the mapping tree, or you need to repair the related
8394b4
 mapping tree. You will need to do this by hand by editing cn=config, or stopping
8394b4
 the instance and editing dse.ldif.
8394b4
-    """
8394b4
+"""
8394b4
 }
8394b4
 
8394b4
+DSBLE0002 = {
8394b4
+    'dsle': 'DSBLE0002',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : [],
8394b4
+    'detail' : """Unable to querying the backend.  LDAP error (ERROR)""",
8394b4
+    'fix' : """Check the server's error and access logs for more information."""
8394b4
+}
8394b4
+
8394b4
+DSBLE0003 = {
8394b4
+    'dsle': 'DSBLE0002',
8394b4
+    'severity': 'LOW',
8394b4
+    'items' : [],
8394b4
+    'detail' : """The backend database has not been initialized yet""",
8394b4
+    'fix' : """You need to import an LDIF file, or create the suffix entry, in order to initialize the database."""
8394b4
+}
8394b4
+
8394b4
+# Config checks
8394b4
 DSCLE0001 = {
8394b4
     'dsle' : 'DSCLE0001',
8394b4
     'severity' : 'LOW',
8394b4
     'items': ['cn=config', ],
8394b4
-    'detail' : """
8394b4
-nsslapd-logging-hr-timestamps-enabled changes the log format in directory server from
8394b4
+    'detail' : """nsslapd-logging-hr-timestamps-enabled changes the log format in directory server from
8394b4
 
8394b4
 [07/Jun/2017:17:15:58 +1000]
8394b4
 
8394b4
@@ -54,18 +69,18 @@ to
8394b4
 
8394b4
 This actually provides a performance improvement. Additionally, this setting will be
8394b4
 removed in a future release.
8394b4
-    """,
8394b4
-    'fix' : """
8394b4
-Set nsslapd-logging-hr-timestamps-enabled to on.
8394b4
-    """
8394b4
+""",
8394b4
+    'fix' : """Set nsslapd-logging-hr-timestamps-enabled to on.
8394b4
+You can use 'dsconf' to set this attribute.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE config replace nsslapd-logging-hr-timestamps-enabled=on"""
8394b4
 }
8394b4
 
8394b4
 DSCLE0002 = {
8394b4
     'dsle': 'DSCLE0002',
8394b4
     'severity': 'HIGH',
8394b4
     'items' : ['cn=config', ],
8394b4
-    'detail' : """
8394b4
-Password storage schemes in Directory Server define how passwords are hashed via a
8394b4
+    'detail' : """Password storage schemes in Directory Server define how passwords are hashed via a
8394b4
 one-way mathematical function for storage. Knowing the hash it is difficult to gain
8394b4
 the input, but knowing the input you can easily compare the hash.
8394b4
 
8394b4
@@ -79,53 +94,253 @@ for "legacy" support (SSHA512).
8394b4
 
8394b4
 Your configuration does not use these for password storage or the root password storage
8394b4
 scheme.
8394b4
-    """,
8394b4
-    'fix': """
8394b4
-Perform a configuration reset of the values:
8394b4
+""",
8394b4
+    'fix': """Perform a configuration reset of the values:
8394b4
 
8394b4
 passwordStorageScheme
8394b4
 nsslapd-rootpwstoragescheme
8394b4
 
8394b4
 IE, stop Directory Server, and in dse.ldif delete these two lines. When Directory Server
8394b4
 is started, they will use the server provided defaults that are secure.
8394b4
-    """
8394b4
+
8394b4
+You can also use 'dsconf' to replace these values.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE config replace passwordStorageScheme=PBKDF2_SHA256 nsslapd-rootpwstoragescheme=PBKDF2_SHA256"""
8394b4
 }
8394b4
 
8394b4
+# Security checks
8394b4
 DSELE0001 = {
8394b4
     'dsle': 'DSELE0001',
8394b4
     'severity': 'MEDIUM',
8394b4
     'items' : ['cn=encryption,cn=config', ],
8394b4
-    'detail': """
8394b4
-This Directory Server may not be using strong TLS protocol versions. TLS1.0 is known to
8394b4
+    'detail': """This Directory Server may not be using strong TLS protocol versions. TLS1.0 is known to
8394b4
 have a number of issues with the protocol. Please see:
8394b4
 
8394b4
 https://tools.ietf.org/html/rfc7457
8394b4
 
8394b4
-It is advised you set this value to the maximum possible.
8394b4
-    """,
8394b4
-    'fix' : """
8394b4
-set cn=encryption,cn=config sslVersionMin to a version greater than TLS1.0
8394b4
-    """
8394b4
+It is advised you set this value to the maximum possible.""",
8394b4
+    'fix' : """There are two options for setting the TLS minimum version allowed.  You,
8394b4
+can set "sslVersionMin" in "cn=encryption,cn=config" to a version greater than "TLS1.0"
8394b4
+You can also use 'dsconf' to set this value.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE security set --tls-protocol-min=TLS1.2
8394b4
+
8394b4
+You must restart the Directory Server for this change to take effect.
8394b4
+
8394b4
+Or, you can set the system wide crypto policy to FUTURE which will use a higher TLS
8394b4
+minimum version, but doing this affects the entire system:
8394b4
+
8394b4
+    # update-crypto-policies --set FUTURE"""
8394b4
 }
8394b4
 
8394b4
+# RI plugin checks
8394b4
 DSRILE0001 = {
8394b4
     'dsle': 'DSRLE0001',
8394b4
     'severity': 'LOW',
8394b4
     'items' : ['cn=referential integrity postoperation,cn=plugins,cn=config', ],
8394b4
-    'detail': """
8394b4
-The referential integrity plugin has an asynchronous processing mode. This is controlled by the update-delay flag.
8394b4
-
8394b4
-When this value is 0, referential integrity plugin processes these changes inside of the operation that modified the entry - ie these are synchronous.
8394b4
+    'detail': """The referential integrity plugin has an asynchronous processing mode.
8394b4
+This is controlled by the update-delay flag.  When this value is 0, referential
8394b4
+integrity plugin processes these changes inside of the operation that modified
8394b4
+the entry - ie these are synchronous.
8394b4
 
8394b4
 However, when this is > 0, these are performed asynchronously.
8394b4
 
8394b4
-This leads to only having refint enabled on one master in MMR to prevent replication conflicts and loops.
8394b4
+This leads to only having referint enabled on one master in MMR to prevent replication conflicts and loops.
8394b4
 Additionally, because these are performed in the background these updates may cause spurious update
8394b4
 delays to your server by batching changes rather than smaller updates during sync processing.
8394b4
 
8394b4
-We advise that you set this value to 0, and enable refint on all masters as it provides a more predictable behaviour.
8394b4
-    """,
8394b4
-    'fix' : """
8394b4
-Set referint-update-delay to 0.
8394b4
-    """
8394b4
+We advise that you set this value to 0, and enable referint on all masters as it provides a more predictable behaviour.
8394b4
+""",
8394b4
+    'fix' : """Set referint-update-delay to 0.
8394b4
+
8394b4
+You can use 'dsconf' to set this value.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE plugin referential-integrity set --update-delay 0
8394b4
+
8394b4
+You must restart the Directory Server for this change to take effect."""
8394b4
+}
8394b4
+
8394b4
+# Note - ATTR and BACKEND are replaced by the reporting function
8394b4
+DSRILE0002 = {
8394b4
+    'dsle': 'DSRLE0002',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['cn=referential integrity postoperation,cn=plugins,cn=config'],
8394b4
+    'detail': """The referential integrity plugin is configured to use an attribute (ATTR)
8394b4
+that does not have an "equality" index in backend (BACKEND).
8394b4
+Failure to have the proper indexing will lead to unindexed searches which
8394b4
+cause high CPU and can significantly slow the server down.""",
8394b4
+    'fix' : """Check the attributes set in "referint-membership-attr" to make sure they have
8394b4
+an index defined that has at least the equality "eq" index type.  You will
8394b4
+need to reindex the database after adding the missing index type. Here is an
8394b4
+example using dsconf:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE backend index --attr=ATTR --reindex --index-type=eq BACKEND
8394b4
+"""
8394b4
+}
8394b4
+
8394b4
+# Disk Space check.  Note - PARTITION is replaced by the calling function
8394b4
+DSDSLE0001 = {
8394b4
+    'dsle': 'DSDSLE0001',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['Server', 'cn=config'],
8394b4
+    'detail': """The disk partition used by the server (PARTITION), either for the database, the
8394b4
+configuration files, or the logs is over 90% full.  If the partition becomes
8394b4
+completely filled serious problems can occur with the database or the server's
8394b4
+stability.""",
8394b4
+    'fix' : """Attempt to free up disk space.  Also try removing old rotated logs, or disable any
8394b4
+verbose logging levels that might have been set.  You might consider enabling
8394b4
+the "Disk Monitoring" feature in cn=config to help prevent a disorderly shutdown
8394b4
+of the server:
8394b4
+
8394b4
+    nsslapd-disk-monitoring: on
8394b4
+
8394b4
+You can use 'dsconf' to set this value.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE config replace nsslapd-disk-monitoring=on
8394b4
+
8394b4
+You must restart the Directory Server for this change to take effect.
8394b4
+
8394b4
+Please see the Administration guide for more information:
8394b4
+
8394b4
+    https://access.redhat.com/documentation/en-us/red_hat_directory_server/10/html/administration_guide/diskmonitoring
8394b4
+"""
8394b4
+}
8394b4
+
8394b4
+# Replication check.   Note - AGMT and SUFFIX are replaced by the reporting function
8394b4
+DSREPLLE0001 = {
8394b4
+    'dsle': 'DSREPLLE0001',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['Replication', 'Agreement'],
8394b4
+    'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization.""",
8394b4
+    'fix' : """You may need to reinitialize this replication agreement.  Please check the errors
8394b4
+log for more information.  If you do need to reinitialize the agreement you can do so
8394b4
+using dsconf.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE repl-agmt init "AGMT" --suffix SUFFIX"""
8394b4
+}
8394b4
+
8394b4
+# Note - SUFFIX and COUNT will be replaced by the calling function
8394b4
+DSREPLLE0002 = {
8394b4
+    'dsle': 'DSREPLLE0002',
8394b4
+    'severity': 'LOW',
8394b4
+    'items' : ['Replication', 'Conflict Entries'],
8394b4
+    'detail': """There were COUNT conflict entries found under the replication suffix "SUFFIX".
8394b4
+Status message: MSG""",
8394b4
+    'fix' : """While conflict entries are expected to occur in an MMR environment, they
8394b4
+should be resolved.  In regards to conflict entries there is always the original/counterpart
8394b4
+entry that has a normal DN, and then the conflict version of that entry.  Technically both
8394b4
+entries are valid, you as the administrator, needs to decide which entry you want to keep.
8394b4
+First examine/compare both entries to determine which one you want to keep or remove.  You
8394b4
+can use the CLI tool "dsconf" to resolve the conflict.  Here is an example:
8394b4
+
8394b4
+    List the conflict entries:
8394b4
+
8394b4
+        # dsconf slapd-YOUR_INSTANCE  repl-conflict list dc=example,dc=com
8394b4
+
8394b4
+    Examine conflict entry and its counterpart entry:
8394b4
+
8394b4
+        # dsconf slapd-YOUR_INSTANCE  repl-conflict compare <DN of conflict entry>
8394b4
+
8394b4
+    Remove conflict entry and keep only the original/counterpart entry:
8394b4
+
8394b4
+        # dsconf slapd-YOUR_INSTANCE  repl-conflict remove <DN of conflict entry>
8394b4
+
8394b4
+    Replace the original/counterpart entry with the conflict entry:
8394b4
+
8394b4
+        # dsconf slapd-YOUR_INSTANCE  repl-conflict swap <DN of conflict entry>
8394b4
+"""
8394b4
+}
8394b4
+
8394b4
+DSREPLLE0003 = {
8394b4
+    'dsle': 'DSREPLLE0003',
8394b4
+    'severity': 'MEDIUM',
8394b4
+    'items' : ['Replication', 'Agreement'],
8394b4
+    'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization.
8394b4
+Status message: MSG""",
8394b4
+    'fix' : """Replication is not in synchronization but it may recover.  Continue to
8394b4
+monitor this agreement."""
8394b4
+}
8394b4
+
8394b4
+DSREPLLE0004 = {
8394b4
+    'dsle': 'DSREPLLE0004',
8394b4
+    'severity': 'MEDIUM',
8394b4
+    'items' : ['Replication', 'Agreement'],
8394b4
+    'detail': """Failed to get the agreement status for agreement (AGMT) under "SUFFIX".  Error (ERROR).""",
8394b4
+    'fix' : """None"""
8394b4
+}
8394b4
+
8394b4
+DSREPLLE0005 = {
8394b4
+    'dsle': 'DSREPLLE0005',
8394b4
+    'severity': 'MEDIUM',
8394b4
+    'items' : ['Replication', 'Agreement'],
8394b4
+    'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization,
8394b4
+because the consumer server is not reachable.""",
8394b4
+    'fix' : """Check if the consumer is running, and also check the errors log for more information."""
8394b4
+}
8394b4
+
8394b4
+# Replication changelog
8394b4
+DSCLLE0001 = {
8394b4
+    'dsle': 'DSCLLE0001',
8394b4
+    'severity': 'LOW',
8394b4
+    'items' : ['Replication', 'Changelog'],
8394b4
+    'detail': """The replication changelog does have any kind of trimming configured.  This will
8394b4
+lead to the changelog size growing indefinitely.""",
8394b4
+    'fix' : """Configure changelog trimming, preferably by setting the maximum age of a changelog
8394b4
+record.  Here is an example:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE replication set-changelog --max-age 30d"""
8394b4
+}
8394b4
+
8394b4
+# Certificate checks
8394b4
+DSCERTLE0001 = {
8394b4
+    'dsle': 'DSCERTLE0001',
8394b4
+    'severity': 'MEDIUM',
8394b4
+    'items' : ['Expiring Certificate'],
8394b4
+    'detail': """The certificate (CERT) will expire in less than 30 days""",
8394b4
+    'fix' : """Renew the certificate before it expires to prevent disruptions with TLS connections."""
8394b4
+}
8394b4
+
8394b4
+DSCERTLE0002 = {
8394b4
+    'dsle': 'DSCERTLE0002',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['Expired Certificate'],
8394b4
+    'detail': """The certificate (CERT) has expired""",
8394b4
+    'fix' : """Renew or remove the certificate."""
8394b4
+}
8394b4
+
8394b4
+# Virtual Attrs & COS.  Note - ATTR and SUFFIX are replaced by the reporting function
8394b4
+DSVIRTLE0001 = {
8394b4
+    'dsle': 'DSVIRTLE0001',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['Virtual Attributes'],
8394b4
+    'detail': """You should not index virtual attributes, and as this will break searches that
8394b4
+use the attribute in a filter.""",
8394b4
+    'fix' : """Remove the index for this attribute from the backend configuration.
8394b4
+Here is an example using 'dsconf' to remove an index:
8394b4
+
8394b4
+    # dsconf slapd-YOUR_INSTANCE backend index delete --attr ATTR SUFFIX"""
8394b4
+}
8394b4
+
8394b4
+# File permissions (resolv.conf
8394b4
+DSPERMLE0001 = {
8394b4
+    'dsle': 'DSPERMLE0001',
8394b4
+    'severity': 'MEDIUM',
8394b4
+    'items' : ['File Permissions'],
8394b4
+    'detail': """The file "FILE" does not have the expected permissions (PERMS).  This
8394b4
+can cause issues with replication and chaining.""",
8394b4
+    'fix' : """Change the file permissions:
8394b4
+
8394b4
+    # chmod PERMS FILE"""
8394b4
+}
8394b4
+
8394b4
+# TLS db password/pin files
8394b4
+DSPERMLE0002 = {
8394b4
+    'dsle': 'DSPERMLE0002',
8394b4
+    'severity': 'HIGH',
8394b4
+    'items' : ['File Permissions'],
8394b4
+    'detail': """The file "FILE" does not have the expected permissions (PERMS).  The
8394b4
+security database pin/password files should only be readable by Directory Server user.""",
8394b4
+    'fix' : """Change the file permissions:
8394b4
+
8394b4
+    # chmod PERMS FILE"""
8394b4
 }
8394b4
diff --git a/src/lib389/lib389/monitor.py b/src/lib389/lib389/monitor.py
8394b4
index 5ca967c64..290cad5e2 100644
8394b4
--- a/src/lib389/lib389/monitor.py
8394b4
+++ b/src/lib389/lib389/monitor.py
8394b4
@@ -9,6 +9,7 @@
8394b4
 from lib389._constants import *
8394b4
 from lib389._mapped_object import DSLdapObject
8394b4
 from lib389.utils import (ds_is_older)
8394b4
+from lib389.lint import DSDSLE0001
8394b4
 
8394b4
 
8394b4
 class Monitor(DSLdapObject):
8394b4
@@ -254,6 +255,19 @@ class MonitorDiskSpace(DSLdapObject):
8394b4
     def __init__(self, instance, dn=None):
8394b4
         super(MonitorDiskSpace, self).__init__(instance=instance, dn=dn)
8394b4
         self._dn = "cn=disk space,cn=monitor"
8394b4
+        self._lint_functions = [self._lint_disk_space]
8394b4
+
8394b4
+    def _lint_disk_space(self):
8394b4
+        partitions = self.get_attr_vals_utf8_l("dsDisk")
8394b4
+        for partition in partitions:
8394b4
+            parts = partition.split()
8394b4
+            percent = parts[4].split('=')[1].strip('"')
8394b4
+            if int(percent) >= 90:
8394b4
+                # this partition is over 90% full, not good
8394b4
+                report = copy.deepcopy(DSDSLE0001)
8394b4
+                report['detail'] = report['detail'].replace('PARTITION', parts[0].split('=')[1].strip('"'))
8394b4
+                report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                yield report
8394b4
 
8394b4
     def get_disks(self):
8394b4
         """Get an information about partitions which contains a Directory Server data"""
8394b4
diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
8394b4
index afe921385..2a7d1637c 100644
8394b4
--- a/src/lib389/lib389/nss_ssl.py
8394b4
+++ b/src/lib389/lib389/nss_ssl.py
8394b4
@@ -9,6 +9,7 @@
8394b4
 """Helpers for managing NSS databases in Directory Server
8394b4
 """
8394b4
 
8394b4
+import copy
8394b4
 import os
8394b4
 import re
8394b4
 import socket
8394b4
@@ -17,10 +18,10 @@ import shutil
8394b4
 import logging
8394b4
 # from nss import nss
8394b4
 import subprocess
8394b4
-from datetime import datetime, timedelta
8394b4
+from datetime import datetime, timedelta, date
8394b4
 from subprocess import check_output
8394b4
 from lib389.passwd import password_generate
8394b4
-
8394b4
+from lib389.lint import DSCERTLE0001, DSCERTLE0002
8394b4
 from lib389.utils import ensure_str, format_cmd_list
8394b4
 import uuid
8394b4
 
8394b4
@@ -58,6 +59,36 @@ class NssSsl(object):
8394b4
         self.db_files = {"dbm_backend": ["%s/%s" % (self._certdb, f) for f in ("key3.db", "cert8.db", "secmod.db")],
8394b4
                          "sql_backend": ["%s/%s" % (self._certdb, f) for f in ("key4.db", "cert9.db", "pkcs11.txt")],
8394b4
                          "support": ["%s/%s" % (self._certdb, f) for f in ("noise.txt", PIN_TXT, PWD_TXT)]}
8394b4
+        self._lint_functions = [self._lint_certificate_expiration,]
8394b4
+
8394b4
+    def lint(self):
8394b4
+        results = []
8394b4
+        for fn in self._lint_functions:
8394b4
+            for result in fn():
8394b4
+                if result is not None:
8394b4
+                    results.append(result)
8394b4
+        return results
8394b4
+
8394b4
+    def _lint_certificate_expiration(self):
8394b4
+        """Check all the certificates in the db if they will expire within 30 days
8394b4
+        or have already expired.
8394b4
+        """
8394b4
+        cert_list = []
8394b4
+        all_certs = self._rsa_cert_list()
8394b4
+        for cert in all_certs:
8394b4
+            cert_list.append(self.get_cert_details(cert[0]))
8394b4
+
8394b4
+        for cert in cert_list:
8394b4
+            if date.fromisoformat(cert[3].split()[0]) - date.today() < timedelta(days=0):
8394b4
+                # Expired
8394b4
+                report = copy.deepcopy(DSCERTLE0002)
8394b4
+                report['detail'] = report['detail'].replace('CERT', cert[0])
8394b4
+                yield report
8394b4
+            elif date.fromisoformat(cert[3].split()[0]) - date.today() < timedelta(days=30):
8394b4
+                # Expiring
8394b4
+                report = copy.deepcopy(DSCERTLE0001)
8394b4
+                report['detail'] = report['detail'].replace('CERT', cert[0])
8394b4
+                yield report
8394b4
 
8394b4
     def detect_alt_names(self, alt_names=[]):
8394b4
         """Attempt to determine appropriate subject alternate names for a host.
8394b4
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
8394b4
index a8b8985fc..97c5d1d3b 100644
8394b4
--- a/src/lib389/lib389/plugins.py
8394b4
+++ b/src/lib389/lib389/plugins.py
8394b4
@@ -10,10 +10,9 @@ import collections
8394b4
 import ldap
8394b4
 import copy
8394b4
 import os.path
8394b4
-
8394b4
 from lib389 import tasks
8394b4
 from lib389._mapped_object import DSLdapObjects, DSLdapObject
8394b4
-from lib389.lint import DSRILE0001
8394b4
+from lib389.lint import DSRILE0001, DSRILE0002
8394b4
 from lib389.utils import ensure_str, ensure_list_bytes
8394b4
 from lib389.schema import Schema
8394b4
 from lib389._constants import DN_PLUGIN
8394b4
@@ -432,7 +431,7 @@ class ReferentialIntegrityPlugin(Plugin):
8394b4
             'referint-logfile',
8394b4
             'referint-membership-attr',
8394b4
         ])
8394b4
-        self._lint_functions = [self._lint_update_delay]
8394b4
+        self._lint_functions = [self._lint_update_delay, self._lint_attr_indexes]
8394b4
 
8394b4
     def create(self, rdn=None, properties=None, basedn=None):
8394b4
         """Create an instance of the plugin"""
8394b4
@@ -448,7 +447,46 @@ class ReferentialIntegrityPlugin(Plugin):
8394b4
         if self.status():
8394b4
             delay = self.get_attr_val_int("referint-update-delay")
8394b4
             if delay is not None and delay != 0:
8394b4
-                return DSRILE0001
8394b4
+                report = copy.deepcopy(DSRILE0001)
8394b4
+                report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                yield report
8394b4
+
8394b4
+    def _lint_attr_indexes(self):
8394b4
+        if self.status():
8394b4
+            from lib389.backend import Backends
8394b4
+            backends = Backends(self._instance).list()
8394b4
+            for backend in backends:
8394b4
+                indexes = backend.get_indexes()
8394b4
+                suffix = backend.get_attr_val_utf8_l('nsslapd-suffix')
8394b4
+                attrs = self.get_attr_vals_utf8_l("referint-membership-attr")
8394b4
+                for attr in attrs:
8394b4
+                    report = copy.deepcopy(DSRILE0002)
8394b4
+                    try:
8394b4
+                        index = indexes.get(attr)
8394b4
+                        types = index.get_attr_vals_utf8_l("nsIndexType")
8394b4
+                        valid = False
8394b4
+                        if "eq" in types:
8394b4
+                            valid = True
8394b4
+
8394b4
+                        if not valid:
8394b4
+                            report['detail'] = report['detail'].replace('ATTR', attr)
8394b4
+                            report['detail'] = report['detail'].replace('BACKEND', suffix)
8394b4
+                            report['fix'] = report['fix'].replace('ATTR', attr)
8394b4
+                            report['fix'] = report['fix'].replace('BACKEND', suffix)
8394b4
+                            report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                            report['items'].append(suffix)
8394b4
+                            report['items'].append(attr)
8394b4
+                            yield report
8394b4
+                    except:
8394b4
+                        # No index at all, bad
8394b4
+                        report['detail'] = report['detail'].replace('ATTR', attr)
8394b4
+                        report['detail'] = report['detail'].replace('BACKEND', suffix)
8394b4
+                        report['fix'] = report['fix'].replace('ATTR', attr)
8394b4
+                        report['fix'] = report['fix'].replace('BACKEND', suffix)
8394b4
+                        report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                        report['items'].append(suffix)
8394b4
+                        report['items'].append(attr)
8394b4
+                        yield report
8394b4
 
8394b4
     def get_update_delay(self):
8394b4
         """Get referint-update-delay attribute"""
8394b4
diff --git a/src/lib389/lib389/properties.py b/src/lib389/lib389/properties.py
8394b4
index d18249d20..9d7ce4161 100644
8394b4
--- a/src/lib389/lib389/properties.py
8394b4
+++ b/src/lib389/lib389/properties.py
8394b4
@@ -319,6 +319,7 @@ AGMT_UPDATE_START = 'nsds5replicaLastUpdateStart'
8394b4
 AGMT_UPDATE_END = 'nsds5replicaLastUpdateEnd'
8394b4
 AGMT_CHANGES_SINCE_STARTUP = 'nsds5replicaChangesSentSinceStartup'  # base64
8394b4
 AGMT_UPDATE_STATUS = 'nsds5replicaLastUpdateStatus'
8394b4
+AGMT_UPDATE_STATUS_JSON = 'nsds5replicaLastUpdateStatusJSON'
8394b4
 AGMT_UPDATE_IN_PROGRESS = 'nsds5replicaUpdateInProgress'
8394b4
 AGMT_INIT_START = 'nsds5replicaLastInitStart'
8394b4
 AGMT_INIT_END = 'nsds5replicaLastInitEnd'
8394b4
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
8394b4
index 7145e86f9..9b84d8f7e 100644
8394b4
--- a/src/lib389/lib389/replica.py
8394b4
+++ b/src/lib389/lib389/replica.py
8394b4
@@ -15,6 +15,7 @@ import datetime
8394b4
 import logging
8394b4
 import uuid
8394b4
 import json
8394b4
+import copy
8394b4
 from operator import itemgetter
8394b4
 from itertools import permutations
8394b4
 from lib389._constants import *
8394b4
@@ -31,6 +32,9 @@ from lib389.idm.domain import Domain
8394b4
 from lib389.idm.group import Groups
8394b4
 from lib389.idm.services import ServiceAccounts
8394b4
 from lib389.idm.organizationalunit import OrganizationalUnits
8394b4
+from lib389.conflicts import ConflictEntries
8394b4
+from lib389.lint import (DSREPLLE0001, DSREPLLE0002, DSREPLLE0003, DSREPLLE0004,
8394b4
+                         DSREPLLE0005, DSCLLE0001)
8394b4
 
8394b4
 
8394b4
 class ReplicaLegacy(object):
8394b4
@@ -1044,6 +1048,19 @@ class Changelog5(DSLdapObject):
8394b4
                 'extensibleobject',
8394b4
             ]
8394b4
         self._protected = False
8394b4
+        self._lint_functions = [self._lint_cl_trimming]
8394b4
+
8394b4
+    def _lint_cl_trimming(self):
8394b4
+        """Check that cl trimming is at least defined to prevent unbounded growth"""
8394b4
+        try:
8394b4
+            if self.get_attr_val_utf8('nsslapd-changelogmaxentries') is None and \
8394b4
+                self.get_attr_val_utf8('nsslapd-changelogmaxage') is None:
8394b4
+                report = copy.deepcopy(DSCLLE0001)
8394b4
+                report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                yield report
8394b4
+        except:
8394b4
+            # No changelog
8394b4
+            pass
8394b4
 
8394b4
     def set_max_entries(self, value):
8394b4
         """Configure the max entries the changelog can hold.
8394b4
@@ -1102,6 +1119,59 @@ class Replica(DSLdapObject):
8394b4
             self._create_objectclasses.append('extensibleobject')
8394b4
         self._protected = False
8394b4
         self._suffix = None
8394b4
+        self._lint_functions = [self._lint_agmts_status, self._lint_conflicts]
8394b4
+
8394b4
+    def _lint_agmts_status(self):
8394b4
+        replicas = Replicas(self._instance).list()
8394b4
+        for replica in replicas:
8394b4
+            agmts = replica.get_agreements().list()
8394b4
+            suffix = replica.get_suffix()
8394b4
+            for agmt in agmts:
8394b4
+                try:
8394b4
+                    status = json.loads(agmt.get_agmt_status(return_json=True))
8394b4
+                    if "Not in Synchronization" in status['msg'] and not "Replication still in progress" in status['reason']:
8394b4
+                        agmt_name = agmt.get_name()
8394b4
+                        if status['state'] == 'red':
8394b4
+                            # Serious error
8394b4
+                            if "Consumer can not be contacted" in status['reason']:
8394b4
+                                report = copy.deepcopy(DSREPLLE0005)
8394b4
+                                report['detail'] = report['detail'].replace('SUFFIX', suffix)
8394b4
+                                report['detail'] = report['detail'].replace('AGMT', agmt_name)
8394b4
+                                yield report
8394b4
+                            else:
8394b4
+                                report = copy.deepcopy(DSREPLLE0001)
8394b4
+                                report['detail'] = report['detail'].replace('SUFFIX', suffix)
8394b4
+                                report['detail'] = report['detail'].replace('AGMT', agmt_name)
8394b4
+                                report['detail'] = report['detail'].replace('MSG', status['reason'])
8394b4
+                                report['fix'] = report['fix'].replace('SUFFIX', suffix)
8394b4
+                                report['fix'] = report['fix'].replace('AGMT', agmt_name)
8394b4
+                                report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                                yield report
8394b4
+                        elif status['state'] == 'amber':
8394b4
+                            # Warning
8394b4
+                            report = copy.deepcopy(DSREPLLE0003)
8394b4
+                            report['detail'] = report['detail'].replace('SUFFIX', suffix)
8394b4
+                            report['detail'] = report['detail'].replace('AGMT', agmt_name)
8394b4
+                            report['detail'] = report['detail'].replace('MSG', status['reason'])
8394b4
+                            yield report
8394b4
+                except ldap.LDAPError as e:
8394b4
+                    report = copy.deepcopy(DSREPLLE0004)
8394b4
+                    report['detail'] = report['detail'].replace('SUFFIX', suffix)
8394b4
+                    report['detail'] = report['detail'].replace('AGMT', agmt_name)
8394b4
+                    report['detail'] = report['detail'].replace('ERROR', str(e))
8394b4
+                    yield report
8394b4
+
8394b4
+    def _lint_conflicts(self):
8394b4
+        replicas = Replicas(self._instance).list()
8394b4
+        for replica in replicas:
8394b4
+            conflicts = ConflictEntries(self._instance, replica.get_suffix()).list()
8394b4
+            suffix = replica.get_suffix()
8394b4
+            if len(conflicts) > 0:
8394b4
+                report = copy.deepcopy(DSREPLLE0002)
8394b4
+                report['detail'] = report['detail'].replace('SUFFIX', suffix)
8394b4
+                report['detail'] = report['detail'].replace('COUNT', len(conflicts))
8394b4
+                report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
8394b4
+                yield report
8394b4
 
8394b4
     def _validate(self, rdn, properties, basedn):
8394b4
         (tdn, str_props) = super(Replica, self)._validate(rdn, properties, basedn)
8394b4
-- 
8394b4
2.21.0
8394b4