diff --git a/dim-testsuite/t/rr-create-dname-1.t b/dim-testsuite/t/rr-create-dname-1.t new file mode 100644 index 00000000..ee79bbb0 --- /dev/null +++ b/dim-testsuite/t/rr-create-dname-1.t @@ -0,0 +1,98 @@ +$ ndcli create zone old.example.com +WARNING - Creating zone old.example.com without profile +WARNING - Primary NS for this Domain is now localhost. +$ ndcli create zone new.example.com +WARNING - Creating zone new.example.com without profile +WARNING - Primary NS for this Domain is now localhost. + +# Test creating a basic DNAME record +$ ndcli create rr dept.old.example.com. dname dept.new.example.com. -q + +# Test creating DNAME with TTL and comment +$ ndcli create rr sales.old.example.com. ttl 1800 dname sales.new.example.com. --comment "Sales department redirect" -q + +# Test DNAME cannot be created at zone apex (RFC 6672 requirement) +$ ndcli create rr old.example.com. dname new.example.com. +ERROR - It is not allowed to create a DNAME for a zone + +# Test DNAME conflicts with other records at same name +$ ndcli create rr conflict.old.example.com. a 1.2.3.4 -q +$ ndcli create rr conflict.old.example.com. dname conflict.new.example.com. +ERROR - conflict.old.example.com. DNAME conflict.new.example.com. cannot be created because other RRs with the same name exist + +# Test other records cannot be created at same name as DNAME +$ ndcli create rr dept.old.example.com. a 1.2.3.5 +ERROR - dept.old.example.com. A 1.2.3.5 cannot be created because a DNAME with the same name exists + +# Test records cannot be created under DNAME subtree +$ ndcli create rr hr.dept.old.example.com. a 1.2.3.6 +ERROR - hr.dept.old.example.com. A 1.2.3.6 cannot be created under DNAME subtree dept.old.example.com. + +# Test DNAME cannot be created if records exist under the subtree +$ ndcli create rr marketing.test.old.example.com. a 1.2.3.7 -q +$ ndcli create rr test.old.example.com. dname test.new.example.com. +ERROR - test.old.example.com. DNAME test.new.example.com. cannot be created because RRs exist under the DNAME subtree + +# Clean up the conflicting record and create the DNAME +$ ndcli delete rr marketing.test.old.example.com. a -q +$ ndcli create rr test.old.example.com. dname test.new.example.com. -q + +# Test multiple DNAME records can coexist if they don't conflict +$ ndcli create rr finance.old.example.com. dname finance.new.example.com. -q + +# Test showing DNAME records +$ ndcli show rr dept.old.example.com. dname +created:2012-11-14 11:03:02 +created_by:user +modified:2012-11-14 11:03:02 +modified_by:user +rr:dept DNAME dept.new.example.com. +zone:old.example.com + +# Test showing DNAME record with comment +$ ndcli show rr sales.old.example.com. dname +comment:Sales department redirect +created:2012-11-14 11:03:02 +created_by:user +modified:2012-11-14 11:03:02 +modified_by:user +rr:sales 1800 DNAME sales.new.example.com. +ttl:1800 +zone:old.example.com + +# Test listing zone with DNAME records +$ ndcli list zone old.example.com +record zone ttl type value +@ old.example.com 86400 SOA localhost. hostmaster.old.example.com. 2012111402 14400 3600 605000 86400 +conflict old.example.com A 1.2.3.4 +dept old.example.com DNAME dept.new.example.com. +finance old.example.com DNAME finance.new.example.com. +sales old.example.com 1800 DNAME sales.new.example.com. +test old.example.com DNAME test.new.example.com. + +# Test deleting DNAME records +$ ndcli delete rr sales.old.example.com. dname sales.new.example.com. -q + +# Test deleting DNAME by name only +$ ndcli delete rr finance.old.example.com. dname -q + +# Verify records were deleted +$ ndcli list zone old.example.com +record zone ttl type value +@ old.example.com 86400 SOA localhost. hostmaster.old.example.com. 2012111403 14400 3600 605000 86400 +conflict old.example.com A 1.2.3.4 +dept old.example.com DNAME dept.new.example.com. +test old.example.com DNAME test.new.example.com. + +# Test that records can be created under DNAME subtree after DNAME is deleted +$ ndcli delete rr dept.old.example.com. dname -q +$ ndcli create rr hr.dept.old.example.com. a 1.2.3.8 -q + +# Cleanup +$ ndcli delete zone old.example.com --cleanup +INFO - Deleting RR conflict A 1.2.3.4 from zone old.example.com +INFO - Freeing IP 1.2.3.4 from layer3domain default +INFO - Deleting RR hr.dept A 1.2.3.8 from zone old.example.com +INFO - Freeing IP 1.2.3.8 from layer3domain default +INFO - Deleting RR test DNAME test.new.example.com. from zone old.example.com +$ ndcli delete zone new.example.com --cleanup \ No newline at end of file diff --git a/dim-testsuite/tests/dns_test.py b/dim-testsuite/tests/dns_test.py index e0fae3ee..fa3e1263 100644 --- a/dim-testsuite/tests/dns_test.py +++ b/dim-testsuite/tests/dns_test.py @@ -601,6 +601,96 @@ def test_parse(self): self.r.rr_create(name=rr_name, type='TXT', strings=original) assert self.r.rr_list(rr_name)[0]['value'] == canonical[original] + def test_dname_create(self): + """Test basic DNAME record creation""" + self.r.zone_create('old.example.com') + self.r.zone_create('new.example.com') + + # Create basic DNAME record + self.r.rr_create(name='dept.old.example.com.', type='DNAME', target='dept.new.example.com.') + rrs_result = self.r.rr_list('dept.old.example.com.') + assert len(rrs_result) == 1 + assert rrs_result[0]['type'] == 'DNAME' + assert rrs_result[0]['value'] == 'dept.new.example.com.' + + def test_dname_zone_apex_forbidden(self): + """Test that DNAME cannot be created at zone apex""" + self.r.zone_create('old.example.com') + self.r.zone_create('new.example.com') + + with raises(InvalidParameterError, 'It is not allowed to create a DNAME for a zone'): + self.r.rr_create(name='old.example.com.', type='DNAME', target='new.example.com.') + + def test_dname_conflicts_with_other_records(self): + """Test that DNAME cannot coexist with other records at same name""" + self.r.zone_create('test.com') + + # Create A record first + self.r.rr_create(name='conflict.test.com.', type='A', ip='1.2.3.4') + + # Try to create DNAME at same name - should fail + with raises(InvalidParameterError, 'cannot be created because other RRs with the same name exist'): + self.r.rr_create(name='conflict.test.com.', type='DNAME', target='target.example.com.') + + def test_other_records_conflict_with_dname(self): + """Test that other records cannot be created at same name as DNAME""" + self.r.zone_create('test.com') + + # Create DNAME first + self.r.rr_create(name='dept.test.com.', type='DNAME', target='dept.example.com.') + + # Try to create A record at same name - should fail + with raises(InvalidParameterError, 'cannot be created because a DNAME with the same name exists'): + self.r.rr_create(name='dept.test.com.', type='A', ip='1.2.3.4') + + def test_dname_subtree_conflict(self): + """Test that records cannot be created under DNAME subtree""" + self.r.zone_create('test.com') + + # Create DNAME first + self.r.rr_create(name='dept.test.com.', type='DNAME', target='dept.example.com.') + + # Try to create record under DNAME subtree - should fail + with raises(InvalidParameterError, 'cannot be created under DNAME subtree'): + self.r.rr_create(name='hr.dept.test.com.', type='A', ip='1.2.3.4') + + def test_dname_existing_subtree_conflict(self): + """Test that DNAME cannot be created if records exist under the subtree""" + self.r.zone_create('test.com') + + # Create record under subtree first + self.r.rr_create(name='marketing.dept.test.com.', type='A', ip='1.2.3.4') + + # Try to create DNAME - should fail + with raises(InvalidParameterError, 'cannot be created because RRs exist under the DNAME subtree'): + self.r.rr_create(name='dept.test.com.', type='DNAME', target='dept.example.com.') + + def test_dname_delete(self): + """Test DNAME record deletion""" + self.r.zone_create('old.example.com') + self.r.zone_create('new.example.com') + + # Create DNAME record + self.r.rr_create(name='dept.old.example.com.', type='DNAME', target='dept.new.example.com.') + assert len(self.r.rr_list('dept.old.example.com.')) == 1 + + # Delete DNAME record + self.r.rr_delete(name='dept.old.example.com.', type='DNAME', target='dept.new.example.com.') + assert len(self.r.rr_list('dept.old.example.com.')) == 0 + + def test_dname_multiple_non_conflicting(self): + """Test that multiple DNAME records can coexist if they don't conflict""" + self.r.zone_create('old.example.com') + self.r.zone_create('new.example.com') + + # Create multiple DNAME records + self.r.rr_create(name='dept1.old.example.com.', type='DNAME', target='dept1.new.example.com.') + self.r.rr_create(name='dept2.old.example.com.', type='DNAME', target='dept2.new.example.com.') + + # Both should exist + assert len(self.r.rr_list('dept1.old.example.com.')) == 1 + assert len(self.r.rr_list('dept2.old.example.com.')) == 1 + class IpblockRRs(RPCTest): def setUp(self): diff --git a/dim/dim/dns.py b/dim/dim/dns.py index 896b4a8f..756760e1 100644 --- a/dim/dim/dns.py +++ b/dim/dim/dns.py @@ -186,6 +186,26 @@ def check_new_rr(new_rr): .filter(or_(RR.name == new_rr.name, and_(~RR.type.in_(('CNAME', 'PTR')), RR.target == new_rr.name))).count(): raise InvalidParameterError('%s cannot be created because other RRs with the same name or target exist' % new_rr) + elif new_rr.type == 'DNAME': + if new_rr.name == new_rr.view.zone.name + '.': + raise InvalidParameterError('It is not allowed to create a DNAME for a zone') + # DNAME cannot coexist with other records at the same name (except NS and DS at zone cuts) + if _same_view_or_different_zone(new_rr)\ + .filter(RR.name == new_rr.name)\ + .filter(~RR.type.in_(('NS', 'DS'))).count(): + raise InvalidParameterError('%s cannot be created because other RRs with the same name exist' % new_rr) + # Check for conflicting records under the DNAME subtree + # Records under the DNAME subtree are those that are subdomains of the DNAME + dname_name = new_rr.name if new_rr.name.endswith('.') else new_rr.name + '.' + + # Find all records that could be under this DNAME subtree + all_records = _same_view_or_different_zone(new_rr).filter(RR.name != dname_name).all() + + for record in all_records: + record_name = record.name if record.name.endswith('.') else record.name + '.' + # Check if this record is a subdomain of the DNAME + if record_name != dname_name and record_name.endswith('.' + dname_name): + raise InvalidParameterError('%s cannot be created because RRs exist under the DNAME subtree' % new_rr) elif new_rr.type == 'PTR': if _same_view_or_different_zone(new_rr)\ .filter(RR.type == 'CNAME').filter(RR.name == new_rr.name).count(): @@ -194,6 +214,20 @@ def check_new_rr(new_rr): if _same_view_or_different_zone(new_rr)\ .filter(RR.type == 'CNAME').filter(or_(RR.name == new_rr.name, RR.name == new_rr.target)).count(): raise InvalidParameterError('%s cannot be created because a CNAME with the same name exists' % new_rr) + # Check if new record conflicts with existing DNAME records + if _same_view_or_different_zone(new_rr)\ + .filter(RR.type == 'DNAME').filter(RR.name == new_rr.name).count(): + raise InvalidParameterError('%s cannot be created because a DNAME with the same name exists' % new_rr) + # Check if new record is under a DNAME subtree + dname_records = _same_view_or_different_zone(new_rr).filter(RR.type == 'DNAME').all() + for dname in dname_records: + # Normalize names by ensuring they end with a dot + dname_name = dname.name if dname.name.endswith('.') else dname.name + '.' + new_rr_name = new_rr.name if new_rr.name.endswith('.') else new_rr.name + '.' + + # Check if the new record is a subdomain under the DNAME + if new_rr_name != dname_name and new_rr_name.endswith('.' + dname_name): + raise InvalidParameterError('%s cannot be created under DNAME subtree %s' % (new_rr, dname.name)) def create_single_rr(name, rr_type, zone, view, user, overwrite=False, **kwargs): diff --git a/dim/dim/rrtype.py b/dim/dim/rrtype.py index 332eb3a1..b552286c 100644 --- a/dim/dim/rrtype.py +++ b/dim/dim/rrtype.py @@ -297,6 +297,11 @@ class CNAME(RRType): validate = {'cname': validate_target} +class DNAME(RRType): + fields = ('target', ) + validate = {'target': validate_target} + + class MX(RRType): fields = ('preference', 'exchange') validate = {'preference': validate_preference, diff --git a/dim/doc/api.rst b/dim/doc/api.rst index ab5dcfdb..2ba6a82c 100644 --- a/dim/doc/api.rst +++ b/dim/doc/api.rst @@ -111,7 +111,7 @@ When creating or specifying a RR, the following options are available: - *name* (string): the fqdn of the RR or the relative name if *zone* was specified; it can be omitted if *type* is PTR and the *ip* is specified -- *type* (string): one of the supported RR types (A, AAAA, PTR, CNAME, MX, NS, +- *type* (string): one of the supported RR types (A, AAAA, PTR, CNAME, DNAME, MX, NS, SRV, TXT, SPF, RP, CERT, HINFO, NAPTR) - :ref:`layer3domain_option`. The layer3domain value is optional when specifying a RR if there is only one RR with that name, type and value. @@ -122,6 +122,7 @@ specified differently for each type: - A/AAAA: *ip* - PTR: *ptrdname* - CNAME: *cname* +- DNAME: *target* - MX: *preference*, *exchange* - NS: *nsdname* - SRV: *priority*, *weight*, *port*, *target* diff --git a/ndcli/dimcli/__init__.py b/ndcli/dimcli/__init__.py index 07acf911..56c735f5 100644 --- a/ndcli/dimcli/__init__.py +++ b/ndcli/dimcli/__init__.py @@ -519,6 +519,7 @@ def complete_rr_value(token, parser): 'caa': {'arguments': [Argument('caa_flags'), Argument('property_tag'), Argument('property_value')]}, + 'dname': {'arguments': [Argument('target')]}, } RR_FIELDS['aaaa'] = RR_FIELDS['a'] rr_types = list(RR_FIELDS.keys()) + ['soa'] diff --git a/ndcli/dimcli/zoneimport.py b/ndcli/dimcli/zoneimport.py index 4cfb37fc..bc09dd8e 100644 --- a/ndcli/dimcli/zoneimport.py +++ b/ndcli/dimcli/zoneimport.py @@ -20,6 +20,7 @@ def __init__(self, src, args): 'AAAA': lambda rdata: dict(ip=rdata.address), 'PTR': lambda rdata: dict(ptrdname=str(rdata.target)), 'CNAME': lambda rdata: dict(cname=str(rdata.target)), + 'DNAME': lambda rdata: dict(target=str(rdata.target)), 'MX': lambda rdata: dict(preference=int(rdata.preference), exchange=str(rdata.exchange)), 'NS': lambda rdata: dict(nsdname=str(rdata.target)), 'TXT': lambda rdata: dict(strings=rdata.to_text()),