summaryrefslogtreecommitdiffstats
path: root/cts/cts-attrd.in
blob: c9a219d326a34efe32d8c89ff46743b3d2a653e4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!@PYTHON@
""" Regression tests for Pacemaker's attribute daemon
"""

# pylint doesn't like the module name "cts-attrd" which is an invalid complaint for this file
# but probably something we want to continue warning about elsewhere
# pylint: disable=invalid-name
# pacemaker imports need to come after we modify sys.path, which pylint will complain about.
# pylint: disable=wrong-import-position

__copyright__ = "Copyright 2023 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"

import argparse
import os
import subprocess
import sys
import tempfile

# These imports allow running from a source checkout after running `make`.
# Note that while this doesn't necessarily mean it will successfully run tests,
# but being able to see --help output can be useful.
if os.path.exists("@abs_top_srcdir@/python"):
    sys.path.insert(0, "@abs_top_srcdir@/python")

# pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant
if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@":
    sys.path.insert(0, "@abs_top_builddir@/python")

from pacemaker.buildoptions import BuildOptions
from pacemaker.exitstatus import ExitStatus
from pacemaker._cts.corosync import Corosync
from pacemaker._cts.process import killall, exit_if_proc_running
from pacemaker._cts.test import Test, Tests

TEST_DIR = sys.path[0]

def update_path():
    """ Set the PATH environment variable appropriately for the tests """

    new_path = os.environ['PATH']
    if os.path.exists("%s/cts-attrd.in" % TEST_DIR):
        # pylint: disable=protected-access
        print("Running tests from the source tree: %s (%s)" % (BuildOptions._BUILD_DIR, TEST_DIR))
        # For pacemaker-attrd
        new_path = "%s/daemons/attrd:%s" % (BuildOptions._BUILD_DIR, new_path)

    else:
        print("Running tests from the install tree: %s (not %s)" % (BuildOptions.DAEMON_DIR, TEST_DIR))
        # For pacemaker-attrd
        new_path = "%s:%s" % (BuildOptions.DAEMON_DIR, new_path)

    print('Using PATH="%s"' % new_path)
    os.environ['PATH'] = new_path


class AttributeTest(Test):
    """ Executor for a single test """

    def __init__(self, name, description, **kwargs):
        Test.__init__(self, name, description, **kwargs)

        self._daemon_location = "pacemaker-attrd"
        self._enable_corosync = True

    def _kill_daemons(self):
        killall([self._daemon_location])

    def _start_daemons(self):
        if self.verbose:
            print("Starting %s" % self._daemon_location)

        cmd = [self._daemon_location, "-s", "-l", self.logpath]
        # pylint: disable=consider-using-with
        self._daemon_process = subprocess.Popen(cmd)


