Files

171 lines
6.5 KiB
Python
Raw Permalink Normal View History

"""ZeroTier API service — thin Flask adapter around the ZeroTierClient SDK.
2026-03-29 23:14:20 +05:45
ZeroTier is managed exclusively at the organization level. Each organization
configures its own ZeroTier credentials (token, URL, mode) via the web UI
(ZeroTier Config page → stored in the organizations table).
Every call that interacts with ZeroTier must supply an organization_id so the correct org credentials
can be loaded from the database.
"""
import logging
from typing import Optional
from gatehouse_app.exceptions import ZeroTierAPIError
from gatehouse_app.utils.zerotier_client import (
APIMode,
ZeroTierAPIError as SDKZeroTierAPIError,
ZeroTierAuthError,
ZeroTierClient,
ZeroTierNotFoundError,
)
logger = logging.getLogger(__name__)
2026-03-29 23:14:20 +05:45
def _get_client(organization_id: Optional[str] = None, app=None) -> ZeroTierClient:
"""Build a ZeroTierClient using the organization's stored ZeroTier credentials.
Credentials are read exclusively from the organization record
(org.zt_api_token / org.zt_api_url / org.zt_api_mode).
Args:
organization_id: The org whose credentials should be used.
Required for any ZeroTier operation.
app: Flask app instance (defaults to current_app, only needed for
background tasks that run outside a request context).
2026-03-29 23:14:20 +05:45
Raises:
ZeroTierAPIError: If organization_id is missing, the org is not found,
or the org has incomplete ZeroTier credentials.
"""
if not organization_id:
raise ZeroTierAPIError(
"organization_id is required — ZeroTier credentials are managed "
"per-organization. Configure them via the ZeroTier Config page."
)
try:
from gatehouse_app.models.organization.organization import Organization
from gatehouse_app.extensions import db
org = db.session.get(Organization, organization_id)
except Exception as exc:
logger.error(f"[ZT] Failed to load org {organization_id} from DB: {exc}")
raise ZeroTierAPIError(
f"Could not load organization {organization_id}: {exc}"
) from exc
if not org:
raise ZeroTierAPIError(f"Organization {organization_id} not found.")
token: Optional[str] = org.zt_api_token or None
if not token:
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier credentials configured. "
"Go to Settings → ZeroTier Config to add a token, mode, and controller URL."
)
mode_str = (org.zt_api_mode or "").strip().lower()
if mode_str not in ("central", "controller"):
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier mode set. "
"Go to Settings → ZeroTier Config and select 'Central' or 'Controller'."
)
url: str = (org.zt_api_url or "").strip()
if not url:
raise ZeroTierAPIError(
f"Organization '{org.name}' has no ZeroTier controller/API URL set. "
"Go to Settings → ZeroTier Config and enter the URL for your ZeroTier "
"controller (e.g. http://host:9993) or Central API."
)
mode = APIMode.CENTRAL if mode_str == "central" else APIMode.CONTROLLER
2026-03-29 23:14:20 +05:45
logger.debug(
f"[ZT] Client for org:{organization_id} mode={mode_str} url={url}"
)
2026-03-29 23:14:20 +05:45
return ZeroTierClient(api_token=token, base_url=url, mode=mode)
2026-03-29 23:14:20 +05:45
def get_status(organization_id: Optional[str] = None) -> dict:
"""Verify connectivity to the ZeroTier controller."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.get_status()
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def list_networks(organization_id: Optional[str] = None):
"""List all networks accessible to the configured token."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.list_networks()
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def get_network(network_id: str, organization_id: Optional[str] = None):
"""Fetch a single network by ID."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.get_network(network_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def list_members(network_id: str, organization_id: Optional[str] = None):
"""List all members on a network."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.list_members(network_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def get_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Fetch a single member on a network."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.get_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def authorize_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Authorize a member on a network. Returns updated member."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.authorize_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def deauthorize_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""De-authorize a member on a network. Returns updated member."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.deauthorize_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def add_member(network_id: str, node_id: str, authorized: bool = False, organization_id: Optional[str] = None):
"""Manually add/pre-provision a member on a network."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.add_member(network_id, node_id, authorized=authorized)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc
2026-03-29 23:14:20 +05:45
def delete_network_member(network_id: str, node_id: str, organization_id: Optional[str] = None):
"""Remove a member entirely from a ZeroTier network."""
2026-03-29 23:14:20 +05:45
client = _get_client(organization_id)
try:
return client.delete_member(network_id, node_id)
except SDKZeroTierAPIError as exc:
raise ZeroTierAPIError(str(exc), status_code=exc.status_code) from exc