Fix NPM integration: correct forward host, SSL, and add UDP stream
- Forward proxy to host IP + dashboard_port instead of container name - Remove redundant advanced_config (Caddy handles internal routing) - Add provider: letsencrypt to SSL certificate request - Add NPM UDP stream creation/deletion for STUN/TURN relay ports - Add npm_stream_id to Deployment model with migration - Fix API docs URL in README (/api/docs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@ Per customer instance (5 containers): **~100 MB RAM**
|
|||||||
| Port | Protocol | Purpose |
|
| Port | Protocol | Purpose |
|
||||||
|------|----------|---------|
|
|------|----------|---------|
|
||||||
| 8000 | TCP | NetBird MSP Appliance Web UI |
|
| 8000 | TCP | NetBird MSP Appliance Web UI |
|
||||||
| 9000+ | TCP | NetBird Web Management per customer (one per customer, increments by 1) |
|
| 9000+ | TCP | NetBird Web Management per customer (only internal, one per customer, increments by 1) |
|
||||||
| 3478+ | UDP | STUN/TURN relay per customer (one per customer, increments by 1) |
|
| 3478+ | UDP | STUN/TURN relay per customer (one per customer, increments by 1) |
|
||||||
|
|
||||||
Example for 3 customers:
|
Example for 3 customers:
|
||||||
@@ -330,7 +330,7 @@ curl -X POST http://localhost:8000/api/auth/token \
|
|||||||
|
|
||||||
Full interactive documentation available at:
|
Full interactive documentation available at:
|
||||||
```
|
```
|
||||||
http://your-server:8000/docs
|
http://your-server:8000/api/docs
|
||||||
```
|
```
|
||||||
|
|
||||||
**Common Endpoints:**
|
**Common Endpoints:**
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ def _run_migrations() -> None:
|
|||||||
("system_config", "branding_subtitle", "TEXT DEFAULT 'Multi-Tenant Management Platform'"),
|
("system_config", "branding_subtitle", "TEXT DEFAULT 'Multi-Tenant Management Platform'"),
|
||||||
("system_config", "default_language", "TEXT DEFAULT 'en'"),
|
("system_config", "default_language", "TEXT DEFAULT 'en'"),
|
||||||
("users", "default_language", "TEXT"),
|
("users", "default_language", "TEXT"),
|
||||||
|
("deployments", "npm_stream_id", "INTEGER"),
|
||||||
]
|
]
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
if not _has_column(table, column):
|
if not _has_column(table, column):
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class Deployment(Base):
|
|||||||
relay_udp_port: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
|
relay_udp_port: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
|
||||||
dashboard_port: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
dashboard_port: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||||
npm_proxy_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
npm_proxy_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||||
|
npm_stream_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||||
relay_secret: Mapped[str] = mapped_column(Text, nullable=False)
|
relay_secret: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
setup_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
setup_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
netbird_admin_email: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
netbird_admin_email: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
@@ -111,6 +112,7 @@ class Deployment(Base):
|
|||||||
"relay_udp_port": self.relay_udp_port,
|
"relay_udp_port": self.relay_udp_port,
|
||||||
"dashboard_port": self.dashboard_port,
|
"dashboard_port": self.dashboard_port,
|
||||||
"npm_proxy_id": self.npm_proxy_id,
|
"npm_proxy_id": self.npm_proxy_id,
|
||||||
|
"npm_stream_id": self.npm_stream_id,
|
||||||
"relay_secret": "***", # Never expose secrets
|
"relay_secret": "***", # Never expose secrets
|
||||||
"setup_url": self.setup_url,
|
"setup_url": self.setup_url,
|
||||||
"has_credentials": bool(self.netbird_admin_email and self.netbird_admin_password),
|
"has_credentials": bool(self.netbird_admin_email and self.netbird_admin_password),
|
||||||
|
|||||||
@@ -220,20 +220,19 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
_log_action(db, customer_id, "deploy", "info",
|
_log_action(db, customer_id, "deploy", "info",
|
||||||
"Auto-setup failed — admin must complete setup manually.")
|
"Auto-setup failed — admin must complete setup manually.")
|
||||||
|
|
||||||
# Step 9: Create NPM proxy host (production only)
|
# Step 9: Create NPM proxy host + stream (production only)
|
||||||
npm_proxy_id = None
|
npm_proxy_id = None
|
||||||
|
npm_stream_id = None
|
||||||
if not local_mode:
|
if not local_mode:
|
||||||
caddy_container = f"netbird-kunde{customer_id}-caddy"
|
forward_host = npm_service._get_forward_host(config.npm_api_url)
|
||||||
npm_result = await npm_service.create_proxy_host(
|
npm_result = await npm_service.create_proxy_host(
|
||||||
api_url=config.npm_api_url,
|
api_url=config.npm_api_url,
|
||||||
npm_email=config.npm_api_email,
|
npm_email=config.npm_api_email,
|
||||||
npm_password=config.npm_api_password,
|
npm_password=config.npm_api_password,
|
||||||
domain=netbird_domain,
|
domain=netbird_domain,
|
||||||
forward_host=caddy_container,
|
forward_host=forward_host,
|
||||||
forward_port=80,
|
forward_port=dashboard_port,
|
||||||
admin_email=config.admin_email,
|
admin_email=config.admin_email,
|
||||||
subdomain=customer.subdomain,
|
|
||||||
customer_id=customer_id,
|
|
||||||
)
|
)
|
||||||
npm_proxy_id = npm_result.get("proxy_id")
|
npm_proxy_id = npm_result.get("proxy_id")
|
||||||
if npm_result.get("error"):
|
if npm_result.get("error"):
|
||||||
@@ -241,8 +240,34 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
db, customer_id, "deploy", "error",
|
db, customer_id, "deploy", "error",
|
||||||
f"NPM proxy creation failed: {npm_result['error']}",
|
f"NPM proxy creation failed: {npm_result['error']}",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
_log_action(
|
||||||
|
db, customer_id, "deploy", "info",
|
||||||
|
f"NPM proxy host created: {netbird_domain} -> {forward_host}:{dashboard_port}",
|
||||||
|
)
|
||||||
|
|
||||||
# Step 9: Create deployment record
|
# Create NPM UDP stream for relay STUN port
|
||||||
|
stream_result = await npm_service.create_stream(
|
||||||
|
api_url=config.npm_api_url,
|
||||||
|
npm_email=config.npm_api_email,
|
||||||
|
npm_password=config.npm_api_password,
|
||||||
|
incoming_port=allocated_port,
|
||||||
|
forwarding_host=forward_host,
|
||||||
|
forwarding_port=allocated_port,
|
||||||
|
)
|
||||||
|
npm_stream_id = stream_result.get("stream_id")
|
||||||
|
if stream_result.get("error"):
|
||||||
|
_log_action(
|
||||||
|
db, customer_id, "deploy", "error",
|
||||||
|
f"NPM stream creation failed: {stream_result['error']}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_log_action(
|
||||||
|
db, customer_id, "deploy", "info",
|
||||||
|
f"NPM UDP stream created: port {allocated_port} -> {forward_host}:{allocated_port}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 10: Create deployment record
|
||||||
setup_url = external_url
|
setup_url = external_url
|
||||||
|
|
||||||
deployment = Deployment(
|
deployment = Deployment(
|
||||||
@@ -251,6 +276,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
relay_udp_port=allocated_port,
|
relay_udp_port=allocated_port,
|
||||||
dashboard_port=dashboard_port,
|
dashboard_port=dashboard_port,
|
||||||
npm_proxy_id=npm_proxy_id,
|
npm_proxy_id=npm_proxy_id,
|
||||||
|
npm_stream_id=npm_stream_id,
|
||||||
relay_secret=encrypt_value(relay_secret),
|
relay_secret=encrypt_value(relay_secret),
|
||||||
setup_url=setup_url,
|
setup_url=setup_url,
|
||||||
netbird_admin_email=encrypt_value(admin_email) if setup_ok else None,
|
netbird_admin_email=encrypt_value(admin_email) if setup_ok else None,
|
||||||
@@ -330,6 +356,17 @@ async def undeploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_log_action(db, customer_id, "undeploy", "error", f"NPM removal error: {exc}")
|
_log_action(db, customer_id, "undeploy", "error", f"NPM removal error: {exc}")
|
||||||
|
|
||||||
|
# Remove NPM stream
|
||||||
|
if deployment.npm_stream_id and config.npm_api_email:
|
||||||
|
try:
|
||||||
|
await npm_service.delete_stream(
|
||||||
|
config.npm_api_url, config.npm_api_email, config.npm_api_password,
|
||||||
|
deployment.npm_stream_id,
|
||||||
|
)
|
||||||
|
_log_action(db, customer_id, "undeploy", "info", "NPM stream removed.")
|
||||||
|
except Exception as exc:
|
||||||
|
_log_action(db, customer_id, "undeploy", "error", f"NPM stream removal error: {exc}")
|
||||||
|
|
||||||
# Remove instance directory
|
# Remove instance directory
|
||||||
if os.path.isdir(instance_dir):
|
if os.path.isdir(instance_dir):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ subsequent requests.
|
|||||||
Creates, updates, and deletes proxy host entries so each customer's NetBird
|
Creates, updates, and deletes proxy host entries so each customer's NetBird
|
||||||
dashboard is accessible at ``{subdomain}.{base_domain}`` with automatic
|
dashboard is accessible at ``{subdomain}.{base_domain}`` with automatic
|
||||||
Let's Encrypt SSL certificates.
|
Let's Encrypt SSL certificates.
|
||||||
|
|
||||||
|
Also manages NPM streams for STUN/TURN relay UDP ports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -21,6 +24,35 @@ logger = logging.getLogger(__name__)
|
|||||||
NPM_TIMEOUT = 30
|
NPM_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
|
def _get_forward_host(npm_api_url: str) -> str:
|
||||||
|
"""Determine the IP/hostname to forward traffic to.
|
||||||
|
|
||||||
|
The NPM proxy host must forward to the MSP appliance's host IP,
|
||||||
|
NOT to a Docker container name, because the customer's Caddy
|
||||||
|
container exposes its port on the host via Docker port mapping.
|
||||||
|
|
||||||
|
We extract the host from the NPM API URL — if the admin configured
|
||||||
|
``http://10.0.0.5:81/api``, we forward to ``10.0.0.5``.
|
||||||
|
If the admin configured ``http://npm:81/api`` (container name),
|
||||||
|
we fall back to the Docker gateway IP ``172.17.0.1``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
npm_api_url: The NPM API base URL from system config.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
IP address or hostname to forward to.
|
||||||
|
"""
|
||||||
|
parsed = urlparse(npm_api_url)
|
||||||
|
host = parsed.hostname or "172.17.0.1"
|
||||||
|
|
||||||
|
# If the host looks like a container name (no dots, not an IP), use Docker gateway
|
||||||
|
if not any(c == "." for c in host) and not host.startswith("172.") and host != "localhost":
|
||||||
|
logger.info("NPM URL host '%s' looks like a container name, using Docker gateway 172.17.0.1", host)
|
||||||
|
return "172.17.0.1"
|
||||||
|
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
async def _npm_login(client: httpx.AsyncClient, api_url: str, email: str, password: str) -> str:
|
async def _npm_login(client: httpx.AsyncClient, api_url: str, email: str, password: str) -> str:
|
||||||
"""Authenticate with NPM and return a JWT token.
|
"""Authenticate with NPM and return a JWT token.
|
||||||
|
|
||||||
@@ -96,60 +128,25 @@ async def create_proxy_host(
|
|||||||
forward_host: str,
|
forward_host: str,
|
||||||
forward_port: int = 80,
|
forward_port: int = 80,
|
||||||
admin_email: str = "",
|
admin_email: str = "",
|
||||||
subdomain: str = "",
|
|
||||||
customer_id: int = 0,
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create a proxy host entry in NPM with SSL for a customer.
|
"""Create a proxy host entry in NPM with SSL for a customer.
|
||||||
|
|
||||||
Logs in first to get a JWT, then creates the proxy host with advanced
|
Forwards traffic to the host IP + dashboard_port where the customer's
|
||||||
routing config for management, signal, and relay containers.
|
Caddy reverse proxy is listening. Caddy handles internal routing to
|
||||||
|
management, signal, relay, and dashboard containers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
api_url: NPM API base URL.
|
api_url: NPM API base URL.
|
||||||
npm_email: NPM login email.
|
npm_email: NPM login email.
|
||||||
npm_password: NPM login password.
|
npm_password: NPM login password.
|
||||||
domain: Full domain (e.g. ``kunde1.example.com``).
|
domain: Full domain (e.g. ``kunde1.example.com``).
|
||||||
forward_host: Container name for the dashboard.
|
forward_host: IP/hostname to forward to (host IP, not container name).
|
||||||
forward_port: Port to forward to (default 80).
|
forward_port: Port to forward to (dashboard_port, e.g. 9001).
|
||||||
admin_email: Email for Let's Encrypt.
|
admin_email: Email for Let's Encrypt.
|
||||||
subdomain: Customer subdomain for building container names.
|
|
||||||
customer_id: Customer ID for building container names.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with ``proxy_id`` on success or ``error`` on failure.
|
Dict with ``proxy_id`` on success or ``error`` on failure.
|
||||||
"""
|
"""
|
||||||
# Build advanced Nginx config to route sub-paths to different containers
|
|
||||||
mgmt_container = f"netbird-kunde{customer_id}-management"
|
|
||||||
signal_container = f"netbird-kunde{customer_id}-signal"
|
|
||||||
relay_container = f"netbird-kunde{customer_id}-relay"
|
|
||||||
|
|
||||||
advanced_config = f"""
|
|
||||||
# NetBird Management API
|
|
||||||
location /api {{
|
|
||||||
proxy_pass http://{mgmt_container}:80;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}}
|
|
||||||
|
|
||||||
# NetBird Signal (gRPC-Web)
|
|
||||||
location /signalexchange. {{
|
|
||||||
grpc_pass grpc://{signal_container}:80;
|
|
||||||
grpc_set_header Host $host;
|
|
||||||
}}
|
|
||||||
|
|
||||||
# NetBird Relay (WebSocket)
|
|
||||||
location /relay {{
|
|
||||||
proxy_pass http://{relay_container}:80;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"domain_names": [domain],
|
"domain_names": [domain],
|
||||||
"forward_scheme": "http",
|
"forward_scheme": "http",
|
||||||
@@ -163,7 +160,7 @@ location /relay {{
|
|||||||
"block_exploits": True,
|
"block_exploits": True,
|
||||||
"allow_websocket_upgrade": True,
|
"allow_websocket_upgrade": True,
|
||||||
"access_list_id": 0,
|
"access_list_id": 0,
|
||||||
"advanced_config": advanced_config.strip(),
|
"advanced_config": "",
|
||||||
"meta": {
|
"meta": {
|
||||||
"letsencrypt_agree": True,
|
"letsencrypt_agree": True,
|
||||||
"letsencrypt_email": admin_email,
|
"letsencrypt_email": admin_email,
|
||||||
@@ -187,7 +184,8 @@ location /relay {{
|
|||||||
if resp.status_code in (200, 201):
|
if resp.status_code in (200, 201):
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
proxy_id = data.get("id")
|
proxy_id = data.get("id")
|
||||||
logger.info("Created NPM proxy host %s (id=%s)", domain, proxy_id)
|
logger.info("Created NPM proxy host %s -> %s:%d (id=%s)",
|
||||||
|
domain, forward_host, forward_port, proxy_id)
|
||||||
|
|
||||||
# Step 3: Request SSL certificate
|
# Step 3: Request SSL certificate
|
||||||
await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
||||||
@@ -225,6 +223,8 @@ async def _request_ssl(
|
|||||||
"""
|
"""
|
||||||
ssl_payload = {
|
ssl_payload = {
|
||||||
"domain_names": [domain],
|
"domain_names": [domain],
|
||||||
|
"provider": "letsencrypt",
|
||||||
|
"nice_name": domain,
|
||||||
"meta": {
|
"meta": {
|
||||||
"letsencrypt_agree": True,
|
"letsencrypt_agree": True,
|
||||||
"letsencrypt_email": admin_email,
|
"letsencrypt_email": admin_email,
|
||||||
@@ -243,13 +243,111 @@ async def _request_ssl(
|
|||||||
json={"certificate_id": cert_id},
|
json={"certificate_id": cert_id},
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
logger.info("SSL certificate assigned to proxy host %s", proxy_id)
|
logger.info("SSL certificate %s assigned to proxy host %s", cert_id, proxy_id)
|
||||||
else:
|
else:
|
||||||
logger.warning("SSL request returned %s: %s", resp.status_code, resp.text[:200])
|
logger.warning("SSL request returned %s: %s", resp.status_code, resp.text[:200])
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("SSL certificate request failed: %s", exc)
|
logger.warning("SSL certificate request failed: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_stream(
|
||||||
|
api_url: str,
|
||||||
|
npm_email: str,
|
||||||
|
npm_password: str,
|
||||||
|
incoming_port: int,
|
||||||
|
forwarding_host: str,
|
||||||
|
forwarding_port: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Create a UDP stream in NPM for STUN/TURN relay forwarding.
|
||||||
|
|
||||||
|
NPM streams forward raw TCP/UDP traffic (Layer 4) without HTTP processing.
|
||||||
|
Used for the relay STUN port (UDP 3478+).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_url: NPM API base URL.
|
||||||
|
npm_email: NPM login email.
|
||||||
|
npm_password: NPM login password.
|
||||||
|
incoming_port: The public-facing port NPM listens on.
|
||||||
|
forwarding_host: IP/hostname to forward to.
|
||||||
|
forwarding_port: The port on the target host.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with ``stream_id`` on success or ``error`` on failure.
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"incoming_port": incoming_port,
|
||||||
|
"forwarding_host": forwarding_host,
|
||||||
|
"forwarding_port": forwarding_port,
|
||||||
|
"tcp_forwarding": False,
|
||||||
|
"udp_forwarding": True,
|
||||||
|
"meta": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
||||||
|
token = await _npm_login(client, api_url, npm_email, npm_password)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"{api_url}/nginx/streams", json=payload, headers=headers
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 201):
|
||||||
|
data = resp.json()
|
||||||
|
stream_id = data.get("id")
|
||||||
|
logger.info(
|
||||||
|
"Created NPM stream: UDP :%d -> %s:%d (id=%s)",
|
||||||
|
incoming_port, forwarding_host, forwarding_port, stream_id,
|
||||||
|
)
|
||||||
|
return {"stream_id": stream_id}
|
||||||
|
else:
|
||||||
|
error_msg = f"NPM stream creation returned {resp.status_code}: {resp.text[:300]}"
|
||||||
|
logger.error("Failed to create NPM stream: %s", error_msg)
|
||||||
|
return {"error": error_msg}
|
||||||
|
except RuntimeError as exc:
|
||||||
|
logger.error("NPM login failed for stream creation: %s", exc)
|
||||||
|
return {"error": f"NPM login failed: {exc}"}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("NPM stream API error: %s", exc)
|
||||||
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_stream(
|
||||||
|
api_url: str, npm_email: str, npm_password: str, stream_id: int
|
||||||
|
) -> bool:
|
||||||
|
"""Delete a stream from NPM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_url: NPM API base URL.
|
||||||
|
npm_email: NPM login email.
|
||||||
|
npm_password: NPM login password.
|
||||||
|
stream_id: The stream ID to delete.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True on success.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
||||||
|
token = await _npm_login(client, api_url, npm_email, npm_password)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
resp = await client.delete(
|
||||||
|
f"{api_url}/nginx/streams/{stream_id}", headers=headers
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 204):
|
||||||
|
logger.info("Deleted NPM stream %d", stream_id)
|
||||||
|
return True
|
||||||
|
logger.warning(
|
||||||
|
"Failed to delete stream %d: %s %s",
|
||||||
|
stream_id, resp.status_code, resp.text[:200],
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("NPM stream delete error: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def delete_proxy_host(
|
async def delete_proxy_host(
|
||||||
api_url: str, npm_email: str, npm_password: str, proxy_id: int
|
api_url: str, npm_email: str, npm_password: str, proxy_id: int
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user