@@ -554,6 +554,7 @@ def process_custom_record(template_record, zone_records):
554554 'AAAA' : ['A' , 'AAAA' , 'CNAME' , 'REDIR301' , 'REDIR302' ],
555555 'MX' : ['MX' , 'CNAME' ],
556556 'CNAME' : ['A' , 'AAAA' , 'CNAME' , 'MX' , 'TXT' , 'REDIR301' , 'REDIR302' ],
557+ 'APEXCNAME' : ['A' , 'AAAA' , 'CNAME' , 'MX' , 'TXT' , 'REDIR301' , 'REDIR302' ],
557558 'REDIR301' : ['A' , 'AAAA' , 'CNAME' ],
558559 'REDIR302' : ['A' , 'AAAA' , 'CNAME' ],
559560}
@@ -604,13 +605,43 @@ def process_other_record(template_record, zone_records):
604605 return new_record
605606
606607
608+ def process_apexcname_record (template_record , zone_records ):
609+ """
610+ Process an APEXCNAME record from a template. APEXCNAME always targets the
611+ zone apex (@) and behaves like a CNAME for conflict resolution purposes.
612+
613+ :param template_record: The record from the template to process.
614+ :type template_record: dict
615+ - keys: 'type', 'pointsTo', 'ttl'; optional 'host' (must be '@' if present)
616+
617+ :param zone_records: A list of all records in the current zone.
618+ :type zone_records: list
619+
620+ :return: The new APEXCNAME record.
621+ :rtype: dict
622+ """
623+ new_record = {'type' : 'APEXCNAME' ,
624+ 'name' : '@' ,
625+ 'data' : template_record ['pointsTo' ],
626+ 'ttl' : int (template_record ['ttl' ])}
627+
628+ for zone_record in zone_records :
629+ zone_record_type = zone_record ['type' ].upper ()
630+ if zone_record_type in _delete_map ['APEXCNAME' ] and \
631+ zone_record ['name' ] == '@' and \
632+ '_replace' not in zone_record :
633+ zone_record ['_delete' ] = 1
634+
635+ return new_record
636+
637+
607638def check_conflict_with_self (new_record , new_records ):
608- # Mark records that conflict with self (affects only CNAME and NS)
639+ # Mark records that conflict with self (affects only CNAME, APEXCNAME and NS)
609640 for zone_record in new_records :
610641 zone_record_type = zone_record ['type' ].upper ()
611642
612643 error = False
613- if (new_record ['type' ] == 'CNAME' or zone_record ['type' ] == 'CNAME' ) \
644+ if (new_record ['type' ] in ( 'CNAME' , 'APEXCNAME' ) or zone_record ['type' ] in ( 'CNAME' , 'APEXCNAME' ) ) \
614645 and zone_record ['name' ] == new_record ['name' ]:
615646 error = True
616647
@@ -629,7 +660,7 @@ def check_conflict_with_self(new_record, new_records):
629660 if error :
630661 raise InvalidData (f"Template record { new_record ['type' ]} { new_record ['name' ]} conflicts with other tempate record { zone_record ['type' ]} { zone_record ['name' ]} " )
631662
632- _CORE_TYPES = {'A' , 'AAAA' , 'CNAME' , 'MX' , 'NS' , 'SRV' , 'TXT' , 'SPFM' ,
663+ _CORE_TYPES = {'A' , 'AAAA' , 'CNAME' , 'APEXCNAME' , ' MX' , 'NS' , 'SRV' , 'TXT' , 'SPFM' ,
633664 'REDIR301' , 'REDIR302' }
634665
635666# Maps RR type → list of fields whose string values must NOT be lowercased
@@ -838,7 +869,7 @@ def process_records(template_records, zone_records, domain, host, params,
838869 template_record_type = template_record ['type' ].upper ()
839870
840871 # We can only handle certain record types
841- supported = ['A' , 'AAAA' , 'MX' , 'CNAME' , 'TXT' , 'SRV' , 'SPFM' , 'NS' ]
872+ supported = ['A' , 'AAAA' , 'MX' , 'CNAME' , 'APEXCNAME' , ' TXT' , 'SRV' , 'SPFM' , 'NS' ]
842873 if redirect_records is not None :
843874 supported += ['REDIR301' , 'REDIR302' ]
844875 is_custom = not template_record_type in supported and is_custom_record_type (template_record_type )
@@ -848,7 +879,7 @@ def process_records(template_records, zone_records, domain, host, params,
848879
849880 # Deal with the variables and validation
850881
851- # Deal with the host/name
882+ # Deal with the host/name
852883 if template_record_type == 'SRV' :
853884 template_record ['name' ] = resolve_variables (
854885 template_record ['name' ], domain , host , params , 'name' )
@@ -861,6 +892,14 @@ def process_records(template_records, zone_records, domain, host, params,
861892 raise InvalidData ('Invalid data for SRV host: ' +
862893 srvhost )
863894
895+ elif template_record_type == 'APEXCNAME' :
896+ # host is optional for APEXCNAME; if present it must be '@'
897+ apex_host = template_record .get ('host' , '@' )
898+ if apex_host != '@' :
899+ raise InvalidData ('Invalid data for APEXCNAME host: ' +
900+ apex_host + ' (must be @ or omitted)' )
901+ template_record ['host' ] = '@'
902+
864903 else :
865904 orig_host = template_record ['host' ]
866905 template_record ['host' ] = resolve_variables (
@@ -889,14 +928,14 @@ def process_records(template_records, zone_records, domain, host, params,
889928 raise InvalidData (err_msg )
890929
891930 # Points To / Target
892- if template_record_type in ['A' , 'AAAA' , 'MX' , 'CNAME' , 'NS' ]:
931+ if template_record_type in ['A' , 'AAAA' , 'MX' , 'CNAME' , 'APEXCNAME' , ' NS' ]:
893932 orig_pointsto = template_record ['pointsTo' ]
894933 if template_record_type == 'NS' and orig_pointsto == '@' :
895934 raise InvalidData ('Invalid data for NS pointsTo: @ would create a circular delegation' )
896935 template_record ['pointsTo' ] = resolve_variables (
897936 template_record ['pointsTo' ], domain , host , params , 'pointsTo' )
898937
899- if template_record_type in ['MX' , 'CNAME' , 'NS' ]:
938+ if template_record_type in ['MX' , 'CNAME' , 'APEXCNAME' , ' NS' ]:
900939 if not is_valid_pointsTo_host (
901940 template_record ['pointsTo' ]):
902941 raise InvalidData ('Invalid data for ' +
@@ -1010,6 +1049,8 @@ def process_records(template_records, zone_records, domain, host, params,
10101049 new_record = process_srv_record (template_record , zone_records )
10111050 elif template_record_type in ['REDIR301' , 'REDIR302' ]:
10121051 new_record = process_redir_record (template_record , zone_records )
1052+ elif template_record_type == 'APEXCNAME' :
1053+ new_record = process_apexcname_record (template_record , zone_records )
10131054 elif is_custom :
10141055 new_record = process_custom_record (template_record , zone_records )
10151056 else :
0 commit comments