From fb04f1b1e9a80e178824ddd00c4fe2bc35016ab5 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 2 Dec 2025 13:19:11 -0500 Subject: [PATCH 1/6] Support increased block storage volume limits --- linode_api4/objects/linode.py | 10 ++++++---- linode_api4/util.py | 26 ++++++++++++++++++++++++++ test/unit/util_test.py | 20 +++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index df2694f66..b82a9fb87 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -39,7 +39,7 @@ from linode_api4.objects.serializable import JSONObject, StrEnum from linode_api4.objects.vpc import VPC, VPCSubnet from linode_api4.paginated_list import PaginatedList -from linode_api4.util import drop_null_keys +from linode_api4.util import drop_null_keys, generate_device_suffixes PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation @@ -1258,9 +1258,11 @@ def config_create( from .volume import Volume # pylint: disable=import-outside-toplevel hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd" - device_names = [ - hypervisor_prefix + string.ascii_lowercase[i] for i in range(0, 8) - ] + + device_limit = int(max(8, min(self.specs.memory // 1024, 64))) + + device_names = [hypervisor_prefix + suffix for suffix in generate_device_suffixes(device_limit)] + device_map = { device_names[i]: None for i in range(0, len(device_names)) } diff --git a/linode_api4/util.py b/linode_api4/util.py index 1ddbcc25b..39b0f0369 100644 --- a/linode_api4/util.py +++ b/linode_api4/util.py @@ -3,6 +3,7 @@ """ from typing import Any, Dict +import string def drop_null_keys(data: Dict[Any, Any], recursive=True) -> Dict[Any, Any]: @@ -27,3 +28,28 @@ def recursive_helper(value: Any) -> Any: return value return recursive_helper(data) + + +def generate_device_suffixes(n): + """ + Generate n alphabetical suffixes starting with a, b, c, etc. + After z, continue with aa, ab, ac, etc. followed by aaa, aab, etc. + Example: + generate_device_suffixes(30) -> + ['a', 'b', 'c', ..., 'z', 'aa', 'ab', 'ac', 'ad'] + """ + letters = string.ascii_lowercase + result = [] + i = 0 + + while len(result) < n: + s = "" + x = i + while True: + s = letters[x % 26] + s + x = x // 26 - 1 + if x < 0: + break + result.append(s) + i += 1 + return result diff --git a/test/unit/util_test.py b/test/unit/util_test.py index 3123a4447..6696af5af 100644 --- a/test/unit/util_test.py +++ b/test/unit/util_test.py @@ -1,6 +1,6 @@ import unittest -from linode_api4.util import drop_null_keys +from linode_api4.util import drop_null_keys, generate_device_suffixes class UtilTest(unittest.TestCase): @@ -53,3 +53,21 @@ def test_drop_null_keys_recursive(self): } assert drop_null_keys(value) == expected_output + + def test_generate_device_suffixes(self): + """ + Tests whether generate_device_suffixes works as expected. + """ + + expected_output_12 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"] + assert generate_device_suffixes(12) == expected_output_12 + + expected_output_30 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad"] + assert generate_device_suffixes(30) == expected_output_30 + + expected_output_60 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", + "ai", "aj", "ak", "al", "am", "an", "ao", "ap", "aq", "ar", "as", "at", "au", "av", "aw", + "ax", "ay", "az", "ba", "bb", "bc", "bd", "be", "bf", "bg", "bh"] + assert generate_device_suffixes(60) == expected_output_60 From 6a2bba74e72225bba8bffcdde6d3caa52ed353dd Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 2 Dec 2025 13:40:45 -0500 Subject: [PATCH 2/6] Fixed lint --- linode_api4/objects/linode.py | 5 +- linode_api4/util.py | 2 +- test/unit/util_test.py | 115 +++++++++++++++++++++++++++++++--- 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index b82a9fb87..2c7df69b7 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1261,7 +1261,10 @@ def config_create( device_limit = int(max(8, min(self.specs.memory // 1024, 64))) - device_names = [hypervisor_prefix + suffix for suffix in generate_device_suffixes(device_limit)] + device_names = [ + hypervisor_prefix + suffix + for suffix in generate_device_suffixes(device_limit) + ] device_map = { device_names[i]: None for i in range(0, len(device_names)) diff --git a/linode_api4/util.py b/linode_api4/util.py index 39b0f0369..f22e7435e 100644 --- a/linode_api4/util.py +++ b/linode_api4/util.py @@ -2,8 +2,8 @@ Contains various utility functions. """ -from typing import Any, Dict import string +from typing import Any, Dict def drop_null_keys(data: Dict[Any, Any], recursive=True) -> Dict[Any, Any]: diff --git a/test/unit/util_test.py b/test/unit/util_test.py index 6696af5af..35adf38ff 100644 --- a/test/unit/util_test.py +++ b/test/unit/util_test.py @@ -59,15 +59,116 @@ def test_generate_device_suffixes(self): Tests whether generate_device_suffixes works as expected. """ - expected_output_12 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"] + expected_output_12 = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + ] assert generate_device_suffixes(12) == expected_output_12 - expected_output_30 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", - "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad"] + expected_output_30 = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "aa", + "ab", + "ac", + "ad", + ] assert generate_device_suffixes(30) == expected_output_30 - expected_output_60 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", - "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", - "ai", "aj", "ak", "al", "am", "an", "ao", "ap", "aq", "ar", "as", "at", "au", "av", "aw", - "ax", "ay", "az", "ba", "bb", "bc", "bd", "be", "bf", "bg", "bh"] + expected_output_60 = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "aa", + "ab", + "ac", + "ad", + "ae", + "af", + "ag", + "ah", + "ai", + "aj", + "ak", + "al", + "am", + "an", + "ao", + "ap", + "aq", + "ar", + "as", + "at", + "au", + "av", + "aw", + "ax", + "ay", + "az", + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "bg", + "bh", + ] assert generate_device_suffixes(60) == expected_output_60 From 0c9466bfb4ef7a4f7095e962ed2ab24fc0e815cc Mon Sep 17 00:00:00 2001 From: Erik Zilber Date: Wed, 3 Dec 2025 13:08:29 -0500 Subject: [PATCH 3/6] Address CoPilot suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- linode_api4/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linode_api4/util.py b/linode_api4/util.py index f22e7435e..f661367af 100644 --- a/linode_api4/util.py +++ b/linode_api4/util.py @@ -30,7 +30,7 @@ def recursive_helper(value: Any) -> Any: return recursive_helper(data) -def generate_device_suffixes(n): +def generate_device_suffixes(n: int) -> list[str]: """ Generate n alphabetical suffixes starting with a, b, c, etc. After z, continue with aa, ab, ac, etc. followed by aaa, aab, etc. From c956b1ca77d7a0e34409318f9c415ab648be33f6 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 3 Dec 2025 13:12:36 -0500 Subject: [PATCH 4/6] Address more Copilot suggestions --- linode_api4/objects/linode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 2c7df69b7..4ecd873ab 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -42,6 +42,9 @@ from linode_api4.util import drop_null_keys, generate_device_suffixes PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation +MIN_DEVICE_LIMIT = 8 +MEMORY_DIVISOR = 1024 +MAX_DEVICE_LIMIT = 64 class InstanceDiskEncryptionType(StrEnum): @@ -1259,7 +1262,7 @@ def config_create( hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd" - device_limit = int(max(8, min(self.specs.memory // 1024, 64))) + device_limit = int(max(MIN_DEVICE_LIMIT, min(self.specs.memory // MEMORY_DIVISOR, MAX_DEVICE_LIMIT))) device_names = [ hypervisor_prefix + suffix From d9a8998b74396c2cf32b5b7002e89c563bf5046d Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 3 Dec 2025 13:16:43 -0500 Subject: [PATCH 5/6] Fix lint --- linode_api4/objects/linode.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 4ecd873ab..26efe93e9 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1262,7 +1262,12 @@ def config_create( hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd" - device_limit = int(max(MIN_DEVICE_LIMIT, min(self.specs.memory // MEMORY_DIVISOR, MAX_DEVICE_LIMIT))) + device_limit = int( + max( + MIN_DEVICE_LIMIT, + min(self.specs.memory // MEMORY_DIVISOR, MAX_DEVICE_LIMIT), + ) + ) device_names = [ hypervisor_prefix + suffix From c2a7f6a6d31592d2b741af56796d39fa1195a08d Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 5 Dec 2025 11:45:23 -0500 Subject: [PATCH 6/6] Addressed PR comments --- linode_api4/objects/linode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 26efe93e9..ee9153375 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -43,7 +43,7 @@ PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation MIN_DEVICE_LIMIT = 8 -MEMORY_DIVISOR = 1024 +MB_PER_GB = 1024 MAX_DEVICE_LIMIT = 64 @@ -1265,7 +1265,7 @@ def config_create( device_limit = int( max( MIN_DEVICE_LIMIT, - min(self.specs.memory // MEMORY_DIVISOR, MAX_DEVICE_LIMIT), + min(self.specs.memory // MB_PER_GB, MAX_DEVICE_LIMIT), ) )