class AttributeTests(Tests):
    """ Collection of all attribute regression tests """

    def __init__(self, **kwargs):
        Tests.__init__(self, **kwargs)

        self._corosync = Corosync(self.verbose, self.logdir, "cts-attrd")

    def new_test(self, name, description):
        """ Create a named test """

        test = AttributeTest(name, description, verbose=self.verbose, logdir=self.logdir)
        self._tests.append(test)
        return test

    def setup_environment(self, use_corosync):
        """ Prepare the host before executing any tests """

        if use_corosync:
            self._corosync.start(kill_first=True)

    def cleanup_environment(self, use_corosync):
        """ Clean up the host after executing desired tests """

        if use_corosync:
            self._corosync.stop()

    def build_basic_tests(self):
        """ Add basic tests - setting, querying, updating, and deleting attributes """

        test = self.new_test("set_attr_1",
                             "Set and query an attribute")
        test.add_cmd("attrd_updater", "--name AAA -U 111 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  "name=\"AAA\" value=\"111\"")
        test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111",
                             regex=True)

        # Setting the delay on an attribute that doesn't exist fails, but the failure is
        # not passed back to attrd_updater.
        test = self.new_test("set_attr_2",
                             "Set an attribute's delay")
        test.add_cmd("attrd_updater", "--name AAA -Y -d 5 --output-as=xml")
        test.add_log_pattern(r"Processed update-delay request from client .*: Error \(Attribute AAA does not exist\)",
                             regex=True)

        test = self.new_test("set_attr_3",
                             "Set and query an attribute's delay and value")
        test.add_cmd("attrd_updater", "--name AAA -B 111 -d 5 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  "name=\"AAA\" value=\"111\"")
        test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111 \| from .* with 5s write delay",
                             regex=True)

        test = self.new_test("set_attr_4",
                             "Update an attribute that does not exist with a delay")
        test.add_cmd("attrd_updater", "--name BBB -U 999 -d 10 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml",
                                  "name=\"BBB\" value=\"999\"")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 999 \| from .* with 10s write delay",
                             regex=True)

        test = self.new_test("update_attr_1",
                             "Update an attribute that already exists")
        test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml")
        test.add_cmd("attrd_updater", "--name BBB -U 333 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml",
                                  "name=\"BBB\" value=\"333\"")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222",
                             regex=True)
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 222 -> 333",
                             regex=True)

        test = self.new_test("update_attr_2",
                             "Update an attribute using a delay other than its default")
        test.add_cmd("attrd_updater", "--name BBB -U 777 -d 10 --output-as=xml")
        test.add_cmd("attrd_updater", "--name BBB -U 888 -d 7 --output-as=xml")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 777 -> 888 \| from .* with 10s write delay",
                             regex=True)

        test = self.new_test("update_attr_delay_1",
                             "Update the delay of an attribute that already exists")
        test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml")
        test.add_cmd("attrd_updater", "--name BBB -Y -d 5 --output-as=xml")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222",
                             regex=True)
        test.add_log_pattern("Update attribute BBB delay to 5000ms (5)")

        test = self.new_test("update_attr_delay_2",
                             "Update the delay and value of an attribute that already exists")
        test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml")
        test.add_cmd("attrd_updater", "--name BBB -B 333 -d 5 --output-as=xml")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222",
                             regex=True)
        test.add_log_pattern("Update attribute BBB delay to 5000ms (5)")
        test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 222 -> 333",
                             regex=True)

        test = self.new_test("missing_attr_1",
                             "Query an attribute that does not exist")
        test.add_cmd_expected_fail("attrd_updater", "--name NOSUCH --output-as=xml",
                                   ExitStatus.CONFIG)

        test = self.new_test("delete_attr_1",
                             "Delete an existing attribute")
        test.add_cmd("attrd_updater", "--name CCC -U 444 --output-as=xml")
        test.add_cmd("attrd_updater", "--name CCC -D --output-as=xml")
        test.add_log_pattern(r"Setting CCC\[.*\] in instance_attributes: \(unset\) -> 444",
                             regex=True)
        test.add_log_pattern(r"Setting CCC\[.*\] in instance_attributes: 444 -> \(unset\)",
                             regex=True)

        test = self.new_test("missing_attr_2",
                             "Delete an attribute that does not exist")
        test.add_cmd("attrd_updater", "--name NOSUCH2 -D --output-as=xml")

        test = self.new_test("attr_in_set_1",
                             "Set and query an attribute in a specific set")
        test.add_cmd("attrd_updater", "--name DDD -U 555 --set=foo --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name DDD -Q --output-as=xml",
                                  "name=\"DDD\" value=\"555\"")
        test.add_log_pattern("Processed 1 private change for DDD (set foo)")

    def build_multiple_query_tests(self):
        """ Add tests that set and query an attribute across multiple nodes """

        # NOTE:  These tests make use of the fact that nothing in attrd actually
        # cares about whether a node exists when you set or query an attribute.
        # It just keeps creating new hash tables for each node you ask it about.

        test = self.new_test("multi_query_1",
                             "Query an attribute set across multiple nodes")
        test.add_cmd("attrd_updater", "--name AAA -U 111 --node cluster1 --output-as=xml")
        test.add_cmd("attrd_updater", "--name AAA -U 222 --node cluster2 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -QA --output-as=xml",
                                  r"""<attribute name="AAA" value="111" host="cluster1"/>\n.*<attribute name="AAA" value="222" host="cluster2"/>""")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster1 --output-as=xml",
                                  """<attribute name="AAA" value="111" host="cluster1"/>""")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster2 --output-as=xml",
                                  """<attribute name="AAA" value="222" host="cluster2"/>""")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -QA --output-as=xml",
                                  r"""<attribute name="AAA" value="111" host="cluster1"/>\n.*<attribute name="AAA" value="222" host="cluster2"/>""",
                                  env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"})
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  """<attribute name="AAA" value="111" host="cluster1"/>""",
                                  env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"})
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster2 --output-as=xml",
                                  """<attribute name="AAA" value="222" host="cluster2"/>""",
                                  env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"})

    def build_regex_tests(self):
        """ Add tests that use regexes """

        test = self.new_test("regex_update_1",
                             "Update attributes using a regex")
        test.add_cmd("attrd_updater", "--name AAA -U 111 --output-as=xml")
        test.add_cmd("attrd_updater", "--name ABB -U 222 --output-as=xml")
        test.add_cmd("attrd_updater", "-P 'A.*' -U 333 --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  "name=\"AAA\" value=\"333\"")
        test.add_cmd_check_stdout("attrd_updater", "--name ABB -Q --output-as=xml",
                                  "name=\"ABB\" value=\"333\"")
        test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111",
                             regex=True)
        test.add_log_pattern(r"Setting ABB\[.*\] in instance_attributes: \(unset\) -> 222",
                             regex=True)
        test.add_log_pattern(r"Setting ABB\[.*\] in instance_attributes: 222 -> 333",
                             regex=True)
        test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: 111 -> 333",
                             regex=True)

        test = self.new_test("regex_delete_1",
                             "Delete attributes using a regex")
        test.add_cmd("attrd_updater", "--name XAX -U 444 --output-as=xml")
        test.add_cmd("attrd_updater", "--name XBX -U 555 --output-as=xml")
        test.add_cmd("attrd_updater", "-P 'X[A|B]X' -D --output-as=xml")
        test.add_log_pattern(r"Setting XAX\[.*\] in instance_attributes: \(unset\) -> 444",
                             regex=True)
        test.add_log_pattern(r"Setting XBX\[.*\] in instance_attributes: \(unset\) -> 555",
                             regex=True)
        test.add_log_pattern(r"Setting XBX\[.*\] in instance_attributes: 555 -> \(unset\)",
                             regex=True)
        test.add_log_pattern(r"Setting XAX\[.*\] in instance_attributes: 444 -> \(unset\)",
                             regex=True)

    def build_utilization_tests(self):
        """ Add tests that involve utilization attributes """

        test = self.new_test("utilization_1",
                             "Set and query a utilization attribute")
        test.add_cmd("attrd_updater", "--name AAA -U ABC -z --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  "name=\"AAA\" value=\"ABC\"")
        test.add_log_pattern(r"Setting AAA\[.*\] in utilization: \(unset\) -> ABC",
                             regex=True)

    def build_sync_point_tests(self):
        """ Add tests that involve sync points """

        test = self.new_test("local_sync_point",
                             "Wait for a local sync point")
        test.add_cmd("attrd_updater", "--name AAA -U 123 --wait=local --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml",
                                  "name=\"AAA\" value=\"123\"")
        test.add_log_pattern(r"Alerting client .* for reached local sync point",
                             regex=True)

        test = self.new_test("cluster_sync_point",
                             "Wait for a cluster-wide sync point")
        test.add_cmd("attrd_updater", "--name BBB -U 456 --wait=cluster --output-as=xml")
        test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml",
                                  "name=\"BBB\" value=\"456\"")
        test.add_log_pattern(r"Alerting client .* for reached cluster sync point",
                             regex=True)


