776 lines
28 KiB
Python
776 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""Deploy a Console certkey to ADCs and link CA certs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import getpass
|
|
import json
|
|
import os
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from certctl.console import NitroConsoleClient, NitroError
|
|
|
|
_DEBUG = False
|
|
|
|
|
|
def _set_debug(enabled: bool) -> None:
|
|
global _DEBUG
|
|
_DEBUG = bool(enabled)
|
|
|
|
|
|
def _redact_payload(value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
redacted = {}
|
|
for key, item in value.items():
|
|
if key in {"password", "key_data", "certificate_data", "token", "secret"}:
|
|
redacted[key] = "<redacted>"
|
|
continue
|
|
if key == "cert_data" and isinstance(item, dict):
|
|
redacted[key] = {**item, "file_data": "<redacted>"} if "file_data" in item else item
|
|
continue
|
|
if key == "certchain_data" and isinstance(item, list):
|
|
redacted[key] = [
|
|
{**entry, "file_data": "<redacted>"} if isinstance(entry, dict) and "file_data" in entry else entry
|
|
for entry in item
|
|
]
|
|
continue
|
|
redacted[key] = _redact_payload(item)
|
|
return redacted
|
|
if isinstance(value, list):
|
|
return [_redact_payload(item) for item in value]
|
|
return value
|
|
|
|
|
|
def _debug_payload(label: str, payload: Dict[str, Any]) -> None:
|
|
if not _DEBUG:
|
|
return
|
|
print(f"[debug] {label}:")
|
|
print(json.dumps(_redact_payload(payload), indent=2, sort_keys=True))
|
|
|
|
|
|
def _load_config(path: str) -> Dict[str, Any]:
|
|
config_path = Path(path).expanduser()
|
|
raw = config_path.read_text(encoding="utf-8")
|
|
if config_path.suffix.lower() in (".yaml", ".yml"):
|
|
try:
|
|
import yaml # type: ignore
|
|
except Exception as exc: # pragma: no cover - optional dependency
|
|
raise RuntimeError("PyYAML is required for YAML config files.") from exc
|
|
data = yaml.safe_load(raw) or {}
|
|
else:
|
|
data = json.loads(raw or "{}")
|
|
if not isinstance(data, dict):
|
|
raise RuntimeError("Config file must contain a JSON/YAML object at the root.")
|
|
return data
|
|
|
|
|
|
def _apply_config(args: argparse.Namespace) -> argparse.Namespace:
|
|
if not args.config:
|
|
return args
|
|
|
|
data = _load_config(args.config)
|
|
defaults = data.get("defaults", {}) if isinstance(data.get("defaults"), dict) else {}
|
|
consoles = data.get("consoles", {}) if isinstance(data.get("consoles"), dict) else {}
|
|
profile = args.profile or data.get("default_profile") or "default"
|
|
profile_data = consoles.get(profile, {}) if isinstance(consoles.get(profile), dict) else {}
|
|
|
|
merged: Dict[str, Any] = {}
|
|
merged.update(defaults)
|
|
merged.update(profile_data)
|
|
|
|
def pick(name: str, current: Any, fallback: Any = None) -> Any:
|
|
if current is not None:
|
|
return current
|
|
return merged.get(name, fallback)
|
|
|
|
args.console = pick("console", args.console) or merged.get("url", args.console)
|
|
args.user = pick("user", args.user)
|
|
args.ca_bundle = pick("ca_bundle", args.ca_bundle)
|
|
args.insecure = bool(pick("insecure", args.insecure, False))
|
|
args.timeout = pick("timeout", args.timeout, 60)
|
|
return args
|
|
|
|
|
|
def _load_password(username: str, console: str, provided: Optional[str]) -> str:
|
|
if provided:
|
|
return provided
|
|
env_pw = os.environ.get("CERTCTL_CONSOLE_PASSWORD")
|
|
if env_pw:
|
|
return env_pw
|
|
return getpass.getpass(f"Console password for {username}@{console}: ")
|
|
|
|
|
|
def _parse_cn(subject: str) -> Optional[str]:
|
|
if not subject:
|
|
return None
|
|
match = re.search(r"(?:^|[,/\\s])CN\\s*=\\s*([^,/]+)", subject)
|
|
if match:
|
|
return match.group(1).strip()
|
|
return None
|
|
|
|
|
|
def _list_certkeys(client: NitroConsoleClient) -> List[Dict[str, Any]]:
|
|
payload = client.get_json("/nitro/v2/config/ns_ssl_certkey", params={"pagesize": "200"})
|
|
items = payload.get("ns_ssl_certkey", [])
|
|
if isinstance(items, dict):
|
|
return [items]
|
|
if isinstance(items, list):
|
|
return [item for item in items if isinstance(item, dict)]
|
|
return []
|
|
|
|
|
|
def _find_certkey(client: NitroConsoleClient, name: str, *, resolve_cn: bool) -> Dict[str, Any]:
|
|
payload = client.get_json(
|
|
"/nitro/v2/config/ns_ssl_certkey",
|
|
params={"filter": f"certkeypair_name:{name}"},
|
|
)
|
|
items = payload.get("ns_ssl_certkey", [])
|
|
if isinstance(items, dict):
|
|
return items
|
|
if isinstance(items, list) and items:
|
|
return items[0]
|
|
normalized = _normalize_certkey_name(name)
|
|
if normalized and normalized != name:
|
|
payload = client.get_json(
|
|
"/nitro/v2/config/ns_ssl_certkey",
|
|
params={"filter": f"certkeypair_name:{normalized}"},
|
|
)
|
|
items = payload.get("ns_ssl_certkey", [])
|
|
if isinstance(items, dict):
|
|
return items
|
|
if isinstance(items, list) and items:
|
|
return items[0]
|
|
if not resolve_cn:
|
|
raise SystemExit(f"Certkeypair not found: {name}")
|
|
|
|
candidates = _list_certkeys(client)
|
|
matches = []
|
|
for cert in candidates:
|
|
subject = str(cert.get("subject") or cert.get("certificate_dn") or "")
|
|
cn = _parse_cn(subject)
|
|
if cn and cn.lower() == name.lower():
|
|
matches.append(cert)
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
if matches:
|
|
names = ", ".join(sorted({m.get("certkeypair_name", "unknown") for m in matches}))
|
|
raise SystemExit(f"Multiple certkeypairs match CN {name}: {names}")
|
|
raise SystemExit(f"Certkeypair not found: {name}")
|
|
|
|
|
|
def _trigger_inventory(client: NitroConsoleClient) -> None:
|
|
client.post_json(
|
|
"/nitro/v2/config/ns_ssl_certkey",
|
|
{"ns_ssl_certkey": {}},
|
|
params={"action": "inventory"},
|
|
)
|
|
|
|
|
|
def _poll_adcs(client: NitroConsoleClient, ns_ips: List[str]) -> None:
|
|
if not ns_ips:
|
|
return
|
|
payload = {"ns_ssl_certkey_policy": {"ns_ip_address": ns_ips}}
|
|
client.post_json("/nitro/v2/config/ns_ssl_certkey_policy", payload, params={"action": "do_poll"})
|
|
|
|
|
|
def _normalize_certkey_name(name: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9]+", "_", name or "").strip("_")
|
|
|
|
|
|
def _list_entity_certs(client: NitroConsoleClient, ns_ip: str) -> List[Dict[str, Any]]:
|
|
payload = {"ns_ssl_certkey": {"source_ipaddress": ns_ip}}
|
|
data = client.post_json("/nitro/v2/config/ns_ssl_certkey", payload, params={"action": "list_entity_cert"})
|
|
items = data.get("ns_ssl_certkey", [])
|
|
if isinstance(items, dict):
|
|
return [items]
|
|
if isinstance(items, list):
|
|
return [item for item in items if isinstance(item, dict)]
|
|
return []
|
|
|
|
|
|
def _try_import_with_name(
|
|
client: NitroConsoleClient,
|
|
target_name: str,
|
|
*,
|
|
ns_ip: str,
|
|
source_name: str,
|
|
) -> Tuple[bool, Optional[str]]:
|
|
payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": target_name,
|
|
"ns_ip_address_arr": [ns_ip],
|
|
"source_ipaddress": ns_ip,
|
|
"source_certificate": source_name,
|
|
}
|
|
}
|
|
try:
|
|
_debug_payload("ns_ssl_certkey POST (import)", payload)
|
|
client.post_json("/nitro/v2/config/ns_ssl_certkey", payload)
|
|
except NitroError as exc:
|
|
return False, f"{exc.status_code} {exc.message}"
|
|
return True, None
|
|
|
|
|
|
def _match_adc_cert(certs: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]:
|
|
matches = []
|
|
normalized = _normalize_certkey_name(name)
|
|
for cert in certs:
|
|
cert_name = str(cert.get("certkeypair_name") or "")
|
|
if cert_name.lower() == name.lower():
|
|
matches.append(cert)
|
|
continue
|
|
if normalized and _normalize_certkey_name(cert_name).lower() == normalized.lower():
|
|
matches.append(cert)
|
|
continue
|
|
subject = str(cert.get("subject") or cert.get("certificate_dn") or "")
|
|
cn = _parse_cn(subject)
|
|
if cn and cn.lower() == name.lower():
|
|
matches.append(cert)
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
if matches:
|
|
names = ", ".join(sorted({m.get("certkeypair_name", "unknown") for m in matches}))
|
|
raise SystemExit(f"Multiple ADC certs match {name}: {names}")
|
|
return None
|
|
|
|
|
|
def _import_from_adc(
|
|
client: NitroConsoleClient,
|
|
target_name: str,
|
|
*,
|
|
adc_ips: List[str],
|
|
) -> bool:
|
|
last_error = None
|
|
for ns_ip in adc_ips:
|
|
try:
|
|
certs = _list_entity_certs(client, ns_ip)
|
|
except NitroError as exc:
|
|
message = str(exc.message).lower()
|
|
if exc.status_code not in (404, 405) and "not supported" not in message:
|
|
raise
|
|
candidates = [target_name, _normalize_certkey_name(target_name)]
|
|
seen = set()
|
|
for candidate in candidates:
|
|
if not candidate or candidate in seen:
|
|
continue
|
|
seen.add(candidate)
|
|
ok, err = _try_import_with_name(client, target_name, ns_ip=ns_ip, source_name=candidate)
|
|
if ok:
|
|
return True
|
|
last_error = err
|
|
continue
|
|
match = _match_adc_cert(certs, target_name)
|
|
if not match:
|
|
continue
|
|
source_name = match.get("certkeypair_name") or target_name
|
|
ok, err = _try_import_with_name(client, target_name, ns_ip=ns_ip, source_name=source_name)
|
|
if ok:
|
|
return True
|
|
last_error = err
|
|
if last_error:
|
|
raise SystemExit(f"Console import failed: {last_error}")
|
|
return False
|
|
|
|
|
|
def _find_certkey_with_sync(
|
|
client: NitroConsoleClient,
|
|
name: str,
|
|
*,
|
|
resolve_cn: bool,
|
|
sync: bool,
|
|
sync_wait: int,
|
|
import_missing: bool,
|
|
import_wait: int,
|
|
import_adc_ips: List[str],
|
|
) -> Dict[str, Any]:
|
|
try:
|
|
return _find_certkey(client, name, resolve_cn=resolve_cn)
|
|
except SystemExit:
|
|
pass
|
|
if sync:
|
|
_trigger_inventory(client)
|
|
if sync_wait > 0:
|
|
time.sleep(sync_wait)
|
|
try:
|
|
return _find_certkey(client, name, resolve_cn=resolve_cn)
|
|
except SystemExit:
|
|
pass
|
|
if not import_missing:
|
|
raise SystemExit(f"Certkeypair not found: {name}")
|
|
if not import_adc_ips:
|
|
raise SystemExit("Cannot import missing cert without --adc-ip or --list-adc menu selection.")
|
|
if not _import_from_adc(client, name, adc_ips=import_adc_ips):
|
|
raise SystemExit(f"Certkeypair not found on ADCs: {name}")
|
|
print(f"Imported cert from ADCs for {name}. Refreshing Console inventory...")
|
|
_trigger_inventory(client)
|
|
if import_wait > 0:
|
|
print(f"Waiting {import_wait} seconds as requested...")
|
|
time.sleep(import_wait)
|
|
try:
|
|
return _find_certkey(client, name, resolve_cn=resolve_cn)
|
|
except SystemExit as exc:
|
|
raise SystemExit(
|
|
f"Imported cert from ADC but not yet visible in Console inventory: {name}. "
|
|
"Try --import-wait or --sync --sync-wait."
|
|
) from exc
|
|
|
|
|
|
def _extract_bindings(cert: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
bindings = cert.get("entity_binding_arr") or []
|
|
if isinstance(bindings, dict):
|
|
bindings = [bindings]
|
|
if isinstance(bindings, list) and bindings:
|
|
return [b for b in bindings if isinstance(b, dict)]
|
|
ips = cert.get("ns_ip_address_arr") or []
|
|
if isinstance(ips, str):
|
|
ips = [ips]
|
|
if isinstance(ips, list):
|
|
return [{"ns_ip_address": ip} for ip in ips]
|
|
return []
|
|
|
|
|
|
def _bind_cert(
|
|
client: NitroConsoleClient,
|
|
certkeypair: str,
|
|
cert_id: Optional[str],
|
|
binding: Dict[str, Any],
|
|
) -> None:
|
|
payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": certkeypair,
|
|
"entity_binding_arr": [
|
|
{
|
|
"certkeypair_name": certkeypair,
|
|
"ns_ip_address": binding.get("ns_ip_address"),
|
|
"id": binding.get("id"),
|
|
}
|
|
],
|
|
}
|
|
}
|
|
try:
|
|
_debug_payload("ns_ssl_certkey POST (bind_cert)", payload)
|
|
client.post_json("/nitro/v2/config/ns_ssl_certkey", payload, params={"action": "bind_cert"})
|
|
return
|
|
except NitroError as exc:
|
|
message = str(exc.message).lower()
|
|
if exc.status_code not in (404, 405) and "not supported" not in message:
|
|
raise
|
|
if not cert_id:
|
|
raise SystemExit("Console does not support bind_cert and cert id is missing for fallback.")
|
|
modify_payload = {
|
|
"ns_ssl_certkey": {
|
|
"id": cert_id,
|
|
"certkeypair_name": certkeypair,
|
|
"entity_binding_arr": payload["ns_ssl_certkey"]["entity_binding_arr"],
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certkey PUT (bind_cert fallback)", modify_payload)
|
|
client.put_json(f"/nitro/v2/config/ns_ssl_certkey/{cert_id}", modify_payload)
|
|
|
|
|
|
def _cert_exists(client: NitroConsoleClient, filename: str) -> bool:
|
|
try:
|
|
client.get_json(f"/nitro/v2/config/ns_ssl_cert/{filename}")
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _certkey_exists(client: NitroConsoleClient, name: str) -> bool:
|
|
try:
|
|
_find_certkey(client, name)
|
|
return True
|
|
except SystemExit:
|
|
return False
|
|
|
|
|
|
def _upload_ca_cert(
|
|
client: NitroConsoleClient,
|
|
ca_name: str,
|
|
cert_path: Path,
|
|
ns_ip_addresses: List[str],
|
|
*,
|
|
client_user: str,
|
|
client_password: str,
|
|
) -> None:
|
|
if not _cert_exists(client, cert_path.name):
|
|
client.upload_file(
|
|
"/nitro/v2/upload/ns_ssl_cert",
|
|
str(cert_path),
|
|
basic_user=client_user,
|
|
basic_password=client_password,
|
|
)
|
|
if not _certkey_exists(client, ca_name):
|
|
payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": ca_name,
|
|
"ns_ip_address_arr": ns_ip_addresses,
|
|
"certificate_file_name": cert_path.name,
|
|
"ssl_certificate": cert_path.name,
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certkey POST (CA upload)", payload)
|
|
client.post_json("/nitro/v2/config/ns_ssl_certkey", payload)
|
|
|
|
|
|
def _link_ca(client: NitroConsoleClient, certkeypair: str, ca_name: str, ns_ip: str) -> None:
|
|
payload = {"ns_ssl_certlink": {"certkey": certkeypair, "linkcertkeyname": ca_name, "ns_ip_address": ns_ip}}
|
|
_debug_payload("ns_ssl_certlink POST (link)", payload)
|
|
client.post_json("/nitro/v2/config/ns_ssl_certlink", payload, params={"action": "link"})
|
|
|
|
|
|
def _parse_ca_inputs(args: argparse.Namespace) -> List[Tuple[str, Optional[Path]]]:
|
|
names = args.ca_certkey or []
|
|
files = args.ca_cert_file or []
|
|
pairs = []
|
|
for idx, name in enumerate(names):
|
|
path = Path(files[idx]).expanduser() if idx < len(files) else None
|
|
pairs.append((name, path))
|
|
return pairs
|
|
|
|
|
|
def _extract_chain_names(cert: Dict[str, Any]) -> List[str]:
|
|
chain = cert.get("certkeychain") or []
|
|
if isinstance(chain, dict):
|
|
chain = [chain]
|
|
names = []
|
|
for entry in chain:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
name = entry.get("cert_name") or entry.get("certificate_file_name") or entry.get("linked_to")
|
|
if name and name not in names:
|
|
names.append(name)
|
|
return names
|
|
|
|
|
|
def _extract_adc_ip(device: Dict[str, Any]) -> Optional[str]:
|
|
return (
|
|
device.get("ip_address")
|
|
or device.get("mgmt_ip_address")
|
|
or device.get("device_host_ip")
|
|
or device.get("ipv4_address")
|
|
)
|
|
|
|
|
|
def _is_primary(device: Dict[str, Any]) -> bool:
|
|
state = str(device.get("ha_master_state") or "").lower()
|
|
if state in ("primary", "master"):
|
|
return True
|
|
if state in ("secondary", "slave"):
|
|
return False
|
|
# If HA status is unknown, default to include.
|
|
return True
|
|
|
|
|
|
def _list_managed_devices(client: NitroConsoleClient, *, primary_only: bool) -> List[Dict[str, Any]]:
|
|
payload = client.get_json("/nitro/v2/config/managed_device")
|
|
items = payload.get("managed_device", [])
|
|
if isinstance(items, dict):
|
|
items = [items]
|
|
if isinstance(items, list):
|
|
devices = [item for item in items if isinstance(item, dict)]
|
|
if primary_only:
|
|
devices = [device for device in devices if _is_primary(device)]
|
|
return devices
|
|
return []
|
|
|
|
|
|
def _write_adc_list_json(path: Path, devices: List[Dict[str, Any]]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
rows = []
|
|
for device in devices:
|
|
rows.append(
|
|
{
|
|
"id": device.get("id"),
|
|
"name": device.get("name"),
|
|
"display_name": device.get("display_name"),
|
|
"hostname": device.get("hostname"),
|
|
"device_family": device.get("device_family"),
|
|
"ip_address": _extract_adc_ip(device),
|
|
"version": device.get("version"),
|
|
"status": device.get("status"),
|
|
"ha_master_state": device.get("ha_master_state"),
|
|
"ha_ip_address": device.get("ha_ip_address"),
|
|
"is_ha_configured": device.get("is_ha_configured"),
|
|
"cluster_node_ip_list": device.get("cluster_node_ip_list"),
|
|
}
|
|
)
|
|
path.write_text(json.dumps(rows, indent=2, sort_keys=True), encoding="utf-8")
|
|
|
|
|
|
def _select_adc_menu(devices: List[Dict[str, Any]]) -> List[str]:
|
|
rows = []
|
|
for idx, device in enumerate(devices, start=1):
|
|
ip = _extract_adc_ip(device) or "unknown"
|
|
name = device.get("display_name") or device.get("name") or device.get("hostname") or "unknown"
|
|
state = device.get("ha_master_state") or "unknown"
|
|
rows.append((idx, name, ip, state))
|
|
|
|
for idx, name, ip, state in rows:
|
|
print(f"{idx:>2}) {name} [{ip}] (HA: {state})")
|
|
choice = input("Select ADCs (comma-separated numbers) or 'q' to cancel: ").strip()
|
|
if not choice or choice.lower() == "q":
|
|
return []
|
|
indices = {int(part.strip()) for part in choice.split(",") if part.strip().isdigit()}
|
|
selected_ips = []
|
|
for idx, _, ip, _ in rows:
|
|
if idx in indices and ip != "unknown":
|
|
selected_ips.append(ip)
|
|
return selected_ips
|
|
|
|
|
|
def run(args: argparse.Namespace) -> int:
|
|
args = _apply_config(args)
|
|
_set_debug(args.debug)
|
|
if args.dry_run:
|
|
_set_debug(True)
|
|
if args.list_adc:
|
|
raise SystemExit("--list-adc is not supported with --dry-run.")
|
|
source_name = args.source_certkeypair or args.certkeypair
|
|
ns_ips = args.adc_ip or ["<adc-ip>"]
|
|
print("[dry-run] ADC deploy payloads:")
|
|
if args.import_missing:
|
|
for ns_ip in ns_ips:
|
|
payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": args.certkeypair,
|
|
"ns_ip_address_arr": [ns_ip],
|
|
"source_ipaddress": ns_ip,
|
|
"source_certificate": source_name,
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certkey POST (import)", payload)
|
|
for ns_ip in ns_ips:
|
|
payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": args.certkeypair,
|
|
"entity_binding_arr": [
|
|
{
|
|
"certkeypair_name": args.certkeypair,
|
|
"ns_ip_address": ns_ip,
|
|
"id": "<binding-id>",
|
|
}
|
|
],
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certkey POST (bind_cert)", payload)
|
|
ca_pairs = _parse_ca_inputs(args)
|
|
if args.link_ca:
|
|
if not ca_pairs:
|
|
ca_pairs = [("CA_CERTKEY", None)]
|
|
for ca_name, ca_path in ca_pairs:
|
|
if ca_path:
|
|
upload_payload = {
|
|
"ns_ssl_certkey": {
|
|
"certkeypair_name": ca_name,
|
|
"ns_ip_address_arr": ns_ips,
|
|
"certificate_file_name": ca_path.name,
|
|
"ssl_certificate": ca_path.name,
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certkey POST (CA upload)", upload_payload)
|
|
for ns_ip in ns_ips:
|
|
link_payload = {
|
|
"ns_ssl_certlink": {
|
|
"certkey": args.certkeypair,
|
|
"linkcertkeyname": ca_name,
|
|
"ns_ip_address": ns_ip,
|
|
}
|
|
}
|
|
_debug_payload("ns_ssl_certlink POST (link)", link_payload)
|
|
return 0
|
|
if not args.console or not args.user:
|
|
raise SystemExit("Console URL and user are required (use CLI args or --config/--profile).")
|
|
|
|
verify: Any
|
|
if args.insecure:
|
|
verify = False
|
|
elif args.ca_bundle:
|
|
verify = args.ca_bundle
|
|
else:
|
|
verify = True
|
|
|
|
password = _load_password(args.user, args.console, args.password)
|
|
client = NitroConsoleClient(base=args.console, verify=verify, timeout=args.timeout)
|
|
client.login(args.user, password)
|
|
|
|
if args.list_adc:
|
|
devices = _list_managed_devices(client, primary_only=not args.all_adc)
|
|
if args.list_adc == "json":
|
|
out_path = Path(args.list_adc_out)
|
|
_write_adc_list_json(out_path, devices)
|
|
print(f"Wrote ADC list: {out_path}")
|
|
return 0
|
|
if args.list_adc == "menu":
|
|
selected = _select_adc_menu(devices)
|
|
if not selected:
|
|
raise SystemExit("No ADCs selected.")
|
|
args.adc_ip = selected
|
|
if args.poll_adc:
|
|
_poll_adcs(client, selected)
|
|
if args.poll_wait > 0:
|
|
time.sleep(args.poll_wait)
|
|
|
|
import_adc_ips = args.adc_ip or []
|
|
|
|
source_name = args.source_certkeypair or args.certkeypair
|
|
source_cert = _find_certkey_with_sync(
|
|
client,
|
|
source_name,
|
|
resolve_cn=args.resolve_cn,
|
|
sync=args.sync,
|
|
sync_wait=args.sync_wait,
|
|
import_missing=args.import_missing,
|
|
import_wait=args.import_wait,
|
|
import_adc_ips=import_adc_ips,
|
|
)
|
|
if args.certkeypair == source_name:
|
|
target_cert = source_cert
|
|
else:
|
|
target_cert = _find_certkey_with_sync(
|
|
client,
|
|
args.certkeypair,
|
|
resolve_cn=args.resolve_cn,
|
|
sync=args.sync,
|
|
sync_wait=args.sync_wait,
|
|
import_missing=args.import_missing,
|
|
import_wait=args.import_wait,
|
|
import_adc_ips=import_adc_ips,
|
|
)
|
|
bindings = _extract_bindings(source_cert)
|
|
if not bindings and args.adc_ip:
|
|
bindings = [{"ns_ip_address": ip} for ip in args.adc_ip]
|
|
if not bindings:
|
|
raise SystemExit(f"No bindings found for certkeypair: {source_name}")
|
|
|
|
ns_ips = [b.get("ns_ip_address") for b in bindings if b.get("ns_ip_address")]
|
|
if ns_ips:
|
|
print(f"Using ADC bindings for {source_name}: {', '.join(ns_ips)}")
|
|
else:
|
|
print(f"No ADC IPs resolved for {source_name}; bindings will be attempted without IPs.")
|
|
|
|
ca_pairs = _parse_ca_inputs(args)
|
|
if args.link_ca and not ca_pairs:
|
|
chain_names = _extract_chain_names(source_cert)
|
|
ca_pairs = [(name, None) for name in chain_names]
|
|
if args.link_ca:
|
|
if ca_pairs:
|
|
ca_names = ", ".join([name for name, _ in ca_pairs if name])
|
|
print(f"CA linking enabled. CA certs: {ca_names}")
|
|
else:
|
|
print("CA linking enabled but no CA certs specified or found in chain metadata.")
|
|
for ca_name, ca_path in ca_pairs:
|
|
if ca_path:
|
|
_upload_ca_cert(
|
|
client,
|
|
ca_name,
|
|
ca_path,
|
|
ns_ips,
|
|
client_user=args.user,
|
|
client_password=password,
|
|
)
|
|
|
|
for binding in bindings:
|
|
_bind_cert(client, args.certkeypair, target_cert.get("id"), binding)
|
|
if args.link_ca:
|
|
for ca_name, _ in ca_pairs:
|
|
_link_ca(client, args.certkeypair, ca_name, binding.get("ns_ip_address"))
|
|
|
|
print(f"Deployed {args.certkeypair} to {len(bindings)} ADC(s).")
|
|
return 0
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Deploy a Console certkey to ADCs via bindings.")
|
|
parser.add_argument("--certkeypair", required=True, help="Target certkeypair name to deploy")
|
|
parser.add_argument("--source-certkeypair", help="Source certkeypair name to copy bindings from")
|
|
parser.add_argument("--adc-ip", action="append", help="Target ADC IP (repeatable)")
|
|
parser.add_argument(
|
|
"--list-adc",
|
|
choices=["json", "menu"],
|
|
help="List managed ADCs and exit or select interactively",
|
|
)
|
|
parser.add_argument(
|
|
"--all-adc",
|
|
action="store_true",
|
|
default=False,
|
|
help="Include non-primary HA nodes in ADC listing",
|
|
)
|
|
parser.add_argument(
|
|
"--list-adc-out",
|
|
default="./out/managed_devices.json",
|
|
help="Output path for --list-adc json",
|
|
)
|
|
parser.add_argument("--link-ca", action="store_true", default=True, help="Link CA certs after deploy")
|
|
parser.add_argument("--no-link-ca", dest="link_ca", action="store_false", help="Disable CA linking")
|
|
parser.add_argument("--ca-certkey", action="append", help="CA certkeypair name to link (repeatable)")
|
|
parser.add_argument("--ca-cert-file", action="append", help="CA cert PEM file to upload (repeatable)")
|
|
parser.add_argument("--console", help="Console base URL, e.g. https://console")
|
|
parser.add_argument("--user", help="Console username")
|
|
parser.add_argument("--password", help="Console password (or set CERTCTL_CONSOLE_PASSWORD)")
|
|
parser.add_argument("--config", help="Path to JSON/YAML config file")
|
|
parser.add_argument("--profile", help="Config profile name (default: default)")
|
|
parser.add_argument("--insecure", action="store_true", help="Disable TLS verification")
|
|
parser.add_argument("--ca-bundle", help="Path to CA bundle for TLS verification")
|
|
parser.add_argument("--timeout", type=int, default=60, help="HTTP timeout in seconds")
|
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging and payload output")
|
|
parser.add_argument("--dry-run", action="store_true", help="Print payloads without API calls")
|
|
parser.add_argument(
|
|
"--sync",
|
|
action="store_true",
|
|
default=False,
|
|
help="Trigger Console inventory refresh before looking up certs",
|
|
)
|
|
parser.add_argument(
|
|
"--sync-wait",
|
|
type=int,
|
|
default=0,
|
|
help="Seconds to wait after inventory refresh (default: 0)",
|
|
)
|
|
parser.add_argument(
|
|
"--import-missing",
|
|
action="store_true",
|
|
default=False,
|
|
help="Import missing certkeypairs from ADCs via Console proxy before failing",
|
|
)
|
|
parser.add_argument(
|
|
"--import-wait",
|
|
type=int,
|
|
default=0,
|
|
help="Seconds to wait after importing from ADCs (default: 0)",
|
|
)
|
|
parser.add_argument(
|
|
"--poll-adc",
|
|
action="store_true",
|
|
default=False,
|
|
help="Poll selected ADCs via Console before lookup (requires --list-adc menu or --adc-ip)",
|
|
)
|
|
parser.add_argument(
|
|
"--poll-wait",
|
|
type=int,
|
|
default=0,
|
|
help="Seconds to wait after ADC poll (default: 0)",
|
|
)
|
|
parser.add_argument(
|
|
"--no-resolve-cn",
|
|
dest="resolve_cn",
|
|
action="store_false",
|
|
help="Disable CN lookup when certkeypair name is not found",
|
|
)
|
|
parser.set_defaults(resolve_cn=True)
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
parser = build_arg_parser()
|
|
args = parser.parse_args()
|
|
raise SystemExit(run(args))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|