diff --git a/cts/lab/CTSlab.py.in b/cts/lab/CTSlab.py.in
index 667c6fe167..31d3a63f6e 100644
--- a/cts/lab/CTSlab.py.in
+++ b/cts/lab/CTSlab.py.in
@@ -1,132 +1,131 @@
 #!@PYTHON@
 """ Command-line interface to Pacemaker's Cluster Test Suite (CTS)
 """
 
 __copyright__ = "Copyright 2001-2023 the Pacemaker project contributors"
 __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
 
 import sys, signal, os
 
 pdir = os.path.dirname(sys.path[0])
 sys.path.insert(0, pdir) # So that things work from the source directory
 
 try:
-    from cts.CM_corosync  import *
-
     from pacemaker._cts.CTS import CtsLab
+    from pacemaker._cts.cmcorosync import Corosync2
     from pacemaker._cts.audits import audit_list
     from pacemaker._cts.logging import LogFactory
     from pacemaker._cts.scenarios import AllOnce, Boot, BootCluster, LeaveBooted, RandomTests, Sequence
     from pacemaker._cts.tests import test_list
 except ImportError as e:
     sys.stderr.write("abort: %s\n" % e)
     sys.stderr.write("check your install and PYTHONPATH; couldn't find cts libraries in:\n%s\n" %
                      ' '.join(sys.path))
     sys.exit(1)
 
 # These are globals so they can be used by the signal handler.
 scenario = None
 LogFactory().add_stderr()
 
 
 def sig_handler(signum, frame) :
     LogFactory().log("Interrupted by signal %d"%signum)
     if scenario: scenario.summarize()
     if signum == 15 :
         if scenario: scenario.teardown()
         sys.exit(1)
 
 
 def plural_s(n, uppercase=False):
     if n == 1:
         return ""
     elif uppercase:
         return "S"
     else:
         return "s"
 
 
 if __name__ == '__main__':
 
     Environment = CtsLab(sys.argv[1:])
     NumIter = Environment["iterations"]
     Tests = []
 
     # Set the signal handler
     signal.signal(15, sig_handler)
     signal.signal(10, sig_handler)
 
     # Create the Cluster Manager object
     cm = None
     if Environment["Stack"] == "corosync 2+":
-        cm = crm_corosync()
+        cm = Corosync2()
         
     else:
         LogFactory().log("Unknown stack: "+Environment["stack"])
         sys.exit(1)
 
     if Environment["TruncateLog"]:
         if Environment["OutputFile"] is None:
             LogFactory().log("Ignoring truncate request because no output file specified")
         else:
             LogFactory().log("Truncating %s" % Environment["OutputFile"])
             with open(Environment["OutputFile"], "w") as outputfile:
                 outputfile.truncate(0)
 
     Audits = audit_list(cm)
 
     if Environment["ListTests"]:
         Tests = test_list(cm, Audits)
         LogFactory().log("Total %d tests"%len(Tests))
         for test in Tests :
             LogFactory().log(str(test.name));
         sys.exit(0)
 
     elif len(Environment["tests"]) == 0:
         Tests = test_list(cm, Audits)
 
     else:
         Chosen = Environment["tests"]
         for TestCase in Chosen:
            match = None
 
            for test in test_list(cm, Audits):
                if test.name == TestCase:
                    match = test
 
            if not match:
                LogFactory().log("--choose: No applicable/valid tests chosen")
                sys.exit(1)
            else:
                Tests.append(match)
 
     # Scenario selection
     if Environment["scenario"] == "all-once":
         NumIter = len(Tests)
         scenario = AllOnce(
             cm, [ BootCluster(cm, Environment) ], Audits, Tests)
     elif Environment["scenario"] == "sequence":
         scenario = Sequence(
             cm, [ BootCluster(cm, Environment) ], Audits, Tests)
     elif Environment["scenario"] == "boot":
         scenario = Boot(cm, [ LeaveBooted(cm, Environment)], Audits, [])
     else:
         scenario = RandomTests(
             cm, [ BootCluster(cm, Environment) ], Audits, Tests)
 
     LogFactory().log(">>>>>>>>>>>>>>>> BEGINNING " + repr(NumIter) + " TEST" + plural_s(NumIter, True) + " ")
     LogFactory().log("Stack:                  %s (%s)" % (Environment["Stack"], Environment["Name"]))
     LogFactory().log("Schema:                 %s" % Environment["Schema"])
     LogFactory().log("Scenario:               %s" % scenario.__doc__)
     LogFactory().log("CTS Exerciser:          %s" % Environment["cts-exerciser"])
     LogFactory().log("CTS Logfile:            %s" % Environment["OutputFile"])
     LogFactory().log("Random Seed:            %s" % Environment["RandSeed"])
     LogFactory().log("Syslog variant:         %s" % Environment["syslogd"].strip())
     LogFactory().log("System log files:       %s" % Environment["LogFileName"])
     if "IPBase" in Environment:
         LogFactory().log("Base IP for resources:  %s" % Environment["IPBase"])
     LogFactory().log("Cluster starts at boot: %d" % Environment["at-boot"])
 
     Environment.dump()
     rc = Environment.run(scenario, NumIter)
     sys.exit(rc)
