2020-05-25 15:03:32 -03:00
|
|
|
#!/usr/bin/python3
|
2021-06-03 08:37:05 +02:00
|
|
|
|
2020-05-25 15:03:32 -03:00
|
|
|
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
|
|
|
#
|
2021-06-03 08:37:05 +02:00
|
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
|
|
#
|
2020-05-25 15:03:32 -03:00
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
2021-06-03 08:37:05 +02:00
|
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
2020-09-14 16:20:40 -07:00
|
|
|
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
2020-05-25 15:03:32 -03:00
|
|
|
#
|
|
|
|
# See the COPYRIGHT file distributed with this work for additional
|
|
|
|
# information regarding copyright ownership.
|
|
|
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
import os
|
|
|
|
import random
|
2021-04-12 18:00:07 +10:00
|
|
|
import signal
|
2020-05-25 15:03:32 -03:00
|
|
|
import subprocess
|
|
|
|
from string import ascii_lowercase as letters
|
|
|
|
import time
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
2023-04-04 21:31:58 +02:00
|
|
|
pytest.importorskip("dns", minversion="2.0.0")
|
2022-03-14 08:59:32 +01:00
|
|
|
import dns.exception
|
|
|
|
import dns.resolver
|
2022-03-14 08:59:32 +01:00
|
|
|
|
2020-05-25 15:03:32 -03:00
|
|
|
|
|
|
|
def do_work(named_proc, resolver, rndc_cmd, kill_method, n_workers, n_queries):
|
|
|
|
"""Creates a number of A queries to run in parallel
|
2022-06-07 16:27:23 +02:00
|
|
|
in order simulate a slightly more realistic test scenario.
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
The main idea of this function is to create and send a bunch
|
|
|
|
of A queries to a target named instance and during this process
|
|
|
|
a request for shutting down named will be issued.
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
In the process of shutting down named, a couple control connections
|
|
|
|
are created (by launching rndc) to ensure that the crash was fixed.
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
if kill_method=="rndc" named will be asked to shutdown by
|
|
|
|
means of rndc stop.
|
|
|
|
if kill_method=="sigterm" named will be killed by SIGTERM on
|
2023-03-30 17:35:00 +02:00
|
|
|
POSIX systems.
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:param named_proc: named process instance
|
|
|
|
:type named_proc: subprocess.Popen
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:param resolver: target resolver
|
|
|
|
:type resolver: dns.resolver.Resolver
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:param rndc_cmd: rndc command with default arguments
|
|
|
|
:type rndc_cmd: list of strings, e.g. ["rndc", "-p", "23750"]
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:kill_method: "rndc" or "sigterm"
|
|
|
|
:type kill_method: str
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:param n_workers: Number of worker threads to create
|
|
|
|
:type n_workers: int
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
:param n_queries: Total number of queries to send
|
|
|
|
:type n_queries: int
|
2020-05-25 15:03:32 -03:00
|
|
|
"""
|
2022-06-07 16:27:23 +02:00
|
|
|
# pylint: disable-msg=too-many-arguments
|
|
|
|
# pylint: disable-msg=too-many-locals
|
2020-05-25 15:03:32 -03:00
|
|
|
|
|
|
|
# helper function, args must be a list or tuple with arguments to rndc.
|
|
|
|
def launch_rndc(args):
|
|
|
|
return subprocess.call(rndc_cmd + args, timeout=10)
|
|
|
|
|
|
|
|
# We're going to execute queries in parallel by means of a thread pool.
|
|
|
|
# dnspython functions block, so we need to circunvent that.
|
2021-05-18 10:53:17 +02:00
|
|
|
with ThreadPoolExecutor(n_workers + 1) as executor:
|
|
|
|
# Helper dict, where keys=Future objects and values are tags used
|
|
|
|
# to process results later.
|
|
|
|
futures = {}
|
|
|
|
|
|
|
|
# 50% of work will be A queries.
|
|
|
|
# 1 work will be rndc stop.
|
|
|
|
# Remaining work will be rndc status (so we test parallel control
|
|
|
|
# connections that were crashing named).
|
|
|
|
shutdown = True
|
|
|
|
for i in range(n_queries):
|
|
|
|
if i < (n_queries // 2):
|
|
|
|
# Half work will be standard A queries.
|
|
|
|
# Among those we split 50% queries relname='www',
|
|
|
|
# 50% queries relname=random characters
|
|
|
|
if random.randrange(2) == 1:
|
|
|
|
tag = "good"
|
|
|
|
relname = "www"
|
|
|
|
else:
|
|
|
|
tag = "bad"
|
|
|
|
length = random.randint(4, 10)
|
2022-06-07 16:27:23 +02:00
|
|
|
relname = "".join(
|
|
|
|
letters[random.randrange(len(letters))] for i in range(length)
|
|
|
|
)
|
2021-05-18 10:53:17 +02:00
|
|
|
|
|
|
|
qname = relname + ".test"
|
2023-04-06 14:33:27 +02:00
|
|
|
futures[executor.submit(resolver.resolve, qname, "A")] = tag
|
2021-05-18 10:53:17 +02:00
|
|
|
elif shutdown: # We attempt to stop named in the middle
|
|
|
|
shutdown = False
|
|
|
|
if kill_method == "rndc":
|
2022-06-07 16:27:23 +02:00
|
|
|
futures[executor.submit(launch_rndc, ["stop"])] = "stop"
|
2021-05-18 10:53:17 +02:00
|
|
|
else:
|
2022-06-07 16:27:23 +02:00
|
|
|
futures[executor.submit(named_proc.terminate)] = "kill"
|
2021-05-18 10:53:17 +02:00
|
|
|
else:
|
|
|
|
# We attempt to send couple rndc commands while named is
|
|
|
|
# being shutdown
|
2022-06-07 16:27:23 +02:00
|
|
|
futures[executor.submit(launch_rndc, ["status"])] = "status"
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2021-05-18 10:53:17 +02:00
|
|
|
ret_code = -1
|
|
|
|
for future in as_completed(futures):
|
|
|
|
try:
|
|
|
|
result = future.result()
|
|
|
|
# If tag is "stop", result is an instance of
|
|
|
|
# subprocess.CompletedProcess, then we check returncode
|
|
|
|
# attribute to know if rncd stop command finished successfully.
|
|
|
|
#
|
|
|
|
# if tag is "kill" then the main function will check if
|
|
|
|
# named process exited gracefully after SIGTERM signal.
|
|
|
|
if futures[future] == "stop":
|
|
|
|
ret_code = result
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2022-06-07 16:27:23 +02:00
|
|
|
except (
|
|
|
|
dns.resolver.NXDOMAIN,
|
|
|
|
dns.resolver.NoNameservers,
|
|
|
|
dns.exception.Timeout,
|
|
|
|
):
|
2021-05-18 10:53:17 +02:00
|
|
|
pass
|
2020-05-25 15:03:32 -03:00
|
|
|
|
2021-05-18 10:53:17 +02:00
|
|
|
if kill_method == "rndc":
|
|
|
|
assert ret_code == 0
|
2020-05-25 15:03:32 -03:00
|
|
|
|
|
|
|
|
2023-04-06 14:01:43 +02:00
|
|
|
def wait_for_named_loaded(resolver, retries=10):
|
|
|
|
for _ in range(retries):
|
|
|
|
try:
|
|
|
|
resolver.resolve("version.bind", "TXT", "CH")
|
|
|
|
return True
|
|
|
|
except (dns.resolver.NoNameservers, dns.exception.Timeout):
|
|
|
|
time.sleep(1)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def wait_for_proc_termination(proc, max_timeout=10):
|
|
|
|
for _ in range(max_timeout):
|
|
|
|
if proc.poll() is not None:
|
|
|
|
return True
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
proc.send_signal(signal.SIGABRT)
|
|
|
|
for _ in range(max_timeout):
|
|
|
|
if proc.poll() is not None:
|
|
|
|
return True
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2020-05-25 15:03:32 -03:00
|
|
|
def test_named_shutdown(named_port, control_port):
|
|
|
|
# pylint: disable-msg=too-many-locals
|
|
|
|
cfg_dir = os.path.join(os.getcwd(), "resolver")
|
|
|
|
assert os.path.isdir(cfg_dir)
|
|
|
|
|
|
|
|
cfg_file = os.path.join(cfg_dir, "named.conf")
|
|
|
|
assert os.path.isfile(cfg_file)
|
|
|
|
|
|
|
|
named = os.getenv("NAMED")
|
|
|
|
assert named is not None
|
|
|
|
|
|
|
|
rndc = os.getenv("RNDC")
|
|
|
|
assert rndc is not None
|
|
|
|
|
2020-07-21 12:12:59 +02:00
|
|
|
# rndc configuration resides in ../common/rndc.conf
|
|
|
|
rndc_cfg = os.path.join("..", "common", "rndc.conf")
|
2020-05-25 15:03:32 -03:00
|
|
|
assert os.path.isfile(rndc_cfg)
|
|
|
|
|
|
|
|
# rndc command with default arguments.
|
2022-06-07 16:27:23 +02:00
|
|
|
rndc_cmd = [rndc, "-c", rndc_cfg, "-p", str(control_port), "-s", "10.53.0.3"]
|
2020-05-25 15:03:32 -03:00
|
|
|
|
|
|
|
# We create a resolver instance that will be used to send queries.
|
|
|
|
resolver = dns.resolver.Resolver()
|
2022-06-07 16:27:23 +02:00
|
|
|
resolver.nameservers = ["10.53.0.3"]
|
2020-05-25 15:03:32 -03:00
|
|
|
resolver.port = named_port
|
|
|
|
|
|
|
|
# We test named shutting down using two methods:
|
|
|
|
# Method 1: using rndc ctop
|
|
|
|
# Method 2: killing with SIGTERM
|
|
|
|
# In both methods named should exit gracefully.
|
|
|
|
for kill_method in ("rndc", "sigterm"):
|
2021-05-18 10:53:17 +02:00
|
|
|
named_cmdline = [named, "-c", cfg_file, "-f"]
|
|
|
|
with subprocess.Popen(named_cmdline, cwd=cfg_dir) as named_proc:
|
2023-04-06 14:05:30 +02:00
|
|
|
try:
|
|
|
|
assert named_proc.poll() is None, "named isn't running"
|
|
|
|
assert wait_for_named_loaded(resolver)
|
|
|
|
do_work(
|
|
|
|
named_proc,
|
|
|
|
resolver,
|
|
|
|
rndc_cmd,
|
|
|
|
kill_method,
|
|
|
|
n_workers=12,
|
|
|
|
n_queries=16,
|
|
|
|
)
|
|
|
|
assert wait_for_proc_termination(named_proc)
|
|
|
|
assert named_proc.returncode == 0, "named crashed"
|
|
|
|
finally: # Ensure named is terminated in case of an exception
|
|
|
|
named_proc.kill()
|