def build_options():
    """ Handle command line arguments """

    parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
                                     description="Run pacemaker-attrd regression tests",
                                     epilog="Example: Run only the test 'start_stop'\n"
                                            "\t " + sys.argv[0] + " --run-only start_stop\n\n"
                                            "Example: Run only the tests with the string 'systemd' present in them\n"
                                            "\t " + sys.argv[0] + " --run-only-pattern systemd")
    parser.add_argument("-l", "--list-tests", action="store_true",
                        help="Print out all registered tests")
    parser.add_argument("-p", "--run-only-pattern", metavar='PATTERN',
                        help="Run only tests matching the given pattern")
    parser.add_argument("-r", "--run-only", metavar='TEST',
                        help="Run a specific test")
    parser.add_argument("-V", "--verbose", action="store_true",
                        help="Verbose output")

    args = parser.parse_args()
    return args


def main():
    """ Run attrd regression tests as specified by arguments """

    update_path()

    # Ensure all command output is in portable locale for comparison
    os.environ['LC_ALL'] = "C"

    opts = build_options()

    exit_if_proc_running("pacemaker-attrd")

    # Create a temporary directory for log files (the directory and its
    # contents will automatically be erased when done)
    with tempfile.TemporaryDirectory(prefix="cts-attrd-") as logdir:
        tests = AttributeTests(verbose=opts.verbose, logdir=logdir)

        tests.build_basic_tests()
        tests.build_multiple_query_tests()
        tests.build_regex_tests()
        tests.build_utilization_tests()
        tests.build_sync_point_tests()

        if opts.list_tests:
            tests.print_list()
            sys.exit(ExitStatus.OK)

        print("Starting ...")

        try:
            tests.setup_environment(True)
        except TimeoutError:
            print("corosync did not start in time, exiting")
            sys.exit(ExitStatus.TIMEOUT)

        if opts.run_only_pattern:
            tests.run_tests_matching(opts.run_only_pattern)
            tests.print_results()
        elif opts.run_only:
            tests.run_single(opts.run_only)
            tests.print_results()
        else:
            tests.run_tests()
            tests.print_results()

        tests.cleanup_environment(True)

    tests.exit()


if __name__ == "__main__":
    main()