andykimpe / rpms / 389-ds-base

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

Blame SOURCES/0011-Issue-50746-Add-option-to-healthcheck-to-list-all-th.patch

d69b2b
From 21ed5224d63e3118a39ddd5ea438367532541a8f Mon Sep 17 00:00:00 2001
d69b2b
From: Matus Honek <mhonek@redhat.com>
d69b2b
Date: Mon, 2 Dec 2019 14:53:31 +0100
d69b2b
Subject: [PATCH 11/12] Issue 50746 - Add option to healthcheck to list all the
d69b2b
 lint reports
d69b2b
d69b2b
Bug Description:
d69b2b
Healthcheck lacks a way to find out what checks are available.
d69b2b
d69b2b
Fix Description:
d69b2b
Add dsctl healthcheck options to list available checks, known error
d69b2b
codes, and ability to run cehcks selectively. The checks are rather
d69b2b
hierarchically structured and in some cases matchable by patterns (by
d69b2b
use of asterisk).
d69b2b
d69b2b
Fixes https://pagure.io/389-ds-base/issue/50746
d69b2b
d69b2b
Author: Matus Honek <mhonek@redhat.com>
d69b2b
d69b2b
Review by: Mark, William, Simon (thanks for the patience!)
d69b2b
d69b2b
(cherry picked from commit 4a55322c7bdb0b9ff57428ad0dc2e4d943572a69)
d69b2b
---
d69b2b
 src/lib389/cli/dsctl                          |   1 +
d69b2b
 src/lib389/lib389/_mapped_object.py           |  34 +---
d69b2b
 src/lib389/lib389/_mapped_object_lint.py      | 157 ++++++++++++++++++
d69b2b
 src/lib389/lib389/backend.py                  |  13 +-
d69b2b
 src/lib389/lib389/cli_ctl/health.py           | 116 +++++++++----
d69b2b
 src/lib389/lib389/config.py                   |  13 +-
d69b2b
 src/lib389/lib389/dseldif.py                  |  29 +---
d69b2b
 src/lib389/lib389/encrypted_attributes.py     |   1 -
d69b2b
 src/lib389/lib389/index.py                    |   3 -
d69b2b
 src/lib389/lib389/lint.py                     | 125 ++++++++------
d69b2b
 src/lib389/lib389/monitor.py                  |   5 +-
d69b2b
 src/lib389/lib389/nss_ssl.py                  |  23 ++-
d69b2b
 src/lib389/lib389/plugins.py                  |   5 +-
d69b2b
 src/lib389/lib389/replica.py                  |  10 +-
d69b2b
 .../lib389/tests/mapped_object_lint_test.py   |  78 +++++++++
d69b2b
 15 files changed, 448 insertions(+), 165 deletions(-)
d69b2b
 create mode 100644 src/lib389/lib389/_mapped_object_lint.py
d69b2b
 create mode 100644 src/lib389/lib389/tests/mapped_object_lint_test.py
d69b2b
d69b2b
diff --git a/src/lib389/cli/dsctl b/src/lib389/cli/dsctl
d69b2b
index fd9bd87c1..9deda7039 100755
d69b2b
--- a/src/lib389/cli/dsctl
d69b2b
+++ b/src/lib389/cli/dsctl
d69b2b
@@ -64,6 +64,7 @@ cli_dbgen.create_parser(subparsers)
d69b2b
 
d69b2b
 argcomplete.autocomplete(parser)
d69b2b
 
d69b2b
+
d69b2b
 # handle a control-c gracefully
d69b2b
 def signal_handler(signal, frame):
d69b2b
     print('\n\nExiting...')
d69b2b
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
d69b2b
index ce0ebfeb8..c60837601 100644
d69b2b
--- a/src/lib389/lib389/_mapped_object.py
d69b2b
+++ b/src/lib389/lib389/_mapped_object.py
d69b2b
@@ -15,6 +15,7 @@ import json
d69b2b
 from functools import partial
d69b2b
 from lib389._entry import Entry
d69b2b
 from lib389._constants import DIRSRV_STATE_ONLINE
