Zone gezielt bei Plesk erfragen statt alle Domains aufzulisten
Ein Name wie endian.fon-aria.de ist oft nur ein Record in der Zone fon-aria.de und existiert weder als Domain noch als Abo. Ausserdem darf nicht jeder API-User alle Domains des Servers auflisten - dann brach das Tool mit "No domains found on the Plesk server" ab, obwohl die Zone da war. Die Zone wird jetzt Label fuer Label von unten nach oben gezielt abgefragt (endian.fon-aria.de -> fon-aria.de), jeweils per site.get und webspace.get mit Namensfilter. Das Auflisten aller Domains dient nur noch der Fehlermeldung, die jetzt auch zeigt, welche Namen probiert wurden und was Plesk dazu gesagt hat. - neue Option --zone, um die Zone bei Bedarf fest vorzugeben - get_rec meldet jetzt als Warnung, wenn Plesk keine Records liefert - run.sh baut das Image immer (Cache), damit kein alter Stand haengen bleibt - Tests bilden den Fall nach: Zone nur als Abo auffindbar, Auflisten verboten Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fd76c18eb0
commit
611593b5c0
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""Wildcard Let's Encrypt certificate creator for Plesk-managed DNS zones."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.0"
|
||||
|
||||
@@ -95,6 +95,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--ip", dest="ip_opt", help="alternative to the positional IP argument")
|
||||
parser.add_argument("--san", action="append", default=[],
|
||||
help="additional SAN (repeatable)")
|
||||
parser.add_argument("--zone",
|
||||
help="name the Plesk DNS zone explicitly instead of detecting it "
|
||||
"(e.g. --zone example.com for host.example.com)")
|
||||
parser.add_argument("--no-wildcard", action="store_true",
|
||||
help="only the plain name, without *.<fqdn>")
|
||||
parser.add_argument("--skip-dns", action="store_true",
|
||||
@@ -150,6 +153,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
log.info("=" * 62)
|
||||
|
||||
plesk = PleskClient(cfg)
|
||||
plesk.zone_hint = args.zone
|
||||
zone, domain_id = plesk.find_zone(fqdn)
|
||||
log.info("Plesk DNS zone: %s (domain id %s)", zone, domain_id)
|
||||
|
||||
|
||||
+87
-15
@@ -69,7 +69,10 @@ class PleskClient:
|
||||
self.session.headers.update(headers)
|
||||
if not cfg.plesk_verify_tls:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
self.zone_hint: str | None = None
|
||||
self._zone_cache: dict[str, str] | None = None
|
||||
self._list_problems: list[str] = []
|
||||
self._fqdn_zone_cache: dict[str, tuple[str, str]] = {}
|
||||
|
||||
# -- low level ---------------------------------------------------------
|
||||
|
||||
@@ -122,6 +125,7 @@ class PleskClient:
|
||||
return self._zone_cache
|
||||
|
||||
zones: dict[str, str] = {}
|
||||
problems: list[str] = []
|
||||
for operator in ("site", "webspace"):
|
||||
op = ET.Element(operator)
|
||||
get = ET.SubElement(op, "get")
|
||||
@@ -132,10 +136,14 @@ class PleskClient:
|
||||
root = self._request(op)
|
||||
except PleskError as exc:
|
||||
log.debug("%s.get failed: %s", operator, exc)
|
||||
problems.append(f"{operator}.get: {exc}")
|
||||
continue
|
||||
|
||||
for result in root.findall(f"./{operator}/get/result"):
|
||||
if result.findtext("status") != "ok":
|
||||
errtext = result.findtext("errtext")
|
||||
if errtext:
|
||||
problems.append(f"{operator}.get: {errtext}")
|
||||
continue
|
||||
domain_id = result.findtext("id")
|
||||
gen_info = result.find("./data/gen_info")
|
||||
@@ -146,26 +154,86 @@ class PleskClient:
|
||||
if name:
|
||||
zones.setdefault(normalise_name(name), domain_id)
|
||||
|
||||
if not zones:
|
||||
raise PleskError(
|
||||
"No domains found on the Plesk server. Does the API user have access to any subscription?"
|
||||
)
|
||||
self._zone_cache = zones
|
||||
self._list_problems = problems
|
||||
log.debug("Known Plesk zones: %s", sorted(zones))
|
||||
return zones
|
||||
|
||||
def _domain_id_by_name(self, name: str) -> str | None:
|
||||
"""Ask Plesk directly for one domain/subscription. None if it does not exist."""
|
||||
for operator in ("site", "webspace"):
|
||||
op = ET.Element(operator)
|
||||
get = ET.SubElement(op, "get")
|
||||
flt = ET.SubElement(get, "filter")
|
||||
ET.SubElement(flt, "name").text = name
|
||||
dataset = ET.SubElement(get, "dataset")
|
||||
ET.SubElement(dataset, "gen_info")
|
||||
try:
|
||||
root = self._request(op)
|
||||
except PleskError as exc:
|
||||
log.debug("%s.get(%s) failed: %s", operator, name, exc)
|
||||
continue
|
||||
|
||||
for result in root.findall(f"./{operator}/get/result"):
|
||||
if result.findtext("status") == "ok":
|
||||
domain_id = result.findtext("id")
|
||||
if domain_id:
|
||||
log.debug("Plesk %s %r has id %s", operator, name, domain_id)
|
||||
return domain_id
|
||||
else:
|
||||
log.debug("%s.get(%s): %s", operator, name, result.findtext("errtext", ""))
|
||||
return None
|
||||
|
||||
def find_zone(self, fqdn: str) -> tuple[str, str]:
|
||||
"""Find the most specific Plesk zone hosting *fqdn*. Returns (zone_name, domain_id)."""
|
||||
"""Find the Plesk zone hosting *fqdn*. Returns (zone_name, domain_id).
|
||||
|
||||
Asks Plesk for each candidate name from the most specific one upwards
|
||||
(``a.b.example.com`` -> ``b.example.com`` -> ``example.com``), so a name
|
||||
that only exists as a DNS record inside a zone works as well. Enumerating
|
||||
every domain on the server is not required - and often not permitted.
|
||||
"""
|
||||
fqdn = normalise_name(fqdn)
|
||||
cached = self._fqdn_zone_cache.get(fqdn)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
if self.zone_hint:
|
||||
zone = normalise_name(self.zone_hint)
|
||||
if fqdn != zone and not fqdn.endswith("." + zone):
|
||||
raise PleskError(f"{fqdn!r} is not inside the zone {zone!r} given via --zone.")
|
||||
domain_id = self._domain_id_by_name(zone)
|
||||
if not domain_id:
|
||||
raise PleskError(f"Plesk does not know a domain or subscription named {zone!r}.")
|
||||
self._fqdn_zone_cache[fqdn] = (zone, domain_id)
|
||||
return zone, domain_id
|
||||
|
||||
labels = fqdn.split(".")
|
||||
tried: list[str] = []
|
||||
for index in range(len(labels) - 1):
|
||||
candidate = ".".join(labels[index:])
|
||||
tried.append(candidate)
|
||||
domain_id = self._domain_id_by_name(candidate)
|
||||
if domain_id:
|
||||
if candidate != fqdn:
|
||||
log.info("%s is a record inside the Plesk zone %s", fqdn, candidate)
|
||||
self._fqdn_zone_cache[fqdn] = (candidate, domain_id)
|
||||
return candidate, domain_id
|
||||
|
||||
# Nothing matched - build the most helpful error we can.
|
||||
message = [
|
||||
f"No Plesk DNS zone found for {fqdn!r}.",
|
||||
f"Tried: {', '.join(tried)}.",
|
||||
]
|
||||
zones = self.list_zones()
|
||||
candidates = [z for z in zones if fqdn == z or fqdn.endswith("." + z)]
|
||||
if not candidates:
|
||||
raise PleskError(
|
||||
f"No Plesk DNS zone found for {fqdn!r}. "
|
||||
f"Known zones: {', '.join(sorted(zones)) or '<none>'}"
|
||||
if zones:
|
||||
message.append(f"Domains visible to this API user: {', '.join(sorted(zones))}.")
|
||||
else:
|
||||
message.append(
|
||||
"This API user cannot see any domain on the server either "
|
||||
"(use --zone <zone> to name the zone explicitly)."
|
||||
)
|
||||
zone = max(candidates, key=len)
|
||||
return zone, zones[zone]
|
||||
message.extend(f"Plesk said: {p}" for p in self._list_problems)
|
||||
raise PleskError(" ".join(message))
|
||||
|
||||
# -- records -----------------------------------------------------------
|
||||
|
||||
@@ -180,9 +248,13 @@ class PleskClient:
|
||||
for result in root.findall("./dns/get_rec/result"):
|
||||
status = result.findtext("status")
|
||||
if status != "ok":
|
||||
errtext = result.findtext("errtext", "")
|
||||
# An empty zone reports an error instead of an empty list on some versions.
|
||||
log.debug("get_rec result not ok: %s", errtext)
|
||||
# A zone without records reports an error instead of an empty list -
|
||||
# worth a warning, since a healthy zone always has SOA/NS entries.
|
||||
log.warning(
|
||||
"Plesk returned no DNS records for domain id %s: %s",
|
||||
domain_id,
|
||||
result.findtext("errtext", "unknown reason"),
|
||||
)
|
||||
continue
|
||||
data = result.find("data")
|
||||
if data is None:
|
||||
|
||||
Reference in New Issue
Block a user