Coverage for frappe_manager / ssl_manager / certificate.py: 53%
47 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:35 +0530
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:35 +0530
1"""
2SSL Certificate models with support for multi-certificate management.
4Provides base SSLCertificate model and specialized CustomDomainCertificate
5for CNAME delegation scenarios.
6"""
8from datetime import datetime, timedelta
9from pathlib import Path
11from pydantic import BaseModel
13from frappe_manager import SSL_RENEW_BEFORE_DAYS
14from frappe_manager.ssl_manager import LETSENCRYPT_PREFERRED_CHALLENGE, SUPPORTED_SSL_TYPES
15from frappe_manager.utils.helpers import get_certificate_expiry_date
18class SSLCertificate(BaseModel):
19 """
20 SSL certificate configuration and state.
22 Represents a single SSL certificate for a single domain, with support
23 for system state tracking and computed properties.
24 """
26 # User configuration
27 domain: str
28 ssl_type: SUPPORTED_SSL_TYPES
29 challenge_type: LETSENCRYPT_PREFERRED_CHALLENGE = LETSENCRYPT_PREFERRED_CHALLENGE.http01
30 enabled: bool = True
31 acme_client: str = "acme.sh" # ACME client for certificate issuance (acme.sh)
32 hsts: str = "off"
34 # System state fields (populated after certificate issuance)
35 cert_path: Path | None = None
36 key_path: Path | None = None
37 issued_date: datetime | None = None
38 last_renewal_attempt: datetime | None = None
39 status: str = "pending" # pending, active, expiring_soon, expired, error
41 # Fields to exclude when serializing to TOML
42 toml_exclude: set | None = {"domain", "toml_exclude"}
44 @property
45 def expiry_date(self) -> datetime | None:
46 """
47 Compute expiry date from certificate file at runtime.
49 This ensures the expiry date is always accurate and in sync with the
50 actual certificate file, avoiding stale data in config.
52 Returns:
53 Certificate expiry datetime, or None if cert doesn't exist
54 """
55 if self.cert_path and self.cert_path.exists():
56 try:
57 return get_certificate_expiry_date(self.cert_path)
58 except Exception:
59 # If we can't read the cert, return None
60 return None
61 return None
63 @property
64 def needs_renewal(self) -> bool:
65 """
66 Check if certificate needs renewal based on expiry date.
68 A certificate needs renewal if it will expire within
69 SSL_RENEW_BEFORE_DAYS days.
71 Returns:
72 True if certificate should be renewed, False otherwise
73 """
74 expiry = self.expiry_date
75 if not expiry:
76 return False
78 renewal_threshold = expiry - timedelta(days=SSL_RENEW_BEFORE_DAYS)
79 now = datetime.now()
81 # Handle timezone-aware dates
82 if expiry.tzinfo:
83 now = now.replace(tzinfo=expiry.tzinfo)
85 return now >= renewal_threshold
87 @property
88 def days_until_expiry(self) -> int | None:
89 """
90 Calculate days until certificate expires.
92 Returns:
93 Number of days until expiry, or None if cert doesn't exist
94 """
95 expiry = self.expiry_date
96 if not expiry:
97 return None
99 now = datetime.now()
100 if expiry.tzinfo:
101 now = now.replace(tzinfo=expiry.tzinfo)
103 delta = expiry - now
104 return delta.days