d69b2b
+from lib389._mapped_object_lint import DSLint, DSLints
d69b2b
 from lib389.utils import (
d69b2b
         ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str,
d69b2b
         ensure_list_int, display_log_value, display_log_data
d69b2b
@@ -82,7 +83,7 @@ class DSLogging(object):
d69b2b
             self._log.setLevel(logging.INFO)
d69b2b
 
d69b2b
 
d69b2b
-class DSLdapObject(DSLogging):
d69b2b
+class DSLdapObject(DSLogging, DSLint):
d69b2b
     """A single instance of DSLdapObjects
d69b2b
 
d69b2b
     :param instance: An instance
d69b2b
@@ -107,7 +108,6 @@ class DSLdapObject(DSLogging):
d69b2b
         self._must_attributes = None
d69b2b
         # attributes, we don't want to compare
d69b2b
         self._compare_exclude = ['entryid', 'modifytimestamp', 'nsuniqueid']
d69b2b
-        self._lint_functions = None
d69b2b
         self._server_controls = None
d69b2b
         self._client_controls = None
d69b2b
         self._object_filter = '(objectClass=*)'
d69b2b
@@ -985,38 +985,10 @@ class DSLdapObject(DSLogging):
d69b2b
         """
d69b2b
         return self._create(rdn, properties, basedn, ensure=True)
d69b2b
 
d69b2b
-    def lint(self):
d69b2b
-        """Override this to create a linter for a type. This means that we can detect
d69b2b
-        and report common administrative errors in the server from our cli and
d69b2b
-        rest tools.
d69b2b
-
d69b2b
-        The structure of a result is::
d69b2b
-
d69b2b
-          {
d69b2b
-            dsle: '<identifier>'. dsle == ds lint error. Will be a code unique to
d69b2b
-                                this module for the error, IE DSBLE0001.
d69b2b
-            severity: '[HIGH:MEDIUM:LOW]'. severity of the error.
d69b2b
-            items: '(dn,dn,dn)'. List of affected DNs or names.
d69b2b
-            detail: 'msg ...'. An explination of the error.
d69b2b
-            fix: 'msg ...'. Steps to resolve the error.
d69b2b
-          }
d69b2b
-
d69b2b
-        :returns: An array of these dicts, on None if there are no errors.
d69b2b
-        """
d69b2b
-
d69b2b
-        if not self._lint_functions:
d69b2b
-            return None
d69b2b
-        results = []
d69b2b
-        for fn in self._lint_functions:
d69b2b
-            for result in fn():
d69b2b
-                if result is not None:
d69b2b
-                    results.append(result)
d69b2b
-        return results
d69b2b
-
d69b2b
 
d69b2b
 # A challenge of this, is how do we manage indexes? They have two naming attributes....
d69b2b
 
d69b2b
-class DSLdapObjects(DSLogging):
d69b2b
+class DSLdapObjects(DSLogging, DSLints):
d69b2b
     """The object represents the next idea: "Everything is an instance of something
d69b2b
     that exists in this way", i.e. we unite LDAP entries by some
d69b2b
     set of parameters with the object.
d69b2b
diff --git a/src/lib389/lib389/_mapped_object_lint.py b/src/lib389/lib389/_mapped_object_lint.py
d69b2b
new file mode 100644
d69b2b
index 000000000..2d03de98f
d69b2b
--- /dev/null
d69b2b
+++ b/src/lib389/lib389/_mapped_object_lint.py
d69b2b
@@ -0,0 +1,157 @@
d69b2b
+from abc import ABC, abstractmethod
d69b2b
+from functools import partial
d69b2b
+from inspect import signature
d69b2b
+from typing import (
d69b2b
+    Callable,
d69b2b
+    List,
d69b2b
+    Optional,
d69b2b
+    Tuple,
d69b2b
+    Union,
d69b2b
+    Type,
d69b2b
+    Generator,
d69b2b
+    Any
d69b2b
+)
d69b2b
+
d69b2b
+
d69b2b
+DSLintSpec = Tuple[str, Callable]
d69b2b
+DSLintParsedSpec = Tuple[Optional[str], Optional[str]]
d69b2b
+DSLintClassSpec = Generator[DSLintSpec, None, None]
d69b2b
+DSLintMethodSpec = Union[str, None, Type[List]]
d69b2b
+DSLintResults = Generator[Any, None, None]
d69b2b
+
d69b2b
+
d69b2b
+class DSLint():
d69b2b
+    """In a super-class, create a method with name beginning with `_lint_`
d69b2b
+    which would yield results (as described below). Such a method will
d69b2b
+    then be available to the `lint()` method of the class.
d69b2b
+
d69b2b
+    `lint_list`: takes a spec and yields available lints, recursively
d69b2b
+    `lint`:      takes a spac and runs lints according to it, yielding results if any
d69b2b
+
d69b2b
+    `spec`: is a colon-separated string, with prefix matching a method name and suffix
d69b2b
+            being passed down to the method.
d69b2b
+
d69b2b
+    A class inheriting from hereby class shall implement a method named `lint_uid()` which
d69b2b
+    returns a pretty name of the object. This is to be used by a higher level code.
d69b2b
+
d69b2b
+    Each lint method has to have a name prefix with _lint_. It may accept an optional
d69b2b
+    parameter `spec` in which case:
d69b2b
+    - it has to accept typing.List class as a parameter, in which case it shall yield
d69b2b
+      all possible lint specs for that method
d69b2b
+    - it receives the suffix provided to the `spec` of hereby `lint` method (as mentioned above)
d69b2b
+
d69b2b
+    This means that we can detect and report common administrative errors
d69b2b
+    in the server from our cli and rest tools.
d69b2b
+
d69b2b
+    The structure of a result shall be:
d69b2b
+
d69b2b
+        {
d69b2b
+        dsle: '<identifier>'. dsle == ds lint error. Will be a code unique to
d69b2b
+                            this module for the error, IE DSBLE0001.
d69b2b
+        severity: '[HIGH:MEDIUM:LOW]'. severity of the error.
d69b2b
+        items: '(dn,dn,dn)'. List of affected DNs or names.
d69b2b
+        detail: 'msg ...'. An explination of the error.
d69b2b
+        fix: 'msg ...'. Steps to resolve the error.
d69b2b
+        }
d69b2b
+    """
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def _dslint_fname(cls, method: Callable) -> Optional[str]:
d69b2b
+        """Return a pretty name for a method."""
d69b2b
+        if callable(method) and method.__name__.startswith('_lint_'):
d69b2b
+            return method.__name__[len('_lint_'):]
d69b2b
+        else:
d69b2b
+            return None
d69b2b
+
d69b2b
+    @staticmethod
d69b2b
+    def _dslint_parse_spec(spec: Optional[str]) -> DSLintParsedSpec:
d69b2b
+        """Split `spec` to prefix and suffix."""
d69b2b
+        wanted, *rest = spec.split(':', 1) if spec else (None, None)
d69b2b
+        return (wanted if wanted not in [None, '*'] else None,
d69b2b
+                rest[0] if rest else None)
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def _dslint_make_spec(cls, method: Callable, spec: Optional[str] = None) -> str:
d69b2b
+        """Build a new spec from prefix (`method` name) and suffix (`spec`)."""
d69b2b
+        fname = cls._dslint_fname(method)
d69b2b
+        return f'{fname}:{spec}' if spec else fname
d69b2b
+
d69b2b
+    def lint_list(self, spec: Optional[str] = None) -> DSLintClassSpec:
d69b2b
+        """Yield specs the object provides.
d69b2b
+
d69b2b
+        This yields from each lint method yielding all specs it can provide.
d69b2b
+        """
d69b2b
+
d69b2b
+        assert hasattr(self, 'lint_uid')
d69b2b
+
d69b2b
+        # Find _lint_ methods
d69b2b
+        # NOTE: There is a caveat: don't you dare try to getattr on a @property, or
d69b2b
+        #       you get it executed. That's why the following line's complexity.
d69b2b
+        fs = [getattr(self, f) for f in dir(self)
d69b2b
+              if f.startswith('_lint_') and self._dslint_fname(getattr(self, f))]
d69b2b
+
d69b2b
+        # Filter acording to the `spec`
d69b2b
+        wanted, rest = self._dslint_parse_spec(spec)
d69b2b
+        if wanted:
d69b2b
+            try:
d69b2b
+                fs = [next(filter(lambda f: self._dslint_fname(f) == wanted, fs))]
d69b2b
+            except StopIteration:
d69b2b
+                raise ValueError('there is no such lint function')
d69b2b
+
d69b2b
+        # Yield known specs
d69b2b
+        for f in fs:
d69b2b
+            fspec_t = signature(f).parameters.get('spec', None)
d69b2b
+            if fspec_t:
d69b2b
+                assert fspec_t.annotation == DSLintMethodSpec
d69b2b
+                for fspec in [rest] if rest else f(spec=List):
d69b2b
+                    yield self._dslint_make_spec(f, fspec), partial(f, spec=fspec)
d69b2b
+            else:
d69b2b
+                yield self._dslint_make_spec(f, rest), f
d69b2b
+
d69b2b
+    def lint(self, spec: DSLintMethodSpec = None) -> DSLintResults:
d69b2b
+        """Lint the object according to the `spec`."""
d69b2b
+
d69b2b
+        if spec == List:
d69b2b
+            yield from self.lint_list()
d69b2b
+        else:
d69b2b
+            for fn, f in self.lint_list(spec):
d69b2b
+                yield from f()
d69b2b
+
d69b2b
+
d69b2b
+class DSLints():
d69b2b
+    """This is a meta class to provide lint functionality to classes that provide
d69b2b
+    method `list` which returns list of objects that inherit from DSLint.
d69b2b
+
d69b2b
+    Calling `lint` or `lint_list` method yields from respective object's methods.
d69b2b
+
d69b2b
+    The `spec` is a colon-separated string. Its prefix matches the respective object's
d69b2b
+    `lint_uid` (or all when asterisk); the suffix is passed down to the respective
d69b2b
+    object's method.
d69b2b
+    """
d69b2b
+
d69b2b
+    def lint_list(self, spec: Optional[str] = None) -> DSLintClassSpec:
d69b2b
+        """Yield specs the objects returned by `list` method provide."""
d69b2b
+
d69b2b
+        assert hasattr(self, 'list')
d69b2b
+
d69b2b
+        # Filter acording to the `spec`
d69b2b
+        wanted, rest_spec = DSLint._dslint_parse_spec(spec)
d69b2b
+        if wanted in [None, '*']:
d69b2b
+            clss = self.list()
d69b2b
+        else:
d69b2b
+            clss = (cls for cls in self.list() if cls.lint_uid() == wanted)
d69b2b
+
d69b2b
+        # Yield known specs
d69b2b
+        for cls in clss:
d69b2b
+            for fn, f in cls.lint_list(spec=rest_spec):
d69b2b
+                yield (f'{cls.lint_uid()}:{fn}',
d69b2b
+                       partial(f, rest_spec) if rest_spec else f)
d69b2b
+
d69b2b
+    def lint(self, spec: DSLintMethodSpec = None) -> DSLintResults:
d69b2b
+        """Lint the objects returned by `list` method according to the `spec`."""
d69b2b
+
d69b2b
+        if spec == List:
d69b2b
+            yield from self.lint_list()
d69b2b
+        else:
d69b2b
+            for obj in self.list():
d69b2b
+                yield from obj.lint()
d69b2b
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
d69b2b
index 4f752f414..8863ad1a8 100644
d69b2b
--- a/src/lib389/lib389/backend.py
d69b2b
+++ b/src/lib389/lib389/backend.py
d69b2b
@@ -393,6 +393,10 @@ class BackendLegacy(object):
d69b2b
         replace = [(ldap.MOD_REPLACE, 'nsslapd-require-index', 'on')]
d69b2b
         self.modify_s(dn, replace)
d69b2b
 
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'backends'
d69b2b
+
d69b2b
 
d69b2b
 class Backend(DSLdapObject):
d69b2b
     """Backend DSLdapObject with:
d69b2b
@@ -413,10 +417,12 @@ class Backend(DSLdapObject):
d69b2b
         self._must_attributes = ['nsslapd-suffix', 'cn']
d69b2b
         self._create_objectclasses = ['top', 'extensibleObject', BACKEND_OBJECTCLASS_VALUE]
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = [self._lint_mappingtree, self._lint_search, self._lint_virt_attrs]
d69b2b
         # Check if a mapping tree for this suffix exists.
d69b2b
         self._mts = MappingTrees(self._instance)
d69b2b
 
d69b2b
+    def lint_uid(self):
d69b2b
+        return self.get_attr_val_utf8_l('cn').lower()
d69b2b
+
d69b2b
     def _lint_virt_attrs(self):
d69b2b
         """Check if any virtual attribute are incorrectly indexed"""
d69b2b
         indexes = self.get_indexes()
d69b2b
@@ -497,7 +503,6 @@ class Backend(DSLdapObject):
d69b2b
             result = DSBLE0001
d69b2b
             result['items'] = [bename, ]
d69b2b
             yield result
d69b2b
-        return None
d69b2b
 
d69b2b
     def create_sample_entries(self, version):
d69b2b
         """Creates sample entries under nsslapd-suffix value
d69b2b
@@ -848,6 +853,10 @@ class Backends(DSLdapObjects):
d69b2b
         self._childobject = Backend
d69b2b
         self._basedn = DN_LDBM
d69b2b
 
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'backends'
d69b2b
+
d69b2b
     def import_ldif(self, be_name, ldifs, chunk_size=None, encrypted=False, gen_uniq_id=None, only_core=False,
d69b2b
                     include_suffixes=None, exclude_suffixes=None):
d69b2b
         """Do an import of the suffix"""
d69b2b
diff --git a/src/lib389/lib389/cli_ctl/health.py b/src/lib389/lib389/cli_ctl/health.py
d69b2b
index 3d15ad85e..6333a753a 100644
d69b2b
--- a/src/lib389/lib389/cli_ctl/health.py
d69b2b
+++ b/src/lib389/lib389/cli_ctl/health.py
d69b2b
@@ -7,6 +7,9 @@
d69b2b
 # --- END COPYRIGHT BLOCK ---
d69b2b
 
d69b2b
 import json
d69b2b
+import re
d69b2b
+from lib389._mapped_object import DSLdapObjects
d69b2b
+from lib389._mapped_object_lint import DSLint
d69b2b
 from lib389.cli_base import connect_instance, disconnect_instance
d69b2b
 from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
d69b2b
 from lib389.backend import Backends
d69b2b
@@ -15,17 +18,17 @@ from lib389.monitor import MonitorDiskSpace
d69b2b
 from lib389.replica import Replica, Changelog5
d69b2b
 from lib389.nss_ssl import NssSsl
d69b2b
 from lib389.dseldif import FSChecks, DSEldif
d69b2b
+from lib389 import lint
d69b2b
 from lib389 import plugins
d69b2b
 from lib389._constants import DSRC_HOME
d69b2b
+from functools import partial
d69b2b
+from typing import Iterable
d69b2b
 
d69b2b
-# These get all instances, then check them all.
d69b2b
-CHECK_MANY_OBJECTS = [
d69b2b
-    Backends,
d69b2b
-]
d69b2b
 
d69b2b
 # These get single instances and check them.
d69b2b
 CHECK_OBJECTS = [
d69b2b
     Config,
d69b2b
+    Backends,
d69b2b
     Encryption,
d69b2b
     FSChecks,
d69b2b
     plugins.ReferentialIntegrityPlugin,
d69b2b
@@ -52,44 +55,51 @@ def _format_check_output(log, result, idx):
d69b2b
     log.info(result['fix'])
d69b2b
 
d69b2b
 
d69b2b
-def health_check_run(inst, log, args):
d69b2b
-    """Connect to the local server using LDAPI, and perform various health checks
d69b2b
-    """
d69b2b
+def _list_targets(inst):
d69b2b
+    for c in CHECK_OBJECTS:
d69b2b
+        o = c(inst)
d69b2b
+        yield o.lint_uid(), o
d69b2b
+
d69b2b
+
d69b2b
+def _list_errors(log):
d69b2b
+    for r in map(partial(getattr, lint),
d69b2b
+                 filter(partial(re.match, r'^DS'),
d69b2b
+                        dir(lint))):
d69b2b
+        log.info(f"{r['dsle']} :: {r['description']}")
d69b2b
 
d69b2b
-    # update the args for connect_instance()
d69b2b
-    args.basedn = None
d69b2b
-    args.binddn = None
d69b2b
-    args.bindpw = None
d69b2b
-    args.starttls = None
d69b2b
-    args.pwdfile = None
d69b2b
-    args.prompt = False
d69b2b
-    dsrc_inst = dsrc_to_ldap(DSRC_HOME, args.instance, log.getChild('dsrc'))
d69b2b
-    dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
d69b2b
-    try:
d69b2b
-        inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
d69b2b
-    except Exception as e:
d69b2b
-        raise ValueError('Failed to connect to Directory Server instance: ' + str(e))
d69b2b
 
d69b2b
+def _list_checks(inst, specs: Iterable[str]):
d69b2b
+    o_uids = dict(_list_targets(inst))
d69b2b
+    for s in specs:
d69b2b
+        wanted, rest = DSLint._dslint_parse_spec(s)
d69b2b
+        if wanted == '*':
d69b2b
+            raise ValueError('Unexpected spec selector asterisk')
d69b2b
+
d69b2b
+        if wanted in o_uids:
d69b2b
+            for l in o_uids[wanted].lint_list(rest):
d69b2b
+                yield o_uids[wanted], l
d69b2b
+        else:
d69b2b
+            raise ValueError('No such object specifier')
d69b2b
+
d69b2b
+
d69b2b
+def _print_checks(inst, specs: Iterable[str]) -> None:
d69b2b
+    for o, s in _list_checks(inst, specs):
d69b2b
+        print(f'{o.lint_uid()}:{s[0]}')
d69b2b
+
d69b2b
+
d69b2b
+def _run(inst, log, args, checks):
d69b2b
     if not args.json:
d69b2b
         log.info("Beginning lint report, this could take a while ...")
d69b2b
+
d69b2b
     report = []
d69b2b
-    for lo in CHECK_MANY_OBJECTS:
d69b2b
+    for o, s in checks:
d69b2b
         if not args.json:
d69b2b
-            log.info("Checking %s ..." % lo.__name__)
d69b2b
-        lo_inst = lo(inst)
d69b2b
-        for clo in lo_inst.list():
d69b2b
-            result = clo.lint()
d69b2b
-            if result is not None:
d69b2b
-                report += result
d69b2b
-    for lo in CHECK_OBJECTS:
d69b2b
-        if not args.json:
d69b2b
-            log.info("Checking %s ..." % lo.__name__)
d69b2b
-        lo_inst = lo(inst)
d69b2b
-        result = lo_inst.lint()
d69b2b
-        if result is not None:
d69b2b
-            report += result
d69b2b
+            log.info(f"Checking {o.lint_uid()}:{s[0]} ...")
d69b2b
+        report += o.lint(s[0]) or []
d69b2b
+
d69b2b
     if not args.json:
d69b2b
         log.info("Healthcheck complete.")
d69b2b
+
d69b2b
     count = len(report)
d69b2b
     if count == 0:
d69b2b
         if not args.json:
d69b2b
@@ -110,6 +120,37 @@ def health_check_run(inst, log, args):
d69b2b
         else:
d69b2b
             log.info(json.dumps(report, indent=4))
d69b2b
 
d69b2b
+
d69b2b
+def health_check_run(inst, log, args):
d69b2b
+    """Connect to the local server using LDAPI, and perform various health checks
d69b2b
+    """
d69b2b
+
d69b2b
+    if args.list_errors:
d69b2b
+        _list_errors(log)
d69b2b
+        return
d69b2b
+
d69b2b
+    # update the args for connect_instance()
d69b2b
+    args.basedn = None
d69b2b
+    args.binddn = None
d69b2b
+    args.bindpw = None
d69b2b
+    args.starttls = None
d69b2b
+    args.pwdfile = None
d69b2b
+    args.prompt = False
d69b2b
+    dsrc_inst = dsrc_to_ldap(DSRC_HOME, args.instance, log.getChild('dsrc'))
d69b2b
+    dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
d69b2b
+    try:
d69b2b
+        inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
d69b2b
+    except Exception as e:
d69b2b
+        raise ValueError('Failed to connect to Directory Server instance: ' + str(e))
d69b2b
+
d69b2b
+    checks = args.check or dict(_list_targets(inst)).keys()
d69b2b
+
d69b2b
+    if args.list_checks or args.dry_run:
d69b2b
+        _print_checks(inst, checks)
d69b2b
+        return
d69b2b
+
d69b2b
+    _run(inst, log, args, _list_checks(inst, checks))
d69b2b
+
d69b2b
     disconnect_instance(inst)
d69b2b
 
d69b2b
 
d69b2b
@@ -120,4 +161,9 @@ def create_parser(subparsers):
d69b2b
         "remote Directory Server as this tool needs access to local resources, "
d69b2b
         "otherwise the report may be inaccurate.")
d69b2b
     run_healthcheck_parser.set_defaults(func=health_check_run)
d69b2b
-
d69b2b
+    run_healthcheck_parser.add_argument('--list-checks', action='store_true', help='List of known checks')
d69b2b
+    run_healthcheck_parser.add_argument('--list-errors', action='store_true', help='List of known error codes')
d69b2b
+    run_healthcheck_parser.add_argument('--dry-run', action='store_true', help='Do not execute the actual check, only list what would be done')
d69b2b
+    run_healthcheck_parser.add_argument('--check', nargs='+', default=None,
d69b2b
+                                        help='Areas to check. These can be obtained by --list-checks. Every element on the left of the colon (:)'
d69b2b
+                                             ' may be replaced by an asterisk if multiple options on the right are available.')
d69b2b
diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py
d69b2b
index a29d0244c..aa4c92beb 100644
d69b2b
--- a/src/lib389/lib389/config.py
d69b2b
+++ b/src/lib389/lib389/config.py
d69b2b
@@ -54,7 +54,6 @@ class Config(DSLdapObject):
d69b2b
         ]
d69b2b
         self._compare_exclude  = self._compare_exclude + config_compare_exclude
d69b2b
         self._rdn_attribute = 'cn'
d69b2b
-        self._lint_functions = [self._lint_hr_timestamp, self._lint_passwordscheme]
d69b2b
 
d69b2b
     @property
d69b2b
     def dn(self):
d69b2b
@@ -197,6 +196,10 @@ class Config(DSLdapObject):
d69b2b
         fields = 'nsslapd-security nsslapd-ssl-check-hostname'.split()
d69b2b
         return self._instance.getEntry(DN_CONFIG, attrlist=fields)
d69b2b
 
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'config'
d69b2b
+
d69b2b
     def _lint_hr_timestamp(self):
d69b2b
         hr_timestamp = self.get_attr_val('nsslapd-logging-hr-timestamps-enabled')
d69b2b
         if ensure_bytes('on') != hr_timestamp:
d69b2b
@@ -242,20 +245,22 @@ class Encryption(DSLdapObject):
d69b2b
         self._rdn_attribute = 'cn'
d69b2b
         self._must_attributes = ['cn']
d69b2b
         self._protected = True
d69b2b
-        self._lint_functions = [self._lint_check_tls_version]
d69b2b
 
d69b2b
     def create(self, rdn=None, properties={'cn': 'encryption', 'nsSSLClientAuth': 'allowed'}):
d69b2b
         if rdn is not None:
d69b2b
             self._log.debug("dn on cn=encryption is not None. This is a mistake.")
d69b2b
         super(Encryption, self).create(properties=properties)
d69b2b
 
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'encryption'
d69b2b
+
d69b2b
     def _lint_check_tls_version(self):
d69b2b
         tls_min = self.get_attr_val('sslVersionMin')
d69b2b
         if tls_min is not None and tls_min < ensure_bytes('TLS1.1'):
d69b2b
             report = copy.deepcopy(DSELE0001)
d69b2b
             report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
d69b2b
             yield report
d69b2b
-        yield None
d69b2b
 
d69b2b
     @property
d69b2b
     def ciphers(self):
d69b2b
@@ -487,7 +492,6 @@ class LDBMConfig(DSLdapObject):
d69b2b
         self._dn = DN_CONFIG_LDBM
d69b2b
         # config_compare_exclude = []
d69b2b
         self._rdn_attribute = 'cn'
d69b2b
-        self._lint_functions = []
d69b2b
         self._protected = True
d69b2b
 
d69b2b
 
d69b2b
@@ -506,5 +510,4 @@ class BDB_LDBMConfig(DSLdapObject):
d69b2b
         self._dn = DN_CONFIG_LDBM_BDB
d69b2b
         self._config_compare_exclude = []
d69b2b
         self._rdn_attribute = 'cn'
d69b2b
-        self._lint_functions = []
d69b2b
         self._protected = True
d69b2b
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
d69b2b
index 5378e6ee9..96c9af9d1 100644
d69b2b
--- a/src/lib389/lib389/dseldif.py
d69b2b
+++ b/src/lib389/lib389/dseldif.py
d69b2b
@@ -16,6 +16,7 @@ from datetime import timedelta
d69b2b
 from stat import ST_MODE
d69b2b
 # from lib389.utils import print_nice_time
d69b2b
 from lib389.paths import Paths
d69b2b
+from lib389._mapped_object_lint import DSLint
d69b2b
 from lib389.lint import (
d69b2b
     DSPERMLE0001,
d69b2b
     DSPERMLE0002,
d69b2b
@@ -25,7 +26,7 @@ from lib389.lint import (
d69b2b
 )
d69b2b
 
d69b2b
 
d69b2b
-class DSEldif(object):
d69b2b
+class DSEldif(DSLint):
d69b2b
     """A class for working with dse.ldif file
d69b2b
 
d69b2b
     :param instance: An instance
d69b2b
@@ -58,15 +59,10 @@ class DSEldif(object):
d69b2b
                         processed_line = line
d69b2b
                 else:
d69b2b
                     processed_line = processed_line[:-1] + line[1:]
d69b2b
-        self._lint_functions = [self._lint_nsstate]
d69b2b
 
d69b2b
-    def lint(self):
d69b2b
-        results = []
d69b2b
-        for fn in self._lint_functions:
d69b2b
-            for result in fn():
d69b2b
-                if result is not None:
d69b2b
-                    results.append(result)
d69b2b
-        return results
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'dseldif'
d69b2b
 
d69b2b
     def _lint_nsstate(self):
d69b2b
         suffixes = self.readNsState()
d69b2b
@@ -320,7 +316,7 @@ class DSEldif(object):
d69b2b
         return states
d69b2b
 
d69b2b
 
d69b2b
-class FSChecks(object):
d69b2b
+class FSChecks(DSLint):
d69b2b
     """This is for the healthcheck feature, check commonly used system config files the
d69b2b
     server uses.  This is here for lack of a better place to add this class.
d69b2b
     """
d69b2b
@@ -344,17 +340,10 @@ class FSChecks(object):
d69b2b
                 'report': DSPERMLE0002
d69b2b
             },
