mirror of
https://github.com/isc-projects/bind9.git
synced 2026-06-11 07:30:01 -04:00
System tests that set up zones — especially DNSSEC tests — require a
chain of common operations: rendering zone files from templates,
generating keys, signing, and propagating DS records to parent zones.
Implement these as methods on isctest.zone.Zone so individual tests
don't need to repeat the logic in shell or ad-hoc Python.
isctest.zone.Zone is a plain class that holds the zone's data and
accumulated state (delegations, keys) alongside the methods that operate
on it. It is intentionally separate from isctest.template.Zone, which
remains a dumb data container for jinja2 template rendering.
Key design points:
- zone.Zone.name is the text form without trailing dot ("." for root);
zone.Zone.dname holds the dns.name.Name for DNS-level operations;
zone.Zone.basename is the filesystem-safe name ("root" for ".").
- filepath_unsigned / filepath_signed are both always available.
filepath returns the appropriate one based on zone.Zone.signed.
- The zones/ subdirectory is the default (subdir="zones"); old-style
tests that place zone files directly in the ns workdir can pass
subdir=None.
- Signing is opt-in via signed=True; configure() auto-detects whether to
generate keys and sign based on this flag, so the same method handles
both signed and unsigned zones.
- delegations and keys are mutable list attributes; callers append to
them before calling configure() rather than threading them through
every call.
Also:
- Add isctest.template.zones() as a bridge from a list of zone.Zone to a
{name: template.Zone} dict suitable for use as the ``zones`` template
variable. template.zones() resolves filepath to the actual zone file
so templates don't need to know whether a zone is signed.
Assisted-by: Claude:claude-opus-4-8
182 lines
5.5 KiB
Python
182 lines
5.5 KiB
Python
#!/usr/bin/python3
|
|
|
|
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
|
#
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
#
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
#
|
|
# See the COPYRIGHT file distributed with this work for additional
|
|
# information regarding copyright ownership.
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from re import compile as Re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import re
|
|
|
|
import jinja2
|
|
|
|
from .log import debug
|
|
from .vars import ALL
|
|
|
|
if TYPE_CHECKING:
|
|
from .zone import Zone as _SetupZone
|
|
|
|
NS_DIR_RE = Re(r"^(a?ns([0-9]+))/")
|
|
|
|
|
|
class TemplateEngine:
|
|
"""
|
|
Engine for rendering jinja2 templates in system test directories.
|
|
"""
|
|
|
|
def __init__(self, directory: str | Path, env_vars=ALL):
|
|
"""
|
|
Initialize the template engine for `directory`, optionally overriding
|
|
the `env_vars` that will be used when rendering the templates (defaults
|
|
to the environment variables set by the pytest runner).
|
|
"""
|
|
self.directory = Path(directory)
|
|
self.env_vars = dict(env_vars)
|
|
self.j2env = jinja2.Environment(
|
|
loader=jinja2.ChoiceLoader(
|
|
[
|
|
jinja2.FileSystemLoader(self.directory),
|
|
jinja2.PrefixLoader(
|
|
{
|
|
"_common": jinja2.FileSystemLoader(
|
|
Path(ALL["srcdir"]) / "_common"
|
|
),
|
|
}
|
|
),
|
|
]
|
|
),
|
|
undefined=jinja2.StrictUndefined,
|
|
variable_start_string="@",
|
|
variable_end_string="@",
|
|
trim_blocks=True,
|
|
keep_trailing_newline=True,
|
|
)
|
|
# allow instantiating the template dataclasses in jinja2 templates when
|
|
# using {% set %}
|
|
self.j2env.globals["Nameserver"] = Nameserver
|
|
self.j2env.globals["TrustAnchor"] = TrustAnchor
|
|
self.j2env.globals["Zone"] = Zone
|
|
|
|
def render(
|
|
self,
|
|
output: str,
|
|
data: dict[str, Any] | None = None,
|
|
template: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Render `output` file from jinja `template` and fill in the `data`. The
|
|
`template` defaults to *.j2.manual or *.j2 file. The environment
|
|
variables which the engine was initialized with are also filled in. In
|
|
case of a variable name clash, `data` has precedence.
|
|
"""
|
|
available = self.j2env.list_templates()
|
|
if template is None:
|
|
template = f"{output}.j2.manual"
|
|
if template not in available:
|
|
template = f"{output}.j2"
|
|
if template not in available:
|
|
raise RuntimeError(f'No jinja2 template found for "{output}"')
|
|
|
|
if data is None:
|
|
data = {**self.env_vars}
|
|
else:
|
|
data = {**self.env_vars, **data}
|
|
|
|
# directory-specific "ns" var
|
|
assert "ns" not in data, '"ns" variable is reserved for nameserver data'
|
|
match = NS_DIR_RE.search(output)
|
|
if match:
|
|
data["ns"] = Nameserver(match.group(1))
|
|
|
|
debug("rendering template `%s` to file `%s`", template, output)
|
|
stream = self.j2env.get_template(template).stream(data)
|
|
stream.dump(output, encoding="utf-8")
|
|
|
|
def render_auto(self, data: dict[str, Any] | None = None):
|
|
"""
|
|
Render all *.j2 templates with default (and optionally the provided)
|
|
values and write the output to files without the .j2 extensions.
|
|
"""
|
|
templates = [
|
|
str(filepath.relative_to(self.directory))
|
|
for filepath in self.directory.rglob("*.j2")
|
|
]
|
|
for template in templates:
|
|
self.render(template[:-3], data)
|
|
|
|
|
|
@dataclass
|
|
class Nameserver:
|
|
|
|
name: str
|
|
num: int | None = None
|
|
ip: str | None = None
|
|
ip6: str | None = None
|
|
|
|
def __post_init__(self):
|
|
if self.num is None:
|
|
match = re.search(r"\d+", self.name)
|
|
assert match
|
|
self.num = int(match.group(0))
|
|
if self.ip is None:
|
|
self.ip = f"10.53.0.{self.num}"
|
|
if self.ip6 is None:
|
|
self.ip6 = f"fd92:7065:b8e:ffff::{self.num}"
|
|
|
|
|
|
NS1 = Nameserver("ns1")
|
|
NS2 = Nameserver("ns2")
|
|
NS3 = Nameserver("ns3")
|
|
NS4 = Nameserver("ns4")
|
|
NS5 = Nameserver("ns5")
|
|
NS6 = Nameserver("ns6")
|
|
NS7 = Nameserver("ns7")
|
|
NS8 = Nameserver("ns8")
|
|
NS9 = Nameserver("ns9")
|
|
NS10 = Nameserver("ns10")
|
|
NS11 = Nameserver("ns11")
|
|
|
|
|
|
@dataclass
|
|
class Zone:
|
|
|
|
name: str
|
|
ns: Nameserver
|
|
type: str = "primary"
|
|
filepath: Path | None = field(default=None)
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.filepath is None:
|
|
base = "root" if self.name == "." else self.name
|
|
self.filepath = Path(f"zones/{base}.db")
|
|
|
|
|
|
@dataclass
|
|
class TrustAnchor:
|
|
domain: str
|
|
type: str
|
|
contents: str
|
|
|
|
|
|
def zones(zone_list: "list[_SetupZone]") -> dict[str, Zone]:
|
|
"""
|
|
Convert a list of zone.Zone instances to a {name: Zone} dict for templates.
|
|
|
|
The returned dict maps zone names to plain template Zone instances, suitable
|
|
for use as the ``zones`` variable in jinja2 templates. The ``filepath`` of
|
|
each template zone is set to the actual zone file (signed or unsigned).
|
|
"""
|
|
return {
|
|
z.name: Zone(name=z.name, ns=z.ns, type=z.type, filepath=z.filepath)
|
|
for z in zone_list
|
|
}
|