Home | History | Annotate | Line # | Download | only in shutdown
      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