From 90ac4408ef7709d4f2b11b633df5da9c73701b3c Mon Sep 17 00:00:00 2001 From: Iman Bakhtiari Date: Sat, 27 Dec 2025 15:43:22 +0330 Subject: [PATCH] Add exact-match option to rr_list and ndcli --- dim-testsuite/tests/dns_test.py | 10 +++++ dim/CHANGES | 2 +- dim/dim/rpc.py | 67 +++++++++++++++++++++------------ dim/doc/api.rst | 1 + ndcli/CHANGES | 1 + ndcli/dimcli/__init__.py | 4 +- ndcli/tests/dimcli_test.py | 12 ++++++ 7 files changed, 71 insertions(+), 26 deletions(-) diff --git a/dim-testsuite/tests/dns_test.py b/dim-testsuite/tests/dns_test.py index e0fae3ee..a1fcf6f0 100644 --- a/dim-testsuite/tests/dns_test.py +++ b/dim-testsuite/tests/dns_test.py @@ -403,6 +403,16 @@ def test_create_cname(self): with raises(InvalidParameterError): self.r.rr_create(name='d.test.com.', type='MX', preference=10, exchange='a.test.com.') + def test_rr_list_exact(self): + self.r.zone_create('test.com') + self.r.rr_create(name='*.test.com.', type='A', ip='12.0.0.1') + self.r.rr_create(name='a.test.com.', type='A', ip='12.0.0.2') + wildcard = rrs(self.r.rr_list(pattern='*.test.com.')) + assert ('*', 'test.com', 'A', '12.0.0.1') in wildcard + assert ('a', 'test.com', 'A', '12.0.0.2') in wildcard + assert rrs(self.r.rr_list(pattern='*.test.com.', exact=True)) == rrs([ + ('*', 'test.com', 'A', '12.0.0.1')]) + def test_create_cname_2(self): # ND-100 self.r.zone_create('test.com') diff --git a/dim/CHANGES b/dim/CHANGES index 19cfe7c4..ec6dc2a6 100644 --- a/dim/CHANGES +++ b/dim/CHANGES @@ -6,6 +6,7 @@ * fix missing check if pool is already in target layer3domain (#232) * fix missing check to avoid creating A or AAAA records in zone profiles (#238) * update dependencies (#248) +* add exact match option to :func:`rr_list`/``rr_list2`` to allow listing literal wildcard records (#298) 5.0.1 ----- @@ -428,4 +429,3 @@ API Changes: * *vlan* option for :func:`ippool_create`, :func:`ippool_set_vlan` * *priority* parameter for :func:`subnet_set_priority` * *prefix*, *maxsplit* parameters for :func:`ippool_get_delegation` - diff --git a/dim/dim/rpc.py b/dim/dim/rpc.py index 3617c73b..a798b9cb 100644 --- a/dim/dim/rpc.py +++ b/dim/dim/rpc.py @@ -2286,7 +2286,8 @@ def rr_set_ttl(self, ttl=None, view=None, **kwargs): @readonly def rr_list(self, pattern=None, type=None, zone=None, view=None, profile=False, limit=None, offset=0, - value_as_object=False, fields=False, created_by=None, modified_by=None, layer3domain=None): + value_as_object=False, fields=False, created_by=None, modified_by=None, layer3domain=None, + exact=False): qfields = [RR.name, Zone.name.label('zone'), ZoneView.name.label('view'), RR.ttl, RR.type, RR.value, Layer3Domain.name.label('layer3domain')] if fields: @@ -2320,22 +2321,31 @@ def rr_list(self, pattern=None, type=None, zone=None, view=None, profile=False, rr_query = rr_query.filter(RR.modified_by == modified_by) if soa_query: soa_query = soa_query.filter(ZoneView.modified_by == modified_by) - try: - ip = IP(pattern) - except: - ip = None + ip = None + if pattern is not None and not exact: + try: + ip = IP(pattern) + except: + ip = None if ip: rr_query = rr_query.filter(inside(Ipblock.address, ip)) soa_query = None elif pattern is not None: - if len(pattern) > 0 and pattern[-1] not in ['*', '?', '.']: - pattern += '.' - wildcard = make_wildcard(pattern) columns = [RR.name, RR.target] - rr_query = rr_query.filter(or_(*[col.like(wildcard) for col in columns])) - if soa_query is not None: - soa_query = soa_query.filter(or_((Zone.name + '.').like(wildcard), - ZoneView.primary.like(wildcard))) + search_value = pattern + if len(search_value) > 0 and search_value[-1] not in ['*', '?', '.']: + search_value += '.' + if exact: + rr_query = rr_query.filter(or_(*[col == search_value for col in columns])) + if soa_query is not None: + soa_query = soa_query.filter(or_((Zone.name + '.') == search_value, + ZoneView.primary == search_value)) + else: + wildcard = make_wildcard(search_value) + rr_query = rr_query.filter(or_(*[col.like(wildcard) for col in columns])) + if soa_query is not None: + soa_query = soa_query.filter(or_((Zone.name + '.').like(wildcard), + ZoneView.primary.like(wildcard))) # We can't limit reverse zones records because we can't do the same sorting in the query if limit is not None and not reverse_zone_sorting: limit = int(limit) @@ -2430,7 +2440,7 @@ def split_by_ip(dict_list): @readonly def rr_list2(self, pattern=None, type=None, zone=None, view=None, profile=False, limit=None, offset=0, value_as_object=False, fields=False, created_by=None, modified_by=None, layer3domain=None, - type_sort='asc', sort_by='record', order='asc'): + type_sort='asc', sort_by='record', order='asc', exact=False): qfields = [RR.name, Zone.name.label('zone'), ZoneView.name.label('view'), RR.ttl, RR.type, RR.value, Layer3Domain.name.label('layer3domain')] if fields: @@ -2464,22 +2474,31 @@ def rr_list2(self, pattern=None, type=None, zone=None, view=None, profile=False, rr_query = rr_query.filter(RR.modified_by == modified_by) if soa_query: soa_query = soa_query.filter(ZoneView.modified_by == modified_by) - try: - ip = IP(pattern) - except: - ip = None + ip = None + if pattern is not None and not exact: + try: + ip = IP(pattern) + except: + ip = None if ip: rr_query = rr_query.filter(inside(Ipblock.address, ip)) soa_query = None elif pattern is not None: - if len(pattern) > 0 and pattern[-1] not in ['*', '?', '.']: - pattern += '.' - wildcard = make_wildcard(pattern) columns = [RR.name, RR.target] - rr_query = rr_query.filter(or_(*[col.like(wildcard) for col in columns])) - if soa_query is not None: - soa_query = soa_query.filter(or_((Zone.name + '.').like(wildcard), - ZoneView.primary.like(wildcard))) + search_value = pattern + if len(search_value) > 0 and search_value[-1] not in ['*', '?', '.']: + search_value += '.' + if exact: + rr_query = rr_query.filter(or_(*[col == search_value for col in columns])) + if soa_query is not None: + soa_query = soa_query.filter(or_((Zone.name + '.') == search_value, + ZoneView.primary == search_value)) + else: + wildcard = make_wildcard(search_value) + rr_query = rr_query.filter(or_(*[col.like(wildcard) for col in columns])) + if soa_query is not None: + soa_query = soa_query.filter(or_((Zone.name + '.').like(wildcard), + ZoneView.primary.like(wildcard))) rr_count = fast_count(rr_query) soa_count = 0 diff --git a/dim/doc/api.rst b/dim/doc/api.rst index ab5dcfdb..004b8a25 100644 --- a/dim/doc/api.rst +++ b/dim/doc/api.rst @@ -1179,6 +1179,7 @@ RR Functions - *limit*: limit the amount of results - *offset*: skip the first *offset* results - *pattern* (string): pattern to match against the RR name or IP address. A relative pattern will be converted into an absolute one. + - *exact* (boolean): if true, the *pattern* is treated as a literal RR name instead of a wildcard/glob - *type* (string): filter by RR type - *zone* (string): filter by RR zone - *view* (string): :ref:`view_option` diff --git a/ndcli/CHANGES b/ndcli/CHANGES index 637d93a7..d88eb3e2 100644 --- a/ndcli/CHANGES +++ b/ndcli/CHANGES @@ -1,6 +1,7 @@ 5.0.4 ----- * fix TXT records when importing zone (#292) +* add ``--exact`` switch to ``list rrs`` for literal wildcard records (#298) 5.0.3 ----- diff --git a/ndcli/dimcli/__init__.py b/ndcli/dimcli/__init__.py index 07acf911..383a4883 100644 --- a/ndcli/dimcli/__init__.py +++ b/ndcli/dimcli/__init__.py @@ -2541,6 +2541,7 @@ def list_zoneprofiles(self, args): Argument('wildcard'), rr_type_arg, layer3domain_group, + Option(None, 'exact', action='store_true', help='match only exact record names'), script_option) def list_rrs(self, args): ''' @@ -2549,7 +2550,8 @@ def list_rrs(self, args): options = OptionDict(pattern=args.wildcard) # do not use get_layer3domain() as CNAMEs etc. do not have a layer3domain options.set_if(type=args.type, - layer3domain=args.get('layer3domain')) + layer3domain=args.get('layer3domain'), + exact=args.exact) logger.info("Result for list rrs %s" % args.wildcard) rrs = self.client.rr_list(**options) _print_table(['record', {}, diff --git a/ndcli/tests/dimcli_test.py b/ndcli/tests/dimcli_test.py index c3b99f8d..34cc8eaa 100644 --- a/ndcli/tests/dimcli_test.py +++ b/ndcli/tests/dimcli_test.py @@ -228,6 +228,18 @@ def test_create_rr_cname(): assert ndcli('delete zone test.com --cleanup').ok +def test_list_rrs_exact(): + assert ndcli('create zone test.com').ok + assert ndcli('create rr *.test.com. a 12.0.0.1').ok + assert ndcli('create rr a.test.com. a 12.0.0.2').ok + wildcard = nosoa(ndcli('list rrs *.test.com. -H').table) + assert ['*', 'test.com', 'default', '', 'A', '12.0.0.1'] in wildcard + assert ['a', 'test.com', 'default', '', 'A', '12.0.0.2'] in wildcard + assert nosoa(ndcli('list rrs *.test.com. --exact -H').table) == [ + ['*', 'test.com', 'default', '', 'A', '12.0.0.1']] + assert ndcli('delete zone test.com --cleanup').ok + + def test_show_rr(): # ND-92 assert ndcli('create zone test.com').ok