d69b2b
         ]
d69b2b
-        self._lint_functions = [self._lint_file_perms]
d69b2b
 
d69b2b
-    def lint(self):
d69b2b
-        """Run a lint/healthcheck for this class
d69b2b
-        """
d69b2b
-        results = []
d69b2b
-        for fn in self._lint_functions:
d69b2b
-            for result in fn():
d69b2b
-                if result is not None:
d69b2b
-                    results.append(result)
d69b2b
-        return results
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'fschecks'
d69b2b
 
d69b2b
     def _lint_file_perms(self):
d69b2b
         """Test file permissions are safe
d69b2b
diff --git a/src/lib389/lib389/encrypted_attributes.py b/src/lib389/lib389/encrypted_attributes.py
d69b2b
index 9afd2e66b..2fa26cef9 100644
d69b2b
--- a/src/lib389/lib389/encrypted_attributes.py
d69b2b
+++ b/src/lib389/lib389/encrypted_attributes.py
d69b2b
@@ -27,7 +27,6 @@ class EncryptedAttr(DSLdapObject):
d69b2b
         self._must_attributes = ['cn', 'nsEncryptionAlgorithm']
d69b2b
         self._create_objectclasses = ['top', 'nsAttributeEncryption']
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = []
d69b2b
 
d69b2b
 
d69b2b
 class EncryptedAttrs(DSLdapObjects):
d69b2b
diff --git a/src/lib389/lib389/index.py b/src/lib389/lib389/index.py
d69b2b
index 6932883b7..a3d019d27 100644
d69b2b
--- a/src/lib389/lib389/index.py
d69b2b
+++ b/src/lib389/lib389/index.py
d69b2b
@@ -41,7 +41,6 @@ class Index(DSLdapObject):
d69b2b
         self._must_attributes = ['cn', 'nsSystemIndex', 'nsIndexType']
d69b2b
         self._create_objectclasses = ['top', 'nsIndex']
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = []
d69b2b
 
d69b2b
 
d69b2b
 class Indexes(DSLdapObjects):
d69b2b
@@ -77,7 +76,6 @@ class VLVSearch(DSLdapObject):
d69b2b
         self._must_attributes = ['cn', 'vlvbase', 'vlvscope', 'vlvfilter']
d69b2b
         self._create_objectclasses = ['top', 'vlvSearch']
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = []
d69b2b
         self._be_name = None
d69b2b
 
d69b2b
     def get_sorts(self):
d69b2b
@@ -163,7 +161,6 @@ class VLVIndex(DSLdapObject):
d69b2b
         self._must_attributes = ['cn', 'vlvsort']
d69b2b
         self._create_objectclasses = ['top', 'vlvIndex']
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = []
d69b2b
 
d69b2b
 
d69b2b
 class VLVIndexes(DSLdapObjects):
d69b2b
diff --git a/src/lib389/lib389/lint.py b/src/lib389/lib389/lint.py
d69b2b
index b5a305bc3..a103feec7 100644
d69b2b
--- a/src/lib389/lib389/lint.py
d69b2b
+++ b/src/lib389/lib389/lint.py
d69b2b
@@ -14,8 +14,9 @@
d69b2b
 DSBLE0001 = {
d69b2b
     'dsle': 'DSBLE0001',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : [],
d69b2b
-    'detail' : """This backend may be missing the correct mapping tree references. Mapping Trees allow
d69b2b
+    'description': 'Possibly incorrect mapping tree.',
d69b2b
+    'items': [],
d69b2b
+    'detail': """This backend may be missing the correct mapping tree references. Mapping Trees allow
d69b2b
 the directory server to determine which backend an operation is routed to in the
d69b2b
 abscence of other information. This is extremely important for correct functioning
d69b2b
 of LDAP ADD for example.
d69b2b
@@ -32,7 +33,7 @@ objectClass: extensibleObject
d69b2b
 objectClass: nsMappingTree
d69b2b
 
d69b2b
 """,
