from typing import Dict, List, Any from dns import resolver, exception def resolve_cname(hostname: str) -> List[str]: """Resolve CNAME records for a hostname and return targets without trailing dots. If there is no CNAME record, this returns an empty list. Raises dns.exception.DNSException on resolver errors. """ answers = resolver.resolve(hostname, "CNAME") targets: List[str] = [] for rdata in answers: # rdata.target may be a Name object; to_text() yields with trailing dot target = getattr(rdata, "target", None) if target is None: # some dnspython versions use .to_text() directly on rdata text = rdata.to_text() else: text = target.to_text() targets.append(text.rstrip(".")) return targets def check_cname_map(cname_map: Dict[str, str]) -> List[Dict[str, Any]]: """Check a mapping of hostname -> expected CNAME target. Returns a list of result dicts: { 'hostname': str, 'expected': str, 'actual': List[str], 'ok': bool, 'error': Optional[str] } """ results: List[Dict[str, Any]] = [] for hostname, expected in cname_map.items(): expected_norm = expected.rstrip(".").lower() try: actual_targets = resolve_cname(hostname) actual_norm = [t.lower() for t in actual_targets] ok = expected_norm in actual_norm results.append({ "hostname": hostname, "expected": expected, "actual": actual_targets, "ok": ok, "error": None, }) except exception.DNSException as e: results.append({ "hostname": hostname, "expected": expected, "actual": [], "ok": False, "error": str(e), }) return results def bytes_to_gb(num_bytes: int) -> float: """Convert bytes to gigabytes (GB, 1024^3).""" return round(num_bytes / (1024 ** 3), 3)