diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index 00391cf19..e0f9c0888 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 @@ -33,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 @@ -83,20 +87,17 @@ 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 }} - - 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 }}" \ + 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}" @@ -104,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 }} @@ -135,4 +136,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/.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/.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/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 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/account.py b/linode_api4/groups/account.py index 4eeadcc11..55eab9436 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, @@ -457,7 +458,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 +469,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 @@ -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/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/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/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/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 66c53e336..d55958884 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 @@ -28,6 +28,7 @@ SupportGroup, TagGroup, VolumeGroup, + VPCGroup, ) from linode_api4.objects import Image, and_ from linode_api4.objects.filtering import Filter @@ -36,7 +37,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__) @@ -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) @@ -272,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/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, 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/account.py b/linode_api4/objects/account.py index 264c6ff60..f50c4da32 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}" @@ -654,3 +656,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/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/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}" diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index d928b31b6..d771aaeeb 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 @@ -246,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) @@ -257,42 +268,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 +412,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 +424,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 +475,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 +491,132 @@ 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 MigrationType: + COLD = "cold" + WARM = "warm" + class Instance(Base): """ @@ -720,7 +954,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 @@ -742,6 +982,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 """ @@ -751,6 +995,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) @@ -790,6 +1035,7 @@ def config_create( devices=[], disks=[], volumes=[], + interfaces=[], **kwargs, ): """ @@ -863,12 +1109,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) @@ -1202,7 +1455,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 @@ -1223,10 +1481,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/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/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/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/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 new file mode 100644 index 000000000..e4199283b --- /dev/null +++ b/linode_api4/objects/serializable.py @@ -0,0 +1,143 @@ +import inspect +from dataclasses import asdict, dataclass +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(metaclass=JSONFilterableMetaclass): + """ + 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. + """ + + 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" + ) + + # 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/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/pyproject.toml b/pyproject.toml index edbd11ac9..16ded274d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,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/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/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_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/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/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/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/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/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/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/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/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 a17a6f847..93cff7867 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -1,12 +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" @@ -19,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 @@ -29,15 +51,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())) + chosen_region = available_regions[4] + 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 +68,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())) + chosen_region = available_regions[4] + 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 +102,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 +115,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 +125,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 +152,37 @@ 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()) + region = client.regions()[4] label = "TestSDK-" + timestamp - volume = client.volume_create(label=label, region="ap-west") + volume = client.volume_create(label=label, region=region) 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,13 +193,15 @@ 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) + nodebalancer = client.nodebalancer_create( + region=get_region(client), label=label + ) yield nodebalancer @@ -171,9 +209,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 +221,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 +232,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 +243,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 +262,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" ) @@ -233,3 +271,89 @@ def create_oauth_client(get_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( + "g6-standard-1", 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/helpers.py b/test/integration/helpers.py index eee46f385..5e9d1c441 100644 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -1,4 +1,3 @@ -import random import time from typing import Callable @@ -8,8 +7,14 @@ def get_test_label(): - unique_timestamp = str(int(time.time()) + random.randint(0, 1000)) - 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())[:-3] + label = "test_" + unique_timestamp return label @@ -88,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 ( @@ -104,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 - + "seconds" + "Timeout Error: resource is not available in " + + str(timeout) + + " seconds" ) time.sleep(10) else: diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 0df8bc8d7..e68f54eb2 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] + chosen_region = available_regions[4] # us-ord (Chicago) 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 @@ -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) @@ -50,7 +47,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 +56,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 +114,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 +125,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 +137,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 +167,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 +192,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 +206,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] + chosen_region = available_regions[4] 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 +228,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,20 +263,20 @@ 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() 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) @@ -291,13 +288,13 @@ 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( - "ap-west", + "us-ord", "example-cluster", {"type": "g6-standard-1", "count": 3}, invalid_version, @@ -310,19 +307,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 +327,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 +336,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 +346,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 +361,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 +397,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..3d5fa2d97 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] + chosen_region = available_regions[4] 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..0e14f5041 100644 --- a/test/integration/models/test_database.py +++ b/test/integration/models/test_database.py @@ -34,10 +34,13 @@ 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" + region = "us-ord" engine_id = get_db_engine_id(client, "mysql") dbtype = "g6-standard-1" @@ -61,10 +64,13 @@ 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" + region = "us-ord" engine_id = get_db_engine_id(client, "postgresql") dbtype = "g6-standard-1" @@ -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..7a7f58ff1 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] + chosen_region = available_regions[4] 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..a622b355e 100644 --- a/test/integration/models/test_image.py +++ b/test/integration/models/test_image.py @@ -10,39 +10,39 @@ @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", + "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 22f5709c7..f46a3fc29 100644 --- a/test/integration/models/test_linode.py +++ b/test/integration/models/test_linode.py @@ -2,20 +2,30 @@ from test.integration.helpers import ( get_test_label, retry_sending_request, + send_request_when_resource_available, wait_for_condition, ) 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, +) +from linode_api4.objects.linode import MigrationType @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] + chosen_region = available_regions[4] label = get_test_label() rules = { @@ -26,9 +36,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 +58,74 @@ 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.fixture(scope="session") +def linode_for_network_interface_tests(test_linode_client): + client = test_linode_client + available_regions = client.regions() + chosen_region = available_regions[4] + 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[4] + 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(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] + chosen_region = available_regions[4] 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 +139,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 +156,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] + chosen_region = available_regions[4] 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 +206,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] + chosen_region = available_regions[4] 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 +281,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") @@ -246,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 @@ -263,40 +343,71 @@ 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() assert len(firewalls) > 0 - assert "TestSDK" in firewalls[0].label + assert "test" 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() assert len(volumes) > 0 - assert "TestSDK" in volumes[0].label + assert "test" in volumes[0].label -def test_linode_disk_duplicate(get_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) + + assert disk.size == 5000 + + dup_disk = disk.duplicate() - disk = get_client.load(Disk, linode.disks[0].id, linode.id) + time.sleep(40) - 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) + 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] @@ -323,74 +434,61 @@ 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] + chosen_region = available_regions[4] label = get_test_label() + "_migration" - linode, password = client.linode.instance_create( - "g5-standard-4", chosen_region, image="linode/debian9", 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 - linode.initiate_migration(region="us-central") + send_request_when_resource_available( + 300, + linode.initiate_migration, + region="us-mia", + migration_type=MigrationType.COLD, + ) 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" - ) - linode = create_linode - disk, gen_pass = linode.disk_create() - - -def test_disk_resize(): - pytest.skip( - "Pre-requisite for the test account need to comply with this test" - ) - - 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(get_client, create_linode): - pytest.skip( - "Model get method: client.load(Config, 123, 123) does not work..." - ) +def test_get_config(test_linode_client, create_linode): linode = create_linode - json = get_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 = test_linode_client.load(Config, linode.configs[0].id, linode.id) 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 +496,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,35 +512,199 @@ 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): - 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(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 + + +class TestNetworkInterface: + 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, + ) + + 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 == label + assert interface[1].ipam_address == "10.0.0.3/32" + + def test_create_public(self, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests + + 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, linode_for_network_interface_tests): + linode = linode_for_network_interface_tests + + 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, + 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] + + 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, + 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] + + 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, 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=label, ipam_address="10.0.0.3/32" + ) + + send_request_when_resource_available(300, linode.shutdown) + + interfaces = config.network_interfaces + interfaces.reverse() + + send_request_when_resource_available( + 300, config.interface_reorder, interfaces + ) + config.invalidate() + + assert [v.id for v in config.interfaces[:2]] == [ + 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 = [] + + # 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, + 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_lke.py b/test/integration/models/test_lke.py index 11df1cbcc..04b479e8e 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,23 @@ 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) - - assert cluster._raw_json == create_lke_cluster._raw_json +def test_get_lke_clusters(test_linode_client, lke_cluster): + cluster = test_linode_client.load(LKECluster, lke_cluster.id) + assert cluster._raw_json == lke_cluster._raw_json -def test_get_lke_pool(get_client, create_lke_cluster): - pytest.skip("client.load(LKENodePool, 123, 123) does not work") - cluster = create_lke_cluster +def test_get_lke_pool(test_linode_client, lke_cluster): + pytest.skip("TPT-2511") + 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 +63,16 @@ 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() + res = send_request_when_resource_available(300, cluster.kubeconfig_delete) + assert res is None -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 +82,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 +93,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 +112,20 @@ 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): - pytest.skip( - "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" - ) - cluster = create_lke_cluster +def test_service_token_delete(lke_cluster): + cluster = lke_cluster - cluster.cluster_regenerate() - - -def test_service_token_delete(create_lke_cluster): - pytest.skip( - "Skipping reason: '400: At least one of kubeconfig or servicetoken is required.'" - ) - cluster = create_lke_cluster + res = cluster.service_token_delete() - cluster.service_token_delete() + assert res is None 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 d3b01e0bd..d9f13063e 100644 --- a/test/integration/models/test_networking.py +++ b/test/integration/models/test_networking.py @@ -1,11 +1,13 @@ +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 Config, ConfigInterfaceIPv4, Firewall, IPAddress @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() @@ -15,37 +17,107 @@ def test_get_networking_rules(get_client, create_firewall): assert "outbound_policy" in str(rules) +def create_linode(test_linode_client): + client = test_linode_client + available_regions = client.regions() + chosen_region = available_regions[4] + label = get_rand_nanosec_test_label() + + linode_instance, _ = 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(test_linode_client): + linode = create_linode(test_linode_client) + + yield linode + + linode.delete() + + +@pytest.fixture +def create_linode_to_be_shared_with_ips(test_linode_client): + linode = create_linode(test_linode_client) + + yield linode + + linode.delete() + + @pytest.mark.smoke -def test_ip_addresses_share(self): +def test_ip_addresses_share( + test_linode_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 + + test_linode_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( + test_linode_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 + + test_linode_client.networking.ip_addresses_share( + [linode_instance1.ips.ipv4.public[0]], linode_instance2.id + ) + + # unshared the ip with instance2 + 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_nodebalancer.py b/test/integration/models/test_nodebalancer.py index 455b88f1a..6cec442b4 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] + chosen_region = available_regions[4] 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,10 +27,10 @@ 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] + chosen_region = available_regions[4] label = "nodebalancer_test" nb = client.nodebalancer_create(region=chosen_region, label=label) @@ -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..1b351c14d 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())) + chosen_region = available_regions[4] + 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) 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/script/add_to_xml_test_report.py b/test/script/add_to_xml_test_report.py deleted file mode 100644 index d486028be..000000000 --- a/test/script/add_to_xml_test_report.py +++ /dev/null @@ -1,41 +0,0 @@ -import argparse -import xml.etree.ElementTree as ET - -# 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( - "--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 - -# 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) - -# 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/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/linode_client_test.py b/test/unit/linode_client_test.py index 69af304f7..3f331c9b7 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): """ @@ -492,6 +514,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): """ @@ -525,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) @@ -534,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): @@ -543,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) @@ -555,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, }, @@ -674,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/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) 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..4121380e4 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): """ @@ -540,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) @@ -554,12 +574,21 @@ 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) 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 @@ -602,3 +631,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/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) 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/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) 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 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) 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