andykimpe / rpms / 389-ds-base

Forked from rpms/389-ds-base 5 months ago
Clone

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

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