From 5321182d084d03484431c8ad27da12875d255768 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 22 Jun 2023 12:34:05 +0200 Subject: [PATCH] feat(network): add field expose_routes_to_vswitch (#208) * fix(network): missing subnets vswitch_id in networks create * feat(network): add field expose_routes_to_vswitch --- hcloud/networks/client.py | 51 +++++++++++++++---- hcloud/networks/domain.py | 5 ++ tests/unit/networks/conftest.py | 31 ++++++++++++ tests/unit/networks/test_client.py | 80 +++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index b0be745..b7e22af 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -30,17 +30,29 @@ def __init__(self, client, data, complete=True): super().__init__(client, data, complete) - def update(self, name=None, labels=None): - # type: (Optional[str], Optional[Dict[str, str]]) -> BoundNetwork + def update( + self, + name=None, # type: Optional[str] + expose_routes_to_vswitch=None, # type: Optional[bool] + labels=None, # type: Optional[Dict[str, str]] + ): # type: (...) -> BoundNetwork """Updates a network. You can update a network’s name and a networks’s labels. :param name: str (optional) New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ - return self._client.update(self, name, labels) + return self._client.update( + self, + name=name, + expose_routes_to_vswitch=expose_routes_to_vswitch, + labels=labels, + ) def delete(self): # type: () -> BoundAction @@ -217,6 +229,7 @@ def create( ip_range, # type: str subnets=None, # type: Optional[List[NetworkSubnet]] routes=None, # type: Optional[List[NetworkRoute]] + expose_routes_to_vswitch=None, # type: Optional[bool] labels=None, # type: Optional[Dict[str, str]] ): """Creates a network with range ip_range. @@ -229,25 +242,37 @@ def create( Array of subnets allocated :param routes: List[:class:`NetworkRoute `] Array of routes set in this network + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ data = {"name": name, "ip_range": ip_range} if subnets is not None: - data["subnets"] = [ - { + data_subnets = [] + for subnet in subnets: + data_subnet = { "type": subnet.type, "ip_range": subnet.ip_range, "network_zone": subnet.network_zone, } - for subnet in subnets - ] + if subnet.vswitch_id is not None: + data_subnet["vswitch_id"] = subnet.vswitch_id + + data_subnets.append(data_subnet) + data["subnets"] = data_subnets + if routes is not None: data["routes"] = [ {"destination": route.destination, "gateway": route.gateway} for route in routes ] + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + if labels is not None: data["labels"] = labels @@ -255,13 +280,16 @@ def create( return BoundNetwork(self, response["network"]) - def update(self, network, name=None, labels=None): - # type:(Network, Optional[str], Optional[Dict[str, str]]) -> BoundNetwork + def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None): + # type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork """Updates a network. You can update a network’s name and a network’s labels. :param network: :class:`BoundNetwork ` or :class:`Network ` :param name: str (optional) New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` @@ -269,8 +297,13 @@ def update(self, network, name=None, labels=None): data = {} if name is not None: data.update({"name": name}) + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + if labels is not None: data.update({"labels": labels}) + response = self._client.request( url=f"/networks/{network.id}", method="PUT", diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 8fc3990..be203ec 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -16,6 +16,8 @@ class Network(BaseDomain): Subnets allocated in this network :param routes: List[:class:`NetworkRoute `] Routes set in this network + :param expose_routes_to_vswitch: bool + Indicates if the routes from this network should be exposed to the vSwitch connection. :param servers: List[:class:`BoundServer `] Servers attached to this network :param protection: dict @@ -30,6 +32,7 @@ class Network(BaseDomain): "ip_range", "subnets", "routes", + "expose_routes_to_vswitch", "servers", "protection", "labels", @@ -44,6 +47,7 @@ def __init__( ip_range=None, subnets=None, routes=None, + expose_routes_to_vswitch=None, servers=None, protection=None, labels=None, @@ -54,6 +58,7 @@ def __init__( self.ip_range = ip_range self.subnets = subnets self.routes = routes + self.expose_routes_to_vswitch = expose_routes_to_vswitch self.servers = servers self.protection = protection self.labels = labels diff --git a/tests/unit/networks/conftest.py b/tests/unit/networks/conftest.py index 109e3b5..7320d4a 100644 --- a/tests/unit/networks/conftest.py +++ b/tests/unit/networks/conftest.py @@ -24,6 +24,7 @@ def network_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -55,6 +56,7 @@ def two_networks_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -73,6 +75,7 @@ def two_networks_response(): } ], "routes": [{"destination": "12.100.1.0/24", "gateway": "12.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [45], "protection": {"delete": False}, "labels": {}, @@ -105,6 +108,7 @@ def one_network_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -129,6 +133,32 @@ def network_create_response(): } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, + "servers": [42], + "protection": {"delete": False}, + "labels": {}, + "created": "2016-01-30T23:50:00+00:00", + } + } + + +@pytest.fixture() +def network_create_response_with_expose_routes_to_vswitch(): + return { + "network": { + "id": 4711, + "name": "mynet", + "ip_range": "10.0.0.0/16", + "subnets": [ + { + "type": "cloud", + "ip_range": "10.0.1.0/24", + "network_zone": "eu-central", + "gateway": "10.0.0.1", + } + ], + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -153,6 +183,7 @@ def response_update_network(): } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, "servers": [42], "protection": {"delete": False}, "labels": {}, diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index cc68390..5d70d99 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -268,6 +268,25 @@ def test_create(self, networks_client, network_create_response): json={"name": "mynet", "ip_range": "10.0.0.0/8"}, ) + def test_create_with_expose_routes_to_vswitch( + self, networks_client, network_create_response_with_expose_routes_to_vswitch + ): + networks_client._client.request.return_value = ( + network_create_response_with_expose_routes_to_vswitch + ) + networks_client.create( + name="mynet", ip_range="10.0.0.0/8", expose_routes_to_vswitch=True + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "expose_routes_to_vswitch": True, + }, + ) + def test_create_with_subnet( self, networks_client, network_subnet, network_create_response ): @@ -291,6 +310,32 @@ def test_create_with_subnet( }, ) + def test_create_with_subnet_vswitch( + self, networks_client, network_subnet, network_create_response + ): + networks_client._client.request.return_value = network_create_response + network_subnet.type = NetworkSubnet.TYPE_VSWITCH + network_subnet.vswitch_id = 1000 + networks_client.create( + name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "subnets": [ + { + "type": NetworkSubnet.TYPE_VSWITCH, + "ip_range": "10.0.1.0/24", + "network_zone": "eu-central", + "vswitch_id": 1000, + } + ], + }, + ) + def test_create_with_route( self, networks_client, network_route, network_create_response ): @@ -308,6 +353,32 @@ def test_create_with_route( }, ) + def test_create_with_route_and_expose_routes_to_vswitch( + self, + networks_client, + network_route, + network_create_response_with_expose_routes_to_vswitch, + ): + networks_client._client.request.return_value = ( + network_create_response_with_expose_routes_to_vswitch + ) + networks_client.create( + name="mynet", + ip_range="10.0.0.0/8", + routes=[network_route], + expose_routes_to_vswitch=True, + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, + }, + ) + def test_create_with_route_and_subnet( self, networks_client, network_subnet, network_route, network_create_response ): @@ -358,13 +429,18 @@ def test_get_actions_list(self, networks_client, network, response_get_actions): ) def test_update(self, networks_client, network, response_update_network): networks_client._client.request.return_value = response_update_network - network = networks_client.update(network, name="new-name") + network = networks_client.update( + network, name="new-name", expose_routes_to_vswitch=True + ) networks_client._client.request.assert_called_with( - url="/networks/1", method="PUT", json={"name": "new-name"} + url="/networks/1", + method="PUT", + json={"name": "new-name", "expose_routes_to_vswitch": True}, ) assert network.id == 4711 assert network.name == "new-name" + assert network.expose_routes_to_vswitch is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]