Source code for pyvergeos.resources.hosts

"""Network DHCP/DNS host override resource manager."""

from __future__ import annotations

import builtins
from typing import TYPE_CHECKING, Any, Literal

from pyvergeos.exceptions import NotFoundError
from pyvergeos.resources.base import ResourceManager, ResourceObject

if TYPE_CHECKING:
    from pyvergeos.client import VergeClient
    from pyvergeos.resources.networks import Network

# Default fields for host data
HOST_DEFAULT_FIELDS = [
    "$key",
    "vnet",
    "vnet#name as vnet_name",
    "type",
    "host",
    "ip",
]

# Type alias for host types
HostType = Literal["host", "domain"]


[docs] class NetworkHost(ResourceObject): """Network DHCP/DNS host override resource object. Host overrides map hostnames to IP addresses for DHCP and DNS resolution. """ @property def network_key(self) -> int: """Get the network key this host belongs to.""" vnet = self.get("vnet") if vnet is None: raise ValueError("Host has no network (vnet) key") return int(vnet) @property def network_name(self) -> str | None: """Get the network name this host belongs to.""" return self.get("vnet_name") @property def hostname(self) -> str: """Get the hostname of this override.""" host = self.get("host") if host is None: raise ValueError("Host has no hostname") return str(host) @property def ip(self) -> str: """Get the IP address of this host override.""" ip = self.get("ip") if ip is None: raise ValueError("Host has no IP address") return str(ip) @property def host_type(self) -> str: """Get the type of this host override ('host' or 'domain').""" return self.get("type") or "host" @property def is_domain(self) -> bool: """Check if this is a domain override.""" return self.host_type == "domain" @property def is_host(self) -> bool: """Check if this is a host override.""" return self.host_type == "host"
[docs] class NetworkHostManager(ResourceManager[NetworkHost]): """Manager for Network DHCP/DNS host override operations. This manager is accessed through a Network object's hosts property. Host overrides provide static DNS entries and DHCP hostname assignment. Examples: List all hosts for a network:: hosts = network.hosts.list() Get a host by hostname:: host = network.hosts.get(hostname="server01") Create a new host override:: host = network.hosts.create( hostname="server01", ip="10.0.0.50", host_type="host" ) Update a host override:: network.hosts.update(host.key, ip="10.0.0.51") Delete a host override:: network.hosts.delete(host.key) """ _endpoint = "vnet_hosts" _default_fields = HOST_DEFAULT_FIELDS
[docs] def __init__(self, client: VergeClient, network: Network) -> None: super().__init__(client) self._network = network
@property def network_key(self) -> int: """Get the network key for this manager.""" return self._network.key def _to_model(self, data: dict[str, Any]) -> NetworkHost: return NetworkHost(data, self)
[docs] def list( # type: ignore[override] self, filter: str | None = None, fields: builtins.list[str] | None = None, hostname: str | None = None, ip: str | None = None, host_type: HostType | None = None, **kwargs: Any, ) -> builtins.list[NetworkHost]: """List DHCP/DNS host overrides for this network. Args: filter: Additional OData filter string. fields: List of fields to return. hostname: Filter by exact hostname. ip: Filter by exact IP address. host_type: Filter by type ('host' or 'domain'). **kwargs: Additional filter arguments. Returns: List of NetworkHost objects sorted by hostname. """ if fields is None: fields = self._default_fields.copy() # Build filter for this network filters: builtins.list[str] = [ f"vnet eq {self.network_key}", ] if hostname: escaped_hostname = hostname.replace("'", "''") filters.append(f"host eq '{escaped_hostname}'") if ip: escaped_ip = ip.replace("'", "''") filters.append(f"ip eq '{escaped_ip}'") if host_type: filters.append(f"type eq '{host_type}'") if filter: filters.append(f"({filter})") combined_filter = " and ".join(filters) params: dict[str, Any] = { "filter": combined_filter, "fields": ",".join(fields), "sort": "+host", } response = self._client._request("GET", self._endpoint, params=params) if response is None: return [] if not isinstance(response, list): return [self._to_model(response)] return [self._to_model(item) for item in response]
[docs] def get( # type: ignore[override] self, key: int | None = None, *, hostname: str | None = None, ip: str | None = None, fields: builtins.list[str] | None = None, ) -> NetworkHost: """Get a host override by key, hostname, or IP address. Args: key: Host $key (ID). hostname: Hostname of the override. ip: IP address of the override. fields: List of fields to return. Returns: NetworkHost object. Raises: NotFoundError: If host not found. ValueError: If no identifier provided. """ if fields is None: fields = self._default_fields.copy() if key is not None: params: dict[str, Any] = {"fields": ",".join(fields)} response = self._client._request("GET", f"{self._endpoint}/{key}", params=params) if response is None: raise NotFoundError(f"Host {key} not found") if not isinstance(response, dict): raise NotFoundError(f"Host {key} returned invalid response") return self._to_model(response) if hostname is not None: hosts = self.list(hostname=hostname, fields=fields) if not hosts: raise NotFoundError(f"Host with hostname '{hostname}' not found on this network") return hosts[0] if ip is not None: hosts = self.list(ip=ip, fields=fields) if not hosts: raise NotFoundError(f"Host with IP '{ip}' not found on this network") return hosts[0] raise ValueError("Either key, hostname, or ip must be provided")
[docs] def create( # type: ignore[override] self, hostname: str, ip: str, host_type: HostType = "host", ) -> NetworkHost: """Create a new DHCP/DNS host override. Args: hostname: Hostname or domain name for the override. ip: IP address to map to the hostname. host_type: Type of override - 'host' (default) or 'domain'. Returns: Created NetworkHost object. Raises: ValidationError: If a host with this hostname already exists. Note: Host override changes require DNS apply to take effect. """ body: dict[str, Any] = { "vnet": self.network_key, "host": hostname, "ip": ip, "type": host_type, } response = self._client._request("POST", self._endpoint, json_data=body) if response is None: raise ValueError("No response from create operation") if not isinstance(response, dict): raise ValueError("Create operation returned invalid response") # Get the created host key and fetch full data key = response.get("$key") if key is None: raise ValueError("Create response missing $key") return self.get(int(key))
[docs] def update( # type: ignore[override] self, key: int, *, hostname: str | None = None, ip: str | None = None, host_type: HostType | None = None, ) -> NetworkHost: """Update an existing host override. Args: key: Host $key (ID). hostname: New hostname for the override. ip: New IP address for the override. host_type: New type ('host' or 'domain'). Returns: Updated NetworkHost object. Raises: NotFoundError: If host not found. ValueError: If no update fields provided. Note: Host override changes require DNS apply to take effect. """ body: dict[str, Any] = {} if hostname is not None: body["host"] = hostname if ip is not None: body["ip"] = ip if host_type is not None: body["type"] = host_type if not body: raise ValueError("At least one field must be provided to update") self._client._request("PUT", f"{self._endpoint}/{key}", json_data=body) return self.get(key)
[docs] def delete(self, key: int) -> None: """Delete a host override. Args: key: Host $key (ID). Note: Host override changes require DNS apply to take effect. """ self._client._request("DELETE", f"{self._endpoint}/{key}")