From 0442a8637e8d5a078a59e0e23b8d162e4aef7a0c Mon Sep 17 00:00:00 2001 From: rammanoj Date: Fri, 16 Jan 2026 14:37:27 -0500 Subject: [PATCH 01/13] Make NodePool optional for LKE-E in python sdk (#630) * make nodepools optional in cluster_create * Update linode_api4/groups/lke.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address feedback --------- Co-authored-by: Erik Zilber Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- linode_api4/groups/lke.py | 11 +++- .../linode_client/test_linode_client.py | 2 +- test/integration/models/lke/test_lke.py | 10 ++-- test/unit/groups/lke_test.py | 52 ++++++++++++++++++- test/unit/linode_client_test.py | 4 +- test/unit/objects/lke_test.py | 4 +- 6 files changed, 71 insertions(+), 12 deletions(-) diff --git a/linode_api4/groups/lke.py b/linode_api4/groups/lke.py index c3d6fdc5d..330c1d378 100644 --- a/linode_api4/groups/lke.py +++ b/linode_api4/groups/lke.py @@ -62,8 +62,8 @@ def cluster_create( self, region, label, - node_pools, kube_version, + node_pools: Optional[list] = None, control_plane: Union[ LKEClusterControlPlaneOptions, Dict[str, Any] ] = None, @@ -119,6 +119,15 @@ def cluster_create( :returns: The new LKE Cluster :rtype: LKECluster """ + if node_pools is None: + node_pools = [] + + if len(node_pools) == 0 and ( + tier is None or tier.lower() != "enterprise" + ): + raise ValueError( + "LKE standard clusters must have at least one node pool." + ) params = { "label": label, diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index eb1b06369..9935fc345 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -350,8 +350,8 @@ def test_fails_to_create_cluster_with_invalid_version(test_linode_client): cluster = client.lke.cluster_create( region, "example-cluster", - {"type": "g6-standard-1", "count": 3}, invalid_version, + {"type": "g6-standard-1", "count": 3}, ) except ApiError as e: assert "not valid" in str(e.json) diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index 71ebc1ff2..116665df6 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -38,7 +38,7 @@ def lke_cluster(test_linode_client): label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, node_pools ) yield cluster @@ -57,8 +57,8 @@ def lke_cluster_with_acl(test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, control_plane=LKEClusterControlPlaneOptions( acl=LKEClusterControlPlaneACLOptions( enabled=True, @@ -103,7 +103,7 @@ def lke_cluster_with_labels_and_taints(test_linode_client): label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, node_pools ) yield cluster @@ -124,8 +124,8 @@ def lke_cluster_with_apl(test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, control_plane=LKEClusterControlPlaneOptions( high_availability=True, ), @@ -160,8 +160,8 @@ def lke_cluster_enterprise(e2e_test_firewall, test_linode_client): cluster = test_linode_client.lke.cluster_create( region, label, - node_pools, version, + node_pools, tier="enterprise", ) diff --git a/test/unit/groups/lke_test.py b/test/unit/groups/lke_test.py index a39db81a6..802960192 100644 --- a/test/unit/groups/lke_test.py +++ b/test/unit/groups/lke_test.py @@ -21,8 +21,8 @@ def test_cluster_create_with_acl(self): self.client.lke.cluster_create( "us-mia", "test-acl-cluster", - [self.client.lke.node_pool("g6-nanode-1", 3)], "1.29", + [self.client.lke.node_pool("g6-nanode-1", 3)], control_plane=LKEClusterControlPlaneOptions( acl=LKEClusterControlPlaneACLOptions( enabled=True, @@ -41,3 +41,53 @@ def test_cluster_create_with_acl(self): assert m.call_data["control_plane"]["acl"]["addresses"]["ipv6"] == [ "1234::5678" ] + + def test_cluster_create_enterprise_without_node_pools(self): + """ + Tests that an enterprise LKE cluster can be created without node pools. + """ + with self.mock_post("lke/clusters") as m: + self.client.lke.cluster_create( + "us-west", + "test-enterprise-cluster", + "1.29", + tier="enterprise", + ) + + assert m.call_data["region"] == "us-west" + assert m.call_data["label"] == "test-enterprise-cluster" + assert m.call_data["k8s_version"] == "1.29" + assert m.call_data["tier"] == "enterprise" + assert m.call_data["node_pools"] == [] + + def test_cluster_create_enterprise_case_insensitive(self): + """ + Tests that tier comparison is case-insensitive for enterprise tier. + """ + with self.mock_post("lke/clusters") as m: + self.client.lke.cluster_create( + "us-west", + "test-enterprise-cluster", + "1.29", + tier="ENTERPRISE", + ) + + assert m.call_data["tier"] == "ENTERPRISE" + assert m.call_data["node_pools"] == [] + + def test_cluster_create_standard_without_node_pools_raises_error(self): + """ + Tests that creating a standard LKE cluster without node pools raises ValueError. + """ + with self.assertRaises(ValueError) as context: + self.client.lke.cluster_create( + "us-east", + "test-standard-cluster", + "1.29", + tier="standard", + ) + + self.assertIn( + "LKE standard clusters must have at least one node pool", + str(context.exception), + ) diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index d87e08894..e82f3562d 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -817,7 +817,7 @@ def test_cluster_create_with_api_objects(self): node_pools = self.client.lke.node_pool(node_type, 3) with self.mock_post("lke/clusters") as m: cluster = self.client.lke.cluster_create( - region, "example-cluster", node_pools, version + region, "example-cluster", version, node_pools ) self.assertEqual(m.call_data["region"], "ap-west") self.assertEqual( @@ -850,8 +850,8 @@ def test_cluster_create_with_string_repr(self): cluster = self.client.lke.cluster_create( "ap-west", "example-cluster", - {"type": "g6-standard-1", "count": 3}, "1.19", + {"type": "g6-standard-1", "count": 3}, ) self.assertEqual(m.call_data["region"], "ap-west") self.assertEqual( diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index 10284a0c9..91f9ed3fe 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -302,6 +302,7 @@ def test_cluster_create_with_labels_and_taints(self): self.client.lke.cluster_create( "us-mia", "test-acl-cluster", + "1.29", [ self.client.lke.node_pool( "g6-nanode-1", @@ -317,7 +318,6 @@ def test_cluster_create_with_labels_and_taints(self): ], ) ], - "1.29", ) assert m.call_data["node_pools"][0] == { @@ -339,13 +339,13 @@ def test_cluster_create_with_apl(self): cluster = self.client.lke.cluster_create( "us-mia", "test-aapl-cluster", + "1.29", [ self.client.lke.node_pool( "g6-dedicated-4", 3, ) ], - "1.29", apl_enabled=True, control_plane=LKEClusterControlPlaneOptions( high_availability=True, From 7708f871b5b61edf7106e7c51a554eb1adf99abc Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:09:38 -0500 Subject: [PATCH 02/13] Remove non-existent doc links from AI imaginations. (#631) --- linode_api4/objects/lock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/linode_api4/objects/lock.py b/linode_api4/objects/lock.py index b6552da7b..9cee64517 100644 --- a/linode_api4/objects/lock.py +++ b/linode_api4/objects/lock.py @@ -10,7 +10,7 @@ class LockType(StrEnum): """ LockType defines valid values for resource lock types. - API Documentation: https://techdocs.akamai.com/linode-api/reference/post-lock + API Documentation: TBD """ cannot_delete = "cannot_delete" @@ -22,7 +22,7 @@ class LockEntity(JSONObject): """ Represents the entity that is locked. - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lock + API Documentation: TBD """ id: int = 0 @@ -35,7 +35,7 @@ class Lock(Base): """ A resource lock that prevents deletion or modification of a resource. - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lock + API Documentation: TBD """ api_endpoint = "/locks/{id}" From 365c7d5380e6f24a04b9c58fdfe19153151433e6 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Thu, 5 Feb 2026 09:47:45 +0100 Subject: [PATCH 03/13] Regression fixes (#636) * Fix of: test_cluster_create_with_api_objects * Revert changes in lke.py, pass [node_pools] to cluster_create in int test instead * Rename node_pools to node_pool as it is single element --- test/integration/linode_client/test_linode_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 9935fc345..4060064d3 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -328,10 +328,10 @@ def test_cluster_create_with_api_objects(test_linode_client): node_type = client.linode.types()[1] # g6-standard-1 version = client.lke.versions()[0] region = get_region(client, {"Kubernetes"}) - node_pools = client.lke.node_pool(node_type, 3) + node_pool = client.lke.node_pool(node_type, 3) label = get_test_label() - cluster = client.lke.cluster_create(region, label, node_pools, version) + cluster = client.lke.cluster_create(region, label, version, [node_pool]) assert cluster.region.id == region.id assert cluster.k8s_version.id == version.id From ce2a79ff7ebfe11aee65be792295776d186a2ef3 Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Tue, 10 Feb 2026 09:13:45 -0500 Subject: [PATCH 04/13] Removed v4beta notices from Maintenance Policy (#643) * Removed v4beta notices from Maintenance Policy fields/methods * Fix lint --- linode_api4/groups/linode.py | 2 -- linode_api4/groups/maintenance.py | 2 -- linode_api4/objects/account.py | 6 ++---- linode_api4/objects/linode.py | 4 +--- test/integration/models/linode/test_linode.py | 3 +-- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index f88808e64..e32a284f1 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -335,8 +335,6 @@ def instance_create( :type network_helper: bool :param maintenance_policy: The slug of the maintenance policy to apply during maintenance. If not provided, the default policy (linode/migrate) will be applied. - NOTE: This field is in beta and may only - function if base_url is set to `https://api.linode.com/v4beta`. :type maintenance_policy: str :returns: A new Instance object, or a tuple containing the new Instance and diff --git a/linode_api4/groups/maintenance.py b/linode_api4/groups/maintenance.py index 7d56cec6e..63cb424df 100644 --- a/linode_api4/groups/maintenance.py +++ b/linode_api4/groups/maintenance.py @@ -9,8 +9,6 @@ class MaintenanceGroup(Group): def maintenance_policies(self): """ - .. note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`. - Returns a collection of MaintenancePolicy objects representing available maintenance policies that can be applied to Linodes diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index 54298ed11..a4aca1848 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -218,9 +218,7 @@ class AccountSettings(Base): "object_storage": Property(), "backups_enabled": Property(mutable=True), "interfaces_for_new_linodes": Property(mutable=True), - "maintenance_policy": Property( - mutable=True - ), # Note: This field is only available when using v4beta. + "maintenance_policy": Property(mutable=True), } @@ -249,7 +247,7 @@ class Event(Base): "duration": Property(), "secondary_entity": Property(), "message": Property(), - "maintenance_policy_set": Property(), # Note: This field is only available when using v4beta. + "maintenance_policy_set": Property(), "description": Property(), "source": Property(), "not_before": Property(is_datetime=True), diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index fae0926d5..1edf4e014 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -800,9 +800,7 @@ class Instance(Base): "lke_cluster_id": Property(), "capabilities": Property(unordered=True), "interface_generation": Property(), - "maintenance_policy": Property( - mutable=True - ), # Note: This field is only available when using v4beta. + "maintenance_policy": Property(mutable=True), "locks": Property(unordered=True), } diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index c485dd19c..574d5d9d2 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -1101,8 +1101,7 @@ def test_delete_interface_containing_vpc( def test_create_linode_with_maintenance_policy(test_linode_client): client = test_linode_client - # TODO: Replace with random region after GA - region = "ap-south" + region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() policies = client.maintenance.maintenance_policies() From f67187addcb32f24a48da85283b7d545b8ea023d Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:43:24 -0500 Subject: [PATCH 05/13] Remove preview section from PR template (#638) Removed the preview section from the pull request template. --- .github/pull_request_template.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5bea77b2c..d97f93452 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,3 @@ **What are the steps to reproduce the issue or verify the changes?** **How do I run the relevant unit/integration tests?** - -## 📷 Preview - -**If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** \ No newline at end of file From f30ac54ad21217e0022d659c44ec088c84548b79 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:44:18 -0500 Subject: [PATCH 06/13] Add resource lock support for NodeBalancer (#637) --- linode_api4/objects/nodebalancer.py | 1 + test/fixtures/nodebalancers.json | 6 ++++-- test/fixtures/nodebalancers_123456.json | 3 +++ test/unit/objects/nodebalancers_test.py | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index f02dda269..cb6e566f7 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -252,6 +252,7 @@ class NodeBalancer(Base): "transfer": Property(), "tags": Property(mutable=True, unordered=True), "client_udp_sess_throttle": Property(mutable=True), + "locks": Property(unordered=True), } # create derived objects diff --git a/test/fixtures/nodebalancers.json b/test/fixtures/nodebalancers.json index 85eec186b..9b4dc8dae 100644 --- a/test/fixtures/nodebalancers.json +++ b/test/fixtures/nodebalancers.json @@ -10,7 +10,8 @@ "updated": "2018-01-01T00:01:01", "label": "balancer123456", "client_conn_throttle": 0, - "tags": ["something"] + "tags": ["something"], + "locks": ["cannot_delete_with_subresources"] }, { "created": "2018-01-01T00:01:01", @@ -22,7 +23,8 @@ "updated": "2018-01-01T00:01:01", "label": "balancer123457", "client_conn_throttle": 0, - "tags": [] + "tags": [], + "locks": [] } ], "results": 2, diff --git a/test/fixtures/nodebalancers_123456.json b/test/fixtures/nodebalancers_123456.json index e965d4379..a78c8d3e3 100644 --- a/test/fixtures/nodebalancers_123456.json +++ b/test/fixtures/nodebalancers_123456.json @@ -10,5 +10,8 @@ "client_conn_throttle": 0, "tags": [ "something" + ], + "locks": [ + "cannot_delete_with_subresources" ] } \ No newline at end of file diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index ed0f0c320..c02b40ea3 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -175,6 +175,24 @@ def test_update(self): }, ) + def test_locks_not_in_put(self): + """ + Test that locks are not included in PUT request when updating a NodeBalancer. + Locks are managed through the separate /v4/locks endpoint. + """ + nb = NodeBalancer(self.client, 123456) + # Access locks to ensure it's loaded + self.assertEqual(nb.locks, ["cannot_delete_with_subresources"]) + + nb.label = "new-label" + + with self.mock_put("nodebalancers/123456") as m: + nb.save() + self.assertEqual(m.call_url, "/nodebalancers/123456") + # Verify locks is NOT in the PUT data + self.assertNotIn("locks", 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. From 8b8c61985730e08581cc736d7102ee41b98b9a33 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Wed, 11 Feb 2026 14:15:25 +0100 Subject: [PATCH 07/13] Align GHA workflows in the scope of report uploads (#642) * Add logic to upload test results for manual runs on demand only * Modify test_report_upload type to boolean * Set test_upload_report to choice type in e2e-test.yml --- .github/workflows/e2e-test-pr.yml | 10 +++++++++- .github/workflows/e2e-test.yml | 13 +++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index 86809d177..f765b0a0d 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -27,6 +27,14 @@ on: pull_request_number: description: 'The number of the PR.' required: false + test_report_upload: + description: 'Indicates whether to upload the test report to object storage. Defaults to "false"' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' name: PR E2E Tests @@ -101,7 +109,7 @@ jobs: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - name: Upload test results - if: always() + if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) run: | filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/add_gha_info_to_xml.py \ diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index e2762ff95..5c24361d0 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -41,6 +41,14 @@ on: options: - 'true' - 'false' + test_report_upload: + description: 'Indicates whether to upload the test report to object storage. Defaults to "false"' + type: choice + required: false + default: 'false' + options: + - 'true' + - 'false' push: branches: - main @@ -172,7 +180,8 @@ jobs: process-upload-report: runs-on: ubuntu-latest needs: [integration-tests] - if: always() && github.repository == 'linode/linode_api4-python' # Run even if integration tests fail and only on main repository + # Run even if integration tests fail on main repository AND push event OR test_report_upload is true in case of manual run + if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) outputs: summary: ${{ steps.set-test-summary.outputs.summary }} @@ -271,4 +280,4 @@ jobs: payload: | channel: ${{ secrets.SLACK_CHANNEL_ID }} thread_ts: "${{ steps.main_message.outputs.ts }}" - text: "${{ needs.process-upload-report.outputs.summary }}" + text: "${{ needs.process-upload-report.outputs.summary }}" \ No newline at end of file From 6dc0564a4a0f3b83bf70cdb6c468d6b7725a9e35 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Thu, 12 Feb 2026 08:38:42 +0100 Subject: [PATCH 08/13] Fix test_lke_cluster_model_filter. Modify lke_cluster_instance fixture (#641) --- test/integration/filters/fixtures.py | 4 ++-- test/integration/filters/model_filters_test.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/integration/filters/fixtures.py b/test/integration/filters/fixtures.py index 31b7edcbf..e753236dd 100644 --- a/test/integration/filters/fixtures.py +++ b/test/integration/filters/fixtures.py @@ -25,11 +25,11 @@ def lke_cluster_instance(test_linode_client): region = get_region(test_linode_client, {"Kubernetes", "Disk Encryption"}) - node_pools = test_linode_client.lke.node_pool(node_type, 3) + node_pool = test_linode_client.lke.node_pool(node_type, 3) label = get_test_label() + "_cluster" cluster = test_linode_client.lke.cluster_create( - region, label, node_pools, version + region, label, version, [node_pool] ) yield cluster diff --git a/test/integration/filters/model_filters_test.py b/test/integration/filters/model_filters_test.py index 22bb8299e..55bed6ac3 100644 --- a/test/integration/filters/model_filters_test.py +++ b/test/integration/filters/model_filters_test.py @@ -63,12 +63,13 @@ def test_linode_type_model_filter(test_linode_client): def test_lke_cluster_model_filter(test_linode_client, lke_cluster_instance): client = test_linode_client + lke_cluster = lke_cluster_instance filtered_cluster = client.lke.clusters( - LKECluster.label.contains(lke_cluster_instance.label) + LKECluster.label.contains(lke_cluster.label) ) - assert filtered_cluster[0].id == lke_cluster_instance.id + assert filtered_cluster[0].id == lke_cluster.id def test_networking_firewall_model_filter( From 4003f2af6ff0eeb608aacc39e57f68f3de7a95a2 Mon Sep 17 00:00:00 2001 From: Michal Wojcik <32574975+mgwoj@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:23:16 +0100 Subject: [PATCH 09/13] Add region capability enum (#644) * python-sdk: Add region capability enum * python-sdk: Add region capability enum * Update linode_api4/objects/region.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- linode_api4/objects/__init__.py | 2 +- linode_api4/objects/region.py | 62 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/linode_api4/objects/__init__.py b/linode_api4/objects/__init__.py index 009e9436e..89a681635 100644 --- a/linode_api4/objects/__init__.py +++ b/linode_api4/objects/__init__.py @@ -3,7 +3,7 @@ from .dbase import DerivedBase from .serializable import JSONObject from .filtering import and_, or_ -from .region import Region +from .region import Region, Capability from .image import Image from .linode import * from .linode_interfaces import * diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index c9dc05099..3c8986259 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -3,6 +3,68 @@ from linode_api4.errors import UnexpectedResponseError from linode_api4.objects.base import Base, JSONObject, Property +from linode_api4.objects.serializable import StrEnum + + +class Capability(StrEnum): + """ + An enum class that represents the capabilities that Linode offers + across different regions and services. + + These capabilities indicate what services are available in each data center. + """ + + linodes = "Linodes" + nodebalancers = "NodeBalancers" + block_storage = "Block Storage" + object_storage = "Object Storage" + object_storage_regions = "Object Storage Access Key Regions" + object_storage_endpoint_types = "Object Storage Endpoint Types" + lke = "Kubernetes" + lke_ha_controlplanes = "LKE HA Control Planes" + lke_e = "Kubernetes Enterprise" + firewall = "Cloud Firewall" + gpu = "GPU Linodes" + vlans = "Vlans" + vpcs = "VPCs" + vpcs_extra = "VPCs Extra" + machine_images = "Machine Images" + dbaas = "Managed Databases" + dbaas_beta = "Managed Databases Beta" + bs_migrations = "Block Storage Migrations" + metadata = "Metadata" + premium_plans = "Premium Plans" + edge_plans = "Edge Plans" + distributed_plans = "Distributed Plans" + lke_control_plane_acl = "LKE Network Access Control List (IP ACL)" + aclb = "Akamai Cloud Load Balancer" + support_ticket_severity = "Support Ticket Severity" + backups = "Backups" + placement_group = "Placement Group" + disk_encryption = "Disk Encryption" + la_disk_encryption = "LA Disk Encryption" + akamai_ram_protection = "Akamai RAM Protection" + blockstorage_encryption = "Block Storage Encryption" + blockstorage_perf_b1 = "Block Storage Performance B1" + blockstorage_perf_b1_default = "Block Storage Performance B1 Default" + aclp = "Akamai Cloud Pulse" + aclp_logs = "Akamai Cloud Pulse Logs" + aclp_logs_lkee = "Akamai Cloud Pulse Logs LKE-E Audit" + aclp_logs_dc_lkee = "ACLP Logs Datacenter LKE-E" + smtp_enabled = "SMTP Enabled" + stackscripts = "StackScripts" + vpu = "NETINT Quadra T1U" + linode_interfaces = "Linode Interfaces" + maintenance_policy = "Maintenance Policy" + vpc_dual_stack = "VPC Dual Stack" + vpc_ipv6_stack = "VPC IPv6 Stack" + nlb = "Network LoadBalancer" + natgateway = "NAT Gateway" + lke_e_byovpc = "Kubernetes Enterprise BYO VPC" + lke_e_stacktype = "Kubernetes Enterprise Dual Stack" + ruleset = "Cloud Firewall Rule Set" + prefixlists = "Cloud Firewall Prefix Lists" + current_prefixlists = "Cloud Firewall Prefix List Current References" @dataclass From 5302e710789c259b9b2f6054d47579bf79ff0e17 Mon Sep 17 00:00:00 2001 From: Maciej Wilk Date: Thu, 26 Feb 2026 08:46:29 +0100 Subject: [PATCH 10/13] Regression fixes (#647) * Add waits for Linodes to improve flaky tests * Increase timeout for Linode's 'offline' status * Revert import changes in test_account.py --- test/integration/models/account/test_account.py | 17 +++++++++++++---- test/integration/models/domain/test_domain.py | 4 ++-- test/integration/models/linode/test_linode.py | 6 ++++++ .../models/networking/test_networking.py | 9 ++++++--- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/test/integration/models/account/test_account.py b/test/integration/models/account/test_account.py index 5833a9344..4c4dcc134 100644 --- a/test/integration/models/account/test_account.py +++ b/test/integration/models/account/test_account.py @@ -1,7 +1,11 @@ import time from datetime import datetime from test.integration.conftest import get_region -from test.integration.helpers import get_test_label, retry_sending_request +from test.integration.helpers import ( + get_test_label, + retry_sending_request, + wait_for_condition, +) import pytest @@ -102,13 +106,18 @@ def test_latest_get_event(test_linode_client, e2e_test_firewall): firewall=e2e_test_firewall, ) - events = client.load(Event, "") + def get_linode_status(): + return linode.status == "running" - latest_events = events._raw_json.get("data") + # To ensure the Linode is running and the 'event' key has been populated + wait_for_condition(3, 100, get_linode_status) + + events = client.load(Event, "") + latest_events = events._raw_json.get("data")[:15] linode.delete() - for event in latest_events[:15]: + for event in latest_events: if label == event["entity"]["label"]: break else: diff --git a/test/integration/models/domain/test_domain.py b/test/integration/models/domain/test_domain.py index 9dc180a6e..d7956d421 100644 --- a/test/integration/models/domain/test_domain.py +++ b/test/integration/models/domain/test_domain.py @@ -43,10 +43,10 @@ def test_clone(test_linode_client, test_domain): dom = "example.clone-" + timestamp + "-inttestsdk.org" domain.clone(dom) - ds = test_linode_client.domains() - time.sleep(1) + ds = test_linode_client.domains() + domains = [i.domain for i in ds] assert dom in domains diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index 574d5d9d2..9f6194fa9 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -611,6 +611,12 @@ def test_linode_initate_migration(test_linode_client, e2e_test_firewall): migration_type=MigrationType.COLD, ) + def get_linode_status(): + return linode.status == "offline" + + # To verify that Linode's status changed before deletion (during migration status is set to 'offline') + wait_for_condition(5, 120, get_linode_status) + res = linode.delete() assert res diff --git a/test/integration/models/networking/test_networking.py b/test/integration/models/networking/test_networking.py index 0edd5bd0a..27ffbb444 100644 --- a/test/integration/models/networking/test_networking.py +++ b/test/integration/models/networking/test_networking.py @@ -32,7 +32,7 @@ ) -def create_linode(test_linode_client): +def create_linode_func(test_linode_client): client = test_linode_client label = get_test_label() @@ -49,7 +49,7 @@ def create_linode(test_linode_client): @pytest.fixture def create_linode_for_ip_share(test_linode_client): - linode = create_linode(test_linode_client) + linode = create_linode_func(test_linode_client) yield linode @@ -58,7 +58,7 @@ def create_linode_for_ip_share(test_linode_client): @pytest.fixture def create_linode_to_be_shared_with_ips(test_linode_client): - linode = create_linode(test_linode_client) + linode = create_linode_func(test_linode_client) yield linode @@ -302,6 +302,8 @@ def test_create_and_delete_vlan(test_linode_client, linode_for_vlan_tests): wait_for_condition(3, 100, get_status, linode, "rebooting") assert linode.status == "rebooting" + wait_for_condition(3, 100, get_status, linode, "running") + # Delete the VLAN is_deleted = test_linode_client.networking.delete_vlan( vlan_label, linode.region @@ -334,6 +336,7 @@ def test_get_global_firewall_settings(test_linode_client): def test_ip_info(test_linode_client, create_linode): linode = create_linode + wait_for_condition(3, 100, get_status, linode, "running") ip_info = test_linode_client.load(IPAddress, linode.ipv4[0]) From 1fd5851129954a5a9e7d11cf771b5e97f23819c7 Mon Sep 17 00:00:00 2001 From: Michal Wojcik <32574975+mgwoj@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:29:44 +0100 Subject: [PATCH 11/13] Deprecate `xen` from SDKs and Tools (#648) * Deprecate `xen` from SDKs and Tools --- linode_api4/objects/linode.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 1edf4e014..ccddd7e40 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1,6 +1,7 @@ import copy import string import sys +import warnings from dataclasses import dataclass, field from datetime import datetime from enum import Enum @@ -217,7 +218,7 @@ def resize(self, new_size): class Kernel(Base): """ The primary component of every Linux system. The kernel interfaces - with the system’s hardware and it controls the operating system’s core functionality. + with the system’s hardware, and it controls the operating system’s core functionality. Your Compute Instance is capable of running one of three kinds of kernels: @@ -237,6 +238,10 @@ class Kernel(Base): to compile the kernel from source than to download it from your package manager. For more information on custom compiled kernels, review our guides for Debian, Ubuntu, and CentOS. + .. note:: + The ``xen`` property is deprecated and is no longer returned by the API. + It is maintained for backward compatibility only. + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-kernel """ @@ -256,6 +261,16 @@ class Kernel(Base): "pvops": Property(), } + def __getattribute__(self, name: str) -> object: + if name == "xen": + warnings.warn( + "The 'xen' property of Kernel is deprecated and is no longer " + "returned by the API. It is maintained for backward compatibility only.", + DeprecationWarning, + stacklevel=2, + ) + return super().__getattribute__(name) + class Type(Base): """ From 90c287ae35505ec0c1a766de5111726ed8149def Mon Sep 17 00:00:00 2001 From: srbhaakamai Date: Thu, 26 Feb 2026 21:30:09 +0530 Subject: [PATCH 12/13] ACLP Alerting get_channles() API changes and Added Enum for 'Status' (#645) * Resolving requirement.txt conflicts * Added test with new Status and Alert Definition change * Revert "Resolving requirement.txt conflicts" This reverts commit d40b96c15ff00b89b29c941fae4d6379ed16f396. * introduced enums for alert status * Update linode_api4/objects/monitor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixed reviewer comments * fixed lint error * fix lint errors from make lint command * changed wait code to wait until its enabled --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- linode_api4/objects/monitor.py | 50 ++++++++++++++++++- .../models/monitor/test_monitor.py | 4 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 4315e4c2e..ca8f83921 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -116,6 +116,19 @@ class DashboardType(StrEnum): custom = "custom" +class AlertStatus(StrEnum): + """ + Enum for supported alert status values. + """ + + AlertDefinitionStatusProvisioning = "provisioning" + AlertDefinitionStatusEnabling = "enabling" + AlertDefinitionStatusDisabling = "disabling" + AlertDefinitionStatusEnabled = "enabled" + AlertDefinitionStatusDisabled = "disabled" + AlertDefinitionStatusFailed = "failed" + + @dataclass class Filter(JSONObject): """ @@ -428,6 +441,40 @@ class ChannelContent(JSONObject): # Other channel types like 'webhook', 'slack' could be added here as Optional fields. +@dataclass +class EmailDetails(JSONObject): + """ + Represents email-specific details for an alert channel. + """ + + usernames: Optional[List[str]] = None + recipient_type: Optional[str] = None + + +@dataclass +class ChannelDetails(JSONObject): + """ + Represents the details block for an AlertChannel, which varies by channel type. + """ + + email: Optional[EmailDetails] = None + + +@dataclass +class AlertInfo(JSONObject): + """ + Represents a reference to alerts associated with an alert channel. + Fields: + - url: str - API URL to fetch the alerts for this channel + - type: str - Type identifier (e.g., 'alerts-definitions') + - alert_count: int - Number of alerts associated with this channel + """ + + url: str = "" + _type: str = field(default="", metadata={"json_key": "type"}) + alert_count: int = 0 + + class AlertChannel(Base): """ Represents an alert channel used to deliver notifications when alerts @@ -450,7 +497,8 @@ class AlertChannel(Base): "label": Property(), "type": Property(), "channel_type": Property(), - "alerts": Property(mutable=False, json_object=Alerts), + "details": Property(mutable=False, json_object=ChannelDetails), + "alerts": Property(mutable=False, json_object=AlertInfo), "content": Property(mutable=False, json_object=ChannelContent), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index b6cf40b54..908ac1a44 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -16,6 +16,7 @@ MonitorService, MonitorServiceToken, ) +from linode_api4.objects.monitor import AlertStatus # List all dashboards @@ -227,7 +228,8 @@ def wait_for_alert_ready(alert_id, service_type: str): interval = initial_timeout alert = client.load(AlertDefinition, alert_id, service_type) while ( - getattr(alert, "status", None) == "in progress" + getattr(alert, "status", None) + != AlertStatus.AlertDefinitionStatusEnabled and (time.time() - start) < timeout ): time.sleep(interval) From 42ca199fe0200026fe0102c93e58dabbfb7118be Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:44:58 -0500 Subject: [PATCH 13/13] Update UDP Session Timeout Value in Test (#653) --- test/integration/models/nodebalancer/test_nodebalancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 9e7537897..692efb027 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -130,7 +130,7 @@ def test_get_nb_config_with_udp(test_linode_client, create_nb_config_with_udp): assert "udp" == config.protocol assert 1234 == config.udp_check_port - assert 16 == config.udp_session_timeout + assert 2 == config.udp_session_timeout def test_update_nb_config(test_linode_client, create_nb_config_with_udp):