diff options
| author | Yuval Adam <_@yuv.al> | 2025-07-18 20:38:02 +0200 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-07-18 20:38:02 +0200 |
| commit | 8ed818f331b000cbcd91e3ee5f96098af7b03ff1 (patch) | |
| tree | fbdbc7a82c498cd5fb6e60f34b3da8467b581205 | |
| parent | ca13be0eec520edafd513f1d0eee5e863cfacf10 (diff) | |
make script idempotent with --status
| -rwxr-xr-x | regflow/domain_manager.py | 393 | ||||
| -rw-r--r-- | regflow/providers/cloudflare_api.py | 5 | ||||
| -rw-r--r-- | regflow/providers/namecheap_api.py | 75 |
3 files changed, 358 insertions, 115 deletions
diff --git a/regflow/domain_manager.py b/regflow/domain_manager.py index fc9d2f0..8905a0a 100755 --- a/regflow/domain_manager.py +++ b/regflow/domain_manager.py @@ -13,49 +13,158 @@ class DomainManager: self.namecheap = NamecheapAPI(config) self.cloudflare = CloudflareAPI(config) - def register_and_setup_domain( + def get_domain_status(self, domain: str) -> Dict[str, Any]: + """Get current status of domain across all services""" + status = { + "domain": domain, + "registered": False, + "cloudflare_zone": None, + "nameservers": {"namecheap": [], "cloudflare": []}, + "nameservers_match": False, + } + + # Check if domain is registered + try: + status["registered"] = self.namecheap.is_domain_registered(domain) + except Exception as e: + status["registration_error"] = str(e) + + # Get Cloudflare zone info + try: + zone_info = self.cloudflare.get_zone_info(domain) + if zone_info: + status["cloudflare_zone"] = { + "id": zone_info["id"], + "name": zone_info["name"], + "status": zone_info.get("status", "unknown"), + } + + # Get Cloudflare nameservers + cf_nameservers = self.cloudflare.get_zone_nameservers(zone_info["id"]) + status["nameservers"]["cloudflare"] = cf_nameservers + except Exception as e: + status["cloudflare_error"] = str(e) + + # Get Namecheap nameservers if domain is registered + if status["registered"]: + try: + nc_nameservers = self.namecheap.get_domain_nameservers(domain) + status["nameservers"]["namecheap"] = nc_nameservers + + # Check if nameservers match + cf_ns = set(status["nameservers"]["cloudflare"]) + nc_ns = set(nc_nameservers) + + # Only consider nameservers matching if we have both sets and they match + if len(cf_ns) > 0 and len(nc_ns) > 0: + status["nameservers_match"] = cf_ns == nc_ns + else: + # If we can't retrieve nameservers from either side, assume they don't match + status["nameservers_match"] = False + except Exception as e: + status["namecheap_ns_error"] = str(e) + + return status + + def print_domain_status(self, domain: str): + """Print formatted status of domain""" + status = self.get_domain_status(domain) + + print(f"\n=== Domain Status: {domain} ===") + + # Registration status + if status["registered"]: + print("✓ Domain is registered in Namecheap") + else: + print("✗ Domain is NOT registered in Namecheap") + if "registration_error" in status: + print(f" Error: {status['registration_error']}") + + # Cloudflare zone status + if status["cloudflare_zone"]: + zone = status["cloudflare_zone"] + print( + f"✓ Cloudflare zone exists (ID: {zone['id']}, Status: {zone['status']})" + ) + else: + print("✗ No Cloudflare zone found") + if "cloudflare_error" in status: + print(f" Error: {status['cloudflare_error']}") + + # Nameserver status + nc_ns = status["nameservers"]["namecheap"] + cf_ns = status["nameservers"]["cloudflare"] + + if nc_ns: + print(f"Namecheap nameservers: {', '.join(nc_ns)}") + else: + print("Namecheap nameservers: None") + + if cf_ns: + print(f"Cloudflare nameservers: {', '.join(cf_ns)}") + else: + print("Cloudflare nameservers: None") + + if len(nc_ns) == 0 and len(cf_ns) == 0: + print("⚠ Cannot retrieve nameservers from either service") + elif len(nc_ns) == 0: + print("⚠ Cannot retrieve Namecheap nameservers - unable to verify configuration") + elif len(cf_ns) == 0: + print("⚠ Cannot retrieve Cloudflare nameservers - unable to verify configuration") + elif status["nameservers_match"]: + print("✓ Nameservers are properly configured") + else: + print("✗ Nameservers do NOT match") + + print("=" * 50) + + def setup_domain( self, domain: str, registrant_info: Optional[Dict[str, str]] = None, setup_workers: bool = True, dry_run: bool = False, - skip_registration: bool = False, + force_registration: bool = False, ) -> Dict[str, Any]: """ - Complete domain registration and setup workflow: - 1. Check domain availability - 2. Verify pricing and account balance - 3. Register domain (with user confirmation) - 4. Add domain to Cloudflare - 5. Update nameservers in Namecheap - 6. Set up basic DNS records + Complete domain setup workflow (idempotent): + 1. Check domain registration status + 2. Register domain if needed (with user confirmation) + 3. Create Cloudflare zone if needed + 4. Update nameservers in Namecheap if needed + 5. Set up basic DNS records """ result = {"domain": domain, "steps_completed": [], "errors": []} try: - if not skip_registration: - # Step 1: Check domain availability + # Get current status + status = self.get_domain_status(domain) + + # Step 1: Handle domain registration + if not status["registered"]: + if not force_registration: + print( + f"Domain {domain} is not registered. Use --force-registration to register it." + ) + result["errors"].append( + "Domain not registered and force_registration not enabled" + ) + return result + + # Check availability print(f"Checking availability for {domain}...") if not self.namecheap.check_domain_availability(domain): - result["errors"].append(f"Domain {domain} is not available") + result["errors"].append( + f"Domain {domain} is not available for registration" + ) return result - result["steps_completed"].append("availability_check") - print(f"✓ Domain {domain} is available") - - # Step 2: Check pricing and balance - print("Checking pricing and account balance...") - + # Get pricing and balance try: pricing = self.namecheap.get_domain_pricing(domain) - except Exception as e: - result["errors"].append(f"Failed to get domain pricing: {str(e)}") - return result - - try: balance = self.namecheap.get_account_balance() except Exception as e: - result["errors"].append(f"Failed to get account balance: {str(e)}") + result["errors"].append(f"Failed to get pricing/balance: {str(e)}") return result if balance < pricing["register"]: @@ -65,10 +174,7 @@ class DomainManager: ) return result - result["steps_completed"].append("pricing_check") - print(f"✓ Pricing: ${pricing['register']:.2f}, Balance: ${balance:.2f}") - - # Step 3: User confirmation with multiple safeguards + # User confirmation print("\n" + "=" * 50) print("DOMAIN REGISTRATION CONFIRMATION") print("=" * 50) @@ -78,68 +184,78 @@ class DomainManager: print(f"Remaining Balance: ${balance - pricing['register']:.2f}") print("=" * 50) - # Multiple confirmation steps to prevent accidents print( f"\nWARNING: This will charge ${pricing['register']:.2f} to your account!" ) - first_confirm = input( - f"Type 'REGISTER' to proceed with registration of {domain}: " - ).strip() - if first_confirm != "REGISTER": - result["errors"].append("Registration cancelled by user") - return result - second_confirm = ( - input( - f"Are you absolutely sure you want to register {domain} for ${pricing['register']:.2f}? (yes/no): " - ) - .lower() - .strip() - ) - if second_confirm != "yes": - result["errors"].append("Registration cancelled by user") - return result + if not dry_run: + first_confirm = input( + f"Type 'REGISTER' to proceed with registration of {domain}: " + ).strip() + if first_confirm != "REGISTER": + result["errors"].append("Registration cancelled by user") + return result - # Step 4: Register domain - if dry_run: - print( - f"DRY RUN: Would register domain {domain} (skipping actual registration)" + second_confirm = ( + input( + f"Are you absolutely sure you want to register {domain} for ${pricing['register']:.2f}? (yes/no): " + ) + .lower() + .strip() ) - result["steps_completed"].append("domain_registration_dry_run") - else: + if second_confirm != "yes": + result["errors"].append("Registration cancelled by user") + return result + + # Register domain print(f"Registering domain {domain}...") if not self.namecheap.register_domain( domain, registrant_info=registrant_info ): result["errors"].append(f"Failed to register domain {domain}") return result - result["steps_completed"].append("domain_registration") - print(f"✓ Domain {domain} registered successfully") + result["steps_completed"].append("domain_registration") + print(f"✓ Domain {domain} registered successfully") + else: + print(f"DRY RUN: Would register domain {domain}") + result["steps_completed"].append("domain_registration_dry_run") else: - print( - f"✓ Skipping domain registration for {domain} (assuming already registered)" - ) - result["steps_completed"].append("domain_registration_skipped") - - # Step 5: Add domain to Cloudflare - print(f"Adding {domain} to Cloudflare...") - try: - zone_info = self.cloudflare.add_zone(domain) - zone_id = zone_info["id"] + print(f"✓ Domain {domain} is already registered") + result["steps_completed"].append("domain_already_registered") - result["steps_completed"].append("cloudflare_zone_creation") + # Step 2: Handle Cloudflare zone creation + if not status["cloudflare_zone"]: + print(f"Creating Cloudflare zone for {domain}...") + try: + if not dry_run: + zone_info = self.cloudflare.add_zone(domain) + zone_id = zone_info["id"] + result["steps_completed"].append("cloudflare_zone_creation") + result["zone_id"] = zone_id + print(f"✓ Cloudflare zone created (ID: {zone_id})") + else: + print(f"DRY RUN: Would create Cloudflare zone for {domain}") + result["steps_completed"].append( + "cloudflare_zone_creation_dry_run" + ) + # For dry run, we can't continue with nameserver setup + return result + except Exception as e: + result["errors"].append( + f"Failed to create Cloudflare zone: {str(e)}" + ) + return result + else: + zone_id = status["cloudflare_zone"]["id"] result["zone_id"] = zone_id - print(f"✓ Domain added to Cloudflare (Zone ID: {zone_id})") - except Exception as e: - result["errors"].append(f"Failed to add domain to Cloudflare: {str(e)}") - return result + print(f"✓ Cloudflare zone already exists (ID: {zone_id})") + result["steps_completed"].append("cloudflare_zone_already_exists") - # Step 6: Get Cloudflare nameservers + # Step 3: Get Cloudflare nameservers print("Getting Cloudflare nameservers...") try: nameservers = self.cloudflare.get_zone_nameservers(zone_id) - result["nameservers"] = nameservers print(f"✓ Cloudflare nameservers: {', '.join(nameservers)}") except Exception as e: @@ -148,33 +264,39 @@ class DomainManager: ) return result - # Step 7: Update nameservers in Namecheap - if not dry_run: - print("Updating nameservers in Namecheap...") - try: - if not self.namecheap.set_dns_servers(domain, nameservers): + # Step 4: Update nameservers in Namecheap if needed + current_nc_nameservers = set(status["nameservers"]["namecheap"]) + cloudflare_nameservers = set(nameservers) + + if current_nc_nameservers != cloudflare_nameservers: + if not dry_run: + print("Updating nameservers in Namecheap...") + try: + if not self.namecheap.set_dns_servers(domain, nameservers): + result["errors"].append( + "Failed to update nameservers in Namecheap" + ) + return result + + result["steps_completed"].append("nameserver_update") + print("✓ Nameservers updated in Namecheap") + except Exception as e: result["errors"].append( - "Failed to update nameservers in Namecheap" + f"Failed to update nameservers in Namecheap: {str(e)}" ) return result - - result["steps_completed"].append("nameserver_update") - print("✓ Nameservers updated in Namecheap") - except Exception as e: - result["errors"].append( - f"Failed to update nameservers in Namecheap: {str(e)}" + else: + print( + f"DRY RUN: Would update nameservers in Namecheap to: {', '.join(nameservers)}" ) - return result + result["steps_completed"].append("nameserver_update_dry_run") else: - print( - f"DRY RUN: Would update nameservers in Namecheap to: {', '.join(nameservers)}" - ) - result["steps_completed"].append("nameserver_update_dry_run") + print("✓ Nameservers already configured correctly") + result["steps_completed"].append("nameservers_already_configured") - # Step 8: Set up basic DNS records (skip for now, empty DNS list) + # Step 5: Set up basic DNS records (placeholder for now) print("Setting up basic DNS records...") try: - # For now, just create an empty DNS list as requested dns_records = [] result["dns_records"] = dns_records result["steps_completed"].append("basic_dns_setup") @@ -183,16 +305,22 @@ class DomainManager: result["errors"].append(f"Failed to set up DNS records: {str(e)}") return result - # Step 9: Set up worker subdomain if requested + # Step 6: Set up worker subdomain if requested if setup_workers: print("Setting up worker subdomain...") try: - worker_record = self.cloudflare.create_worker_subdomain( - zone_id, f"app.{domain}" - ) - result["worker_record"] = worker_record - result["steps_completed"].append("worker_subdomain_setup") - print(f"✓ Worker subdomain app.{domain} created") + if not dry_run: + worker_record = self.cloudflare.create_worker_subdomain( + zone_id, f"app.{domain}" + ) + result["worker_record"] = worker_record + result["steps_completed"].append("worker_subdomain_setup") + print(f"✓ Worker subdomain app.{domain} created") + else: + print(f"DRY RUN: Would create worker subdomain app.{domain}") + result["steps_completed"].append( + "worker_subdomain_setup_dry_run" + ) except Exception as e: result["errors"].append( f"Failed to set up worker subdomain: {str(e)}" @@ -201,8 +329,10 @@ class DomainManager: result["success"] = True print(f"\n🎉 Domain {domain} setup completed successfully!") - print(f"Zone ID: {zone_id}") - print(f"Nameservers: {', '.join(nameservers)}") + if "zone_id" in result: + print(f"Zone ID: {result['zone_id']}") + if "nameservers" in result: + print(f"Nameservers: {', '.join(result['nameservers'])}") return result @@ -228,12 +358,35 @@ class DomainManager: def main(): if len(sys.argv) < 2: - print("Usage: python domain_manager.py <domain> [--dry-run] [--setup-only]") + print("Usage: regflow <domain> [options]") + print("") + print("Options:") + print(" --status Show current status of domain") + print(" --setup Set up domain (idempotent)") + print( + " --dry-run Show what would be done without making changes" + ) + print(" --force-registration Allow domain registration (costs money!)") + print(" --no-workers Skip worker subdomain setup") + print("") + print("Examples:") + print(" regflow example.com --status") + print(" regflow example.com --setup --dry-run") + print(" regflow example.com --setup --force-registration") sys.exit(1) domain = sys.argv[1] + + # Parse command line arguments + show_status = "--status" in sys.argv + setup_domain = "--setup" in sys.argv dry_run = "--dry-run" in sys.argv - setup_only = "--setup-only" in sys.argv + force_registration = "--force-registration" in sys.argv + setup_workers = "--no-workers" not in sys.argv + + # Default to setup if no action specified + if not show_status and not setup_domain: + setup_domain = True # Load configuration config = Config.from_env() @@ -254,22 +407,32 @@ def main(): # Initialize domain manager manager = DomainManager(config) - # Register and setup domain - if setup_only: - print("Running in SETUP-ONLY mode - skipping domain registration") - if dry_run: - print("Running in DRY RUN mode - no actual registration will occur") - result = manager.register_and_setup_domain( - domain, dry_run=dry_run, skip_registration=setup_only - ) + # Handle status command + if show_status: + manager.print_domain_status(domain) + return - if result.get("success"): - print(f"\nSuccess! Domain {domain} is ready for use.") - else: - print("\nErrors occurred:") - for error in result.get("errors", []): - print(f" - {error}") - sys.exit(1) + # Handle setup command + if setup_domain: + if dry_run: + print("Running in DRY RUN mode - no actual changes will be made") + if force_registration: + print("FORCE REGISTRATION enabled - will register domain if needed") + + result = manager.setup_domain( + domain, + dry_run=dry_run, + force_registration=force_registration, + setup_workers=setup_workers, + ) + + if result.get("success"): + print(f"\nSuccess! Domain {domain} is ready for use.") + else: + print("\nErrors occurred:") + for error in result.get("errors", []): + print(f" - {error}") + sys.exit(1) if __name__ == "__main__": diff --git a/regflow/providers/cloudflare_api.py b/regflow/providers/cloudflare_api.py index 3446ba3..edc17a7 100644 --- a/regflow/providers/cloudflare_api.py +++ b/regflow/providers/cloudflare_api.py @@ -163,3 +163,8 @@ class CloudflareAPI: """Get all DNS records for a zone""" result = self._make_request("GET", f"/zones/{zone_id}/dns_records") return result.get("result", []) + + def zone_exists(self, domain: str) -> bool: + """Check if domain exists as a zone in Cloudflare""" + zone_info = self.get_zone_info(domain) + return zone_info is not None diff --git a/regflow/providers/namecheap_api.py b/regflow/providers/namecheap_api.py index 6449ecd..20cdfa9 100644 --- a/regflow/providers/namecheap_api.py +++ b/regflow/providers/namecheap_api.py @@ -161,3 +161,78 @@ class NamecheapAPI: raise Exception(f"Could not find DNS update result for {domain}") return dns_result.get("Updated") == "true" + + def is_domain_registered(self, domain: str) -> bool: + """Check if domain is registered in user's account""" + try: + # Get list of all domains in the account + root = self._make_request("namecheap.domains.getList", {}) + + ns = {"ns": "http://api.namecheap.com/xml.response"} + + # Look for the domain in the list + for domain_elem in root.findall(".//ns:Domain", ns): + domain_name = domain_elem.get("Name") + if domain_name and domain_name.lower() == domain.lower(): + return True + + return False + except Exception: + # If API call fails, assume not registered + return False + + def get_domain_nameservers(self, domain: str) -> list: + """Get current nameservers for a domain""" + try: + params = {"DomainName": domain} + + # Use getInfo to get domain details including nameservers + root = self._make_request("namecheap.domains.getInfo", params) + + ns = {"ns": "http://api.namecheap.com/xml.response"} + nameservers = [] + + # Look for nameservers in the domain info response + # The structure might be different, let's check multiple possible locations + + # Try to find nameservers in DnsDetails + dns_details = root.find(".//ns:DnsDetails", ns) + if dns_details is not None: + # Look for nameserver elements + for ns_elem in dns_details.findall(".//ns:Nameserver", ns): + if ns_elem.text: + nameservers.append(ns_elem.text) + + # If no nameservers found in DnsDetails, try other locations + if not nameservers: + # Try to find in different structure + for ns_elem in root.findall(".//ns:Nameserver", ns): + if ns_elem.text: + nameservers.append(ns_elem.text) + + # If still no nameservers, check for attributes in the domain result + if not nameservers: + domain_result = root.find(".//ns:DomainGetInfoResult", ns) + if domain_result is not None: + # Check for nameserver attributes (common in Namecheap responses) + for i in range(1, 5): # Check for ns1, ns2, ns3, ns4 + ns_attr = domain_result.get(f"Nameserver{i}") + if ns_attr: + nameservers.append(ns_attr) + + # Also check for DNS details in attributes + dns_type = domain_result.get("DnsProviderType") + if dns_type == "CUSTOM": + # For custom DNS, nameservers should be in the response + # Try looking for nameservers in different attribute names + for attr_name in domain_result.attrib: + if "nameserver" in attr_name.lower() or "ns" in attr_name.lower(): + ns_value = domain_result.get(attr_name) + if ns_value and ns_value not in nameservers: + nameservers.append(ns_value) + + return nameservers + except Exception as e: + # For debugging, you might want to print the exception + # print(f"Error getting nameservers for {domain}: {e}") + return [] |
