diff --git a/linode_api4/groups/nodebalancer.py b/linode_api4/groups/nodebalancer.py index 20252efaa..4c9cb72fa 100644 --- a/linode_api4/groups/nodebalancer.py +++ b/linode_api4/groups/nodebalancer.py @@ -35,6 +35,27 @@ def create(self, region, **kwargs): :param ipv4: A reserved IPv4 address to assign to this NodeBalancer. NOTE: Reserved IP feature may not currently be available to all users. :type ipv4: str + :param type: The NodeBalancer type. Supported values include + ``common``, ``basic``, ``premium``, ``premium_40g``, + and ``enterprise``. This cannot be changed after creation. + NOTE: Creating premium or enterprise NodeBalancers may not + currently be available to all users. + :type type: str + :param backend_connectivity: How this NodeBalancer communicates with + backends (``legacy``, ``ipv6``, or ``vpc``). If omitted, + the API infers a value from ``vpcs`` or config nodes, or + returns ``undefined`` until the first node is added. + ``undefined`` cannot be sent by clients. This cannot be + changed after creation. + NOTE: This field may not currently be available to all users. + :type backend_connectivity: str + :param vpcs: VPC attachments for this NodeBalancer. Required when + ``backend_connectivity`` is ``vpc``. + :type vpcs: list[dict] + :param configs: NodeBalancer configs and optional nodes to create + with this NodeBalancer. Node addresses must match the + selected or inferred backend connectivity. + :type configs: list[dict] :returns: The new NodeBalancer :rtype: NodeBalancer diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index f70553295..7822b3444 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -162,8 +162,10 @@ def node_create(self, label, address, **kwargs): API documentation: https://techdocs.akamai.com/linode-api/reference/post-node-balancer-node - :param address: The private IP Address where this backend can be reached. - This must be a private IP address. + :param address: The address where this backend can be reached. This may + be a private IPv4 address, a public IPv6 address in + ``[IPv6]:port`` format, or a VPC address. The address + type must match this NodeBalancer's ``backend_connectivity``. :type address: str :param label: The label for this node. This is for display purposes only. @@ -255,6 +257,8 @@ class NodeBalancer(Base): "tags": Property(mutable=True, unordered=True), "client_udp_sess_throttle": Property(mutable=True), "locks": Property(unordered=True), + "type": Property(), + "backend_connectivity": Property(), } # create derived objects diff --git a/test/fixtures/linode_instances_123_nodebalancers.json b/test/fixtures/linode_instances_123_nodebalancers.json index 821ff4801..a291e4fc2 100644 --- a/test/fixtures/linode_instances_123_nodebalancers.json +++ b/test/fixtures/linode_instances_123_nodebalancers.json @@ -8,6 +8,8 @@ "ipv4": "203.0.113.1", "ipv6": null, "label": "balancer12345", + "type": "premium", + "backend_connectivity": "ipv6", "region": "us-east", "tags": [ "example tag", diff --git a/test/fixtures/nodebalancers.json b/test/fixtures/nodebalancers.json index 9b4dc8dae..2175096b6 100644 --- a/test/fixtures/nodebalancers.json +++ b/test/fixtures/nodebalancers.json @@ -11,7 +11,9 @@ "label": "balancer123456", "client_conn_throttle": 0, "tags": ["something"], - "locks": ["cannot_delete_with_subresources"] + "locks": ["cannot_delete_with_subresources"], + "type": "premium", + "backend_connectivity": "ipv6" }, { "created": "2018-01-01T00:01:01", @@ -24,7 +26,9 @@ "label": "balancer123457", "client_conn_throttle": 0, "tags": [], - "locks": [] + "locks": [], + "type": "premium", + "backend_connectivity": "ipv6" } ], "results": 2, diff --git a/test/fixtures/nodebalancers_123456.json b/test/fixtures/nodebalancers_123456.json index a78c8d3e3..3261ea46b 100644 --- a/test/fixtures/nodebalancers_123456.json +++ b/test/fixtures/nodebalancers_123456.json @@ -13,5 +13,7 @@ ], "locks": [ "cannot_delete_with_subresources" - ] -} \ No newline at end of file + ], + "type": "premium", + "backend_connectivity": "ipv6" +} diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 4ddefb15c..102e19cbf 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -106,11 +106,15 @@ def test_create_nb(test_linode_client, e2e_test_firewall): label=label, firewall=e2e_test_firewall.id, client_udp_sess_throttle=5, + type="premium", + backend_connectivity="ipv6", ) assert TEST_REGION, nb.region assert label == nb.label assert 5 == nb.client_udp_sess_throttle + assert nb.type == "premium" + assert nb.backend_connectivity == "ipv6" nb.delete() diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 3b225d9e2..a12d0d204 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -1589,6 +1589,78 @@ def test_nodebalancer_types(self): self.assertEqual(types[0].region_prices[0].hourly, 0.018) self.assertEqual(types[0].region_prices[0].monthly, 12) + def test_create_with_type_and_backend_connectivity(self): + """ + Tests that creating a NodeBalancer forwards type, backend_connectivity, + vpcs, and config node addresses. + """ + with self.mock_post( + { + "id": 1234, + "label": "my-premium-nb", + "type": "premium", + "backend_connectivity": "ipv6", + "region": "us-east", + } + ) as m: + nb = self.client.nodebalancers.create( + "us-east", + label="my-premium-nb", + type="premium", + backend_connectivity="ipv6", + configs=[ + { + "port": 80, + "nodes": [ + { + "address": "[2001:db8:abcd:0012::1]:80", + "label": "node1", + } + ], + } + ], + ) + + self.assertEqual(m.call_url, "/nodebalancers") + self.assertEqual(m.call_data["region"], "us-east") + self.assertEqual(m.call_data["type"], "premium") + self.assertEqual(m.call_data["backend_connectivity"], "ipv6") + self.assertEqual( + m.call_data["configs"][0]["nodes"][0]["address"], + "[2001:db8:abcd:0012::1]:80", + ) + self.assertEqual(nb.id, 1234) + self.assertEqual(nb.type, "premium") + self.assertEqual(nb.backend_connectivity, "ipv6") + + def test_create_with_vpc_backend_connectivity(self): + """ + Tests that creating a NodeBalancer forwards vpc backend connectivity. + """ + with self.mock_post( + { + "id": 1234, + "label": "my-nb", + "type": "common", + "backend_connectivity": "vpc", + "region": "us-east", + } + ) as m: + nb = self.client.nodebalancers.create( + "us-east", + label="my-nb", + backend_connectivity="vpc", + vpcs=[{"subnet_id": 123456, "ipv4_range": "10.0.250.4/30"}], + ) + + self.assertEqual(m.call_url, "/nodebalancers") + self.assertEqual(m.call_data["backend_connectivity"], "vpc") + self.assertEqual( + m.call_data["vpcs"], + [{"subnet_id": 123456, "ipv4_range": "10.0.250.4/30"}], + ) + self.assertEqual(nb.backend_connectivity, "vpc") + class VolumeGroupTest(ClientBaseCase): """ diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index c02b40ea3..a3e6ef166 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -113,6 +113,29 @@ def test_create_node(self): }, ) + def test_create_ipv6_node(self): + """ + Tests that a node can be created with a public IPv6 backend address. + """ + with self.mock_post( + "nodebalancers/123456/configs/65432/nodes/54321" + ) as m: + config = NodeBalancerConfig(self.client, 65432, 123456) + node = config.node_create( + "node54321", + "[2001:db8:abcd:0012::1]:80", + weight=50, + mode="accept", + ) + + self.assertIsNotNone(node) + self.assertEqual( + m.call_url, "/nodebalancers/123456/configs/65432/nodes" + ) + self.assertEqual( + m.call_data["address"], "[2001:db8:abcd:0012::1]:80" + ) + def test_update_node(self): """ Tests that a node can be updated @@ -154,6 +177,18 @@ def test_delete_node(self): class NodeBalancerTest(ClientBaseCase): + def test_get(self): + """ + Tests that a NodeBalancer is loaded correctly by ID. + """ + nb = NodeBalancer(self.client, 123456) + self.assertEqual(nb._populated, False) + + self.assertEqual(nb.label, "balancer123456") + self.assertEqual(nb._populated, True) + self.assertEqual(nb.type, "premium") + self.assertEqual(nb.backend_connectivity, "ipv6") + def test_update(self): """ Test that you can update a NodeBalancer. @@ -193,6 +228,24 @@ def test_locks_not_in_put(self): self.assertNotIn("locks", m.call_data) self.assertEqual(m.call_data["label"], "new-label") + def test_type_and_backend_connectivity_not_in_put(self): + """ + Test that type and backend_connectivity are not included in PUT + requests. These fields cannot be changed after creation. + """ + nb = NodeBalancer(self.client, 123456) + self.assertEqual(nb.type, "premium") + self.assertEqual(nb.backend_connectivity, "ipv6") + + nb.label = "new-label" + + with self.mock_put("nodebalancers/123456") as m: + nb.save() + self.assertEqual(m.call_url, "/nodebalancers/123456") + self.assertNotIn("type", m.call_data) + self.assertNotIn("backend_connectivity", m.call_data) + self.assertEqual(m.call_data["label"], "new-label") + def test_firewalls(self): """ Test that you can get the firewalls for the requested NodeBalancer. @@ -251,6 +304,30 @@ def test_config_rebuild(self): }, ) + def test_config_rebuild_ipv6(self): + """ + Test that a config can be rebuilt with public IPv6 backend addresses. + """ + config_rebuild_url = "/nodebalancers/12345/configs/4567/rebuild" + with self.mock_post(config_rebuild_url) as m: + nb = NodeBalancer(self.client, 12345) + nodes = [ + { + "address": "[2001:db8:abcd:0012::1]:80", + "label": "node1", + "weight": 50, + "mode": "accept", + } + ] + + result = nb.config_rebuild(4567, nodes, port=80, protocol="http") + self.assertIsNotNone(result) + self.assertEqual(m.call_url, config_rebuild_url) + self.assertEqual( + m.call_data["nodes"][0]["address"], + "[2001:db8:abcd:0012::1]:80", + ) + def test_statistics(self): """ Test that you can get the statistics about the requested NodeBalancer.