From 6d9a8bc0afa1fc4f3cb0ce5186d44e4b173696eb Mon Sep 17 00:00:00 2001 From: Dawid <56120592+dawiddzhafarov@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:33:19 +0100 Subject: [PATCH 1/3] Add support for NB frontend VPC IP and NB plan type (#660) * Add support for NB frontend VPC IP and NB plan type * Fix test fixture and doc link * Fix NodeBalancerVPCConfig struct and address copilot suggestions * Fix doc links --- linode_api4/objects/nodebalancer.py | 102 +++++++++++++++ test/fixtures/nodebalancers.json | 12 +- test/fixtures/nodebalancers_123456.json | 5 +- .../nodebalancers_12345_backend__vpcs.json | 16 +++ .../nodebalancers_12345_frontend__vpcs.json | 16 +++ test/fixtures/nodebalancers_12345_vpcs.json | 25 ++++ .../fixtures/nodebalancers_12345_vpcs_99.json | 9 ++ test/unit/objects/nodebalancers_test.py | 117 ++++++++++++++++++ 8 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/nodebalancers_12345_backend__vpcs.json create mode 100644 test/fixtures/nodebalancers_12345_frontend__vpcs.json create mode 100644 test/fixtures/nodebalancers_12345_vpcs.json create mode 100644 test/fixtures/nodebalancers_12345_vpcs_99.json diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index cb6e566f7..bae9621ff 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -229,6 +229,28 @@ def load_ssl_data(self, cert_file, key_file): self.ssl_key = f.read() +class NodeBalancerVPCConfig(DerivedBase): + """ + The VPC configuration for this NodeBalancer. + + API documentation: https://techdocs.akamai.com/linode-api/reference/get-node-balancer-vpc-config + """ + + api_endpoint = "/nodebalancers/{nodebalancer_id}/vpcs/{id}" + derived_url_path = "vpcs" + parent_id_name = "nodebalancer_id" + + properties = { + "id": Property(identifier=True), + "nodebalancer_id": Property(identifier=True), + "ipv4_range": Property(), + "ipv6_range": Property(), + "subnet_id": Property(), + "vpc_id": Property(), + "purpose": Property(), + } + + class NodeBalancer(Base): """ A single NodeBalancer you can access. @@ -253,6 +275,9 @@ class NodeBalancer(Base): "tags": Property(mutable=True, unordered=True), "client_udp_sess_throttle": Property(mutable=True), "locks": Property(unordered=True), + "type": Property(), + "frontend_address_type": Property(), + "frontend_vpc_subnet_id": Property(), } # create derived objects @@ -356,3 +381,80 @@ def firewalls(self): Firewall(self._client, firewall["id"]) for firewall in result["data"] ] + + def vpcs(self): + """ + View VPC information for VPCs associated with this NodeBalancer. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-node-balancer-vpcs + + :returns: A List of NodeBalancerVPCConfig of the Linode NodeBalancer. + :rtype: List[NodeBalancerVPCConfig] + """ + result = self._client.get( + "{}/vpcs".format(NodeBalancer.api_endpoint), model=self + ) + + return [ + NodeBalancerVPCConfig(self._client, vpc["id"], self.id, json=vpc) + for vpc in result["data"] + ] + + def vpc(self, id): + """ + View VPC information for a VPC associated with this NodeBalancer. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-node-balancer-vpc-config + + :param id: The ID of the NodeBalancer VPC Config to view. + :type id: int + + :returns: A NodeBalancerVPCConfig of the Linode NodeBalancer. + :rtype: NodeBalancerVPCConfig + """ + result = self._client.get( + "{}/vpcs/{}".format( + NodeBalancer.api_endpoint, parse.quote(str(id)) + ), + model=self, + ) + + return NodeBalancerVPCConfig( + self._client, result["id"], self.id, json=result + ) + + def backend_vpcs(self): + """ + View VPC information for backend VPCs associated with this NodeBalancer. + + API Documentation: TODO + + :returns: A List of NodeBalancerVPCConfig of the Linode NodeBalancer. + :rtype: List[NodeBalancerVPCConfig] + """ + result = self._client.get( + "{}/backend_vpcs".format(NodeBalancer.api_endpoint), model=self + ) + + return [ + NodeBalancerVPCConfig(self._client, vpc["id"], self.id, json=vpc) + for vpc in result["data"] + ] + + def frontend_vpcs(self): + """ + View VPC information for frontend VPCs associated with this NodeBalancer. + + API Documentation: TODO + + :returns: A List of NodeBalancerVPCConfig of the Linode NodeBalancer. + :rtype: List[NodeBalancerVPCConfig] + """ + result = self._client.get( + "{}/frontend_vpcs".format(NodeBalancer.api_endpoint), model=self + ) + + return [ + NodeBalancerVPCConfig(self._client, vpc["id"], self.id, json=vpc) + for vpc in result["data"] + ] diff --git a/test/fixtures/nodebalancers.json b/test/fixtures/nodebalancers.json index 9b4dc8dae..2ccbed4b1 100644 --- a/test/fixtures/nodebalancers.json +++ b/test/fixtures/nodebalancers.json @@ -11,7 +11,10 @@ "label": "balancer123456", "client_conn_throttle": 0, "tags": ["something"], - "locks": ["cannot_delete_with_subresources"] + "locks": ["cannot_delete_with_subresources"], + "type": "common", + "frontend_address_type": "vpc", + "frontend_vpc_subnet_id": 5555 }, { "created": "2018-01-01T00:01:01", @@ -24,10 +27,13 @@ "label": "balancer123457", "client_conn_throttle": 0, "tags": [], - "locks": [] + "locks": [], + "type": "premium_40gb", + "frontend_address_type": "vpc", + "frontend_vpc_subnet_id": 6666 } ], "results": 2, "page": 1, "pages": 1 -} +} \ No newline at end of file diff --git a/test/fixtures/nodebalancers_123456.json b/test/fixtures/nodebalancers_123456.json index a78c8d3e3..d6d66233c 100644 --- a/test/fixtures/nodebalancers_123456.json +++ b/test/fixtures/nodebalancers_123456.json @@ -13,5 +13,8 @@ ], "locks": [ "cannot_delete_with_subresources" - ] + ], + "type": "common", + "frontend_address_type": "vpc", + "frontend_vpc_subnet_id": 5555 } \ No newline at end of file diff --git a/test/fixtures/nodebalancers_12345_backend__vpcs.json b/test/fixtures/nodebalancers_12345_backend__vpcs.json new file mode 100644 index 000000000..8ff080273 --- /dev/null +++ b/test/fixtures/nodebalancers_12345_backend__vpcs.json @@ -0,0 +1,16 @@ +{ + "data": [ + { + "id": 101, + "nodebalancer_id": 12345, + "subnet_id": 6666, + "vpc_id": 222, + "ipv4_range": "10.200.1.0/24", + "ipv6_range": "2001:db8:2::/64", + "purpose": "backend" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/fixtures/nodebalancers_12345_frontend__vpcs.json b/test/fixtures/nodebalancers_12345_frontend__vpcs.json new file mode 100644 index 000000000..9085f82d1 --- /dev/null +++ b/test/fixtures/nodebalancers_12345_frontend__vpcs.json @@ -0,0 +1,16 @@ +{ + "data": [ + { + "id": 99, + "nodebalancer_id": 12345, + "subnet_id": 5555, + "vpc_id": 111, + "ipv4_range": "10.100.5.0/24", + "ipv6_range": "2001:db8::/64", + "purpose": "frontend" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/fixtures/nodebalancers_12345_vpcs.json b/test/fixtures/nodebalancers_12345_vpcs.json new file mode 100644 index 000000000..2b0be0a5d --- /dev/null +++ b/test/fixtures/nodebalancers_12345_vpcs.json @@ -0,0 +1,25 @@ +{ + "data": [ + { + "id": 99, + "nodebalancer_id": 12345, + "subnet_id": 5555, + "vpc_id": 111, + "ipv4_range": "10.100.5.0/24", + "ipv6_range": "2001:db8::/64", + "purpose": "frontend" + }, + { + "id": 100, + "nodebalancer_id": 12345, + "subnet_id": 5556, + "vpc_id": 112, + "ipv4_range": "10.100.6.0/24", + "ipv6_range": "2001:db8:1::/64", + "purpose": "backend" + } + ], + "page": 1, + "pages": 1, + "results": 2 +} \ No newline at end of file diff --git a/test/fixtures/nodebalancers_12345_vpcs_99.json b/test/fixtures/nodebalancers_12345_vpcs_99.json new file mode 100644 index 000000000..3f27bcc6d --- /dev/null +++ b/test/fixtures/nodebalancers_12345_vpcs_99.json @@ -0,0 +1,9 @@ +{ + "id": 99, + "nodebalancer_id": 12345, + "subnet_id": 5555, + "vpc_id": 111, + "ipv4_range": "10.100.5.0/24", + "ipv6_range": "2001:db8::/64", + "purpose": "frontend" +} \ No newline at end of file diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index c02b40ea3..2eae14664 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -4,6 +4,7 @@ NodeBalancer, NodeBalancerConfig, NodeBalancerNode, + NodeBalancerVPCConfig, ) @@ -266,3 +267,119 @@ def test_statistics(self): "linode.com - balancer12345 (12345) - day (5 min avg)", ) self.assertEqual(m.call_url, statistics_url) + + def test_list_nodebalancers(self): + """ + Test that you can list all NodeBalancers. + """ + nbs = self.client.nodebalancers() + + self.assertEqual(len(nbs), 2) + + self.assertEqual(nbs[0].id, 123456) + self.assertEqual(nbs[0].label, "balancer123456") + self.assertEqual(nbs[0].type, "common") + self.assertEqual(nbs[0].frontend_address_type, "vpc") + self.assertEqual(nbs[0].frontend_vpc_subnet_id, 5555) + + self.assertEqual(nbs[1].id, 123457) + self.assertEqual(nbs[1].label, "balancer123457") + self.assertEqual(nbs[1].type, "premium_40gb") + self.assertEqual(nbs[1].frontend_address_type, "vpc") + self.assertEqual(nbs[1].frontend_vpc_subnet_id, 6666) + + def test_get_nodebalancer(self): + """ + Test that you can get a single NodeBalancer by ID. + """ + nb = NodeBalancer(self.client, 123456) + + self.assertEqual(nb.id, 123456) + self.assertEqual(nb.label, "balancer123456") + self.assertEqual(nb.type, "common") + self.assertEqual(nb.frontend_address_type, "vpc") + self.assertEqual(nb.frontend_vpc_subnet_id, 5555) + + def test_vpcs(self): + """ + Test that you can list VPC configurations for a NodeBalancer. + """ + vpcs_url = "/nodebalancers/12345/vpcs" + with self.mock_get(vpcs_url) as m: + nb = NodeBalancer(self.client, 12345) + result = nb.vpcs() + + self.assertEqual(m.call_url, vpcs_url) + self.assertEqual(len(result), 2) + + self.assertIsInstance(result[0], NodeBalancerVPCConfig) + self.assertEqual(result[0].id, 99) + self.assertEqual(result[0].subnet_id, 5555) + self.assertEqual(result[0].vpc_id, 111) + self.assertEqual(result[0].ipv4_range, "10.100.5.0/24") + self.assertEqual(result[0].ipv6_range, "2001:db8::/64") + self.assertEqual(result[0].purpose, "frontend") + + self.assertIsInstance(result[1], NodeBalancerVPCConfig) + self.assertEqual(result[1].id, 100) + self.assertEqual(result[1].subnet_id, 5556) + self.assertEqual(result[1].vpc_id, 112) + self.assertEqual(result[1].ipv4_range, "10.100.6.0/24") + self.assertEqual(result[1].ipv6_range, "2001:db8:1::/64") + self.assertEqual(result[1].purpose, "backend") + + def test_vpc(self): + """ + Test that you can get a single VPC configuration for a NodeBalancer. + """ + vpc_url = "/nodebalancers/12345/vpcs/99" + with self.mock_get(vpc_url) as m: + nb = NodeBalancer(self.client, 12345) + result = nb.vpc(99) + + self.assertEqual(m.call_url, vpc_url) + self.assertIsInstance(result, NodeBalancerVPCConfig) + self.assertEqual(result.id, 99) + self.assertEqual(result.subnet_id, 5555) + self.assertEqual(result.vpc_id, 111) + self.assertEqual(result.ipv4_range, "10.100.5.0/24") + self.assertEqual(result.ipv6_range, "2001:db8::/64") + self.assertEqual(result.purpose, "frontend") + + def test_backend_vpcs(self): + """ + Test that you can list backend VPC configurations for a NodeBalancer. + """ + backend_vpcs_url = "/nodebalancers/12345/backend_vpcs" + with self.mock_get(backend_vpcs_url) as m: + nb = NodeBalancer(self.client, 12345) + result = nb.backend_vpcs() + + self.assertEqual(m.call_url, backend_vpcs_url) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], NodeBalancerVPCConfig) + self.assertEqual(result[0].id, 101) + self.assertEqual(result[0].subnet_id, 6666) + self.assertEqual(result[0].vpc_id, 222) + self.assertEqual(result[0].ipv4_range, "10.200.1.0/24") + self.assertEqual(result[0].ipv6_range, "2001:db8:2::/64") + self.assertEqual(result[0].purpose, "backend") + + def test_frontend_vpcs(self): + """ + Test that you can list frontend VPC configurations for a NodeBalancer. + """ + frontend_vpcs_url = "/nodebalancers/12345/frontend_vpcs" + with self.mock_get(frontend_vpcs_url) as m: + nb = NodeBalancer(self.client, 12345) + result = nb.frontend_vpcs() + + self.assertEqual(m.call_url, frontend_vpcs_url) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0], NodeBalancerVPCConfig) + self.assertEqual(result[0].id, 99) + self.assertEqual(result[0].subnet_id, 5555) + self.assertEqual(result[0].vpc_id, 111) + self.assertEqual(result[0].ipv4_range, "10.100.5.0/24") + self.assertEqual(result[0].ipv6_range, "2001:db8::/64") + self.assertEqual(result[0].purpose, "frontend") From 6c4292d1b759a9365eb27df653ec5ebc853b724c Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Mon, 16 Mar 2026 09:28:21 +0100 Subject: [PATCH 2/3] TPT-4206: Integration tests for NB Front-End IP & 40Gbps (#664) * Create integration tests for NodeBalancer Front-End IP & 40Gbps * Update TODO message * Linter fixes * Copilot remarks refactor * Remove redundant variables (PR remarks) * Modify tests to filter out invalid regions for premium NBs * Modify test names to indicate that test is expected to fail * Add DevCloud region to PREMIUM_REGIONS * Linter fix * Add note about no DevCloud region in PREMIUM_40GB_REGIONS list --- test/integration/conftest.py | 88 ++++++- .../models/nodebalancer/test_nodebalancer.py | 236 ++++++++++++++++++ 2 files changed, 320 insertions(+), 4 deletions(-) diff --git a/test/integration/conftest.py b/test/integration/conftest.py index a5c832f4f..5ba55387f 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -9,7 +9,7 @@ wait_for_condition, ) from test.integration.models.database.helpers import get_db_engine_id -from typing import Optional, Set +from typing import List, Optional, Set import pytest import requests @@ -112,9 +112,18 @@ def get_regions( def get_region( - client: LinodeClient, capabilities: Set[str] = None, site_type: str = "core" + client: LinodeClient, + capabilities: Set[str] = None, + site_type: str = "core", + valid_regions: List[str] = None, ): - return random.choice(get_regions(client, capabilities, site_type)) + regions = get_regions(client, capabilities, site_type) + + # To filter out regions that cannot be used for the Linode resource + if valid_regions: + regions = [reg for reg in regions if reg.id in valid_regions] + + return random.choice(regions) def get_api_ca_file(): @@ -457,7 +466,14 @@ def create_vpc(test_linode_client): vpc = client.vpcs.create( label=label, region=get_region( - test_linode_client, {"VPCs", "VPC IPv6 Stack", "Linode Interfaces"} + test_linode_client, + { + "VPCs", + "VPC IPv6 Stack", + "VPC Dual Stack", + "Linode Interfaces", + "NodeBalancers", + }, ), description="test description", ipv6=[{"range": "auto"}], @@ -467,6 +483,24 @@ def create_vpc(test_linode_client): vpc.delete() +@pytest.fixture(scope="session") +def create_vpc_ipv4(test_linode_client): + client = test_linode_client + + label = get_test_label(length=10) + "-ipv4" + + vpc = client.vpcs.create( + label=label, + region=get_region( + test_linode_client, {"VPCs", "Linode Interfaces", "NodeBalancers"} + ), + description="test description", + ) + yield vpc + + vpc.delete() + + @pytest.fixture(scope="session") def create_vpc_with_subnet(test_linode_client, create_vpc): subnet = create_vpc.subnet_create( @@ -480,6 +514,52 @@ def create_vpc_with_subnet(test_linode_client, create_vpc): subnet.delete() +@pytest.fixture(scope="session") +def create_vpc_with_subnet_ipv4(test_linode_client, create_vpc_ipv4): + subnet = create_vpc_ipv4.subnet_create( + label="test-subnet", ipv4="10.0.0.0/24" + ) + + yield create_vpc_ipv4, subnet + + subnet.delete() + + +@pytest.fixture(scope="function") +def create_vpc_with_subnet_in_premium_region(request, test_linode_client): + premium_regions = getattr(request, "param") + client = test_linode_client + label = get_test_label(length=10) + + vpc = client.vpcs.create( + label=label, + region=get_region( + client, + { + "VPCs", + "VPC IPv6 Stack", + "VPC Dual Stack", + "Linode Interfaces", + "NodeBalancers", + }, + valid_regions=premium_regions, + ), + description="test description", + ipv6=[{"range": "auto"}], + ) + + subnet = vpc.subnet_create( + label="test-subnet", + ipv4="10.0.0.0/24", + ipv6=[{"range": "auto"}], + ) + + yield vpc, subnet + + subnet.delete() + vpc.delete() + + @pytest.fixture(scope="session") def create_vpc_with_subnet_and_linode( test_linode_client, create_vpc_with_subnet, e2e_test_firewall diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 692efb027..66565d3da 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -1,3 +1,4 @@ +import ipaddress import re from test.integration.conftest import ( get_api_ca_file, @@ -17,6 +18,29 @@ RegionPrice, ) +# Lists of valid regions where NodeBalancers of type "premium" or "premium_40gb" can be created +PREMIUM_REGIONS = [ + "nl-ams", + "jp-tyo-3", + "sg-sin-2", + "de-fra-2", + "in-bom-2", + "gb-lon", + "us-lax", + "id-cgk", + "us-mia", + "it-mil", + "jp-osa", + "in-maa", + "se-sto", + "br-gru", + "us-sea", + "fr-par", + "us-iad", + "pl-labkrk-2", # DevCloud +] +PREMIUM_40GB_REGIONS = ["us-iad"] # No DevCloud region for premium_40gb type + TEST_REGION = get_region( LinodeClient( token=get_token(), @@ -272,3 +296,215 @@ def test_nodebalancer_types(test_linode_client): isinstance(region_price.monthly, (float, int)) and region_price.monthly >= 0 ) + + +def test_nb_with_backend_only(test_linode_client, create_vpc_with_subnet): + client = test_linode_client + label = get_test_label(8) + + nb = client.nodebalancer_create( + region=create_vpc_with_subnet[0].region, + label=label, + vpcs=[ + { + "vpc_id": create_vpc_with_subnet[0].id, + "subnet_id": create_vpc_with_subnet[1].id, + } + ], + ) + + assert isinstance( + ipaddress.ip_address(nb.ipv4.address), ipaddress.IPv4Address + ) + assert isinstance(ipaddress.ip_address(nb.ipv6), ipaddress.IPv6Address) + assert nb.frontend_address_type == "public" + assert nb.frontend_vpc_subnet_id is None + + nb_get = NodeBalancer(client, nb.id) + nb_vpcs = nb_get.vpcs() + + assert len(nb_vpcs) == 1 + assert nb_vpcs[0].purpose == "backend" + + nb_vpc = nb_get.vpc(nb_vpcs[0].id) + + assert nb_vpc.purpose == "backend" + + # TODO: Uncomment when API implementation of /backend_vpcs and /frontend_vpcs endpoints is finished + # nb_backend_vpcs = nb_get.backend_vpcs() + # assert len(nb_backend_vpcs) == 1 + # assert nb_backend_vpcs[0].purpose == 'backend' + # + # nb_frontend_vpcs = nb_get.frontend_vpcs() + # assert len(nb_frontend_vpcs) == 0 + + nb.delete() + + +def test_nb_with_frontend_ipv4_only_in_single_stack_vpc( + test_linode_client, create_vpc_with_subnet_ipv4 +): + client = test_linode_client + subnet = create_vpc_with_subnet_ipv4[1].id + label = get_test_label(8) + ipv4_address = "10.0.0.2" # first available address + + nb = client.nodebalancer_create( + region=create_vpc_with_subnet_ipv4[0].region, + label=label, + frontend_vpcs=[{"subnet_id": subnet, "ipv4_range": "10.0.0.0/24"}], + type="premium", + ) + assert nb.ipv4.address == ipv4_address + assert nb.ipv6 is None + assert nb.frontend_address_type == "vpc" + assert nb.frontend_vpc_subnet_id == subnet + + # TODO: Uncomment when API implementation of /backend_vpcs and /frontend_vpcs endpoints is finished + # nb_frontend_vpcs = nb_get.frontend_vpcs() + # assert len(nb_frontend_vpcs) == 1 + # assert nb_frontend_vpcs[0].purpose == 'frontend' + # + # nb_backend_vpcs = nb_get.backend_vpcs() + # assert len(nb_backend_vpcs) == 0 + + nb.delete() + + +def test_nb_with_frontend_ipv6_in_single_stack_vpc_fail( + test_linode_client, create_vpc_with_subnet_ipv4 +): + client = test_linode_client + label = get_test_label(8) + + with pytest.raises(ApiError) as excinfo: + client.nodebalancer_create( + region=create_vpc_with_subnet_ipv4[0].region, + label=label, + frontend_vpcs=[ + { + "subnet_id": create_vpc_with_subnet_ipv4[1].id, + "ipv6_range": "/62", + } + ], + type="premium", + ) + + error_msg = str(excinfo.value.json) + assert excinfo.value.status == 400 + assert "No IPv6 subnets available in VPC" in error_msg + + +def test_nb_with_frontend_and_default_type_fail( + test_linode_client, create_vpc_with_subnet +): + client = test_linode_client + label = get_test_label(8) + + with pytest.raises(ApiError) as excinfo: + client.nodebalancer_create( + region=create_vpc_with_subnet[0].region, + label=label, + frontend_vpcs=[{"subnet_id": create_vpc_with_subnet[1].id}], + ) + + error_msg = str(excinfo.value.json) + assert excinfo.value.status == 400 + assert "NodeBalancer with frontend VPC IP must be premium" in error_msg + + +@pytest.mark.parametrize( + "create_vpc_with_subnet_in_premium_region", + [PREMIUM_40GB_REGIONS], + indirect=True, +) +def test_nb_with_premium40gb_type( + test_linode_client, create_vpc_with_subnet_in_premium_region +): + client = test_linode_client + + nb = client.nodebalancer_create( + region=create_vpc_with_subnet_in_premium_region[0].region, + label=get_test_label(length=8), + type="premium_40gb", + ) + assert nb.type == "premium_40gb" + + nb_get = test_linode_client.load( + NodeBalancer, + nb.id, + ) + assert nb_get.type == "premium_40gb" + + nb.delete() + + +@pytest.mark.parametrize( + "create_vpc_with_subnet_in_premium_region", [PREMIUM_REGIONS], indirect=True +) +def test_nb_with_frontend_and_backend_in_different_vpcs( + test_linode_client, create_vpc_with_subnet_in_premium_region +): + client = test_linode_client + region = create_vpc_with_subnet_in_premium_region[0].region + vpc_backend = create_vpc_with_subnet_in_premium_region[0].id + subnet_backend = create_vpc_with_subnet_in_premium_region[1].id + label = get_test_label(8) + ipv4_range = "10.0.0.0/24" + ipv4_address = "10.0.0.2" # first available address + + vpc_frontend = client.vpcs.create( + label=get_test_label(length=10), + region=region, + description="test description", + ipv6=[{"range": "auto"}], + ) + + subnet_frontend = vpc_frontend.subnet_create( + label="test-subnet", + ipv4=ipv4_range, + ipv6=[{"range": "auto"}], + ) + ipv6_range = subnet_frontend.ipv6[0].range + ipv6_address = ipv6_range.split("::")[0] + ":1::1" + + nb = client.nodebalancer_create( + region=region, + label=label, + vpcs=[{"vpc_id": vpc_backend, "subnet_id": subnet_backend}], + frontend_vpcs=[ + { + "subnet_id": subnet_frontend.id, + "ipv4_range": ipv4_range, + "ipv6_range": ipv6_range, + } + ], + type="premium", + ) + + assert nb.ipv4.address == ipv4_address + assert nb.ipv6 == ipv6_address + assert nb.frontend_address_type == "vpc" + assert nb.frontend_vpc_subnet_id == subnet_frontend.id + + nb_get = NodeBalancer(client, nb.id) + nb_vpcs = nb_get.vpcs() + nb_vpcs.sort(key=lambda x: x.purpose) + + assert len(nb_vpcs) == 2 + assert nb_vpcs[0].purpose == "backend" + assert nb_vpcs[1].ipv4_range == f"{ipv4_address}/32" + assert nb_vpcs[1].ipv6_range == f"{ipv6_address[:-1]}/64" + assert nb_vpcs[1].purpose == "frontend" + + # TODO: Uncomment when API implementation of /backend_vpcs and /frontend_vpcs endpoints is finished + # nb_backend_vpcs = nb_get.backend_vpcs() + # assert len(nb_backend_vpcs) == 1 + # assert nb_backend_vpcs[0].purpose == 'backend' + # + # nb_frontend_vpcs = nb_get.frontend_vpcs() + # assert len(nb_frontend_vpcs) == 1 + # assert nb_frontend_vpcs[0].purpose == 'frontend' + + nb.delete() + vpc_frontend.delete() From e34a81a7ef642c313405d6e6b08074afebb830a9 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Wed, 1 Apr 2026 13:05:11 +0200 Subject: [PATCH 3/3] TPT-4368: Update integration tests due to deprecated "vpcs" for NB create (#676) * Replace vpcs with backend_vpcs to be passed to POST /nodebalancers * Add test for deprecated VPCs attribute --- .../models/nodebalancer/test_nodebalancer.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 66565d3da..1fa081012 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -305,7 +305,7 @@ def test_nb_with_backend_only(test_linode_client, create_vpc_with_subnet): nb = client.nodebalancer_create( region=create_vpc_with_subnet[0].region, label=label, - vpcs=[ + backend_vpcs=[ { "vpc_id": create_vpc_with_subnet[0].id, "subnet_id": create_vpc_with_subnet[1].id, @@ -471,7 +471,7 @@ def test_nb_with_frontend_and_backend_in_different_vpcs( nb = client.nodebalancer_create( region=region, label=label, - vpcs=[{"vpc_id": vpc_backend, "subnet_id": subnet_backend}], + backend_vpcs=[{"vpc_id": vpc_backend, "subnet_id": subnet_backend}], frontend_vpcs=[ { "subnet_id": subnet_frontend.id, @@ -508,3 +508,30 @@ def test_nb_with_frontend_and_backend_in_different_vpcs( nb.delete() vpc_frontend.delete() + + +def test_nb_with_deprecated_vpcs_attribute( + test_linode_client, create_vpc_with_subnet +): + # TODO: The test will be deleted when a deprecated vpcs attribute can no longer be used + client = test_linode_client + label = get_test_label(8) + + nb = client.nodebalancer_create( + region=create_vpc_with_subnet[0].region, + label=label, + vpcs=[ + { + "subnet_id": create_vpc_with_subnet[1].id, + } + ], + ) + + assert isinstance( + ipaddress.ip_address(nb.ipv4.address), ipaddress.IPv4Address + ) + assert isinstance(ipaddress.ip_address(nb.ipv6), ipaddress.IPv6Address) + assert nb.frontend_address_type == "public" + assert nb.frontend_vpc_subnet_id is None + + nb.delete()