11from __future__ import annotations
22
3+ import base64
4+ import binascii
5+
36from o2switch_cli .core .audit import AuditService
47from o2switch_cli .core .cpanel_client import CpanelClient
58from o2switch_cli .core .domain_service import DomainService
@@ -45,6 +48,50 @@ def _line_index(record: DNSRecord) -> int:
4548 raw_value = record .record_id or record .raw .get ("line_index" ) or record .raw .get ("line" )
4649 return int (raw_value )
4750
51+ @staticmethod
52+ def _decode_b64_text (value : object ) -> str | None :
53+ if not isinstance (value , str ) or not value :
54+ return None
55+ try :
56+ decoded = base64 .b64decode (value , validate = True )
57+ except (binascii .Error , ValueError ):
58+ return None
59+ try :
60+ return decoded .decode ("utf-8" )
61+ except UnicodeDecodeError :
62+ return decoded .decode ("utf-8" , errors = "replace" )
63+
64+ @classmethod
65+ def _record_values (cls , item : dict [str , object ]) -> list [str ]:
66+ data = item .get ("data" )
67+ if isinstance (data , list ):
68+ return [str (value ) for value in data ]
69+ if data not in (None , "" ):
70+ return [str (data )]
71+
72+ data_b64 = item .get ("data_b64" )
73+ if isinstance (data_b64 , list ):
74+ return [decoded for value in data_b64 if (decoded := cls ._decode_b64_text (value )) is not None ]
75+ if decoded := cls ._decode_b64_text (data_b64 ):
76+ return [decoded ]
77+ return []
78+
79+ @classmethod
80+ def _record_name (cls , item : dict [str , object ], root_domain : str ) -> str :
81+ raw_name = item .get ("dname" ) or item .get ("name" ) or item .get ("domain" )
82+ if raw_name in (None , "" ):
83+ raw_name = cls ._decode_b64_text (item .get ("dname_b64" )) or root_domain
84+ return canonical_record_name (str (raw_name ), root_domain )
85+
86+ @staticmethod
87+ def _require_serial (operation : str , root_domain : str , serial : int | None ) -> int :
88+ if serial is None :
89+ raise TransportAppError (
90+ operation ,
91+ f"Could not determine the current DNS serial for zone { root_domain } ." ,
92+ )
93+ return serial
94+
4895 def _zone_state (self , root_domain : str ) -> tuple [list [DNSRecord ], int | None ]:
4996 result = self ._client .parse_zone (root_domain )
5097 payload = result .data or {}
@@ -64,13 +111,7 @@ def _zone_state(self, root_domain: str) -> tuple[list[DNSRecord], int | None]:
64111 if not isinstance (item , dict ):
65112 continue
66113 record_type = str (item .get ("record_type" ) or item .get ("type" ) or "" ).upper ()
67- data = item .get ("data" )
68- if isinstance (data , list ):
69- values = [str (value ) for value in data ]
70- elif data is None :
71- values = []
72- else :
73- values = [str (data )]
114+ values = self ._record_values (item )
74115 value = (
75116 item .get ("address" )
76117 or item .get ("target" )
@@ -80,10 +121,7 @@ def _zone_state(self, root_domain: str) -> tuple[list[DNSRecord], int | None]:
80121 )
81122 ttl = item .get ("ttl" )
82123 record_id = item .get ("line_index" ) or item .get ("line" ) or item .get ("record_id" ) or item .get ("id" )
83- name = canonical_record_name (
84- str (item .get ("dname" ) or item .get ("name" ) or item .get ("domain" ) or root_domain ),
85- root_domain ,
86- )
124+ name = self ._record_name (item , root_domain )
87125 records .append (
88126 DNSRecord (
89127 name = name ,
@@ -106,15 +144,10 @@ def _extract_serial(records: list[DNSRecord]) -> int | None:
106144 for record in records :
107145 if record .type != "SOA" :
108146 continue
109- values = record .raw .get ("data" , [])
110- if isinstance (values , list ):
111- for value in values :
112- if str (value ).isdigit () and len (str (value )) >= 8 :
113- return int (value )
114- elif isinstance (values , str ):
115- for part in values .split ():
116- if part .isdigit () and len (part ) >= 8 :
117- return int (part )
147+ values = DNSService ._record_values (record .raw )
148+ for value in values :
149+ if str (value ).isdigit () and len (str (value )) >= 8 :
150+ return int (value )
118151 return None
119152
120153 def get_zone_state (self , root_domain : str ) -> list [DNSRecord ]:
@@ -318,6 +351,7 @@ def upsert_a_record(
318351 applied = False
319352 action = "dry-run" if dry_run else "created"
320353 if not dry_run :
354+ serial = self ._require_serial ("dns_upsert" , root_domain , serial )
321355 add = None
322356 edit = None
323357 remove = None
@@ -407,6 +441,7 @@ def delete_a_record(
407441 hostname = normalize_hostname (fqdn )
408442 root_domain , serial , matches , plan = self .plan_delete_a_record (hostname , force = force )
409443 if not dry_run :
444+ serial = self ._require_serial ("dns_delete" , root_domain , serial )
410445 remove = [self ._line_index (record ) for record in matches ]
411446 self ._client .mass_edit_zone (zone = root_domain , serial = serial , remove = remove )
412447 verification = VerificationStatus .SKIPPED
0 commit comments