summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xregflow/domain_manager.py393
-rw-r--r--regflow/providers/cloudflare_api.py5
-rw-r--r--regflow/providers/namecheap_api.py75
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 []