"""Network IP alias resource manager."""
from __future__ import annotations
import builtins
from typing import TYPE_CHECKING, Any
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 alias data
ALIAS_DEFAULT_FIELDS = [
"$key",
"vnet",
"vnet#name as vnet_name",
"ip",
"hostname",
"description",
"mac",
"type",
]
[docs]
class NetworkAlias(ResourceObject):
"""Network IP alias resource object.
IP aliases can be referenced in firewall rules using alias:name syntax.
"""
@property
def network_key(self) -> int:
"""Get the network key this alias belongs to."""
vnet = self.get("vnet")
if vnet is None:
raise ValueError("Alias has no network (vnet) key")
return int(vnet)
@property
def network_name(self) -> str | None:
"""Get the network name this alias belongs to."""
return self.get("vnet_name")
@property
def ip(self) -> str:
"""Get the IP address of this alias."""
ip = self.get("ip")
if ip is None:
raise ValueError("Alias has no IP address")
return str(ip)
@property
def hostname(self) -> str | None:
"""Get the hostname/name of this alias."""
return self.get("hostname")
# Alias for consistency with PowerShell module
@property
def alias_name(self) -> str | None:
"""Get the name of this alias (same as hostname)."""
return self.hostname
@property
def description(self) -> str | None:
"""Get the description of this alias."""
return self.get("description")
@property
def mac(self) -> str | None:
"""Get the MAC address associated with this alias."""
return self.get("mac")
[docs]
class NetworkAliasManager(ResourceManager[NetworkAlias]):
"""Manager for Network IP alias operations.
This manager is accessed through a Network object's aliases property.
IP aliases are used in firewall rules to reference groups of IP addresses.
Examples:
List all aliases for a network::
aliases = network.aliases.list()
Get an alias by IP::
alias = network.aliases.get(ip="10.0.0.100")
Create a new IP alias::
alias = network.aliases.create(
ip="10.0.0.100",
name="webserver",
description="Main web server"
)
Delete an alias::
network.aliases.delete(alias.key)
"""
_endpoint = "vnet_addresses"
_default_fields = ALIAS_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]) -> NetworkAlias:
return NetworkAlias(data, self)
[docs]
def list( # type: ignore[override]
self,
filter: str | None = None,
fields: builtins.list[str] | None = None,
ip: str | None = None,
hostname: str | None = None,
**kwargs: Any,
) -> builtins.list[NetworkAlias]:
"""List IP aliases for this network.
Args:
filter: Additional OData filter string.
fields: List of fields to return.
ip: Filter by exact IP address.
hostname: Filter by exact hostname/name.
**kwargs: Additional filter arguments.
Returns:
List of NetworkAlias objects sorted by IP.
"""
if fields is None:
fields = self._default_fields.copy()
# Build filter for this network and type='ipalias'
filters: builtins.list[str] = [
f"vnet eq {self.network_key}",
"type eq 'ipalias'",
]
if ip:
escaped_ip = ip.replace("'", "''")
filters.append(f"ip eq '{escaped_ip}'")
if hostname:
escaped_hostname = hostname.replace("'", "''")
filters.append(f"hostname eq '{escaped_hostname}'")
if filter:
filters.append(f"({filter})")
combined_filter = " and ".join(filters)
params: dict[str, Any] = {
"filter": combined_filter,
"fields": ",".join(fields),
"sort": "+ip",
}
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(
self,
key: int | None = None,
*,
ip: str | None = None,
hostname: str | None = None,
name: str | None = None,
fields: builtins.list[str] | None = None,
) -> NetworkAlias:
"""Get an alias by key, IP address, or hostname.
Args:
key: Alias $key (ID).
ip: IP address.
hostname: Hostname/name of the alias.
name: Alias for hostname (for convenience).
fields: List of fields to return.
Returns:
NetworkAlias object.
Raises:
NotFoundError: If alias not found.
ValueError: If no identifier provided.
"""
if fields is None:
fields = self._default_fields.copy()
# Allow 'name' as alias for 'hostname'
if name is not None and hostname is None:
hostname = name
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"Alias {key} not found")
if not isinstance(response, dict):
raise NotFoundError(f"Alias {key} returned invalid response")
return self._to_model(response)
if ip is not None:
aliases = self.list(ip=ip, fields=fields)
if not aliases:
raise NotFoundError(f"Alias with IP '{ip}' not found on this network")
return aliases[0]
if hostname is not None:
aliases = self.list(hostname=hostname, fields=fields)
if not aliases:
raise NotFoundError(f"Alias with hostname '{hostname}' not found on this network")
return aliases[0]
raise ValueError("Either key, ip, or hostname must be provided")
[docs]
def create( # type: ignore[override]
self,
ip: str,
name: str,
description: str = "",
) -> NetworkAlias:
"""Create a new IP alias.
Args:
ip: IP address for the alias. Can be a single IP or CIDR notation.
name: Name/hostname for the alias (used in firewall rules as alias:name).
description: Optional description.
Returns:
Created NetworkAlias object.
Raises:
ValidationError: If an alias with this IP already exists.
"""
body: dict[str, Any] = {
"vnet": self.network_key,
"ip": ip,
"hostname": name,
"type": "ipalias",
}
if description:
body["description"] = description
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 alias 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 delete(self, key: int) -> None:
"""Delete an IP alias.
Args:
key: Alias $key (ID).
Note:
Aliases referenced by firewall rules cannot be deleted
until the rules are removed.
"""
self._client._request("DELETE", f"{self._endpoint}/{key}")