diff options
author | Aleš Mrázek <ales.mrazek@nic.cz> | 2023-06-09 14:26:57 +0200 |
---|---|---|
committer | Aleš Mrázek <ales.mrazek@nic.cz> | 2023-06-09 14:26:57 +0200 |
commit | 8f756fdde96076a113a11d1e0531011d21f0891e (patch) | |
tree | 9b5da2d133ee21c9fee329ec39da2033d90db8fe | |
parent | Merge branch 'manager-docs' into 'manager' (diff) | |
parent | manager: forward config example (diff) | |
download | knot-resolver-8f756fdde96076a113a11d1e0531011d21f0891e.tar.xz knot-resolver-8f756fdde96076a113a11d1e0531011d21f0891e.zip |
Merge branch 'manager-datamodel-improvements' into 'manager'
manager: datamodel improvements
See merge request knot/knot-resolver!1313
57 files changed, 1222 insertions, 231 deletions
diff --git a/manager/etc/knot-resolver/config.policy.dev.yml b/manager/etc/knot-resolver/config.policy.dev.yml new file mode 100644 index 00000000..b93fcceb --- /dev/null +++ b/manager/etc/knot-resolver/config.policy.dev.yml @@ -0,0 +1,71 @@ +rundir: runtime +workers: 1 +management: + interface: 127.0.0.1@5000 +cache: + storage: cache +logging: + level: notice + groups: + - manager + - supervisord +network: + listen: + - interface: 127.0.0.1@5353 + +views: + - subnets: [127.0.0.0/24] + tags: [t01, t02, t03] + options: + dns64: false + - subnets: [ 0.0.0.0/0, "::/0" ] + answer: refused + - subnets: [10.0.10.0/24] + answer: allow + +local-data: + ttl: 60m + nodata: false + records: | + example.net. TXT "foo bar" + A 192.168.2.3 + A 192.168.2.4 + local.example.org AAAA ::1 + subtrees: + - type: empty + tags: [ t2 ] + roots: [ example1.org ] + - type: nxdomain + roots: [ sub4.example.org ] + rpz: + - file: runtime/blocklist.rpz + tags: [t01, t02] + +# ttl: 1d +# nodata: true +# addresses: +# foo.bar: [ 127.0.0.1, "::1" ] +# my.pc.corp: 192.168.12.95 +# addresses-files: +# - /etc/hosts +# records: | +# example.net. TXT "foo bar" +# A 192.168.2.3 +# A 192.168.2.4 +# local.example.org AAAA ::1 + +forward: + - subtree: '.' + options: + dnssec: true + authoritative: false + servers: + - address: [2001:148f:fffe::1, 185.43.135.1] + transport: tls + hostname: odvr.nic.cz + - address: [ 192.0.2.1, 192.0.2.2 ] + pin-sha256: ['YQ==', 'Wg=='] + - subtree: 1.168.192.in-addr.arpa + options: + dnssec: false + servers: [ 192.0.2.1@5353 ] diff --git a/manager/knot_resolver_manager/datamodel/cache_schema.py b/manager/knot_resolver_manager/datamodel/cache_schema.py index 783b2c15..e40f1e23 100644 --- a/manager/knot_resolver_manager/datamodel/cache_schema.py +++ b/manager/knot_resolver_manager/datamodel/cache_schema.py @@ -1,6 +1,8 @@ -from typing import List, Optional +from typing import List, Optional, Union -from knot_resolver_manager.datamodel.types import Dir, DomainName, File, SizeUnit, TimeUnit +from typing_extensions import Literal + +from knot_resolver_manager.datamodel.types import Dir, DomainName, File, IntNonNegative, Percent, SizeUnit, TimeUnit from knot_resolver_manager.utils.modeling import ConfigSchema @@ -25,23 +27,50 @@ class PrefillSchema(ConfigSchema): raise ValueError("cache prefilling is not yet supported for non-root zones") +class GarbageCollectorSchema(ConfigSchema): + """ + Configuration options of the cache garbage collector (kres-cache-gc). + + --- + interval: Time interval how often the garbage collector will be run. + threshold: Cache usage in percent that triggers the garbage collector. + release: Percent of used cache to be freed by the garbage collector. + temp_keys_space: Maximum amount of temporary memory for copied keys (0 = unlimited). + rw_deletes: Maximum number of deleted records per read-write transaction (0 = unlimited). + rw_reads: Maximum number of readed records per read-write transaction (0 = unlimited). + rw_duration: Maximum duration of read-write transaction (0 = unlimited). + rw_delay: Wait time between two read-write transactions. + dry_run: Run the garbage collector in dry-run mode. + """ + + interval: TimeUnit = TimeUnit("1s") + threshold: Percent = Percent(80) + release: Percent = Percent(10) + temp_keys_space: SizeUnit = SizeUnit(0) + rw_deletes: IntNonNegative = IntNonNegative(100) + rw_reads: IntNonNegative = IntNonNegative(200) + rw_duration: TimeUnit = TimeUnit(0) + rw_delay: TimeUnit = TimeUnit(0) + dry_run: bool = False + + class CacheSchema(ConfigSchema): """ DNS resolver cache configuration. --- - garbage_collector: Automatically use garbage collector to periodically clear cache. storage: Cache storage of the DNS resolver. size_max: Maximum size of the cache. + garbage_collector: Use the garbage collector (kres-cache-gc) to periodically clear cache. ttl_min: Minimum time-to-live for the cache entries. ttl_max: Maximum time-to-live for the cache entries. ns_timeout: Time interval for which a nameserver address will be ignored after determining that it does not return (useful) answers. prefill: Prefill the cache periodically by importing zone data obtained over HTTP. """ - garbage_collector: bool = True storage: Dir = Dir("/var/cache/knot-resolver") size_max: SizeUnit = SizeUnit("100M") + garbage_collector: Union[GarbageCollectorSchema, Literal[False]] = GarbageCollectorSchema() ttl_min: TimeUnit = TimeUnit("5s") ttl_max: TimeUnit = TimeUnit("6d") ns_timeout: TimeUnit = TimeUnit("1000ms") diff --git a/manager/knot_resolver_manager/datamodel/config_schema.py b/manager/knot_resolver_manager/datamodel/config_schema.py index 9e1903a3..5df263c6 100644 --- a/manager/knot_resolver_manager/datamodel/config_schema.py +++ b/manager/knot_resolver_manager/datamodel/config_schema.py @@ -11,7 +11,8 @@ from knot_resolver_manager.constants import MAX_WORKERS from knot_resolver_manager.datamodel.cache_schema import CacheSchema from knot_resolver_manager.datamodel.dns64_schema import Dns64Schema from knot_resolver_manager.datamodel.dnssec_schema import DnssecSchema -from knot_resolver_manager.datamodel.forward_zone_schema import ForwardZoneSchema +from knot_resolver_manager.datamodel.forward_schema import ForwardSchema +from knot_resolver_manager.datamodel.local_data_schema import LocalDataSchema from knot_resolver_manager.datamodel.logging_schema import LoggingSchema from knot_resolver_manager.datamodel.lua_schema import LuaSchema from knot_resolver_manager.datamodel.management_schema import ManagementSchema @@ -19,10 +20,7 @@ from knot_resolver_manager.datamodel.monitoring_schema import MonitoringSchema from knot_resolver_manager.datamodel.network_schema import NetworkSchema from knot_resolver_manager.datamodel.options_schema import OptionsSchema from knot_resolver_manager.datamodel.policy_schema import PolicySchema -from knot_resolver_manager.datamodel.rpz_schema import RPZSchema from knot_resolver_manager.datamodel.slice_schema import SliceSchema -from knot_resolver_manager.datamodel.static_hints_schema import StaticHintsSchema -from knot_resolver_manager.datamodel.stub_zone_schema import StubZoneSchema from knot_resolver_manager.datamodel.types import IntPositive from knot_resolver_manager.datamodel.types.files import UncheckedPath from knot_resolver_manager.datamodel.view_schema import ViewSchema @@ -73,9 +71,9 @@ def _cpu_count() -> Optional[int]: return cpus -def _default_max_worker_count() -> Optional[int]: +def _default_max_worker_count() -> int: c = _cpu_count() - if c is not None: + if c: return c * 10 return MAX_WORKERS @@ -96,13 +94,11 @@ class KresConfig(ConfigSchema): webmgmt: Configuration of legacy web management endpoint. options: Fine-tuning global parameters of DNS resolver operation. network: Network connections and protocols configuration. - static_hints: Static hints for forward records (A/AAAA) and reverse records (PTR) views: List of views and its configuration. + local_data: Local data for forward records (A/AAAA) and reverse records (PTR). slices: Split the entire DNS namespace into distinct slices. policy: List of policy rules and its configuration. - rpz: List of Response Policy Zones and its configuration. - stub_zones: List of Stub Zones and its configuration. - forward_zones: List of Forward Zones and its configuration. + forward: List of Forward Zones and its configuration. cache: DNS resolver cache configuration. dnssec: Disable DNSSEC, enable with defaults or set new configuration. dns64: Disable DNS64 (RFC 6147), enable with defaults or set new configuration. @@ -121,13 +117,11 @@ class KresConfig(ConfigSchema): webmgmt: Optional[WebmgmtSchema] = None options: OptionsSchema = OptionsSchema() network: NetworkSchema = NetworkSchema() - static_hints: StaticHintsSchema = StaticHintsSchema() - views: Optional[Dict[str, ViewSchema]] = None + views: Optional[List[ViewSchema]] = None + local_data: LocalDataSchema = LocalDataSchema() slices: Optional[List[SliceSchema]] = None policy: Optional[List[PolicySchema]] = None - rpz: Optional[List[RPZSchema]] = None - stub_zones: Optional[List[StubZoneSchema]] = None - forward_zones: Optional[List[ForwardZoneSchema]] = None + forward: Optional[List[ForwardSchema]] = None cache: CacheSchema = CacheSchema() dnssec: Union[bool, DnssecSchema] = True dns64: Union[bool, Dns64Schema] = False @@ -146,13 +140,11 @@ class KresConfig(ConfigSchema): webmgmt: Optional[WebmgmtSchema] options: OptionsSchema network: NetworkSchema - static_hints: StaticHintsSchema - views: Optional[Dict[str, ViewSchema]] + views: Optional[List[ViewSchema]] + local_data: LocalDataSchema slices: Optional[List[SliceSchema]] policy: Optional[List[PolicySchema]] - rpz: Optional[List[RPZSchema]] - stub_zones: Optional[List[StubZoneSchema]] - forward_zones: Optional[List[ForwardZoneSchema]] + forward: Optional[List[ForwardSchema]] cache: CacheSchema dnssec: Union[Literal[False], DnssecSchema] dns64: Union[Literal[False], Dns64Schema] diff --git a/manager/knot_resolver_manager/datamodel/design-notes.yml b/manager/knot_resolver_manager/datamodel/design-notes.yml new file mode 100644 index 00000000..fb909acc --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/design-notes.yml @@ -0,0 +1,237 @@ +###### Working notes about configuration schema + + +## TODO nit: nest one level deeper inside `dnssec`, probably +dnssec: + keep-removed: 0 + refresh-time: 10s + hold-down-time: 30d + +## TODO nit: I don't like this name, at least not for the experimental thing we have there +network: + tls: + auto_discovery: boolean + +#### General questions +Plurals: do we name attributes in plural if they're a list; + some of them even allow a non-list if using a single element. + + +#### New-policy brainstorming + +dnssec: + # Convert to key: style instead of list? + # - easier to handle in API/CLI (which might be a common action on names with broken DNSSEC) + # - allows to supply a value - stamp for expiration of that NTA + # (absolute time, but I can imagine API/CLI converting from duration when executed) + # - syntax isn't really more difficult, mainly it forces one entry per line (seems OK) + negative-trust-anchors: + example.org: + my.example.net: + + +view: + # When a client request arrives, based on the `view` class of rules we may either + # decide for a direct answer or for marking the request with a set of tags. + # The concepts of matching and actions are a very good fit for this, + # and that matches our old policy approach. Matching here should avoid QNAME+QTYPE; + # instead it's e.g. suitable for access control. + # RPZ files also support rules that fall into this `view` class. + # + # Selecting a single rule: the most specific client-IP prefix + # that also matches additional conditions. + - subnet: [ 0.0.0.0/0, ::/0 ] + answer: refused + # some might prefer `allow: refused` ? + # Also, RCODEs are customary in CAPITALS though maybe not in configs. + + - subnet: [ 10.0.0.0/8, 192.168.0.0/16 ] + # Adding `tags` implies allowing the query. + tags: [ t1, t2, t3 ] # theoretically we could use space-separated string + options: # only some of the global options can be overridden in view + minimize: true + dns64: true + rate-limit: # future option, probably (optionally?) structured + # LATER: rulesets are a relatively unclear feature for now. + # Their main point is to allow prioritization and avoid + # intermixing rules that come from different sources. + # Also some properties might be specifyable per ruleset. + ruleset: tt + + - subnet: [ 10.0.10.0/24 ] # maybe allow a single value instead of a list? + # LATER: special addresses? + # - for kresd-internal requests + # - shorthands for all private IPv4 and/or IPv6; + # though yaml's repeated nodes could mostly cover that + # or just copy&paste from docs + answer: allow + +# Or perhaps a more complex approach? Probably not. +# We might have multiple conditions at once and multiple actions at once, +# but I don't expect these to be common, so the complication is probably not worth it. +# An advantage would be that the separation of the two parts would be more visible. +view: + - match: + subnet: [ 10.0.0.0/8, 192.168.0.0/16 ] + do: + tags: [ t1, t2, t3 ] + options: # ... + + +local-data: # TODO: name + #FIXME: tags - allow assigning them to (groups of) addresses/records. + + addresses: # automatically adds PTR records and NODATA (LATER: overridable NODATA?) + foo.bar: [ 127.0.0.1, ::1 ] + my.pc.corp: 192.168.12.95 + addresses-files: # files in /etc/hosts format (and semantics like `addresses`) + - /etc/hosts + + # Zonefile format seems quite handy here. Details: + # - probably use `local-data.ttl` from model as the default + # - and . root to avoid confusion if someone misses a final dot. + records: | + example.net. TXT "foo bar" + A 192.168.2.3 + A 192.168.2.4 + local.example.org AAAA ::1 + + subtrees: + nodata: true # impl ATM: defaults to false, set (only) for each rule/name separately + # impl: options like `ttl` and `nodata` might make sense to be settable (only?) per ruleset + + subtrees: # TODO: perhaps just allow in the -tagged style, if we can't avoid lists anyway? + - type: empty + roots: [ sub2.example.org ] # TODO: name it the same as for forwarding + tags: [ t2 ] + - type: nxdomain + # Will we need to support multiple file formats in future and choose here? + roots-file: /path/to/file.txt + - type: empty + roots-url: https://example.org/blocklist.txt + refresh: 1d + # Is it a separate ruleset? Optionally? Persistence? + # (probably the same questions for local files as well) + + - type: redirect + roots: [ sub4.example.org ] + addresses: [ 127.0.0.1, ::1 ] + +local-data-tagged: # TODO: name (view?); and even structure seems unclear. + # TODO: allow only one "type" per list entry? (addresses / addresses-files / subtrees / ...) + - tags: [ t1, t2 ] + addresses: #... otherwise the same as local-data + - tags: [ t2 ] + records: # ... + - tags: [ t3 ] + subtrees: empty + roots: [ sub2.example.org ] + +local-data-tagged: # this avoids lists, so it's relatively easy to amend through API + "t1 t2": # perhaps it's not nice that tags don't form a proper list? + addresses: + foo.bar: [ 127.0.0.1, ::1 ] + t4: + addresses: + foo.bar: [ 127.0.0.1, ::1 ] +local-data: # avoids lists and merges into the untagged `local-data` config subtree + tagged: # (getting quite deep, though) + t1 t2: + addresses: + foo.bar: [ 127.0.0.1, ::1 ] +# or even this ugly thing: +local-data-tagged t1 t2: + addresses: + foo.bar: [ 127.0.0.1, ::1 ] + +forward: # TODO: "name" is from Unbound, but @vcunat would prefer "subtree" or something. + - name: '.' # Root is the default so could be omitted? + servers: [2001:148f:fffe::1, 2001:148f:ffff::1, 185.43.135.1, 193.14.47.1] + # TLS forward, server authenticated using hostname and system-wide CA certificates + # https://knot-resolver.readthedocs.io/en/stable/modules-policy.html?highlight=forward#tls-examples + - name: '.' + servers: + - address: [ 192.0.2.1, 192.0.2.2@5353 ] + transport: tls + pin-sha256: Wg== + - address: 2001:DB8::d0c + transport: tls + hostname: res.example.com + ca-file: /etc/knot-resolver/tlsca.crt + options: + # LATER: allow a subset of options here, per sub-tree? + # Though that's not necessarily related to forwarding (e.g. TTL limits), + # especially implementation-wise it probably won't matter. + + +# Too confusing approach, I suppose? Different from usual way of thinking but closer to internal model. +# Down-sides: +# - multiple rules for the same name won't be possible (future, with different tags) +# - loading names from a file won't be possible (or URL, etc.) +rules: + example.org: &fwd_odvr + type: forward + servers: [2001:148f:fffe::1, 2001:148f:ffff::1, 185.43.135.1, 193.14.47.1] + sub2.example.org: + type: empty + tags: [ t3, t5 ] + sub3.example.org: + type: forward-auth + dnssec: no + + +# @amrazek: current valid config + +views: + - subnets: [ 0.0.0.0/0, "::/0" ] + answer: refused + - subnets: [ 0.0.0.0/0, "::/0" ] + tags: [t01, t02, t03] + options: + minimize: true # default + dns64: true # default + - subnets: 10.0.10.0/24 # can be single value + answer: allow + +local-data: + ttl: 1d + nodata: true + addresses: + foo.bar: [ 127.0.0.1, "::1" ] + my.pc.corp: 192.168.12.95 + addresses-files: + - /etc/hosts + records: | + example.net. TXT "foo bar" + A 192.168.2.3 + A 192.168.2.4 + local.example.org AAAA ::1 + subtrees: + - type: empty + roots: [ sub2.example.org ] + tags: [ t2 ] + - type: nxdomain + roots-file: /path/to/file.txt + - type: empty + roots-url: https://example.org/blocklist.txt + refresh: 1d + - type: redirect + roots: [ sub4.example.org ] + addresses: [ 127.0.0.1, "::1" ] + +forward: + - subtree: '.' + servers: + - address: [ 192.0.2.1, 192.0.2.2@5353 ] + transport: tls + pin-sha256: Wg== + - address: 2001:DB8::d0c + transport: tls + hostname: res.example.com + ca-file: /etc/knot-resolver/tlsca.crt + options: + dnssec: true # default + - subtree: 1.168.192.in-addr.arpa + servers: [ 192.0.2.1@5353 ] + options: + dnssec: false # policy.STUB?
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/forward_schema.py b/manager/knot_resolver_manager/datamodel/forward_schema.py new file mode 100644 index 00000000..df30229d --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/forward_schema.py @@ -0,0 +1,58 @@ +from typing import List, Optional, Union + +from typing_extensions import Literal + +from knot_resolver_manager.datamodel.types import DomainName, IPAddressOptionalPort, ListOrItem +from knot_resolver_manager.datamodel.types.files import FilePath +from knot_resolver_manager.utils.modeling import ConfigSchema + + +class ForwardServerSchema(ConfigSchema): + """ + Forward server configuration options. + + --- + address: IP address(es) of a forward server. + transport: Transport protocol for a forward server. + pin_sha256: Hash of accepted CA certificate. + hostname: Hostname of the Forward server. + ca_file: Path to CA certificate file. + """ + + address: ListOrItem[IPAddressOptionalPort] + transport: Optional[Literal["tls"]] = None + pin_sha256: Optional[ListOrItem[str]] = None + hostname: Optional[DomainName] = None + ca_file: Optional[FilePath] = None + + def _validate(self) -> None: + if self.pin_sha256 and (self.hostname or self.ca_file): + ValueError("'pin-sha256' cannot be configurad together with 'hostname' or 'ca-file'") + + +class ForwardOptionsSchema(ConfigSchema): + """ + Configuration options for forward subtree. + + --- + authoritative: The forwarding target is an authoritative server. + dnssec: Enable/disable DNSSEC. + """ + + authoritative: bool = False + dnssec: bool = True + + +class ForwardSchema(ConfigSchema): + """ + Configuration of forward subtree. + + --- + subtree: Subtree to forward. + servers: Forward server configuration. + options: Configuration options for forward subtree. + """ + + subtree: DomainName + servers: Union[List[IPAddressOptionalPort], List[ForwardServerSchema]] + options: ForwardOptionsSchema = ForwardOptionsSchema() diff --git a/manager/knot_resolver_manager/datamodel/forward_zone_schema.py b/manager/knot_resolver_manager/datamodel/forward_zone_schema.py deleted file mode 100644 index 8b7973b6..00000000 --- a/manager/knot_resolver_manager/datamodel/forward_zone_schema.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List, Optional, Union - -from knot_resolver_manager.datamodel.policy_schema import ForwardServerSchema -from knot_resolver_manager.datamodel.types import DomainName, IPAddressOptionalPort, PolicyFlagEnum -from knot_resolver_manager.utils.modeling import ConfigSchema - - -class ForwardZoneSchema(ConfigSchema): - """ - Configuration of Forward Zone. - - --- - name: Domain name of the zone. - tls: Enable/disable TLS for Forward servers. - servers: IP address of Forward server. - views: Use this Forward Zone only for clients defined by views. - options: Configuration flags for Forward Zone. - """ - - name: DomainName - tls: bool = False - servers: Union[List[IPAddressOptionalPort], List[ForwardServerSchema]] - views: Optional[List[str]] = None - options: Optional[List[PolicyFlagEnum]] = None diff --git a/manager/knot_resolver_manager/datamodel/local_data_schema.py b/manager/knot_resolver_manager/datamodel/local_data_schema.py new file mode 100644 index 00000000..9461362f --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/local_data_schema.py @@ -0,0 +1,79 @@ +from typing import Dict, List, Optional + +from typing_extensions import Literal + +from knot_resolver_manager.datamodel.types import DomainName, IDPattern, IPAddress, TimeUnit +from knot_resolver_manager.datamodel.types.files import FilePath, UncheckedPath +from knot_resolver_manager.utils.modeling import ConfigSchema + + +class SubtreeSchema(ConfigSchema): + """ + Local data and configuration of subtree. + + --- + type: Type of the subtree. + tags: Tags to link with other policy rules. + ttl: Default TTL value used for added local subtree. + nodata: Use NODATA synthesis. NODATA will be synthesised for matching name, but mismatching type(e.g. AAAA query when only A exists). + addresses: Subtree addresses. + roots: Subtree roots. + roots_file: Subtree roots from given file. + roots_url: Subtree roots form given URL. + refresh: Refresh time to update data from 'roots-file' or 'roots-url'. + """ + + type: Literal["empty", "nxdomain", "redirect"] + tags: Optional[List[IDPattern]] = None + ttl: Optional[TimeUnit] = None + nodata: bool = True + addresses: Optional[List[IPAddress]] = None + roots: Optional[List[DomainName]] = None + roots_file: Optional[UncheckedPath] = None + roots_url: Optional[str] = None + refresh: Optional[TimeUnit] = None + + def _validate(self) -> None: + options_sum = sum([bool(self.roots), bool(self.roots_file), bool(self.roots_url)]) + if options_sum > 1: + raise ValueError("only one of, 'roots', 'roots-file' or 'roots-url' can be configured") + elif options_sum < 1: + raise ValueError("one of, 'roots', 'roots-file' or 'roots-url' must be configured") + if self.refresh and not (self.roots_file or self.roots_url): + raise ValueError("'refresh' can be only configured with 'roots-file' or 'roots-url'") + + +class RPZSchema(ConfigSchema): + """ + Configuration or Response Policy Zone (RPZ). + + --- + file: Path to the RPZ zone file. + tags: Tags to link with other policy rules. + """ + + file: FilePath + tags: Optional[List[IDPattern]] = None + + +class LocalDataSchema(ConfigSchema): + """ + Local data for forward records (A/AAAA) and reverse records (PTR). + + --- + ttl: Default TTL value used for added local data/records. + nodata: Use NODATA synthesis. NODATA will be synthesised for matching name, but mismatching type(e.g. AAAA query when only A exists). + addresses: Direct addition of hostname and IP addresses pairs. + addresses_files: Direct addition of hostname and IP addresses pairs from files in '/etc/hosts' like format. + records: Direct addition of records in DNS zone file format. + subtrees: Direct addition of subtrees. + rpz: List of Response Policy Zones and its configuration. + """ + + ttl: Optional[TimeUnit] = None + nodata: bool = True + addresses: Optional[Dict[DomainName, List[IPAddress]]] = None + addresses_files: Optional[List[UncheckedPath]] = None + records: Optional[str] = None + subtrees: Optional[List[SubtreeSchema]] = None + rpz: Optional[List[RPZSchema]] = None diff --git a/manager/knot_resolver_manager/datamodel/logging_schema.py b/manager/knot_resolver_manager/datamodel/logging_schema.py index 1217db23..fb05b826 100644 --- a/manager/knot_resolver_manager/datamodel/logging_schema.py +++ b/manager/knot_resolver_manager/datamodel/logging_schema.py @@ -21,6 +21,7 @@ LogTargetEnum = Literal["syslog", "stderr", "stdout"] LogGroupsEnum: TypeAlias = Literal[ "manager", "supervisord", + "cache-gc", "system", "cache", "io", diff --git a/manager/knot_resolver_manager/datamodel/network_schema.py b/manager/knot_resolver_manager/datamodel/network_schema.py index 0a177e39..2349bdc5 100644 --- a/manager/knot_resolver_manager/datamodel/network_schema.py +++ b/manager/knot_resolver_manager/datamodel/network_schema.py @@ -12,6 +12,7 @@ from knot_resolver_manager.datamodel.types import ( IPNetwork, IPv4Address, IPv6Address, + ListOrItem, PortNumber, SizeUnit, ) @@ -84,24 +85,24 @@ class ListenSchema(ConfigSchema): freebind: Used for binding to non-local address. """ - interface: Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]] = None - unix_socket: Union[None, FilePath, List[FilePath]] = None + interface: Optional[ListOrItem[InterfaceOptionalPort]] = None + unix_socket: Optional[ListOrItem[FilePath]] = None port: Optional[PortNumber] = None kind: KindEnum = "dns" freebind: bool = False _LAYER = Raw - interface: Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]] - unix_socket: Union[None, FilePath, List[FilePath]] + interface: Optional[ListOrItem[InterfaceOptionalPort]] + unix_socket: Optional[ListOrItem[FilePath]] port: Optional[PortNumber] kind: KindEnum freebind: bool - def _interface(self, origin: Raw) -> Union[None, InterfaceOptionalPort, List[InterfaceOptionalPort]]: - if isinstance(origin.interface, list): + def _interface(self, origin: Raw) -> Optional[ListOrItem[InterfaceOptionalPort]]: + if origin.interface: port_set: Optional[bool] = None - for intrfc in origin.interface: + for intrfc in origin.interface: # type: ignore[attr-defined] if origin.port and intrfc.port: raise ValueError("The port number is defined in two places ('port' option and '@<port>' syntax).") if port_set is not None and (bool(intrfc.port) != port_set): @@ -109,8 +110,6 @@ class ListenSchema(ConfigSchema): "The '@<port>' syntax must be used either for all or none of the interface in the list." ) port_set = bool(intrfc.port) - elif isinstance(origin.interface, InterfaceOptionalPort) and origin.interface.port and origin.port: - raise ValueError("The port number is defined in two places ('port' option and '@<port>' syntax).") return origin.interface def _port(self, origin: Raw) -> Optional[PortNumber]: diff --git a/manager/knot_resolver_manager/datamodel/options_schema.py b/manager/knot_resolver_manager/datamodel/options_schema.py index cee709a2..e95e5f88 100644 --- a/manager/knot_resolver_manager/datamodel/options_schema.py +++ b/manager/knot_resolver_manager/datamodel/options_schema.py @@ -28,7 +28,7 @@ class OptionsSchema(ConfigSchema): --- glue_checking: Glue records scrictness checking level. - qname_minimisation: Send minimum amount of information in recursive queries to enhance privacy. + minimize: Send minimum amount of information in recursive queries to enhance privacy. query_loopback: Permits queries to loopback addresses. reorder_rrset: Controls whether resource records within a RRSet are reordered each time it is served from the cache. query_case_randomization: Randomize Query Character Case. @@ -42,7 +42,7 @@ class OptionsSchema(ConfigSchema): """ glue_checking: GlueCheckingEnum = "normal" - qname_minimisation: bool = True + minimize: bool = True query_loopback: bool = False reorder_rrset: bool = True query_case_randomization: bool = True @@ -57,7 +57,7 @@ class OptionsSchema(ConfigSchema): _LAYER = Raw glue_checking: GlueCheckingEnum - qname_minimisation: bool + minimize: bool query_loopback: bool reorder_rrset: bool query_case_randomization: bool diff --git a/manager/knot_resolver_manager/datamodel/policy_schema.py b/manager/knot_resolver_manager/datamodel/policy_schema.py index 3f5962ff..bbc61cd1 100644 --- a/manager/knot_resolver_manager/datamodel/policy_schema.py +++ b/manager/knot_resolver_manager/datamodel/policy_schema.py @@ -1,10 +1,9 @@ from typing import List, Optional, Union +from knot_resolver_manager.datamodel.forward_schema import ForwardServerSchema from knot_resolver_manager.datamodel.network_schema import AddressRenumberingSchema from knot_resolver_manager.datamodel.types import ( DNSRecordTypeEnum, - DomainName, - File, IPAddressOptionalPort, PolicyActionEnum, PolicyFlagEnum, @@ -45,23 +44,6 @@ class AnswerSchema(ConfigSchema): nodata: bool = False -class ForwardServerSchema(ConfigSchema): - """ - Configuration of Forward server. - - --- - address: IP address of Forward server. - pin_sha256: Hash of accepted CA certificate. - hostname: Hostname of the Forward server. - ca_file: Path to CA certificate file. - """ - - address: IPAddressOptionalPort - pin_sha256: Optional[Union[str, List[str]]] = None - hostname: Optional[DomainName] = None - ca_file: Optional[File] = None - - def _validate_policy_action(policy_action: Union["ActionSchema", "PolicySchema"]) -> None: servers = ["mirror", "forward", "stub"] diff --git a/manager/knot_resolver_manager/datamodel/stub_zone_schema.py b/manager/knot_resolver_manager/datamodel/stub_zone_schema.py index 76e4d82c..b9945ecc 100644 --- a/manager/knot_resolver_manager/datamodel/stub_zone_schema.py +++ b/manager/knot_resolver_manager/datamodel/stub_zone_schema.py @@ -20,13 +20,13 @@ class StubZoneSchema(ConfigSchema): Configuration of Stub Zone. --- - name: Domain name of the zone. + subtree: Domain name of the zone. servers: IP address of Stub server. views: Use this Stub Zone only for clients defined by views. options: Configuration flags for Stub Zone. """ - name: DomainName + subtree: DomainName servers: Union[List[IPAddressOptionalPort], List[StubServerSchema]] views: Optional[List[str]] = None options: Optional[List[PolicyFlagEnum]] = None diff --git a/manager/knot_resolver_manager/datamodel/templates/config.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/config.lua.j2 index 93a58ace..442354ab 100644 --- a/manager/knot_resolver_manager/datamodel/templates/config.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/config.lua.j2 @@ -1,5 +1,9 @@ {% if not cfg.lua.script_only %} +-- FFI library +ffi = require('ffi') +local C = ffi.C + -- hostname hostname('{{ cfg.hostname }}') @@ -12,6 +16,9 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id) -- LOGGING section ---------------------------------- {% include "logging.lua.j2" %} +-- MONITORING section ------------------------------- +{% include "monitoring.lua.j2" %} + -- WEBMGMT section ---------------------------------- {% include "webmgmt.lua.j2" %} @@ -21,26 +28,23 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id) -- NETWORK section ---------------------------------- {% include "network.lua.j2" %} --- STATIC-HINTS section ----------------------------- -{% include "static_hints.lua.j2" %} - -- VIEWS section ------------------------------------ {% include "views.lua.j2" %} +-- LOCAL-DATA section ------------------------------- +{% include "local_data.lua.j2" %} + -- SLICES section ----------------------------------- -{% include "slices.lua.j2" %} +{# {% include "slices.lua.j2" %} #} -- POLICY section ----------------------------------- -{% include "policy.lua.j2" %} +{# {% include "policy.lua.j2" %} #} -- RPZ section -------------------------------------- -{% include "rpz.lua.j2" %} - --- STUB-ZONES section ------------------------------- -{% include "stub_zones.lua.j2" %} +{# {% include "rpz.lua.j2" %} #} --- FORWARD-ZONES section ---------------------------- -{% include "forward_zones.lua.j2" %} +-- FORWARD section ---------------------------------- +{% include "forward.lua.j2" %} -- CACHE section ------------------------------------ {% include "cache.lua.j2" %} @@ -64,6 +68,3 @@ nsid.name('{{ cfg.nsid }}_' .. worker.id) {% if cfg.lua.script %} {{ cfg.lua.script }} {% endif %} - --- manager's monitoring configuration -{% include "monitoring.lua.j2" %}
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2 index b26961bb..31a29bea 100644 --- a/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/dnssec.lua.j2 @@ -1,3 +1,5 @@ +{% from 'macros/common_macros.lua.j2' import boolean %} + {% if not cfg.dnssec %} -- disable dnssec trust_anchors.remove('.') @@ -51,6 +53,6 @@ trust_anchors.set_insecure({ {% if cfg.dnssec.trust_anchors_files %} -- dnssec.trust-anchors-files {% for taf in cfg.dnssec.trust_anchors_files %} -trust_anchors.add_file('{{ taf.file }}', readonly = {{ 'true' if taf.read_only else 'false' }}) +trust_anchors.add_file('{{ taf.file }}', readonly = {{ boolean(taf.read_only) }}) {% endfor %} {% endif %}
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2 new file mode 100644 index 00000000..afb2c492 --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/templates/forward.lua.j2 @@ -0,0 +1,7 @@ +{% from 'macros/forward_macros.lua.j2' import policy_rule_forward_add %} + +{% if cfg.forward %} +{% for fwd in cfg.forward %} +{{ policy_rule_forward_add(fwd) }} +{% endfor %} +{% endif %} diff --git a/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2 index 3a1fb4ae..26e0a9e8 100644 --- a/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/forward_zones.lua.j2 @@ -3,7 +3,7 @@ {% if cfg.forward_zones %} {% for zone in cfg.forward_zones %} --- forward-zone: {{ zone.name }} +-- forward-zone: {{ zone.subtree }} {% if zone.views -%} {# views set for forward-zone #} {% for view_id in zone.views -%} @@ -24,13 +24,13 @@ {% for tsig in view.tsig %} {%- if options -%} -{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }} +{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% if zone.tls -%} -{{ view_tsig(tsig|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_tsig(tsig|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {% else %} -{{ view_tsig(tsig|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_tsig(tsig|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% endfor %} @@ -41,13 +41,13 @@ {% for addr in view.subnets %} {%- if options -%} -{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }} +{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% if zone.tls -%} -{{ view_addr(addr|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_addr(addr|string, policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {% else %} -{{ view_addr(addr|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_addr(addr|string, policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% endfor %} @@ -58,13 +58,13 @@ {# no views set for forward-zone #} {% if zone.options -%} -{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.name|string))) }} +{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% if zone.tls -%} -{{ policy_add(policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ policy_add(policy_suffix(policy_tls_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {% else %} -{{ policy_add(policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.name|string))) }} +{{ policy_add(policy_suffix(policy_forward(zone.servers|list), policy_todname(zone.subtree|string))) }} {%- endif %} {% endif %} diff --git a/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2 new file mode 100644 index 00000000..d7e2110f --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/templates/local_data.lua.j2 @@ -0,0 +1,30 @@ +{% from 'macros/local_data_macros.lua.j2' import local_data_subtree_root, local_data_records %} + +{# TODO: implemented all other options/features from local_data_schema #} + +{# records #} +{% if cfg.local_data.records -%} +{{ local_data_records(cfg.local_data.records, false, cfg.local_data.ttl, cfg.local_data.nodata) }} +{%- endif %} + +{# subtrees #} +{% if cfg.local_data.subtrees -%} +{% for subtree in cfg.local_data.subtrees %} +{% if subtree.roots -%} +{% for root in subtree.roots %} +{{ local_data_subtree_root(subtree.type, root, subtree.tags) }} +{% endfor %} +{%- elif subtree.roots_file -%} +{# TODO: not implemented yet #} +{%- elif subtree.roots_url -%} +{# TODO: not implemented yet #} +{%- endif %} +{% endfor %} +{%- endif %} + +{# rpz #} +{% if cfg.local_data.rpz -%} +{% for rpz in cfg.local_data.rpz %} +{{ local_data_records(rpz.file, true, cfg.local_data.ttl, cfg.local_data.nodata, rpz.tags) }} +{% endfor %} +{%- endif %} diff --git a/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2 index d7fb845d..2fb398e9 100644 --- a/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/logging.lua.j2 @@ -1,3 +1,5 @@ +{% from 'macros/common_macros.lua.j2' import boolean %} + -- logging.level log_level('{{ cfg.logging.level }}') @@ -27,15 +29,15 @@ modules.load('dnstap') dnstap.config({ socket_path = '{{ cfg.logging.dnstap.unix_socket }}', client = { - log_queries = {{ 'true' if cfg.logging.dnstap.log_queries else 'false' }}, - log_responses = {{ 'true' if cfg.logging.dnstap.log_responses else 'false' }}, - log_tcp_rtt = {{ 'true' if cfg.logging.dnstap.log_tcp_rtt else 'false' }} + log_queries = {{ boolean(cfg.logging.dnstap.log_queries) }}, + log_responses = {{ boolean(cfg.logging.dnstap.log_responses) }}, + log_tcp_rtt = {{ boolean(cfg.logging.dnstap.log_tcp_rtt) }} } }) {%- endif %} -- logging.debugging.assertion-abort -debugging.assertion_abort = {{ 'true' if cfg.logging.debugging.assertion_abort else 'false' }} +debugging.assertion_abort = {{ boolean(cfg.logging.debugging.assertion_abort) }} -- logging.debugging.assertion-fork debugging.assertion_fork = {{ cfg.logging.debugging.assertion_fork.millis() }} diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2 index 1084a9b7..4c2ba11a 100644 --- a/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2 @@ -1,3 +1,15 @@ +{% macro quotes(string) -%} +'{{ string }}' +{%- endmacro %} + +{% macro boolean(val, negation=false) -%} +{%- if negation -%} +{{ 'false' if val else 'true' }} +{%- else-%} +{{ 'true' if val else 'false' }} +{%- endif -%} +{%- endmacro %} + {# Return string or table of strings #} {% macro string_table(table) -%} {%- if table is string -%} diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2 new file mode 100644 index 00000000..f6777324 --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/templates/macros/forward_macros.lua.j2 @@ -0,0 +1,42 @@ +{% from 'macros/common_macros.lua.j2' import boolean, string_table %} + +{% macro forward_options(options) -%} +{dnssec={{ boolean(options.dnssec) }},auth={{ boolean(options.authoritative) }}} +{%- endmacro %} + +{% macro forward_server(server) -%} +{%- if server.address -%} +{%- for addr in server.address -%} +{'{{ addr }}', +{%- if server.transport == 'tls' -%} +tls=true, +{%- else -%} +tls=false, +{%- endif -%} +{%- if server.hostname -%} +hostname='{{ server.hostname }}', +{%- endif -%} +{%- if server.pin_sha256 -%} +pin_sha256={{ string_table(server.pin_sha256) }}, +{%- endif -%} +{%- if server.ca_file -%} +ca_file='{{ server.ca_file }}', +{%- endif -%} +}, +{%- endfor -%} +{% else %} +{'{{ server }}'}, +{%- endif -%} +{%- endmacro %} + +{% macro forward_servers(servers) -%} +{ +{%- for server in servers -%} +{{ forward_server(server) }} +{%- endfor -%} +} +{%- endmacro %} + +{% macro policy_rule_forward_add(forward) -%} +policy.rule_forward_add('{{ forward.subtree }}',{{ forward_options(forward.options) }},{{ forward_servers(forward.servers) }}) +{%- endmacro %} diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2 new file mode 100644 index 00000000..dde204e3 --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/templates/macros/local_data_macros.lua.j2 @@ -0,0 +1,44 @@ +{% from 'macros/common_macros.lua.j2' import string_table, boolean %} +{% from 'macros/policy_macros.lua.j2' import policy_get_tagset, policy_todname %} + +{% macro local_data_records(input_str, is_rpz, ttl, nodata, tags=none, id='rrs') -%} +{{ id }} = ffi.new('struct kr_rule_zonefile_config') +{% if ttl %} +{{ id }}.ttl = {{ ttl.millis() }} +{% endif %} +{% if tags %} +{{ id }}.tags = {{ policy_get_tagset(tags) }} +{% endif %} +{{ id }}.nodata = {{ boolean(nodata) }} +{{ id }}.is_rpz = {{ boolean(is_rpz) }} +{% if is_rpz -%} +{{ id }}.filename = '{{ input_str }}' +{% else %} +{{ id }}.input_str = [[ +{{ input_str }}]] +{% endif %} +assert(C.kr_rule_zonefile({{ id }})==0) +{%- endmacro %} + +{% macro local_data_emptyzone(dname, tags) -%} +assert(C.kr_rule_local_data_emptyzone({{ dname }},{{ tags }})==0) +{%- endmacro %} + +{% macro local_data_nxdomain(dname, tags) -%} +assert(C.kr_rule_local_data_nxdomain({{ dname }},{{ tags }})==0) +{%- endmacro %} + +{% macro local_data_subtree_root(type, root, tags) -%} +{%- if tags -%} +{%- set get_tags = policy_get_tagset(tags) -%} +{%- else -%} +{%- set get_tags = '0' -%} +{%- endif -%} +{%- if type == 'empty' -%} +{{ local_data_emptyzone(policy_todname(root), get_tags) }} +{%- elif type == 'nxdomain' -%} +{{ local_data_nxdomain(policy_todname(root), get_tags) }} +{%- else -%} +{# TODO: implement other possible types #} +{%- endif -%} +{%- endmacro %} diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2 index 933ecdfa..ff78fbd8 100644 --- a/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/macros/network_macros.lua.j2 @@ -44,20 +44,12 @@ net.{{ interface.if_name }}, {% macro network_listen(listen) -%} {%- if listen.unix_socket -%} - {%- if listen.unix_socket is iterable-%} - {% for path in listen.unix_socket -%} - {{ net_listen_unix_socket(path, listen.kind, listen.freebind) }} - {% endfor -%} - {%- else -%} - {{ net_listen_unix_socket(listen.unix_socket, listen.kind, listen.freebind) }} - {%- endif -%} +{% for path in listen.unix_socket %} +{{ net_listen_unix_socket(path, listen.kind, listen.freebind) }} +{% endfor %} {%- elif listen.interface -%} - {%- if listen.interface is iterable-%} - {% for interface in listen.interface -%} - {{ net_listen_interface(interface, listen.kind, listen.freebind, listen.port) }} - {% endfor -%} - {%- else -%} - {{ net_listen_interface(listen.interface, listen.kind, listen.freebind, listen.port) }} - {%- endif -%} +{% for interface in listen.interface %} +{{ net_listen_interface(interface, listen.kind, listen.freebind, listen.port) }} +{% endfor %} {%- endif -%} {%- endmacro %}
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2 index 8ffd83a5..36ce102f 100644 --- a/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/macros/policy_macros.lua.j2 @@ -37,17 +37,22 @@ policy.slice_randomize_psl() {% macro policy_flags(flags) -%} policy.FLAGS({ -{%- if flags is string -%} -'{{ flags.upper().replace("-", "_")|string }}' -{%- else -%} -{%- for flag in flags|list -%} -'{{ flag.upper().replace("-", "_") }}', -{%- endfor -%} -{%- endif -%} +{{- flags -}} }) {%- endmacro %} +{# Tags assign #} + +{% macro policy_tags_assign(tags) -%} +policy.TAGS_ASSIGN({{ string_table(tags) }}) +{%- endmacro %} + +{% macro policy_get_tagset(tags) -%} +policy.get_tagset({{ string_table(tags) }}) +{%- endmacro %} + + {# Filters #} {% macro policy_all(action) -%} @@ -253,7 +258,11 @@ policy.TLS_FORWARD({{ tls_servers_table(servers) }}) {# Other #} -{% macro policy_todname(names) -%} +{% macro policy_todname(name) -%} +todname('{{ name.punycode()|string }}') +{%- endmacro %} + +{% macro policy_todnames(names) -%} policy.todnames({ {%- if names is string -%} '{{ names.punycode()|string }}' diff --git a/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2 index b829f2ef..efd03211 100644 --- a/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/macros/view_macros.lua.j2 @@ -1,7 +1,22 @@ -{% macro view_tsig(tsig, rule) -%} -view:tsig('{{ tsig }}',{{ rule }}) +{% macro view_insert_action(subnet, action) -%} +assert(C.kr_view_insert_action('{{ subnet }}',{{ action }})==0) {%- endmacro %} -{% macro view_addr(addr, rule) -%} -view:addr('{{ addr }}',{{ rule }}) -{%- endmacro %}
\ No newline at end of file +{% macro view_flags(options) -%} +{% if not options.minimize -%} +"NO_MINIMIZE", +{%- endif %} +{% if not options.dns64 -%} +"DNS64_DISABLE", +{%- endif %} +{%- endmacro %} + +{% macro view_answer(answer) -%} +{%- if answer == 'allow' -%} +policy.TAGS_ASSIGN({}) +{%- elif answer == 'refused' -%} +'policy.REFUSE' +{%- elif answer == 'noanswer' -%} +'policy.NO_ANSWER' +{%- endif -%} +{%- endmacro %} diff --git a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 index 60b09652..665ee454 100644 --- a/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/network.lua.j2 @@ -1,8 +1,9 @@ +{% from 'macros/common_macros.lua.j2' import boolean %} {% from 'macros/network_macros.lua.j2' import network_listen, http_config %} -- network.do-ipv4/6 -net.ipv4 = {{ 'true' if cfg.network.do_ipv4 else 'false' }} -net.ipv6 = {{ 'true' if cfg.network.do_ipv6 else 'false' }} +net.ipv4 = {{ boolean(cfg.network.do_ipv4) }} +net.ipv6 = {{ boolean(cfg.network.do_ipv6) }} {% if cfg.network.out_interface_v4 %} -- network.out-interface-v4 diff --git a/manager/knot_resolver_manager/datamodel/templates/options.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/options.lua.j2 index 4abe977b..8210fb6d 100644 --- a/manager/knot_resolver_manager/datamodel/templates/options.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/options.lua.j2 @@ -1,3 +1,5 @@ +{% from 'macros/common_macros.lua.j2' import boolean %} + -- options.glue-checking mode('{{ cfg.options.glue_checking }}') @@ -38,13 +40,13 @@ modules.unload('refuse_nord') {% endif %} -- options.qname-minimisation -option('NO_MINIMIZE', {{ 'false' if cfg.options.qname_minimisation else 'true' }}) +option('NO_MINIMIZE', {{ boolean(cfg.options.minimize,true) }}) -- options.query-loopback -option('ALLOW_LOCAL', {{ 'true' if cfg.options.query_loopback else 'false' }}) +option('ALLOW_LOCAL', {{ boolean(cfg.options.query_loopback) }}) -- options.reorder-rrset -option('REORDER_RR', {{ 'true' if cfg.options.reorder_rrset else 'false' }}) +option('REORDER_RR', {{ boolean(cfg.options.reorder_rrset) }}) -- options.query-case-randomization -option('NO_0X20', {{ 'false' if cfg.options.query_case_randomization else 'true' }})
\ No newline at end of file +option('NO_0X20', {{ boolean(cfg.options.query_case_randomization,true) }})
\ No newline at end of file diff --git a/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2 index 85483e6e..85290982 100644 --- a/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/stub_zones.lua.j2 @@ -3,7 +3,7 @@ {% if cfg.stub_zones %} {% for zone in cfg.stub_zones %} --- stub-zone: {{ zone.name }} +-- stub-zone: {{ zone.subtree }} {% if zone.views -%} {# views set for stub-zone #} {% for view_id in zone.views -%} @@ -23,10 +23,10 @@ {% for tsig in view.tsig -%} {%- if options -%} -{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }} +{{ view_tsig(tsig|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }} {%- endif %} -{{ view_tsig(tsig|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_tsig(tsig|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }} {% endfor %} {%- endif -%} @@ -35,10 +35,10 @@ {% for addr in view.subnets -%} {%- if options -%} -{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.name|string))) }} +{{ view_addr(addr|string, policy_suffix(policy_flags(options|list), policy_todname(zone.subtree|string))) }} {%- endif %} -{{ view_addr(addr|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }} +{{ view_addr(addr|string, policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }} {% endfor %} {% endif %} @@ -48,10 +48,10 @@ {# no views set for stub-zone #} {% if zone.options -%} -{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.name|string))) }} +{{ policy_add(policy_suffix(policy_flags(zone.options|list), policy_todname(zone.subtree|string))) }} {%- endif %} -{{ policy_add(policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.name|string))) }} +{{ policy_add(policy_suffix(policy_stub(zone.servers|list), policy_todname(zone.subtree|string))) }} {% endif %} {% endfor %} diff --git a/manager/knot_resolver_manager/datamodel/templates/views.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/views.lua.j2 index fbe8617e..99c654c9 100644 --- a/manager/knot_resolver_manager/datamodel/templates/views.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/views.lua.j2 @@ -1,3 +1,22 @@ +{% from 'macros/common_macros.lua.j2' import quotes %} +{% from 'macros/view_macros.lua.j2' import view_insert_action, view_flags, view_answer %} +{% from 'macros/policy_macros.lua.j2' import policy_flags, policy_tags_assign %} + {% if cfg.views %} -modules.load('view') -{% endif %}
\ No newline at end of file +{% for view in cfg.views %} +{% for subnet in view.subnets %} + +{% if view.tags -%} +{{ view_insert_action(subnet, policy_tags_assign(view.tags)) }} +{% elif view.answer %} +{{ view_insert_action(subnet, view_answer(view.answer)) }} +{%- endif %} + +{%- set flags = view_flags(view.options) -%} +{% if flags -%} +{{ view_insert_action(subnet, quotes(policy_flags(flags))) }} +{%- endif %} + +{% endfor %} +{% endfor %} +{% endif %} diff --git a/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2 b/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2 index 1dd0098c..938ea8da 100644 --- a/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2 +++ b/manager/knot_resolver_manager/datamodel/templates/webmgmt.lua.j2 @@ -1,7 +1,9 @@ +{% from 'macros/common_macros.lua.j2' import boolean %} + {% if cfg.webmgmt -%} -- webmgmt modules.load('http') -http.config({tls = {{ 'true' if cfg.webmgmt.tls else 'false'}}, +http.config({tls = {{ boolean(cfg.webmgmt.tls) }}, {%- if cfg.webmgmt.cert_file -%} cert = '{{ cfg.webmgmt.cert_file }}', {%- endif -%} diff --git a/manager/knot_resolver_manager/datamodel/types/__init__.py b/manager/knot_resolver_manager/datamodel/types/__init__.py index bdd22c82..33d8c90d 100644 --- a/manager/knot_resolver_manager/datamodel/types/__init__.py +++ b/manager/knot_resolver_manager/datamodel/types/__init__.py @@ -1,7 +1,9 @@ from .enums import DNSRecordTypeEnum, PolicyActionEnum, PolicyFlagEnum from .files import AbsoluteDir, Dir, File, FilePath +from .generic_types import ListOrItem from .types import ( DomainName, + IDPattern, Int0_512, Int0_65535, InterfaceName, @@ -16,6 +18,7 @@ from .types import ( IPv4Address, IPv6Address, IPv6Network96, + Percent, PortNumber, SizeUnit, TimeUnit, @@ -26,6 +29,7 @@ __all__ = [ "PolicyFlagEnum", "DNSRecordTypeEnum", "DomainName", + "IDPattern", "Int0_512", "Int0_65535", "InterfaceName", @@ -40,6 +44,8 @@ __all__ = [ "IPv4Address", "IPv6Address", "IPv6Network96", + "ListOrItem", + "Percent", "PortNumber", "SizeUnit", "TimeUnit", diff --git a/manager/knot_resolver_manager/datamodel/types/base_types.py b/manager/knot_resolver_manager/datamodel/types/base_types.py index 9bf78402..96c0a393 100644 --- a/manager/knot_resolver_manager/datamodel/types/base_types.py +++ b/manager/knot_resolver_manager/datamodel/types/base_types.py @@ -155,6 +155,9 @@ class UnitBase(IntBase): self._value = int(val) * type(self)._units[unit] else: raise ValueError(f"{type(self._value)} Failed to convert: {self}") + elif source_value in (0, "0"): + self._value_orig = source_value + self._value = int(source_value) elif isinstance(source_value, int): raise ValueError( f"number without units, please convert to string and add unit - {list(type(self)._units.keys())}", diff --git a/manager/knot_resolver_manager/datamodel/types/generic_types.py b/manager/knot_resolver_manager/datamodel/types/generic_types.py new file mode 100644 index 00000000..bf4e8680 --- /dev/null +++ b/manager/knot_resolver_manager/datamodel/types/generic_types.py @@ -0,0 +1,33 @@ +from typing import Any, List, TypeVar, Union + +from knot_resolver_manager.utils.modeling import BaseGenericTypeWrapper + +T = TypeVar("T") + + +class ListOrItem(BaseGenericTypeWrapper[Union[List[T], T]]): + _value_orig: Union[List[T], T] + _list: List[T] + + def __init__(self, source_value: Any, object_path: str = "/") -> None: # pylint: disable=unused-argument + super().__init__(source_value) + self._value_orig: Union[List[T], T] = source_value + self._list: List[T] = source_value if isinstance(source_value, list) else [source_value] + + def __getitem__(self, index: Any) -> T: + return self._list[index] + + def __int__(self) -> int: + raise ValueError(f"Can't convert '{type(self).__name__}' to an integer.") + + def __str__(self) -> str: + return str(self._value_orig) + + def to_std(self) -> List[T]: + return self._list + + def __eq__(self, o: object) -> bool: + return isinstance(o, ListOrItem) and o._value_orig == self._value_orig + + def serialize(self) -> Union[List[T], T]: + return self._value_orig diff --git a/manager/knot_resolver_manager/datamodel/types/types.py b/manager/knot_resolver_manager/datamodel/types/types.py index 08623535..f38759c8 100644 --- a/manager/knot_resolver_manager/datamodel/types/types.py +++ b/manager/knot_resolver_manager/datamodel/types/types.py @@ -24,6 +24,11 @@ class Int0_65535(IntRangeBase): _max: int = 65_535 +class Percent(IntRangeBase): + _min: int = 0 + _max: int = 100 + + class PortNumber(IntRangeBase): _min: int = 1 _max: int = 65_535 @@ -42,14 +47,20 @@ class SizeUnit(UnitBase): def bytes(self) -> int: return self._value + def mbytes(self) -> int: + return self._value // 1024**2 + class TimeUnit(UnitBase): - _units = {"ms": 1, "s": 1000, "m": 60 * 1000, "h": 3600 * 1000, "d": 24 * 3600 * 1000} + _units = {"us": 1, "ms": 10**3, "s": 10**6, "m": 60 * 10**6, "h": 3600 * 10**6, "d": 24 * 3600 * 10**6} def seconds(self) -> int: - return self._value // 1000 + return self._value // 1000**2 def millis(self) -> int: + return self._value // 1000 + + def micros(self) -> int: return self._value diff --git a/manager/knot_resolver_manager/datamodel/view_schema.py b/manager/knot_resolver_manager/datamodel/view_schema.py index f84ab428..74bf5a11 100644 --- a/manager/knot_resolver_manager/datamodel/view_schema.py +++ b/manager/knot_resolver_manager/datamodel/view_schema.py @@ -1,23 +1,40 @@ from typing import List, Optional -from knot_resolver_manager.datamodel.types import IPNetwork, PolicyFlagEnum +from typing_extensions import Literal + +from knot_resolver_manager.datamodel.types import IDPattern, IPNetwork from knot_resolver_manager.utils.modeling import ConfigSchema +class ViewOptionsSchema(ConfigSchema): + """ + Configuration options for clients identified by the view. + + --- + minimize: Send minimum amount of information in recursive queries to enhance privacy. + dns64: Enable/disable DNS64. + """ + + minimize: bool = True + dns64: bool = True + + class ViewSchema(ConfigSchema): """ Configuration parameters that allow you to create personalized policy rules and other. --- subnets: Identifies the client based on his subnet. - tsig: Identifies the client based on a TSIG key name (for testing purposes, TSIG signature is not verified!). - options: Configuration flags for clients identified by the view. + tags: Tags to link with other policy rules. + answer: Direct approach how to handle request from clients identified by the view. + options: Configuration options for clients identified by the view. """ - subnets: Optional[List[IPNetwork]] = None - tsig: Optional[List[str]] = None - options: Optional[List[PolicyFlagEnum]] = None + subnets: List[IPNetwork] + tags: Optional[List[IDPattern]] = None + answer: Optional[Literal["allow", "refused", "noanswer"]] = None + options: ViewOptionsSchema = ViewOptionsSchema() def _validate(self) -> None: - if self.tsig is None and self.subnets is None: - raise ValueError("'subnets' or 'rsig' must be configured") + if bool(self.tags) == bool(self.answer): + raise ValueError("only one of 'tags' and 'answer' options must be configured") diff --git a/manager/knot_resolver_manager/kres_manager.py b/manager/knot_resolver_manager/kres_manager.py index 36cddc13..072c73fc 100644 --- a/manager/knot_resolver_manager/kres_manager.py +++ b/manager/knot_resolver_manager/kres_manager.py @@ -193,7 +193,7 @@ class KresManager: # pylint: disable=too-many-instance-attributes await self._rolling_restart(config) await self._ensure_number_of_children(config, int(config.workers)) - if self._is_gc_running() != config.cache.garbage_collector: + if self._is_gc_running() != bool(config.cache.garbage_collector): if config.cache.garbage_collector: logger.debug("Starting cache GC") await self._start_gc(config) diff --git a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py index 758a9da5..08450739 100644 --- a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py +++ b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py @@ -48,6 +48,30 @@ class SupervisordKresID(KresID): raise RuntimeError(f"Unexpected subprocess type {self.subprocess_type}") +def kres_cache_gc_args(config: KresConfig) -> str: + args = "" + + if config.logging.level == "debug" or (config.logging.groups and "cache-gc" in config.logging.groups): + args += " -v" + + gc_config = config.cache.garbage_collector + if gc_config: + args += ( + f" -d {gc_config.interval.millis()}" + f" -u {gc_config.threshold}" + f" -f {gc_config.release}" + f" -l {gc_config.rw_deletes}" + f" -L {gc_config.rw_reads}" + f" -t {gc_config.temp_keys_space.mbytes()}" + f" -m {gc_config.rw_duration.micros()}" + f" -w {gc_config.rw_delay.micros()}" + ) + if gc_config.dry_run: + args += " -n" + return args + raise ValueError("missing configuration for the cache garbage collector") + + @dataclass class ProcessTypeConfig: """ @@ -66,7 +90,7 @@ class ProcessTypeConfig: return ProcessTypeConfig( # type: ignore[call-arg] logfile=supervisord_subprocess_log_dir(config) / "gc.log", workdir=cwd, - command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)} -d 1000", + command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)}{kres_cache_gc_args(config)}", environment="", ) @@ -152,6 +176,7 @@ async def write_config_file(config: KresConfig) -> None: manager=ProcessTypeConfig.create_manager_config(config), config=SupervisordConfig.create(config), ) + print(config_string) await writefile(supervisord_config_file_tmp(config), config_string) # atomically replace (we don't technically need this right now, but better safe then sorry) os.rename(supervisord_config_file_tmp(config), supervisord_config_file(config)) diff --git a/manager/knot_resolver_manager/utils/modeling/__init__.py b/manager/knot_resolver_manager/utils/modeling/__init__.py index c72c60c7..d16f6c12 100644 --- a/manager/knot_resolver_manager/utils/modeling/__init__.py +++ b/manager/knot_resolver_manager/utils/modeling/__init__.py @@ -1,8 +1,10 @@ +from .base_generic_type_wrapper import BaseGenericTypeWrapper from .base_schema import BaseSchema, ConfigSchema from .base_value_type import BaseValueType from .parsing import parse_json, parse_yaml, try_to_parse __all__ = [ + "BaseGenericTypeWrapper", "BaseValueType", "BaseSchema", "ConfigSchema", diff --git a/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py b/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py new file mode 100644 index 00000000..1f2c1767 --- /dev/null +++ b/manager/knot_resolver_manager/utils/modeling/base_generic_type_wrapper.py @@ -0,0 +1,9 @@ +from typing import Generic, TypeVar + +from .base_value_type import BaseTypeABC + +T = TypeVar("T") + + +class BaseGenericTypeWrapper(Generic[T], BaseTypeABC): # pylint: disable=abstract-method + pass diff --git a/manager/knot_resolver_manager/utils/modeling/base_schema.py b/manager/knot_resolver_manager/utils/modeling/base_schema.py index 31cea7cc..32388816 100644 --- a/manager/knot_resolver_manager/utils/modeling/base_schema.py +++ b/manager/knot_resolver_manager/utils/modeling/base_schema.py @@ -7,15 +7,18 @@ import yaml from knot_resolver_manager.utils.functional import all_matches +from .base_generic_type_wrapper import BaseGenericTypeWrapper from .base_value_type import BaseValueType from .exceptions import AggregateDataValidationError, DataDescriptionError, DataValidationError from .renaming import Renamed, renamed from .types import ( get_generic_type_argument, get_generic_type_arguments, + get_generic_type_wrapper_argument, get_optional_inner_type, is_dict, is_enum, + is_generic_type_wrapper, is_internal_field_name, is_list, is_literal, @@ -54,6 +57,7 @@ class Serializable(ABC): or is_literal(typ) or is_dict(typ) or is_list(typ) + or is_generic_type_wrapper(typ) or (inspect.isclass(typ) and issubclass(typ, Serializable)) or (inspect.isclass(typ) and issubclass(typ, BaseValueType)) or (inspect.isclass(typ) and issubclass(typ, BaseSchema)) @@ -66,8 +70,11 @@ class Serializable(ABC): if isinstance(obj, Serializable): return obj.to_dict() - elif isinstance(obj, BaseValueType): - return obj.serialize() + elif isinstance(obj, (BaseValueType, BaseGenericTypeWrapper)): + o = obj.serialize() + # if Serializable.is_serializable(o): + return Serializable.serialize(o) + # return o elif isinstance(obj, list): res: List[Any] = [Serializable.serialize(i) for i in cast(List[Any], obj)] @@ -171,6 +178,10 @@ def _describe_type(typ: Type[Any]) -> Dict[Any, Any]: elif inspect.isclass(typ) and issubclass(typ, BaseValueType): return typ.json_schema() + elif is_generic_type_wrapper(typ): + wrapped = get_generic_type_wrapper_argument(typ) + return _describe_type(wrapped) + elif is_none_type(typ): return {"type": "null"} @@ -279,11 +290,15 @@ class ObjectMapper: inner_type = get_generic_type_argument(tp) errs: List[DataValidationError] = [] res: List[Any] = [] - for i, val in enumerate(obj): - try: + + try: + for i, val in enumerate(obj): res.append(self.map_object(inner_type, val, object_path=f"{object_path}[{i}]")) - except DataValidationError as e: - errs.append(e) + except DataValidationError as e: + errs.append(e) + except TypeError as e: + errs.append(DataValidationError(str(e), object_path)) + if len(errs) == 1: raise errs[0] elif len(errs) > 1: @@ -465,6 +480,12 @@ class ObjectMapper: elif inspect.isclass(tp) and issubclass(tp, BaseValueType): return self.create_value_type_object(tp, obj, object_path) + # BaseGenericTypeWrapper subclasses + elif is_generic_type_wrapper(tp): + inner_type = get_generic_type_wrapper_argument(tp) + obj_valid = self.map_object(inner_type, obj, object_path) + return tp(obj_valid, object_path=object_path) # type: ignore + # nested BaseSchema subclasses elif inspect.isclass(tp) and issubclass(tp, BaseSchema): return self._create_base_schema_object(tp, obj, object_path) diff --git a/manager/knot_resolver_manager/utils/modeling/base_value_type.py b/manager/knot_resolver_manager/utils/modeling/base_value_type.py index 1b07e3d4..dff4a3fe 100644 --- a/manager/knot_resolver_manager/utils/modeling/base_value_type.py +++ b/manager/knot_resolver_manager/utils/modeling/base_value_type.py @@ -2,18 +2,7 @@ from abc import ABC, abstractmethod # pylint: disable=[no-name-in-module] from typing import Any, Dict, Type -class BaseValueType(ABC): - """ - Subclasses of this class can be used as type annotations in 'DataParser'. When a value - is being parsed from a serialized format (e.g. JSON/YAML), an object will be created by - calling the constructor of the appropriate type on the field value. The only limitation - is that the value MUST NOT be `None`. - - There is no validation done on the wrapped value. The only condition is that - it can't be `None`. If you want to perform any validation during creation, - raise a `ValueError` in case of errors. - """ - +class BaseTypeABC(ABC): @abstractmethod def __init__(self, source_value: Any, object_path: str = "/") -> None: pass @@ -37,6 +26,19 @@ class BaseValueType(ABC): """ raise NotImplementedError(f"{type(self).__name__}'s' 'serialize()' not implemented.") + +class BaseValueType(BaseTypeABC): + """ + Subclasses of this class can be used as type annotations in 'DataParser'. When a value + is being parsed from a serialized format (e.g. JSON/YAML), an object will be created by + calling the constructor of the appropriate type on the field value. The only limitation + is that the value MUST NOT be `None`. + + There is no validation done on the wrapped value. The only condition is that + it can't be `None`. If you want to perform any validation during creation, + raise a `ValueError` in case of errors. + """ + @classmethod @abstractmethod def json_schema(cls: Type["BaseValueType"]) -> Dict[Any, Any]: diff --git a/manager/knot_resolver_manager/utils/modeling/types.py b/manager/knot_resolver_manager/utils/modeling/types.py index aaeded9e..4ce9aecc 100644 --- a/manager/knot_resolver_manager/utils/modeling/types.py +++ b/manager/knot_resolver_manager/utils/modeling/types.py @@ -8,6 +8,8 @@ from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union from typing_extensions import Literal +from .base_generic_type_wrapper import BaseGenericTypeWrapper + NoneType = type(None) @@ -46,6 +48,11 @@ def is_literal(tp: Any) -> bool: return getattr(tp, "__origin__", None) == Literal +def is_generic_type_wrapper(tp: Any) -> bool: + orig = getattr(tp, "__origin__", None) + return inspect.isclass(orig) and issubclass(orig, BaseGenericTypeWrapper) + + def get_generic_type_arguments(tp: Any) -> List[Any]: default: List[Any] = [] if sys.version_info.minor == 6 and is_literal(tp): @@ -62,6 +69,17 @@ def get_generic_type_argument(tp: Any) -> Any: return args[0] +def get_generic_type_wrapper_argument(tp: Type["BaseGenericTypeWrapper[Any]"]) -> Any: + assert hasattr(tp, "__origin__") + origin = getattr(tp, "__origin__") + + assert hasattr(origin, "__orig_bases__") + orig_base: List[Any] = getattr(origin, "__orig_bases__", [])[0] + + arg = get_generic_type_argument(tp) + return get_generic_type_argument(orig_base[arg]) + + def is_none_type(tp: Any) -> bool: return tp is None or tp == NoneType diff --git a/manager/knot_resolver_manager/utils/requests.py b/manager/knot_resolver_manager/utils/requests.py index e406ab3b..ab95c5d2 100644 --- a/manager/knot_resolver_manager/utils/requests.py +++ b/manager/knot_resolver_manager/utils/requests.py @@ -1,6 +1,6 @@ import socket -from http.client import HTTPConnection import sys +from http.client import HTTPConnection from typing import Any, Optional, Union from urllib.error import HTTPError, URLError from urllib.request import AbstractHTTPHandler, Request, build_opener, install_opener, urlopen diff --git a/manager/poetry.lock b/manager/poetry.lock index 08dea305..364a19eb 100644 --- a/manager/poetry.lock +++ b/manager/poetry.lock @@ -2933,4 +2933,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "16ce34509fd2c4b4bedee9c8fc289f077bbe3057e95e7a2736ce71d994499444" +content-hash = "c79ae903180b91d16637ed8d463843473edcae8d0ff2fbd89921b34922a56ba8" diff --git a/manager/pyproject.toml b/manager/pyproject.toml index 4d1f2988..7aec2a29 100644 --- a/manager/pyproject.toml +++ b/manager/pyproject.toml @@ -53,6 +53,7 @@ knot-resolver = 'knot_resolver_manager.__main__:run' [tool.poe.tasks] run = { cmd = "scripts/run", help = "Run the manager" } +run-new-policy = { cmd = "scripts/run-new-policy", help = "Run the manager with 'new-policy' kresd" } run-debug = { cmd = "scripts/run-debug", help = "Run the manager under debugger" } docs = { cmd = "scripts/docs", help = "Create HTML documentation" } test = { shell = "env PYTHONPATH=. pytest --junitxml=unit.junit.xml --cov=knot_resolver_manager --show-capture=all tests/unit/", help = "Run tests" } @@ -173,5 +174,8 @@ implicit_reexport = false no_implicit_optional = true [build-system] -requires = ["poetry-core>=1.0.0"] +requires = [ + "poetry-core>=1.0.0", + "setuptools>=67.8.0" +] build-backend = "poetry.core.masonry.api" diff --git a/manager/scripts/_env.sh b/manager/scripts/_env.sh index b1941edf..52697ca0 100644 --- a/manager/scripts/_env.sh +++ b/manager/scripts/_env.sh @@ -50,3 +50,36 @@ function build_kresd { export PATH="$(realpath manager/.install_kresd)/sbin:$PATH" popd } + +function build_kresd_new_policy { + echo + echo Building Knot Resolver + echo ---------------------- + echo -e "${blue}In case of an compilation error, run this command to try to fix it:${reset}" + echo -e "\t${blue}rm -r $(realpath .install_kresd) $(realpath .build_kresd)${reset}" + echo + + + pushd .. + rm -rf manager/.install_kresd manager/.build_kresd + mkdir -p manager/.build_kresd manager/.install_kresd + + if [ -d "kres-new-policy" ] + then + echo updating repository... + cd kres-new-policy + git pull + else + echo cloning repository... + git clone -b new-policy https://gitlab.nic.cz/knot/knot-resolver.git kres-new-policy + cd kres-new-policy + git submodule update --init --recursive + fi + + meson setup ../manager/.build_kresd --prefix=$(realpath ../manager/.install_kresd) --default-library=static --buildtype=debug + ninja -C ../manager/.build_kresd + ninja install -C ../manager/.build_kresd + cd .. + export PATH="$(realpath manager/.install_kresd)/sbin:$PATH" + popd +}
\ No newline at end of file diff --git a/manager/scripts/run-new-policy b/manager/scripts/run-new-policy new file mode 100755 index 00000000..771710c2 --- /dev/null +++ b/manager/scripts/run-new-policy @@ -0,0 +1,29 @@ +#!/bin/bash + +# ensure consistent behaviour +src_dir="$(dirname "$(realpath "$0")")" +source $src_dir/_env.sh + +build_kresd_new_policy + +echo +echo Building Knot Resolver Manager native extensions +echo ------------------------------------------------ +poetry build +# copy native modules from build directory to source directory +shopt -s globstar +shopt -s nullglob +for d in build/lib*; do + for f in "$d/"**/*.so; do + cp -v "$f" ${f#"$d/"} + done +done +shopt -u globstar +shopt -u nullglob + + +echo +echo Knot Manager API is accessible on http://localhost:5000 +echo ------------------------------------------------------- + +python3 -m knot_resolver_manager -c etc/knot-resolver/config.policy.dev.yml $@ diff --git a/manager/tests/unit/datamodel/templates/test_common_macros.py b/manager/tests/unit/datamodel/templates/test_common_macros.py index 6cb24050..d730fb9d 100644 --- a/manager/tests/unit/datamodel/templates/test_common_macros.py +++ b/manager/tests/unit/datamodel/templates/test_common_macros.py @@ -1,5 +1,23 @@ from knot_resolver_manager.datamodel.config_schema import template_from_str -from knot_resolver_manager.datamodel.forward_zone_schema import ForwardServerSchema +from knot_resolver_manager.datamodel.forward_schema import ForwardServerSchema + + +def test_boolean(): + tmpl_str = """{% from 'macros/common_macros.lua.j2' import boolean %} +{{ boolean(x) }}""" + + tmpl = template_from_str(tmpl_str) + assert tmpl.render(x=True) == "true" + assert tmpl.render(x=False) == "false" + + +def test_boolean_neg(): + tmpl_str = """{% from 'macros/common_macros.lua.j2' import boolean %} +{{ boolean(x,true) }}""" + + tmpl = template_from_str(tmpl_str) + assert tmpl.render(x=True) == "false" + assert tmpl.render(x=False) == "true" def test_string_table(): @@ -50,9 +68,9 @@ def test_servers_table(): def test_tls_servers_table(): d = ForwardServerSchema( # the ca-file is a dummy, because it's existence is checked - {"address": "2001:DB8::d0c", "hostname": "res.example.com", "ca-file": "/etc/passwd"} + {"address": ["2001:DB8::d0c"], "hostname": "res.example.com", "ca-file": "/etc/passwd"} ) - t = [d, ForwardServerSchema({"address": "192.0.2.1", "pin-sha256": "YQ=="})] + t = [d, ForwardServerSchema({"address": ["192.0.2.1"], "pin-sha256": "YQ=="})] tmpl_str = """{% from 'macros/common_macros.lua.j2' import tls_servers_table %} {{ tls_servers_table(x) }}""" @@ -60,5 +78,5 @@ def test_tls_servers_table(): assert tmpl.render(x=[d.address, t[1].address]) == f"{{'{d.address}','{t[1].address}',}}" assert ( tmpl.render(x=t) - == f"{{{{'{d.address}',hostname='{d.hostname}',ca_file='{d.ca_file}',}},{{'{t[1].address}',pin_sha256='{t[1].pin_sha256}',}},}}" + == f"{{{{'{d.address}',hostname='{d.hostname}',ca_file='{d.ca_file}',}},{{'{t[1].address}',pin_sha256={{'{t[1].pin_sha256}',}}}},}}" ) diff --git a/manager/tests/unit/datamodel/templates/test_forward_macros.py b/manager/tests/unit/datamodel/templates/test_forward_macros.py new file mode 100644 index 00000000..5f80df15 --- /dev/null +++ b/manager/tests/unit/datamodel/templates/test_forward_macros.py @@ -0,0 +1,27 @@ +from knot_resolver_manager.datamodel.config_schema import template_from_str +from knot_resolver_manager.datamodel.forward_schema import ForwardSchema +from knot_resolver_manager.datamodel.types import IPAddressOptionalPort + + +def test_policy_rule_forward_add(): + tmpl_str = """{% from 'macros/forward_macros.lua.j2' import policy_rule_forward_add %} +{{ policy_rule_forward_add(rule) }}""" + + rule = ForwardSchema( + { + "subtree": ".", + "servers": [{"address": ["2001:148f:fffe::1", "185.43.135.1"], "hostname": "odvr.nic.cz"}], + "options": { + "authoritative": False, + "dnssec": True, + }, + } + ) + result = "policy.rule_forward_add('.',{dnssec=true,auth=false},{{'2001:148f:fffe::1',tls=false,hostname='odvr.nic.cz',},{'185.43.135.1',tls=false,hostname='odvr.nic.cz',},})" + + tmpl = template_from_str(tmpl_str) + assert tmpl.render(rule=rule) == result + + rule.servers = [IPAddressOptionalPort("2001:148f:fffe::1"), IPAddressOptionalPort("185.43.135.1")] + result = "policy.rule_forward_add('.',{dnssec=true,auth=false},{{'2001:148f:fffe::1'},{'185.43.135.1'},})" + assert tmpl.render(rule=rule) == result diff --git a/manager/tests/unit/datamodel/templates/test_network_macros.py b/manager/tests/unit/datamodel/templates/test_network_macros.py index 6d6637ed..ad193d98 100644 --- a/manager/tests/unit/datamodel/templates/test_network_macros.py +++ b/manager/tests/unit/datamodel/templates/test_network_macros.py @@ -8,8 +8,8 @@ def test_network_listen(): tmpl = template_from_str(tmpl_str) soc = ListenSchema({"unix-socket": "/tmp/kresd-socket", "kind": "dot"}) - assert tmpl.render(listen=soc) == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})" - soc_list = ListenSchema({"unix-socket": [soc.unix_socket, "/tmp/kresd-socket2"], "kind": "dot"}) + assert tmpl.render(listen=soc) == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})\n" + soc_list = ListenSchema({"unix-socket": [soc.unix_socket.to_std()[0], "/tmp/kresd-socket2"], "kind": "dot"}) assert ( tmpl.render(listen=soc_list) == "net.listen('/tmp/kresd-socket',nil,{kind='tls',freebind=false})\n" @@ -17,8 +17,8 @@ def test_network_listen(): ) ip = ListenSchema({"interface": "::1@55", "freebind": True}) - assert tmpl.render(listen=ip) == "net.listen('::1',55,{kind='dns',freebind=true})" - ip_list = ListenSchema({"interface": [ip.interface, "127.0.0.1@5353"]}) + assert tmpl.render(listen=ip) == "net.listen('::1',55,{kind='dns',freebind=true})\n" + ip_list = ListenSchema({"interface": [ip.interface.to_std()[0], "127.0.0.1@5353"]}) assert ( tmpl.render(listen=ip_list) == "net.listen('::1',55,{kind='dns',freebind=false})\n" @@ -26,8 +26,8 @@ def test_network_listen(): ) intrfc = ListenSchema({"interface": "eth0", "kind": "doh2"}) - assert tmpl.render(listen=intrfc) == "net.listen(net.eth0,443,{kind='doh2',freebind=false})" - intrfc_list = ListenSchema({"interface": [intrfc.interface, "lo"], "port": 5555, "kind": "doh2"}) + assert tmpl.render(listen=intrfc) == "net.listen(net.eth0,443,{kind='doh2',freebind=false})\n" + intrfc_list = ListenSchema({"interface": [intrfc.interface.to_std()[0], "lo"], "port": 5555, "kind": "doh2"}) assert ( tmpl.render(listen=intrfc_list) == "net.listen(net.eth0,5555,{kind='doh2',freebind=false})\n" diff --git a/manager/tests/unit/datamodel/templates/test_policy_macros.py b/manager/tests/unit/datamodel/templates/test_policy_macros.py index 28f8d5aa..2920a206 100644 --- a/manager/tests/unit/datamodel/templates/test_policy_macros.py +++ b/manager/tests/unit/datamodel/templates/test_policy_macros.py @@ -16,16 +16,24 @@ def test_policy_add(): assert tmpl.render(rule=rule, postrule=True) == f"policy.add({rule},true)" -def test_policy_flags(): - flags: List[PolicyFlagEnum] = ["no-cache", "no-edns"] - tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_flags %} -{{ policy_flags(flags) }}""" +def test_policy_tags_assign(): + tags: List[str] = ["t01", "t02", "t03"] + tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_tags_assign %} +{{ policy_tags_assign(tags) }}""" tmpl = template_from_str(tmpl_str) - assert tmpl.render(flags=flags[1]) == f"policy.FLAGS({{'{flags[1].upper().replace('-', '_')}'}})" - assert ( - tmpl.render(flags=flags) == f"policy.FLAGS({{{str(flags).upper().replace('-', '_').replace(' ', '')[1:-1]},}})" - ) + assert tmpl.render(tags=tags[1]) == f"policy.TAGS_ASSIGN('{tags[1]}')" + assert tmpl.render(tags=tags) == "policy.TAGS_ASSIGN({" + ",".join([f"'{x}'" for x in tags]) + ",})" + + +def test_policy_get_tagset(): + tags: List[str] = ["t01", "t02", "t03"] + tmpl_str = """{% from 'macros/policy_macros.lua.j2' import policy_get_tagset %} +{{ policy_get_tagset(tags) }}""" + + tmpl = template_from_str(tmpl_str) + assert tmpl.render(tags=tags[1]) == f"policy.get_tagset('{tags[1]}')" + assert tmpl.render(tags=tags) == "policy.get_tagset({" + ",".join([f"'{x}'" for x in tags]) + ",})" # Filters diff --git a/manager/tests/unit/datamodel/templates/test_view_macros.py b/manager/tests/unit/datamodel/templates/test_view_macros.py index 32d881e2..3a3f35f9 100644 --- a/manager/tests/unit/datamodel/templates/test_view_macros.py +++ b/manager/tests/unit/datamodel/templates/test_view_macros.py @@ -1,22 +1,53 @@ +from typing import Any + +import pytest + from knot_resolver_manager.datamodel.config_schema import template_from_str -from knot_resolver_manager.datamodel.types import IPAddressOptionalPort +from knot_resolver_manager.datamodel.view_schema import ViewOptionsSchema, ViewSchema + + +def test_view_insert_action(): + subnet = "10.0.0.0/8" + action = "policy.DENY" + tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_insert_action %} +{{ view_insert_action(subnet, action) }}""" + + tmpl = template_from_str(tmpl_str) + assert tmpl.render(subnet=subnet, action=action) == f"assert(C.kr_view_insert_action('{ subnet }',{ action })==0)" + + +def test_view_flags(): + tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_flags %} +{{ view_flags(options) }}""" + + tmpl = template_from_str(tmpl_str) + options = ViewOptionsSchema({"dns64": False, "minimize": False}) + assert tmpl.render(options=options) == '"NO_MINIMIZE","DNS64_DISABLE",' + assert tmpl.render(options=ViewOptionsSchema()) == "" -def test_view_tsig(): - tsig: str = r"\5mykey" - rule = "policy.all(policy.DENY)" - tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_tsig %} -{{ view_tsig(tsig, rule) }}""" +def test_view_answer(): + tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_options_flags %} +{{ view_options_flags(options) }}""" tmpl = template_from_str(tmpl_str) - assert tmpl.render(tsig=tsig, rule=rule) == f"view:tsig('{tsig}',{rule})" + options = ViewOptionsSchema({"dns64": False, "minimize": False}) + assert tmpl.render(options=options) == "policy.FLAGS({'NO_MINIMIZE','DNS64_DISABLE',})" + assert tmpl.render(options=ViewOptionsSchema()) == "policy.FLAGS({})" -def test_view_addr(): - addr: IPAddressOptionalPort = IPAddressOptionalPort("10.0.0.1") - rule = "policy.all(policy.DENY)" - tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_addr %} -{{ view_addr(addr, rule) }}""" +@pytest.mark.parametrize( + "val,res", + [ + ("allow", "policy.TAGS_ASSIGN({})"), + ("refused", "'policy.REFUSE'"), + ("noanswer", "'policy.NO_ANSWER'"), + ], +) +def test_view_answer(val: Any, res: Any): + tmpl_str = """{% from 'macros/view_macros.lua.j2' import view_answer %} +{{ view_answer(view.answer) }}""" tmpl = template_from_str(tmpl_str) - assert tmpl.render(addr=addr, rule=rule) == f"view:addr('{addr}',{rule})" + view = ViewSchema({"subnets": ["10.0.0.0/8"], "answer": val}) + assert tmpl.render(view=view) == res diff --git a/manager/tests/unit/datamodel/test_config_schema.py b/manager/tests/unit/datamodel/test_config_schema.py index 50233473..31703b96 100644 --- a/manager/tests/unit/datamodel/test_config_schema.py +++ b/manager/tests/unit/datamodel/test_config_schema.py @@ -49,6 +49,6 @@ def test_config_json_schema(): try: _ = json.dumps(obj) except BaseException as e: - raise Exception(f"failed to serialize '{path}'") from e + raise Exception(f"failed to serialize '{path}': {e}") from e recser(dct) diff --git a/manager/tests/unit/datamodel/test_local_data.py b/manager/tests/unit/datamodel/test_local_data.py new file mode 100644 index 00000000..198bccd2 --- /dev/null +++ b/manager/tests/unit/datamodel/test_local_data.py @@ -0,0 +1,33 @@ +from typing import Any + +import pytest +from pytest import raises + +from knot_resolver_manager.datamodel.local_data_schema import LocalDataSchema, SubtreeSchema +from knot_resolver_manager.utils.modeling.exceptions import DataValidationError + + +@pytest.mark.parametrize( + "val", + [ + {"type": "empty", "roots": ["sub2.example.org"]}, + {"type": "empty", "roots-url": "https://example.org/blocklist.txt", "refresh": "1d"}, + {"type": "nxdomain", "roots-file": "/path/to/file.txt"}, + {"type": "redirect", "roots": ["sub4.example.org"], "addresses": ["127.0.0.1", "::1"]}, + ], +) +def test_subtree_valid(val: Any): + SubtreeSchema(val) + + +@pytest.mark.parametrize( + "val", + [ + {"type": "empty"}, + {"type": "empty", "roots": ["sub2.example.org"], "roots-url": "https://example.org/blocklist.txt"}, + {"type": "redirect", "roots": ["sub4.example.org"], "refresh": "1d"}, + ], +) +def test_subtree_invalid(val: Any): + with raises(DataValidationError): + SubtreeSchema(val) diff --git a/manager/tests/unit/datamodel/test_network_schema.py b/manager/tests/unit/datamodel/test_network_schema.py index 1a398b50..7b616f34 100644 --- a/manager/tests/unit/datamodel/test_network_schema.py +++ b/manager/tests/unit/datamodel/test_network_schema.py @@ -13,12 +13,12 @@ def test_listen_defaults(): assert len(o.listen) == 2 # {"ip-address": "127.0.0.1"} - assert o.listen[0].interface == InterfaceOptionalPort("127.0.0.1") + assert o.listen[0].interface.to_std() == [InterfaceOptionalPort("127.0.0.1")] assert o.listen[0].port == PortNumber(53) assert o.listen[0].kind == "dns" assert o.listen[0].freebind == False # {"ip-address": "::1", "freebind": True} - assert o.listen[1].interface == InterfaceOptionalPort("::1") + assert o.listen[1].interface.to_std() == [InterfaceOptionalPort("::1")] assert o.listen[1].port == PortNumber(53) assert o.listen[1].kind == "dns" assert o.listen[1].freebind == True @@ -27,11 +27,11 @@ def test_listen_defaults(): @pytest.mark.parametrize( "listen,port", [ - ({"unix-socket": "/tmp/kresd-socket"}, None), - ({"interface": "::1"}, 53), - ({"interface": "::1", "kind": "dot"}, 853), - ({"interface": "::1", "kind": "doh-legacy"}, 443), - ({"interface": "::1", "kind": "doh2"}, 443), + ({"unix-socket": ["/tmp/kresd-socket"]}, None), + ({"interface": ["::1"]}, 53), + ({"interface": ["::1"], "kind": "dot"}, 853), + ({"interface": ["::1"], "kind": "doh-legacy"}, 443), + ({"interface": ["::1"], "kind": "doh2"}, 443), ], ) def test_listen_port_defaults(listen: Dict[str, Any], port: Optional[int]): @@ -64,8 +64,8 @@ def test_listen_valid(listen: Dict[str, Any]): @pytest.mark.parametrize( "listen", [ - {"unit-socket": "/tmp/kresd-socket", "port": "53"}, - {"interface": "::1", "unit-socket": "/tmp/kresd-socket"}, + {"unix-socket": "/tmp/kresd-socket", "port": "53"}, + {"interface": "::1", "unix-socket": "/tmp/kresd-socket"}, {"interface": "::1@5353", "port": 5353}, {"interface": ["127.0.0.1", "::1@5353"]}, {"interface": ["127.0.0.1@5353", "::1@5353"], "port": 5353}, diff --git a/manager/tests/unit/datamodel/test_policy_schema.py b/manager/tests/unit/datamodel/test_policy_schema.py index ac3761b4..aeb98a71 100644 --- a/manager/tests/unit/datamodel/test_policy_schema.py +++ b/manager/tests/unit/datamodel/test_policy_schema.py @@ -51,7 +51,7 @@ def test_action_invalid(val: Dict[str, Any]): {"action": "mirror", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]}, {"action": "forward", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]}, {"action": "stub", "servers": ["192.0.2.1@5353", "2001:148f:ffff::1"]}, - {"action": "forward", "servers": [{"address": "127.0.0.1@5353"}]}, + {"action": "forward", "servers": [{"address": ["127.0.0.1@5353"]}]}, ], ) def test_policy_valid(val: Dict[str, Any]): @@ -68,7 +68,7 @@ def test_policy_valid(val: Dict[str, Any]): {"action": "pass", "reroute": [{"source": "192.0.2.0/24", "destination": "127.0.0.0"}]}, {"action": "pass", "answer": {"rtype": "AAAA", "rdata": "::1"}}, {"action": "pass", "servers": ["127.0.0.1@5353"]}, - {"action": "mirror", "servers": [{"address": "127.0.0.1@5353"}]}, + {"action": "mirror", "servers": [{"address": ["127.0.0.1@5353"]}]}, ], ) def test_policy_invalid(val: Dict[str, Any]): diff --git a/manager/tests/unit/datamodel/types/test_custom_types.py b/manager/tests/unit/datamodel/types/test_custom_types.py index 5fba82ee..b9d6f567 100644 --- a/manager/tests/unit/datamodel/types/test_custom_types.py +++ b/manager/tests/unit/datamodel/types/test_custom_types.py @@ -56,13 +56,14 @@ def test_size_unit_invalid(val: Any): SizeUnit(val) -@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms"]) +@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms", "86400000000us"]) def test_time_unit_valid(val: str): o = TimeUnit(val) - assert int(o) == 86400000 + assert int(o) == 86400000000 assert str(o) == val assert o.seconds() == 86400 assert o.millis() == 86400000 + assert o.micros() == 86400000000 @pytest.mark.parametrize("val", ["-1", "-24h", "1440mm", 6575, -1440]) diff --git a/manager/tests/unit/datamodel/types/test_generic_types.py b/manager/tests/unit/datamodel/types/test_generic_types.py new file mode 100644 index 00000000..7803ed00 --- /dev/null +++ b/manager/tests/unit/datamodel/types/test_generic_types.py @@ -0,0 +1,56 @@ +from typing import Any, List, Optional, Union + +import pytest +from pytest import raises + +from knot_resolver_manager.datamodel.types import ListOrItem +from knot_resolver_manager.utils.modeling import BaseSchema +from knot_resolver_manager.utils.modeling.exceptions import DataValidationError +from knot_resolver_manager.utils.modeling.types import get_generic_type_wrapper_argument + + +@pytest.mark.parametrize("val", [str, int]) +def test_list_or_item_inner_type(val: Any): + assert get_generic_type_wrapper_argument(ListOrItem[val]) == Union[List[val], val] + + +@pytest.mark.parametrize( + "typ,val", + [ + (int, [1, 65_535, 5353, 5000]), + (int, 65_535), + (str, ["string1", "string2"]), + (str, "string1"), + ], +) +def test_list_or_item_valid(typ: Any, val: Any): + class ListOrItemSchema(BaseSchema): + test: ListOrItem[typ] + + o = ListOrItemSchema({"test": val}) + assert o.test.serialize() == val + assert o.test.to_std() == val if isinstance(val, list) else [val] + + i = 0 + for item in o.test: + assert item == val[i] if isinstance(val, list) else val + i += 1 + + +@pytest.mark.parametrize( + "typ,val", + [ + (str, [True, False, True, False]), + (str, False), + (bool, [1, 65_535, 5353, 5000]), + (bool, 65_535), + (int, "string1"), + (int, ["string1", "string2"]), + ], +) +def test_list_or_item_invalid(typ: Any, val: Any): + class ListOrItemSchema(BaseSchema): + test: ListOrItem[typ] + + with raises(DataValidationError): + ListOrItemSchema({"test": val}) |