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