From c8f4bd59dcf2207f81a291d43c6c7fe1ccd5fbc5 Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:57:32 -0400 Subject: [PATCH 1/7] new: List ips under a specific VPC (#391) * list ips under a vpc * fix --- linode_api4/objects/vpc.py | 21 ++++++++++++++++ test/fixtures/vpcs_123456_ips.json | 34 ++++++++++++++++++++++++++ test/integration/conftest.py | 16 ------------ test/integration/models/test_linode.py | 9 ++++++- test/unit/objects/vpc_test.py | 25 +++++++++++++++++++ 5 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/vpcs_123456_ips.json diff --git a/linode_api4/objects/vpc.py b/linode_api4/objects/vpc.py index 989c542ee..682b7a0ab 100644 --- a/linode_api4/objects/vpc.py +++ b/linode_api4/objects/vpc.py @@ -4,6 +4,7 @@ from linode_api4.errors import UnexpectedResponseError from linode_api4.objects import Base, DerivedBase, Property, Region from linode_api4.objects.serializable import JSONObject +from linode_api4.paginated_list import PaginatedList @dataclass @@ -97,3 +98,23 @@ def subnet_create( d = VPCSubnet(self._client, result["id"], self.id, result) return d + + @property + def ips(self, *filters) -> PaginatedList: + """ + Get all the IP addresses under this VPC. + + API Documentation: TODO + + :returns: A list of VPCIPAddresses the acting user can access. + :rtype: PaginatedList of VPCIPAddress + """ + + # need to avoid circular import + from linode_api4.objects import ( # pylint: disable=import-outside-toplevel + VPCIPAddress, + ) + + return self._client._get_and_filter( + VPCIPAddress, *filters, endpoint="/vpcs/{}/ips".format(self.id) + ) diff --git a/test/fixtures/vpcs_123456_ips.json b/test/fixtures/vpcs_123456_ips.json new file mode 100644 index 000000000..70b4b8a60 --- /dev/null +++ b/test/fixtures/vpcs_123456_ips.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "address": "10.0.0.2", + "address_range": null, + "vpc_id": 123456, + "subnet_id": 654321, + "region": "us-ord", + "linode_id": 111, + "config_id": 222, + "interface_id": 333, + "active": true, + "nat_1_1": null, + "gateway": "10.0.0.1", + "prefix": 8, + "subnet_mask": "255.0.0.0" + }, + { + "address": "10.0.0.3", + "address_range": null, + "vpc_id": 41220, + "subnet_id": 41184, + "region": "us-ord", + "linode_id": 56323949, + "config_id": 59467106, + "interface_id": 1248358, + "active": true, + "nat_1_1": null, + "gateway": "10.0.0.1", + "prefix": 8, + "subnet_mask": "255.0.0.0" + } + ] +} diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 93cff7867..03295e59c 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -316,22 +316,6 @@ def create_vpc_with_subnet_and_linode( 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 diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 40d1e735f..ce122226a 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -15,7 +15,6 @@ ConfigInterface, ConfigInterfaceIPv4, Disk, - Image, Instance, Type, ) @@ -643,6 +642,14 @@ def test_create_vpc( ) assert all_vpc_ips[0].dict == vpc_ip.dict + # Test getting the ips under this specific VPC + vpc_ips = vpc.ips + + assert len(vpc_ips) > 0 + assert vpc_ips[0].vpc_id == vpc.id + assert vpc_ips[0].linode_id == linode.id + assert vpc_ips[0].nat_1_1 == linode.ips.ipv4.public[0].address + def test_update_vpc( self, linode_for_network_interface_tests, diff --git a/test/unit/objects/vpc_test.py b/test/unit/objects/vpc_test.py index 830e9fb9f..7e4963d33 100644 --- a/test/unit/objects/vpc_test.py +++ b/test/unit/objects/vpc_test.py @@ -173,3 +173,28 @@ def validate_vpc_subnet_789(self, subnet: VPCSubnet): self.assertEqual(subnet.linodes[0].id, 12345) self.assertEqual(subnet.created, expected_dt) self.assertEqual(subnet.updated, expected_dt) + + def test_list_vpc_ips(self): + """ + Test that the ips under a specific VPC can be listed. + """ + vpc = VPC(self.client, 123456) + vpc_ips = vpc.ips + + self.assertGreater(len(vpc_ips), 0) + + vpc_ip = vpc_ips[0] + + self.assertEqual(vpc_ip.vpc_id, vpc.id) + self.assertEqual(vpc_ip.address, "10.0.0.2") + self.assertEqual(vpc_ip.address_range, None) + self.assertEqual(vpc_ip.subnet_id, 654321) + self.assertEqual(vpc_ip.region, "us-ord") + self.assertEqual(vpc_ip.linode_id, 111) + self.assertEqual(vpc_ip.config_id, 222) + self.assertEqual(vpc_ip.interface_id, 333) + self.assertEqual(vpc_ip.active, True) + self.assertEqual(vpc_ip.nat_1_1, None) + self.assertEqual(vpc_ip.gateway, "10.0.0.1") + self.assertEqual(vpc_ip.prefix, 8) + self.assertEqual(vpc_ip.subnet_mask, "255.0.0.0") From bcb66cb30270f7815fdcf37f3aa23dc2bc64b5ed Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:21:20 -0400 Subject: [PATCH 2/7] Annotate set fields as unordered (#390) --- linode_api4/objects/account.py | 4 ++-- linode_api4/objects/base.py | 6 ++++++ linode_api4/objects/database.py | 6 +++--- linode_api4/objects/domain.py | 6 +++--- linode_api4/objects/image.py | 4 +++- linode_api4/objects/linode.py | 8 +++++--- linode_api4/objects/lke.py | 4 ++-- linode_api4/objects/networking.py | 8 +++++--- linode_api4/objects/nodebalancer.py | 4 ++-- linode_api4/objects/region.py | 2 +- linode_api4/objects/volume.py | 2 +- linode_api4/objects/vpc.py | 2 +- 12 files changed, 34 insertions(+), 22 deletions(-) diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index f50c4da32..349331be3 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -43,7 +43,7 @@ class Account(Base): "zip": Property(mutable=True), "address_2": Property(mutable=True), "tax_id": Property(mutable=True), - "capabilities": Property(), + "capabilities": Property(unordered=True), "credit_card": Property(), "active_promotions": Property(), "active_since": Property(), @@ -670,5 +670,5 @@ class AccountAvailability(Base): properties = { "region": Property(identifier=True), - "unavailable": Property(), + "unavailable": Property(unordered=True), } diff --git a/linode_api4/objects/base.py b/linode_api4/objects/base.py index 3e42e098a..2021ef3b7 100644 --- a/linode_api4/objects/base.py +++ b/linode_api4/objects/base.py @@ -32,6 +32,7 @@ def __init__( id_relationship=False, slug_relationship=False, nullable=False, + unordered=False, json_object=None, ): """ @@ -50,6 +51,10 @@ def __init__( (This should be used on fields ending with '_id' only) slug_relationship - This property is a slug related for a given type. nullable - This property can be explicitly null on PUT requests. + unordered - The order of this property is not significant. + NOTE: This field is currently only for annotations purposes + and does not influence any update or decoding/encoding logic. + json_object - The JSONObject class this property should be decoded into. """ self.mutable = mutable self.identifier = identifier @@ -60,6 +65,7 @@ def __init__( self.id_relationship = id_relationship self.slug_relationship = slug_relationship self.nullable = nullable + self.unordered = unordered self.json_class = json_object diff --git a/linode_api4/objects/database.py b/linode_api4/objects/database.py index efddee0c7..f71115758 100644 --- a/linode_api4/objects/database.py +++ b/linode_api4/objects/database.py @@ -129,7 +129,7 @@ class MySQLDatabase(Base): properties = { "id": Property(identifier=True), "label": Property(mutable=True), - "allow_list": Property(mutable=True), + "allow_list": Property(mutable=True, unordered=True), "backups": Property(derived_class=MySQLDatabaseBackup), "cluster_size": Property(), "created": Property(is_datetime=True), @@ -262,7 +262,7 @@ class PostgreSQLDatabase(Base): properties = { "id": Property(identifier=True), "label": Property(mutable=True), - "allow_list": Property(mutable=True), + "allow_list": Property(mutable=True, unordered=True), "backups": Property(derived_class=PostgreSQLDatabaseBackup), "cluster_size": Property(), "created": Property(is_datetime=True), @@ -404,7 +404,7 @@ class Database(Base): properties = { "id": Property(), "label": Property(), - "allow_list": Property(), + "allow_list": Property(unordered=True), "cluster_size": Property(), "created": Property(), "encrypted": Property(), diff --git a/linode_api4/objects/domain.py b/linode_api4/objects/domain.py index 38778c78d..aeca7d837 100644 --- a/linode_api4/objects/domain.py +++ b/linode_api4/objects/domain.py @@ -49,14 +49,14 @@ class Domain(Base): "status": Property(mutable=True), "soa_email": Property(mutable=True), "retry_sec": Property(mutable=True), - "master_ips": Property(mutable=True), - "axfr_ips": Property(mutable=True), + "master_ips": Property(mutable=True, unordered=True), + "axfr_ips": Property(mutable=True, unordered=True), "expire_sec": Property(mutable=True), "refresh_sec": Property(mutable=True), "ttl_sec": Property(mutable=True), "records": Property(derived_class=DomainRecord), "type": Property(mutable=True), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), } def record_create(self, record_type, **kwargs): diff --git a/linode_api4/objects/image.py b/linode_api4/objects/image.py index 606743ce0..a919d25e0 100644 --- a/linode_api4/objects/image.py +++ b/linode_api4/objects/image.py @@ -25,5 +25,7 @@ class Image(Base): "vendor": Property(), "size": Property(), "deprecated": Property(), - "capabilities": Property(), + "capabilities": Property( + unordered=True, + ), } diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 9477dd105..f459f5918 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -642,11 +642,11 @@ class Instance(Base): "configs": Property(derived_class=Config), "type": Property(slug_relationship=Type), "backups": Property(mutable=True), - "ipv4": Property(), + "ipv4": Property(unordered=True), "ipv6": Property(), "hypervisor": Property(), "specs": Property(), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), "host_uuid": Property(), "watchdog_enabled": Property(mutable=True), "has_user_data": Property(), @@ -1745,7 +1745,9 @@ class StackScript(Base): "created": Property(is_datetime=True), "deployments_active": Property(), "script": Property(mutable=True), - "images": Property(mutable=True), # TODO make slug_relationship + "images": Property( + mutable=True, unordered=True + ), # TODO make slug_relationship "deployments_total": Property(), "description": Property(mutable=True), "updated": Property(is_datetime=True), diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index 2e24b76f7..f1769685a 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -74,7 +74,7 @@ class LKENodePool(DerivedBase): volatile=True ), # this is formatted in _populate below "autoscaler": Property(mutable=True), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), } def _populate(self, json): @@ -121,7 +121,7 @@ class LKECluster(Base): "id": Property(identifier=True), "created": Property(is_datetime=True), "label": Property(mutable=True), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), "updated": Property(is_datetime=True), "region": Property(slug_relationship=Region), "k8s_version": Property(slug_relationship=KubeVersion, mutable=True), diff --git a/linode_api4/objects/networking.py b/linode_api4/objects/networking.py index 17d0ec4c6..dac295360 100644 --- a/linode_api4/objects/networking.py +++ b/linode_api4/objects/networking.py @@ -34,7 +34,9 @@ class IPv6Range(Base): "region": Property(slug_relationship=Region), "prefix": Property(), "route_target": Property(), - "linodes": Property(), + "linodes": Property( + unordered=True, + ), "is_bgp": Property(), } @@ -151,7 +153,7 @@ class VLAN(Base): properties = { "label": Property(identifier=True), "created": Property(is_datetime=True), - "linodes": Property(), + "linodes": Property(unordered=True), "region": Property(slug_relationship=Region), } @@ -189,7 +191,7 @@ class Firewall(Base): properties = { "id": Property(identifier=True), "label": Property(mutable=True), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), "status": Property(mutable=True), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), diff --git a/linode_api4/objects/nodebalancer.py b/linode_api4/objects/nodebalancer.py index 99c88f3c5..c6f161ac8 100644 --- a/linode_api4/objects/nodebalancer.py +++ b/linode_api4/objects/nodebalancer.py @@ -34,7 +34,7 @@ class NodeBalancerNode(DerivedBase): "weight": Property(mutable=True), "mode": Property(mutable=True), "status": Property(), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), } def __init__(self, client, id, parent_id, nodebalancer_id=None, json=None): @@ -217,7 +217,7 @@ class NodeBalancer(Base): "region": Property(slug_relationship=Region), "configs": Property(derived_class=NodeBalancerConfig), "transfer": Property(), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), } # create derived objects diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index a20d5ee94..ab77074d0 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -18,7 +18,7 @@ class Region(Base): properties = { "id": Property(identifier=True), "country": Property(), - "capabilities": Property(), + "capabilities": Property(unordered=True), "status": Property(), "resolvers": Property(), "label": Property(), diff --git a/linode_api4/objects/volume.py b/linode_api4/objects/volume.py index d572c12f5..365ceb2d3 100644 --- a/linode_api4/objects/volume.py +++ b/linode_api4/objects/volume.py @@ -21,7 +21,7 @@ class Volume(Base): "size": Property(), "status": Property(), "region": Property(slug_relationship=Region), - "tags": Property(mutable=True), + "tags": Property(mutable=True, unordered=True), "filesystem_path": Property(), "hardware_type": Property(), "linode_label": Property(), diff --git a/linode_api4/objects/vpc.py b/linode_api4/objects/vpc.py index 682b7a0ab..3f80b0925 100644 --- a/linode_api4/objects/vpc.py +++ b/linode_api4/objects/vpc.py @@ -34,7 +34,7 @@ class VPCSubnet(DerivedBase): "id": Property(identifier=True), "label": Property(mutable=True), "ipv4": Property(), - "linodes": Property(json_object=VPCSubnetLinode), + "linodes": Property(json_object=VPCSubnetLinode, unordered=True), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), } From 37fbc5e5f53d7d108318e096c9460f123ea8fb4a Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:51:09 -0400 Subject: [PATCH 3/7] Serialize `Base` objects in `MappedObject` (#389) --- linode_api4/objects/base.py | 10 +++++++-- test/fixtures/testmappedobj1.json | 3 +++ test/unit/objects/mapped_object_test.py | 29 ++++++++++++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/testmappedobj1.json diff --git a/linode_api4/objects/base.py b/linode_api4/objects/base.py index 2021ef3b7..9d86b8808 100644 --- a/linode_api4/objects/base.py +++ b/linode_api4/objects/base.py @@ -109,9 +109,15 @@ def dict(self): result[k] = v.dict elif isinstance(v, list): result[k] = [ - item.dict if isinstance(item, cls) else item for item in v + ( + item.dict + if isinstance(item, cls) + else item._raw_json if isinstance(item, Base) else item + ) + for item in v ] - + elif isinstance(v, Base): + result[k] = v._raw_json return result diff --git a/test/fixtures/testmappedobj1.json b/test/fixtures/testmappedobj1.json new file mode 100644 index 000000000..0914c1ded --- /dev/null +++ b/test/fixtures/testmappedobj1.json @@ -0,0 +1,3 @@ +{ + "bar": "bar" +} \ No newline at end of file diff --git a/test/unit/objects/mapped_object_test.py b/test/unit/objects/mapped_object_test.py index 87284af8f..2d83008ae 100644 --- a/test/unit/objects/mapped_object_test.py +++ b/test/unit/objects/mapped_object_test.py @@ -1,9 +1,9 @@ -from unittest import TestCase +from test.unit.base import ClientBaseCase -from linode_api4 import MappedObject +from linode_api4.objects import Base, MappedObject, Property -class MappedObjectCase(TestCase): +class MappedObjectCase(ClientBaseCase): def test_mapped_object_dict(self): test_dict = { "key1": 1, @@ -19,3 +19,26 @@ def test_mapped_object_dict(self): mapped_obj = MappedObject(**test_dict) self.assertEqual(mapped_obj.dict, test_dict) + + def test_mapped_object_dict(self): + test_property_name = "bar" + test_property_value = "bar" + + class Foo(Base): + api_endpoint = "/testmappedobj1" + id_attribute = test_property_name + properties = { + test_property_name: Property(mutable=True), + } + + foo = Foo(self.client, test_property_value) + foo._api_get() + + expected_dict = { + "foo": { + test_property_name: test_property_value, + } + } + + mapped_obj = MappedObject(foo=foo) + self.assertEqual(mapped_obj.dict, expected_dict) From 22e1778a29e45123f616a7845105d696a8a7b61e Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:55:48 -0700 Subject: [PATCH 4/7] test: remove unnecessary warnings when running integration tests (#392) * Remove warnings and additional fixture for stability * make format * address other warning --- Makefile | 2 +- test/integration/conftest.py | 7 +++++++ test/integration/linode_client/test_linode_client.py | 5 +---- test/integration/models/test_linode.py | 3 --- tod_scripts | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 0589eec02..32d31cf20 100644 --- a/Makefile +++ b/Makefile @@ -75,4 +75,4 @@ testunit: .PHONY: smoketest smoketest: - $(PYTHON) -m pytest -m smoke test/integration --disable-warnings \ No newline at end of file + $(PYTHON) -m pytest -m smoke test/integration \ No newline at end of file diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 03295e59c..3ba7d2b40 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -341,3 +341,10 @@ def create_multiple_vpcs(test_linode_client): vpc_1.delete() vpc_2.delete() + + +@pytest.mark.smoke +def pytest_configure(config): + config.addinivalue_line( + "markers", "smoke: mark test as part of smoke test suite" + ) diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 2d7994a20..bc5d31292 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -31,7 +31,7 @@ def test_get_account(setup_client_and_linode): assert re.search("^$|[a-zA-Z]+", account.first_name) assert re.search("^$|[a-zA-Z]+", account.last_name) assert re.search( - "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", account.email + "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$", account.email ) assert re.search("^$|[a-zA-Z0-9]+", account.address_1) assert re.search("^$|[a-zA-Z0-9]+", account.address_2) @@ -401,9 +401,6 @@ def test_keys_create(test_linode_client, ssh_keys_object_storage): # NetworkingGroupTests -# TODO:: creating vlans -# def test_get_vlans(): - @pytest.fixture def create_firewall_with_inbound_outbound_rules(test_linode_client): diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index ce122226a..ba44e157f 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -114,7 +114,6 @@ def linode_for_disk_tests(test_linode_client): linode_instance.delete() -@pytest.mark.smoke @pytest.fixture def create_linode_for_long_running_tests(test_linode_client): client = test_linode_client @@ -372,7 +371,6 @@ def wait_for_disk_status(disk: Disk, 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 @@ -396,7 +394,6 @@ def test_disk_resize_and_duplicate(test_linode_client, linode_for_disk_tests): 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) diff --git a/tod_scripts b/tod_scripts index eec4b9955..41b85dd2c 160000 --- a/tod_scripts +++ b/tod_scripts @@ -1 +1 @@ -Subproject commit eec4b99557cef6f40e8b5b7de00357dc49fb041c +Subproject commit 41b85dd2c5588b5b343b8ee365b2f4f196cd9a7f From a25850f21f387454d9e4cebb9c5abe9257ae0e5b Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:51:28 -0700 Subject: [PATCH 5/7] add integration workflow for main and dev (#393) --- .github/workflows/e2e-test.yml | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/e2e-test.yml diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 000000000..166b70e75 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,70 @@ +name: Integration Tests + +on: + workflow_dispatch: null + push: + branches: + - main + - dev + +jobs: + integration-tests: + runs-on: ubuntu-latest + env: + EXIT_STATUS: 0 + steps: + - name: Clone Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Python deps + run: pip install -U setuptools wheel boto3 certifi + + - name: Install Python SDK + run: make dev-install + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Integration tests + run: | + 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} --disable-warnings --junitxml="${report_filename}"; then + echo "EXIT_STATUS=1" >> $GITHUB_ENV + fi + env: + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + + - name: Add additional information to XML report + run: | + filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') + 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" \ + --xmlfile "${filename}" + + - name: Upload test results + run: | + report_filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') + 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 }} + + - 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 From b1c56a6bff4562f57f0e75dc26ab18b88e7565d4 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:41:05 -0700 Subject: [PATCH 6/7] test: address intermittent test failures (#394) * address intermittent test failures and some warnings * format/lint * replace wait with polling * fix testcase --- test/integration/models/test_database.py | 6 +-- test/integration/models/test_linode.py | 62 +++++++++++++++++------- test/integration/models/test_lke.py | 28 +++++++++-- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/test/integration/models/test_database.py b/test/integration/models/test_database.py index 0e14f5041..b9502abdc 100644 --- a/test/integration/models/test_database.py +++ b/test/integration/models/test_database.py @@ -101,9 +101,9 @@ def test_get_types(test_linode_client): client = test_linode_client types = client.database.types() - assert (types[0].type_class, "nanode") - assert (types[0].id, "g6-nanode-1") - assert (types[0].engines.mongodb[0].price.monthly, 15) + assert "nanode" in types[0].type_class + assert "g6-nanode-1" in types[0].id + assert types[0].engines.mongodb[0].price.monthly == 15 def test_get_engines(test_linode_client): diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index ba44e157f..a749baad4 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -6,9 +6,10 @@ wait_for_condition, ) +import polling import pytest -from linode_api4 import VPCIPAddress +from linode_api4 import LinodeClient, VPCIPAddress from linode_api4.errors import ApiError from linode_api4.objects import ( Config, @@ -84,7 +85,7 @@ def linode_for_network_interface_tests(test_linode_client): linode_instance.delete() -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture def linode_for_disk_tests(test_linode_client): client = test_linode_client available_regions = client.regions() @@ -94,21 +95,24 @@ def linode_for_disk_tests(test_linode_client): linode_instance, password = client.linode.instance_create( "g6-nanode-1", chosen_region, - image="linode/debian10", + image="linode/alpine3.19", 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") + # Now it allocates 100% disk space hence need to clear some space for tests + linode_instance.disks[1].delete() + + test_linode_client.polling.event_poller_create( + "linode", "disk_delete", entity_id=linode_instance.id + ) + yield linode_instance linode_instance.delete() @@ -138,6 +142,10 @@ def get_status(linode: Instance, status: str): return linode.status == status +def instance_type_condition(linode: Instance, type: str): + return type in str(linode.type) + + def test_get_linode(test_linode_client, linode_with_volume_firewall): linode = test_linode_client.load(Instance, linode_with_volume_firewall.id) @@ -303,6 +311,7 @@ def test_linode_resize_with_class( def test_linode_resize_with_migration_type( + test_linode_client, create_linode_for_long_running_tests, ): linode = create_linode_for_long_running_tests @@ -311,18 +320,32 @@ def test_linode_resize_with_migration_type( 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 + assert "g6-nanode-1" in str(linode.type) + assert linode.specs.disk == 25600 - wait_for_condition(10, 300, get_status, linode, "resizing") + res = linode.resize(new_type="g6-standard-1", migration_type=m_type) - assert linode.status == "resizing" + if res: + # there is no resizing state in warm migration anymore hence wait for resizing and poll event + test_linode_client.polling.event_poller_create( + "linode", "linode_resize", entity_id=linode.id + ).wait_for_next_event_finished(interval=5) + + wait_for_condition( + 10, + 100, + get_status, + linode, + "running", + ) + else: + raise ApiError - # Takes about 3-5 minute to resize, sometimes longer... - wait_for_condition(30, 600, get_status, linode, "running") + # reload resized linode + resized_linode = test_linode_client.load(Instance, linode.id) - assert linode.status == "running" + assert resized_linode.specs.disk == 51200 def test_linode_boot_with_config(create_linode): @@ -376,10 +399,9 @@ def test_disk_resize_and_duplicate(test_linode_client, linode_for_disk_tests): disk = linode.disks[0] - disk.resize(5000) + send_request_when_resource_available(300, disk.resize, 5000) - # Using hard sleep instead of wait as the status shows ready when it is resizing - time.sleep(120) + time.sleep(100) disk = test_linode_client.load(Disk, linode.disks[0].id, linode.id) @@ -397,7 +419,11 @@ def test_disk_resize_and_duplicate(test_linode_client, linode_for_disk_tests): 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) + disk = send_request_when_resource_available( + 300, + linode.disk_create, + size=500, + ) wait_for_disk_status(disk, 120) diff --git a/test/integration/models/test_lke.py b/test/integration/models/test_lke.py index 04b479e8e..1cff9ec44 100644 --- a/test/integration/models/test_lke.py +++ b/test/integration/models/test_lke.py @@ -95,7 +95,7 @@ def test_lke_node_delete(lke_cluster): 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 send_request_when_resource_available(300, cluster.node_recycle, node_id) @@ -106,9 +106,18 @@ def test_lke_node_recycle(test_linode_client, lke_cluster): assert node.status == "not_ready" # wait for provisioning - wait_for_condition(10, 300, get_node_status, cluster, "ready") + wait_for_condition( + 10, + 500, + get_node_status, + test_linode_client.load(LKECluster, lke_cluster.id), + "ready", + ) - node = cluster.pools[0].nodes[0] + node_pool = test_linode_client.load( + LKENodePool, cluster.pools[0].id, cluster.id + ) + node = node_pool.nodes[0] assert node.status == "ready" @@ -117,9 +126,18 @@ def test_lke_cluster_nodes_recycle(test_linode_client, lke_cluster): send_request_when_resource_available(300, cluster.cluster_nodes_recycle) - wait_for_condition(5, 300, get_node_status, cluster, "not_ready") + wait_for_condition( + 5, + 300, + get_node_status, + test_linode_client.load(LKECluster, cluster.id), + "not_ready", + ) - node = cluster.pools[0].nodes[0] + node_pool = test_linode_client.load( + LKENodePool, cluster.pools[0].id, cluster.id + ) + node = node_pool.nodes[0] assert node.status == "not_ready" From 23d41fd0177729c7a16764cc4f9e92de3accab96 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:20:34 -0400 Subject: [PATCH 7/7] new: Add `available` field to AccountAvailability class (#395) * Add available field to account availabilities response * Update docs --- linode_api4/groups/account.py | 4 +-- linode_api4/objects/account.py | 6 ++-- test/fixtures/account_availability.json | 33 ++++++++++++------- .../account_availability_us-east.json | 3 +- test/unit/objects/account_test.py | 12 +++++++ 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/linode_api4/groups/account.py b/linode_api4/groups/account.py index 55eab9436..8805c6416 100644 --- a/linode_api4/groups/account.py +++ b/linode_api4/groups/account.py @@ -487,10 +487,10 @@ def join_beta_program(self, beta: Union[str, BetaProgram]): def availabilities(self, *filters): """ - Returns a list of all available regions and the resources which are NOT available + Returns a list of all available regions and the resource types which are available to the account. - API doc: TBD + API doc: https://www.linode.com/docs/api/account/#region-service-availability :returns: a list of region availability information. :rtype: PaginatedList of AccountAvailability diff --git a/linode_api4/objects/account.py b/linode_api4/objects/account.py index 349331be3..19ecd79a0 100644 --- a/linode_api4/objects/account.py +++ b/linode_api4/objects/account.py @@ -660,9 +660,10 @@ class AccountBetaProgram(Base): class AccountAvailability(Base): """ - The resources information in a region which are NOT available to an account. + Contains information about the resources available for a region under the + current account. - API doc: TBD + API doc: https://www.linode.com/docs/api/account/#region-service-availability """ api_endpoint = "/account/availability/{region}" @@ -671,4 +672,5 @@ class AccountAvailability(Base): properties = { "region": Property(identifier=True), "unavailable": Property(unordered=True), + "available": Property(unordered=True), } diff --git a/test/fixtures/account_availability.json b/test/fixtures/account_availability.json index a09feb1db..f308cb975 100644 --- a/test/fixtures/account_availability.json +++ b/test/fixtures/account_availability.json @@ -2,47 +2,58 @@ "data": [ { "region": "ap-west", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "ca-central", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "ap-southeast", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "us-central", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "us-west", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "us-southeast", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "us-east", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "Kubernetes"] }, { "region": "eu-west", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "Cloud Firewall"] }, { "region": "ap-south", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "eu-central", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "NodeBalancers"] }, { "region": "ap-northeast", - "unavailable": [] + "unavailable": [], + "available": ["Linodes"] } ], "page": 1, diff --git a/test/fixtures/account_availability_us-east.json b/test/fixtures/account_availability_us-east.json index 5bcceb526..765aeba6e 100644 --- a/test/fixtures/account_availability_us-east.json +++ b/test/fixtures/account_availability_us-east.json @@ -1,4 +1,5 @@ { "region": "us-east", - "unavailable": [] + "unavailable": [], + "available": ["Linodes", "Kubernetes"] } \ No newline at end of file diff --git a/test/unit/objects/account_test.py b/test/unit/objects/account_test.py index 0f53240f4..b3f7be1e3 100644 --- a/test/unit/objects/account_test.py +++ b/test/unit/objects/account_test.py @@ -268,6 +268,17 @@ class AccountAvailabilityTest(ClientBaseCase): Test methods of the AccountAvailability """ + def test_account_availability_api_list(self): + with self.mock_get("/account/availability") as m: + availabilities = self.client.account.availabilities() + + for avail in availabilities: + assert avail.region is not None + assert len(avail.unavailable) == 0 + assert len(avail.available) > 0 + + self.assertEqual(m.call_url, "/account/availability") + def test_account_availability_api_get(self): region_id = "us-east" account_availability_url = "/account/availability/{}".format(region_id) @@ -276,5 +287,6 @@ def test_account_availability_api_get(self): availability = AccountAvailability(self.client, region_id) self.assertEqual(availability.region, region_id) self.assertEqual(availability.unavailable, []) + self.assertEqual(availability.available, ["Linodes", "Kubernetes"]) self.assertEqual(m.call_url, account_availability_url)