From 2d23cefc18bc17d80ca117a6e372ff5ef937ff38 Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Fri, 17 Jan 2025 12:55:00 -0500 Subject: [PATCH 1/3] Add support for Nodebalancers UDP (#494) * Implemented changes for NodeBalancers UDP * Added unit tests * Fix lint * Fixed issue with cipher_suite in save * Lint * Addressed PR comments * Removed overriden _serialize method --- linode_api4/objects/nodebalancer.py | 36 ++++++ .../nodebalancers_123456_configs.json | 28 ++++- ...ebalancers_123456_configs_65432_nodes.json | 12 +- .../models/nodebalancer/test_nodebalancer.py | 110 +++++++++++++++++- test/unit/objects/nodebalancers_test.py | 20 ++++ 5 files changed, 203 insertions(+), 3 deletions(-) diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index 840d5b965..ef1aee951 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -97,6 +97,8 @@ class NodeBalancerConfig(DerivedBase): "check_path": Property(mutable=True), "check_body": Property(mutable=True), "check_passive": Property(mutable=True), + "udp_check_port": Property(mutable=True), + "udp_session_timeout": Property(), "ssl_cert": Property(mutable=True), "ssl_key": Property(mutable=True), "ssl_commonname": Property(), @@ -106,6 +108,39 @@ class NodeBalancerConfig(DerivedBase): "proxy_protocol": Property(mutable=True), } + def save(self, force=True) -> bool: + """ + Send this NodeBalancerConfig's mutable values to the server in a PUT request. + :param force: If true, this method will always send a PUT request regardless of + whether the field has been explicitly updated. For optimization + purposes, this field should be set to false for typical update + operations. (Defaults to True) + :type force: bool + """ + + if not force and not self._changed: + return False + + data = self._serialize() + + print(data) + + if data.get("protocol") == "udp" and "cipher_suite" in data: + data.pop("cipher_suite") + + print(data) + + result = self._client.put( + NodeBalancerConfig.api_endpoint, model=self, data=data + ) + + if "error" in result: + return False + + self._populate(result) + + return True + @property def nodes(self): """ @@ -233,6 +268,7 @@ class NodeBalancer(Base): "configs": Property(derived_class=NodeBalancerConfig), "transfer": Property(), "tags": Property(mutable=True, unordered=True), + "client_udp_sess_throttle": Property(mutable=True), } # create derived objects diff --git a/test/fixtures/nodebalancers_123456_configs.json b/test/fixtures/nodebalancers_123456_configs.json index f12f1345f..cab9fb981 100644 --- a/test/fixtures/nodebalancers_123456_configs.json +++ b/test/fixtures/nodebalancers_123456_configs.json @@ -24,9 +24,35 @@ "protocol": "http", "ssl_fingerprint": "", "proxy_protocol": "none" + }, + { + "check": "connection", + "check_attempts": 2, + "stickiness": "table", + "check_interval": 5, + "check_body": "", + "id": 65431, + "check_passive": true, + "algorithm": "roundrobin", + "check_timeout": 3, + "check_path": "/", + "ssl_cert": null, + "ssl_commonname": "", + "port": 80, + "nodebalancer_id": 123456, + "cipher_suite": "none", + "ssl_key": null, + "nodes_status": { + "up": 0, + "down": 0 + }, + "protocol": "udp", + "ssl_fingerprint": "", + "proxy_protocol": "none", + "udp_check_port": 12345 } ], - "results": 1, + "results": 2, "page": 1, "pages": 1 } diff --git a/test/fixtures/nodebalancers_123456_configs_65432_nodes.json b/test/fixtures/nodebalancers_123456_configs_65432_nodes.json index 658edbb50..f8ffd9edf 100644 --- a/test/fixtures/nodebalancers_123456_configs_65432_nodes.json +++ b/test/fixtures/nodebalancers_123456_configs_65432_nodes.json @@ -9,9 +9,19 @@ "mode": "accept", "config_id": 54321, "nodebalancer_id": 123456 + }, + { + "id": 12345, + "address": "192.168.210.120", + "label": "node12345", + "status": "UP", + "weight": 50, + "mode": "none", + "config_id": 123456, + "nodebalancer_id": 123456 } ], "pages": 1, "page": 1, - "results": 1 + "results": 2 } diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 21f4d0322..df07de215 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -9,7 +9,7 @@ import pytest -from linode_api4 import ApiError, LinodeClient +from linode_api4 import ApiError, LinodeClient, NodeBalancer from linode_api4.objects import ( NodeBalancerConfig, NodeBalancerNode, @@ -64,6 +64,55 @@ def create_nb_config(test_linode_client, e2e_test_firewall): nb.delete() +@pytest.fixture(scope="session") +def create_nb_config_with_udp(test_linode_client, e2e_test_firewall): + client = test_linode_client + label = get_test_label(8) + + nb = client.nodebalancer_create( + region=TEST_REGION, label=label, firewall=e2e_test_firewall.id + ) + + config = nb.config_create(protocol="udp", udp_check_port=1234) + + yield config + + config.delete() + nb.delete() + + +@pytest.fixture(scope="session") +def create_nb(test_linode_client, e2e_test_firewall): + client = test_linode_client + label = get_test_label(8) + + nb = client.nodebalancer_create( + region=TEST_REGION, label=label, firewall=e2e_test_firewall.id + ) + + yield nb + + nb.delete() + + +def test_create_nb(test_linode_client, e2e_test_firewall): + client = test_linode_client + label = get_test_label(8) + + nb = client.nodebalancer_create( + region=TEST_REGION, + label=label, + firewall=e2e_test_firewall.id, + client_udp_sess_throttle=5, + ) + + assert TEST_REGION, nb.region + assert label == nb.label + assert 5 == nb.client_udp_sess_throttle + + nb.delete() + + def test_get_nodebalancer_config(test_linode_client, create_nb_config): config = test_linode_client.load( NodeBalancerConfig, @@ -72,6 +121,65 @@ def test_get_nodebalancer_config(test_linode_client, create_nb_config): ) +def test_get_nb_config_with_udp(test_linode_client, create_nb_config_with_udp): + config = test_linode_client.load( + NodeBalancerConfig, + create_nb_config_with_udp.id, + create_nb_config_with_udp.nodebalancer_id, + ) + + assert "udp" == config.protocol + assert 1234 == config.udp_check_port + assert 16 == config.udp_session_timeout + + +def test_update_nb_config(test_linode_client, create_nb_config_with_udp): + config = test_linode_client.load( + NodeBalancerConfig, + create_nb_config_with_udp.id, + create_nb_config_with_udp.nodebalancer_id, + ) + + config.udp_check_port = 4321 + config.save() + + config_updated = test_linode_client.load( + NodeBalancerConfig, + create_nb_config_with_udp.id, + create_nb_config_with_udp.nodebalancer_id, + ) + + assert 4321 == config_updated.udp_check_port + + +def test_get_nb(test_linode_client, create_nb): + nb = test_linode_client.load( + NodeBalancer, + create_nb.id, + ) + + assert nb.id == create_nb.id + + +def test_update_nb(test_linode_client, create_nb): + nb = test_linode_client.load( + NodeBalancer, + create_nb.id, + ) + + nb.label = "ThisNewLabel" + nb.client_udp_sess_throttle = 5 + nb.save() + + nb_updated = test_linode_client.load( + NodeBalancer, + create_nb.id, + ) + + assert "ThisNewLabel" == nb_updated.label + assert 5 == nb_updated.client_udp_sess_throttle + + @pytest.mark.smoke def test_create_nb_node( test_linode_client, create_nb_config, linode_with_private_ip diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index 05f0ad7de..ed0f0c320 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -42,6 +42,23 @@ def test_get_config(self): self.assertEqual(config.ssl_fingerprint, "") self.assertEqual(config.proxy_protocol, "none") + config_udp = NodeBalancerConfig(self.client, 65431, 123456) + self.assertEqual(config_udp.protocol, "udp") + self.assertEqual(config_udp.udp_check_port, 12345) + + def test_update_config_udp(self): + """ + Tests that a config with a protocol of udp can be updated and that cipher suite is properly excluded in save() + """ + with self.mock_put("nodebalancers/123456/configs/65431") as m: + config = self.client.load(NodeBalancerConfig, 65431, 123456) + config.udp_check_port = 54321 + config.save() + + self.assertEqual(m.call_url, "/nodebalancers/123456/configs/65431") + self.assertEqual(m.call_data["udp_check_port"], 54321) + self.assertNotIn("cipher_suite", m.call_data) + class NodeBalancerNodeTest(ClientBaseCase): """ @@ -66,6 +83,9 @@ def test_get_node(self): self.assertEqual(node.config_id, 65432) self.assertEqual(node.nodebalancer_id, 123456) + node_udp = NodeBalancerNode(self.client, 12345, (65432, 123456)) + self.assertEqual(node_udp.mode, "none") + def test_create_node(self): """ Tests that a node can be created From b0a92bf271091b972ea57e1546f5eabf2efb206e Mon Sep 17 00:00:00 2001 From: Lena Garber Date: Thu, 15 May 2025 14:19:47 -0400 Subject: [PATCH 2/3] Drop residual prints --- linode_api4/objects/nodebalancer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index ef1aee951..fbb1d2458 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -123,13 +123,9 @@ def save(self, force=True) -> bool: data = self._serialize() - print(data) - if data.get("protocol") == "udp" and "cipher_suite" in data: data.pop("cipher_suite") - print(data) - result = self._client.put( NodeBalancerConfig.api_endpoint, model=self, data=data ) From 0eced308f96bcabb1c4cf9f5079c7797dbfea127 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Thu, 22 May 2025 10:40:21 -0400 Subject: [PATCH 3/3] Implement _serialize(...) override in NodeBalancerConfig (#555) --- linode_api4/objects/nodebalancer.py | 31 ++++++++--------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index fbb1d2458..9e05bd3f6 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -108,34 +108,19 @@ class NodeBalancerConfig(DerivedBase): "proxy_protocol": Property(mutable=True), } - def save(self, force=True) -> bool: + def _serialize(self, is_put: bool = False): """ - Send this NodeBalancerConfig's mutable values to the server in a PUT request. - :param force: If true, this method will always send a PUT request regardless of - whether the field has been explicitly updated. For optimization - purposes, this field should be set to false for typical update - operations. (Defaults to True) - :type force: bool + This override removes the `cipher_suite` field from the PUT request + body on calls to save(...) for UDP configs, which is rejected by + the API. """ - if not force and not self._changed: - return False + result = super()._serialize(is_put) - data = self._serialize() + if is_put and result["protocol"] == "udp" and "cipher_suite" in result: + del result["cipher_suite"] - if data.get("protocol") == "udp" and "cipher_suite" in data: - data.pop("cipher_suite") - - result = self._client.put( - NodeBalancerConfig.api_endpoint, model=self, data=data - ) - - if "error" in result: - return False - - self._populate(result) - - return True + return result @property def nodes(self):