diff --git a/cts/lab/Makefile.am b/cts/lab/Makefile.am
index 88c771a45a..cb7ef99415 100644
--- a/cts/lab/Makefile.am
+++ b/cts/lab/Makefile.am
@@ -1,25 +1,21 @@
 #
 # Copyright 2001-2023 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 MAINTAINERCLEANFILES    = Makefile.in
 
 noinst_SCRIPTS		= cluster_test		\
 			  OCFIPraTest.py
 
 # Commands intended to be run only via other commands
 halibdir		= $(CRM_DAEMON_DIR)
 dist_halib_SCRIPTS	= cts-log-watcher
 
-ctslibdir		= $(pythondir)/cts
-ctslib_PYTHON		= __init__.py		\
-			  CM_corosync.py
-
 ctsdir			= $(datadir)/$(PACKAGE)/tests/cts
 cts_SCRIPTS		= CTSlab.py		\
 			  cts
diff --git a/cts/lab/__init__.py b/cts/lab/__init__.py
deleted file mode 100644
index ba6a429ea2..0000000000
--- a/cts/lab/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""Python modules for Pacemaker's Cluster Test Suite (CTS)
-
-This package provides the following modules:
-
-CIB
-cib_xml
-CM_common
-CM_corosync
-CTS
-CTSscenarios
-CTStests
-patterns
-watcher
-"""
diff --git a/python/pacemaker/_cts/Makefile.am b/python/pacemaker/_cts/Makefile.am
index f20726874a..d1d12cebef 100644
--- a/python/pacemaker/_cts/Makefile.am
+++ b/python/pacemaker/_cts/Makefile.am
@@ -1,34 +1,35 @@
 #
 # Copyright 2023 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 MAINTAINERCLEANFILES    = Makefile.in
 
 pkgpythondir = $(pythondir)/$(PACKAGE)/_cts
 
 pkgpython_PYTHON = CTS.py \
 				   __init__.py \
 				   audits.py \
 				   cib.py \
 				   cibxml.py \
 				   clustermanager.py \
+				   cmcorosync.py \
 				   corosync.py \
 				   environment.py \
 				   errors.py \
 				   input.py \
 				   logging.py \
 				   network.py \
 				   patterns.py \
 				   process.py \
 				   remote.py \
 				   scenarios.py \
 				   test.py \
 				   timer.py \
 				   watcher.py
 
 SUBDIRS = tests
diff --git a/cts/lab/CM_corosync.py b/python/pacemaker/_cts/cmcorosync.py
similarity index 97%
rename from cts/lab/CM_corosync.py
rename to python/pacemaker/_cts/cmcorosync.py
index 9a79e46d0a..c609ddeac8 100644
--- a/cts/lab/CM_corosync.py
+++ b/python/pacemaker/_cts/cmcorosync.py
@@ -1,60 +1,61 @@
 """ Corosync-specific class for Pacemaker's Cluster Test Suite (CTS)
 """
 
+__all__ = ["Corosync2"]
 __copyright__ = "Copyright 2007-2023 the Pacemaker project contributors"
 __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
 
 from pacemaker._cts.CTS import Process
 from pacemaker._cts.clustermanager import ClusterManager
 from pacemaker._cts.patterns import PatternSelector
 
-class crm_corosync(ClusterManager):
+class Corosync2(ClusterManager):
     '''
     Corosync version 2 cluster manager class
     '''
     def __init__(self, name=None):
         if not name: name="crm-corosync"
         ClusterManager.__init__(self)
 
         self.fullcomplist = {}
         self.templates = PatternSelector(self.name)
 
     @property
     def components(self):
         complist = []
         if not len(list(self.fullcomplist.keys())):
             for c in [ "pacemaker-based", "pacemaker-controld", "pacemaker-attrd", "pacemaker-execd", "pacemaker-fenced" ]:
                 self.fullcomplist[c] = Process(
                     self, c, 
                     pats = self.templates.get_component(c),
                     badnews_ignore = self.templates.get_component("%s-ignore" % c) +
                                      self.templates.get_component("common-ignore"))
 
             # the scheduler uses dc_pats instead of pats
             self.fullcomplist["pacemaker-schedulerd"] = Process(
                 self, "pacemaker-schedulerd", 
                 dc_pats = self.templates.get_component("pacemaker-schedulerd"),
                 badnews_ignore = self.templates.get_component("pacemaker-schedulerd-ignore") +
                                  self.templates.get_component("common-ignore"))
 
             # add (or replace) extra components
             self.fullcomplist["corosync"] = Process(
                 self, "corosync", 
                 pats = self.templates.get_component("corosync"),
                 badnews_ignore = self.templates.get_component("corosync-ignore") +
                                  self.templates.get_component("common-ignore")
             )
 
         # Processes running under valgrind can't be shot with "killall -9 processname",
         # so don't include them in the returned list
         vgrind = self.env["valgrind-procs"].split()
         for key in list(self.fullcomplist.keys()):
             if self.env["valgrind-tests"]:
                 if key in vgrind:
                     self.log("Filtering %s from the component list as it is being profiled by valgrind" % key)
                     continue
             if key == "pacemaker-fenced" and not self.env["DoFencing"]:
                 continue
             complist.append(self.fullcomplist[key])
 
         return complist