d69b2b
-    'fix' : """Either you need to create the mapping tree, or you need to repair the related
d69b2b
+    'fix': """Either you need to create the mapping tree, or you need to repair the related
d69b2b
 mapping tree. You will need to do this by hand by editing cn=config, or stopping
d69b2b
 the instance and editing dse.ldif.
d69b2b
 """
d69b2b
@@ -41,25 +42,28 @@ the instance and editing dse.ldif.
d69b2b
 DSBLE0002 = {
d69b2b
     'dsle': 'DSBLE0002',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : [],
d69b2b
-    'detail' : """Unable to query the backend.  LDAP error (ERROR)""",
d69b2b
-    'fix' : """Check the server's error and access logs for more information."""
d69b2b
+    'description': 'Unable to query backend.',
d69b2b
+    'items': [],
d69b2b
+    'detail': """Unable to query the backend.  LDAP error (ERROR)""",
d69b2b
+    'fix': """Check the server's error and access logs for more information."""
d69b2b
 }
d69b2b
 
d69b2b
 DSBLE0003 = {
d69b2b
     'dsle': 'DSBLE0003',
d69b2b
     'severity': 'LOW',
d69b2b
-    'items' : [],
d69b2b
-    'detail' : """The backend database has not been initialized yet""",
d69b2b
-    'fix' : """You need to import an LDIF file, or create the suffix entry, in order to initialize the database."""
d69b2b
+    'description': 'Uninitialized backend database.',
d69b2b
+    'items': [],
d69b2b
+    'detail': """The backend database has not been initialized yet""",
d69b2b
+    'fix': """You need to import an LDIF file, or create the suffix entry, in order to initialize the database."""
d69b2b
 }
