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 14 from concurrent.futures import ThreadPoolExecutor, as_completed 15 import os 16 import random 17 import signal 18 import subprocess 19 from string import ascii_lowercase as letters 20 import time 21 22 import pytest 23 24 pytest.importorskip("dns", minversion="2.0.0") 25 import dns.exception 26 27 import isctest 28 29 pytestmark = pytest.mark.extra_artifacts( 30 [ 31 "resolver/named.conf", 32 "resolver/named.run", 33 ] 34 ) 35 36 37 def 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 ret = instance.rndc(command, raise_on_exception=False) 75 return 0 if ret.rc == 0 else -1 76 77 # We're going to execute queries in parallel by means of a thread pool. 78 # dnspython functions block, so we need to circumvent that. 79 with ThreadPoolExecutor(n_workers + 1) as executor: 80 # Helper dict, where keys=Future objects and values are tags used 81 # to process results later. 82 futures = {} 83 84 # 50% of work will be A queries. 85 # 1 work will be rndc stop. 86 # Remaining work will be rndc status (so we test parallel control 87 # connections that were crashing named). 88 shutdown = True 89 for i in range(n_queries): 90 if i < (n_queries // 2): 91 # Half work will be standard A queries. 92 # Among those we split 50% queries relname='www', 93 # 50% queries relname=random characters 94 if random.randrange(2) == 1: 95 tag = "good" 96 relname = "www" 97 else: 98 tag = "bad" 99 length = random.randint(4, 10) 100 relname = "".join( 101 letters[random.randrange(len(letters))] for i in range(length) 102 ) 103 104 qname = relname + ".test" 105 msg = isctest.query.create(qname, "A") 106 futures[ 107 executor.submit( 108 isctest.query.udp, msg, resolver_ip, timeout=1, attempts=1 109 ) 110 ] = tag 111 elif shutdown: # We attempt to stop named in the middle 112 shutdown = False 113 if kill_method == "rndc": 114 futures[executor.submit(launch_rndc, "stop")] = "stop" 115 else: 116 futures[executor.submit(named_proc.terminate)] = "kill" 117 else: 118 # We attempt to send couple rndc commands while named is 119 # being shutdown 120 futures[executor.submit(launch_rndc, "-t 5 status")] = "status" 121 122 ret_code = -1 123 for future in as_completed(futures): 124 try: 125 result = future.result() 126 # If tag is "stop", result is an instance of 127 # subprocess.CompletedProcess, then we check returncode 128 # attribute to know if rncd stop command finished successfully. 129 # 130 # if tag is "kill" then the main function will check if 131 # named process exited gracefully after SIGTERM signal. 132 if futures[future] == "stop": 133 ret_code = result 134 except dns.exception.Timeout: 135 pass 136 137 if kill_method == "rndc": 138 assert ret_code == 0 139 140 141 def wait_for_proc_termination(proc, max_timeout=10): 142 for _ in range(max_timeout): 143 if proc.poll() is not None: 144 return True 145 time.sleep(1) 146 147 proc.send_signal(signal.SIGABRT) 148 for _ in range(max_timeout): 149 if proc.poll() is not None: 150 return True 151 time.sleep(1) 152 153 return False 154 155 156 # We test named shutting down using two methods: 157 # Method 1: using rndc ctop 158 # Method 2: killing with SIGTERM 159 # In both methods named should exit gracefully. 160 @pytest.mark.parametrize( 161 "kill_method", 162 ["rndc", "sigterm"], 163 ) 164 def test_named_shutdown(kill_method): 165 resolver_ip = "10.53.0.3" 166 167 cfg_dir = "resolver" 168 169 named_cmdline = isctest.run.get_named_cmdline(cfg_dir) 170 instance = isctest.instance.NamedInstance("resolver", num=3) 171 172 with open(os.path.join(cfg_dir, "named.run"), "ab") as named_log: 173 with subprocess.Popen( 174 named_cmdline, cwd=cfg_dir, stderr=named_log 175 ) as named_proc: 176 try: 177 isctest.check.named_alive(named_proc, resolver_ip) 178 do_work( 179 named_proc, 180 resolver_ip, 181 instance, 182 kill_method, 183 n_workers=12, 184 n_queries=16, 185 ) 186 assert wait_for_proc_termination(named_proc) 187 assert named_proc.returncode == 0, "named crashed" 188 finally: # Ensure named is terminated in case of an exception 189 named_proc.kill() 190