xref: /netbsd-src/external/mpl/bind/dist/bin/tests/system/shutdown/tests_shutdown.py (revision 9689912e6b171cbda866ec33f15ae94a04e2c02d)
1#!/usr/bin/python3
2
3# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
4#
5# SPDX-License-Identifier: MPL-2.0
6#
7# This Source Code Form is subject to the terms of the Mozilla Public
8# License, v. 2.0.  If a copy of the MPL was not distributed with this
9# file, you can obtain one at https://mozilla.org/MPL/2.0/.
10#
11# See the COPYRIGHT file distributed with this work for additional
12# information regarding copyright ownership.
13
14from concurrent.futures import ThreadPoolExecutor, as_completed
15import os
16import random
17import signal
18import subprocess
19from string import ascii_lowercase as letters
20import time
21
22import pytest
23
24pytest.importorskip("dns", minversion="2.0.0")
25import dns.exception
26
27import isctest
28
29pytestmark = pytest.mark.extra_artifacts(
30    [
31        "resolver/named.conf",
32        "resolver/named.run",
33    ]
34)
35
36
37def do_work(named_proc, resolver_ip, instance, kill_method, n_workers, n_queries):
38    """Creates a number of A queries to run in parallel
39    in order simulate a slightly more realistic test scenario.
40
41    The main idea of this function is to create and send a bunch
42    of A queries to a target named instance and during this process
43    a request for shutting down named will be issued.
44
45    In the process of shutting down named, a couple control connections
46    are created (by launching rndc) to ensure that the crash was fixed.
47
48    if kill_method=="rndc" named will be asked to shutdown by
49    means of rndc stop.
50    if kill_method=="sigterm" named will be killed by SIGTERM on
51    POSIX systems.
52
53    :param named_proc: named process instance
54    :type named_proc: subprocess.Popen
55
56    :param resolver_ip: target resolver's IP address
57    :type resolver_ip: str
58
59    :param instance: the named instance to send RNDC commands to
60    :type instance: isctest.instance.NamedInstance
61
62    :kill_method: "rndc" or "sigterm"
63    :type kill_method: str
64
65    :param n_workers: Number of worker threads to create
66    :type n_workers: int
67
68    :param n_queries: Total number of queries to send
69    :type n_queries: int
70    """
71
72    # helper function, 'command' is the rndc command to run
73    def launch_rndc(command):
74        try:
75            instance.rndc(command, log=False)
76            return 0
77        except isctest.rndc.RNDCException:
78            return -1
79
80    # We're going to execute queries in parallel by means of a thread pool.
81    # dnspython functions block, so we need to circumvent that.
82    with ThreadPoolExecutor(n_workers + 1) as executor:
83        # Helper dict, where keys=Future objects and values are tags used
84        # to process results later.
85        futures = {}
86
87        # 50% of work will be A queries.
88        # 1 work will be rndc stop.
89        # Remaining work will be rndc status (so we test parallel control
90        # connections that were crashing named).
91        shutdown = True
92        for i in range(n_queries):
93            if i < (n_queries // 2):
94                # Half work will be standard A queries.
95                # Among those we split 50% queries relname='www',
96                # 50% queries relname=random characters
97                if random.randrange(2) == 1:
98                    tag = "good"
99                    relname = "www"
100                else:
101                    tag = "bad"
102                    length = random.randint(4, 10)
103                    relname = "".join(
104                        letters[random.randrange(len(letters))] for i in range(length)
105                    )
106
107                qname = relname + ".test"
108                msg = dns.message.make_query(qname, "A")
109                futures[
110                    executor.submit(
111                        isctest.query.udp, msg, resolver_ip, timeout=1, attempts=1
112                    )
113                ] = tag
114            elif shutdown:  # We attempt to stop named in the middle
115                shutdown = False
116                if kill_method == "rndc":
117                    futures[executor.submit(launch_rndc, "stop")] = "stop"
118                else:
119                    futures[executor.submit(named_proc.terminate)] = "kill"
120            else:
121                # We attempt to send couple rndc commands while named is
122                # being shutdown
123                futures[executor.submit(launch_rndc, "-t 5 status")] = "status"
124
125        ret_code = -1
126        for future in as_completed(futures):
127            try:
128                result = future.result()
129                # If tag is "stop", result is an instance of
130                # subprocess.CompletedProcess, then we check returncode
131                # attribute to know if rncd stop command finished successfully.
132                #
133                # if tag is "kill" then the main function will check if
134                # named process exited gracefully after SIGTERM signal.
135                if futures[future] == "stop":
136                    ret_code = result
137            except dns.exception.Timeout:
138                pass
139
140        if kill_method == "rndc":
141            assert ret_code == 0
142
143
144def wait_for_proc_termination(proc, max_timeout=10):
145    for _ in range(max_timeout):
146        if proc.poll() is not None:
147            return True
148        time.sleep(1)
149
150    proc.send_signal(signal.SIGABRT)
151    for _ in range(max_timeout):
152        if proc.poll() is not None:
153            return True
154        time.sleep(1)
155
156    return False
157
158
159# We test named shutting down using two methods:
160# Method 1: using rndc ctop
161# Method 2: killing with SIGTERM
162# In both methods named should exit gracefully.
163@pytest.mark.parametrize(
164    "kill_method",
165    ["rndc", "sigterm"],
166)
167def test_named_shutdown(kill_method):
168    resolver_ip = "10.53.0.3"
169
170    cfg_dir = "resolver"
171
172    named_cmdline = isctest.run.get_named_cmdline(cfg_dir)
173    instance = isctest.run.get_custom_named_instance("ns3")
174
175    with open(os.path.join(cfg_dir, "named.run"), "ab") as named_log:
176        with subprocess.Popen(
177            named_cmdline, cwd=cfg_dir, stderr=named_log
178        ) as named_proc:
179            try:
180                isctest.run.assert_custom_named_is_alive(named_proc, resolver_ip)
181                do_work(
182                    named_proc,
183                    resolver_ip,
184                    instance,
185                    kill_method,
186                    n_workers=12,
187                    n_queries=16,
188                )
189                assert wait_for_proc_termination(named_proc)
190                assert named_proc.returncode == 0, "named crashed"
191            finally:  # Ensure named is terminated in case of an exception
192                named_proc.kill()
193