d69b2b
 
d69b2b
 # Config checks
d69b2b
 DSCLE0001 = {
d69b2b
-    'dsle' : 'DSCLE0001',
d69b2b
-    'severity' : 'LOW',
d69b2b
+    'dsle': 'DSCLE0001',
d69b2b
+    'severity': 'LOW',
d69b2b
+    'description': 'Different log timestamp format.',
d69b2b
     'items': ['cn=config', ],
d69b2b
-    'detail' : """nsslapd-logging-hr-timestamps-enabled changes the log format in directory server from
d69b2b
+    'detail': """nsslapd-logging-hr-timestamps-enabled changes the log format in directory server from
d69b2b
 
d69b2b
 [07/Jun/2017:17:15:58 +1000]
d69b2b
 
d69b2b
@@ -70,7 +74,7 @@ to
d69b2b
 This actually provides a performance improvement. Additionally, this setting will be
d69b2b
 removed in a future release.
d69b2b
 """,
d69b2b
-    'fix' : """Set nsslapd-logging-hr-timestamps-enabled to on.
d69b2b
+    'fix': """Set nsslapd-logging-hr-timestamps-enabled to on.
d69b2b
 You can use 'dsconf' to set this attribute.  Here is an example:
d69b2b
 
d69b2b
     # dsconf slapd-YOUR_INSTANCE config replace nsslapd-logging-hr-timestamps-enabled=on"""
d69b2b
@@ -79,8 +83,9 @@ You can use 'dsconf' to set this attribute.  Here is an example:
d69b2b
 DSCLE0002 = {
d69b2b
     'dsle': 'DSCLE0002',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['cn=config', ],
d69b2b
-    'detail' : """Password storage schemes in Directory Server define how passwords are hashed via a
d69b2b
+    'description': 'Weak passwordStorageScheme.',
d69b2b
+    'items': ['cn=config', ],
d69b2b
+    'detail': """Password storage schemes in Directory Server define how passwords are hashed via a
d69b2b
 one-way mathematical function for storage. Knowing the hash it is difficult to gain
d69b2b
 the input, but knowing the input you can easily compare the hash.
d69b2b
 
d69b2b
@@ -112,14 +117,15 @@ You can also use 'dsconf' to replace these values.  Here is an example:
d69b2b
 DSELE0001 = {
d69b2b
     'dsle': 'DSELE0001',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['cn=encryption,cn=config', ],
d69b2b
+    'description': 'Weak TLS protocol version.',
d69b2b
+    'items': ['cn=encryption,cn=config', ],
d69b2b
     'detail': """This Directory Server may not be using strong TLS protocol versions. TLS1.0 is known to
d69b2b
 have a number of issues with the protocol. Please see:
d69b2b
 
d69b2b
 https://tools.ietf.org/html/rfc7457
d69b2b
 
d69b2b
 It is advised you set this value to the maximum possible.""",
d69b2b
-    'fix' : """There are two options for setting the TLS minimum version allowed.  You,
d69b2b
+    'fix': """There are two options for setting the TLS minimum version allowed.  You,
d69b2b
 can set "sslVersionMin" in "cn=encryption,cn=config" to a version greater than "TLS1.0"
d69b2b
 You can also use 'dsconf' to set this value.  Here is an example:
d69b2b
 
d69b2b
@@ -137,7 +143,8 @@ minimum version, but doing this affects the entire system:
d69b2b
 DSRILE0001 = {
d69b2b
     'dsle': 'DSRILE0001',
d69b2b
     'severity': 'LOW',
d69b2b
-    'items' : ['cn=referential integrity postoperation,cn=plugins,cn=config', ],
d69b2b
+    'description': 'Referential integrity plugin may be slower.',
d69b2b
+    'items': ['cn=referential integrity postoperation,cn=plugins,cn=config', ],
d69b2b
     'detail': """The referential integrity plugin has an asynchronous processing mode.
d69b2b
 This is controlled by the update-delay flag.  When this value is 0, referential
d69b2b
 integrity plugin processes these changes inside of the operation that modified
d69b2b
@@ -151,7 +158,7 @@ delays to your server by batching changes rather than smaller updates during syn
d69b2b
 
d69b2b
 We advise that you set this value to 0, and enable referint on all masters as it provides a more predictable behaviour.
d69b2b
 """,
d69b2b
-    'fix' : """Set referint-update-delay to 0.
d69b2b
+    'fix': """Set referint-update-delay to 0.
d69b2b
 
d69b2b
 You can use 'dsconf' to set this value.  Here is an example:
d69b2b
 
d69b2b
@@ -164,12 +171,13 @@ You must restart the Directory Server for this change to take effect."""
d69b2b
 DSRILE0002 = {
d69b2b
     'dsle': 'DSRILE0002',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['cn=referential integrity postoperation,cn=plugins,cn=config'],
d69b2b
+    'description': 'Referential integrity plugin configured with unindexed attribute.',
d69b2b
+    'items': ['cn=referential integrity postoperation,cn=plugins,cn=config'],
d69b2b
     'detail': """The referential integrity plugin is configured to use an attribute (ATTR)
d69b2b
 that does not have an "equality" index in backend (BACKEND).
d69b2b
 Failure to have the proper indexing will lead to unindexed searches which
d69b2b
 cause high CPU and can significantly slow the server down.""",
d69b2b
-    'fix' : """Check the attributes set in "referint-membership-attr" to make sure they have
d69b2b
+    'fix': """Check the attributes set in "referint-membership-attr" to make sure they have
d69b2b
 an index defined that has at least the equality "eq" index type.  You will
d69b2b
 need to reindex the database after adding the missing index type. Here is an
d69b2b
 example using dsconf:
d69b2b
@@ -182,12 +190,13 @@ example using dsconf:
d69b2b
 DSDSLE0001 = {
d69b2b
     'dsle': 'DSDSLE0001',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['Server', 'cn=config'],
d69b2b
+    'description': 'Low disk space.',
d69b2b
+    'items': ['Server', 'cn=config'],
d69b2b
     'detail': """The disk partition used by the server (PARTITION), either for the database, the
d69b2b
 configuration files, or the logs is over 90% full.  If the partition becomes
d69b2b
 completely filled serious problems can occur with the database or the server's
d69b2b
 stability.""",
d69b2b
-    'fix' : """Attempt to free up disk space.  Also try removing old rotated logs, or disable any
d69b2b
+    'fix': """Attempt to free up disk space.  Also try removing old rotated logs, or disable any
d69b2b
 verbose logging levels that might have been set.  You might consider enabling
d69b2b
 the "Disk Monitoring" feature in cn=config to help prevent a disorderly shutdown
d69b2b
 of the server:
d69b2b
@@ -210,9 +219,10 @@ Please see the Administration guide for more information:
d69b2b
 DSREPLLE0001 = {
d69b2b
     'dsle': 'DSREPLLE0001',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['Replication', 'Agreement'],
d69b2b
+    'description': 'Replication agreement not set to be synchronized.',
d69b2b
+    'items': ['Replication', 'Agreement'],
d69b2b
     'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization.""",
d69b2b
-    'fix' : """You may need to reinitialize this replication agreement.  Please check the errors
d69b2b
+    'fix': """You may need to reinitialize this replication agreement.  Please check the errors
d69b2b
 log for more information.  If you do need to reinitialize the agreement you can do so
d69b2b
 using dsconf.  Here is an example:
d69b2b
 
d69b2b
@@ -223,9 +233,10 @@ using dsconf.  Here is an example:
d69b2b
 DSREPLLE0002 = {
d69b2b
     'dsle': 'DSREPLLE0002',
d69b2b
     'severity': 'LOW',
d69b2b
-    'items' : ['Replication', 'Conflict Entries'],
d69b2b
+    'description': 'Replication conflict entries found.',
d69b2b
+    'items': ['Replication', 'Conflict Entries'],
d69b2b
     'detail': "There were COUNT conflict entries found under the replication suffix \"SUFFIX\".",
d69b2b
-    'fix' : """While conflict entries are expected to occur in an MMR environment, they
d69b2b
+    'fix': """While conflict entries are expected to occur in an MMR environment, they
d69b2b
 should be resolved.  In regards to conflict entries there is always the original/counterpart
d69b2b
 entry that has a normal DN, and then the conflict version of that entry.  Technically both
d69b2b
 entries are valid, you as the administrator, needs to decide which entry you want to keep.
d69b2b
@@ -253,38 +264,42 @@ can use the CLI tool "dsconf" to resolve the conflict.  Here is an example:
d69b2b
 DSREPLLE0003 = {
d69b2b
     'dsle': 'DSREPLLE0003',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['Replication', 'Agreement'],
d69b2b
+    'description': 'Unsynchronized replication agreement.',
d69b2b
+    'items': ['Replication', 'Agreement'],
d69b2b
     'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization.
d69b2b
 Status message: MSG""",
d69b2b
-    'fix' : """Replication is not in synchronization but it may recover.  Continue to
d69b2b
+    'fix': """Replication is not in synchronization but it may recover.  Continue to
d69b2b
 monitor this agreement."""
d69b2b
 }
d69b2b
 
d69b2b
 DSREPLLE0004 = {
d69b2b
     'dsle': 'DSREPLLE0004',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['Replication', 'Agreement'],
d69b2b
+    'description': 'Unable to get replication agreement status.',
d69b2b
+    'items': ['Replication', 'Agreement'],
d69b2b
     'detail': """Failed to get the agreement status for agreement (AGMT) under "SUFFIX".  Error (ERROR).""",
d69b2b
-    'fix' : """None"""
d69b2b
+    'fix': """None"""
d69b2b
 }
d69b2b
 
d69b2b
 DSREPLLE0005 = {
d69b2b
     'dsle': 'DSREPLLE0005',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['Replication', 'Agreement'],
d69b2b
+    'description': 'Replication consumer not reachable.',
d69b2b
+    'items': ['Replication', 'Agreement'],
d69b2b
     'detail': """The replication agreement (AGMT) under "SUFFIX" is not in synchronization,
d69b2b
 because the consumer server is not reachable.""",
d69b2b
-    'fix' : """Check if the consumer is running, and also check the errors log for more information."""
d69b2b
+    'fix': """Check if the consumer is running, and also check the errors log for more information."""
d69b2b
 }
d69b2b
 
d69b2b
 # Replication changelog
d69b2b
 DSCLLE0001 = {
d69b2b
     'dsle': 'DSCLLE0001',
d69b2b
     'severity': 'LOW',
d69b2b
-    'items' : ['Replication', 'Changelog'],
d69b2b
+    'description': 'Changelog trimming not configured.',
d69b2b
+    'items': ['Replication', 'Changelog'],
d69b2b
     'detail': """The replication changelog does have any kind of trimming configured.  This will
d69b2b
 lead to the changelog size growing indefinitely.""",
d69b2b
-    'fix' : """Configure changelog trimming, preferably by setting the maximum age of a changelog
d69b2b
+    'fix': """Configure changelog trimming, preferably by setting the maximum age of a changelog
d69b2b
 record.  Here is an example:
d69b2b
 
d69b2b
     # dsconf slapd-YOUR_INSTANCE replication set-changelog --max-age 30d"""
d69b2b
@@ -294,27 +309,30 @@ record.  Here is an example:
d69b2b
 DSCERTLE0001 = {
d69b2b
     'dsle': 'DSCERTLE0001',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['Expiring Certificate'],
d69b2b
+    'description': 'Certificate about to expire.',
d69b2b
+    'items': ['Expiring Certificate'],
d69b2b
     'detail': """The certificate (CERT) will expire in less than 30 days""",
d69b2b
-    'fix' : """Renew the certificate before it expires to prevent disruptions with TLS connections."""
d69b2b
+    'fix': """Renew the certificate before it expires to prevent disruptions with TLS connections."""
d69b2b
 }
d69b2b
 
d69b2b
 DSCERTLE0002 = {
d69b2b
     'dsle': 'DSCERTLE0002',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['Expired Certificate'],
d69b2b
+    'description': 'Certificate expired.',
d69b2b
+    'items': ['Expired Certificate'],
d69b2b
     'detail': """The certificate (CERT) has expired""",
d69b2b
-    'fix' : """Renew or remove the certificate."""
d69b2b
+    'fix': """Renew or remove the certificate."""
d69b2b
 }
d69b2b
 
d69b2b
 # Virtual Attrs & COS.  Note - ATTR and SUFFIX are replaced by the reporting function
d69b2b
 DSVIRTLE0001 = {
d69b2b
     'dsle': 'DSVIRTLE0001',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['Virtual Attributes'],
d69b2b
+    'description': 'Virtual attribute indexed.',
d69b2b
+    'items': ['Virtual Attributes'],
d69b2b
     'detail': """You should not index virtual attributes, and as this will break searches that
d69b2b
 use the attribute in a filter.""",
d69b2b
-    'fix' : """Remove the index for this attribute from the backend configuration.
d69b2b
+    'fix': """Remove the index for this attribute from the backend configuration.
d69b2b
 Here is an example using 'dsconf' to remove an index:
d69b2b
 
d69b2b
     # dsconf slapd-YOUR_INSTANCE backend index delete --attr ATTR SUFFIX"""
d69b2b
@@ -324,10 +342,11 @@ Here is an example using 'dsconf' to remove an index:
d69b2b
 DSPERMLE0001 = {
d69b2b
     'dsle': 'DSPERMLE0001',
d69b2b
     'severity': 'MEDIUM',
d69b2b
-    'items' : ['File Permissions'],
d69b2b
+    'description': 'Incorrect file permissions.',
d69b2b
+    'items': ['File Permissions'],
d69b2b
     'detail': """The file "FILE" does not have the expected permissions (PERMS).  This
d69b2b
 can cause issues with replication and chaining.""",
d69b2b
-    'fix' : """Change the file permissions:
d69b2b
+    'fix': """Change the file permissions:
d69b2b
 
d69b2b
     # chmod PERMS FILE"""
d69b2b
 }
d69b2b
@@ -336,10 +355,11 @@ can cause issues with replication and chaining.""",
d69b2b
 DSPERMLE0002 = {
d69b2b
     'dsle': 'DSPERMLE0002',
d69b2b
     'severity': 'HIGH',
d69b2b
-    'items' : ['File Permissions'],
d69b2b
+    'description': 'Incorrect security database file permissions.',
d69b2b
+    'items': ['File Permissions'],
d69b2b
     'detail': """The file "FILE" does not have the expected permissions (PERMS).  The
d69b2b
 security database pin/password files should only be readable by Directory Server user.""",
d69b2b
-    'fix' : """Change the file permissions:
d69b2b
+    'fix': """Change the file permissions:
d69b2b
 
d69b2b
     # chmod PERMS FILE"""
d69b2b
 }
d69b2b
@@ -348,11 +368,12 @@ security database pin/password files should only be readable by Directory Server
d69b2b
 DSSKEWLE0001 = {
d69b2b
     'dsle': 'DSSKEWLE0001',
d69b2b
     'severity': 'Low',
d69b2b
-    'items' : ['Replication'],
d69b2b
+    'description': 'Medium time skew.',
d69b2b
+    'items': ['Replication'],
d69b2b
     'detail': """The time skew is over 6 hours.  If this time skew continues to increase
d69b2b
 to 24 hours then replication can potentially stop working.  Please continue to
d69b2b
 monitor the time skew offsets for increasing values.""",
d69b2b
-    'fix' : """Monitor the time skew and avoid making changes to the system time.
d69b2b
+    'fix': """Monitor the time skew and avoid making changes to the system time.
d69b2b
 Also look at https://access.redhat.com/documentation/en-us/red_hat_directory_server/11/html/administration_guide/managing_replication-troubleshooting_replication_related_problems
d69b2b
 and find the paragraph "Too much time skew"."""
d69b2b
 }
d69b2b
@@ -360,13 +381,14 @@ and find the paragraph "Too much time skew"."""
d69b2b
 DSSKEWLE0002 = {
d69b2b
     'dsle': 'DSSKEWLE0002',
d69b2b
     'severity': 'Medium',
d69b2b
-    'items' : ['Replication'],
d69b2b
+    'description': 'Major time skew.',
d69b2b
+    'items': ['Replication'],
d69b2b
     'detail': """The time skew is over 12 hours.  If this time skew continues to increase
d69b2b
 to 24 hours then replication can potentially stop working.  Please continue to
d69b2b
 monitor the time skew offsets for increasing values.  Setting nsslapd-ignore-time-skew
d69b2b
 to "on" on each replica will allow replication to continue, but if the time skew
d69b2b
 continues to increase other more serious replication problems can occur.""",
d69b2b
-    'fix' : """Monitor the time skew and avoid making changes to the system time.
d69b2b
+    'fix': """Monitor the time skew and avoid making changes to the system time.
d69b2b
 If you get close to 24 hours of time skew replication may stop working.
d69b2b
 In that case configure the server to ignore the time skew until the system
d69b2b
 times can be fixed/synchronized:
d69b2b
@@ -380,12 +402,13 @@ and find the paragraph "Too much time skew"."""
d69b2b
 DSSKEWLE0003 = {
d69b2b
     'dsle': 'DSSKEWLE0003',
d69b2b
     'severity': 'High',
d69b2b
-    'items' : ['Replication'],
d69b2b
+    'description': 'Extensive time skew.',
d69b2b
+    'items': ['Replication'],
d69b2b
     'detail': """The time skew is over 24 hours.  Setting nsslapd-ignore-time-skew
d69b2b
 to "on" on each replica will allow replication to continue, but if the
d69b2b
 time skew continues to increase other serious replication problems can
d69b2b
 occur.""",
d69b2b
-    'fix' : """Avoid making changes to the system time, and make sure the clocks
d69b2b
+    'fix': """Avoid making changes to the system time, and make sure the clocks
d69b2b
 on all the replicas are correct.  If you haven't set the server's
d69b2b
 "ignore time skew" setting then do the following on all the replicas
d69b2b
 until the time issues have been resolved:
d69b2b
diff --git a/src/lib389/lib389/monitor.py b/src/lib389/lib389/monitor.py
d69b2b
index 73750c3c2..4ac7d7174 100644
d69b2b
--- a/src/lib389/lib389/monitor.py
d69b2b
+++ b/src/lib389/lib389/monitor.py
d69b2b
@@ -358,7 +358,10 @@ class MonitorDiskSpace(DSLdapObject):
d69b2b
     def __init__(self, instance, dn=None):
d69b2b
         super(MonitorDiskSpace, self).__init__(instance=instance, dn=dn)
d69b2b
         self._dn = "cn=disk space,cn=monitor"
d69b2b
-        self._lint_functions = [self._lint_disk_space]
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'monitor-disk-space'
d69b2b
 
d69b2b
     def _lint_disk_space(self):
d69b2b
         partitions = self.get_attr_vals_utf8_l("dsDisk")
d69b2b
diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
d69b2b
index d14e7ce6f..e257424fd 100644
d69b2b
--- a/src/lib389/lib389/nss_ssl.py
d69b2b
+++ b/src/lib389/lib389/nss_ssl.py
d69b2b
@@ -21,6 +21,7 @@ import subprocess
d69b2b
 from datetime import datetime, timedelta
d69b2b
 from subprocess import check_output, run, PIPE
d69b2b
 from lib389.passwd import password_generate
d69b2b
+from lib389._mapped_object_lint import DSLint
d69b2b
 from lib389.lint import DSCERTLE0001, DSCERTLE0002
d69b2b
 from lib389.utils import ensure_str, format_cmd_list
d69b2b
 import uuid
d69b2b
@@ -42,7 +43,7 @@ VALID_MIN = 61  # Days
d69b2b
 log = logging.getLogger(__name__)
d69b2b
 
d69b2b
 
d69b2b
-class NssSsl(object):
d69b2b
+class NssSsl(DSLint):
d69b2b
     def __init__(self, dirsrv=None, dbpassword=None, dbpath=None):
d69b2b
         self.dirsrv = dirsrv
d69b2b
         self._certdb = dbpath
d69b2b
@@ -56,18 +57,14 @@ class NssSsl(object):
d69b2b
         else:
d69b2b
             self.dbpassword = dbpassword
d69b2b
 
d69b2b
-        self.db_files = {"dbm_backend": ["%s/%s" % (self._certdb, f) for f in ("key3.db", "cert8.db", "secmod.db")],
d69b2b
-                         "sql_backend": ["%s/%s" % (self._certdb, f) for f in ("key4.db", "cert9.db", "pkcs11.txt")],
d69b2b
-                         "support": ["%s/%s" % (self._certdb, f) for f in ("noise.txt", PIN_TXT, PWD_TXT)]}
d69b2b
-        self._lint_functions = [self._lint_certificate_expiration,]
d69b2b
-
d69b2b
-    def lint(self):
d69b2b
-        results = []
d69b2b
-        for fn in self._lint_functions:
d69b2b
-            for result in fn():
d69b2b
-                if result is not None:
d69b2b
-                    results.append(result)
d69b2b
-        return results
d69b2b
+        self.db_files = {group: [f"{self._certdb}/{f}" for f in files]
d69b2b
+                         for group, files in {"dbm_backend": ("key3.db", "cert8.db", "secmod.db"),
d69b2b
+                                              "sql_backend": ("key4.db", "cert9.db", "pkcs11.txt"),
d69b2b
+                                              "support": ("noise.txt", PIN_TXT, PWD_TXT)}.items()}
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'ssl'
d69b2b
 
d69b2b
     def _lint_certificate_expiration(self):
d69b2b
         """Check all the certificates in the db if they will expire within 30 days
d69b2b
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
d69b2b
index f68a1d114..89e660287 100644
d69b2b
--- a/src/lib389/lib389/plugins.py
d69b2b
+++ b/src/lib389/lib389/plugins.py
d69b2b
@@ -431,7 +431,6 @@ class ReferentialIntegrityPlugin(Plugin):
d69b2b
             'referint-logfile',
d69b2b
             'referint-membership-attr',
d69b2b
         ])
d69b2b
-        self._lint_functions = [self._lint_update_delay, self._lint_attr_indexes]
d69b2b
 
d69b2b
     def create(self, rdn=None, properties=None, basedn=None):
d69b2b
         """Create an instance of the plugin"""
d69b2b
@@ -443,6 +442,10 @@ class ReferentialIntegrityPlugin(Plugin):
d69b2b
             properties['referint-logfile'] = referint_log
d69b2b
         return super(ReferentialIntegrityPlugin, self).create(rdn, properties, basedn)
d69b2b
 
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'refint'
d69b2b
+
d69b2b
     def _lint_update_delay(self):
d69b2b
         if self.status():
d69b2b
             delay = self.get_attr_val_int("referint-update-delay")
d69b2b
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
d69b2b
index f8adb3ce2..f575e58d5 100644
d69b2b
--- a/src/lib389/lib389/replica.py
d69b2b
+++ b/src/lib389/lib389/replica.py
d69b2b
@@ -1049,7 +1049,10 @@ class Changelog5(DSLdapObject):
d69b2b
                 'extensibleobject',
d69b2b
             ]
d69b2b
         self._protected = False
d69b2b
-        self._lint_functions = [self._lint_cl_trimming]
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'changelog'
d69b2b
 
d69b2b
     def _lint_cl_trimming(self):
d69b2b
         """Check that cl trimming is at least defined to prevent unbounded growth"""
d69b2b
@@ -1120,7 +1123,10 @@ class Replica(DSLdapObject):
d69b2b
             self._create_objectclasses.append('extensibleobject')
d69b2b
         self._protected = False
d69b2b
         self._suffix = None
d69b2b
-        self._lint_functions = [self._lint_agmts_status, self._lint_conflicts]
d69b2b
+
d69b2b
+    @classmethod
d69b2b
+    def lint_uid(cls):
d69b2b
+        return 'replication'
d69b2b
 
d69b2b
     def _lint_agmts_status(self):
d69b2b
         replicas = Replicas(self._instance).list()
d69b2b
diff --git a/src/lib389/lib389/tests/mapped_object_lint_test.py b/src/lib389/lib389/tests/mapped_object_lint_test.py
d69b2b
new file mode 100644
d69b2b
index 000000000..a4ca0ea3c
d69b2b
--- /dev/null
d69b2b
+++ b/src/lib389/lib389/tests/mapped_object_lint_test.py
d69b2b
@@ -0,0 +1,78 @@
d69b2b
+from typing import List
d69b2b
+
d69b2b
+import pytest
d69b2b
+
d69b2b
+from lib389._mapped_object_lint import (
d69b2b
+    DSLint,
d69b2b
+    DSLints,
d69b2b
+    DSLintMethodSpec
d69b2b
+)
d69b2b
+
d69b2b
+
d69b2b
+def test_dslint():
d69b2b
+    class DS(DSLint):
d69b2b
+        def lint_uid(self) -> str:
d69b2b
+            return self.param
d69b2b
+
d69b2b
+        def __init__(self, param):
d69b2b
+            self.param = param
d69b2b
+            self.suffixes = ['suffixA', 'suffixB']
d69b2b
+
d69b2b
+        def _lint_nsstate(self, spec: DSLintMethodSpec = None):
d69b2b
+            if spec == List:
d69b2b
+                yield from self.suffixes
d69b2b
+            else:
d69b2b
+                to_lint = [spec] if spec else self._lint_nsstate(spec=List)
d69b2b
+                for tl in to_lint:
d69b2b
+                    if tl == 'suffixA':
d69b2b
+                        pass
d69b2b
+                    elif tl == 'suffixB':
d69b2b
+                        yield 'suffixB is bad'
d69b2b
+                    else:
d69b2b
+                        raise ValueError('There is no such suffix')
d69b2b
+
d69b2b
+        def _lint_second(self):
d69b2b
+            yield from ()
d69b2b
+
d69b2b
+        def _lint_third(self):
d69b2b
+            yield from ['this is a fail']
d69b2b
+
d69b2b
+    class DSs(DSLints):
d69b2b
+        def list(self):
d69b2b
+            for i in [DS("ma"), DS("mb")]:
d69b2b
+                yield i
d69b2b
+
d69b2b
+    # single
d69b2b
+    inst = DS("a")
d69b2b
+    inst_lints = {'nsstate:suffixA', 'nsstate:suffixB', 'second', 'third'}
d69b2b
+
d69b2b
+    assert inst.param == "a"
d69b2b
+
d69b2b
+    assert set(dict(inst.lint_list()).keys()) == inst_lints
d69b2b
+
d69b2b
+    assert set(dict(inst.lint_list('nsstate')).keys()) \
d69b2b
+        == {f'nsstate:suffix{s}' for s in "AB"}
d69b2b
+
d69b2b
+    assert list(inst._lint_nsstate(spec=List)) == ['suffixA', 'suffixB']
d69b2b
+    assert list(inst.lint()) == ['suffixB is bad', 'this is a fail']
d69b2b
+
d69b2b
+    assert list(inst.lint('nsstate')) == ['suffixB is bad']
d69b2b
+    assert list(inst.lint('nsstate:suffixA')) == []
d69b2b
+    assert list(inst.lint('nsstate:suffixB')) == ['suffixB is bad']
d69b2b
+    with pytest.raises(ValueError):
d69b2b
+        list(inst.lint('nonexistent'))
d69b2b
+
d69b2b
+    # multiple
d69b2b
+    insts = DSs()
d69b2b
+
d69b2b
+    assert insts.lint_list
d69b2b
+    assert insts.lint
d69b2b
+
d69b2b
+    assert set(dict(insts.lint_list()).keys()) \
d69b2b
+        == {f'{m}:{s}' for m in ['ma', 'mb'] for s in inst_lints}
d69b2b
+    assert set(dict(insts.lint_list('*')).keys()) \
d69b2b
+        == {f'{m}:{s}' for m in ['ma', 'mb'] for s in inst_lints}
d69b2b
+    assert set(dict(insts.lint_list('*:nsstate')).keys()) \
d69b2b
+        == {f'{m}:nsstate:suffix{s}' for m in ['ma', 'mb'] for s in "AB"}
d69b2b
+    assert set(dict(insts.lint_list('mb:nsstate')).keys()) \
d69b2b
+        == {f'mb:nsstate:suffix{s}' for s in "AB"}
d69b2b
-- 
d69b2b
2.26.2
d69b2b