from typing import Optional, Dict, List, Self, DefaultDict, TextIO, Tuple, TYPE_CHECKING
from nsconfig.util import flatten_list, parse_address, parse_network, parse_name, parse_duration
-from nsconfig.util import IPAddress, IPNetwork, IPAddr
+from nsconfig.util import IPAddress, IPNetwork, IPAddr, NameParseMode
if TYPE_CHECKING:
def __init__(self, nsc_zone: 'NscZonePrimary', name: str) -> None:
self.nsc_zone = nsc_zone
self.name = name
- self.node = nsc_zone.zone.find_node(name, create=True)
+ self.node = nsc_zone.zone.find_node(parse_name(name, NameParseMode.relative), create=True)
self._ttl = nsc_zone.config.default_ttl
def ttl(self, seconds: Optional[int] = None, **kwargs) -> Self:
rds = self.node.find_rdataset(rec.rdclass, rec.rdtype, create=True)
rds.add(rec, ttl=self._ttl)
+ def _parse_name(self, name, **kwargs):
+ return parse_name(name, mode=self.nsc_zone.config.name_parse_mode, **kwargs)
+
def A(self, *addrs: IPAddr, reverse: bool = True) -> Self:
for a in map(parse_address, flatten_list(addrs)):
if isinstance(a, IPv4Address):
else:
self._add(dns.rdtypes.IN.AAAA.AAAA(RdataClass.IN, RdataType.AAAA, str(a)))
if reverse:
- self.nsc_zone.nsc._add_reverse_mapping(a, parse_name(self.name, origin=self.nsc_zone.dns_name))
+ self.nsc_zone.nsc._add_reverse_mapping(a, parse_name(self.name, mode=NameParseMode.relative, origin=self.nsc_zone.dns_name))
return self
def CNAME(self, target: Name | str) -> Self:
def MX(self, pri: int, name: str) -> Self:
self._add(
- dns.rdtypes.ANY.MX.MX(RdataClass.IN, RdataType.MX, pri, parse_name(name))
+ dns.rdtypes.ANY.MX.MX(RdataClass.IN, RdataType.MX, pri, self._parse_name(name))
)
return self
return self
def NS(self, *names: str | List[str]) -> Self:
- for name in map(parse_name, flatten_list(names)):
- self._add(dns.rdtypes.ANY.NS.NS(RdataClass.IN, RdataType.NS, name))
+ for name in flatten_list(names):
+ self._add(dns.rdtypes.ANY.NS.NS(RdataClass.IN, RdataType.NS, self._parse_name(name)))
return self
def PTR(self, target: Name | str) -> Self:
return self
def SRV(self, priority: int, weight: int, port: int, target: Name | str) -> Self:
- self._add(dns.rdtypes.IN.SRV.SRV(RdataClass.IN, RdataType.SRV, priority, weight, port, parse_name(target)))
+ self._add(dns.rdtypes.IN.SRV.SRV(RdataClass.IN, RdataType.SRV, priority, weight, port, self._parse_name(target)))
return self
def TXT(self, *text: str | List[str]) -> Self:
origin_server: str
daemon_options: List[str]
add_null_mx: bool
+ name_parse_mode: NameParseMode
default_config: Optional['NscZoneConfig'] = None
daemon_options: Optional[List[str]] = None,
add_daemon_options: Optional[List[str]] = None,
add_null_mx: Optional[bool] = None,
+ name_parse_mode: Optional[NameParseMode] = None,
inherit_config: Optional['NscZoneConfig'] = None,
) -> None:
if inherit_config is None:
self.origin_server = origin_server if origin_server is not None else inherit_config.origin_server
self.daemon_options = daemon_options if daemon_options is not None else inherit_config.daemon_options
self.add_null_mx = add_null_mx if add_null_mx is not None else inherit_config.add_null_mx
+ self.name_parse_mode = name_parse_mode if name_parse_mode is not None else inherit_config.name_parse_mode
if add_daemon_options is not None:
self.daemon_options += add_daemon_options
origin_server="",
daemon_options=[],
add_null_mx=False,
+ name_parse_mode=NameParseMode.absolute,
)
dns_name: Name
safe_name: str # For use in file names
zone_type: ZoneType
+ config: NscZoneConfig
reverse_for: Optional[IPNetwork]
def __init__(self,
for i in range(start, start + num):
target = f'{i}.{subdomain}'
- self[str(i)].CNAME(parse_name(target, relative=True))
+ self[str(i)].CNAME(parse_name(target, mode=NameParseMode.relative))
return self[subdomain]
import dns.name
from dns.name import Name
+from enum import Enum, auto
from ipaddress import ip_address, IPv4Address, IPv6Address, ip_network, IPv4Network, IPv6Network
from datetime import timedelta
from typing import Any, List, Optional
IPAddr = str | IPAddress | List[str | IPAddress]
+class NameParseMode(Enum):
+ # How to parse DNS names (first matching rule wins):
+ # - names with no dots are always relative
+ # - names ending with ".@" are also relative
+ # - names ending with "." are always absolute
+ # - names on the left-hand side of records are relative
+ # - other names are interpreted according to the parsing mode
+ absolute = auto() # default
+ relative = auto()
+
+
def flatten_list(args: Any) -> List[Any]:
def flat(args):
if isinstance(args, list) or isinstance(args, tuple):
raise ValueError('Cannot parse IP network')
-def parse_name(name: str, relative: bool = False, origin: Optional[Name] = None) -> Name:
- # FIXME: Names with escaped dots
- if '.' in name and not relative and origin is None:
- return dns.name.from_text(name)
- else:
+def parse_name(name: str, mode: NameParseMode = NameParseMode.relative, origin: Optional[Name] = None) -> Name:
+ if name.endswith('.@'):
+ return dns.name.from_text(name[:-2], origin=origin)
+ if mode == NameParseMode.relative:
return dns.name.from_text(name, origin=origin)
+ elif mode == NameParseMode.absolute:
+ if '.' in name:
+ return dns.name.from_text(name)
+ else:
+ return dns.name.from_text(name, origin=origin)
+ else:
+ ...
def parse_duration(delta: timedelta | int) -> int: