From 448aa4cf3fdcab797041850967506f60e1b388ee Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:22:46 -0400 Subject: [PATCH 01/19] Remove deprecated `pkg_resources` and drop Python 3.7 support (#337) --- .github/workflows/main.yml | 2 +- linode_api4/linode_client.py | 4 ++-- pyproject.toml | 2 +- setup.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ea73fb40..0813c2581 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7','3.8','3.9','3.10','3.11'] + python-version: ['3.8','3.9','3.10','3.11'] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py index 66c53e336..3a6f2b8b0 100644 --- a/linode_api4/linode_client.py +++ b/linode_api4/linode_client.py @@ -2,10 +2,10 @@ import json import logging +from importlib.metadata import version from typing import BinaryIO, Tuple from urllib import parse -import pkg_resources import requests from requests.adapters import HTTPAdapter, Retry @@ -36,7 +36,7 @@ from .paginated_list import PaginatedList from .util import drop_null_keys -package_version = pkg_resources.require("linode_api4")[0].version +package_version = version("linode_api4") logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index ccb0ec5f1..dc391f2ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ line_length = 80 [tool.black] line-length = 80 -target-version = ["py37", "py38", "py39", "py310", "py311"] +target-version = ["py38", "py39", "py310", "py311", "py312"] [tool.autoflake] expand-star-imports = true diff --git a/setup.py b/setup.py index 9139ee5af..8c73e3384 100755 --- a/setup.py +++ b/setup.py @@ -103,11 +103,11 @@ def bake_version(v): # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], # What does your project relate to? @@ -118,7 +118,7 @@ def bake_version(v): packages=find_packages(exclude=['contrib', 'docs', 'test', 'test.*']), # What do we need for this to run - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ "requests", From e2edd7132238cd35ff1e5d5919bc62488ecf0e53 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:25:40 -0400 Subject: [PATCH 02/19] Consistent Python binary in makefile (#338) --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 9ed3e4eb8..7482f9073 100644 --- a/Makefile +++ b/Makefile @@ -25,47 +25,47 @@ build: clean @PHONEY: release release: build - twine upload dist/* + $(PYTHON) -m twine upload dist/* @PHONEY: install install: clean requirements - python3 -m pip install . + $(PYTHON) -m pip install . @PHONEY: requirements requirements: - pip install -r requirements.txt -r requirements-dev.txt + $(PYTHON) -m pip install -r requirements.txt -r requirements-dev.txt @PHONEY: black black: - black linode_api4 test + $(PYTHON) -m black linode_api4 test @PHONEY: isort isort: - isort linode_api4 test + $(PYTHON) -m isort linode_api4 test @PHONEY: autoflake autoflake: - autoflake linode_api4 test + $(PYTHON) -m autoflake linode_api4 test @PHONEY: format format: black isort autoflake @PHONEY: lint lint: build - isort --check-only linode_api4 test - autoflake --check linode_api4 test - black --check --verbose linode_api4 test - pylint linode_api4 - twine check dist/* + $(PYTHON) -m isort --check-only linode_api4 test + $(PYTHON) -m autoflake --check linode_api4 test + $(PYTHON) -m black --check --verbose linode_api4 test + $(PYTHON) -m pylint linode_api4 + $(PYTHON) -m twine check dist/* @PHONEY: testint testint: - python3 -m pytest test/integration/${INTEGRATION_TEST_PATH}${MODEL_COMMAND} ${TEST_CASE_COMMAND} + $(PYTHON) -m pytest test/integration/${INTEGRATION_TEST_PATH}${MODEL_COMMAND} ${TEST_CASE_COMMAND} @PHONEY: testunit testunit: - python3 -m python test/unit + $(PYTHON) -m pytest test/unit @PHONEY: smoketest smoketest: - pytest -m smoke test/integration --disable-warnings \ No newline at end of file + $(PYTHON) -m pytest -m smoke test/integration --disable-warnings \ No newline at end of file From e6e0d454b0966fd912ade2ae11f9b737e09f0b61 Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:43:23 -0400 Subject: [PATCH 03/19] test: Integration tests for ip share (#339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 Description Make the tests case for `ip_addresses_share` to be integration test instead of using mocked data. ## ✔️ How to Test `pytest test/integration/models/test_networking.py` --- test/fixtures/networking_ips_share.json | 1 - test/integration/helpers.py | 6 ++ test/integration/models/test_networking.py | 106 +++++++++++++++------ 3 files changed, 81 insertions(+), 32 deletions(-) delete mode 100644 test/fixtures/networking_ips_share.json diff --git a/test/fixtures/networking_ips_share.json b/test/fixtures/networking_ips_share.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/test/fixtures/networking_ips_share.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/integration/helpers.py b/test/integration/helpers.py index eee46f385..2ea66464b 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -13,6 +13,12 @@ def get_test_label(): return label +def get_rand_nanosec_test_label(): + unique_timestamp = str(time.time_ns()) + label = "IntTestSDK_" + unique_timestamp + return label + + def delete_instance_with_test_kw(paginated_list: PaginatedList): for i in paginated_list: try: diff --git a/test/integration/models/test_networking.py b/test/integration/models/test_networking.py index d3b01e0bd..970ee6880 100644 --- a/test/integration/models/test_networking.py +++ b/test/integration/models/test_networking.py @@ -1,6 +1,8 @@ +from test.integration.helpers import get_rand_nanosec_test_label + import pytest -from linode_api4.objects import Firewall, IPAddress, IPv6Pool, IPv6Range +from linode_api4.objects import Firewall @pytest.mark.smoke @@ -15,37 +17,79 @@ def test_get_networking_rules(get_client, create_firewall): assert "outbound_policy" in str(rules) +def create_linode(get_client): + client = get_client + available_regions = client.regions() + chosen_region = available_regions[0] + label = get_rand_nanosec_test_label() + + linode_instance, password = client.linode.instance_create( + "g6-nanode-1", + chosen_region, + image="linode/debian12", + label=label, + ) + + return linode_instance + + +@pytest.fixture +def create_linode_for_ip_share(get_client): + linode = create_linode(get_client) + + yield linode + + linode.delete() + + +@pytest.fixture +def create_linode_to_be_shared_with_ips(get_client): + linode = create_linode(get_client) + + yield linode + + linode.delete() + + @pytest.mark.smoke -def test_ip_addresses_share(self): +def test_ip_addresses_share( + get_client, create_linode_for_ip_share, create_linode_to_be_shared_with_ips +): """ Test that you can share IP addresses with Linode. """ - ip_share_url = "/networking/ips/share" - ips = ["127.0.0.1"] - linode_id = 12345 - with self.mock_post(ip_share_url) as m: - result = self.client.networking.ip_addresses_share(ips, linode_id) - - self.assertIsNotNone(result) - self.assertEqual(m.call_url, ip_share_url) - self.assertEqual( - m.call_data, - { - "ips": ips, - "linode": linode_id, - }, - ) - - # Test that entering an empty IP array is allowed. - with self.mock_post(ip_share_url) as m: - result = self.client.networking.ip_addresses_share([], linode_id) - - self.assertIsNotNone(result) - self.assertEqual(m.call_url, ip_share_url) - self.assertEqual( - m.call_data, - { - "ips": [], - "linode": linode_id, - }, - ) + + # create two linode instances and share the ip of instance1 with instance2 + linode_instance1 = create_linode_for_ip_share + linode_instance2 = create_linode_to_be_shared_with_ips + + get_client.networking.ip_addresses_share( + [linode_instance1.ips.ipv4.public[0]], linode_instance2.id + ) + + assert ( + linode_instance1.ips.ipv4.public[0].address + == linode_instance2.ips.ipv4.shared[0].address + ) + + +@pytest.mark.smoke +def test_ip_addresses_unshare( + get_client, create_linode_for_ip_share, create_linode_to_be_shared_with_ips +): + """ + Test that you can unshare IP addresses with Linode. + """ + + # create two linode instances and share the ip of instance1 with instance2 + linode_instance1 = create_linode_for_ip_share + linode_instance2 = create_linode_to_be_shared_with_ips + + get_client.networking.ip_addresses_share( + [linode_instance1.ips.ipv4.public[0]], linode_instance2.id + ) + + # unshared the ip with instance2 + get_client.networking.ip_addresses_share([], linode_instance2.id) + + assert [] == linode_instance2.ips.ipv4.shared From 7ffe2bda3f1b77064693bdcf8d0a10fb696f2d86 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Fri, 3 Nov 2023 01:37:50 -0700 Subject: [PATCH 04/19] test: Update E2E Tests, improve test fixture namin/teardowns (#341) --- test/integration/conftest.py | 84 ++++--- .../linode_client/test_linode_client.py | 112 ++++----- test/integration/models/test_account.py | 28 +-- test/integration/models/test_database.py | 212 +++++++++++++----- test/integration/models/test_domain.py | 26 +-- test/integration/models/test_firewall.py | 30 +-- test/integration/models/test_image.py | 16 +- test/integration/models/test_linode.py | 100 +++++---- test/integration/models/test_lke.py | 60 ++--- test/integration/models/test_longview.py | 20 +- test/integration/models/test_networking.py | 30 +-- test/integration/models/test_nodebalancer.py | 38 ++-- test/integration/models/test_tag.py | 10 +- test/integration/models/test_volume.py | 58 +++-- 14 files changed, 477 insertions(+), 347 deletions(-) diff --git a/test/integration/conftest.py b/test/integration/conftest.py index a17a6f847..25bae0710 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -3,6 +3,7 @@ import pytest +from linode_api4 import ApiError from linode_api4.linode_client import LinodeClient ENV_TOKEN_NAME = "LINODE_TOKEN" @@ -29,15 +30,15 @@ def run_long_tests(): @pytest.fixture(scope="session") -def create_linode(get_client): - client = get_client +def create_linode(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp linode_instance, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) yield linode_instance @@ -46,15 +47,15 @@ def create_linode(get_client): @pytest.fixture -def create_linode_for_pass_reset(get_client): - client = get_client +def create_linode_for_pass_reset(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp linode_instance, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) yield linode_instance, password @@ -80,7 +81,7 @@ def ssh_key_gen(): @pytest.fixture(scope="session") -def get_client(): +def test_linode_client(): token = get_token() api_url = get_api_url() api_ca_file = get_api_ca_file() @@ -93,8 +94,8 @@ def get_client(): @pytest.fixture -def set_account_settings(get_client): - client = get_client +def test_account_settings(test_linode_client): + client = test_linode_client account_settings = client.account.settings() account_settings._populated = True account_settings.network_helper = True @@ -103,10 +104,10 @@ def set_account_settings(get_client): @pytest.fixture(scope="session") -def create_domain(get_client): - client = get_client +def test_domain(test_linode_client): + client = test_linode_client - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) domain_addr = timestamp + "-example.com" soa_email = "pathiel-test123@linode.com" @@ -130,23 +131,36 @@ def create_domain(get_client): @pytest.fixture(scope="session") -def create_volume(get_client): - client = get_client - timestamp = str(int(time.time())) +def test_volume(test_linode_client): + client = test_linode_client + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp volume = client.volume_create(label=label, region="ap-west") yield volume - volume.delete() + timeout = 100 # give 100s for volume to be detached before deletion + + start_time = time.time() + + while time.time() - start_time < timeout: + try: + res = volume.delete() + if res: + break + else: + time.sleep(3) + except ApiError as e: + if time.time() - start_time > timeout: + raise e @pytest.fixture -def create_tag(get_client): - client = get_client +def test_tag(test_linode_client): + client = test_linode_client - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp tag = client.tag_create(label=label) @@ -157,10 +171,10 @@ def create_tag(get_client): @pytest.fixture -def create_nodebalancer(get_client): - client = get_client +def test_nodebalancer(test_linode_client): + client = test_linode_client - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp nodebalancer = client.nodebalancer_create(region="us-east", label=label) @@ -171,9 +185,9 @@ def create_nodebalancer(get_client): @pytest.fixture -def create_longview_client(get_client): - client = get_client - timestamp = str(int(time.time())) +def test_longview_client(test_linode_client): + client = test_linode_client + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp longview_client = client.longview.client_create(label=label) @@ -183,9 +197,9 @@ def create_longview_client(get_client): @pytest.fixture -def upload_sshkey(get_client, ssh_key_gen): +def test_sshkey(test_linode_client, ssh_key_gen): pub_key = ssh_key_gen[0] - client = get_client + client = test_linode_client key = client.profile.ssh_key_upload(pub_key, "IntTestSDK-sshkey") yield key @@ -194,8 +208,8 @@ def upload_sshkey(get_client, ssh_key_gen): @pytest.fixture -def create_ssh_keys_object_storage(get_client): - client = get_client +def ssh_keys_object_storage(test_linode_client): + client = test_linode_client label = "TestSDK-obj-storage-key" key = client.object_storage.keys_create(label) @@ -205,8 +219,8 @@ def create_ssh_keys_object_storage(get_client): @pytest.fixture(scope="session") -def create_firewall(get_client): - client = get_client +def test_firewall(test_linode_client): + client = test_linode_client rules = { "outbound": [], "outbound_policy": "DROP", @@ -224,8 +238,8 @@ def create_firewall(get_client): @pytest.fixture -def create_oauth_client(get_client): - client = get_client +def test_oauth_client(test_linode_client): + client = test_linode_client oauth_client = client.account.oauth_client_create( "test-oauth-client", "https://localhost/oauth/callback" ) diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 0df8bc8d7..08b7e2383 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -9,14 +9,14 @@ @pytest.fixture(scope="session", autouse=True) -def setup_client_and_linode(get_client): - client = get_client +def setup_client_and_linode(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() linode_instance, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) yield client, linode_instance @@ -50,7 +50,7 @@ def test_get_account(setup_client_and_linode): def test_fails_to_create_domain_without_soa_email(setup_client_and_linode): client = setup_client_and_linode[0] - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) domain_addr = timestamp + "example.com" try: domain = client.domain_create(domain=domain_addr) @@ -59,9 +59,9 @@ def test_fails_to_create_domain_without_soa_email(setup_client_and_linode): @pytest.mark.smoke -def test_get_domains(get_client, create_domain): - client = get_client - domain = create_domain +def test_get_domains(test_linode_client, test_domain): + client = test_linode_client + domain = test_domain domain_dict = client.domains() dom_list = [i.domain for i in domain_dict] @@ -117,9 +117,9 @@ def test_fails_to_delete_predefined_images(setup_client_and_linode): assert e.status == 403 -def test_get_volume(get_client, create_volume): - client = get_client - label = create_volume.label +def test_get_volume(test_linode_client, test_volume): + client = test_linode_client + label = test_volume.label volume_dict = client.volumes() @@ -128,9 +128,9 @@ def test_get_volume(get_client, create_volume): assert label in volume_label_list -def test_get_tag(get_client, create_tag): - client = get_client - label = create_tag.label +def test_get_tag(test_linode_client, test_tag): + client = test_linode_client + label = test_tag.label tags = client.tags() @@ -140,13 +140,13 @@ def test_get_tag(get_client, create_tag): def test_create_tag_with_id( - setup_client_and_linode, create_nodebalancer, create_domain, create_volume + setup_client_and_linode, test_nodebalancer, test_domain, test_volume ): client = setup_client_and_linode[0] linode = setup_client_and_linode[1] - nodebalancer = create_nodebalancer - domain = create_domain - volume = create_volume + nodebalancer = test_nodebalancer + domain = test_domain + volume = test_volume label = get_test_label() @@ -170,13 +170,13 @@ def test_create_tag_with_id( @pytest.mark.smoke def test_create_tag_with_entities( - setup_client_and_linode, create_nodebalancer, create_domain, create_volume + setup_client_and_linode, test_nodebalancer, test_domain, test_volume ): client = setup_client_and_linode[0] linode = setup_client_and_linode[1] - nodebalancer = create_nodebalancer - domain = create_domain - volume = create_volume + nodebalancer = test_nodebalancer + domain = test_domain + volume = test_volume label = get_test_label() @@ -195,8 +195,8 @@ def test_create_tag_with_entities( # AccountGroupTests -def test_get_account_settings(get_client): - client = get_client +def test_get_account_settings(test_linode_client): + client = test_linode_client account_settings = client.account.settings() assert account_settings._populated == True @@ -209,14 +209,14 @@ def test_get_account_settings(get_client): # LinodeGroupTests -def test_create_linode_instance_without_image(get_client): - client = get_client +def test_create_linode_instance_without_image(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() linode_instance = client.linode.instance_create( - "g5-standard-4", chosen_region, label=label + "g6-nanode-1", chosen_region, label=label ) assert linode_instance.label == label @@ -231,22 +231,22 @@ def test_create_linode_instance_without_image(get_client): def test_create_linode_instance_with_image(setup_client_and_linode): linode = setup_client_and_linode[1] - assert re.search("linode/debian9", str(linode.image)) + assert re.search("linode/debian10", str(linode.image)) # LongviewGroupTests -def test_get_longview_clients(get_client, create_longview_client): - client = get_client +def test_get_longview_clients(test_linode_client, test_longview_client): + client = test_linode_client longview_client = client.longview.clients() client_labels = [i.label for i in longview_client] - assert create_longview_client.label in client_labels + assert test_longview_client.label in client_labels -def test_client_create_with_label(get_client): - client = get_client +def test_client_create_with_label(test_linode_client): + client = test_linode_client label = get_test_label() longview_client = client.longview.client_create(label=label) @@ -266,15 +266,15 @@ def test_client_create_with_label(get_client): # LKEGroupTest -def test_kube_version(get_client): - client = get_client +def test_kube_version(test_linode_client): + client = test_linode_client lke_version = client.lke.versions() assert re.search("[0-9].[0-9]+", lke_version.first().id) -def test_cluster_create_with_api_objects(get_client): - client = get_client +def test_cluster_create_with_api_objects(test_linode_client): + client = test_linode_client node_type = client.linode.types()[1] # g6-standard-1 version = client.lke.versions()[0] region = client.regions().first() @@ -291,9 +291,9 @@ def test_cluster_create_with_api_objects(get_client): assert res -def test_fails_to_create_cluster_with_invalid_version(get_client): +def test_fails_to_create_cluster_with_invalid_version(test_linode_client): invalid_version = "a.12" - client = get_client + client = test_linode_client try: cluster = client.lke.cluster_create( @@ -310,19 +310,19 @@ def test_fails_to_create_cluster_with_invalid_version(get_client): # ProfileGroupTest -def test_get_sshkeys(get_client, upload_sshkey): - client = get_client +def test_get_sshkeys(test_linode_client, test_sshkey): + client = test_linode_client ssh_keys = client.profile.ssh_keys() ssh_labels = [i.label for i in ssh_keys] - assert upload_sshkey.label in ssh_labels + assert test_sshkey.label in ssh_labels -def test_ssh_key_create(upload_sshkey, ssh_key_gen): +def test_ssh_key_create(test_sshkey, ssh_key_gen): pub_key = ssh_key_gen[0] - key = upload_sshkey + key = test_sshkey assert pub_key == key._raw_json["ssh_key"] @@ -330,8 +330,8 @@ def test_ssh_key_create(upload_sshkey, ssh_key_gen): # ObjectStorageGroupTests -def test_get_object_storage_clusters(get_client): - client = get_client +def test_get_object_storage_clusters(test_linode_client): + client = test_linode_client clusters = client.object_storage.clusters() @@ -339,9 +339,9 @@ def test_get_object_storage_clusters(get_client): assert "us-east" in clusters[0].region.id -def test_get_keys(get_client, create_ssh_keys_object_storage): - client = get_client - key = create_ssh_keys_object_storage +def test_get_keys(test_linode_client, ssh_keys_object_storage): + client = test_linode_client + key = ssh_keys_object_storage keys = client.object_storage.keys() key_labels = [i.label for i in keys] @@ -349,10 +349,12 @@ def test_get_keys(get_client, create_ssh_keys_object_storage): assert key.label in key_labels -def test_keys_create(get_client, create_ssh_keys_object_storage): - key = create_ssh_keys_object_storage +def test_keys_create(test_linode_client, ssh_keys_object_storage): + key = ssh_keys_object_storage - assert type(key) == type(ObjectStorageKeys(client=get_client, id="123")) + assert type(key) == type( + ObjectStorageKeys(client=test_linode_client, id="123") + ) # NetworkingGroupTests @@ -362,8 +364,8 @@ def test_keys_create(get_client, create_ssh_keys_object_storage): @pytest.fixture -def create_firewall_with_inbound_outbound_rules(get_client): - client = get_client +def create_firewall_with_inbound_outbound_rules(test_linode_client): + client = test_linode_client label = get_test_label() + "-firewall" rules = { "outbound": [ @@ -398,9 +400,9 @@ def create_firewall_with_inbound_outbound_rules(get_client): def test_get_firewalls_with_inbound_outbound_rules( - get_client, create_firewall_with_inbound_outbound_rules + test_linode_client, create_firewall_with_inbound_outbound_rules ): - client = get_client + client = test_linode_client firewalls = client.networking.firewalls() firewall = create_firewall_with_inbound_outbound_rules diff --git a/test/integration/models/test_account.py b/test/integration/models/test_account.py index 308d2425b..9c2efc787 100644 --- a/test/integration/models/test_account.py +++ b/test/integration/models/test_account.py @@ -14,8 +14,8 @@ @pytest.mark.smoke -def test_get_account(get_client): - client = get_client +def test_get_account(test_linode_client): + client = test_linode_client account = client.account() account_id = account.id account_get = client.load(Account, account_id) @@ -33,8 +33,8 @@ def test_get_account(get_client): assert account_get.tax_id == account.tax_id -def test_get_login(get_client): - client = get_client +def test_get_login(test_linode_client): + client = test_linode_client login = client.load(Login(client, "", {}), "") updated_time = int(time.mktime(getattr(login, "_last_updated").timetuple())) @@ -48,8 +48,8 @@ def test_get_login(get_client): assert login_updated < 15 -def test_get_account_settings(get_client): - client = get_client +def test_get_account_settings(test_linode_client): + client = test_linode_client account_settings = client.load(AccountSettings(client, ""), "") assert "managed" in str(account_settings._raw_json) @@ -60,15 +60,15 @@ def test_get_account_settings(get_client): @pytest.mark.smoke -def test_latest_get_event(get_client): - client = get_client +def test_latest_get_event(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() linode, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) events = client.load(Event, "") @@ -80,17 +80,17 @@ def test_latest_get_event(get_client): assert label in latest_event["entity"]["label"] -def test_get_oathclient(get_client, create_oauth_client): - client = get_client +def test_get_oathclient(test_linode_client, test_oauth_client): + client = test_linode_client - oauth_client = client.load(OAuthClient, create_oauth_client.id) + oauth_client = client.load(OAuthClient, test_oauth_client.id) assert "test-oauth-client" == oauth_client.label assert "https://localhost/oauth/callback" == oauth_client.redirect_uri -def test_get_user(get_client): - client = get_client +def test_get_user(test_linode_client): + client = test_linode_client events = client.load(Event, "") diff --git a/test/integration/models/test_database.py b/test/integration/models/test_database.py index 974c5c923..7cd41be66 100644 --- a/test/integration/models/test_database.py +++ b/test/integration/models/test_database.py @@ -34,8 +34,11 @@ def get_postgres_db_status(client: LinodeClient, db_id, status: str): @pytest.fixture(scope="session") -def test_create_sql_db(get_client): - client = get_client +def test_create_sql_db(test_linode_client): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + client = test_linode_client label = get_test_label() + "-sqldb" region = "us-east" engine_id = get_db_engine_id(client, "mysql") @@ -61,8 +64,11 @@ def get_db_status(): @pytest.fixture(scope="session") -def test_create_postgres_db(get_client): - client = get_client +def test_create_postgres_db(test_linode_client): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + client = test_linode_client label = get_test_label() + "-postgresqldb" region = "us-east" engine_id = get_db_engine_id(client, "postgresql") @@ -88,8 +94,11 @@ def get_db_status(): # ------- SQL DB Test cases ------- -def test_get_types(get_client): - client = get_client +def test_get_types(test_linode_client): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + client = test_linode_client types = client.database.types() assert (types[0].type_class, "nanode") @@ -97,8 +106,11 @@ def test_get_types(get_client): assert (types[0].engines.mongodb[0].price.monthly, 15) -def test_get_engines(get_client): - client = get_client +def test_get_engines(test_linode_client): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + client = test_linode_client engines = client.database.engines() for e in engines: @@ -107,15 +119,21 @@ def test_get_engines(get_client): assert e.id == e.engine + "/" + e.version -def test_database_instance(get_client, test_create_sql_db): - dbs = get_client.database.mysql_instances() +def test_database_instance(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + dbs = test_linode_client.database.mysql_instances() assert str(test_create_sql_db.id) in str(dbs.lists) # ------- POSTGRESQL DB Test cases ------- -def test_get_sql_db_instance(get_client, test_create_sql_db): - dbs = get_client.database.mysql_instances() +def test_get_sql_db_instance(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + dbs = test_linode_client.database.mysql_instances() database = "" for db in dbs: if db.id == test_create_sql_db.id: @@ -128,8 +146,11 @@ def test_get_sql_db_instance(get_client, test_create_sql_db): assert "-mysql-primary.servers.linodedb.net" in database.hosts.primary -def test_update_sql_db(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_update_sql_db(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) new_allow_list = ["192.168.0.1/32"] label = get_test_label() + "updatedSQLDB" @@ -140,10 +161,15 @@ def test_update_sql_db(get_client, test_create_sql_db): res = db.save() - database = get_client.load(MySQLDatabase, test_create_sql_db.id) + database = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) wait_for_condition( - 30, 300, get_sql_db_status, get_client, test_create_sql_db.id, "active" + 30, + 300, + get_sql_db_status, + test_linode_client, + test_create_sql_db.id, + "active", ) assert res @@ -152,12 +178,20 @@ def test_update_sql_db(get_client, test_create_sql_db): assert database.updates.day_of_week == 2 -def test_create_sql_backup(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_create_sql_backup(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) label = "database_backup_test" wait_for_condition( - 30, 300, get_sql_db_status, get_client, test_create_sql_db.id, "active" + 30, + 300, + get_sql_db_status, + test_linode_client, + test_create_sql_db.id, + "active", ) db.backup_create(label=label, target="secondary") @@ -166,7 +200,7 @@ def test_create_sql_backup(get_client, test_create_sql_db): 10, 300, get_sql_db_status, - get_client, + test_linode_client, test_create_sql_db.id, "backing_up", ) @@ -175,7 +209,12 @@ def test_create_sql_backup(get_client, test_create_sql_db): # list backup and most recently created one is first element of the array wait_for_condition( - 30, 600, get_sql_db_status, get_client, test_create_sql_db.id, "active" + 30, + 600, + get_sql_db_status, + test_linode_client, + test_create_sql_db.id, + "active", ) backup = db.backups[0] @@ -188,8 +227,11 @@ def test_create_sql_backup(get_client, test_create_sql_db): backup.delete() -def test_sql_backup_restore(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_sql_backup_restore(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) try: backup = db.backups[0] except IndexError as e: @@ -203,7 +245,7 @@ def test_sql_backup_restore(get_client, test_create_sql_db): 10, 300, get_sql_db_status, - get_client, + test_linode_client, test_create_sql_db.id, "restoring", ) @@ -211,20 +253,31 @@ def test_sql_backup_restore(get_client, test_create_sql_db): assert db.status == "restoring" wait_for_condition( - 30, 1000, get_sql_db_status, get_client, test_create_sql_db.id, "active" + 30, + 1000, + get_sql_db_status, + test_linode_client, + test_create_sql_db.id, + "active", ) assert db.status == "active" -def test_get_sql_ssl(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_get_sql_ssl(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) assert "ca_certificate" in str(db.ssl) -def test_sql_patch(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_sql_patch(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) db.patch() @@ -232,7 +285,7 @@ def test_sql_patch(get_client, test_create_sql_db): 10, 300, get_sql_db_status, - get_client, + test_linode_client, test_create_sql_db.id, "updating", ) @@ -240,21 +293,32 @@ def test_sql_patch(get_client, test_create_sql_db): assert db.status == "updating" wait_for_condition( - 30, 1000, get_sql_db_status, get_client, test_create_sql_db.id, "active" + 30, + 1000, + get_sql_db_status, + test_linode_client, + test_create_sql_db.id, + "active", ) assert db.status == "active" -def test_get_sql_credentials(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_get_sql_credentials(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) assert db.credentials.username == "linroot" assert db.credentials.password -def test_reset_sql_credentials(get_client, test_create_sql_db): - db = get_client.load(MySQLDatabase, test_create_sql_db.id) +def test_reset_sql_credentials(test_linode_client, test_create_sql_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) old_pass = str(db.credentials.password) @@ -268,8 +332,11 @@ def test_reset_sql_credentials(get_client, test_create_sql_db): # ------- POSTGRESQL DB Test cases ------- -def test_get_postgres_db_instance(get_client, test_create_postgres_db): - dbs = get_client.database.postgresql_instances() +def test_get_postgres_db_instance(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + dbs = test_linode_client.database.postgresql_instances() for db in dbs: if db.id == test_create_postgres_db.id: @@ -282,8 +349,11 @@ def test_get_postgres_db_instance(get_client, test_create_postgres_db): assert "pgsql-primary.servers.linodedb.net" in database.hosts.primary -def test_update_postgres_db(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_update_postgres_db(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) new_allow_list = ["192.168.0.1/32"] label = get_test_label() + "updatedPostgresDB" @@ -294,13 +364,15 @@ def test_update_postgres_db(get_client, test_create_postgres_db): res = db.save() - database = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) + database = test_linode_client.load( + PostgreSQLDatabase, test_create_postgres_db.id + ) wait_for_condition( 30, 1000, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "active", ) @@ -311,18 +383,21 @@ def test_update_postgres_db(get_client, test_create_postgres_db): assert database.updates.day_of_week == 2 -def test_create_postgres_backup(get_client, test_create_postgres_db): +def test_create_postgres_backup(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) pytest.skip( "Failing due to '400: The backup snapshot request failed, please contact support.'" ) - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) label = "database_backup_test" wait_for_condition( 30, 1000, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "active", ) @@ -334,7 +409,7 @@ def test_create_postgres_backup(get_client, test_create_postgres_db): 10, 300, get_sql_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "backing_up", ) @@ -346,7 +421,7 @@ def test_create_postgres_backup(get_client, test_create_postgres_db): 30, 600, get_sql_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "active", ) @@ -358,8 +433,11 @@ def test_create_postgres_backup(get_client, test_create_postgres_db): assert backup.database_id == test_create_postgres_db.id -def test_postgres_backup_restore(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_postgres_backup_restore(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) try: backup = db.backups[0] @@ -374,7 +452,7 @@ def test_postgres_backup_restore(get_client, test_create_postgres_db): 30, 1000, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "restoring", ) @@ -383,7 +461,7 @@ def test_postgres_backup_restore(get_client, test_create_postgres_db): 30, 1000, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "active", ) @@ -391,14 +469,20 @@ def test_postgres_backup_restore(get_client, test_create_postgres_db): assert db.status == "active" -def test_get_postgres_ssl(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_get_postgres_ssl(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) assert "ca_certificate" in str(db.ssl) -def test_postgres_patch(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_postgres_patch(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) db.patch() @@ -406,7 +490,7 @@ def test_postgres_patch(get_client, test_create_postgres_db): 10, 300, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "updating", ) @@ -417,7 +501,7 @@ def test_postgres_patch(get_client, test_create_postgres_db): 30, 600, get_postgres_db_status, - get_client, + test_linode_client, test_create_postgres_db.id, "active", ) @@ -425,15 +509,23 @@ def test_postgres_patch(get_client, test_create_postgres_db): assert db.status == "active" -def test_get_postgres_credentials(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_get_postgres_credentials(test_linode_client, test_create_postgres_db): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) assert db.credentials.username == "linpostgres" assert db.credentials.password -def test_reset_postgres_credentials(get_client, test_create_postgres_db): - db = get_client.load(PostgreSQLDatabase, test_create_postgres_db.id) +def test_reset_postgres_credentials( + test_linode_client, test_create_postgres_db +): + pytest.skip( + "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" + ) + db = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) old_pass = str(db.credentials.password) diff --git a/test/integration/models/test_domain.py b/test/integration/models/test_domain.py index 2144fff55..cf5a54710 100644 --- a/test/integration/models/test_domain.py +++ b/test/integration/models/test_domain.py @@ -8,16 +8,16 @@ @pytest.mark.smoke -def test_get_domain_record(get_client, create_domain): +def test_get_domain_record(test_linode_client, test_domain): dr = DomainRecord( - get_client, create_domain.records.first().id, create_domain.id + test_linode_client, test_domain.records.first().id, test_domain.id ) - assert dr.id == create_domain.records.first().id + assert dr.id == test_domain.records.first().id -def test_save_null_values_excluded(get_client, create_domain): - domain = get_client.load(Domain, create_domain.id) +def test_save_null_values_excluded(test_linode_client, test_domain): + domain = test_linode_client.load(Domain, test_domain.id) domain.type = "master" domain.master_ips = ["127.0.0.1"] @@ -26,8 +26,8 @@ def test_save_null_values_excluded(get_client, create_domain): assert res -def test_zone_file_view(get_client, create_domain): - domain = get_client.load(Domain, create_domain.id) +def test_zone_file_view(test_linode_client, test_domain): + domain = test_linode_client.load(Domain, test_domain.id) def get_zone_file_view(): res = domain.zone_file_view() @@ -39,13 +39,13 @@ def get_zone_file_view(): assert re.search("ns[0-9].linode.com", str(domain.zone_file_view())) -def test_clone(get_client, create_domain): - domain = get_client.load(Domain, create_domain.id) - timestamp = str(int(time.time())) +def test_clone(test_linode_client, test_domain): + domain = test_linode_client.load(Domain, test_domain.id) + timestamp = str(time.time_ns()) dom = "example.clone-" + timestamp + "-IntTestSDK.org" domain.clone(dom) - ds = get_client.domains() + ds = test_linode_client.domains() time.sleep(1) @@ -54,8 +54,8 @@ def test_clone(get_client, create_domain): assert dom in domains -def test_import(get_client, create_domain): +def test_import(test_linode_client, test_domain): pytest.skip( 'Currently failing with message: linode_api4.errors.ApiError: 400: An unknown error occured. Please open a ticket for further assistance. Command: domain_import(domain, "google.ca")' ) - domain = get_client.load(Domain, create_domain.id) + domain = test_linode_client.load(Domain, test_domain.id) diff --git a/test/integration/models/test_firewall.py b/test/integration/models/test_firewall.py index c45812677..e9e7b8bcc 100644 --- a/test/integration/models/test_firewall.py +++ b/test/integration/models/test_firewall.py @@ -6,14 +6,14 @@ @pytest.fixture(scope="session") -def create_linode_fw(get_client): - client = get_client +def linode_fw(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = "linode_instance_fw_device" linode_instance, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) yield linode_instance @@ -22,8 +22,8 @@ def create_linode_fw(get_client): @pytest.mark.smoke -def test_get_firewall_rules(get_client, create_firewall): - firewall = get_client.load(Firewall, create_firewall.id) +def test_get_firewall_rules(test_linode_client, test_firewall): + firewall = test_linode_client.load(Firewall, test_firewall.id) rules = firewall.rules assert rules.inbound_policy in ["ACCEPT", "DROP"] @@ -31,8 +31,8 @@ def test_get_firewall_rules(get_client, create_firewall): @pytest.mark.smoke -def test_update_firewall_rules(get_client, create_firewall): - firewall = get_client.load(Firewall, create_firewall.id) +def test_update_firewall_rules(test_linode_client, test_firewall): + firewall = test_linode_client.load(Firewall, test_firewall.id) new_rules = { "inbound": [ { @@ -56,26 +56,26 @@ def test_update_firewall_rules(get_client, create_firewall): time.sleep(1) - firewall = get_client.load(Firewall, create_firewall.id) + firewall = test_linode_client.load(Firewall, test_firewall.id) assert firewall.rules.inbound_policy == "ACCEPT" assert firewall.rules.outbound_policy == "DROP" -def test_get_devices(get_client, create_linode_fw, create_firewall): - linode = create_linode_fw +def test_get_devices(test_linode_client, linode_fw, test_firewall): + linode = linode_fw - create_firewall.device_create(int(linode.id)) + test_firewall.device_create(int(linode.id)) - firewall = get_client.load(Firewall, create_firewall.id) + firewall = test_linode_client.load(Firewall, test_firewall.id) assert len(firewall.devices) > 0 -def test_get_device(get_client, create_firewall, create_linode_fw): - firewall = create_firewall +def test_get_device(test_linode_client, test_firewall, linode_fw): + firewall = test_firewall - firewall_device = get_client.load( + firewall_device = test_linode_client.load( FirewallDevice, firewall.devices.first().id, firewall.id ) diff --git a/test/integration/models/test_image.py b/test/integration/models/test_image.py index fe828643e..239e65784 100644 --- a/test/integration/models/test_image.py +++ b/test/integration/models/test_image.py @@ -10,37 +10,37 @@ @pytest.fixture(scope="session") -def image_upload(get_client): +def image_upload(test_linode_client): label = get_test_label() + "_image" - get_client.image_create_upload( + test_linode_client.image_create_upload( label, "us-east", "integration test image upload" ) - image = get_client.images()[0] + image = test_linode_client.images()[0] yield image image.delete() - images = get_client.images() + images = test_linode_client.images() delete_instance_with_test_kw(images) @pytest.mark.smoke -def test_get_image(get_client, image_upload): - image = get_client.load(Image, image_upload.id) +def test_get_image(test_linode_client, image_upload): + image = test_linode_client.load(Image, image_upload.id) assert image.label == image_upload.label -def test_image_create_upload(get_client): +def test_image_create_upload(test_linode_client): test_image_content = ( b"\x1F\x8B\x08\x08\xBD\x5C\x91\x60\x00\x03\x74\x65\x73\x74\x2E\x69" b"\x6D\x67\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) label = get_test_label() + "_image" - image = get_client.image_upload( + image = test_linode_client.image_upload( label, "us-east", BytesIO(test_image_content), diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 22f5709c7..9f6f76d65 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -12,8 +12,8 @@ @pytest.fixture(scope="session") -def create_linode_with_volume_firewall(get_client): - client = get_client +def linode_with_volume_firewall(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() @@ -26,9 +26,9 @@ def create_linode_with_volume_firewall(get_client): } linode_instance, password = client.linode.instance_create( - "g5-standard-4", + "g6-nanode-1", chosen_region, - image="linode/debian9", + image="linode/debian10", label=label + "_modlinode", ) @@ -48,27 +48,27 @@ def create_linode_with_volume_firewall(get_client): firewall.delete() - linode_instance.delete() - volume.detach() - # wait for volume detach, can't currently get the attach/unattached status via SDK + # wait for volume detach, can't currently get the attached/unattached status via SDK time.sleep(30) volume.delete() + linode_instance.delete() + @pytest.mark.smoke @pytest.fixture -def create_linode_for_long_running_tests(get_client): - client = get_client +def create_linode_for_long_running_tests(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() linode_instance, password = client.linode.instance_create( - "g5-standard-4", + "g6-nanode-1", chosen_region, - image="linode/debian9", + image="linode/debian10", label=label + "_long_tests", ) @@ -82,15 +82,15 @@ def get_status(linode: Instance, status: str): return linode.status == status -def test_get_linode(get_client, create_linode_with_volume_firewall): - linode = get_client.load(Instance, create_linode_with_volume_firewall.id) +def test_get_linode(test_linode_client, linode_with_volume_firewall): + linode = test_linode_client.load(Instance, linode_with_volume_firewall.id) - assert linode.label == create_linode_with_volume_firewall.label - assert linode.id == create_linode_with_volume_firewall.id + assert linode.label == linode_with_volume_firewall.label + assert linode.id == linode_with_volume_firewall.id -def test_linode_transfer(get_client, create_linode_with_volume_firewall): - linode = get_client.load(Instance, create_linode_with_volume_firewall.id) +def test_linode_transfer(test_linode_client, linode_with_volume_firewall): + linode = test_linode_client.load(Instance, linode_with_volume_firewall.id) transfer = linode.transfer @@ -99,24 +99,24 @@ def test_linode_transfer(get_client, create_linode_with_volume_firewall): assert "billable" in str(transfer) -def test_linode_rebuild(get_client): - client = get_client +def test_linode_rebuild(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() + "_rebuild" linode, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) wait_for_condition(10, 100, get_status, linode, "running") - retry_sending_request(3, linode.rebuild, "linode/debian9") + retry_sending_request(3, linode.rebuild, "linode/debian10") wait_for_condition(10, 100, get_status, linode, "rebuilding") assert linode.status == "rebuilding" - assert linode.image.id == "linode/debian9" + assert linode.image.id == "linode/debian10" wait_for_condition(10, 300, get_status, linode, "running") @@ -149,16 +149,16 @@ def test_update_linode(create_linode): assert linode.label == new_label -def test_delete_linode(get_client): - client = get_client +def test_delete_linode(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() linode_instance, password = client.linode.instance_create( - "g5-standard-4", + "g6-nanode-1", chosen_region, - image="linode/debian9", + image="linode/debian10", label=label + "_linode", ) @@ -224,10 +224,10 @@ def test_linode_resize(create_linode_for_long_running_tests): def test_linode_resize_with_class( - get_client, create_linode_for_long_running_tests + test_linode_client, create_linode_for_long_running_tests ): linode = create_linode_for_long_running_tests - ltype = Type(get_client, "g6-standard-6") + ltype = Type(test_linode_client, "g6-standard-6") wait_for_condition(10, 100, get_status, linode, "running") @@ -263,8 +263,8 @@ def test_linode_boot_with_config(create_linode): assert linode.status == "running" -def test_linode_firewalls(create_linode_with_volume_firewall): - linode = create_linode_with_volume_firewall +def test_linode_firewalls(linode_with_volume_firewall): + linode = linode_with_volume_firewall firewalls = linode.firewalls() @@ -272,8 +272,8 @@ def test_linode_firewalls(create_linode_with_volume_firewall): assert "TestSDK" in firewalls[0].label -def test_linode_volumes(create_linode_with_volume_firewall): - linode = create_linode_with_volume_firewall +def test_linode_volumes(linode_with_volume_firewall): + linode = linode_with_volume_firewall volumes = linode.volumes() @@ -281,11 +281,11 @@ def test_linode_volumes(create_linode_with_volume_firewall): assert "TestSDK" in volumes[0].label -def test_linode_disk_duplicate(get_client, create_linode): +def test_linode_disk_duplicate(test_linode_client, create_linode): pytest.skip("Need to find out the space sizing when duplicating disks") linode = create_linode - disk = get_client.load(Disk, linode.disks[0].id, linode.id) + disk = test_linode_client.load(Disk, linode.disks[0].id, linode.id) try: dup_disk = disk.duplicate() @@ -323,14 +323,14 @@ def test_linode_ips(create_linode): assert ips.ipv4.public[0].address == linode.ipv4[0] -def test_linode_initate_migration(get_client): - client = get_client +def test_linode_initate_migration(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_test_label() + "_migration" linode, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) wait_for_condition(10, 100, get_status, linode, "running") @@ -373,24 +373,26 @@ def test_config_update_interfaces(create_linode): assert "cool-vlan" in str(config.interfaces) -def test_get_config(get_client, create_linode): +def test_get_config(test_linode_client, create_linode): pytest.skip( "Model get method: client.load(Config, 123, 123) does not work..." ) linode = create_linode - json = get_client.get( + json = test_linode_client.get( "linode/instances/" + str(linode.id) + "/configs/" + str(linode.configs[0].id) ) - config = Config(get_client, linode.id, linode.configs[0].id, json=json) + config = Config( + test_linode_client, linode.id, linode.configs[0].id, json=json + ) assert config.id == linode.configs[0].id -def test_get_linode_types(get_client): - types = get_client.linode.types() +def test_get_linode_types(test_linode_client): + types = test_linode_client.linode.types() ids = [i.id for i in types] @@ -398,8 +400,8 @@ def test_get_linode_types(get_client): assert "g6-nanode-1" in ids -def test_get_linode_types_overrides(get_client): - types = get_client.linode.types() +def test_get_linode_types_overrides(test_linode_client): + types = test_linode_client.linode.types() target_types = [ v @@ -414,7 +416,7 @@ def test_get_linode_types_overrides(get_client): assert linode_type.region_prices[0].monthly >= 0 -def test_get_linode_type_by_id(get_client): +def test_get_linode_type_by_id(test_linode_client): pytest.skip( "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" ) @@ -426,23 +428,23 @@ def test_get_linode_type_gpu(): ) -def test_save_linode_noforce(get_client, create_linode): +def test_save_linode_noforce(test_linode_client, create_linode): linode = create_linode old_label = linode.label linode.label = "updated_no_force_label" linode.save(force=False) - linode = get_client.load(Instance, linode.id) + linode = test_linode_client.load(Instance, linode.id) assert old_label != linode.label -def test_save_linode_force(get_client, create_linode): +def test_save_linode_force(test_linode_client, create_linode): linode = create_linode old_label = linode.label linode.label = "updated_force_label" linode.save(force=False) - linode = get_client.load(Instance, linode.id) + linode = test_linode_client.load(Instance, linode.id) assert old_label != linode.label diff --git a/test/integration/models/test_lke.py b/test/integration/models/test_lke.py index 11df1cbcc..45b1ac8a1 100644 --- a/test/integration/models/test_lke.py +++ b/test/integration/models/test_lke.py @@ -12,14 +12,16 @@ @pytest.fixture(scope="session") -def create_lke_cluster(get_client): - node_type = get_client.linode.types()[1] # g6-standard-1 - version = get_client.lke.versions()[0] - region = get_client.regions().first() - node_pools = get_client.lke.node_pool(node_type, 3) +def lke_cluster(test_linode_client): + node_type = test_linode_client.linode.types()[1] # g6-standard-1 + version = test_linode_client.lke.versions()[0] + region = test_linode_client.regions().first() + node_pools = test_linode_client.lke.node_pool(node_type, 3) label = get_test_label() + "_cluster" - cluster = get_client.lke.cluster_create(region, label, node_pools, version) + cluster = test_linode_client.lke.cluster_create( + region, label, node_pools, version + ) yield cluster @@ -36,24 +38,24 @@ def get_node_status(cluster: LKECluster, status: str): @pytest.mark.smoke -def test_get_lke_clusters(get_client, create_lke_cluster): - cluster = get_client.load(LKECluster, create_lke_cluster.id) +def test_get_lke_clusters(test_linode_client, lke_cluster): + cluster = test_linode_client.load(LKECluster, lke_cluster.id) - assert cluster._raw_json == create_lke_cluster._raw_json + assert cluster._raw_json == lke_cluster._raw_json -def test_get_lke_pool(get_client, create_lke_cluster): +def test_get_lke_pool(test_linode_client, lke_cluster): pytest.skip("client.load(LKENodePool, 123, 123) does not work") - cluster = create_lke_cluster + cluster = lke_cluster - pool = get_client.load(LKENodePool, cluster.pools[0].id, cluster.id) + pool = test_linode_client.load(LKENodePool, cluster.pools[0].id, cluster.id) assert cluster.pools[0]._raw_json == pool -def test_cluster_dashboard_url_view(create_lke_cluster): - cluster = create_lke_cluster +def test_cluster_dashboard_url_view(lke_cluster): + cluster = lke_cluster url = send_request_when_resource_available( 300, cluster.cluster_dashboard_url_view @@ -62,14 +64,14 @@ def test_cluster_dashboard_url_view(create_lke_cluster): assert re.search("https://+", url) -def test_kubeconfig_delete(create_lke_cluster): - cluster = create_lke_cluster +def test_kubeconfig_delete(lke_cluster): + cluster = lke_cluster cluster.kubeconfig_delete() -def test_lke_node_view(create_lke_cluster): - cluster = create_lke_cluster +def test_lke_node_view(lke_cluster): + cluster = lke_cluster node_id = cluster.pools[0].nodes[0].id node = cluster.node_view(node_id) @@ -79,8 +81,8 @@ def test_lke_node_view(create_lke_cluster): assert node.instance_id -def test_lke_node_delete(create_lke_cluster): - cluster = create_lke_cluster +def test_lke_node_delete(lke_cluster): + cluster = lke_cluster node_id = cluster.pools[0].nodes[0].id cluster.node_delete(node_id) @@ -90,8 +92,8 @@ def test_lke_node_delete(create_lke_cluster): assert "Not found" in str(err.json) -def test_lke_node_recycle(get_client, create_lke_cluster): - cluster = get_client.load(LKECluster, create_lke_cluster.id) +def test_lke_node_recycle(test_linode_client, lke_cluster): + cluster = test_linode_client.load(LKECluster, lke_cluster.id) node = cluster.pools[0].nodes[0] node_id = cluster.pools[0].nodes[0].id @@ -109,30 +111,30 @@ def test_lke_node_recycle(get_client, create_lke_cluster): assert node.status == "ready" -def test_lke_cluster_nodes_recycle(get_client, create_lke_cluster): - cluster = create_lke_cluster +def test_lke_cluster_nodes_recycle(test_linode_client, lke_cluster): + cluster = lke_cluster send_request_when_resource_available(300, cluster.cluster_nodes_recycle) - wait_for_condition(5, 120, get_node_status, cluster, "not_ready") + wait_for_condition(5, 300, get_node_status, cluster, "not_ready") node = cluster.pools[0].nodes[0] assert node.status == "not_ready" -def test_lke_cluster_regenerate(create_lke_cluster): +def test_lke_cluster_regenerate(lke_cluster): pytest.skip( "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" ) - cluster = create_lke_cluster + cluster = lke_cluster cluster.cluster_regenerate() -def test_service_token_delete(create_lke_cluster): +def test_service_token_delete(lke_cluster): pytest.skip( "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" ) - cluster = create_lke_cluster + cluster = lke_cluster cluster.service_token_delete() diff --git a/test/integration/models/test_longview.py b/test/integration/models/test_longview.py index a137564d9..0fb7daf7f 100644 --- a/test/integration/models/test_longview.py +++ b/test/integration/models/test_longview.py @@ -7,14 +7,14 @@ @pytest.mark.smoke -def test_get_longview_client(get_client, create_longview_client): - longview = get_client.load(LongviewClient, create_longview_client.id) +def test_get_longview_client(test_linode_client, test_longview_client): + longview = test_linode_client.load(LongviewClient, test_longview_client.id) - assert longview.id == create_longview_client.id + assert longview.id == test_longview_client.id -def test_update_longview_label(get_client, create_longview_client): - longview = get_client.load(LongviewClient, create_longview_client.id) +def test_update_longview_label(test_linode_client, test_longview_client): + longview = test_linode_client.load(LongviewClient, test_longview_client.id) old_label = longview.label label = "updated_longview_label" @@ -26,8 +26,8 @@ def test_update_longview_label(get_client, create_longview_client): assert longview.label != old_label -def test_delete_client(get_client, create_longview_client): - client = get_client +def test_delete_client(test_linode_client, test_longview_client): + client = test_linode_client label = "TestSDK-longview" longview_client = client.longview.client_create(label=label) @@ -38,9 +38,9 @@ def test_delete_client(get_client, create_longview_client): assert res -def test_get_longview_subscription(get_client, create_longview_client): - subs = get_client.longview.subscriptions() - sub = get_client.load(LongviewSubscription, subs[0].id) +def test_get_longview_subscription(test_linode_client, test_longview_client): + subs = test_linode_client.longview.subscriptions() + sub = test_linode_client.load(LongviewSubscription, subs[0].id) assert "clients_included" in str(subs.first().__dict__) diff --git a/test/integration/models/test_networking.py b/test/integration/models/test_networking.py index 970ee6880..95bc2196b 100644 --- a/test/integration/models/test_networking.py +++ b/test/integration/models/test_networking.py @@ -6,8 +6,8 @@ @pytest.mark.smoke -def test_get_networking_rules(get_client, create_firewall): - firewall = get_client.load(Firewall, create_firewall.id) +def test_get_networking_rules(test_linode_client, test_firewall): + firewall = test_linode_client.load(Firewall, test_firewall.id) rules = firewall.get_rules() @@ -17,8 +17,8 @@ def test_get_networking_rules(get_client, create_firewall): assert "outbound_policy" in str(rules) -def create_linode(get_client): - client = get_client +def create_linode(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = get_rand_nanosec_test_label() @@ -34,8 +34,8 @@ def create_linode(get_client): @pytest.fixture -def create_linode_for_ip_share(get_client): - linode = create_linode(get_client) +def create_linode_for_ip_share(test_linode_client): + linode = create_linode(test_linode_client) yield linode @@ -43,8 +43,8 @@ def create_linode_for_ip_share(get_client): @pytest.fixture -def create_linode_to_be_shared_with_ips(get_client): - linode = create_linode(get_client) +def create_linode_to_be_shared_with_ips(test_linode_client): + linode = create_linode(test_linode_client) yield linode @@ -53,7 +53,9 @@ def create_linode_to_be_shared_with_ips(get_client): @pytest.mark.smoke def test_ip_addresses_share( - get_client, create_linode_for_ip_share, create_linode_to_be_shared_with_ips + test_linode_client, + create_linode_for_ip_share, + create_linode_to_be_shared_with_ips, ): """ Test that you can share IP addresses with Linode. @@ -63,7 +65,7 @@ def test_ip_addresses_share( linode_instance1 = create_linode_for_ip_share linode_instance2 = create_linode_to_be_shared_with_ips - get_client.networking.ip_addresses_share( + test_linode_client.networking.ip_addresses_share( [linode_instance1.ips.ipv4.public[0]], linode_instance2.id ) @@ -75,7 +77,9 @@ def test_ip_addresses_share( @pytest.mark.smoke def test_ip_addresses_unshare( - get_client, create_linode_for_ip_share, create_linode_to_be_shared_with_ips + test_linode_client, + create_linode_for_ip_share, + create_linode_to_be_shared_with_ips, ): """ Test that you can unshare IP addresses with Linode. @@ -85,11 +89,11 @@ def test_ip_addresses_unshare( linode_instance1 = create_linode_for_ip_share linode_instance2 = create_linode_to_be_shared_with_ips - get_client.networking.ip_addresses_share( + test_linode_client.networking.ip_addresses_share( [linode_instance1.ips.ipv4.public[0]], linode_instance2.id ) # unshared the ip with instance2 - get_client.networking.ip_addresses_share([], linode_instance2.id) + test_linode_client.networking.ip_addresses_share([], linode_instance2.id) assert [] == linode_instance2.ips.ipv4.shared diff --git a/test/integration/models/test_nodebalancer.py b/test/integration/models/test_nodebalancer.py index 455b88f1a..332f10214 100644 --- a/test/integration/models/test_nodebalancer.py +++ b/test/integration/models/test_nodebalancer.py @@ -7,16 +7,16 @@ @pytest.fixture(scope="session") -def create_linode_with_private_ip(get_client): - client = get_client +def linode_with_private_ip(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = "linode_with_privateip" linode_instance, password = client.linode.instance_create( - "g5-standard-4", + "g6-nanode-1", chosen_region, - image="linode/debian9", + image="linode/debian10", label=label, private_ip=True, ) @@ -27,8 +27,8 @@ def create_linode_with_private_ip(get_client): @pytest.fixture(scope="session") -def create_nb_config(get_client): - client = get_client +def create_nb_config(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] label = "nodebalancer_test" @@ -43,8 +43,8 @@ def create_nb_config(get_client): nb.delete() -def test_get_nodebalancer_config(get_client, create_nb_config): - config = get_client.load( +def test_get_nodebalancer_config(test_linode_client, create_nb_config): + config = test_linode_client.load( NodeBalancerConfig, create_nb_config.id, create_nb_config.nodebalancer_id, @@ -53,14 +53,14 @@ def test_get_nodebalancer_config(get_client, create_nb_config): @pytest.mark.smoke def test_create_nb_node( - get_client, create_nb_config, create_linode_with_private_ip + test_linode_client, create_nb_config, linode_with_private_ip ): - config = get_client.load( + config = test_linode_client.load( NodeBalancerConfig, create_nb_config.id, create_nb_config.nodebalancer_id, ) - linode = create_linode_with_private_ip + linode = linode_with_private_ip address = [a for a in linode.ipv4 if re.search("192.168.+", a)][0] node = config.node_create( "node_test", address + ":80", weight=50, mode="accept" @@ -70,16 +70,16 @@ def test_create_nb_node( assert "node_test" == node.label -def test_get_nb_node(get_client, create_nb_config): - node = get_client.load( +def test_get_nb_node(test_linode_client, create_nb_config): + node = test_linode_client.load( NodeBalancerNode, create_nb_config.nodes[0].id, (create_nb_config.id, create_nb_config.nodebalancer_id), ) -def test_update_nb_node(get_client, create_nb_config): - config = get_client.load( +def test_update_nb_node(test_linode_client, create_nb_config): + config = test_linode_client.load( NodeBalancerConfig, create_nb_config.id, create_nb_config.nodebalancer_id, @@ -90,7 +90,7 @@ def test_update_nb_node(get_client, create_nb_config): node.mode = "accept" node.save() - node_updated = get_client.load( + node_updated = test_linode_client.load( NodeBalancerNode, create_nb_config.nodes[0].id, (create_nb_config.id, create_nb_config.nodebalancer_id), @@ -101,8 +101,8 @@ def test_update_nb_node(get_client, create_nb_config): assert "accept" == node_updated.mode -def test_delete_nb_node(get_client, create_nb_config): - config = get_client.load( +def test_delete_nb_node(test_linode_client, create_nb_config): + config = test_linode_client.load( NodeBalancerConfig, create_nb_config.id, create_nb_config.nodebalancer_id, @@ -112,7 +112,7 @@ def test_delete_nb_node(get_client, create_nb_config): node.delete() with pytest.raises(ApiError) as e: - get_client.load( + test_linode_client.load( NodeBalancerNode, create_nb_config.nodes[0].id, (create_nb_config.id, create_nb_config.nodebalancer_id), diff --git a/test/integration/models/test_tag.py b/test/integration/models/test_tag.py index 42b5ec7c5..a9357a896 100644 --- a/test/integration/models/test_tag.py +++ b/test/integration/models/test_tag.py @@ -6,9 +6,9 @@ @pytest.fixture -def create_tag(get_client): +def test_tag(test_linode_client): unique_tag = get_test_label() + "_tag" - tag = get_client.tag_create(unique_tag) + tag = test_linode_client.tag_create(unique_tag) yield tag @@ -16,7 +16,7 @@ def create_tag(get_client): @pytest.mark.smoke -def test_get_tag(get_client, create_tag): - tag = get_client.load(Tag, create_tag.id) +def test_get_tag(test_linode_client, test_tag): + tag = test_linode_client.load(Tag, test_tag.id) - assert tag.id == create_tag.id + assert tag.id == test_tag.id diff --git a/test/integration/models/test_volume.py b/test/integration/models/test_volume.py index 92eb67ba3..ca63cb105 100644 --- a/test/integration/models/test_volume.py +++ b/test/integration/models/test_volume.py @@ -8,25 +8,39 @@ import pytest -from linode_api4 import LinodeClient +from linode_api4 import ApiError, LinodeClient from linode_api4.objects import Volume @pytest.fixture(scope="session") -def create_linode_for_volume(get_client): - client = get_client +def linode_for_volume(test_linode_client): + client = test_linode_client available_regions = client.regions() chosen_region = available_regions[0] - timestamp = str(int(time.time())) + timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp linode_instance, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", label=label + "g6-nanode-1", chosen_region, image="linode/debian10", label=label ) yield linode_instance - linode_instance.delete() + timeout = 100 # give 100s for volume to be detached before deletion + + start_time = time.time() + + while time.time() - start_time < timeout: + try: + res = linode_instance.delete() + + if res: + break + else: + time.sleep(3) + except ApiError as e: + if time.time() - start_time > timeout: + raise e def get_status(volume: Volume, status: str): @@ -36,27 +50,27 @@ def get_status(volume: Volume, status: str): @pytest.mark.smoke -def test_get_volume(get_client, create_volume): - volume = get_client.load(Volume, create_volume.id) +def test_get_volume(test_linode_client, test_volume): + volume = test_linode_client.load(Volume, test_volume.id) - assert volume.id == create_volume.id + assert volume.id == test_volume.id -def test_update_volume_tag(get_client, create_volume): - volume = create_volume +def test_update_volume_tag(test_linode_client, test_volume): + volume = test_volume tag_1 = "volume_test_tag1" tag_2 = "volume_test_tag2" volume.tags = [tag_1, tag_2] volume.save() - volume = get_client.load(Volume, create_volume.id) + volume = test_linode_client.load(Volume, test_volume.id) assert [tag_1, tag_2] == volume.tags -def test_volume_resize(get_client, create_volume): - volume = get_client.load(Volume, create_volume.id) +def test_volume_resize(test_linode_client, test_volume): + volume = test_linode_client.load(Volume, test_volume.id) wait_for_condition(10, 100, get_status, volume, "active") @@ -65,8 +79,8 @@ def test_volume_resize(get_client, create_volume): assert res -def test_volume_clone_and_delete(get_client, create_volume): - volume = get_client.load(Volume, create_volume.id) +def test_volume_clone_and_delete(test_linode_client, test_volume): + volume = test_linode_client.load(Volume, test_volume.id) label = get_test_label() wait_for_condition(10, 100, get_status, volume, "active") @@ -81,10 +95,10 @@ def test_volume_clone_and_delete(get_client, create_volume): def test_attach_volume_to_linode( - get_client, create_volume, create_linode_for_volume + test_linode_client, test_volume, linode_for_volume ): - volume = create_volume - linode = create_linode_for_volume + volume = test_volume + linode = linode_for_volume res = retry_sending_request(5, volume.attach, linode.id) @@ -92,10 +106,10 @@ def test_attach_volume_to_linode( def test_detach_volume_to_linode( - get_client, create_volume, create_linode_for_volume + test_linode_client, test_volume, linode_for_volume ): - volume = create_volume - linode = create_linode_for_volume + volume = test_volume + linode = linode_for_volume res = retry_sending_request(5, volume.detach) From 0e46856404396e31c946802e5b69850a7b2c698e Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:19:23 -0500 Subject: [PATCH 05/19] Project: Virtual Private Cloud (#345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for VPCs * Drop debug statement * Clean up * Revert * oops * Add docs * Add reorder test * Add reorder test * make format * oops * Fix VPCSubnet docs * Address feedback * Use `asdict` to convert dataclass objects to dicts (#324) * test: additional integration tests for vpc (#335) ## 📝 Description - Extending test coverage for VPC ## ✔️ How to Test 1. Setup API token for alpha/beta environment and export it LINODE_TOKEN=mytoken 2. get cacert.pem (e.g. wget https://certurl.com/cacert.pem) 3. make slight modification to `def get_client()` in conftest.py e.g. `client = LinodeClient(token, base_url='https://api.dev.linode.com/v4beta', ca_path='/Users/ykim/linode/ykim/linode_api4-python/cacert.pem')` 4. run test `pytest test/integration/models/test_vpc.py` ## 📷 Preview **If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** --------- Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Co-authored-by: Zhiwei Liang * Add `vpc_nat_1_1` to IPAddress (#342) ## 📝 Description This change is adding `vpc_nat_1_1` to the IPAddress for VPC. If a public IPv4 address is NAT 1:1 mapped to a private VPC IP, this field is returned VPC IP together with the VPC and subnet ids. Also trying to merge two commits from dev to proj/vpc to update the feature branch. The actual change is focusing on https://github.com/linode/linode_api4-python/pull/342/commits/3206ab838e088712b1e47098123ca7a1511a088b. ## ✔️ How to Test Build unit test to make sure that we can retrieve this field from IPAddress object: `tox` --------- Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> * implementation * fixing tests * json_object * Support list deserialise in `JSONObject` * cleaning up code * make format * Replace `get_client` with `test_linode_client` (#346) * fix: Handle `null` values in `JSONObject` fields (#344) ## 📝 Description **What does this PR do and why is this change necessary?** Handle null values for `JSONObject` fields ## ✔️ How to Test **What are the steps to reproduce the issue or verify the changes?** `pytest test/integration/models/test_networking.py` --------- Co-authored-by: Lena Garber Co-authored-by: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Co-authored-by: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Co-authored-by: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Co-authored-by: Ania Misiorek Co-authored-by: Ania Misiorek <139170033+amisiorek-akamai@users.noreply.github.com> Co-authored-by: Jacob Riddle <87780794+jriddle-linode@users.noreply.github.com> --- linode_api4/groups/__init__.py | 1 + linode_api4/groups/linode.py | 9 + linode_api4/groups/vpc.py | 83 +++++ linode_api4/linode_client.py | 4 + linode_api4/objects/__init__.py | 2 + linode_api4/objects/base.py | 27 +- linode_api4/objects/linode.py | 316 +++++++++++++++--- linode_api4/objects/networking.py | 17 +- linode_api4/objects/serializable.py | 102 ++++++ linode_api4/objects/vpc.py | 99 ++++++ .../linode_instances_123_configs.json | 28 +- .../linode_instances_123_configs_456789.json | 26 +- ...stances_123_configs_456789_interfaces.json | 34 ++ ...ces_123_configs_456789_interfaces_123.json | 15 + ...123_configs_456789_interfaces_123_put.json | 14 + ...ces_123_configs_456789_interfaces_321.json | 7 + ...ces_123_configs_456789_interfaces_456.json | 5 + test/fixtures/networking_ips_127.0.0.1.json | 7 +- test/fixtures/vpcs.json | 15 + test/fixtures/vpcs_123456.json | 8 + test/fixtures/vpcs_123456_subnets.json | 29 ++ test/fixtures/vpcs_123456_subnets_789.json | 22 ++ test/integration/conftest.py | 115 ++++++- test/integration/models/test_linode.py | 182 +++++++++- test/integration/models/test_networking.py | 28 +- test/integration/models/test_vpc.py | 100 ++++++ test/unit/objects/domain_test.py | 1 - test/unit/objects/linode_test.py | 229 ++++++++++++- test/unit/objects/networking_test.py | 11 + test/unit/objects/vpc_test.py | 149 +++++++++ 30 files changed, 1618 insertions(+), 67 deletions(-) create mode 100644 linode_api4/groups/vpc.py create mode 100644 linode_api4/objects/serializable.py create mode 100644 linode_api4/objects/vpc.py create mode 100644 test/fixtures/linode_instances_123_configs_456789_interfaces.json create mode 100644 test/fixtures/linode_instances_123_configs_456789_interfaces_123.json create mode 100644 test/fixtures/linode_instances_123_configs_456789_interfaces_123_put.json create mode 100644 test/fixtures/linode_instances_123_configs_456789_interfaces_321.json create mode 100644 test/fixtures/linode_instances_123_configs_456789_interfaces_456.json create mode 100644 test/fixtures/vpcs.json create mode 100644 test/fixtures/vpcs_123456.json create mode 100644 test/fixtures/vpcs_123456_subnets.json create mode 100644 test/fixtures/vpcs_123456_subnets_789.json create mode 100644 test/integration/models/test_vpc.py create mode 100644 test/unit/objects/vpc_test.py diff --git a/linode_api4/groups/__init__.py b/linode_api4/groups/__init__.py index f41f8cb9b..25c4858eb 100644 --- a/linode_api4/groups/__init__.py +++ b/linode_api4/groups/__init__.py @@ -18,3 +18,4 @@ from .support import * from .tag import * from .volume import * +from .vpc import * diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index ae575ed3c..c20a033a3 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -8,6 +8,8 @@ from linode_api4.objects import ( AuthorizedApp, Base, + ConfigInterface, + Firewall, Image, Instance, Kernel, @@ -250,6 +252,8 @@ def instance_create( The contents of this field can be built using the :any:`build_instance_metadata` method. :type metadata: dict + :param firewall: The firewall to attach this Linode to. + :type firewall: int or Firewall :returns: A new Instance object, or a tuple containing the new Instance and the generated password. @@ -284,6 +288,10 @@ def instance_create( ) del kwargs["backup"] + if "firewall" in kwargs: + fw = kwargs.pop("firewall") + kwargs["firewall_id"] = fw.id if isinstance(fw, Firewall) else fw + params = { "type": ltype.id if issubclass(type(ltype), Base) else ltype, "region": region.id if issubclass(type(region), Base) else region, @@ -292,6 +300,7 @@ def instance_create( else None, "authorized_keys": authorized_keys, } + params.update(kwargs) result = self.client.post("/linode/instances", data=params) diff --git a/linode_api4/groups/vpc.py b/linode_api4/groups/vpc.py new file mode 100644 index 000000000..635e392dd --- /dev/null +++ b/linode_api4/groups/vpc.py @@ -0,0 +1,83 @@ +from typing import Any, Dict, List, Optional, Union + +from linode_api4 import VPCSubnet +from linode_api4.errors import UnexpectedResponseError +from linode_api4.groups import Group +from linode_api4.objects import VPC, Base, Region +from linode_api4.paginated_list import PaginatedList + + +class VPCGroup(Group): + def __call__(self, *filters) -> PaginatedList: + """ + Retrieves all of the VPCs the acting user has access to. + + This is intended to be called off of the :any:`LinodeClient` + class, like this:: + + vpcs = client.vpcs() + + API Documentation: TODO + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A list of VPC the acting user can access. + :rtype: PaginatedList of VPC + """ + return self.client._get_and_filter(VPC, *filters) + + def create( + self, + label: str, + region: Union[Region, str], + description: Optional[str] = None, + subnets: Optional[List[Dict[str, Any]]] = None, + **kwargs, + ) -> VPC: + """ + Creates a new VPC under your Linode account. + + API Documentation: TODO + + :param label: The label of the newly created VPC. + :type label: str + :param region: The region of the newly created VPC. + :type region: Union[Region, str] + :param description: The user-defined description of this VPC. + :type description: Optional[str] + :param subnets: A list of subnets to create under this VPC. + :type subnets: List[Dict[str, Any]] + + :returns: The new VPC object. + :rtype: VPC + """ + params = { + "label": label, + "region": region.id if isinstance(region, Region) else region, + } + + if description is not None: + params["description"] = description + + if subnets is not None and len(subnets) > 0: + for subnet in subnets: + if not isinstance(subnet, dict): + raise ValueError( + f"Unsupported type for subnet: {type(subnet)}" + ) + + params["subnets"] = subnets + + params.update(kwargs) + + result = self.client.post("/vpcs", data=params) + + if not "id" in result: + raise UnexpectedResponseError( + "Unexpected response when creating VPC", json=result + ) + + d = VPC(self.client, result["id"], result) + return d diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py index 3a6f2b8b0..a6ceb0178 100644 --- a/linode_api4/linode_client.py +++ b/linode_api4/linode_client.py @@ -28,6 +28,7 @@ SupportGroup, TagGroup, VolumeGroup, + VPCGroup, ) from linode_api4.objects import Image, and_ from linode_api4.objects.filtering import Filter @@ -190,6 +191,9 @@ def __init__( #: Access methods related to Images - See :any:`ImageGroup` for more information. self.images = ImageGroup(self) + #: Access methods related to VPCs - See :any:`VPCGroup` for more information. + self.vpcs = VPCGroup(self) + #: Access methods related to Event polling - See :any:`PollingGroup` for more information. self.polling = PollingGroup(self) diff --git a/linode_api4/objects/__init__.py b/linode_api4/objects/__init__.py index cd02fcc01..f10d4d04f 100644 --- a/linode_api4/objects/__init__.py +++ b/linode_api4/objects/__init__.py @@ -1,6 +1,7 @@ # isort: skip_file from .base import Base, Property, MappedObject, DATE_FORMAT, ExplicitNullValue from .dbase import DerivedBase +from .serializable import JSONObject from .filtering import and_, or_ from .region import Region from .image import Image @@ -17,4 +18,5 @@ from .object_storage import * from .lke import * from .database import * +from .vpc import * from .beta import * diff --git a/linode_api4/objects/base.py b/linode_api4/objects/base.py index ba613c76d..3e42e098a 100644 --- a/linode_api4/objects/base.py +++ b/linode_api4/objects/base.py @@ -1,6 +1,8 @@ import time from datetime import datetime, timedelta +from linode_api4.objects.serializable import JSONObject + from .filtering import FilterableMetaclass DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" @@ -30,6 +32,7 @@ def __init__( id_relationship=False, slug_relationship=False, nullable=False, + json_object=None, ): """ A Property is an attribute returned from the API, and defines metadata @@ -56,6 +59,8 @@ def __init__( self.is_datetime = is_datetime self.id_relationship = id_relationship self.slug_relationship = slug_relationship + self.nullable = nullable + self.json_class = json_object class MappedObject: @@ -111,7 +116,7 @@ class Base(object, metaclass=FilterableMetaclass): properties = {} - def __init__(self, client, id, json={}): + def __init__(self, client: object, id: object, json: object = {}) -> object: self._set("_populated", False) self._set("_last_updated", datetime.min) self._set("_client", client) @@ -123,8 +128,8 @@ def __init__(self, client, id, json={}): #: be updated on access. self._set("_raw_json", None) - for prop in type(self).properties: - self._set(prop, None) + for k in type(self).properties: + self._set(k, None) self._set("id", id) if hasattr(type(self), "id_attribute"): @@ -289,7 +294,7 @@ def _serialize(self): value = getattr(self, k) - if not value: + if not v.nullable and (value is None or value == ""): continue # Let's allow explicit null values as both classes and instances @@ -305,7 +310,7 @@ def _serialize(self): for k, v in result.items(): if isinstance(v, Base): result[k] = v.id - elif isinstance(v, MappedObject): + elif isinstance(v, MappedObject) or issubclass(type(v), JSONObject): result[k] = v.dict return result @@ -376,6 +381,18 @@ def _populate(self, json): .properties[key] .slug_relationship(self._client, json[key]), ) + elif type(self).properties[key].json_class: + json_class = type(self).properties[key].json_class + json_value = json[key] + + # build JSON object + if isinstance(json_value, list): + # We need special handling for list responses + value = [json_class.from_json(v) for v in json_value] + else: + value = json_class.from_json(json_value) + + self._set(key, value) elif type(json[key]) is dict: self._set(key, MappedObject(**json[key])) elif type(json[key]) is list: diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index d928b31b6..6ddbd9fe2 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1,18 +1,28 @@ import string import sys +from dataclasses import dataclass from datetime import datetime from enum import Enum from os import urandom from random import randint +from typing import Any, Dict, List, Optional, Union from urllib import parse from linode_api4 import util from linode_api4.common import load_and_validate_keys from linode_api4.errors import UnexpectedResponseError -from linode_api4.objects import Base, DerivedBase, Image, Property, Region +from linode_api4.objects import ( + Base, + DerivedBase, + Image, + JSONObject, + Property, + Region, +) from linode_api4.objects.base import MappedObject from linode_api4.objects.filtering import FilterableAttribute from linode_api4.objects.networking import IPAddress, IPv6Range +from linode_api4.objects.vpc import VPC, VPCSubnet from linode_api4.paginated_list import PaginatedList PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation @@ -257,42 +267,133 @@ def _populate(self, json): type_class = FilterableAttribute("class") -class ConfigInterface: +@dataclass +class ConfigInterfaceIPv4(JSONObject): + vpc: str = "" + nat_1_1: str = "" + + +class NetworkInterface(DerivedBase): """ - This is a helper class used to populate 'interfaces' in the Config calss - below. + This class represents a Configuration Profile's network interface object. + NOTE: This class cannot be used for the `interfaces` attribute on Config + POST and PUT requests. + + API Documentation: TODO """ - def __init__(self, purpose, label="", ipam_address=""): + api_endpoint = ( + "/linode/instances/{instance_id}/configs/{config_id}/interfaces/{id}" + ) + derived_url_path = "interfaces" + parent_id_name = "config_id" + + properties = { + "id": Property(identifier=True), + "purpose": Property(), + "label": Property(), + "ipam_address": Property(), + "primary": Property(mutable=True), + "active": Property(), + "vpc_id": Property(id_relationship=VPC), + "subnet_id": Property(), + "ipv4": Property(mutable=True, json_object=ConfigInterfaceIPv4), + "ip_ranges": Property(mutable=True), + } + + def __init__(self, client, id, parent_id, instance_id=None, json=None): """ - Creates a new ConfigInterface + We need a special constructor here because this object's parent + has a parent itself. """ - #: The Label for the VLAN this interface is connected to. Blank for public - #: interfaces. - self.label = label + if not instance_id and not isinstance(parent_id, tuple): + raise ValueError( + "ConfigInterface must either be created with a instance_id or a tuple of " + "(config_id, instance_id) for parent_id!" + ) + + if isinstance(parent_id, tuple): + instance_id = parent_id[1] + parent_id = parent_id[0] - #: The IPAM Address this interface will bring up. Blank for public interfaces. - self.ipam_address = ipam_address + DerivedBase.__init__(self, client, id, parent_id, json=json) - #: The purpose of this interface. "public" means this interface can access - #: the internet, "vlan" means it is a VLAN interface. - self.purpose = purpose + self._set("instance_id", instance_id) def __repr__(self): - if self.purpose == "public": - return "Public Interface" - return "Interface {}; purpose: {}; ipam_address: {}".format( - self.label, self.purpose, self.ipam_address - ) + return f"Interface: {self.purpose} {self.id}" - def _serialize(self): + @property + def subnet(self) -> VPCSubnet: """ - Returns this object as a dict + Get the subnet this VPC is referencing. + + :returns: The VPCSubnet associated with this interface. + :rtype: VPCSubnet """ + return VPCSubnet(self._client, self.subnet_id, self.vpc_id) + + +@dataclass +class ConfigInterface(JSONObject): + """ + Represents a single interface in a Configuration Profile. + This class only contains data about a config interface. + If you would like to access a config interface directly, + consider using :any:`NetworkInterface`. + + API Documentation: TODO + """ + + purpose: str = "public" + + # Public/VPC-specific + primary: Optional[bool] = None + + # VLAN-specific + label: Optional[str] = None + ipam_address: Optional[str] = None + + # VPC-specific + vpc_id: Optional[int] = None + subnet_id: Optional[int] = None + ipv4: Optional[Union[ConfigInterfaceIPv4, Dict[str, Any]]] = None + ip_ranges: Optional[List[str]] = None + + # Computed + id: int = 0 + + def __repr__(self): + return f"Interface: {self.purpose}" + + def _serialize(self): + purpose_formats = { + "public": {"purpose": "public", "primary": self.primary}, + "vlan": { + "purpose": "vlan", + "label": self.label, + "ipam_address": self.ipam_address, + }, + "vpc": { + "purpose": "vpc", + "primary": self.primary, + "subnet_id": self.subnet_id, + "ipv4": self.ipv4.dict + if isinstance(self.ipv4, ConfigInterfaceIPv4) + else self.ipv4, + "ip_ranges": self.ip_ranges, + }, + } + + if self.purpose not in purpose_formats: + raise ValueError( + f"Unknown interface purpose: {self.purpose}", + ) + return { - "label": self.label, - "ipam_address": self.ipam_address, - "purpose": self.purpose, + k: v + for k, v in purpose_formats[self.purpose].items() + if v is not None } @@ -310,7 +411,7 @@ class Config(DerivedBase): properties = { "id": Property(identifier=True), "linode_id": Property(identifier=True), - "helpers": Property(), # TODO: mutable=True), + "helpers": Property(mutable=True), "created": Property(is_datetime=True), "root_device": Property(mutable=True), "kernel": Property(relationship=Kernel, mutable=True), @@ -322,14 +423,33 @@ class Config(DerivedBase): "run_level": Property(mutable=True), "virt_mode": Property(mutable=True), "memory_limit": Property(mutable=True), - "interfaces": Property(mutable=True), # gets setup in _populate below - "helpers": Property(mutable=True), + "interfaces": Property(mutable=True, json_object=ConfigInterface), } + @property + def network_interfaces(self): + """ + Returns the Network Interfaces for this Configuration Profile. + This differs from the `interfaces` field as each NetworkInterface + object is treated as its own API object. + + API Documentation: TODO + """ + + return [ + NetworkInterface( + self._client, v.id, self.id, instance_id=self.linode_id + ) + for v in self.interfaces + ] + def _populate(self, json): """ Map devices more nicely while populating. """ + if json is None or len(json) < 1: + return + # needed here to avoid circular imports from .volume import Volume # pylint: disable=import-outside-toplevel @@ -354,19 +474,6 @@ def _populate(self, json): self._set("devices", MappedObject(**devices)) - interfaces = [] - if "interfaces" in json: - interfaces = [ - ConfigInterface( - c["purpose"], - label=c["label"], - ipam_address=c["ipam_address"], - ) - for c in json["interfaces"] - ] - - self._set("interfaces", interfaces) - def _serialize(self): """ Overrides _serialize to transform interfaces into json @@ -383,6 +490,127 @@ def _serialize(self): partial["interfaces"] = interfaces return partial + def interface_create_public(self, primary=False) -> NetworkInterface: + """ + Creates a public interface for this Configuration Profile. + + API Documentation: TODO + + :param primary: Whether this interface is a primary interface. + :type primary: bool + + :returns: The newly created NetworkInterface. + :rtype: NetworkInterface + + """ + return self._interface_create({"purpose": "public", "primary": primary}) + + def interface_create_vlan( + self, label: str, ipam_address=None + ) -> NetworkInterface: + """ + Creates a VLAN interface for this Configuration Profile. + + API Documentation: TODO + + :param label: The label of the VLAN to associate this interface with. + :type label: str + :param ipam_address: The IPAM address of this interface for the associated VLAN. + :type ipam_address: str + + :returns: The newly created NetworkInterface. + :rtype: NetworkInterface + """ + params = { + "purpose": "vlan", + "label": label, + } + if ipam_address is not None: + params["ipam_address"] = ipam_address + + return self._interface_create(params) + + def interface_create_vpc( + self, + subnet: Union[int, VPCSubnet], + primary=False, + ipv4: Union[Dict[str, Any], ConfigInterfaceIPv4] = None, + ip_ranges: Optional[List[str]] = None, + ) -> NetworkInterface: + """ + Creates a VPC interface for this Configuration Profile. + + API Documentation: TODO + + :param subnet: The VPC subnet to associate this interface with. + :type subnet: int or VPCSubnet + :param primary: Whether this is a primary interface. + :type primary: bool + :param ipv4: The IPv4 configuration of the interface for the associated subnet. + :type ipv4: Dict or ConfigInterfaceIPv4 + :param ip_ranges: A list of IPs or IP ranges in the VPC subnet. + Packets to these CIDRs are routed through the + VPC network interface. + :type ip_ranges: List of str + + :returns: The newly created NetworkInterface. + :rtype: NetworkInterface + """ + params = { + "purpose": "vpc", + "subnet_id": subnet.id if isinstance(subnet, VPCSubnet) else subnet, + "primary": primary, + } + + if ipv4 is not None: + params["ipv4"] = ( + ipv4.dict if isinstance(ipv4, ConfigInterfaceIPv4) else ipv4 + ) + + if ip_ranges is not None: + params["ip_ranges"] = ip_ranges + + return self._interface_create(params) + + def interface_reorder(self, interfaces: List[Union[int, NetworkInterface]]): + """ + Change the order of the interfaces for this Configuration Profile. + + API Documentation: TODO + + :param interfaces: A list of interfaces in the desired order. + :type interfaces: List of str or NetworkInterface + """ + ids = [ + v.id if isinstance(v, NetworkInterface) else v for v in interfaces + ] + + self._client.post( + "{}/interfaces/order".format(Config.api_endpoint), + model=self, + data={"ids": ids}, + ) + self.invalidate() + + def _interface_create(self, body: Dict[str, Any]) -> NetworkInterface: + """ + The underlying ConfigInterface creation API call. + """ + result = self._client.post( + "{}/interfaces".format(Config.api_endpoint), model=self, data=body + ) + self.invalidate() + + if not "id" in result: + raise UnexpectedResponseError( + "Unexpected response creating Interface", json=result + ) + + i = NetworkInterface( + self._client, result["id"], self.id, self.linode_id, result + ) + return i + class Instance(Base): """ @@ -790,6 +1018,7 @@ def config_create( devices=[], disks=[], volumes=[], + interfaces=[], **kwargs, ): """ @@ -863,12 +1092,19 @@ def config_create( else: raise TypeError("Disk or Volume expected!") + param_interfaces = [] + for interface in interfaces: + if isinstance(interface, ConfigInterface): + interface = interface._serialize() + param_interfaces.append(interface) + params = { "kernel": kernel.id if issubclass(type(kernel), Base) else kernel, "label": label if label else "{}_config_{}".format(self.label, len(self.configs)), "devices": device_map, + "interfaces": param_interfaces, } params.update(kwargs) diff --git a/linode_api4/objects/networking.py b/linode_api4/objects/networking.py index 433c318f6..1b0e46994 100644 --- a/linode_api4/objects/networking.py +++ b/linode_api4/objects/networking.py @@ -1,5 +1,7 @@ +from dataclasses import dataclass + from linode_api4.errors import UnexpectedResponseError -from linode_api4.objects import Base, DerivedBase, Property, Region +from linode_api4.objects import Base, DerivedBase, JSONObject, Property, Region class IPv6Pool(Base): @@ -36,6 +38,18 @@ class IPv6Range(Base): } +@dataclass +class InstanceIPNAT1To1(JSONObject): + """ + InstanceIPNAT1To1 contains information about the NAT 1:1 mapping + of VPC IP together with the VPC and subnet ids. + """ + + address: str = "" + subnet_id: int = 0 + vpc_id: int = 0 + + class IPAddress(Base): """ note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`. @@ -67,6 +81,7 @@ class IPAddress(Base): "rdns": Property(mutable=True), "linode_id": Property(), "region": Property(slug_relationship=Region), + "vpc_nat_1_1": Property(json_object=InstanceIPNAT1To1), } @property diff --git a/linode_api4/objects/serializable.py b/linode_api4/objects/serializable.py new file mode 100644 index 000000000..d0cf63282 --- /dev/null +++ b/linode_api4/objects/serializable.py @@ -0,0 +1,102 @@ +import inspect +from dataclasses import asdict, dataclass +from typing import Any, Dict, Optional, get_args, get_origin, get_type_hints + + +@dataclass +class JSONObject: + """ + A simple helper class for serializable API objects. + This is typically used for nested object values. + + This class act similarly to MappedObject but with explicit + fields and static typing. + """ + + def __init__(self): + raise NotImplementedError( + "JSONObject is not intended to be constructed directly" + ) + + # TODO: Implement __repr__ + + @staticmethod + def _try_from_json(json_value: Any, field_type: type): + """ + Determines whether a JSON dict is an instance of a field type. + """ + if inspect.isclass(field_type) and issubclass(field_type, JSONObject): + return field_type.from_json(json_value) + return json_value + + @classmethod + def _parse_attr_list(cls, json_value, field_type): + """ + Attempts to parse a list attribute with a given value and field type. + """ + + type_hint_args = get_args(field_type) + + if len(type_hint_args) < 1: + return cls._try_from_json(json_value, field_type) + + return [ + cls._try_from_json(item, type_hint_args[0]) for item in json_value + ] + + @classmethod + def _parse_attr(cls, json_value, field_type): + """ + Attempts to parse an attribute with a given value and field type. + """ + + if list in (field_type, get_origin(field_type)): + return cls._parse_attr_list(json_value, field_type) + + return cls._try_from_json(json_value, field_type) + + @classmethod + def from_json(cls, json: Dict[str, Any]) -> Optional["JSONObject"]: + """ + Creates an instance of this class from a JSON dict. + """ + if json is None: + return None + + obj = cls() + + type_hints = get_type_hints(cls) + + for k in vars(obj): + setattr(obj, k, cls._parse_attr(json.get(k), type_hints.get(k))) + + return obj + + def _serialize(self) -> Dict[str, Any]: + """ + Serializes this object into a JSON dict. + """ + return asdict(self) + + @property + def dict(self) -> Dict[str, Any]: + """ + Alias for JSONObject._serialize() + """ + return self._serialize() + + # Various dict methods for backwards compat + def __getitem__(self, key) -> Any: + return getattr(self, key) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def __iter__(self) -> Any: + return vars(self) + + def __delitem__(self, key): + setattr(self, key, None) + + def __len__(self): + return len(vars(self)) diff --git a/linode_api4/objects/vpc.py b/linode_api4/objects/vpc.py new file mode 100644 index 000000000..989c542ee --- /dev/null +++ b/linode_api4/objects/vpc.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass +from typing import List, Optional + +from linode_api4.errors import UnexpectedResponseError +from linode_api4.objects import Base, DerivedBase, Property, Region +from linode_api4.objects.serializable import JSONObject + + +@dataclass +class VPCSubnetLinodeInterface(JSONObject): + id: int = 0 + active: bool = False + + +@dataclass +class VPCSubnetLinode(JSONObject): + id: int = 0 + interfaces: List[VPCSubnetLinodeInterface] = None + + +class VPCSubnet(DerivedBase): + """ + An instance of a VPC subnet. + + API Documentation: TODO + """ + + api_endpoint = "/vpcs/{vpc_id}/subnets/{id}" + derived_url_path = "subnets" + parent_id_name = "vpc_id" + + properties = { + "id": Property(identifier=True), + "label": Property(mutable=True), + "ipv4": Property(), + "linodes": Property(json_object=VPCSubnetLinode), + "created": Property(is_datetime=True), + "updated": Property(is_datetime=True), + } + + +class VPC(Base): + """ + An instance of a VPC. + + API Documentation: TODO + """ + + api_endpoint = "/vpcs/{id}" + + properties = { + "id": Property(identifier=True), + "label": Property(mutable=True), + "description": Property(mutable=True), + "region": Property(slug_relationship=Region), + "subnets": Property(derived_class=VPCSubnet), + "created": Property(is_datetime=True), + "updated": Property(is_datetime=True), + } + + def subnet_create( + self, + label: str, + ipv4: Optional[str] = None, + **kwargs, + ) -> VPCSubnet: + """ + Creates a new Subnet object under this VPC. + + API Documentation: TODO + + :param label: The label of this subnet. + :type label: str + :param ipv4: The IPv4 range of this subnet in CIDR format. + :type ipv4: str + :param ipv6: The IPv6 range of this subnet in CIDR format. + :type ipv6: str + """ + params = { + "label": label, + } + + if ipv4 is not None: + params["ipv4"] = ipv4 + + params.update(kwargs) + + result = self._client.post( + "{}/subnets".format(VPC.api_endpoint), model=self, data=params + ) + self.invalidate() + + if not "id" in result: + raise UnexpectedResponseError( + "Unexpected response creating Subnet", json=result + ) + + d = VPCSubnet(self._client, result["id"], self.id, result) + return d diff --git a/test/fixtures/linode_instances_123_configs.json b/test/fixtures/linode_instances_123_configs.json index a45ef1dd8..581b84caa 100644 --- a/test/fixtures/linode_instances_123_configs.json +++ b/test/fixtures/linode_instances_123_configs.json @@ -16,9 +16,31 @@ "id": 456789, "interfaces": [ { - "ipam_address": "0.0.0.0/24", - "label": "test-interface", - "purpose": "vlan" + "id": 456, + "purpose": "public", + "primary": true + }, + { + "id": 123, + "purpose": "vpc", + "primary": true, + "active": true, + "vpc_id": 123456, + "subnet_id": 789, + "ipv4": { + "vpc": "10.0.0.2", + "nat_1_1": "any" + }, + "ip_ranges": [ + "10.0.0.0/24" + ] + }, + { + "id": 321, + "primary": false, + "ipam_address":"10.0.0.2", + "label":"test-interface", + "purpose":"vlan" } ], "run_level": "default", diff --git a/test/fixtures/linode_instances_123_configs_456789.json b/test/fixtures/linode_instances_123_configs_456789.json index b19cba3af..93e41f86b 100644 --- a/test/fixtures/linode_instances_123_configs_456789.json +++ b/test/fixtures/linode_instances_123_configs_456789.json @@ -12,9 +12,31 @@ "created":"2014-10-07T20:04:00", "memory_limit":0, "id":456789, - "interfaces":[ + "interfaces": [ { - "ipam_address":"0.0.0.0/24", + "id": 456, + "purpose": "public", + "primary": true + }, + { + "id": 123, + "purpose": "vpc", + "primary": true, + "active": true, + "vpc_id": 123456, + "subnet_id": 789, + "ipv4": { + "vpc": "10.0.0.2", + "nat_1_1": "any" + }, + "ip_ranges": [ + "10.0.0.0/24" + ] + }, + { + "id": 321, + "primary": false, + "ipam_address":"10.0.0.2", "label":"test-interface", "purpose":"vlan" } diff --git a/test/fixtures/linode_instances_123_configs_456789_interfaces.json b/test/fixtures/linode_instances_123_configs_456789_interfaces.json new file mode 100644 index 000000000..86c709071 --- /dev/null +++ b/test/fixtures/linode_instances_123_configs_456789_interfaces.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": 456, + "purpose": "public", + "primary": true + }, + { + "id": 123, + "purpose": "vpc", + "primary": true, + "active": true, + "vpc_id": 123456, + "subnet_id": 789, + "ipv4": { + "vpc": "10.0.0.2", + "nat_1_1": "any" + }, + "ip_ranges": [ + "10.0.0.0/24" + ] + }, + { + "id": 321, + "primary": false, + "ipam_address":"10.0.0.2", + "label":"test-interface", + "purpose":"vlan" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/fixtures/linode_instances_123_configs_456789_interfaces_123.json b/test/fixtures/linode_instances_123_configs_456789_interfaces_123.json new file mode 100644 index 000000000..d02673aeb --- /dev/null +++ b/test/fixtures/linode_instances_123_configs_456789_interfaces_123.json @@ -0,0 +1,15 @@ +{ + "id": 123, + "purpose": "vpc", + "primary": true, + "active": true, + "vpc_id": 123456, + "subnet_id": 789, + "ipv4": { + "vpc": "10.0.0.2", + "nat_1_1": "any" + }, + "ip_ranges": [ + "10.0.0.0/24" + ] +} \ No newline at end of file diff --git a/test/fixtures/linode_instances_123_configs_456789_interfaces_123_put.json b/test/fixtures/linode_instances_123_configs_456789_interfaces_123_put.json new file mode 100644 index 000000000..684e26cf0 --- /dev/null +++ b/test/fixtures/linode_instances_123_configs_456789_interfaces_123_put.json @@ -0,0 +1,14 @@ +{ + "id": 123, + "purpose": "vpc", + "primary": false, + "vpc_id": 123456, + "subnet_id": 789, + "ipv4": { + "vpc": "10.0.0.3", + "nat_1_1": "any" + }, + "ip_ranges": [ + "10.0.0.0/24" + ] +} \ No newline at end of file diff --git a/test/fixtures/linode_instances_123_configs_456789_interfaces_321.json b/test/fixtures/linode_instances_123_configs_456789_interfaces_321.json new file mode 100644 index 000000000..d41133eb2 --- /dev/null +++ b/test/fixtures/linode_instances_123_configs_456789_interfaces_321.json @@ -0,0 +1,7 @@ +{ + "id": 321, + "primary": false, + "ipam_address":"10.0.0.2", + "label":"test-interface", + "purpose":"vlan" +} \ No newline at end of file diff --git a/test/fixtures/linode_instances_123_configs_456789_interfaces_456.json b/test/fixtures/linode_instances_123_configs_456789_interfaces_456.json new file mode 100644 index 000000000..94c7bc339 --- /dev/null +++ b/test/fixtures/linode_instances_123_configs_456789_interfaces_456.json @@ -0,0 +1,5 @@ +{ + "id": 456, + "purpose": "public", + "primary": true +} \ No newline at end of file diff --git a/test/fixtures/networking_ips_127.0.0.1.json b/test/fixtures/networking_ips_127.0.0.1.json index f6567ebd5..9d3cfb449 100644 --- a/test/fixtures/networking_ips_127.0.0.1.json +++ b/test/fixtures/networking_ips_127.0.0.1.json @@ -7,5 +7,10 @@ "rdns": "test.example.org", "region": "us-east", "subnet_mask": "255.255.255.0", - "type": "ipv4" + "type": "ipv4", + "vpc_nat_1_1": { + "vpc_id": 242, + "subnet_id": 194, + "address": "139.144.244.36" + } } \ No newline at end of file diff --git a/test/fixtures/vpcs.json b/test/fixtures/vpcs.json new file mode 100644 index 000000000..9a7cc5038 --- /dev/null +++ b/test/fixtures/vpcs.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "label": "test-vpc", + "id": 123456, + "description": "A very real VPC.", + "region": "us-southeast", + "created": "2018-01-01T00:01:01", + "updated": "2018-01-01T00:01:01" + } + ], + "results": 1, + "page": 1, + "pages": 1 +} diff --git a/test/fixtures/vpcs_123456.json b/test/fixtures/vpcs_123456.json new file mode 100644 index 000000000..e4c16437a --- /dev/null +++ b/test/fixtures/vpcs_123456.json @@ -0,0 +1,8 @@ +{ + "label": "test-vpc", + "id": 123456, + "description": "A very real VPC.", + "region": "us-southeast", + "created": "2018-01-01T00:01:01", + "updated": "2018-01-01T00:01:01" +} \ No newline at end of file diff --git a/test/fixtures/vpcs_123456_subnets.json b/test/fixtures/vpcs_123456_subnets.json new file mode 100644 index 000000000..f846399df --- /dev/null +++ b/test/fixtures/vpcs_123456_subnets.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "label": "test-subnet", + "id": 789, + "ipv4": "10.0.0.0/24", + "linodes": [ + { + "id": 12345, + "interfaces": [ + { + "id": 678, + "active": true + }, + { + "id": 543, + "active": false + } + ] + } + ], + "created": "2018-01-01T00:01:01", + "updated": "2018-01-01T00:01:01" + } + ], + "results": 1, + "page": 1, + "pages": 1 +} \ No newline at end of file diff --git a/test/fixtures/vpcs_123456_subnets_789.json b/test/fixtures/vpcs_123456_subnets_789.json new file mode 100644 index 000000000..ba6973472 --- /dev/null +++ b/test/fixtures/vpcs_123456_subnets_789.json @@ -0,0 +1,22 @@ +{ + "label": "test-subnet", + "id": 789, + "ipv4": "10.0.0.0/24", + "linodes": [ + { + "id": 12345, + "interfaces": [ + { + "id": 678, + "active": true + }, + { + "id": 543, + "active": false + } + ] + } + ], + "created": "2018-01-01T00:01:01", + "updated": "2018-01-01T00:01:01" +} \ No newline at end of file diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 25bae0710..a3e4b12e4 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -1,13 +1,17 @@ import os +import random import time +from typing import Set import pytest from linode_api4 import ApiError from linode_api4.linode_client import LinodeClient +from linode_api4.objects import Region ENV_TOKEN_NAME = "LINODE_TOKEN" ENV_API_URL_NAME = "LINODE_API_URL" +ENV_REGION_OVERRIDE = "LINODE_TEST_REGION_OVERRIDE" ENV_API_CA_NAME = "LINODE_API_CA" RUN_LONG_TESTS = "RUN_LONG_TESTS" @@ -20,6 +24,23 @@ def get_api_url(): return os.environ.get(ENV_API_URL_NAME, "https://api.linode.com/v4beta") +def get_region(client: LinodeClient, capabilities: Set[str] = None): + region_override = os.environ.get(ENV_REGION_OVERRIDE) + + # Allow overriding the target test region + if region_override is not None: + return Region(client, region_override) + + regions = client.regions() + + if capabilities is not None: + regions = [ + v for v in regions if set(capabilities).issubset(v.capabilities) + ] + + return random.choice(regions) + + def get_api_ca_file(): result = os.environ.get(ENV_API_CA_NAME, None) return result if result != "" else None @@ -136,7 +157,9 @@ def test_volume(test_linode_client): timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp - volume = client.volume_create(label=label, region="ap-west") + volume = client.volume_create( + label=label, region=get_region(client, {"Block Storage"}) + ) yield volume @@ -177,7 +200,9 @@ def test_nodebalancer(test_linode_client): timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp - nodebalancer = client.nodebalancer_create(region="us-east", label=label) + nodebalancer = client.nodebalancer_create( + region=get_region(client), label=label + ) yield nodebalancer @@ -247,3 +272,89 @@ def test_oauth_client(test_linode_client): yield oauth_client oauth_client.delete() + + +@pytest.fixture(scope="session") +def create_vpc(test_linode_client): + client = test_linode_client + + timestamp = str(int(time.time())) + + vpc = client.vpcs.create( + "pythonsdk-" + timestamp, + get_region(test_linode_client, {"VPCs"}), + 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("test-subnet", ipv4="10.0.0.0/24") + + yield create_vpc, subnet + + subnet.delete() + + +@pytest.fixture(scope="session") +def create_vpc_with_subnet_and_linode( + test_linode_client, create_vpc_with_subnet +): + vpc, subnet = create_vpc_with_subnet + + timestamp = str(int(time.time())) + label = "TestSDK-" + timestamp + + instance, password = test_linode_client.linode.instance_create( + "g5-standard-4", vpc.region, image="linode/debian11", label=label + ) + + yield vpc, subnet, instance, password + + instance.delete() + + +@pytest.fixture(scope="session") +def create_vpc(test_linode_client): + client = test_linode_client + + timestamp = str(int(time.time_ns() % 10**10)) + + vpc = client.vpcs.create( + "pythonsdk-" + timestamp, + get_region(test_linode_client, {"VPCs"}), + description="test description", + ) + yield vpc + + vpc.delete() + + +@pytest.fixture(scope="session") +def create_multiple_vpcs(test_linode_client): + client = test_linode_client + + timestamp = str(int(time.time_ns() % 10**10)) + + timestamp_2 = str(int(time.time_ns() % 10**10)) + + vpc_1 = client.vpcs.create( + "pythonsdk-" + timestamp, + get_region(test_linode_client, {"VPCs"}), + description="test description", + ) + + vpc_2 = client.vpcs.create( + "pythonsdk-" + timestamp_2, + get_region(test_linode_client, {"VPCs"}), + description="test description", + ) + + yield vpc_1, vpc_2 + + vpc_1.delete() + + vpc_2.delete() diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 9f6f76d65..5b68fac34 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -8,7 +8,15 @@ import pytest from linode_api4.errors import ApiError -from linode_api4.objects import Config, Disk, Image, Instance, Type +from linode_api4.objects import ( + Config, + ConfigInterface, + ConfigInterfaceIPv4, + Disk, + Image, + Instance, + Type, +) @pytest.fixture(scope="session") @@ -358,19 +366,24 @@ def test_disk_resize(): def test_config_update_interfaces(create_linode): linode = create_linode + config = linode.configs[0] + new_interfaces = [ {"purpose": "public"}, - {"purpose": "vlan", "label": "cool-vlan"}, + ConfigInterface( + purpose="vlan", label="cool-vlan", ipam_address="10.0.0.4/32" + ), ] - - config = linode.configs[0] - config.interfaces = new_interfaces res = config.save() + config.invalidate() assert res - assert "cool-vlan" in str(config.interfaces) + assert config.interfaces[0].purpose == "public" + assert config.interfaces[1].purpose == "vlan" + assert config.interfaces[1].label == "cool-vlan" + assert config.interfaces[1].ipam_address == "10.0.0.4/32" def test_get_config(test_linode_client, create_linode): @@ -448,3 +461,160 @@ def test_save_linode_force(test_linode_client, create_linode): linode = test_linode_client.load(Instance, linode.id) assert old_label != linode.label + + +class TestNetworkInterface: + def test_list(self, create_linode): + linode = create_linode + + config: Config = linode.configs[0] + + config.interface_create_public( + primary=True, + ) + config.interface_create_vlan( + label="testvlan", ipam_address="10.0.0.3/32" + ) + + interface = config.network_interfaces + + assert interface[0].purpose == "public" + assert interface[0].primary + assert interface[1].purpose == "vlan" + assert interface[1].label == "testvlan" + assert interface[1].ipam_address == "10.0.0.3/32" + + def test_create_public(self, create_linode): + linode = create_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + interface = config.interface_create_public( + primary=True, + ) + + config.invalidate() + + assert interface.id == config.interfaces[0].id + assert interface.purpose == "public" + assert interface.primary + + def test_create_vlan(self, create_linode): + linode = create_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + interface = config.interface_create_vlan( + label="testvlan", ipam_address="10.0.0.2/32" + ) + + config.invalidate() + + assert interface.id == config.interfaces[0].id + assert interface.purpose == "vlan" + assert interface.label == "testvlan" + assert interface.ipam_address == "10.0.0.2/32" + + def test_create_vpc(self, create_linode, create_vpc_with_subnet_and_linode): + vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + interface = config.interface_create_vpc( + subnet=subnet, + primary=True, + ipv4=ConfigInterfaceIPv4(vpc="10.0.0.2", nat_1_1="any"), + ip_ranges=["10.0.0.5/32"], + ) + + config.invalidate() + + assert interface.id == config.interfaces[0].id + assert interface.subnet.id == subnet.id + assert interface.purpose == "vpc" + assert interface.ipv4.vpc == "10.0.0.2" + assert interface.ipv4.nat_1_1 == linode.ipv4[0] + assert interface.ip_ranges == ["10.0.0.5/32"] + + def test_update_vpc(self, create_linode, create_vpc_with_subnet_and_linode): + vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + interface = config.interface_create_vpc( + subnet=subnet, + primary=True, + ip_ranges=["10.0.0.5/32"], + ) + + interface.primary = False + interface.ip_ranges = ["10.0.0.6/32"] + interface.ipv4.vpc = "10.0.0.3" + interface.ipv4.nat_1_1 = "any" + + interface.save() + interface.invalidate() + config.invalidate() + + assert interface.id == config.interfaces[0].id + assert interface.subnet.id == subnet.id + assert interface.purpose == "vpc" + assert interface.ipv4.vpc == "10.0.0.3" + assert interface.ipv4.nat_1_1 == linode.ipv4[0] + assert interface.ip_ranges == ["10.0.0.6/32"] + + def test_reorder(self, create_linode): + linode = create_linode + + config: Config = linode.configs[0] + + pub_interface = config.interface_create_public( + primary=True, + ) + vlan_interface = config.interface_create_vlan( + label="testvlan", ipam_address="10.0.0.3/32" + ) + + interfaces = config.network_interfaces + interfaces.reverse() + + config.interface_reorder(interfaces) + config.invalidate() + + assert [v.id for v in config.interfaces] == [ + vlan_interface.id, + pub_interface.id, + ] + + def test_delete_interface_containing_vpc( + self, create_vpc_with_subnet_and_linode + ): + vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + interface = config.interface_create_vpc( + subnet=subnet, + primary=True, + ip_ranges=["10.0.0.8/32"], + ) + + result = interface.delete() + + # returns true when delete successful + assert result diff --git a/test/integration/models/test_networking.py b/test/integration/models/test_networking.py index 95bc2196b..4bd994f38 100644 --- a/test/integration/models/test_networking.py +++ b/test/integration/models/test_networking.py @@ -2,7 +2,7 @@ import pytest -from linode_api4.objects import Firewall +from linode_api4.objects import Config, ConfigInterfaceIPv4, Firewall, IPAddress @pytest.mark.smoke @@ -23,7 +23,7 @@ def create_linode(test_linode_client): chosen_region = available_regions[0] label = get_rand_nanosec_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance, _ = client.linode.instance_create( "g6-nanode-1", chosen_region, image="linode/debian12", @@ -97,3 +97,27 @@ def test_ip_addresses_unshare( test_linode_client.networking.ip_addresses_share([], linode_instance2.id) assert [] == linode_instance2.ips.ipv4.shared + + +def test_ip_info_vpc(test_linode_client, create_vpc_with_subnet_and_linode): + vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + _ = config.interface_create_vpc( + subnet=subnet, + primary=True, + ipv4=ConfigInterfaceIPv4(vpc="10.0.0.2", nat_1_1="any"), + ip_ranges=["10.0.0.5/32"], + ) + + config.invalidate() + + ip_info = test_linode_client.load(IPAddress, linode.ipv4[0]) + + assert ip_info.vpc_nat_1_1.address == "10.0.0.2" + assert ip_info.vpc_nat_1_1.vpc_id == vpc.id + assert ip_info.vpc_nat_1_1.subnet_id == subnet.id diff --git a/test/integration/models/test_vpc.py b/test/integration/models/test_vpc.py new file mode 100644 index 000000000..6af3380b7 --- /dev/null +++ b/test/integration/models/test_vpc.py @@ -0,0 +1,100 @@ +from test.integration.conftest import get_region + +import pytest + +from linode_api4 import VPC, ApiError, VPCSubnet + + +def test_get_vpc(test_linode_client, create_vpc): + vpc = test_linode_client.load(VPC, create_vpc.id) + test_linode_client.vpcs() + assert vpc.id == create_vpc.id + + +def test_update_vpc(test_linode_client, create_vpc): + vpc = create_vpc + new_label = create_vpc.label + "-updated" + new_desc = "updated description" + + vpc.label = new_label + vpc.description = new_desc + vpc.save() + + vpc = test_linode_client.load(VPC, create_vpc.id) + + assert vpc.label == new_label + assert vpc.description == new_desc + + +def test_get_subnet(test_linode_client, create_vpc_with_subnet): + vpc, subnet = create_vpc_with_subnet + loaded_subnet = test_linode_client.load(VPCSubnet, subnet.id, vpc.id) + + assert loaded_subnet.id == subnet.id + + +def test_update_subnet(test_linode_client, create_vpc_with_subnet): + vpc, subnet = create_vpc_with_subnet + new_label = subnet.label + "-updated" + + subnet.label = new_label + subnet.save() + + subnet = test_linode_client.load(VPCSubnet, subnet.id, vpc.id) + + assert subnet.label == new_label + + +def test_fails_create_vpc_invalid_data(test_linode_client): + with pytest.raises(ApiError) as excinfo: + test_linode_client.vpcs.create( + label="invalid_label!!", + region=get_region(test_linode_client, {"VPCs"}), + description="test description", + ) + assert excinfo.value.status == 400 + assert "Label must include only ASCII" in str(excinfo.value.json) + + +def test_get_all_vpcs(test_linode_client, create_multiple_vpcs): + vpc_1, vpc_2 = create_multiple_vpcs + + all_vpcs = test_linode_client.vpcs() + + assert str(vpc_1) in str(all_vpcs.lists) + assert str(vpc_2) in str(all_vpcs.lists) + + +def test_fails_update_vpc_invalid_data(create_vpc): + vpc = create_vpc + + invalid_label = "invalid!!" + vpc.label = invalid_label + + with pytest.raises(ApiError) as excinfo: + vpc.save() + + assert excinfo.value.status == 400 + assert "Label must include only ASCII" in str(excinfo.value.json) + + +def test_fails_create_subnet_invalid_data(create_vpc): + invalid_ipv4 = "10.0.0.0" + + with pytest.raises(ApiError) as excinfo: + create_vpc.subnet_create("test-subnet", ipv4=invalid_ipv4) + + assert excinfo.value.status == 400 + assert "ipv4 must be an IPv4 network" in str(excinfo.value.json) + + +def test_fails_update_subnet_invalid_data(create_vpc_with_subnet): + invalid_label = "invalid_subnet_label!!" + vpc, subnet = create_vpc_with_subnet + subnet.label = invalid_label + + with pytest.raises(ApiError) as excinfo: + subnet.save() + + assert excinfo.value.status == 400 + assert "Label must include only ASCII" in str(excinfo.value.json) diff --git a/test/unit/objects/domain_test.py b/test/unit/objects/domain_test.py index 64376fb37..f67503c9c 100644 --- a/test/unit/objects/domain_test.py +++ b/test/unit/objects/domain_test.py @@ -20,7 +20,6 @@ def test_save_null_values_excluded(self): domain.type = "slave" domain.master_ips = ["127.0.0.1"] domain.save() - self.assertTrue("group" not in m.call_data.keys()) def test_zone_file_view(self): diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 2aa280fef..951bd561f 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -1,7 +1,18 @@ from datetime import datetime from test.unit.base import ClientBaseCase -from linode_api4.objects import Config, Disk, Image, Instance, StackScript, Type +from linode_api4 import NetworkInterface +from linode_api4.objects import ( + Config, + ConfigInterface, + ConfigInterfaceIPv4, + Disk, + Image, + Instance, + StackScript, + Type, + VPCSubnet, +) class LinodeTest(ClientBaseCase): @@ -464,16 +475,17 @@ def test_update_interfaces(self): with self.mock_put("/linode/instances/123/configs/456789") as m: new_interfaces = [ - {"purpose": "public"}, - {"purpose": "vlan", "label": "cool-vlan"}, + {"purpose": "public", "primary": True}, + ConfigInterface("vlan", label="cool-vlan"), ] + expected_body = [new_interfaces[0], new_interfaces[1]._serialize()] config.interfaces = new_interfaces config.save() self.assertEqual(m.call_url, "/linode/instances/123/configs/456789") - self.assertEqual(m.call_data.get("interfaces"), new_interfaces) + self.assertEqual(m.call_data.get("interfaces"), expected_body) def test_get_config(self): json = self.client.get("/linode/instances/123/configs/456789") @@ -495,6 +507,14 @@ def test_get_config(self): self.assertEqual(config.virt_mode, "paravirt") self.assertIsNotNone(config.devices) + def test_interface_ipv4(self): + json = {"vpc": "10.0.0.1", "nat_1_1": "any"} + + ipv4 = ConfigInterfaceIPv4.from_json(json) + + self.assertEqual(ipv4.vpc, "10.0.0.1") + self.assertEqual(ipv4.nat_1_1, "any") + class StackScriptTest(ClientBaseCase): """ @@ -602,3 +622,204 @@ def test_save_force(self): with self.mock_put("linode/instances") as m: linode.save() assert m.called + + +class ConfigInterfaceTest(ClientBaseCase): + def test_list(self): + config = Config(self.client, 456789, 123) + config._api_get() + assert {v.id for v in config.interfaces} == {123, 321, 456} + assert {v.purpose for v in config.interfaces} == { + "vlan", + "vpc", + "public", + } + + def test_update(self): + config = Config(self.client, 456789, 123) + config._api_get() + config.interfaces = [ + {"purpose": "public"}, + ConfigInterface( + purpose="vlan", label="cool-vlan", ipam_address="10.0.0.4/32" + ), + ] + + with self.mock_put("linode/instances/123/configs/456789") as m: + config.save() + assert m.call_url == "/linode/instances/123/configs/456789" + assert m.call_data["interfaces"] == [ + {"purpose": "public"}, + { + "purpose": "vlan", + "label": "cool-vlan", + "ipam_address": "10.0.0.4/32", + }, + ] + + +class TestNetworkInterface(ClientBaseCase): + def test_create_interface_public(self): + config = Config(self.client, 456789, 123) + config._api_get() + + with self.mock_post( + "linode/instances/123/configs/456789/interfaces/456" + ) as m: + interface = config.interface_create_public(primary=True) + + assert m.called + assert ( + m.call_url == "/linode/instances/123/configs/456789/interfaces" + ) + assert m.method == "post" + assert m.call_data == {"purpose": "public", "primary": True} + + assert interface.id == 456 + assert interface.purpose == "public" + assert interface.primary + + def test_create_interface_vlan(self): + config = Config(self.client, 456789, 123) + config._api_get() + + with self.mock_post( + "linode/instances/123/configs/456789/interfaces/321" + ) as m: + interface = config.interface_create_vlan( + "test-interface", ipam_address="10.0.0.2/32" + ) + + assert m.called + assert ( + m.call_url == "/linode/instances/123/configs/456789/interfaces" + ) + assert m.method == "post" + assert m.call_data == { + "purpose": "vlan", + "label": "test-interface", + "ipam_address": "10.0.0.2/32", + } + + assert interface.id == 321 + assert interface.purpose == "vlan" + assert not interface.primary + assert interface.label == "test-interface" + assert interface.ipam_address == "10.0.0.2" + + def test_create_interface_vpc(self): + config = Config(self.client, 456789, 123) + config._api_get() + + with self.mock_post( + "linode/instances/123/configs/456789/interfaces/123" + ) as m: + interface = config.interface_create_vpc( + subnet=VPCSubnet(self.client, 789, 123456), + primary=True, + ipv4=ConfigInterfaceIPv4(vpc="10.0.0.4", nat_1_1="any"), + ip_ranges=["10.0.0.0/24"], + ) + + assert m.called + assert ( + m.call_url == "/linode/instances/123/configs/456789/interfaces" + ) + assert m.method == "post" + assert m.call_data == { + "purpose": "vpc", + "primary": True, + "subnet_id": 789, + "ipv4": {"vpc": "10.0.0.4", "nat_1_1": "any"}, + "ip_ranges": ["10.0.0.0/24"], + } + + assert interface.id == 123 + assert interface.purpose == "vpc" + assert interface.primary + assert interface.vpc.id == 123456 + assert interface.subnet.id == 789 + assert interface.ipv4.vpc == "10.0.0.2" + assert interface.ipv4.nat_1_1 == "any" + assert interface.ip_ranges == ["10.0.0.0/24"] + + def test_update(self): + interface = NetworkInterface(self.client, 123, 456789, 123) + interface._api_get() + + interface.ipv4.vpc = "10.0.0.3" + interface.primary = False + interface.ip_ranges = ["10.0.0.2/32"] + + with self.mock_put( + "linode/instances/123/configs/456789/interfaces/123/put" + ) as m: + interface.save() + + assert m.called + assert ( + m.call_url + == "/linode/instances/123/configs/456789/interfaces/123" + ) + assert m.method == "put" + assert m.call_data == { + "primary": False, + "ipv4": {"vpc": "10.0.0.3", "nat_1_1": "any"}, + "ip_ranges": ["10.0.0.2/32"], + } + + def test_get_vlan(self): + interface = NetworkInterface(self.client, 321, 456789, instance_id=123) + interface._api_get() + + self.assertEqual(interface.id, 321) + self.assertEqual(interface.ipam_address, "10.0.0.2") + self.assertEqual(interface.purpose, "vlan") + self.assertEqual(interface.label, "test-interface") + + def test_get_vpc(self): + interface = NetworkInterface(self.client, 123, 456789, instance_id=123) + interface._api_get() + + self.assertEqual(interface.id, 123) + self.assertEqual(interface.purpose, "vpc") + self.assertEqual(interface.vpc.id, 123456) + self.assertEqual(interface.subnet.id, 789) + self.assertEqual(interface.ipv4.vpc, "10.0.0.2") + self.assertEqual(interface.ipv4.nat_1_1, "any") + self.assertEqual(interface.ip_ranges, ["10.0.0.0/24"]) + self.assertEqual(interface.active, True) + + def test_list(self): + config = Config(self.client, 456789, 123) + config._api_get() + interfaces = config.network_interfaces + + assert {v.id for v in interfaces} == {123, 321, 456} + assert {v.purpose for v in interfaces} == { + "vlan", + "vpc", + "public", + } + + for v in interfaces: + assert isinstance(v, NetworkInterface) + + def test_reorder(self): + config = Config(self.client, 456789, 123) + config._api_get() + interfaces = config.network_interfaces + + with self.mock_post({}) as m: + interfaces.reverse() + # Let's make sure it supports both IDs and NetworkInterfaces + interfaces[2] = interfaces[2].id + + config.interface_reorder(interfaces) + + assert ( + m.call_url + == "/linode/instances/123/configs/456789/interfaces/order" + ) + + assert m.call_data == {"ids": [321, 123, 456]} diff --git a/test/unit/objects/networking_test.py b/test/unit/objects/networking_test.py index 7d32ae68a..dabf1ee2b 100644 --- a/test/unit/objects/networking_test.py +++ b/test/unit/objects/networking_test.py @@ -72,3 +72,14 @@ def test_rdns_reset(self): self.assertEqual(m.call_url, "/networking/ips/127.0.0.1") self.assertEqual(m.call_data_raw, '{"rdns": null}') + + def test_vpc_nat_1_1(self): + """ + Tests that the vpc_nat_1_1 of an IP can be retrieved. + """ + + ip = IPAddress(self.client, "127.0.0.1") + + self.assertEqual(ip.vpc_nat_1_1.vpc_id, 242) + self.assertEqual(ip.vpc_nat_1_1.subnet_id, 194) + self.assertEqual(ip.vpc_nat_1_1.address, "139.144.244.36") diff --git a/test/unit/objects/vpc_test.py b/test/unit/objects/vpc_test.py new file mode 100644 index 000000000..c8453ada1 --- /dev/null +++ b/test/unit/objects/vpc_test.py @@ -0,0 +1,149 @@ +import datetime +from test.unit.base import ClientBaseCase + +from linode_api4 import DATE_FORMAT, VPC, VPCSubnet +from linode_api4.objects import Volume + + +class VPCTest(ClientBaseCase): + """ + Tests methods of the VPC Group + """ + + def test_get_vpc(self): + """ + Tests that a VPC is loaded correctly by ID + """ + + vpc = VPC(self.client, 123456) + self.assertEqual(vpc._populated, False) + + self.validate_vpc_123456(vpc) + self.assertEqual(vpc._populated, True) + + def test_list_vpcs(self): + """ + Tests that you can list VPCs. + """ + + vpcs = self.client.vpcs() + + self.validate_vpc_123456(vpcs[0]) + self.assertEqual(vpcs[0]._populated, True) + + def test_create_vpc(self): + """ + Tests that you can create a VPC. + """ + + with self.mock_post("/vpcs/123456") as m: + vpc = self.client.vpcs.create("test-vpc", "us-southeast") + + self.assertEqual(m.call_url, "/vpcs") + + self.assertEqual( + m.call_data, + { + "label": "test-vpc", + "region": "us-southeast", + }, + ) + + self.assertEqual(vpc._populated, True) + self.validate_vpc_123456(vpc) + + def test_create_vpc_with_subnet(self): + """ + Tests that you can create a VPC. + """ + + with self.mock_post("/vpcs/123456") as m: + vpc = self.client.vpcs.create( + "test-vpc", + "us-southeast", + subnets=[{"label": "test-subnet", "ipv4": "10.0.0.0/24"}], + ) + + self.assertEqual(m.call_url, "/vpcs") + + self.assertEqual( + m.call_data, + { + "label": "test-vpc", + "region": "us-southeast", + "subnets": [ + {"label": "test-subnet", "ipv4": "10.0.0.0/24"} + ], + }, + ) + + self.assertEqual(vpc._populated, True) + self.validate_vpc_123456(vpc) + + def test_get_subnet(self): + """ + Tests that you can list VPCs. + """ + + subnet = VPCSubnet(self.client, 789, 123456) + + self.assertEqual(subnet._populated, False) + + self.validate_vpc_subnet_789(subnet) + self.assertEqual(subnet._populated, True) + self.assertEqual(subnet.linodes[0].id, 12345) + self.assertEqual(subnet.linodes[0].interfaces[0].id, 678) + self.assertEqual(len(subnet.linodes[0].interfaces), 2) + self.assertEqual(subnet.linodes[0].interfaces[1].active, False) + + def test_list_subnets(self): + """ + Tests that you can list VPCs. + """ + + subnets = self.client.vpcs()[0].subnets + + self.validate_vpc_subnet_789(subnets[0]) + + def test_create_subnet(self): + """ + Tests that you can create a subnet. + """ + + with self.mock_post("/vpcs/123456/subnets/789") as m: + vpc = VPC(self.client, 123456) + subnet = vpc.subnet_create("test-subnet", "10.0.0.0/24") + + self.assertEqual(m.call_url, "/vpcs/123456/subnets") + + self.assertEqual( + m.call_data, + { + "label": "test-subnet", + "ipv4": "10.0.0.0/24", + }, + ) + + self.validate_vpc_subnet_789(subnet) + + def validate_vpc_123456(self, vpc: VPC): + expected_dt = datetime.datetime.strptime( + "2018-01-01T00:01:01", DATE_FORMAT + ) + + self.assertEqual(vpc.label, "test-vpc") + self.assertEqual(vpc.description, "A very real VPC.") + self.assertEqual(vpc.region.id, "us-southeast") + self.assertEqual(vpc.created, expected_dt) + self.assertEqual(vpc.updated, expected_dt) + + def validate_vpc_subnet_789(self, subnet: VPCSubnet): + expected_dt = datetime.datetime.strptime( + "2018-01-01T00:01:01", DATE_FORMAT + ) + + self.assertEqual(subnet.label, "test-subnet") + self.assertEqual(subnet.ipv4, "10.0.0.0/24") + self.assertEqual(subnet.linodes[0].id, 12345) + self.assertEqual(subnet.created, expected_dt) + self.assertEqual(subnet.updated, expected_dt) From 27161d7d036fb6b4c70d41012531465ef49a47f5 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:18:51 -0500 Subject: [PATCH 06/19] new: Support region availability endpoints (#349) * Add availability object * Add filter assertion * Avail docs --- linode_api4/groups/region.py | 20 + linode_api4/objects/region.py | 34 +- linode_api4/objects/serializable.py | 45 +- linode_api4/paginated_list.py | 6 + test/fixtures/regions_availability.json | 507 ++++++++++++++++++ .../regions_us-east_availability.json | 67 +++ test/unit/base.py | 7 + test/unit/objects/region_test.py | 60 ++- 8 files changed, 742 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/regions_availability.json create mode 100644 test/fixtures/regions_us-east_availability.json diff --git a/linode_api4/groups/region.py b/linode_api4/groups/region.py index 4221c74a8..9ddc8fb63 100644 --- a/linode_api4/groups/region.py +++ b/linode_api4/groups/region.py @@ -1,5 +1,6 @@ from linode_api4.groups import Group from linode_api4.objects import Region +from linode_api4.objects.region import RegionAvailabilityEntry class RegionGroup(Group): @@ -23,3 +24,22 @@ def __call__(self, *filters): """ return self.client._get_and_filter(Region, *filters) + + def availability(self, *filters): + """ + Returns the availability of Linode plans within a Region. + + + API Documentation: https://www.linode.com/docs/api/regions/#regions-availability-list + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A list of entries describing the availability of a plan in a region. + :rtype: PaginatedList of RegionAvailabilityEntry + """ + + return self.client._get_and_filter( + RegionAvailabilityEntry, *filters, endpoint="/regions/availability" + ) diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index a9919f94b..7f48ea846 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -1,4 +1,10 @@ -from linode_api4.objects import Base, Property +from dataclasses import dataclass +from typing import List + +from linode_api4.errors import UnexpectedResponseError +from linode_api4.objects.base import Base, JSONObject, Property +from linode_api4.objects.filtering import FilterableAttribute +from linode_api4.objects.serializable import JSONFilterableMetaclass class Region(Base): @@ -17,3 +23,29 @@ class Region(Base): "resolvers": Property(), "label": Property(), } + + @property + def availability(self) -> List["RegionAvailabilityEntry"]: + result = self._client.get( + f"{self.api_endpoint}/availability", model=self + ) + + if result is None: + raise UnexpectedResponseError( + "Expected availability data, got None." + ) + + return [RegionAvailabilityEntry.from_json(v) for v in result] + + +@dataclass +class RegionAvailabilityEntry(JSONObject): + """ + Represents the availability of a Linode type within a region. + + API Documentation: https://www.linode.com/docs/api/regions/#region-availability-view + """ + + region: str = None + plan: str = None + available: bool = False diff --git a/linode_api4/objects/serializable.py b/linode_api4/objects/serializable.py index d0cf63282..e4199283b 100644 --- a/linode_api4/objects/serializable.py +++ b/linode_api4/objects/serializable.py @@ -1,10 +1,41 @@ import inspect from dataclasses import asdict, dataclass -from typing import Any, Dict, Optional, get_args, get_origin, get_type_hints +from types import SimpleNamespace +from typing import ( + Any, + ClassVar, + Dict, + Optional, + get_args, + get_origin, + get_type_hints, +) + +from linode_api4.objects.filtering import FilterableAttribute + +# Wraps the SimpleNamespace class and allows for +# SQLAlchemy-style filter generation on JSONObjects. +JSONFilterGroup = SimpleNamespace + + +class JSONFilterableMetaclass(type): + def __init__(cls, name, bases, dct): + setattr( + cls, + "filters", + JSONFilterGroup( + **{ + k: FilterableAttribute(k) + for k in cls.__annotations__.keys() + } + ), + ) + + super().__init__(name, bases, dct) @dataclass -class JSONObject: +class JSONObject(metaclass=JSONFilterableMetaclass): """ A simple helper class for serializable API objects. This is typically used for nested object values. @@ -13,6 +44,16 @@ class JSONObject: fields and static typing. """ + filters: ClassVar[JSONFilterGroup] = None + """ + A group containing FilterableAttributes used to create SQLAlchemy-style filters. + + Example usage:: + self.client.regions.availability( + RegionAvailabilityEntry.filters.plan == "premium4096.7" + ) + """ + def __init__(self): raise NotImplementedError( "JSONObject is not intended to be constructed directly" diff --git a/linode_api4/paginated_list.py b/linode_api4/paginated_list.py index 1db5bfc5d..b9421de6a 100644 --- a/linode_api4/paginated_list.py +++ b/linode_api4/paginated_list.py @@ -1,5 +1,7 @@ import math +from linode_api4.objects.serializable import JSONObject + class PaginatedList(object): """ @@ -205,6 +207,10 @@ def make_list(json_arr, client, cls, parent_id=None): for obj in json_arr: id_val = None + # Special handling for JSON objects + if issubclass(cls, JSONObject): + result.append(cls.from_json(obj)) + continue if "id" in obj: id_val = obj["id"] diff --git a/test/fixtures/regions_availability.json b/test/fixtures/regions_availability.json new file mode 100644 index 000000000..ff5122df8 --- /dev/null +++ b/test/fixtures/regions_availability.json @@ -0,0 +1,507 @@ +{ + "data": [ + { + "region": "us-central", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "us-central", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "us-central", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "us-central", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "us-central", + "plan": "premium131072.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium16384.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium262144.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium32768.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium4096.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium524288.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium65536.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium8192.7", + "available": false + }, + { + "region": "us-central", + "plan": "premium98304.7", + "available": false + }, + { + "region": "us-west", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "us-west", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "us-west", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "us-west", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "us-west", + "plan": "premium131072.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium16384.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium262144.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium32768.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium4096.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium524288.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium65536.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium8192.7", + "available": false + }, + { + "region": "us-west", + "plan": "premium98304.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "us-southeast", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "us-southeast", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "us-southeast", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium131072.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium16384.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium262144.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium32768.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium4096.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium524288.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium65536.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium8192.7", + "available": false + }, + { + "region": "us-southeast", + "plan": "premium98304.7", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "us-east", + "plan": "premium131072.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium16384.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium262144.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium32768.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium4096.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium524288.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium65536.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium8192.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium98304.7", + "available": false + }, + { + "region": "eu-west", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "eu-west", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "eu-west", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "eu-west", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "eu-west", + "plan": "premium131072.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium16384.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium262144.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium32768.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium4096.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium524288.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium65536.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium8192.7", + "available": false + }, + { + "region": "eu-west", + "plan": "premium98304.7", + "available": false + }, + { + "region": "ap-south", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "ap-south", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "ap-south", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "ap-south", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "ap-south", + "plan": "premium131072.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium16384.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium262144.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium32768.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium4096.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium524288.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium65536.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium8192.7", + "available": false + }, + { + "region": "ap-south", + "plan": "premium98304.7", + "available": false + }, + { + "region": "eu-central", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "eu-central", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "eu-central", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "eu-central", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "eu-central", + "plan": "premium131072.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium16384.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium262144.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium32768.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium4096.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium524288.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium65536.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium8192.7", + "available": false + }, + { + "region": "eu-central", + "plan": "premium98304.7", + "available": false + }, + { + "region": "ap-west", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "ap-west", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "ap-west", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "ap-west", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "ap-west", + "plan": "premium131072.7", + "available": false + }, + { + "region": "ap-west", + "plan": "premium16384.7", + "available": false + }, + { + "region": "ap-west", + "plan": "premium262144.7", + "available": false + }, + { + "region": "ap-west", + "plan": "premium32768.7", + "available": false + }, + { + "region": "ap-west", + "plan": "premium4096.7", + "available": false + } + ], + "page": 1, + "pages": 3, + "results": 299 +} \ No newline at end of file diff --git a/test/fixtures/regions_us-east_availability.json b/test/fixtures/regions_us-east_availability.json new file mode 100644 index 000000000..f7dc11ea2 --- /dev/null +++ b/test/fixtures/regions_us-east_availability.json @@ -0,0 +1,67 @@ +[ + { + "region": "us-east", + "plan": "gpu-rtx6000-1.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-2.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-3.1", + "available": false + }, + { + "region": "us-east", + "plan": "gpu-rtx6000-4.1", + "available": false + }, + { + "region": "us-east", + "plan": "premium131072.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium16384.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium262144.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium32768.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium4096.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium524288.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium65536.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium8192.7", + "available": false + }, + { + "region": "us-east", + "plan": "premium98304.7", + "available": false + } +] \ No newline at end of file diff --git a/test/unit/base.py b/test/unit/base.py index 1af94ff5e..e143f8f64 100644 --- a/test/unit/base.py +++ b/test/unit/base.py @@ -134,6 +134,13 @@ def called(self): """ return self.mock.called + @property + def call_count(self): + """ + A shortcut to check how many times the mock function was called. + """ + return self.mock.call_count + class ClientBaseCase(TestCase): def setUp(self): diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 5fd1ee7a3..9c954a3da 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -1,6 +1,8 @@ +import json from test.unit.base import ClientBaseCase -from linode_api4.objects import Region +from linode_api4.objects import Region, Type +from linode_api4.objects.region import RegionAvailabilityEntry class RegionTest(ClientBaseCase): @@ -20,3 +22,59 @@ def test_get_region(self): self.assertEqual(region.label, "label7") self.assertEqual(region.status, "ok") self.assertIsNotNone(region.resolvers) + + def test_list_availability(self): + """ + Tests that region availability can be listed and filtered on. + """ + + with self.mock_get("/regions/availability") as m: + avail_entries = self.client.regions.availability( + RegionAvailabilityEntry.filters.region == "us-east", + RegionAvailabilityEntry.filters.plan == "premium4096.7", + ) + + assert len(avail_entries) > 0 + + for entry in avail_entries: + assert entry.region is not None + assert len(entry.region) > 0 + + assert entry.plan is not None + assert len(entry.plan) > 0 + + assert entry.available is not None + + # Ensure all three pages are read + assert m.call_count == 3 + assert m.mock.call_args_list[0].args[0] == "//regions/availability" + + assert ( + m.mock.call_args_list[1].args[0] + == "//regions/availability?page=2&page_size=100" + ) + assert ( + m.mock.call_args_list[2].args[0] + == "//regions/availability?page=3&page_size=100" + ) + + # Ensure the filter headers are correct + for k, call in m.mock.call_args_list: + assert json.loads(call.get("headers").get("X-Filter")) == { + "+and": [{"region": "us-east"}, {"plan": "premium4096.7"}] + } + + def test_region_availability(self): + """ + Tests that availability for a specific region can be listed and filtered on. + """ + avail_entries = Region(self.client, "us-east").availability + + for entry in avail_entries: + assert entry.region is not None + assert len(entry.region) > 0 + + assert entry.plan is not None + assert len(entry.plan) > 0 + + assert entry.available is not None From 78e2de616ef5d5afba54c9cbc009c09ed4b3cd6f Mon Sep 17 00:00:00 2001 From: mkaminsk-akamai <151915970+mkaminsk-akamai@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:57:52 +0100 Subject: [PATCH 07/19] new: Migrating legacy expire endpoint /oauth/token/expire to /oauth/revoke (#352) --- linode_api4/login_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/linode_api4/login_client.py b/linode_api4/login_client.py index 765dbbe2e..2b69d9eed 100644 --- a/linode_api4/login_client.py +++ b/linode_api4/login_client.py @@ -490,9 +490,10 @@ def refresh_oauth_token(self, refresh_token): def expire_token(self, token): """ - Given a token, makes a request to the authentication server to expire - it immediately. This is considered a responsible way to log out a - user. If you simply remove the session your application has for the + Given a token, makes a request to the authentication server to expire both + access token and refresh token. + This is considered a responsible way to log out a user. + If you remove only the session your application has for the user without expiring their token, the user is not _really_ logged out. :param token: The OAuth token you wish to expire @@ -504,8 +505,9 @@ def expire_token(self, token): :raises ApiError: If the expiration attempt failed. """ r = requests.post( - self._login_uri("/oauth/token/expire"), + self._login_uri("/oauth/revoke"), data={ + "token_type_hint": "access_token", "client_id": self.client_id, "client_secret": self.client_secret, "token": token, From fdcc03fb7c4223c38cf02a7a4b3b76faeb720484 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:25:47 -0800 Subject: [PATCH 08/19] test: evaluate skipped tests, fix failing tests, add step in e2e workflow to mark build as failed (#351) * update test_linode.py * evaluate skipped tests, add steps in e2e workflow, fix flaky tests * lint * fix flaky tests in lke and volume * add some safety wait until functions for reorder test * update test_delete_interface_containing_vpc, test_linode_initate_migration --- .github/workflows/e2e-test-pr.yml | 17 ++- test/integration/conftest.py | 5 +- test/integration/helpers.py | 2 +- test/integration/models/test_linode.py | 199 ++++++++++++++++--------- test/integration/models/test_lke.py | 23 +-- 5 files changed, 156 insertions(+), 90 deletions(-) diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index 00391cf19..de14c9669 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -19,6 +19,8 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' && inputs.sha != '' + env: + EXIT_STATUS: 0 steps: - uses: actions-ecosystem/action-regex-match@v2 @@ -83,8 +85,8 @@ jobs: timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" status=0 - if ! python3 -m pytest test/integration/${INTEGRATION_TEST_PATH} --junitxml="${report_filename}"; then - echo "Tests failed, but attempting to upload results anyway" + if ! python3 -m pytest test/integration/${INTEGRATION_TEST_PATH} --disable-warnings --junitxml="${report_filename}"; then + echo "EXIT_STATUS=1" >> $GITHUB_ENV fi env: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} @@ -135,4 +137,13 @@ jobs: status: 'completed', conclusion: process.env.conclusion }); - return result; \ No newline at end of file + return result; + + - name: Test Execution Status Handler + run: | + if [[ "$EXIT_STATUS" != 0 ]]; then + echo "Test execution contains failure(s)" + exit $EXIT_STATUS + else + echo "Tests passed!" + fi \ No newline at end of file diff --git a/test/integration/conftest.py b/test/integration/conftest.py index a3e4b12e4..ae3550eb7 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -155,11 +155,10 @@ def test_domain(test_linode_client): def test_volume(test_linode_client): client = test_linode_client timestamp = str(time.time_ns()) + region = client.regions()[0] label = "TestSDK-" + timestamp - volume = client.volume_create( - label=label, region=get_region(client, {"Block Storage"}) - ) + volume = client.volume_create(label=label, region=region) yield volume diff --git a/test/integration/helpers.py b/test/integration/helpers.py index 2ea66464b..c178ad4dd 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -111,7 +111,7 @@ def send_request_when_resource_available( if time.time() - start_time > timeout: raise TimeoutError( "Timeout Error: resource is not available in" - + timeout + + str(timeout) + "seconds" ) time.sleep(10) diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 5b68fac34..50497a706 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -2,6 +2,7 @@ from test.integration.helpers import ( get_test_label, retry_sending_request, + send_request_when_resource_available, wait_for_condition, ) @@ -65,6 +66,53 @@ def linode_with_volume_firewall(test_linode_client): linode_instance.delete() +@pytest.fixture(scope="session") +def linode_for_network_interface_tests(test_linode_client): + client = test_linode_client + available_regions = client.regions() + chosen_region = available_regions[0] + timestamp = str(time.time_ns()) + label = "TestSDK-" + timestamp + + linode_instance, password = client.linode.instance_create( + "g6-nanode-1", chosen_region, image="linode/debian10", label=label + ) + + yield linode_instance + + linode_instance.delete() + + +@pytest.fixture(scope="session", autouse=True) +def linode_for_disk_tests(test_linode_client): + client = test_linode_client + available_regions = client.regions() + chosen_region = available_regions[0] + label = get_test_label() + + linode_instance, password = client.linode.instance_create( + "g6-nanode-1", + chosen_region, + image="linode/debian10", + label=label + "_long_tests", + ) + + time.sleep(10) + + # Provisioning time + wait_for_condition(10, 300, get_status, linode_instance, "running") + + time.sleep(10) + + linode_instance.shutdown() + + wait_for_condition(10, 100, get_status, linode_instance, "offline") + + yield linode_instance + + linode_instance.delete() + + @pytest.mark.smoke @pytest.fixture def create_linode_for_long_running_tests(test_linode_client): @@ -289,22 +337,53 @@ def test_linode_volumes(linode_with_volume_firewall): assert "TestSDK" in volumes[0].label -def test_linode_disk_duplicate(test_linode_client, create_linode): - pytest.skip("Need to find out the space sizing when duplicating disks") - linode = create_linode +def wait_for_disk_status(disk: Disk, timeout): + start_time = time.time() + while True: + try: + if disk.status == "ready": + return disk.status + except ApiError: + if time.time() - start_time > timeout: + raise TimeoutError("Wait for condition timeout error") + + +@pytest.mark.dependency() +def test_disk_resize_and_duplicate(test_linode_client, linode_for_disk_tests): + linode = linode_for_disk_tests + + disk = linode.disks[0] + + disk.resize(5000) + + # Using hard sleep instead of wait as the status shows ready when it is resizing + time.sleep(120) disk = test_linode_client.load(Disk, linode.disks[0].id, linode.id) - try: - dup_disk = disk.duplicate() - assert dup_disk.linode_id == linode.id - except ApiError as e: - assert e.status == 400 - assert "Insufficient space" in str(e.json) + assert disk.size == 5000 + + dup_disk = disk.duplicate() + + time.sleep(40) + + wait_for_disk_status(dup_disk, 120) + + assert dup_disk.linode_id == linode.id + + +@pytest.mark.dependency(depends=["test_disk_resize_and_duplicate"]) +def test_linode_create_disk(test_linode_client, linode_for_disk_tests): + linode = test_linode_client.load(Instance, linode_for_disk_tests.id) + + disk = linode.disk_create(size=500) + + wait_for_disk_status(disk, 120) + + assert disk.linode_id == linode.id def test_linode_instance_password(create_linode_for_pass_reset): - pytest.skip("Failing due to mismatched request body") linode = create_linode_for_pass_reset[0] password = create_linode_for_pass_reset[1] @@ -343,25 +422,14 @@ def test_linode_initate_migration(test_linode_client): wait_for_condition(10, 100, get_status, linode, "running") # Says it could take up to ~6 hrs for migration to fully complete - linode.initiate_migration(region="us-central") - res = linode.delete() - - assert res - - -def test_linode_create_disk(create_linode): - pytest.skip( - "Pre-requisite for the test account need to comply with this test" + send_request_when_resource_available( + 300, linode.initiate_migration, "us-central" ) - linode = create_linode - disk, gen_pass = linode.disk_create() + res = linode.delete() -def test_disk_resize(): - pytest.skip( - "Pre-requisite for the test account need to comply with this test" - ) + assert res def test_config_update_interfaces(create_linode): @@ -387,19 +455,9 @@ def test_config_update_interfaces(create_linode): def test_get_config(test_linode_client, create_linode): - pytest.skip( - "Model get method: client.load(Config, 123, 123) does not work..." - ) linode = create_linode - json = test_linode_client.get( - "linode/instances/" - + str(linode.id) - + "/configs/" - + str(linode.configs[0].id) - ) - config = Config( - test_linode_client, linode.id, linode.configs[0].id, json=json - ) + + config = test_linode_client.load(Config, linode.configs[0].id, linode.id) assert config.id == linode.configs[0].id @@ -429,18 +487,6 @@ def test_get_linode_types_overrides(test_linode_client): assert linode_type.region_prices[0].monthly >= 0 -def test_get_linode_type_by_id(test_linode_client): - pytest.skip( - "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" - ) - - -def test_get_linode_type_gpu(): - pytest.skip( - "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)" - ) - - def test_save_linode_noforce(test_linode_client, create_linode): linode = create_linode old_label = linode.label @@ -464,28 +510,29 @@ def test_save_linode_force(test_linode_client, create_linode): class TestNetworkInterface: - def test_list(self, create_linode): - linode = create_linode + def test_list(self, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests config: Config = linode.configs[0] config.interface_create_public( primary=True, ) - config.interface_create_vlan( - label="testvlan", ipam_address="10.0.0.3/32" - ) + + label = str(time.time_ns()) + "vlabel" + + config.interface_create_vlan(label=label, ipam_address="10.0.0.3/32") interface = config.network_interfaces assert interface[0].purpose == "public" assert interface[0].primary assert interface[1].purpose == "vlan" - assert interface[1].label == "testvlan" + assert interface[1].label == label assert interface[1].ipam_address == "10.0.0.3/32" - def test_create_public(self, create_linode): - linode = create_linode + def test_create_public(self, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests config: Config = linode.configs[0] @@ -502,8 +549,8 @@ def test_create_public(self, create_linode): assert interface.purpose == "public" assert interface.primary - def test_create_vlan(self, create_linode): - linode = create_linode + def test_create_vlan(self, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests config: Config = linode.configs[0] @@ -521,7 +568,11 @@ def test_create_vlan(self, create_linode): assert interface.label == "testvlan" assert interface.ipam_address == "10.0.0.2/32" - def test_create_vpc(self, create_linode, create_vpc_with_subnet_and_linode): + def test_create_vpc( + self, + linode_for_network_interface_tests, + create_vpc_with_subnet_and_linode, + ): vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode config: Config = linode.configs[0] @@ -545,7 +596,11 @@ def test_create_vpc(self, create_linode, create_vpc_with_subnet_and_linode): assert interface.ipv4.nat_1_1 == linode.ipv4[0] assert interface.ip_ranges == ["10.0.0.5/32"] - def test_update_vpc(self, create_linode, create_vpc_with_subnet_and_linode): + def test_update_vpc( + self, + linode_for_network_interface_tests, + create_vpc_with_subnet_and_linode, + ): vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode config: Config = linode.configs[0] @@ -575,25 +630,31 @@ def test_update_vpc(self, create_linode, create_vpc_with_subnet_and_linode): assert interface.ipv4.nat_1_1 == linode.ipv4[0] assert interface.ip_ranges == ["10.0.0.6/32"] - def test_reorder(self, create_linode): - linode = create_linode + def test_reorder(self, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests config: Config = linode.configs[0] pub_interface = config.interface_create_public( primary=True, ) + + label = str(time.time_ns()) + "vlabel" vlan_interface = config.interface_create_vlan( - label="testvlan", ipam_address="10.0.0.3/32" + label=label, ipam_address="10.0.0.3/32" ) + send_request_when_resource_available(300, linode.shutdown) + interfaces = config.network_interfaces interfaces.reverse() - config.interface_reorder(interfaces) + send_request_when_resource_available( + 300, config.interface_reorder, interfaces + ) config.invalidate() - assert [v.id for v in config.interfaces] == [ + assert [v.id for v in config.interfaces[:2]] == [ vlan_interface.id, pub_interface.id, ] @@ -606,7 +667,11 @@ def test_delete_interface_containing_vpc( config: Config = linode.configs[0] config.interfaces = [] - config.save() + + # must power off linode before saving + send_request_when_resource_available(300, linode.shutdown) + + send_request_when_resource_available(60, config.save) interface = config.interface_create_vpc( subnet=subnet, diff --git a/test/integration/models/test_lke.py b/test/integration/models/test_lke.py index 45b1ac8a1..04b479e8e 100644 --- a/test/integration/models/test_lke.py +++ b/test/integration/models/test_lke.py @@ -45,8 +45,7 @@ def test_get_lke_clusters(test_linode_client, lke_cluster): def test_get_lke_pool(test_linode_client, lke_cluster): - pytest.skip("client.load(LKENodePool, 123, 123) does not work") - + pytest.skip("TPT-2511") cluster = lke_cluster pool = test_linode_client.load(LKENodePool, cluster.pools[0].id, cluster.id) @@ -67,7 +66,9 @@ def test_cluster_dashboard_url_view(lke_cluster): def test_kubeconfig_delete(lke_cluster): cluster = lke_cluster - cluster.kubeconfig_delete() + res = send_request_when_resource_available(300, cluster.kubeconfig_delete) + + assert res is None def test_lke_node_view(lke_cluster): @@ -122,19 +123,9 @@ def test_lke_cluster_nodes_recycle(test_linode_client, lke_cluster): assert node.status == "not_ready" -def test_lke_cluster_regenerate(lke_cluster): - pytest.skip( - "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" - ) - cluster = lke_cluster - - cluster.cluster_regenerate() - - def test_service_token_delete(lke_cluster): - pytest.skip( - "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" - ) cluster = lke_cluster - cluster.service_token_delete() + res = cluster.service_token_delete() + + assert res is None From f7fb7bd11ccf9a1b5b0759802dfbc99881339719 Mon Sep 17 00:00:00 2001 From: okokes-akamai <126059888+okokes-akamai@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:37:15 +0100 Subject: [PATCH 09/19] Preserve `client.session.verify` when set by user (#353) --- linode_api4/linode_client.py | 5 ++++- test/unit/linode_client_test.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py index a6ceb0178..d55958884 100644 --- a/linode_api4/linode_client.py +++ b/linode_api4/linode_client.py @@ -276,7 +276,10 @@ def _api_call( body = json.dumps(data) response = method( - url, headers=headers, data=body, verify=self.ca_path or True + url, + headers=headers, + data=body, + verify=self.ca_path or self.session.verify, ) warning = response.headers.get("Warning", None) diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 69af304f7..09e0f9755 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -280,6 +280,28 @@ def get_mock(*params, verify=True, **kwargs): assert called + def test_custom_verify(self): + """ + If we set a custom `verify` value on our session, + we want it preserved. + """ + called = False + + self.client.session.verify = False + old_get = self.client.session.get + + def get_mock(*params, verify=True, **kwargs): + nonlocal called + called = True + assert verify is False + return old_get(*params, **kwargs) + + self.client.session.get = get_mock + + self.client.linode.instances() + + assert called + class AccountGroupTest(ClientBaseCase): """ From 8c9fc482505d9839489a93d31ad2813d12929b6e Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:38:59 -0500 Subject: [PATCH 10/19] docs: Update API documentations for Beta Program (#355) --- linode_api4/groups/account.py | 4 ++-- linode_api4/groups/beta.py | 2 +- linode_api4/objects/account.py | 2 ++ linode_api4/objects/beta.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/linode_api4/groups/account.py b/linode_api4/groups/account.py index 4eeadcc11..50df1fc37 100644 --- a/linode_api4/groups/account.py +++ b/linode_api4/groups/account.py @@ -457,7 +457,7 @@ def enrolled_betas(self, *filters): """ Returns a list of all Beta Programs an account is enrolled in. - API doc: TBD + API doc: https://www.linode.com/docs/api/beta-programs/#enrolled-beta-programs-list :returns: a list of Beta Programs. :rtype: PaginatedList of AccountBetaProgram @@ -468,7 +468,7 @@ def join_beta_program(self, beta: Union[str, BetaProgram]): """ Enrolls an account into a beta program. - API doc: TBD + API doc: https://www.linode.com/docs/api/beta-programs/#beta-program-enroll :param beta: The object or id of a beta program to join. :type beta: BetaProgram or str diff --git a/linode_api4/groups/beta.py b/linode_api4/groups/beta.py index 18095dc03..1da34ee25 100644 --- a/linode_api4/groups/beta.py +++ b/linode_api4/groups/beta.py @@ -12,7 +12,7 @@ def betas(self, *filters): """ Returns a list of available active Beta Programs. - API Documentation: TBD + API Documentation: https://www.linode.com/docs/api/beta-programs/#beta-programs-list :param filters: Any number of filters to apply to this query. See :doc:`Filtering Collections` diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index 264c6ff60..f71dd0204 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -642,6 +642,8 @@ def save(self): class AccountBetaProgram(Base): """ The details and enrollment information of a Beta program that an account is enrolled in. + + API Documentation: https://www.linode.com/docs/api/beta-programs/#enrolled-beta-program-view """ api_endpoint = "/account/betas/{id}" diff --git a/linode_api4/objects/beta.py b/linode_api4/objects/beta.py index 3124f4f15..42a3eef85 100644 --- a/linode_api4/objects/beta.py +++ b/linode_api4/objects/beta.py @@ -6,7 +6,7 @@ class BetaProgram(Base): Beta program is a new product or service that's not generally available to all customers. User with permissions can enroll into a beta program and access the functionalities. - API Documentation: TBD + API Documentation: https://www.linode.com/docs/api/beta-programs/#beta-program-view """ api_endpoint = "/betas/{id}" From ddb0b9800b5a3b71cb0e470dd20d0597c120a3cd Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:34:53 -0800 Subject: [PATCH 11/19] test: add release info to xml file before uploading to obj storage (#357) * update script and workflow to include release info * update url * lint --- .github/workflows/e2e-test-pr.yml | 5 +--- test/script/add_to_xml_test_report.py | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index de14c9669..ba0ff6cc4 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -91,14 +91,11 @@ jobs: env: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - - name: Set release version env - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Add additional information to XML report run: | filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') python test/script/add_to_xml_test_report.py \ - --branch_name "${{ env.RELEASE_VERSION }}" \ + --branch_name "${GITHUB_REF#refs/*/}" \ --gha_run_id "$GITHUB_RUN_ID" \ --gha_run_number "$GITHUB_RUN_NUMBER" \ --xmlfile "${filename}" diff --git a/test/script/add_to_xml_test_report.py b/test/script/add_to_xml_test_report.py index d486028be..d978e396f 100644 --- a/test/script/add_to_xml_test_report.py +++ b/test/script/add_to_xml_test_report.py @@ -1,6 +1,35 @@ import argparse import xml.etree.ElementTree as ET +import requests + +latest_release_url = ( + "https://api.github.com/repos/linode/linode_api4-python/releases/latest" +) + + +def get_release_version(): + url = latest_release_url + + try: + response = requests.get(url) + response.raise_for_status() # Check for HTTP errors + + release_info = response.json() + version = release_info["tag_name"] + + # Remove 'v' prefix if it exists + if version.startswith("v"): + version = version[1:] + + return str(version) + + except requests.exceptions.RequestException as e: + print("Error:", e) + except KeyError: + print("Error: Unable to fetch release information from GitHub API.") + + # Parse command-line arguments parser = argparse.ArgumentParser( description="Modify XML with workflow information" @@ -8,6 +37,7 @@ parser.add_argument("--branch_name", required=True) parser.add_argument("--gha_run_id", required=True) parser.add_argument("--gha_run_number", required=True) +parser.add_argument("--release_tag", required=False) parser.add_argument( "--xmlfile", required=True ) # Added argument for XML file path @@ -29,10 +59,14 @@ gha_run_number_element = ET.Element("gha_run_number") gha_run_number_element.text = args.gha_run_number +gha_release_tag_element = ET.Element("release_tag") +gha_release_tag_element.text = get_release_version() + # Add the new elements to the root of the XML root.append(branch_name_element) root.append(gha_run_id_element) root.append(gha_run_number_element) +root.append(gha_release_tag_element) # Save the modified XML modified_xml_file_path = xml_file_path # Overwrite it From 8bf3b19a3a2f1cfd53ee7179a5dd8fc72cfe6c8c Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:25:21 -0500 Subject: [PATCH 12/19] new: Support getting account availability info (#354) * add account availbility * update id to dc * replace dc with region --- linode_api4/groups/account.py | 13 +++++ linode_api4/objects/account.py | 16 ++++++ test/fixtures/account_availability.json | 51 +++++++++++++++++++ .../account_availability_us-east.json | 4 ++ test/unit/linode_client_test.py | 12 +++++ test/unit/objects/account_test.py | 18 +++++++ 6 files changed, 114 insertions(+) create mode 100644 test/fixtures/account_availability.json create mode 100644 test/fixtures/account_availability_us-east.json diff --git a/linode_api4/groups/account.py b/linode_api4/groups/account.py index 4eeadcc11..d69a305f3 100644 --- a/linode_api4/groups/account.py +++ b/linode_api4/groups/account.py @@ -4,6 +4,7 @@ from linode_api4.groups import Group from linode_api4.objects import ( Account, + AccountAvailability, AccountBetaProgram, AccountSettings, BetaProgram, @@ -483,3 +484,15 @@ def join_beta_program(self, beta: Union[str, BetaProgram]): ) return True + + def availabilities(self, *filters): + """ + Returns a list of all available regions and the resources which are NOT available + to the account. + + API doc: TBD + + :returns: a list of region availability information. + :rtype: PaginatedList of AccountAvailability + """ + return self.client._get_and_filter(AccountAvailability, *filters) diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index 264c6ff60..c7ca8bded 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -654,3 +654,19 @@ class AccountBetaProgram(Base): "ended": Property(is_datetime=True), "enrolled": Property(is_datetime=True), } + + +class AccountAvailability(Base): + """ + The resources information in a region which are NOT available to an account. + + API doc: TBD + """ + + api_endpoint = "/account/availability/{region}" + id_attribute = "region" + + properties = { + "region": Property(identifier=True), + "unavailable": Property(), + } diff --git a/test/fixtures/account_availability.json b/test/fixtures/account_availability.json new file mode 100644 index 000000000..a09feb1db --- /dev/null +++ b/test/fixtures/account_availability.json @@ -0,0 +1,51 @@ +{ + "data": [ + { + "region": "ap-west", + "unavailable": [] + }, + { + "region": "ca-central", + "unavailable": [] + }, + { + "region": "ap-southeast", + "unavailable": [] + }, + { + "region": "us-central", + "unavailable": [] + }, + { + "region": "us-west", + "unavailable": [] + }, + { + "region": "us-southeast", + "unavailable": [] + }, + { + "region": "us-east", + "unavailable": [] + }, + { + "region": "eu-west", + "unavailable": [] + }, + { + "region": "ap-south", + "unavailable": [] + }, + { + "region": "eu-central", + "unavailable": [] + }, + { + "region": "ap-northeast", + "unavailable": [] + } + ], + "page": 1, + "pages": 1, + "results": 11 +} diff --git a/test/fixtures/account_availability_us-east.json b/test/fixtures/account_availability_us-east.json new file mode 100644 index 000000000..5bcceb526 --- /dev/null +++ b/test/fixtures/account_availability_us-east.json @@ -0,0 +1,4 @@ +{ + "region": "us-east", + "unavailable": [] +} \ No newline at end of file diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 69af304f7..77088820a 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -492,6 +492,18 @@ def test_account_transfer(self): self.assertEqual(transfer.region_transfers[0].quota, 5010) self.assertEqual(transfer.region_transfers[0].billable, 0) + def test_account_availabilities(self): + """ + Tests that account availabilities can be retrieved + """ + availabilities = self.client.account.availabilities() + + self.assertEqual(len(availabilities), 11) + availability = availabilities[0] + + self.assertEqual(availability.region, "ap-west") + self.assertEqual(availability.unavailable, []) + class BetaProgramGroupTest(ClientBaseCase): """ diff --git a/test/unit/objects/account_test.py b/test/unit/objects/account_test.py index f58aa677d..0f53240f4 100644 --- a/test/unit/objects/account_test.py +++ b/test/unit/objects/account_test.py @@ -3,6 +3,7 @@ from linode_api4.objects import ( Account, + AccountAvailability, AccountBetaProgram, AccountSettings, Database, @@ -260,3 +261,20 @@ def test_account_beta_program_api_get(self): self.assertEqual(beta.ended, datetime(2018, 1, 2, 3, 4, 5)) self.assertEqual(m.call_url, account_beta_url) + + +class AccountAvailabilityTest(ClientBaseCase): + """ + Test methods of the AccountAvailability + """ + + def test_account_availability_api_get(self): + region_id = "us-east" + account_availability_url = "/account/availability/{}".format(region_id) + + with self.mock_get(account_availability_url) as m: + availability = AccountAvailability(self.client, region_id) + self.assertEqual(availability.region, region_id) + self.assertEqual(availability.unavailable, []) + + self.assertEqual(m.call_url, account_availability_url) From e6718c0b81fb78ceda589a330de74c6714915428 Mon Sep 17 00:00:00 2001 From: Jacob Riddle <87780794+jriddle-linode@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:21:18 -0500 Subject: [PATCH 13/19] new: add support for listing `nodebalancer` `firewalls` (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 Description **What does this PR do and why is this change necessary?** Add support for `nodebalancers/{nb_id}/firewalls` endpoint ## ✔️ How to Test **How do I run the relevant unit/integration tests?** ```bash make testunit ``` --- linode_api4/objects/nodebalancer.py | 20 ++++++- .../nodebalancers_12345_firewalls.json | 56 +++++++++++++++++++ test/unit/objects/nodebalancers_test.py | 12 ++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/nodebalancers_12345_firewalls.json diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index 3f9b8e8b6..ca4228d16 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -9,7 +9,7 @@ Property, Region, ) -from linode_api4.objects.networking import IPAddress +from linode_api4.objects.networking import Firewall, IPAddress class NodeBalancerNode(DerivedBase): @@ -303,3 +303,21 @@ def statistics(self): "Unexpected response generating stats!", json=result ) return MappedObject(**result) + + def firewalls(self): + """ + View Firewall information for Firewalls associated with this NodeBalancer. + + API Documentation: https://www.linode.com/docs/api/nodebalancers/#nodebalancer-firewalls-list + + :returns: A List of Firewalls of the Linode NodeBalancer. + :rtype: List[Firewall] + """ + result = self._client.get( + "{}/firewalls".format(NodeBalancer.api_endpoint), model=self + ) + + return [ + Firewall(self._client, firewall["id"]) + for firewall in result["data"] + ] diff --git a/test/fixtures/nodebalancers_12345_firewalls.json b/test/fixtures/nodebalancers_12345_firewalls.json new file mode 100644 index 000000000..17a4a9199 --- /dev/null +++ b/test/fixtures/nodebalancers_12345_firewalls.json @@ -0,0 +1,56 @@ +{ + "data": [ + { + "created": "2018-01-01T00:01:01", + "id": 123, + "label": "firewall123", + "rules": { + "inbound": [ + { + "action": "ACCEPT", + "addresses": { + "ipv4": [ + "192.0.2.0/24" + ], + "ipv6": [ + "2001:DB8::/32" + ] + }, + "description": "An example firewall rule description.", + "label": "firewallrule123", + "ports": "22-24, 80, 443", + "protocol": "TCP" + } + ], + "inbound_policy": "DROP", + "outbound": [ + { + "action": "ACCEPT", + "addresses": { + "ipv4": [ + "192.0.2.0/24" + ], + "ipv6": [ + "2001:DB8::/32" + ] + }, + "description": "An example firewall rule description.", + "label": "firewallrule123", + "ports": "22-24, 80, 443", + "protocol": "TCP" + } + ], + "outbound_policy": "DROP" + }, + "status": "enabled", + "tags": [ + "example tag", + "another example" + ], + "updated": "2018-01-02T00:01:01" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/test/unit/objects/nodebalancers_test.py b/test/unit/objects/nodebalancers_test.py index a02054aa4..24f702f7f 100644 --- a/test/unit/objects/nodebalancers_test.py +++ b/test/unit/objects/nodebalancers_test.py @@ -193,3 +193,15 @@ def test_statistics(self): "linode.com - balancer12345 (12345) - day (5 min avg)", ) self.assertEqual(m.call_url, statistics_url) + + def test_firewalls(self): + """ + Test that you can get the firewalls for the requested NodeBalancer. + """ + nb = NodeBalancer(self.client, 12345) + firewalls_url = "/nodebalancers/12345/firewalls" + + with self.mock_get(firewalls_url) as m: + result = nb.firewalls() + self.assertEqual(m.call_url, firewalls_url) + self.assertEqual(len(result), 1) From fd3a96e70aafa926e9e4040f2257dc3ecb79a7d7 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:52:46 -0500 Subject: [PATCH 14/19] fix: Resolve issue that prevented LKENodePools from being loaded with LinodeClient.load(...) (#359) * Fix issue that prevent LinodeClient.load(...) from working with LKENodePool * make format --- linode_api4/objects/lke.py | 7 +++++-- test/unit/objects/lke_test.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index b7edf181a..647080f44 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -81,9 +81,12 @@ def _populate(self, json): """ Parse Nodes into more useful LKENodePoolNode objects """ - if json != {}: + if json is not None and json != {}: new_nodes = [ - LKENodePoolNode(self._client, c) for c in json["nodes"] + LKENodePoolNode(self._client, c) + if not isinstance(c, dict) + else c + for c in json["nodes"] ] json["nodes"] = new_nodes diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index 5c9902b38..03afc1983 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -132,3 +132,17 @@ def test_service_token_delete(self): with self.mock_delete() as m: cluster.service_token_delete() self.assertEqual(m.call_url, "/lke/clusters/18881/servicetoken") + + def test_load_node_pool(self): + """ + Tests that an LKE Node Pool can be retrieved using LinodeClient.load(...) + """ + pool = self.client.load(LKENodePool, 456, 18881) + + self.assertEqual(pool.id, 456) + self.assertEqual(pool.cluster_id, 18881) + self.assertEqual(pool.type.id, "g6-standard-4") + self.assertIsNotNone(pool.disks) + self.assertIsNotNone(pool.nodes) + self.assertIsNotNone(pool.autoscaler) + self.assertIsNotNone(pool.tags) From 38cac2bdd752c69d660fef887d35332afe5aea09 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Mon, 18 Dec 2023 09:53:48 -0800 Subject: [PATCH 15/19] test: update legacy regions with new compute regions (#358) * update legacy regions with new compute regions * update volume region for test_attach_volume_to_linode --- test/integration/conftest.py | 6 +++--- .../linode_client/test_linode_client.py | 9 +++------ test/integration/models/test_account.py | 2 +- test/integration/models/test_database.py | 4 ++-- test/integration/models/test_firewall.py | 2 +- test/integration/models/test_image.py | 2 +- test/integration/models/test_linode.py | 16 ++++++++-------- test/integration/models/test_networking.py | 2 +- test/integration/models/test_nodebalancer.py | 4 ++-- test/integration/models/test_volume.py | 2 +- 10 files changed, 23 insertions(+), 26 deletions(-) diff --git a/test/integration/conftest.py b/test/integration/conftest.py index ae3550eb7..0a3344398 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -54,7 +54,7 @@ def run_long_tests(): def create_linode(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp @@ -71,7 +71,7 @@ def create_linode(test_linode_client): def create_linode_for_pass_reset(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp @@ -155,7 +155,7 @@ def test_domain(test_linode_client): def test_volume(test_linode_client): client = test_linode_client timestamp = str(time.time_ns()) - region = client.regions()[0] + region = client.regions()[4] label = "TestSDK-" + timestamp volume = client.volume_create(label=label, region=region) diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 08b7e2383..60eb901b4 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -12,7 +12,7 @@ def setup_client_and_linode(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] # us-ord (Chicago) label = get_test_label() linode_instance, password = client.linode.instance_create( @@ -33,9 +33,6 @@ def test_get_account(setup_client_and_linode): assert re.search( "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", account.email ) - assert re.search( - "^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$", account.phone - ) assert re.search("^$|[a-zA-Z0-9]+", account.address_1) assert re.search("^$|[a-zA-Z0-9]+", account.address_2) assert re.search("^$|[a-zA-Z]+", account.city) @@ -212,7 +209,7 @@ def test_get_account_settings(test_linode_client): def test_create_linode_instance_without_image(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() linode_instance = client.linode.instance_create( @@ -297,7 +294,7 @@ def test_fails_to_create_cluster_with_invalid_version(test_linode_client): try: cluster = client.lke.cluster_create( - "ap-west", + "us-ord", "example-cluster", {"type": "g6-standard-1", "count": 3}, invalid_version, diff --git a/test/integration/models/test_account.py b/test/integration/models/test_account.py index 9c2efc787..3d5fa2d97 100644 --- a/test/integration/models/test_account.py +++ b/test/integration/models/test_account.py @@ -64,7 +64,7 @@ def test_latest_get_event(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() linode, password = client.linode.instance_create( diff --git a/test/integration/models/test_database.py b/test/integration/models/test_database.py index 7cd41be66..0e14f5041 100644 --- a/test/integration/models/test_database.py +++ b/test/integration/models/test_database.py @@ -40,7 +40,7 @@ def test_create_sql_db(test_linode_client): ) client = test_linode_client label = get_test_label() + "-sqldb" - region = "us-east" + region = "us-ord" engine_id = get_db_engine_id(client, "mysql") dbtype = "g6-standard-1" @@ -70,7 +70,7 @@ def test_create_postgres_db(test_linode_client): ) client = test_linode_client label = get_test_label() + "-postgresqldb" - region = "us-east" + region = "us-ord" engine_id = get_db_engine_id(client, "postgresql") dbtype = "g6-standard-1" diff --git a/test/integration/models/test_firewall.py b/test/integration/models/test_firewall.py index e9e7b8bcc..7a7f58ff1 100644 --- a/test/integration/models/test_firewall.py +++ b/test/integration/models/test_firewall.py @@ -9,7 +9,7 @@ def linode_fw(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = "linode_instance_fw_device" linode_instance, password = client.linode.instance_create( diff --git a/test/integration/models/test_image.py b/test/integration/models/test_image.py index 239e65784..a622b355e 100644 --- a/test/integration/models/test_image.py +++ b/test/integration/models/test_image.py @@ -42,7 +42,7 @@ def test_image_create_upload(test_linode_client): label = get_test_label() + "_image" image = test_linode_client.image_upload( label, - "us-east", + "us-ord", BytesIO(test_image_content), description="integration test image upload", ) diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 50497a706..9429ebaf6 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -24,7 +24,7 @@ def linode_with_volume_firewall(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() rules = { @@ -70,7 +70,7 @@ def linode_with_volume_firewall(test_linode_client): def linode_for_network_interface_tests(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp @@ -87,7 +87,7 @@ def linode_for_network_interface_tests(test_linode_client): def linode_for_disk_tests(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() linode_instance, password = client.linode.instance_create( @@ -118,7 +118,7 @@ def linode_for_disk_tests(test_linode_client): def create_linode_for_long_running_tests(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() linode_instance, password = client.linode.instance_create( @@ -158,7 +158,7 @@ def test_linode_transfer(test_linode_client, linode_with_volume_firewall): def test_linode_rebuild(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() + "_rebuild" linode, password = client.linode.instance_create( @@ -208,7 +208,7 @@ def test_update_linode(create_linode): def test_delete_linode(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() linode_instance, password = client.linode.instance_create( @@ -413,7 +413,7 @@ def test_linode_ips(create_linode): def test_linode_initate_migration(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_test_label() + "_migration" linode, password = client.linode.instance_create( @@ -424,7 +424,7 @@ def test_linode_initate_migration(test_linode_client): # Says it could take up to ~6 hrs for migration to fully complete send_request_when_resource_available( - 300, linode.initiate_migration, "us-central" + 300, linode.initiate_migration, "us-mia" ) res = linode.delete() diff --git a/test/integration/models/test_networking.py b/test/integration/models/test_networking.py index 4bd994f38..d9f13063e 100644 --- a/test/integration/models/test_networking.py +++ b/test/integration/models/test_networking.py @@ -20,7 +20,7 @@ def test_get_networking_rules(test_linode_client, test_firewall): def create_linode(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = get_rand_nanosec_test_label() linode_instance, _ = client.linode.instance_create( diff --git a/test/integration/models/test_nodebalancer.py b/test/integration/models/test_nodebalancer.py index 332f10214..6cec442b4 100644 --- a/test/integration/models/test_nodebalancer.py +++ b/test/integration/models/test_nodebalancer.py @@ -10,7 +10,7 @@ def linode_with_private_ip(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = "linode_with_privateip" linode_instance, password = client.linode.instance_create( @@ -30,7 +30,7 @@ def linode_with_private_ip(test_linode_client): def create_nb_config(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] label = "nodebalancer_test" nb = client.nodebalancer_create(region=chosen_region, label=label) diff --git a/test/integration/models/test_volume.py b/test/integration/models/test_volume.py index ca63cb105..1b351c14d 100644 --- a/test/integration/models/test_volume.py +++ b/test/integration/models/test_volume.py @@ -16,7 +16,7 @@ def linode_for_volume(test_linode_client): client = test_linode_client available_regions = client.regions() - chosen_region = available_regions[0] + chosen_region = available_regions[4] timestamp = str(time.time_ns()) label = "TestSDK-" + timestamp From 33d70542ac8e2f135b4cc1f05e8b592dd5eb9904 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:41:15 -0500 Subject: [PATCH 16/19] Project: Unified Migration (#364) --------- Co-authored-by: Ania Misiorek Co-authored-by: Ania Misiorek <139170033+amisiorek-akamai@users.noreply.github.com> --- linode_api4/objects/linode.py | 31 +++++++++++++++++++++-- test/integration/helpers.py | 11 ++++---- test/integration/models/test_linode.py | 35 ++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 6ddbd9fe2..a25ed082c 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -612,6 +612,11 @@ def _interface_create(self, body: Dict[str, Any]) -> NetworkInterface: return i +class MigrationType: + COLD = "cold" + WARM = "warm" + + class Instance(Base): """ A Linode Instance. @@ -948,7 +953,13 @@ def reboot(self): return False return True - def resize(self, new_type, allow_auto_disk_resize=True, **kwargs): + def resize( + self, + new_type, + allow_auto_disk_resize=True, + migration_type: MigrationType = MigrationType.COLD, + **kwargs, + ): """ Resizes a Linode you have the read_write permission to a different Type. If any actions are currently running or queued, those actions must be completed first @@ -970,6 +981,10 @@ def resize(self, new_type, allow_auto_disk_resize=True, **kwargs): data must fit within the smaller disk size. Defaults to true. :type: allow_auto_disk_resize: bool + :param migration_type: Type of migration to be used when resizing a Linode. + Customers can choose between warm and cold, the default type is cold. + :type: migration_type: str + :returns: True if the operation was successful. :rtype: bool """ @@ -979,6 +994,7 @@ def resize(self, new_type, allow_auto_disk_resize=True, **kwargs): params = { "type": new_type, "allow_auto_disk_resize": allow_auto_disk_resize, + "migration_type": migration_type, } params.update(kwargs) @@ -1438,7 +1454,12 @@ def mutate(self, allow_auto_disk_resize=True): return True - def initiate_migration(self, region=None, upgrade=None): + def initiate_migration( + self, + region=None, + upgrade=None, + migration_type: MigrationType = MigrationType.COLD, + ): """ Initiates a pending migration that is already scheduled for this Linode Instance @@ -1459,10 +1480,16 @@ def initiate_migration(self, region=None, upgrade=None): region field does not allow upgrades, then the endpoint will return a 400 error code and the migration will not be performed. :type: upgrade: bool + + :param migration_type: The type of migration that will be used for this Linode migration. + Customers can only use this param when activating a support-created migration. + Customers can choose between a cold and warm migration, cold is the default type. + :type: mirgation_type: str """ params = { "region": region.id if issubclass(type(region), Base) else region, "upgrade": upgrade, + "type": migration_type, } util.drop_null_keys(params) diff --git a/test/integration/helpers.py b/test/integration/helpers.py index c178ad4dd..3a7217a95 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -1,4 +1,3 @@ -import random import time from typing import Callable @@ -8,7 +7,7 @@ def get_test_label(): - unique_timestamp = str(int(time.time()) + random.randint(0, 1000)) + unique_timestamp = str(time.time_ns()) label = "IntTestSDK_" + unique_timestamp return label @@ -94,13 +93,13 @@ def retry_sending_request(retries: int, condition: Callable, *args) -> object: def send_request_when_resource_available( - timeout: int, func: Callable, *args + timeout: int, func: Callable, *args, **kwargs ) -> object: start_time = time.time() while True: try: - res = func(*args) + res = func(*args, **kwargs) return res except ApiError as e: if ( @@ -110,9 +109,9 @@ def send_request_when_resource_available( ): if time.time() - start_time > timeout: raise TimeoutError( - "Timeout Error: resource is not available in" + "Timeout Error: resource is not available in " + str(timeout) - + "seconds" + + " seconds" ) time.sleep(10) else: diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 9429ebaf6..9bb41a116 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -18,6 +18,7 @@ Instance, Type, ) +from linode_api4.objects.linode import MigrationType @pytest.fixture(scope="session") @@ -302,6 +303,29 @@ def test_linode_resize_with_class( assert linode.status == "running" +def test_linode_resize_with_migration_type( + create_linode_for_long_running_tests, +): + linode = create_linode_for_long_running_tests + m_type = MigrationType.WARM + + wait_for_condition(10, 100, get_status, linode, "running") + + time.sleep(5) + res = linode.resize(new_type="g6-standard-1", migration_type=m_type) + + assert res + + wait_for_condition(10, 300, get_status, linode, "resizing") + + assert linode.status == "resizing" + + # Takes about 3-5 minute to resize, sometimes longer... + wait_for_condition(30, 600, get_status, linode, "running") + + assert linode.status == "running" + + def test_linode_boot_with_config(create_linode): linode = create_linode @@ -416,15 +440,16 @@ def test_linode_initate_migration(test_linode_client): chosen_region = available_regions[4] label = get_test_label() + "_migration" - linode, password = client.linode.instance_create( - "g6-nanode-1", chosen_region, image="linode/debian10", label=label + linode, _ = client.linode.instance_create( + "g6-nanode-1", chosen_region, image="linode/debian12", label=label ) - wait_for_condition(10, 100, get_status, linode, "running") # Says it could take up to ~6 hrs for migration to fully complete - send_request_when_resource_available( - 300, linode.initiate_migration, "us-mia" + 300, + linode.initiate_migration, + region="us-mia", + migration_type=MigrationType.COLD, ) res = linode.delete() From b56ee0e808008c36b4cd7edc27a7151083cb7768 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 3 Jan 2024 10:15:10 -0800 Subject: [PATCH 17/19] test: move test upload logic to using submodule with external repository (#366) * move test upload logic to git submodule, and use it in e2e workflow * update script folder name * checkout submodule in workflow * change submodule name --- .github/workflows/e2e-test-pr.yml | 6 +- .gitmodules | 3 + test/script/add_to_xml_test_report.py | 75 ------------------------ test/script/test_report_upload_script.py | 43 -------------- tod_scripts | 1 + 5 files changed, 8 insertions(+), 120 deletions(-) create mode 100644 .gitmodules delete mode 100644 test/script/add_to_xml_test_report.py delete mode 100644 test/script/test_report_upload_script.py create mode 160000 tod_scripts diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index ba0ff6cc4..e0f9c0888 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -35,6 +35,8 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ inputs.sha }} + fetch-depth: 0 + submodules: 'recursive' - name: Get the hash value of the latest commit from the PR branch uses: octokit/graphql-action@v2.x @@ -94,7 +96,7 @@ jobs: - name: Add additional information to XML report run: | filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') - python test/script/add_to_xml_test_report.py \ + python tod_scripts/add_to_xml_test_report.py \ --branch_name "${GITHUB_REF#refs/*/}" \ --gha_run_id "$GITHUB_RUN_ID" \ --gha_run_number "$GITHUB_RUN_NUMBER" \ @@ -103,7 +105,7 @@ jobs: - name: Upload test results run: | report_filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') - python3 test/script/test_report_upload_script.py "${report_filename}" + python3 tod_scripts/test_report_upload_script.py "${report_filename}" env: LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }} LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..df7dc11d7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tod_scripts"] + path = tod_scripts + url = https://github.com/linode/TOD-test-report-uploader.git diff --git a/test/script/add_to_xml_test_report.py b/test/script/add_to_xml_test_report.py deleted file mode 100644 index d978e396f..000000000 --- a/test/script/add_to_xml_test_report.py +++ /dev/null @@ -1,75 +0,0 @@ -import argparse -import xml.etree.ElementTree as ET - -import requests - -latest_release_url = ( - "https://api.github.com/repos/linode/linode_api4-python/releases/latest" -) - - -def get_release_version(): - url = latest_release_url - - try: - response = requests.get(url) - response.raise_for_status() # Check for HTTP errors - - release_info = response.json() - version = release_info["tag_name"] - - # Remove 'v' prefix if it exists - if version.startswith("v"): - version = version[1:] - - return str(version) - - except requests.exceptions.RequestException as e: - print("Error:", e) - except KeyError: - print("Error: Unable to fetch release information from GitHub API.") - - -# Parse command-line arguments -parser = argparse.ArgumentParser( - description="Modify XML with workflow information" -) -parser.add_argument("--branch_name", required=True) -parser.add_argument("--gha_run_id", required=True) -parser.add_argument("--gha_run_number", required=True) -parser.add_argument("--release_tag", required=False) -parser.add_argument( - "--xmlfile", required=True -) # Added argument for XML file path - -args = parser.parse_args() - -# Open and parse the XML file -xml_file_path = args.xmlfile -tree = ET.parse(xml_file_path) -root = tree.getroot() - -# Create new elements for the information -branch_name_element = ET.Element("branch_name") -branch_name_element.text = args.branch_name - -gha_run_id_element = ET.Element("gha_run_id") -gha_run_id_element.text = args.gha_run_id - -gha_run_number_element = ET.Element("gha_run_number") -gha_run_number_element.text = args.gha_run_number - -gha_release_tag_element = ET.Element("release_tag") -gha_release_tag_element.text = get_release_version() - -# Add the new elements to the root of the XML -root.append(branch_name_element) -root.append(gha_run_id_element) -root.append(gha_run_number_element) -root.append(gha_release_tag_element) - -# Save the modified XML -modified_xml_file_path = xml_file_path # Overwrite it -tree.write(modified_xml_file_path) - -print(f"Modified XML saved to {modified_xml_file_path}") diff --git a/test/script/test_report_upload_script.py b/test/script/test_report_upload_script.py deleted file mode 100644 index 5dd1a9e31..000000000 --- a/test/script/test_report_upload_script.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import sys - -import boto3 -from botocore.exceptions import NoCredentialsError - -ACCESS_KEY = os.environ.get("LINODE_CLI_OBJ_ACCESS_KEY") -SECRET_KEY = os.environ.get("LINODE_CLI_OBJ_SECRET_KEY") -BUCKET_NAME = "dx-test-results" - -linode_obj_config = { - "aws_access_key_id": ACCESS_KEY, - "aws_secret_access_key": SECRET_KEY, - "endpoint_url": "https://us-southeast-1.linodeobjects.com", -} - - -def upload_to_linode_object_storage(file_name): - try: - s3 = boto3.client("s3", **linode_obj_config) - - s3.upload_file(Filename=file_name, Bucket=BUCKET_NAME, Key=file_name) - - print(f"Successfully uploaded {file_name} to Linode Object Storage.") - - except NoCredentialsError: - print( - "Credentials not available. Ensure you have set your AWS credentials." - ) - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python upload_to_linode.py ") - sys.exit(1) - - file_name = sys.argv[1] - - if not file_name: - print("Error: The provided file name is empty or invalid.") - sys.exit(1) - - upload_to_linode_object_storage(file_name) diff --git a/tod_scripts b/tod_scripts new file mode 160000 index 000000000..eec4b9955 --- /dev/null +++ b/tod_scripts @@ -0,0 +1 @@ +Subproject commit eec4b99557cef6f40e8b5b7de00357dc49fb041c From d8c3b8a72f78ab55db3d1786be4145b33f751044 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:19:37 -0500 Subject: [PATCH 18/19] Fix issue loading Type using LinodeClient.load(...) (#365) --- linode_api4/objects/linode.py | 3 +- test/fixtures/linode_types_g6-nanode-1.json | 48 +++++++++++++++++++++ test/unit/objects/linode_test.py | 9 ++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/linode_types_g6-nanode-1.json diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index a25ed082c..d771aaeeb 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -256,9 +256,10 @@ def _populate(self, json): """ Allows changing the name "class" in JSON to "type_class" in python """ + super()._populate(json) - if "class" in json: + if json is not None and "class" in json: setattr(self, "type_class", json["class"]) else: setattr(self, "type_class", None) diff --git a/test/fixtures/linode_types_g6-nanode-1.json b/test/fixtures/linode_types_g6-nanode-1.json new file mode 100644 index 000000000..8fc590638 --- /dev/null +++ b/test/fixtures/linode_types_g6-nanode-1.json @@ -0,0 +1,48 @@ +{ + "disk": 20480, + "memory": 1024, + "transfer": 1000, + "addons": { + "backups": { + "price": { + "hourly": 0.003, + "monthly": 2 + }, + "region_prices": [ + { + "id": "ap-west", + "hourly": 0.02, + "monthly": 20 + }, + { + "id": "ap-northeast", + "hourly": 0.02, + "monthly": 20 + } + ] + } + }, + "class": "nanode", + "network_out": 1000, + "vcpus": 1, + "gpus": 0, + "id": "g5-nanode-1", + "label": "Linode 1024", + "price": { + "hourly": 0.0075, + "monthly": 5 + }, + "region_prices": [ + { + "id": "us-east", + "hourly": 0.02, + "monthly": 20 + }, + { + "id": "ap-northeast", + "hourly": 0.02, + "monthly": 20 + } + ], + "successor": null +} diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 951bd561f..62a043edb 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -580,6 +580,15 @@ def test_get_type_gpu(self): self.assertEqual(t.gpus, 1) self.assertEqual(t._populated, True) + def test_load_type(self): + """ + Tests that a type can be loaded using LinodeClient.load(...) + """ + + t = self.client.load(Type, "g6-nanode-1") + self.assertEqual(t._populated, True) + self.assertEqual(t.type_class, "nanode") + def test_save_noforce(self): """ Tests that a client will only save if changes are detected From d09d2eac217bdd7a883d8d9651648239c2fb1e1f Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Mon, 8 Jan 2024 08:25:17 -0800 Subject: [PATCH 19/19] test: Migrate from g5 to g6 instances, fix test failures caused by invalid labels (#367) * move test upload logic to git submodule, and use it in e2e workflow * update script folder name * migrate g5 to g6 and fix label too long error in tests * remove test script folder --- test/fixtures/linode_instances.json | 2 +- test/fixtures/linode_types.json | 6 +++--- test/fixtures/tags_something.json | 2 +- test/integration/conftest.py | 2 +- test/integration/helpers.py | 8 ++++---- test/integration/linode_client/test_linode_client.py | 2 +- test/integration/models/test_linode.py | 4 ++-- test/unit/linode_client_test.py | 10 +++++----- test/unit/objects/linode_test.py | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/fixtures/linode_instances.json b/test/fixtures/linode_instances.json index efb502e7e..3d257938d 100644 --- a/test/fixtures/linode_instances.json +++ b/test/fixtures/linode_instances.json @@ -8,7 +8,7 @@ "hypervisor": "kvm", "id": 123, "status": "running", - "type": "g5-standard-1", + "type": "g6-standard-1", "alerts": { "network_in": 5, "network_out": 5, diff --git a/test/fixtures/linode_types.json b/test/fixtures/linode_types.json index c864082e8..819867b79 100644 --- a/test/fixtures/linode_types.json +++ b/test/fixtures/linode_types.json @@ -31,7 +31,7 @@ "network_out": 1000, "vcpus": 1, "gpus": 0, - "id": "g5-nanode-1", + "id": "g6-nanode-1", "label": "Linode 1024", "price": { "hourly": 0.0075, @@ -127,7 +127,7 @@ "network_out": 1000, "vcpus": 1, "gpus": 0, - "id": "g5-standard-1", + "id": "g6-standard-1", "label": "Linode 2048", "price": { "hourly": 0.015, @@ -175,7 +175,7 @@ "network_out": 1000, "vcpus": 2, "gpus": 1, - "id": "g5-gpu-2", + "id": "g6-gpu-2", "label": "Linode 4096", "price": { "hourly": 0.03, diff --git a/test/fixtures/tags_something.json b/test/fixtures/tags_something.json index 67bf59097..7cce51301 100644 --- a/test/fixtures/tags_something.json +++ b/test/fixtures/tags_something.json @@ -10,7 +10,7 @@ "hypervisor": "kvm", "id": 123, "status": "running", - "type": "g5-standard-1", + "type": "g6-standard-1", "alerts": { "network_in": 5, "network_out": 5, diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 0a3344398..93cff7867 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -308,7 +308,7 @@ def create_vpc_with_subnet_and_linode( label = "TestSDK-" + timestamp instance, password = test_linode_client.linode.instance_create( - "g5-standard-4", vpc.region, image="linode/debian11", label=label + "g6-standard-1", vpc.region, image="linode/debian11", label=label ) yield vpc, subnet, instance, password diff --git a/test/integration/helpers.py b/test/integration/helpers.py index 3a7217a95..5e9d1c441 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -7,14 +7,14 @@ def get_test_label(): - unique_timestamp = str(time.time_ns()) - label = "IntTestSDK_" + unique_timestamp + unique_timestamp = str(time.time_ns())[:-3] + label = "test_" + unique_timestamp return label def get_rand_nanosec_test_label(): - unique_timestamp = str(time.time_ns()) - label = "IntTestSDK_" + unique_timestamp + unique_timestamp = str(time.time_ns())[:-3] + label = "test_" + unique_timestamp return label diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 60eb901b4..e68f54eb2 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -276,7 +276,7 @@ def test_cluster_create_with_api_objects(test_linode_client): version = client.lke.versions()[0] region = client.regions().first() node_pools = client.lke.node_pool(node_type, 3) - label = get_test_label() + "-cluster" + label = get_test_label() cluster = client.lke.cluster_create(region, label, node_pools, version) diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 9bb41a116..f46a3fc29 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -349,7 +349,7 @@ def test_linode_firewalls(linode_with_volume_firewall): firewalls = linode.firewalls() assert len(firewalls) > 0 - assert "TestSDK" in firewalls[0].label + assert "test" in firewalls[0].label def test_linode_volumes(linode_with_volume_firewall): @@ -358,7 +358,7 @@ def test_linode_volumes(linode_with_volume_firewall): volumes = linode.volumes() assert len(volumes) > 0 - assert "TestSDK" in volumes[0].label + assert "test" in volumes[0].label def wait_for_disk_status(disk: Disk, timeout): diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 1b8924f47..3f331c9b7 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -559,7 +559,7 @@ def test_instance_create(self): """ with self.mock_post("linode/instances/123") as m: l = self.client.linode.instance_create( - "g5-standard-1", "us-east-1a" + "g6-standard-1", "us-east-1a" ) self.assertIsNotNone(l) @@ -568,7 +568,7 @@ def test_instance_create(self): self.assertEqual(m.call_url, "/linode/instances") self.assertEqual( - m.call_data, {"region": "us-east-1a", "type": "g5-standard-1"} + m.call_data, {"region": "us-east-1a", "type": "g6-standard-1"} ) def test_instance_create_with_image(self): @@ -577,7 +577,7 @@ def test_instance_create_with_image(self): """ with self.mock_post("linode/instances/123") as m: l, pw = self.client.linode.instance_create( - "g5-standard-1", "us-east-1a", image="linode/debian9" + "g6-standard-1", "us-east-1a", image="linode/debian9" ) self.assertIsNotNone(l) @@ -589,7 +589,7 @@ def test_instance_create_with_image(self): m.call_data, { "region": "us-east-1a", - "type": "g5-standard-1", + "type": "g6-standard-1", "image": "linode/debian9", "root_pass": pw, }, @@ -708,7 +708,7 @@ def test_cluster_create_with_api_objects(self): ) self.assertEqual(m.call_data["region"], "ap-west") self.assertEqual( - m.call_data["node_pools"], [{"type": "g5-nanode-1", "count": 3}] + m.call_data["node_pools"], [{"type": "g6-nanode-1", "count": 3}] ) self.assertEqual(m.call_data["k8s_version"], "1.19") diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 62a043edb..4121380e4 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -560,7 +560,7 @@ def test_get_type_by_id(self): """ Tests that a Linode type is loaded correctly by ID """ - t = Type(self.client, "g5-nanode-1") + t = Type(self.client, "g6-nanode-1") self.assertEqual(t._populated, False) self.assertEqual(t.vcpus, 1) @@ -574,7 +574,7 @@ def test_get_type_gpu(self): """ Tests that gpu types load up right """ - t = Type(self.client, "g5-gpu-2") + t = Type(self.client, "g6-gpu-2") self.assertEqual(t._populated, False) self.assertEqual(t.gpus, 1)