From bef0e55a20c0366b5672a8bc454a39c51c3c8ed3 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 10:38:18 +0200 Subject: [PATCH 1/8] Fix `naptr_remove` inner loop break bug --- mreg_cli/commands/host_submodules/rr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mreg_cli/commands/host_submodules/rr.py b/mreg_cli/commands/host_submodules/rr.py index cedefe6d..3a9d715e 100644 --- a/mreg_cli/commands/host_submodules/rr.py +++ b/mreg_cli/commands/host_submodules/rr.py @@ -418,8 +418,9 @@ def naptr_remove(args: argparse.Namespace) -> None: for attribute in ("preference", "order", "flag", "service", "regex", "replacement"): if getattr(args, attribute) and getattr(naptr, attribute) != getattr(args, attribute): break - - to_delete.append(naptr) + else: + # did not break, all attributes match + to_delete.append(naptr) if not to_delete: raise EntityNotFound(f"No matching NAPTR record found for {host}") From 626591df41050274809d03a5e3781672df47bf43 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 10:39:58 +0200 Subject: [PATCH 2/8] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2b667a..4c3a32fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `host find` now shows IP address(es) for each host. +### Fixed + +- `naptr_remove` removing all records regardless of the provided options. Now requires all options to match for a record to be removed. + ## [1.9.0](https://github.com/unioslo/mreg-cli/releases/tag/1.9.0) - 2026-04-14 ### Changed From 1d18947e0cba3a1f0e23fd418c65d3b40287a625 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 14:20:21 +0200 Subject: [PATCH 3/8] Fix + refactor `naptr_remove`. Add tests. --- mreg_cli/commands/host_submodules/rr.py | 25 +++--- tests/commands/test_rr.py | 107 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 tests/commands/test_rr.py diff --git a/mreg_cli/commands/host_submodules/rr.py b/mreg_cli/commands/host_submodules/rr.py index 3a9d715e..07806a17 100644 --- a/mreg_cli/commands/host_submodules/rr.py +++ b/mreg_cli/commands/host_submodules/rr.py @@ -367,6 +367,19 @@ def naptr_add(args: argparse.Namespace) -> None: OutputManager().add_ok(f"Added NAPTR record to {host.name}.") +def naptrs_from_args(naptrs: list[NAPTR], args: argparse.Namespace) -> list[NAPTR]: + """Filter naptrs based on provided args.""" + attrs = ("preference", "order", "flag", "service", "regex", "replacement") + active_args = [attr for attr in attrs if getattr(args, attr, None) is not None] + + def naptr_matches(naptr: NAPTR) -> bool: + return all( + getattr(args, attribute) == getattr(naptr, attribute) for attribute in active_args + ) + + return [naptr for naptr in naptrs if naptr_matches(naptr)] + + @command_registry.register_command( prog="naptr_remove", description="Remove matching NAPTR records from a host.", @@ -410,17 +423,7 @@ def naptr_remove(args: argparse.Namespace) -> None: :param args: argparse.Namespace (name, preference, order, flag, service, regex, replacement) """ host = Host.get_by_any_means_or_raise(args.name) - naptrs = host.naptrs - - to_delete: list[NAPTR] = [] - - for naptr in naptrs: - for attribute in ("preference", "order", "flag", "service", "regex", "replacement"): - if getattr(args, attribute) and getattr(naptr, attribute) != getattr(args, attribute): - break - else: - # did not break, all attributes match - to_delete.append(naptr) + to_delete = naptrs_from_args(host.naptrs, args) if not to_delete: raise EntityNotFound(f"No matching NAPTR record found for {host}") diff --git a/tests/commands/test_rr.py b/tests/commands/test_rr.py new file mode 100644 index 00000000..aa629be8 --- /dev/null +++ b/tests/commands/test_rr.py @@ -0,0 +1,107 @@ +import argparse +import datetime +from mreg_api.models import NAPTR +from mreg_cli.commands.host_submodules.rr import naptrs_from_args +import pytest + +_created_at = datetime.datetime(2024, 1, 1, 0, 0, 0) +_updated_at = datetime.datetime(2025, 1, 1, 0, 0, 0) + +# Overlap matrix (field -> ids sharing same value): +# host: 123=[1,2,3] 456=[4,5] 789=[6] +# preference: 10=[1,2,3] 20=[4,5] 30=[6] +# order: 20=[1,2,5] 10=[4,6] 30=[3] +# flag: "U"=[1,2,4] "S"=[3,5] None=[6] +# service: "SIP+D2U"=[1,2,5] "E2U+SIP"=[3,4] None=[6] +# regex: ""=[1,3,4] "!^.*$!..."=[2,5] None=[6] +_naptr1 = NAPTR( + id=1, + host=123, + preference=10, + order=20, + flag="U", + service="SIP+D2U", + regex="", + replacement="naptr1.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr2 = NAPTR( + id=2, + host=123, + preference=10, + order=20, + flag="U", + service="SIP+D2U", + regex="!^.*$!sip:info@example.com!", + replacement="naptr2.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr3 = NAPTR( + id=3, + host=123, + preference=10, + order=30, + flag="S", + service="E2U+SIP", + regex="", + replacement="naptr3.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr4 = NAPTR( + id=4, + host=456, + preference=20, + order=10, + flag="U", + service="E2U+SIP", + regex="", + replacement="naptr4.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr5 = NAPTR( + id=5, + host=456, + preference=20, + order=20, + flag="S", + service="SIP+D2U", + regex="!^.*$!sip:info@example.com!", + replacement="naptr5.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr6 = NAPTR( + id=6, + host=789, + preference=30, + order=10, + flag=None, + service=None, + regex=None, + replacement="naptr6.example.com", + created_at=_created_at, + updated_at=_updated_at, +) + +naptrs = [_naptr1, _naptr2, _naptr3, _naptr4, _naptr5, _naptr6] + + +@pytest.mark.parametrize( + "inp,expected", + [ + (argparse.Namespace(preference=10), [_naptr1, _naptr2, _naptr3]), + (argparse.Namespace(order=20), [_naptr1, _naptr2, _naptr5]), + (argparse.Namespace(flag="U"), [_naptr1, _naptr2, _naptr4]), + (argparse.Namespace(service="SIP+D2U"), [_naptr1, _naptr2, _naptr5]), + (argparse.Namespace(regex=""), [_naptr1, _naptr3, _naptr4]), + (argparse.Namespace(preference=10, order=20), [_naptr1, _naptr2]), + (argparse.Namespace(flag="U", service="E2U+SIP"), [_naptr4]), + (argparse.Namespace(regex="!^.*$!sip:info@example.com!"), [_naptr2, _naptr5]), + ], +) +def test_naptrs_from_args(inp: argparse.Namespace, expected: list[NAPTR]) -> None: + assert naptrs_from_args(naptrs, inp) == expected From 291ea0426fce7ed60e7d527fbafb82e5495ddca4 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 15:03:03 +0200 Subject: [PATCH 4/8] Refactor+rename NAPTR filtering --- mreg_cli/commands/host_submodules/rr.py | 44 ++++++++++++++++++------- tests/commands/test_rr.py | 41 +++++++++++++++-------- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/mreg_cli/commands/host_submodules/rr.py b/mreg_cli/commands/host_submodules/rr.py index 07806a17..32d7d97d 100644 --- a/mreg_cli/commands/host_submodules/rr.py +++ b/mreg_cli/commands/host_submodules/rr.py @@ -367,17 +367,31 @@ def naptr_add(args: argparse.Namespace) -> None: OutputManager().add_ok(f"Added NAPTR record to {host.name}.") -def naptrs_from_args(naptrs: list[NAPTR], args: argparse.Namespace) -> list[NAPTR]: - """Filter naptrs based on provided args.""" - attrs = ("preference", "order", "flag", "service", "regex", "replacement") - active_args = [attr for attr in attrs if getattr(args, attr, None) is not None] - - def naptr_matches(naptr: NAPTR) -> bool: - return all( - getattr(args, attribute) == getattr(naptr, attribute) for attribute in active_args +def filter_naptrs( + naptrs: list[NAPTR], + preference: int, + order: int, + flag: str | None, + service: str | None, + regex: str | None, + replacement: str, +) -> list[NAPTR]: + """Filter NAPTRs by exact match on all fields.""" + return [ + naptr + for naptr in naptrs + if ( + # NOTE: these comparisons assume NAPTRs will never have None values, + # as None cannot be represented with the current naptr_remove flags. + # Is the model wrong, or are our filtering assumptions wrong? + naptr.preference == preference + and naptr.order == order + and naptr.flag == flag + and naptr.service == service + and naptr.regex == regex + and naptr.replacement == replacement ) - - return [naptr for naptr in naptrs if naptr_matches(naptr)] + ] @command_registry.register_command( @@ -423,7 +437,15 @@ def naptr_remove(args: argparse.Namespace) -> None: :param args: argparse.Namespace (name, preference, order, flag, service, regex, replacement) """ host = Host.get_by_any_means_or_raise(args.name) - to_delete = naptrs_from_args(host.naptrs, args) + to_delete = filter_naptrs( + host.naptrs, + preference=args.preference, + order=args.order, + flag=args.flag, + service=args.service, + regex=args.regex, + replacement=args.replacement, + ) if not to_delete: raise EntityNotFound(f"No matching NAPTR record found for {host}") diff --git a/tests/commands/test_rr.py b/tests/commands/test_rr.py index aa629be8..df666ebe 100644 --- a/tests/commands/test_rr.py +++ b/tests/commands/test_rr.py @@ -1,8 +1,11 @@ -import argparse +from __future__ import annotations + import datetime -from mreg_api.models import NAPTR -from mreg_cli.commands.host_submodules.rr import naptrs_from_args + import pytest +from mreg_api.models import NAPTR + +from mreg_cli.commands.host_submodules.rr import filter_naptrs _created_at = datetime.datetime(2024, 1, 1, 0, 0, 0) _updated_at = datetime.datetime(2025, 1, 1, 0, 0, 0) @@ -91,17 +94,27 @@ @pytest.mark.parametrize( - "inp,expected", + "preference,order,flag,service,regex,replacement,expected", [ - (argparse.Namespace(preference=10), [_naptr1, _naptr2, _naptr3]), - (argparse.Namespace(order=20), [_naptr1, _naptr2, _naptr5]), - (argparse.Namespace(flag="U"), [_naptr1, _naptr2, _naptr4]), - (argparse.Namespace(service="SIP+D2U"), [_naptr1, _naptr2, _naptr5]), - (argparse.Namespace(regex=""), [_naptr1, _naptr3, _naptr4]), - (argparse.Namespace(preference=10, order=20), [_naptr1, _naptr2]), - (argparse.Namespace(flag="U", service="E2U+SIP"), [_naptr4]), - (argparse.Namespace(regex="!^.*$!sip:info@example.com!"), [_naptr2, _naptr5]), + (10, 20, "U", "SIP+D2U", "", "naptr1.example.com", [_naptr1]), + (10, 20, "U", "SIP+D2U", "!^.*$!sip:info@example.com!", "naptr2.example.com", [_naptr2]), + (10, 30, "S", "E2U+SIP", "", "naptr3.example.com", [_naptr3]), + (20, 10, "U", "E2U+SIP", "", "naptr4.example.com", [_naptr4]), + (20, 20, "S", "SIP+D2U", "!^.*$!sip:info@example.com!", "naptr5.example.com", [_naptr5]), + (30, 10, None, None, None, "naptr6.example.com", [_naptr6]), + (10, 20, "U", "SIP+D2U", "", "naptr-nonexistent.example.com", []), + (99, 20, "U", "SIP+D2U", "", "naptr1.example.com", []), ], ) -def test_naptrs_from_args(inp: argparse.Namespace, expected: list[NAPTR]) -> None: - assert naptrs_from_args(naptrs, inp) == expected +def test_filter_naptrs( + preference: int, + order: int, + flag: str | None, + service: str | None, + regex: str | None, + replacement: str, + expected: list[NAPTR], +) -> None: + assert ( + filter_naptrs(naptrs, preference, order, flag, service, regex, replacement) == expected + ) From 2c4d4209154d126688f9dbdca5a7935aa6e5f195 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 15:20:03 +0200 Subject: [PATCH 5/8] Best-effort deletion --- mreg_cli/commands/host_submodules/rr.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mreg_cli/commands/host_submodules/rr.py b/mreg_cli/commands/host_submodules/rr.py index 32d7d97d..eacd2def 100644 --- a/mreg_cli/commands/host_submodules/rr.py +++ b/mreg_cli/commands/host_submodules/rr.py @@ -42,7 +42,6 @@ import argparse from mreg_api.models import ( - MX, NAPTR, SSHFP, TXT, @@ -56,7 +55,6 @@ Srv, ) from mreg_api.models.fields import HostName -from prompt_toolkit.output import Output from mreg_cli.commands.host import registry as command_registry from mreg_cli.exceptions import ( @@ -67,6 +65,7 @@ ForceMissing, InputFailure, PatchError, + handle_exception, ) from mreg_cli.output.host import ( output_hinfo, @@ -457,11 +456,13 @@ def naptr_remove(args: argparse.Namespace) -> None: # This should ideally be done in a transaction, but the API doesn't support it. # Right now we may end up in a situation where some records are deleted and some are not. + # Best-effort lets us delete as many as possible at the very least. for naptr in to_delete: - if naptr.delete(): + try: + naptr.delete() OutputManager().add_ok(f"Deleted NAPTR record from {host.name}.") - else: - raise DeleteError(f"Failed to remove NAPTR for {host}") + except Exception as e: + handle_exception(e) @command_registry.register_command( From 721c9d40ed39d17bc64300b635448d3a68072149 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 17:34:41 +0200 Subject: [PATCH 6/8] Fix optional/blank fields handling --- mreg_cli/commands/host_submodules/rr.py | 19 ++-- tests/commands/test_rr.py | 115 ++++++++++++++++++++---- uv.lock | 6 +- 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/mreg_cli/commands/host_submodules/rr.py b/mreg_cli/commands/host_submodules/rr.py index eacd2def..23060906 100644 --- a/mreg_cli/commands/host_submodules/rr.py +++ b/mreg_cli/commands/host_submodules/rr.py @@ -375,20 +375,19 @@ def filter_naptrs( regex: str | None, replacement: str, ) -> list[NAPTR]: - """Filter NAPTRs by exact match on all fields.""" + """Filter NAPTRs, matching on all required fields and any optional fields that are provided.""" return [ naptr for naptr in naptrs if ( - # NOTE: these comparisons assume NAPTRs will never have None values, - # as None cannot be represented with the current naptr_remove flags. - # Is the model wrong, or are our filtering assumptions wrong? naptr.preference == preference and naptr.order == order - and naptr.flag == flag - and naptr.service == service - and naptr.regex == regex and naptr.replacement == replacement + # These 3 fields can be blank, and we need to + # know if we should filter on them or not based on user input + and (flag is None or naptr.flag == flag) + and (service is None or naptr.service == service) + and (regex is None or naptr.regex == regex) ) ] @@ -418,15 +417,15 @@ def filter_naptrs( required=True, metavar="ORDER", ), - Flag("-flag", description="NAPTR flag.", required=True, metavar="FLAG"), - Flag("-service", description="NAPTR service.", required=True, metavar="SERVICE"), - Flag("-regex", description="NAPTR regexp.", required=True, metavar="REGEXP"), Flag( "-replacement", description="NAPTR replacement.", required=True, metavar="REPLACEMENT", ), + Flag("-flag", description="NAPTR flag.", default=None, metavar="FLAG"), + Flag("-service", description="NAPTR service.", default=None, metavar="SERVICE"), + Flag("-regex", description="NAPTR regexp.", default=None, metavar="REGEXP"), Flag("-force", action="store_true", description="Force deletion for multiple records."), ], ) diff --git a/tests/commands/test_rr.py b/tests/commands/test_rr.py index df666ebe..8286ff0e 100644 --- a/tests/commands/test_rr.py +++ b/tests/commands/test_rr.py @@ -11,12 +11,13 @@ _updated_at = datetime.datetime(2025, 1, 1, 0, 0, 0) # Overlap matrix (field -> ids sharing same value): -# host: 123=[1,2,3] 456=[4,5] 789=[6] -# preference: 10=[1,2,3] 20=[4,5] 30=[6] -# order: 20=[1,2,5] 10=[4,6] 30=[3] -# flag: "U"=[1,2,4] "S"=[3,5] None=[6] -# service: "SIP+D2U"=[1,2,5] "E2U+SIP"=[3,4] None=[6] -# regex: ""=[1,3,4] "!^.*$!..."=[2,5] None=[6] +# host: 123=[1,2,3] 456=[4,5] 789=[6] 999=[7,8,9,10] +# preference: 10=[1,2,3] 20=[4,5] 30=[6] 40=[7,8,9,10] +# order: 20=[1,2,5] 10=[4,6] 30=[3] 15=[7,8,9,10] +# flag: "U"=[1,2,4,7,9,10] "S"=[3,5,8] ""=[6] +# service: "SIP+D2U"=[1,2,5] "E2U+SIP"=[3,4] ""=[6] "X"=[7,8,10] "Y"=[9] +# regex: ""=[1,3,4,6] "!^.*$!..."=[2,5] "r1"=[7,8,9] "r2"=[10] +# replacement: "multi.example.com"=[7,8,9,10] _naptr1 = NAPTR( id=1, host=123, @@ -82,15 +83,79 @@ host=789, preference=30, order=10, - flag=None, - service=None, - regex=None, + flag="", + service="", + regex="", replacement="naptr6.example.com", created_at=_created_at, updated_at=_updated_at, ) -naptrs = [_naptr1, _naptr2, _naptr3, _naptr4, _naptr5, _naptr6] +# NAPTRs 7-10 share preference/order/replacement to exercise multi-match via optional fields. +# flag varies: 7,9,10="U" 8="S" +# service varies: 7,8,10="X" 9="Y" +# regex varies: 7,8,9="r1" 10="r2" +_naptr7 = NAPTR( + id=7, + host=999, + preference=40, + order=15, + flag="U", + service="X", + regex="r1", + replacement="multi.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr8 = NAPTR( + id=8, + host=999, + preference=40, + order=15, + flag="S", + service="X", + regex="r1", + replacement="multi.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr9 = NAPTR( + id=9, + host=999, + preference=40, + order=15, + flag="U", + service="Y", + regex="r1", + replacement="multi.example.com", + created_at=_created_at, + updated_at=_updated_at, +) +_naptr10 = NAPTR( + id=10, + host=999, + preference=40, + order=15, + flag="U", + service="X", + regex="r2", + replacement="multi.example.com", + created_at=_created_at, + updated_at=_updated_at, +) + +naptrs = [ + _naptr1, + _naptr2, + _naptr3, + _naptr4, + _naptr5, + _naptr6, + _naptr7, + _naptr8, + _naptr9, + _naptr10, +] @pytest.mark.parametrize( @@ -101,12 +166,34 @@ (10, 30, "S", "E2U+SIP", "", "naptr3.example.com", [_naptr3]), (20, 10, "U", "E2U+SIP", "", "naptr4.example.com", [_naptr4]), (20, 20, "S", "SIP+D2U", "!^.*$!sip:info@example.com!", "naptr5.example.com", [_naptr5]), - (30, 10, None, None, None, "naptr6.example.com", [_naptr6]), + (30, 10, "", "", "", "naptr6.example.com", [_naptr6]), + (40, 15, "U", "X", "r1", "multi.example.com", [_naptr7]), (10, 20, "U", "SIP+D2U", "", "naptr-nonexistent.example.com", []), (99, 20, "U", "SIP+D2U", "", "naptr1.example.com", []), ], ) -def test_filter_naptrs( +def test_filter_naptrs_single( + preference: int, + order: int, + flag: str | None, + service: str | None, + regex: str | None, + replacement: str, + expected: list[NAPTR], +) -> None: + assert filter_naptrs(naptrs, preference, order, flag, service, regex, replacement) == expected + + +@pytest.mark.parametrize( + "preference,order,flag,service,regex,replacement,expected", + [ + (40, 15, None, "X", "r1", "multi.example.com", [_naptr7, _naptr8]), + (40, 15, "U", None, "r1", "multi.example.com", [_naptr7, _naptr9]), + (40, 15, "U", "X", None, "multi.example.com", [_naptr7, _naptr10]), + (40, 15, None, None, None, "multi.example.com", [_naptr7, _naptr8, _naptr9, _naptr10]), + ], +) +def test_filter_naptrs_multi( preference: int, order: int, flag: str | None, @@ -115,6 +202,4 @@ def test_filter_naptrs( replacement: str, expected: list[NAPTR], ) -> None: - assert ( - filter_naptrs(naptrs, preference, order, flag, service, regex, replacement) == expected - ) + assert filter_naptrs(naptrs, preference, order, flag, service, regex, replacement) == expected diff --git a/uv.lock b/uv.lock index b40038ae..8cda67eb 100644 --- a/uv.lock +++ b/uv.lock @@ -263,7 +263,7 @@ wheels = [ [[package]] name = "mreg-api" -version = "0.2.0" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "diskcache" }, @@ -273,9 +273,9 @@ dependencies = [ { name = "pydantic-settings" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e8/2577d1d9b37fe6da434ce0341d4ee9c9ef89fa9b1c86619d8ad8210f8ad2/mreg_api-0.2.0.tar.gz", hash = "sha256:4bbfcca6eaff604407a1c593aff54a840dfab49eb0597926ff24eb03b6185c81", size = 154556, upload-time = "2026-04-16T10:40:58.927Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/5c/41bdf1a040a172cd48acf54b7bf84c1e361c7567d438fa855daa977b83f2/mreg_api-0.2.2.tar.gz", hash = "sha256:b1334ddb35a4852a9f71ec73419728cd0f63c2c1a5d5f1ff2eae2e78c4828026", size = 153973, upload-time = "2026-04-23T15:17:38.65Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/6f/e9bd687a8b99aa3636278926ad15924b8f8ff84306788a45861bde301078/mreg_api-0.2.0-py3-none-any.whl", hash = "sha256:1266cfdb2460616a9ed7b5e8b39bbaa038f89e253a400b7f28bbd18efa66543c", size = 84334, upload-time = "2026-04-16T10:40:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/65/a8/e3b222be5ca60ef214d726e58bdf166fa2135d01f531aefcf2148ad6ebec/mreg_api-0.2.2-py3-none-any.whl", hash = "sha256:9cde2424c36221301f04a7dec6eea2214a8c20f16ff14141087435ba35446af8", size = 83421, upload-time = "2026-04-23T15:17:37.324Z" }, ] [[package]] From 874038cca89c156c178ed29abd1d1ff87336d7c9 Mon Sep 17 00:00:00 2001 From: pederhan Date: Thu, 23 Apr 2026 17:37:36 +0200 Subject: [PATCH 7/8] Fix invalid NAPTR definiton in test --- tests/commands/test_host.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/commands/test_host.py b/tests/commands/test_host.py index 090dedc0..2778ecd1 100644 --- a/tests/commands/test_host.py +++ b/tests/commands/test_host.py @@ -77,6 +77,9 @@ def test_override_from_string() -> None: preference=1, order=1, replacement="naptr.example.com", + flag="U", + service="SIP+D2U", + regex="", ), "naptr.example.com", ), From ec0868a3a4233dcee69fbbfed91a72b72e1752cb Mon Sep 17 00:00:00 2001 From: pederhan Date: Fri, 24 Apr 2026 14:33:33 +0200 Subject: [PATCH 8/8] Add tests --- ci/testsuite | 5 + ci/testsuite-result.json | 343 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 348 insertions(+) diff --git a/ci/testsuite b/ci/testsuite index 570a36ba..50b6186c 100644 --- a/ci/testsuite +++ b/ci/testsuite @@ -315,6 +315,11 @@ host mx_remove baz 10 mail.example.org host naptr_add -name baz -preference 16384 -order 3 -flag u -service "SIP" -regex "[abc]+" -replacement "wonk" host naptr_show baz host naptr_remove -name baz -preference 16384 -order 3 -flag u -service "sip" -regex "[abc]+" -replacement "wonk" +## Add 2 nearly identical records with different services +host naptr_add -name baz -preference 101 -order 4 -flag s -service http+I2R -regex "" -replacement . +host naptr_add -name baz -preference 101 -order 4 -flag s -service ftp+I2R -regex "" -replacement . +host naptr_remove -name baz -preference 101 -order 4 -replacement . # fails, too many matches +host naptr_remove -name baz -preference 101 -order 4 -replacement . -force # PTR host ptr_add 10.0.0.20 baz.example.org host ptr_show 10.0.0.20 diff --git a/ci/testsuite-result.json b/ci/testsuite-result.json index 8a626759..75a8acec 100644 --- a/ci/testsuite-result.json +++ b/ci/testsuite-result.json @@ -27487,6 +27487,349 @@ ], "time": null }, + { + "command": "host naptr_add -name baz -preference 101 -order 4 -flag s -service http+I2R -regex \"\" -replacement .", + "command_filter": null, + "command_filter_negate": false, + "command_issued": "host naptr_add -name baz -preference 101 -order 4 -flag s -service http+I2R -regex \"\" -replacement .", + "ok": [ + "Added NAPTR record to baz.example.org." + ], + "warning": [], + "error": [], + "output": [], + "api_requests": [ + { + "method": "GET", + "url": "/api/v1/hosts/baz.example.org", + "data": {}, + "status": 200, + "response": { + "ipaddresses": [ + { + "macaddress": "11:22:33:aa:bb:cc", + "created_at": "2026-04-24T13:14:13.137107+02:00", + "updated_at": "2026-04-24T13:14:23.189953+02:00", + "ipaddress": "10.0.0.10", + "host": 20 + }, + { + "macaddress": "11:22:33:44:55:67", + "created_at": "2026-04-24T13:14:24.505828+02:00", + "updated_at": "2026-04-24T13:14:27.229134+02:00", + "ipaddress": "2001:db8::14", + "host": 20 + } + ], + "cnames": [], + "mxs": [], + "txts": [ + { + "created_at": "2026-04-24T13:14:22.395738+02:00", + "updated_at": "2026-04-24T13:14:22.395797+02:00", + "txt": "v=spf1 -all", + "host": 20 + } + ], + "ptr_overrides": [], + "srvs": [], + "naptrs": [], + "sshfps": [], + "hostgroups": [], + "roles": [], + "hinfo": null, + "loc": null, + "bacnetid": null, + "communities": [], + "contacts": [], + "created_at": "2026-04-24T13:14:22.378394+02:00", + "updated_at": "2026-04-24T13:14:22.378456+02:00", + "name": "baz.example.org", + "ttl": null, + "comment": "", + "zone": 1, + "contact": "" + } + }, + { + "method": "GET", + "url": "/api/v1/naptrs/?preference=101&order=4&flag=s&service=http%2BI2R®ex=&replacement=.&host=20", + "data": {}, + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } + }, + { + "method": "POST", + "url": "/api/v1/naptrs/", + "data": { + "preference": 101, + "order": 4, + "flag": "s", + "service": "http+I2R", + "regex": "", + "replacement": ".", + "host": 20 + }, + "status": 201, + "response": { + "created_at": "2026-04-24T13:14:31.810704+02:00", + "updated_at": "2026-04-24T13:14:31.810798+02:00", + "preference": 101, + "order": 4, + "flag": "s", + "service": "http+I2R", + "regex": "", + "replacement": ".", + "host": 20 + } + } + ], + "time": null + }, + { + "command": "host naptr_add -name baz -preference 101 -order 4 -flag s -service ftp+I2R -regex \"\" -replacement .", + "command_filter": null, + "command_filter_negate": false, + "command_issued": "host naptr_add -name baz -preference 101 -order 4 -flag s -service ftp+I2R -regex \"\" -replacement .", + "ok": [ + "Added NAPTR record to baz.example.org." + ], + "warning": [], + "error": [], + "output": [], + "api_requests": [ + { + "method": "GET", + "url": "/api/v1/hosts/baz.example.org", + "data": {}, + "status": 200, + "response": { + "ipaddresses": [ + { + "macaddress": "11:22:33:aa:bb:cc", + "created_at": "2026-04-24T13:14:13.137107+02:00", + "updated_at": "2026-04-24T13:14:23.189953+02:00", + "ipaddress": "10.0.0.10", + "host": 20 + }, + { + "macaddress": "11:22:33:44:55:67", + "created_at": "2026-04-24T13:14:24.505828+02:00", + "updated_at": "2026-04-24T13:14:27.229134+02:00", + "ipaddress": "2001:db8::14", + "host": 20 + } + ], + "cnames": [], + "mxs": [], + "txts": [ + { + "created_at": "2026-04-24T13:14:22.395738+02:00", + "updated_at": "2026-04-24T13:14:22.395797+02:00", + "txt": "v=spf1 -all", + "host": 20 + } + ], + "ptr_overrides": [], + "srvs": [], + "naptrs": [ + { + "created_at": "2026-04-24T13:14:31.810704+02:00", + "updated_at": "2026-04-24T13:14:31.810798+02:00", + "preference": 101, + "order": 4, + "flag": "s", + "service": "http+i2r", + "regex": "", + "replacement": ".", + "host": 20 + } + ], + "sshfps": [], + "hostgroups": [], + "roles": [], + "hinfo": null, + "loc": null, + "bacnetid": null, + "communities": [], + "contacts": [], + "created_at": "2026-04-24T13:14:22.378394+02:00", + "updated_at": "2026-04-24T13:14:22.378456+02:00", + "name": "baz.example.org", + "ttl": null, + "comment": "", + "zone": 1, + "contact": "" + } + }, + { + "method": "GET", + "url": "/api/v1/naptrs/?preference=101&order=4&flag=s&service=ftp%2BI2R®ex=&replacement=.&host=20", + "data": {}, + "status": 200, + "response": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } + }, + { + "method": "POST", + "url": "/api/v1/naptrs/", + "data": { + "preference": 101, + "order": 4, + "flag": "s", + "service": "ftp+I2R", + "regex": "", + "replacement": ".", + "host": 20 + }, + "status": 201, + "response": { + "created_at": "2026-04-24T13:14:32.141394+02:00", + "updated_at": "2026-04-24T13:14:32.141451+02:00", + "preference": 101, + "order": 4, + "flag": "s", + "service": "ftp+I2R", + "regex": "", + "replacement": ".", + "host": 20 + } + } + ], + "time": null + }, + { + "command": "host naptr_remove -name baz -preference 101 -order 4 -replacement .", + "command_filter": null, + "command_filter_negate": false, + "command_issued": "host naptr_remove -name baz -preference 101 -order 4 -replacement . # fails, too many matches", + "ok": [], + "warning": [ + "Use --force to delete all matching records." + ], + "error": [], + "output": [ + "Found multiple matching NAPTR records:", + "NAPTRs: Preference Order Flag Service Regex Replacement ", + " 101 4 s ftp+i2r \"\" . ", + " 101 4 s http+i2r \"\" . " + ], + "api_requests": [ + { + "method": "GET", + "url": "/api/v1/hosts/baz.example.org", + "data": {}, + "status": 200, + "response": { + "ipaddresses": [ + { + "macaddress": "11:22:33:aa:bb:cc", + "created_at": "2026-04-24T13:14:13.137107+02:00", + "updated_at": "2026-04-24T13:14:23.189953+02:00", + "ipaddress": "10.0.0.10", + "host": 20 + }, + { + "macaddress": "11:22:33:44:55:67", + "created_at": "2026-04-24T13:14:24.505828+02:00", + "updated_at": "2026-04-24T13:14:27.229134+02:00", + "ipaddress": "2001:db8::14", + "host": 20 + } + ], + "cnames": [], + "mxs": [], + "txts": [ + { + "created_at": "2026-04-24T13:14:22.395738+02:00", + "updated_at": "2026-04-24T13:14:22.395797+02:00", + "txt": "v=spf1 -all", + "host": 20 + } + ], + "ptr_overrides": [], + "srvs": [], + "naptrs": [ + { + "created_at": "2026-04-24T13:14:32.141394+02:00", + "updated_at": "2026-04-24T13:14:32.141451+02:00", + "preference": 101, + "order": 4, + "flag": "s", + "service": "ftp+i2r", + "regex": "", + "replacement": ".", + "host": 20 + }, + { + "created_at": "2026-04-24T13:14:31.810704+02:00", + "updated_at": "2026-04-24T13:14:31.810798+02:00", + "preference": 101, + "order": 4, + "flag": "s", + "service": "http+i2r", + "regex": "", + "replacement": ".", + "host": 20 + } + ], + "sshfps": [], + "hostgroups": [], + "roles": [], + "hinfo": null, + "loc": null, + "bacnetid": null, + "communities": [], + "contacts": [], + "created_at": "2026-04-24T13:14:22.378394+02:00", + "updated_at": "2026-04-24T13:14:22.378456+02:00", + "name": "baz.example.org", + "ttl": null, + "comment": "", + "zone": 1, + "contact": "" + } + } + ], + "time": null + }, + { + "command": "host naptr_remove -name baz -preference 101 -order 4 -replacement . -force", + "command_filter": null, + "command_filter_negate": false, + "command_issued": "host naptr_remove -name baz -preference 101 -order 4 -replacement . -force", + "ok": [ + "Deleted NAPTR record from baz.example.org.", + "Deleted NAPTR record from baz.example.org." + ], + "warning": [], + "error": [], + "output": [], + "api_requests": [ + { + "method": "DELETE", + "url": "/api/v1/naptrs/3", + "data": {}, + "status": 204 + }, + { + "method": "DELETE", + "url": "/api/v1/naptrs/2", + "data": {}, + "status": 204 + } + ], + "time": null + }, { "command": "host ptr_add 10.0.0.20 baz.example.org", "command_filter": null,