From bf7f1732ca5f442900db7cf25bfe0cc7db508974 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:46:58 -0700 Subject: [PATCH 1/9] new: Adding cross repo testing workflow for Release (#378) * move test upload logic to git submodule, and use it in e2e workflow * update script folder name * Test release-cross-repo-test.yml * trial 2 * 3 * switch order of python build * 5 * fix syntax * update python version * 6 * 7 * 8 * Final clean up and fix make dep installs * remove test_scripts * change job name * Pr comments * Pr comments --- .github/workflows/release-cross-repo-test.yml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/release-cross-repo-test.yml diff --git a/.github/workflows/release-cross-repo-test.yml b/.github/workflows/release-cross-repo-test.yml new file mode 100644 index 00000000..0f484d9a --- /dev/null +++ b/.github/workflows/release-cross-repo-test.yml @@ -0,0 +1,61 @@ +name: Release Ansible cross repository test + +on: + pull_request: + branches: + - main + types: [opened] # Workflow will only be executed when PR is opened to main branch + workflow_dispatch: # Manual trigger + + +jobs: + ansible_integration_test: + runs-on: ubuntu-latest + steps: + - name: Checkout linode_api4 repository + uses: actions/checkout@v4 + + - name: update packages + run: sudo apt-get update -y + + - name: install make + run: sudo apt-get install -y build-essential + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install linode_api4 + run: make install + + - name: checkout repo + uses: actions/checkout@v3 + with: + repository: linode/ansible_linode + path: .ansible/collections/ansible_collections/linode/cloud + + - name: install dependencies + run: | + cd .ansible/collections/ansible_collections/linode/cloud + pip install -r requirements.txt -r requirements-dev.txt --upgrade-strategy only-if-needed + + - name: install ansible dependencies + run: ansible-galaxy collection install amazon.aws:==6.0.1 + + - name: install collection + run: | + cd .ansible/collections/ansible_collections/linode/cloud + make install + + - name: replace existing keys + run: | + cd .ansible/collections/ansible_collections/linode/cloud + rm -rf ~/.ansible/test && mkdir -p ~/.ansible/test && ssh-keygen -m PEM -q -t rsa -N '' -f ~/.ansible/test/id_rsa + + - name: run tests + run: | + cd .ansible/collections/ansible_collections/linode/cloud + make testall + env: + LINODE_API_TOKEN: ${{ secrets.LINODE_TOKEN }} From a0393db7354d682ba87a64a7b4e17c862c4035c0 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Wed, 13 Mar 2024 15:32:58 -0400 Subject: [PATCH 2/9] doc: Add .readthedocs.yaml config file (#381) * Add .readthedocs.yaml --- .readthedocs.yaml | 13 +++++++++++++ docs/requirements.txt | 1 + 2 files changed, 14 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..c088d2c5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" +sphinx: + configuration: docs/conf.py +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 0eee2117..2b939141 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ sphinxcontrib-fulltoc +. \ No newline at end of file From fd878dde71c771440ac3911911f77959bb335359 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:32:01 -0400 Subject: [PATCH 3/9] doc: Add missing models and groups to documentation (#383) * Include missing models and groups * Update copyright --- docs/conf.py | 2 +- docs/linode_api4/linode_client.rst | 20 +++++++++++++++++++- docs/linode_api4/objects/models.rst | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cd1654ef..cd15307a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # -- Project information ----------------------------------------------------- project = 'linode_api4' -copyright = '2023, Linode' +copyright = '2024, Akamai Technologies Inc.' author = 'Linode' # The short X.Y version diff --git a/docs/linode_api4/linode_client.rst b/docs/linode_api4/linode_client.rst index b87a6a18..58c7025b 100644 --- a/docs/linode_api4/linode_client.rst +++ b/docs/linode_api4/linode_client.rst @@ -61,6 +61,15 @@ Includes methods for managing your account. :members: :special-members: +BetaGroup +^^^^^^^^^ + +Includes methods for enrolling in beta programs. + +.. autoclass:: linode_api4.linode_client.BetaGroup + :members: + :special-members: + DatabaseGroup ^^^^^^^^^^^^^ @@ -98,7 +107,7 @@ accessing and working with associated features. :members: :special-members: -LKE Group +LKEGroup ^^^^^^^^^ Includes methods for interacting with Linode Kubernetes Engine. @@ -199,3 +208,12 @@ Includes methods for managing Linode Volumes. .. autoclass:: linode_api4.linode_client.VolumeGroup :members: :special-members: + +VPCGroup +^^^^^^^^ + +Includes methods for managing Linode VPCs. + +.. autoclass:: linode_api4.linode_client.VPCGroup + :members: + :special-members: diff --git a/docs/linode_api4/objects/models.rst b/docs/linode_api4/objects/models.rst index 7ea66494..6805ad88 100644 --- a/docs/linode_api4/objects/models.rst +++ b/docs/linode_api4/objects/models.rst @@ -14,6 +14,15 @@ Account Models :undoc-members: :inherited-members: +Beta Models +----------- + +.. automodule:: linode_api4.objects.beta + :members: + :exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name + :undoc-members: + :inherited-members: + Database Models ------------- @@ -139,3 +148,12 @@ Volume Models :exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name :undoc-members: :inherited-members: + +VPC Models +---------- + +.. automodule:: linode_api4.objects.vpc + :members: + :exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name + :undoc-members: + :inherited-members: From 49dd2c6deab5d64fadf7e69f91d1324282ffee27 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:58:16 -0400 Subject: [PATCH 4/9] Improve `.readthedocs.yaml` config file (#382) --- .readthedocs.yaml | 11 +++++++---- docs/requirements.txt | 2 -- pyproject.toml | 8 +++++++- 3 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c088d2c5..3fad08aa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,11 +3,14 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3.12" + python: latest sphinx: configuration: docs/conf.py python: - install: - - requirements: docs/requirements.txt \ No newline at end of file + install: + - method: pip + path: . + extra_requirements: + - doc \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 2b939141..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinxcontrib-fulltoc -. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 787cbc46..cec2adf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,11 +50,17 @@ dev = [ "build>=0.10.0", "Sphinx>=6.0.0", "sphinx-autobuild>=2021.3.14", - "sphinxcontrib-fulltoc>=1.2.0", + "sphinxcontrib-fulltoc>=1.2.0", "build>=0.10.0", "twine>=4.0.2", ] +doc = [ + "Sphinx>=6.0.0", + "sphinx-autobuild>=2021.3.14", + "sphinxcontrib-fulltoc>=1.2.0", +] + [project.urls] Homepage = "https://github.com/linode/linode_api4-python" Documentation = "https://linode-api4.readthedocs.io/" From 95d0b20ea2f50d6277c658288f1e35966aae40b2 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:04:48 -0400 Subject: [PATCH 5/9] new: Add `vpc` field to `Instance(...).ips` property method result (#379) * Support ipv4.vpc field in Instance.ips property method * Update integration test * Add null check --- linode_api4/objects/linode.py | 7 +- linode_api4/objects/networking.py | 27 ++++ test/fixtures/linode_instances_123_ips.json | 171 +++++++++++--------- test/integration/models/test_linode.py | 14 ++ test/unit/objects/linode_test.py | 28 ++-- 5 files changed, 160 insertions(+), 87 deletions(-) diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 9fcdae11..9477dd10 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -21,7 +21,7 @@ ) 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.networking import IPAddress, IPv6Range, VPCIPAddress from linode_api4.objects.vpc import VPC, VPCSubnet from linode_api4.paginated_list import PaginatedList @@ -693,6 +693,10 @@ def ips(self): i = IPAddress(self._client, c["address"], c) reserved.append(i) + vpc = [ + VPCIPAddress.from_json(v) for v in result["ipv4"].get("vpc", []) + ] + slaac = IPAddress( self._client, result["ipv6"]["slaac"]["address"], @@ -716,6 +720,7 @@ def ips(self): "private": v4pri, "shared": shared_ips, "reserved": reserved, + "vpc": vpc, }, "ipv6": { "slaac": slaac, diff --git a/linode_api4/objects/networking.py b/linode_api4/objects/networking.py index 1b0e4699..17d0ec4c 100644 --- a/linode_api4/objects/networking.py +++ b/linode_api4/objects/networking.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional from linode_api4.errors import UnexpectedResponseError from linode_api4.objects import Base, DerivedBase, JSONObject, Property, Region @@ -106,6 +107,32 @@ def to(self, linode): return {"address": self.address, "linode_id": linode.id} +@dataclass +class VPCIPAddress(JSONObject): + """ + VPCIPAddress represents the IP address of a VPC. + + NOTE: This is not implemented as a typical API object (Base) because VPC IPs + cannot be refreshed through the /networking/ips/{address} endpoint. + """ + + address: str = "" + gateway: str = "" + region: str = "" + subnet_mask: str = "" + vpc_id: int = 0 + subnet_id: int = 0 + linode_id: int = 0 + config_id: int = 0 + interface_id: int = 0 + prefix: int = 0 + + active: bool = False + + address_range: Optional[str] = None + nat_1_1: Optional[str] = None + + class VLAN(Base): """ .. note:: At this time, the Linode API only supports listing VLANs. diff --git a/test/fixtures/linode_instances_123_ips.json b/test/fixtures/linode_instances_123_ips.json index 14702024..22d61f7b 100644 --- a/test/fixtures/linode_instances_123_ips.json +++ b/test/fixtures/linode_instances_123_ips.json @@ -1,89 +1,106 @@ { - "ipv4": { - "private": [ - { - "address": "192.168.133.234", - "gateway": null, - "linode_id": 123, - "prefix": 17, - "public": false, - "rdns": null, - "region": "us-east", - "subnet_mask": "255.255.128.0", - "type": "ipv4" - } - ], - "public": [ - { - "address": "97.107.143.141", - "gateway": "97.107.143.1", - "linode_id": 123, - "prefix": 24, - "public": true, - "rdns": "test.example.org", - "region": "us-east", - "subnet_mask": "255.255.255.0", - "type": "ipv4" - } - ], - "reserved": [ - { - "address": "97.107.143.141", - "gateway": "97.107.143.1", - "linode_id": 123, - "prefix": 24, - "public": true, - "rdns": "test.example.org", - "region": "us-east", - "subnet_mask": "255.255.255.0", - "type": "ipv4" - } - ], - "shared": [ - { - "address": "97.107.143.141", - "gateway": "97.107.143.1", - "linode_id": 123, - "prefix": 24, - "public": true, - "rdns": "test.example.org", - "region": "us-east", - "subnet_mask": "255.255.255.0", - "type": "ipv4" - } - ] - }, - "ipv6": { - "global": [ - { - "prefix": 124, - "range": "2600:3c01::2:5000:0", - "region": "us-east", - "route_target": "2600:3c01::2:5000:f" - } - ], - "link_local": { - "address": "fe80::f03c:91ff:fe24:3a2f", - "gateway": "fe80::1", + "ipv4": { + "private": [ + { + "address": "192.168.133.234", + "gateway": null, "linode_id": 123, - "prefix": 64, + "prefix": 17, "public": false, "rdns": null, "region": "us-east", - "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - "type": "ipv6" - }, - "slaac": { - "address": "2600:3c03::f03c:91ff:fe24:3a2f", - "gateway": "fe80::1", + "subnet_mask": "255.255.128.0", + "type": "ipv4" + } + ], + "public": [ + { + "address": "97.107.143.141", + "gateway": "97.107.143.1", "linode_id": 123, - "prefix": 64, + "prefix": 24, "public": true, - "rdns": null, + "rdns": "test.example.org", + "region": "us-east", + "subnet_mask": "255.255.255.0", + "type": "ipv4" + } + ], + "reserved": [ + { + "address": "97.107.143.141", + "gateway": "97.107.143.1", + "linode_id": 123, + "prefix": 24, + "public": true, + "rdns": "test.example.org", "region": "us-east", - "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - "type": "ipv6" + "subnet_mask": "255.255.255.0", + "type": "ipv4" + } + ], + "vpc": [ + { + "address": "10.0.0.2", + "address_range": null, + "vpc_id": 39246, + "subnet_id": 39388, + "region": "us-mia", + "linode_id": 55904908, + "config_id": 59036295, + "interface_id": 1186165, + "active": true, + "nat_1_1": "172.233.179.133", + "gateway": "10.0.0.1", + "prefix": 24, + "subnet_mask": "255.255.255.0" } + ], + "shared": [ + { + "address": "97.107.143.141", + "gateway": "97.107.143.1", + "linode_id": 123, + "prefix": 24, + "public": true, + "rdns": "test.example.org", + "region": "us-east", + "subnet_mask": "255.255.255.0", + "type": "ipv4" + } + ] + }, + "ipv6": { + "global": [ + { + "prefix": 124, + "range": "2600:3c01::2:5000:0", + "region": "us-east", + "route_target": "2600:3c01::2:5000:f" + } + ], + "link_local": { + "address": "fe80::f03c:91ff:fe24:3a2f", + "gateway": "fe80::1", + "linode_id": 123, + "prefix": 64, + "public": false, + "rdns": null, + "region": "us-east", + "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "type": "ipv6" + }, + "slaac": { + "address": "2600:3c03::f03c:91ff:fe24:3a2f", + "gateway": "fe80::1", + "linode_id": 123, + "prefix": 64, + "public": true, + "rdns": null, + "region": "us-east", + "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "type": "ipv6" } } +} \ No newline at end of file diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index f46a3fc2..2a69afb6 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -621,6 +621,20 @@ def test_create_vpc( assert interface.ipv4.nat_1_1 == linode.ipv4[0] assert interface.ip_ranges == ["10.0.0.5/32"] + vpc_ip = linode.ips.ipv4.vpc[0] + vpc_range_ip = linode.ips.ipv4.vpc[1] + + assert vpc_ip.nat_1_1 == linode.ips.ipv4.public[0].address + assert vpc_ip.address_range is None + assert vpc_ip.vpc_id == vpc.id + assert vpc_ip.subnet_id == subnet.id + assert vpc_ip.config_id == config.id + assert vpc_ip.interface_id == interface.id + assert not vpc_ip.active + + assert vpc_range_ip.address_range == "10.0.0.5/32" + assert not vpc_range_ip.active + def test_update_vpc( self, linode_for_network_interface_tests, diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index e9362eab..c68dcfef 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -348,15 +348,25 @@ def test_ips(self): ips = linode.ips - self.assertIsNotNone(ips.ipv4) - self.assertIsNotNone(ips.ipv6) - self.assertIsNotNone(ips.ipv4.public) - self.assertIsNotNone(ips.ipv4.private) - self.assertIsNotNone(ips.ipv4.shared) - self.assertIsNotNone(ips.ipv4.reserved) - self.assertIsNotNone(ips.ipv6.slaac) - self.assertIsNotNone(ips.ipv6.link_local) - self.assertIsNotNone(ips.ipv6.ranges) + assert ips.ipv4 is not None + assert ips.ipv6 is not None + assert ips.ipv4.public is not None + assert ips.ipv4.private is not None + assert ips.ipv4.shared is not None + assert ips.ipv4.reserved is not None + assert ips.ipv4.vpc is not None + assert ips.ipv6.slaac is not None + assert ips.ipv6.link_local is not None + assert ips.ipv6.ranges is not None + + vpc_ip = ips.ipv4.vpc[0] + assert vpc_ip.nat_1_1 == "172.233.179.133" + assert vpc_ip.address_range == None + assert vpc_ip.vpc_id == 39246 + assert vpc_ip.subnet_id == 39388 + assert vpc_ip.config_id == 59036295 + assert vpc_ip.interface_id == 1186165 + assert vpc_ip.active def test_initiate_migration(self): """ From fd151d58b876bd924f04275dfdc0cc8e4b2815a3 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:15:52 -0400 Subject: [PATCH 6/9] Support LinodeClient(...).vpcs.ips(...) (#385) --- linode_api4/groups/vpc.py | 25 +++++++++++++++++++++++-- test/fixtures/vpcs_ips.json | 22 ++++++++++++++++++++++ test/integration/models/test_linode.py | 8 ++++++++ test/unit/objects/vpc_test.py | 26 ++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/vpcs_ips.json diff --git a/linode_api4/groups/vpc.py b/linode_api4/groups/vpc.py index 635e392d..f3f4f27b 100644 --- a/linode_api4/groups/vpc.py +++ b/linode_api4/groups/vpc.py @@ -1,9 +1,8 @@ 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.objects import VPC, Region, VPCIPAddress from linode_api4.paginated_list import PaginatedList @@ -81,3 +80,25 @@ def create( d = VPC(self.client, result["id"], result) return d + + def ips(self, *filters) -> PaginatedList: + """ + Retrieves all of the VPC IP addresses for the current account matching the given filters. + + This is intended to be called from the :any:`LinodeClient` + class, like this:: + + vpc_ips = client.vpcs.ips() + + 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 VPCIPAddresses the acting user can access. + :rtype: PaginatedList of VPCIPAddress + """ + return self.client._get_and_filter( + VPCIPAddress, *filters, endpoint="/vpcs/ips" + ) diff --git a/test/fixtures/vpcs_ips.json b/test/fixtures/vpcs_ips.json new file mode 100644 index 00000000..d6f16c2e --- /dev/null +++ b/test/fixtures/vpcs_ips.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "address": "10.0.0.2", + "address_range": null, + "vpc_id": 123, + "subnet_id": 456, + "region": "us-mia", + "linode_id": 123, + "config_id": 456, + "interface_id": 789, + "active": true, + "nat_1_1": "172.233.179.133", + "gateway": "10.0.0.1", + "prefix": 24, + "subnet_mask": "255.255.255.0" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py index 2a69afb6..40d1e735 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -8,6 +8,7 @@ import pytest +from linode_api4 import VPCIPAddress from linode_api4.errors import ApiError from linode_api4.objects import ( Config, @@ -595,6 +596,7 @@ def test_create_vlan(self, linode_for_network_interface_tests): def test_create_vpc( self, + test_linode_client, linode_for_network_interface_tests, create_vpc_with_subnet_and_linode, ): @@ -635,6 +637,12 @@ def test_create_vpc( assert vpc_range_ip.address_range == "10.0.0.5/32" assert not vpc_range_ip.active + # Attempt to resolve the IP from /vpcs/ips + all_vpc_ips = test_linode_client.vpcs.ips( + VPCIPAddress.filters.linode_id == linode.id + ) + assert all_vpc_ips[0].dict == vpc_ip.dict + 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 c8453ada..830e9fb9 100644 --- a/test/unit/objects/vpc_test.py +++ b/test/unit/objects/vpc_test.py @@ -126,6 +126,32 @@ def test_create_subnet(self): self.validate_vpc_subnet_789(subnet) + def test_list_ips(self): + """ + Validates that all VPC IPs can be listed. + """ + + with self.mock_get("/vpcs/ips") as m: + result = self.client.vpcs.ips() + + assert m.call_url == "/vpcs/ips" + assert len(result) == 1 + + ip = result[0] + assert ip.address == "10.0.0.2" + assert ip.address_range == None + assert ip.vpc_id == 123 + assert ip.subnet_id == 456 + assert ip.region == "us-mia" + assert ip.linode_id == 123 + assert ip.config_id == 456 + assert ip.interface_id == 789 + assert ip.active + assert ip.nat_1_1 == "172.233.179.133" + assert ip.gateway == "10.0.0.1" + assert ip.prefix == 24 + assert ip.subnet_mask == "255.255.255.0" + def validate_vpc_123456(self, vpc: VPC): expected_dt = datetime.datetime.strptime( "2018-01-01T00:01:01", DATE_FORMAT From 58dcd1d231289ff86be0a754b556a6b6e07ecdf4 Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:16:35 -0400 Subject: [PATCH 7/9] new: Add handling for failed events in EventPoller(...).wait_for_next_event_finished(...) (#384) * Add handling for failed events in EventPoller * oops * oops --- linode_api4/polling.py | 24 +++++++++++++ test/unit/objects/polling_test.py | 58 ++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/linode_api4/polling.py b/linode_api4/polling.py index 53723963..6ba02a5b 100644 --- a/linode_api4/polling.py +++ b/linode_api4/polling.py @@ -6,6 +6,26 @@ from linode_api4.objects import Event +class EventError(Exception): + """ + Represents a failed Linode event. + """ + + def __init__(self, event_id: int, message: Optional[str]): + # Edge case, sometimes the message is populated with an empty string + if len(message) < 1: + message = None + + self.event_id = event_id + self.message = message + + error_fmt = f"Event {event_id} failed" + if message is not None: + error_fmt += f": {message}" + + super().__init__(error_fmt) + + class TimeoutContext: """ TimeoutContext should be used by polling resources to track their provisioning time. @@ -212,6 +232,10 @@ def wait_for_next_event_finished( def poll_func(): event._api_get() + + if event.status == "failed": + raise EventError(event.id, event.message) + return event.status in ["finished", "notification"] if poll_func(): diff --git a/test/unit/objects/polling_test.py b/test/unit/objects/polling_test.py index b4d3a88c..7fb7c684 100644 --- a/test/unit/objects/polling_test.py +++ b/test/unit/objects/polling_test.py @@ -1,9 +1,11 @@ import json +from typing import Optional import httpretty import pytest from linode_api4 import LinodeClient +from linode_api4.polling import EventError class TestPolling: @@ -12,7 +14,11 @@ def client(self): return LinodeClient("testing", base_url="https://localhost") @staticmethod - def body_event_status(status: str, action: str = "linode_shutdown"): + def body_event_status( + status: str, + action: str = "linode_shutdown", + message: Optional[str] = None, + ): return { "action": action, "entity": { @@ -21,6 +27,7 @@ def body_event_status(status: str, action: str = "linode_shutdown"): }, "id": 123, "status": status, + "message": message, } @staticmethod @@ -272,3 +279,52 @@ def test_wait_for_event_finished_creation( assert len(get_requests) == 3 assert result.entity.id == 11111 assert result.status == "finished" + + @httpretty.activate + def test_wait_for_event_finished_failed( + self, + client, + ): + """ + Tests that the EventPoller.wait_for_event_finished method raises errors for failed events. + """ + + httpretty.register_uri( + httpretty.GET, + "https://localhost/account/events/123", + responses=[ + httpretty.Response( + body=json.dumps(self.body_event_status("started")), + ), + httpretty.Response( + body=json.dumps( + self.body_event_status("failed", message="oh no!") + ), + ), + ], + ) + + httpretty.register_uri( + httpretty.GET, + "https://localhost/account/events", + responses=[ + httpretty.Response( + body=json.dumps(self.body_event_list_empty()), + status=200, + ), + httpretty.Response( + body=json.dumps(self.body_event_list_status("started")), + status=200, + ), + ], + ) + + try: + client.polling.event_poller_create( + "linode", "linode_shutdown", entity_id=11111 + ).wait_for_next_event_finished(interval=0.1) + except EventError as err: + assert err.event_id == 123 + assert err.message == "oh no!" + else: + raise Exception("Expected event error, got none") From cb1e30a307b99c1063cb2523e9e9253eb32f1ed2 Mon Sep 17 00:00:00 2001 From: Jacob Riddle <87780794+jriddle-linode@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:02:14 -0400 Subject: [PATCH 8/9] ci: update labels and release drafter (#387) 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?** Updates to use githubs built in release notes and using the following labels. `**NOTE**: The labeler job is dry running on the PR to show what it will do, doesn't execute until we merge.` ### โš ๏ธ Breaking Change breaking-change: any changes that break end users or downstream workflows ### ๐Ÿ› Bug Fixes bugfix: changes that fix a existing bug ### ๐Ÿš€ New Features new-feature: changes that add new features such as endpoints or tools ### ๐Ÿ’ก Improvements improvement: changes that improve existing features or reflect small API changes ### ๐Ÿงช Testing Improvements testing: improvements to the testing workflows ### โš™๏ธ Repo/CI Improvements repo-ci-improvement: improvements to the CI workflow, like this PR! ### ๐Ÿ“– Documentation documentation: updates to the package/repo documentation or wiki ### ๐Ÿ“ฆ Dependency Updates dependencies: Used by dependabot mostly ### Ignore For Release ignore-for-release: for PRs you dont want rendered in the changelog, usually the release merge to main --- .github/labels.yml | 38 +++++++++++++++++++++++++++ .github/release-drafter.yml | 21 --------------- .github/release.yml | 32 ++++++++++++++++++++++ .github/workflows/labeler.yml | 31 ++++++++++++++++++++++ .github/workflows/release-drafter.yml | 16 ----------- 5 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 .github/labels.yml delete mode 100644 .github/release-drafter.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 00000000..2a28fc81 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,38 @@ +# PR Labels +- name: new-feature + description: for new features in the changelog. + color: 225fee +- name: improvement + description: for improvements in existing functionality in the changelog. + color: 22ee47 +- name: repo-ci-improvement + description: for improvements in the repository or CI workflow in the changelog. + color: c922ee +- name: bugfix + description: for any bug fixes in the changelog. + color: ed8e21 +- name: documentation + description: for updates to the documentation in the changelog. + color: d3e1e6 +- name: dependencies + description: dependency updates usually from dependabot + color: 5c9dff +- name: testing + description: for updates to the testing suite in the changelog. + color: 933ac9 +- name: breaking-change + description: for breaking changes in the changelog. + color: ff0000 +- name: ignore-for-release + description: PRs you do not want to render in the changelog + color: 7b8eac +- name: do-not-merge + description: PRs that should not be merged until the commented issue is resolved + color: eb1515 +# Issue Labels +- name: enhancement + description: issues that request a enhancement + color: 22ee47 +- name: bug + description: issues that report a bug + color: ed8e21 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 79452136..00000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,21 +0,0 @@ -name-template: 'v$NEXT_PATCH_VERSION' -tag-template: 'v$NEXT_PATCH_VERSION' -categories: - - title: '๐Ÿš€ Added' - label: 'added-feature' - - title: '๐Ÿงฐ Changed' - label: 'changed' - - title: "โš ๏ธ Deprecated" - label: "deprecated" - - title: "โš ๏ธ Removed" - label: "removed" - - title: '๐Ÿ› Bug Fixes' - label: 'bugfix' - - title: "โš ๏ธ Security" - label: "security" -change-template: '- $TITLE @$AUTHOR (#$NUMBER)' -no-changes-template: "- No changes" -template: | - ## Changes - - $CHANGES \ No newline at end of file diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..8417f9fb --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,32 @@ +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: โš ๏ธ Breaking Change + labels: + - breaking-change + - title: ๐Ÿ› Bug Fixes + labels: + - bugfix + - title: ๐Ÿš€ New Features + labels: + - new-feature + - title: ๐Ÿ’ก Improvements + labels: + - improvement + - title: ๐Ÿงช Testing Improvements + labels: + - testing + - title: โš™๏ธ Repo/CI Improvements + labels: + - repo-ci-improvement + - title: ๐Ÿ“– Documentation + labels: + - documentation + - title: ๐Ÿ“ฆ Dependency Updates + labels: + - dependencies + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..da42b7e4 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,31 @@ +name: labeler + +on: + push: + branches: + - 'main' + paths: + - '.github/labels.yml' + - '.github/workflows/labeler.yml' + pull_request: + paths: + - '.github/labels.yml' + - '.github/workflows/labeler.yml' + +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Run Labeler + uses: crazy-max/ghaction-github-labeler@de749cf181958193cb7debf1a9c5bb28922f3e1b + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + yaml-file: .github/labels.yml + dry-run: ${{ github.event_name == 'pull_request' }} + exclude: | + help* + *issue diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index b4207e7d..00000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Release Drafter - -on: - push: - branches: - - main # Preemptive for branch change - -jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@569eb7ee3a85817ab916c8f8ff03a5bd96c9c83e # pin@v5 - with: - config-name: release-drafter.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0951c3444d3fe18484e53c95e11f975b28727d4f Mon Sep 17 00:00:00 2001 From: Ye Chen <127243817+yec-akamai@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:52:17 -0400 Subject: [PATCH 9/9] new: Add site_type to `Region` (#386) * add site_type * lint --- linode_api4/objects/region.py | 1 + test/fixtures/regions.json | 33 ++++++++++++------- .../linode_client/test_linode_client.py | 15 ++++++++- test/unit/linode_client_test.py | 1 + test/unit/objects/region_test.py | 1 + 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index 7f48ea84..a20d5ee9 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -22,6 +22,7 @@ class Region(Base): "status": Property(), "resolvers": Property(), "label": Property(), + "site_type": Property(), } @property diff --git a/test/fixtures/regions.json b/test/fixtures/regions.json index ab848b3f..9200455d 100644 --- a/test/fixtures/regions.json +++ b/test/fixtures/regions.json @@ -13,7 +13,8 @@ "ipv4": "172.105.34.5,172.105.35.5,172.105.36.5,172.105.37.5,172.105.38.5,172.105.39.5,172.105.40.5,172.105.41.5,172.105.42.5,172.105.43.5", "ipv6": "2400:8904::f03c:91ff:fea5:659,2400:8904::f03c:91ff:fea5:9282,2400:8904::f03c:91ff:fea5:b9b3,2400:8904::f03c:91ff:fea5:925a,2400:8904::f03c:91ff:fea5:22cb,2400:8904::f03c:91ff:fea5:227a,2400:8904::f03c:91ff:fea5:924c,2400:8904::f03c:91ff:fea5:f7e2,2400:8904::f03c:91ff:fea5:2205,2400:8904::f03c:91ff:fea5:9207" }, - "label": "label1" + "label": "label1", + "site_type": "core" }, { "id": "ca-central", @@ -28,7 +29,8 @@ "ipv4": "172.105.0.5,172.105.3.5,172.105.4.5,172.105.5.5,172.105.6.5,172.105.7.5,172.105.8.5,172.105.9.5,172.105.10.5,172.105.11.5", "ipv6": "2600:3c04::f03c:91ff:fea9:f63,2600:3c04::f03c:91ff:fea9:f6d,2600:3c04::f03c:91ff:fea9:f80,2600:3c04::f03c:91ff:fea9:f0f,2600:3c04::f03c:91ff:fea9:f99,2600:3c04::f03c:91ff:fea9:fbd,2600:3c04::f03c:91ff:fea9:fdd,2600:3c04::f03c:91ff:fea9:fe2,2600:3c04::f03c:91ff:fea9:f68,2600:3c04::f03c:91ff:fea9:f4a" }, - "label": "label2" + "label": "label2", + "site_type": "core" }, { "id": "ap-southeast", @@ -43,7 +45,8 @@ "ipv4": "172.105.166.5,172.105.169.5,172.105.168.5,172.105.172.5,172.105.162.5,172.105.170.5,172.105.167.5,172.105.171.5,172.105.181.5,172.105.161.5", "ipv6": "2400:8907::f03c:92ff:fe6e:ec8,2400:8907::f03c:92ff:fe6e:98e4,2400:8907::f03c:92ff:fe6e:1c58,2400:8907::f03c:92ff:fe6e:c299,2400:8907::f03c:92ff:fe6e:c210,2400:8907::f03c:92ff:fe6e:c219,2400:8907::f03c:92ff:fe6e:1c5c,2400:8907::f03c:92ff:fe6e:c24e,2400:8907::f03c:92ff:fe6e:e6b,2400:8907::f03c:92ff:fe6e:e3d" }, - "label": "label3" + "label": "label3", + "site_type": "core" }, { "id": "us-central", @@ -58,7 +61,8 @@ "ipv4": "72.14.179.5,72.14.188.5,173.255.199.5,66.228.53.5,96.126.122.5,96.126.124.5,96.126.127.5,198.58.107.5,198.58.111.5,23.239.24.5", "ipv6": "2600:3c00::2,2600:3c00::9,2600:3c00::7,2600:3c00::5,2600:3c00::3,2600:3c00::8,2600:3c00::6,2600:3c00::4,2600:3c00::c,2600:3c00::b" }, - "label": "label4" + "label": "label4", + "site_type": "core" }, { "id": "us-west", @@ -73,7 +77,8 @@ "ipv4": "173.230.145.5,173.230.147.5,173.230.155.5,173.255.212.5,173.255.219.5,173.255.241.5,173.255.243.5,173.255.244.5,74.207.241.5,74.207.242.5", "ipv6": "2600:3c01::2,2600:3c01::9,2600:3c01::5,2600:3c01::7,2600:3c01::3,2600:3c01::8,2600:3c01::4,2600:3c01::b,2600:3c01::c,2600:3c01::6" }, - "label": "label5" + "label": "label5", + "site_type": "core" }, { "id": "us-southeast", @@ -88,7 +93,8 @@ "ipv4": "74.207.231.5,173.230.128.5,173.230.129.5,173.230.136.5,173.230.140.5,66.228.59.5,66.228.62.5,50.116.35.5,50.116.41.5,23.239.18.5", "ipv6": "2600:3c02::3,2600:3c02::5,2600:3c02::4,2600:3c02::6,2600:3c02::c,2600:3c02::7,2600:3c02::2,2600:3c02::9,2600:3c02::8,2600:3c02::b" }, - "label": "label6" + "label": "label6", + "site_type": "core" }, { "id": "us-east", @@ -104,7 +110,8 @@ "ipv4": "66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5", "ipv6": "2600:3c03::7,2600:3c03::4,2600:3c03::9,2600:3c03::6,2600:3c03::3,2600:3c03::c,2600:3c03::5,2600:3c03::b,2600:3c03::2,2600:3c03::8" }, - "label": "label7" + "label": "label7", + "site_type": "core" }, { "id": "eu-west", @@ -119,7 +126,8 @@ "ipv4": "178.79.182.5,176.58.107.5,176.58.116.5,176.58.121.5,151.236.220.5,212.71.252.5,212.71.253.5,109.74.192.20,109.74.193.20,109.74.194.20", "ipv6": "2a01:7e00::9,2a01:7e00::3,2a01:7e00::c,2a01:7e00::5,2a01:7e00::6,2a01:7e00::8,2a01:7e00::b,2a01:7e00::4,2a01:7e00::7,2a01:7e00::2" }, - "label": "label8" + "label": "label8", + "site_type": "core" }, { "id": "ap-south", @@ -135,7 +143,8 @@ "ipv4": "139.162.11.5,139.162.13.5,139.162.14.5,139.162.15.5,139.162.16.5,139.162.21.5,139.162.27.5,103.3.60.18,103.3.60.19,103.3.60.20", "ipv6": "2400:8901::5,2400:8901::4,2400:8901::b,2400:8901::3,2400:8901::9,2400:8901::2,2400:8901::8,2400:8901::7,2400:8901::c,2400:8901::6" }, - "label": "label9" + "label": "label9", + "site_type": "core" }, { "id": "eu-central", @@ -151,7 +160,8 @@ "ipv4": "139.162.130.5,139.162.131.5,139.162.132.5,139.162.133.5,139.162.134.5,139.162.135.5,139.162.136.5,139.162.137.5,139.162.138.5,139.162.139.5", "ipv6": "2a01:7e01::5,2a01:7e01::9,2a01:7e01::7,2a01:7e01::c,2a01:7e01::2,2a01:7e01::4,2a01:7e01::3,2a01:7e01::6,2a01:7e01::b,2a01:7e01::8" }, - "label": "label10" + "label": "label10", + "site_type": "core" }, { "id": "ap-northeast", @@ -166,7 +176,8 @@ "ipv4": "139.162.66.5,139.162.67.5,139.162.68.5,139.162.69.5,139.162.70.5,139.162.71.5,139.162.72.5,139.162.73.5,139.162.74.5,139.162.75.5", "ipv6": "2400:8902::3,2400:8902::6,2400:8902::c,2400:8902::4,2400:8902::2,2400:8902::8,2400:8902::7,2400:8902::5,2400:8902::b,2400:8902::9" }, - "label": "label11" + "label": "label11", + "site_type": "core" } ], "page": 1, diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index d7aee9d8..2d7994a2 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -5,7 +5,7 @@ import pytest from linode_api4 import ApiError, LinodeClient -from linode_api4.objects import ConfigInterface, ObjectStorageKeys +from linode_api4.objects import ConfigInterface, ObjectStorageKeys, Region @pytest.fixture(scope="session", autouse=True) @@ -66,6 +66,19 @@ def test_get_domains(test_linode_client, test_domain): assert domain.domain in dom_list +@pytest.mark.smoke +def test_get_regions(test_linode_client): + client = test_linode_client + regions = client.regions() + + region_list = [r.id for r in regions] + + test_region = Region(client, "us-east") + + assert test_region.id in region_list + assert test_region.site_type in ["core", "edge"] + + @pytest.mark.smoke def test_image_create(setup_client_and_linode): client = setup_client_and_linode[0] diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index 3f331c9b..3facd2e9 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -74,6 +74,7 @@ def test_get_regions(self): self.assertIsNotNone(region.resolvers) self.assertIsNotNone(region.resolvers.ipv4) self.assertIsNotNone(region.resolvers.ipv6) + self.assertEqual(region.site_type, "core") def test_get_images(self): r = self.client.images() diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 9c954a3d..77b7ee2a 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -22,6 +22,7 @@ def test_get_region(self): self.assertEqual(region.label, "label7") self.assertEqual(region.status, "ok") self.assertIsNotNone(region.resolvers) + self.assertEqual(region.site_type, "core") def test_list_availability(self): """