certbot/certbot-ci/certbot_integration_tests/utils/dns_server.py
Brad Warren 315ddb247f
Upgrade pylint (#8855)
This is part of https://github.com/certbot/certbot/issues/8782. I took it on now because the currently pinned version of `pylint` doesn't work with newer versions of `poetry` which I wanted to upgrade as part of https://github.com/certbot/certbot/issues/8787.

To say a bit more about the specific changes in this PR:

* Newer versions of `pylint` complain if `Popen` isn't used as a context manager. Instead of making this change, I switched to using `subprocess.run` which is simpler and [recommended in the Python docs](https://docs.python.org/3/library/subprocess.html#using-the-subprocess-module). I also disabled this check in a few places where no longer using `Popen` would require significant refactoring.
* The deleted code in `certbot/certbot/_internal/renewal.py` is cruft since https://github.com/certbot/certbot/pull/8685.
* The unused argument to `enable_mod` in the Apache plugin is used in some over the override classes that subclass that class.

* unpin pylint and repin dependencies

* disable raise-missing-from

* disable wrong-input-order

* remove unused code

* misc lint fixes

* remove unused import

* various lint fixes
2021-05-24 10:02:55 -07:00

157 lines
5.3 KiB
Python

#!/usr/bin/env python
"""Module to setup an RFC2136-capable DNS server"""
import os
import os.path
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from typing import Optional
from pkg_resources import resource_filename
BIND_DOCKER_IMAGE = "internetsystemsconsortium/bind9:9.16"
BIND_BIND_ADDRESS = ("127.0.0.1", 45953)
# A TCP DNS message which is a query for '. CH A' transaction ID 0xcb37. This is used
# by _wait_until_ready to check that BIND is responding without depending on dnspython.
BIND_TEST_QUERY = bytearray.fromhex("0011cb37000000010000000000000000010003")
class DNSServer:
"""
DNSServer configures and handles the lifetime of an RFC2136-capable server.
DNServer provides access to the dns_xdist parameter, listing the address and port
to use for each pytest node.
At this time, DNSServer should only be used with a single node, but may be expanded in
future to support parallelization (https://github.com/certbot/certbot/issues/8455).
"""
def __init__(self, unused_nodes, show_output=False):
"""
Create an DNSServer instance.
:param list nodes: list of node names that will be setup by pytest xdist
:param bool show_output: if True, print the output of the DNS server
"""
self.bind_root = tempfile.mkdtemp()
self.process: Optional[subprocess.Popen] = None
self.dns_xdist = {"address": BIND_BIND_ADDRESS[0], "port": BIND_BIND_ADDRESS[1]}
# Unfortunately the BIND9 image forces everything to stderr with -g and we can't
# modify the verbosity.
self._output = sys.stderr if show_output else open(os.devnull, "w")
def start(self):
"""Start the DNS server"""
try:
self._configure_bind()
self._start_bind()
except:
self.stop()
raise
def stop(self):
"""Stop the DNS server, and clean its resources"""
if self.process:
try:
self.process.terminate()
self.process.wait()
except BaseException as e:
print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr)
shutil.rmtree(self.bind_root, ignore_errors=True)
if self._output != sys.stderr:
self._output.close()
def _configure_bind(self):
"""Configure the BIND9 server based on the prebaked configuration"""
bind_conf_src = resource_filename(
"certbot_integration_tests", "assets/bind-config"
)
for directory in ("conf", "zones"):
shutil.copytree(
os.path.join(bind_conf_src, directory), os.path.join(self.bind_root, directory)
)
def _start_bind(self):
"""Launch the BIND9 server as a Docker container"""
addr_str = "{}:{}".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1])
# pylint: disable=consider-using-with
self.process = subprocess.Popen(
[
"docker",
"run",
"--rm",
"-p",
"{}:53/udp".format(addr_str),
"-p",
"{}:53/tcp".format(addr_str),
"-v",
"{}/conf:/etc/bind".format(self.bind_root),
"-v",
"{}/zones:/var/lib/bind".format(self.bind_root),
BIND_DOCKER_IMAGE,
],
stdout=self._output,
stderr=self._output,
)
if self.process.poll():
raise ValueError("BIND9 server stopped unexpectedly")
try:
self._wait_until_ready()
except:
# The container might be running even if we think it isn't
self.stop()
raise
def _wait_until_ready(self, attempts: int = 30) -> None:
"""
Polls the DNS server over TCP until it gets a response, or until
it runs out of attempts and raises a ValueError.
The DNS response message must match the txn_id of the DNS query message,
but otherwise the contents are ignored.
:param int attempts: The number of attempts to make.
"""
if not self.process:
raise ValueError("DNS server has not been started. Please run start() first.")
for _ in range(attempts):
if self.process.poll():
raise ValueError("BIND9 server stopped unexpectedly")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
sock.connect(BIND_BIND_ADDRESS)
sock.sendall(BIND_TEST_QUERY)
buf = sock.recv(1024)
# We should receive a DNS message with the same tx_id
if buf and len(buf) > 4 and buf[2:4] == BIND_TEST_QUERY[2:4]:
return
# If we got a response but it wasn't the one we wanted, wait a little
time.sleep(1)
except: # pylint: disable=bare-except
# If there was a network error, wait a little
time.sleep(1)
finally:
sock.close()
raise ValueError(
"Gave up waiting for DNS server {} to respond".format(BIND_BIND_ADDRESS)
)
def __enter__(self):
self.start()
return self.dns_xdist
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop()