Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions hcloud/networks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from .domain import (
CreateNetworkResponse,
Network,
NetworkMember,
NetworkMemberStatus,
NetworkMemberType,
NetworkProtection,
NetworkRoute,
NetworkSubnet,
Expand All @@ -18,4 +21,7 @@
"NetworkSubnet",
"NetworksClient",
"NetworksPageResult",
"NetworkMemberType",
"NetworkMemberStatus",
"NetworkMember",
]
108 changes: 106 additions & 2 deletions hcloud/networks/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, NamedTuple
from typing import TYPE_CHECKING, Any, Literal, NamedTuple

from ..actions import (
ActionSort,
Expand All @@ -11,7 +11,14 @@
)
from ..actions.client import ResourceClientBaseActionsMixin
from ..core import BoundModelBase, Meta, ResourceClientBase
from .domain import Network, NetworkRoute, NetworkSubnet
from .domain import (
Network,
NetworkMember,
NetworkMemberStatus,
NetworkMemberType,
NetworkRoute,
NetworkSubnet,
)

if TYPE_CHECKING:
from .._client import Client
Expand Down Expand Up @@ -185,6 +192,27 @@ class NetworksPageResult(NamedTuple):
meta: Meta


NetworkMemberSort = Literal[
"id",
"id:asc",
"id:desc",
"type",
"type:asc",
"type:desc",
"status",
"status:asc",
"status:desc",
"ip",
"ip:asc",
"ip:desc",
]


class NetworkMembersPageResult(NamedTuple):
members: list[NetworkMember]
meta: Meta


class NetworksClient(
ResourceClientBaseActionsMixin,
ResourceClientBase,
Expand Down Expand Up @@ -372,6 +400,82 @@ def delete(self, network: Network | BoundNetwork) -> bool:
self._client.request(url=f"{self._base_url}/{network.id}", method="DELETE")
return True

def get_member_list(
self,
network: Network | BoundNetwork,
*,
type: list[NetworkMemberType] | None = None,
status: list[NetworkMemberStatus] | None = None,
subnet: list[str] | None = None,
sort: list[NetworkMemberSort] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> NetworkMembersPageResult:
"""
Returns a paginated list of Members for a Network.

:param network: Network to get the Members for.
:param type: Filter the Members by type.
:param status: Filter the Members by status.
:param subnet: Filter the Members by the subnet they are attached to.
:param sort: Sort Members by field and direction.
:param page: Page number to get.
:param per_page: Maximum number of Members returned per page.
"""
params: dict[str, Any] = {}
if type is not None:
params["type"] = type
if status is not None:
params["status"] = status
if subnet is not None:
params["subnet"] = subnet
if sort is not None:
params["sort"] = sort
if page is not None:
params["page"] = page
if per_page is not None:
params["per_page"] = per_page

response = self._client.request(
url=f"{self._base_url}/{network.id}/members",
method="GET",
params=params,
)

return NetworkMembersPageResult(
members=[NetworkMember(**o) for o in response["members"]],
meta=Meta.parse_meta(response),
)

def get_member_all(
self,
network: Network | BoundNetwork,
*,
type: list[NetworkMemberType] | None = None,
status: list[NetworkMemberStatus] | None = None,
subnet: list[str] | None = None,
sort: list[NetworkMemberSort] | None = None,
) -> list[NetworkMember]:
"""
Returns all of Members for a Network.

:param network: Network to get the Members for.
:param type: Filter the Members by type.
:param status: Filter the Members by status.
:param subnet: Filter the Members by the subnet they are attached to.
:param sort: Sort Members by field and direction.
:param page: Page number to get.
:param per_page: Maximum number of Members returned per page.
"""
return self._iter_pages(
self.get_member_list,
network,
type=type,
status=status,
subnet=subnet,
sort=sort,
)

def get_actions_list(
self,
network: Network | BoundNetwork,
Expand Down
76 changes: 75 additions & 1 deletion hcloud/networks/domain.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import warnings
from typing import TYPE_CHECKING, TypedDict
from typing import TYPE_CHECKING, Literal, TypedDict

from ..core import BaseDomain, DomainIdentityMixin

Expand All @@ -16,6 +16,9 @@
"NetworkSubnet",
"NetworkRoute",
"CreateNetworkResponse",
"NetworkMemberType",
"NetworkMemberStatus",
"NetworkMember",
]


Expand Down Expand Up @@ -180,3 +183,74 @@ def __init__(
):
self.network = network
self.action = action


NetworkMemberType = Literal[
"server",
"load_balancer",
]

NetworkMemberStatus = Literal[
"ok",
"attaching",
"detaching",
"updating",
"error",
]


class NetworkMember(BaseDomain):
"""Network Member Domain

:param id: ID of the resource attached to the Network.
:param type: Type of the resource attached to the Network.
:param ip: IP address of the resource within the Network.
:param alias_ips: Additional IP address of the resource within the Network.
:param subnet: IP range of the subnet the resource is attached to.
:param status: Status of the resource within the Network.
"""

TYPE_SERVER: NetworkMemberType = "server"
"""The member is a Server."""
TYPE_LOAD_BALANCER: NetworkMemberType = "load_balancer"
"""The member is a Load Balancer."""

STATUS_OK: NetworkMemberStatus = "ok"
"""The resource is attached and its network configuration is up to date."""
STATUS_ATTACHING: NetworkMemberStatus = "attaching"
"""The resource is being attached to the Network."""
STATUS_DETACHING: NetworkMemberStatus = "detaching"
"""The resource is being detached from the Network."""
STATUS_UPDATING: NetworkMemberStatus = "updating"
"""The resource network configuration is being updated."""
STATUS_ERROR: NetworkMemberStatus = "error"
"""
The last operation on this member failed. Its network configuration might be out of date,
and the resource might not be reachable within the Network.
"""

__api_properties__ = (
"id",
"type",
"ip",
"alias_ips",
"subnet",
"status",
)
__slots__ = __api_properties__

def __init__(
self,
id: int,
type: NetworkMemberType,
ip: str,
alias_ips: list[str],
subnet: str,
status: NetworkMemberStatus,
):
self.id = id
self.type = type
self.ip = ip
self.alias_ips = alias_ips
self.subnet = subnet
self.status = status
24 changes: 24 additions & 0 deletions tests/unit/networks/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,27 @@ def response_get_actions():
}
]
}


@pytest.fixture()
def network_member1():
return {
"type": "server",
"id": 123,
"ip": "10.0.1.2",
"status": "attaching",
"alias_ips": ["10.0.1.20"],
"subnet": "10.0.1.0/24",
}


@pytest.fixture()
def network_member2():
return {
"type": "load_balancer",
"id": 456,
"ip": "10.0.1.3",
"status": "updating",
"alias_ips": [],
"subnet": "10.0.1.0/24",
}
86 changes: 86 additions & 0 deletions tests/unit/networks/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,92 @@ def test_delete(

assert delete_success is True

@pytest.mark.parametrize(
"params",
[
{"type": ["server"]},
{"status": ["ok"]},
{"subnet": ["10.0.1.0/24"]},
{"sort": ["ip:asc"]},
{"page": 1, "per_page": 50},
{},
],
)
@pytest.mark.parametrize(
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
)
def test_get_member_list(
self,
request_mock: mock.MagicMock,
networks_client: NetworksClient,
network,
network_member1,
network_member2,
params,
):
request_mock.return_value = {"members": [network_member1, network_member2]}

result = networks_client.get_member_list(network, **params)

request_mock.assert_called_with(
method="GET",
url="/networks/1/members",
params=params,
)

assert result.meta is not None
assert len(result.members) == 2

assert result.members[0].id == 123
assert result.members[0].type == "server"

assert result.members[0].ip == "10.0.1.2"
assert result.members[0].alias_ips == ["10.0.1.20"]
assert result.members[0].subnet == "10.0.1.0/24"
assert result.members[0].status == "attaching"

assert result.members[1].id == 456
assert result.members[1].type == "load_balancer"

@pytest.mark.parametrize(
"params",
[
{"type": ["server"]},
{"status": ["ok"]},
{"subnet": ["10.0.1.0/24"]},
{"sort": ["ip:asc"]},
{},
],
)
@pytest.mark.parametrize(
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
)
def test_get_member_all(
self,
request_mock: mock.MagicMock,
networks_client: NetworksClient,
network,
network_member1,
network_member2,
params,
):
request_mock.return_value = {"members": [network_member1, network_member2]}

result = networks_client.get_member_all(network, **params)

request_mock.assert_called_with(
method="GET",
url="/networks/1/members",
params={**params, "page": 1, "per_page": 50},
)

assert len(result) == 2
assert result[0].id == 123
assert result[0].type == "server"

assert result[1].id == 456
assert result[1].type == "load_balancer"

@pytest.mark.parametrize(
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
)
Expand Down
Loading