diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e1826bae2..7168ea488 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,16 +13,8 @@ jobs: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: - # required for all workflows security-events: write - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - strategy: fail-fast: false matrix: diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index 5e81a7829..31e695aca 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -2,6 +2,22 @@ on: pull_request: workflow_dispatch: inputs: + run_db_fork_tests: + description: 'Set this parameter to "true" to run fork database related test cases' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + run_db_tests: + description: 'Set this parameter to "true" to run database related test cases' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' test_suite: description: 'Enter specific test suite. E.g. domain, linode_client' required: false @@ -80,7 +96,7 @@ jobs: run: | timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" - make testint TEST_ARGS="--junitxml=${report_filename}" TEST_SUITE="${{ github.event.inputs.test_suite }}" + make test-int RUN_DB_FORK_TESTS=${{ github.event.inputs.run_db_fork_tests }} RUN_DB_TESTS=${{ github.event.inputs.run_db_tests }} TEST_ARGS="--junitxml=${report_filename}" TEST_SUITE="${{ github.event.inputs.test_suite }}" env: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index e02b708e1..229aba540 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -3,19 +3,38 @@ name: Integration Tests on: workflow_dispatch: inputs: + run_db_fork_tests: + description: 'Set this parameter to "true" to run fork database related test cases' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + run_db_tests: + description: 'Set this parameter to "true" to run database related test cases' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + test_suite: + description: 'Enter specific test suite. E.g. domain, linode_client' + required: false use_minimal_test_account: - description: 'Use minimal test account' + description: 'Indicate whether to use a minimal test account with limited resources for testing. Defaults to "false"' required: false default: 'false' sha: - description: 'The hash value of the commit' - required: false + description: 'Specify commit hash to test. This value is mandatory to ensure the tests run against a specific commit' + required: true default: '' python-version: - description: 'Specify Python version to use' + description: 'Specify the Python version to use for running tests. Leave empty to use the default Python version configured in the environment' required: false run-eol-python-version: - description: 'Run EOL python version?' + description: 'Indicates whether to run tests using an End-of-Life (EOL) Python version. Defaults to "false". Choose "true" to include tests for deprecated Python versions' required: false default: 'false' type: choice @@ -28,8 +47,8 @@ on: - dev env: - DEFAULT_PYTHON_VERSION: "3.9" - EOL_PYTHON_VERSION: "3.8" + DEFAULT_PYTHON_VERSION: "3.10" + EOL_PYTHON_VERSION: "3.9" EXIT_STATUS: 0 jobs: @@ -72,24 +91,18 @@ jobs: run: | timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" - make testint TEST_ARGS="--junitxml=${report_filename}" + make test-int RUN_DB_FORK_TESTS=${{ github.event.inputs.run_db_fork_tests }} RUN_DB_TESTS=${{ github.event.inputs.run_db_tests }} TEST_SUITE="${{ github.event.inputs.test_suite }}" TEST_ARGS="--junitxml=${report_filename}" env: LINODE_TOKEN: ${{ env.LINODE_TOKEN }} - - name: Upload test results + - name: Upload Test Report as Artifact if: always() - run: | - filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') - python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/add_gha_info_to_xml.py \ - --branch_name "${GITHUB_REF#refs/*/}" \ - --gha_run_id "$GITHUB_RUN_ID" \ - --gha_run_number "$GITHUB_RUN_NUMBER" \ - --xmlfile "${filename}" - sync - python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/xml_to_obj.py "${filename}" - env: - LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }} - LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }} + uses: actions/upload-artifact@v4 + with: + name: test-report-file + if-no-files-found: ignore + path: '*.xml' + retention-days: 1 apply-calico-rules: runs-on: ubuntu-latest @@ -156,68 +169,82 @@ jobs: env: LINODE_CLI_TOKEN: ${{ env.LINODE_TOKEN }} - notify-slack: + process-upload-report: runs-on: ubuntu-latest needs: [integration-tests] - if: ${{ (success() || failure()) && github.repository == 'linode/linode_api4-python' }} # Run even if integration tests fail and only on main repository + if: always() && github.repository == 'linode/linode_api4-python' # Run even if integration tests fail and only on main repository + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Download test report + uses: actions/download-artifact@v4 + with: + name: test-report-file + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Python dependencies + run: pip3 install requests wheel boto3==1.35.99 + + - name: Set release version env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + + - name: Add variables and upload test results + if: always() + run: | + filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') + python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/add_gha_info_to_xml.py \ + --branch_name "${GITHUB_REF#refs/*/}" \ + --gha_run_id "$GITHUB_RUN_ID" \ + --gha_run_number "$GITHUB_RUN_NUMBER" \ + --xmlfile "${filename}" + sync + python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/xml_to_obj.py "${filename}" + env: + LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }} + LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }} + + notify-slack: + runs-on: ubuntu-latest + needs: [integration-tests] + if: ${{ (success() || failure()) }} # Run even if integration tests fail and only on main repository steps: - name: Notify Slack - uses: slackapi/slack-github-action@v1.27.0 + uses: slackapi/slack-github-action@v2.0.0 with: - channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | - { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" - } - }, - { - "type": "divider" - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Build Result:*\n${{ needs.integration-tests.result == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" - }, - { - "type": "mrkdwn", - "text": "*Branch:*\n`${{ github.ref_name }}`" - } - ] - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" - }, - { - "type": "mrkdwn", - "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" - } - ] - }, - { - "type": "divider" - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" - } - ] - } - ] - } - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} \ No newline at end of file + channel: ${{ secrets.SLACK_CHANNEL_ID }} + blocks: + - type: section + text: + type: mrkdwn + text: ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + - type: divider + - type: section + fields: + - type: mrkdwn + text: "*Build Result:*\n${{ needs.integration-tests.result == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + - type: mrkdwn + text: "*Branch:*\n`${{ github.ref_name }}`" + - type: section + fields: + - type: mrkdwn + text: "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + - type: mrkdwn + text: "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + - type: divider + - type: context + elements: + - type: mrkdwn + text: "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 444c69ffd..8a9bcadd2 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Run Labeler - uses: crazy-max/ghaction-github-labeler@b54af0c25861143e7c8813d7cbbf46d2c341680c + uses: crazy-max/ghaction-github-labeler@31674a3852a9074f2086abcf1c53839d466a47e7 with: github-token: ${{ secrets.GITHUB_TOKEN }} yaml-file: .github/labels.yml diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index c0b7b87c1..fc48ee010 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -39,68 +39,39 @@ jobs: - name: Run smoke tests id: smoke_tests run: | - make smoketest + make test-smoke env: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - name: Notify Slack if: always() && github.repository == 'linode/linode_api4-python' - uses: slackapi/slack-github-action@v1.27.0 + uses: slackapi/slack-github-action@v2.0.0 with: - channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | - { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" - } - }, - { - "type": "divider" - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Build Result:*\n${{ steps.smoke_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" - }, - { - "type": "mrkdwn", - "text": "*Branch:*\n`${{ github.ref_name }}`" - } - ] - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" - }, - { - "type": "mrkdwn", - "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" - } - ] - }, - { - "type": "divider" - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" - } - ] - } - ] - } - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + channel: ${{ secrets.SLACK_CHANNEL_ID }} + blocks: + - type: section + text: + type: mrkdwn + text: ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + - type: divider + - type: section + fields: + - type: mrkdwn + text: "*Build Result:*\n${{ steps.smoke_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + - type: mrkdwn + text: "*Branch:*\n`${{ github.ref_name }}`" + - type: section + fields: + - type: mrkdwn + text: "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + - type: mrkdwn + text: "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + - type: divider + - type: context + elements: + - type: mrkdwn + text: "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" diff --git a/.github/workflows/publish-pypi.yaml b/.github/workflows/publish-pypi.yaml index e0bf9e1db..d5338b7a7 100644 --- a/.github/workflows/publish-pypi.yaml +++ b/.github/workflows/publish-pypi.yaml @@ -24,6 +24,6 @@ jobs: LINODE_SDK_VERSION: ${{ github.event.release.tag_name }} - name: Publish the release artifacts to PyPI - uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # pin@release/v1.12.3 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # pin@release/v1.12.4 with: password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/release-notify-slack.yml b/.github/workflows/release-notify-slack.yml index aa3d30af4..ea1a4da68 100644 --- a/.github/workflows/release-notify-slack.yml +++ b/.github/workflows/release-notify-slack.yml @@ -11,20 +11,14 @@ jobs: steps: - name: Notify Slack - Main Message id: main_message - uses: slackapi/slack-github-action@v1.27.0 + uses: slackapi/slack-github-action@v2.0.0 with: - channel-id: ${{ secrets.DEV_DX_SLACK_CHANNEL_ID }} + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | - { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*New Release Published: _linode_api4-python_ <${{ github.event.release.html_url }}|${{ github.event.release.tag_name }}> is now live!* :tada:" - } - } - ] - } - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} \ No newline at end of file + channel: ${{ secrets.DEV_DX_SLACK_CHANNEL_ID }} + blocks: + - type: section + text: + type: mrkdwn + text: "*New Release Published: _linode_api4-python_ <${{ github.event.release.html_url }}|${{ github.event.release.tag_name }}> is now live!* :tada:" \ No newline at end of file diff --git a/Makefile b/Makefile index 03a527169..4bfb1c348 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,9 @@ PYTHON ?= python3 -TEST_CASE_COMMAND := -TEST_SUITE := -TEST_ARGS := - LINODE_SDK_VERSION ?= "0.0.0.dev" VERSION_MODULE_DOCSTRING ?= \"\"\"\nThe version of this linode_api4 package.\n\"\"\"\n\n VERSION_FILE := ./linode_api4/version.py -ifdef TEST_CASE - TEST_CASE_COMMAND = -k $(TEST_CASE) -endif - -ifdef TEST_SUITE - ifneq ($(TEST_SUITE),linode_client) - ifneq ($(TEST_SUITE),login_client) - TEST_COMMAND = models/$(TEST_SUITE) - else - TEST_COMMAND = login_client - endif - else - TEST_COMMAND = linode_client - endif -endif - .PHONY: clean clean: mkdir -p dist @@ -73,14 +53,21 @@ lint: build $(PYTHON) -m pylint linode_api4 $(PYTHON) -m twine check dist/* -.PHONY: testint -testint: - $(PYTHON) -m pytest test/integration/${TEST_COMMAND} ${TEST_CASE_COMMAND} ${TEST_ARGS} +# Integration Test Arguments +# TEST_SUITE: Optional, specify a test suite (e.g. domain), Default to run everything if not set +# TEST_CASE: Optional, specify a test case (e.g. 'test_image_replication') +# TEST_ARGS: Optional, additional arguments for pytest (e.g. '-v' for verbose mode) + +TEST_COMMAND = $(if $(TEST_SUITE),$(if $(filter $(TEST_SUITE),linode_client login_client),$(TEST_SUITE),models/$(TEST_SUITE))) + +.PHONY: test-int +test-int: + $(PYTHON) -m pytest test/integration/${TEST_COMMAND} $(if $(TEST_CASE),-k $(TEST_CASE)) ${TEST_ARGS} -.PHONY: testunit -testunit: +.PHONY: test-unit +test-unit: $(PYTHON) -m pytest test/unit -.PHONY: smoketest -smoketest: +.PHONY: test-smoke +test-smoke: $(PYTHON) -m pytest -m smoke test/integration \ No newline at end of file diff --git a/README.rst b/README.rst index 1e6b310f4..5615bb488 100644 --- a/README.rst +++ b/README.rst @@ -148,16 +148,16 @@ Running the tests ^^^^^^^^^^^^^^^^^ Run the tests locally using the make command. Run the entire test suite using command below:: - make testint + make test-int To run a specific package/suite, use the environment variable `TEST_SUITE` using directory names in `integration/...` folder :: - make TEST_SUITE="account" testint // Runs tests in `integration/models/account` directory - make TEST_SUITE="linode_client" testint // Runs tests in `integration/linode_client` directory + make TEST_SUITE="account" test-int // Runs tests in `integration/models/account` directory + make TEST_SUITE="linode_client" test-int // Runs tests in `integration/linode_client` directory -Lastly to run a specific test case use environment variable `TEST_CASE` with `testint` command:: +Lastly to run a specific test case use environment variable `TEST_CASE` with `test-int` command:: - make TEST_CASE=test_get_domain_record testint + make TEST_CASE=test_get_domain_record test-int Documentation ------------- diff --git a/e2e_scripts b/e2e_scripts index 6b71cb72e..0f2ff0169 160000 --- a/e2e_scripts +++ b/e2e_scripts @@ -1 +1 @@ -Subproject commit 6b71cb72eb20a18ace82f9e73a0f99fe1141d625 +Subproject commit 0f2ff016956090c6fff046f4479e7efe8d0086e5 diff --git a/linode_api4/groups/database.py b/linode_api4/groups/database.py index 957c136cf..8110ea888 100644 --- a/linode_api4/groups/database.py +++ b/linode_api4/groups/database.py @@ -1,13 +1,14 @@ from linode_api4.errors import UnexpectedResponseError from linode_api4.groups import Group from linode_api4.objects import ( - Base, Database, DatabaseEngine, DatabaseType, MySQLDatabase, PostgreSQLDatabase, + drop_null_keys, ) +from linode_api4.objects.base import _flatten_request_body_recursive class DatabaseGroup(Group): @@ -126,13 +127,71 @@ def mysql_create(self, label, region, engine, ltype, **kwargs): params = { "label": label, - "region": region.id if issubclass(type(region), Base) else region, - "engine": engine.id if issubclass(type(engine), Base) else engine, - "type": ltype.id if issubclass(type(ltype), Base) else ltype, + "region": region, + "engine": engine, + "type": ltype, } params.update(kwargs) - result = self.client.post("/databases/mysql/instances", data=params) + result = self.client.post( + "/databases/mysql/instances", + data=_flatten_request_body_recursive(drop_null_keys(params)), + ) + + if "id" not in result: + raise UnexpectedResponseError( + "Unexpected response when creating MySQL Database", json=result + ) + + d = MySQLDatabase(self.client, result["id"], result) + return d + + def mysql_fork(self, source, restore_time, **kwargs): + """ + Forks an :any:`MySQLDatabase` on this account with + the given restore_time. label, region, engine, and ltype are optional. + For example:: + + client = LinodeClient(TOKEN) + + db_to_fork = client.database.mysql_instances()[0] + + new_fork = client.database.mysql_fork( + db_to_fork.id, + db_to_fork.updated, + label="new-fresh-label" + ) + + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-databases-mysql-instances + + :param source: The id of the source database + :type source: int + :param restore_time: The timestamp for the fork + :type restore_time: datetime + :param label: The name for this cluster + :type label: str + :param region: The region to deploy this cluster in + :type region: str | Region + :param engine: The engine to deploy this cluster with + :type engine: str | Engine + :param ltype: The Linode Type to use for this cluster + :type ltype: str | Type + """ + + params = { + "fork": { + "source": source, + "restore_time": restore_time.strftime("%Y-%m-%dT%H:%M:%S"), + } + } + if "ltype" in kwargs: + params["type"] = kwargs["ltype"] + params.update(kwargs) + + result = self.client.post( + "/databases/mysql/instances", + data=_flatten_request_body_recursive(drop_null_keys(params)), + ) if "id" not in result: raise UnexpectedResponseError( @@ -191,14 +250,71 @@ def postgresql_create(self, label, region, engine, ltype, **kwargs): params = { "label": label, - "region": region.id if issubclass(type(region), Base) else region, - "engine": engine.id if issubclass(type(engine), Base) else engine, - "type": ltype.id if issubclass(type(ltype), Base) else ltype, + "region": region, + "engine": engine, + "type": ltype, + } + params.update(kwargs) + + result = self.client.post( + "/databases/postgresql/instances", + data=_flatten_request_body_recursive(drop_null_keys(params)), + ) + + if "id" not in result: + raise UnexpectedResponseError( + "Unexpected response when creating PostgreSQL Database", + json=result, + ) + + d = PostgreSQLDatabase(self.client, result["id"], result) + return d + + def postgresql_fork(self, source, restore_time, **kwargs): + """ + Forks an :any:`PostgreSQLDatabase` on this account with + the given restore_time. label, region, engine, and ltype are optional. + For example:: + + client = LinodeClient(TOKEN) + + db_to_fork = client.database.postgresql_instances()[0] + + new_fork = client.database.postgresql_fork( + db_to_fork.id, + db_to_fork.updated, + label="new-fresh-label" + ) + + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-databases-postgresql-instances + + :param source: The id of the source database + :type source: int + :param restore_time: The timestamp for the fork + :type restore_time: datetime + :param label: The name for this cluster + :type label: str + :param region: The region to deploy this cluster in + :type region: str | Region + :param engine: The engine to deploy this cluster with + :type engine: str | Engine + :param ltype: The Linode Type to use for this cluster + :type ltype: str | Type + """ + + params = { + "fork": { + "source": source, + "restore_time": restore_time.strftime("%Y-%m-%dT%H:%M:%S"), + } } + if "ltype" in kwargs: + params["type"] = kwargs["ltype"] params.update(kwargs) result = self.client.post( - "/databases/postgresql/instances", data=params + "/databases/postgresql/instances", + data=_flatten_request_body_recursive(drop_null_keys(params)), ) if "id" not in result: diff --git a/linode_api4/groups/lke.py b/linode_api4/groups/lke.py index d0de66f37..d64d45536 100644 --- a/linode_api4/groups/lke.py +++ b/linode_api4/groups/lke.py @@ -66,6 +66,7 @@ def cluster_create( control_plane: Union[ LKEClusterControlPlaneOptions, Dict[str, Any] ] = None, + apl_enabled: bool = False, **kwargs, ): """ @@ -100,8 +101,12 @@ def cluster_create( formatted dicts. :param kube_version: The version of Kubernetes to use :type kube_version: KubeVersion or str - :param control_plane: Dict[str, Any] or LKEClusterControlPlaneRequest - :type control_plane: The control plane configuration of this LKE cluster. + :param control_plane: The control plane configuration of this LKE cluster. + :type control_plane: Dict[str, Any] or LKEClusterControlPlaneRequest + :param apl_enabled: Whether this cluster should use APL. + NOTE: This endpoint is in beta and may only + function if base_url is set to `https://api.linode.com/v4beta`. + :type apl_enabled: bool :param kwargs: Any other arguments to pass along to the API. See the API docs for possible values. @@ -120,6 +125,10 @@ def cluster_create( } params.update(kwargs) + # Prevent errors for users without access to APL + if apl_enabled: + params["apl_enabled"] = apl_enabled + result = self.client.post( "/lke/clusters", data=_flatten_request_body_recursive(drop_null_keys(params)), diff --git a/linode_api4/groups/networking.py b/linode_api4/groups/networking.py index 4820b706d..ba1e656bd 100644 --- a/linode_api4/groups/networking.py +++ b/linode_api4/groups/networking.py @@ -367,3 +367,30 @@ def transfer_prices(self, *filters): return self.client._get_and_filter( NetworkTransferPrice, *filters, endpoint="/network-transfer/prices" ) + + def delete_vlan(self, vlan, region): + """ + This operation deletes a VLAN. + You can't delete a VLAN if it's still attached to a Linode. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/delete-vlan + + :param vlan: The label of the VLAN to be deleted. + :type vlan: str or VLAN + :param region: The VLAN's region. + :type region: str or Region + """ + if isinstance(region, Region): + region = region.id + + if isinstance(vlan, VLAN): + vlan = vlan.label + resp = self.client.delete( + "/networking/vlans/{}/{}".format(region, vlan), + model=self, + ) + + if "error" in resp: + return False + + return True diff --git a/linode_api4/objects/database.py b/linode_api4/objects/database.py index 6a028722c..ea833eb8a 100644 --- a/linode_api4/objects/database.py +++ b/linode_api4/objects/database.py @@ -131,7 +131,7 @@ class MySQLDatabase(Base): "label": Property(mutable=True), "allow_list": Property(mutable=True, unordered=True), "backups": Property(derived_class=MySQLDatabaseBackup), - "cluster_size": Property(), + "cluster_size": Property(mutable=True), "created": Property(is_datetime=True), "encrypted": Property(), "engine": Property(), @@ -141,7 +141,9 @@ class MySQLDatabase(Base): "replication_type": Property(), "ssl_connection": Property(), "status": Property(volatile=True), - "type": Property(), + "type": Property(mutable=True), + "fork": Property(), + "oldest_restore_time": Property(is_datetime=True), "updated": Property(volatile=True, is_datetime=True), "updates": Property(mutable=True), "version": Property(), @@ -264,7 +266,7 @@ class PostgreSQLDatabase(Base): "label": Property(mutable=True), "allow_list": Property(mutable=True, unordered=True), "backups": Property(derived_class=PostgreSQLDatabaseBackup), - "cluster_size": Property(), + "cluster_size": Property(mutable=True), "created": Property(is_datetime=True), "encrypted": Property(), "engine": Property(), @@ -275,7 +277,9 @@ class PostgreSQLDatabase(Base): "replication_type": Property(), "ssl_connection": Property(), "status": Property(volatile=True), - "type": Property(), + "type": Property(mutable=True), + "fork": Property(), + "oldest_restore_time": Property(is_datetime=True), "updated": Property(volatile=True, is_datetime=True), "updates": Property(mutable=True), "version": Property(), @@ -414,6 +418,7 @@ class Database(Base): "region": Property(), "status": Property(), "type": Property(), + "fork": Property(), "updated": Property(), "updates": Property(), "version": Property(), diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 8fe71bb7d..46af5d970 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -269,6 +269,7 @@ class Type(Base): "vcpus": Property(), "gpus": Property(), "successor": Property(), + "accelerated_devices": Property(), # type_class is populated from the 'class' attribute of the returned JSON } @@ -1638,6 +1639,22 @@ def firewalls(self): for firewall in result["data"] ] + def apply_firewalls(self): + """ + Reapply assigned firewalls to a Linode in case they were not applied successfully. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-apply-firewalls + + :returns: Returns True if the operation was successful + :rtype: bool + """ + + self._client.post( + "{}/firewalls/apply".format(Instance.api_endpoint), model=self + ) + + return True + def nodebalancers(self): """ View a list of NodeBalancers that are assigned to this Linode and readable by the requesting User. @@ -1919,7 +1936,7 @@ def _serialize(self): def _expand_placement_group_assignment( pg: Union[ InstancePlacementGroupAssignment, "PlacementGroup", Dict[str, Any], int - ] + ], ) -> Optional[Dict[str, Any]]: """ Expands the placement group argument into a dict for use in an API request body. diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index 7ff6b0fd8..e675eae8e 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -257,6 +257,7 @@ class LKECluster(Base): "k8s_version": Property(slug_relationship=KubeVersion, mutable=True), "pools": Property(derived_class=LKENodePool), "control_plane": Property(mutable=True), + "apl_enabled": Property(), } def invalidate(self): @@ -353,6 +354,36 @@ def control_plane_acl(self) -> LKEClusterControlPlaneACL: return LKEClusterControlPlaneACL.from_json(self._control_plane_acl) + @property + def apl_console_url(self) -> Optional[str]: + """ + Returns the URL of this cluster's APL installation if this cluster + is APL-enabled, else None. + + :returns: The URL of the APL console for this cluster. + :rtype: str or None + """ + + if not self.apl_enabled: + return None + + return f"https://console.lke{self.id}.akamai-apl.net" + + @property + def apl_health_check_url(self) -> Optional[str]: + """ + Returns the URL of this cluster's APL health check endpoint if this cluster + is APL-enabled, else None. + + :returns: The URL of the APL console for this cluster. + :rtype: str or None + """ + + if not self.apl_enabled: + return None + + return f"https://auth.lke{self.id}.akamai-apl.net/ready" + def node_pool_create( self, node_type: Union[Type, str], diff --git a/linode_api4/objects/networking.py b/linode_api4/objects/networking.py index 613eca21c..25130a919 100644 --- a/linode_api4/objects/networking.py +++ b/linode_api4/objects/networking.py @@ -112,6 +112,20 @@ def to(self, linode): return {"address": self.address, "linode_id": linode.id} + def delete(self): + """ + Override the delete() function from Base to use the correct endpoint. + """ + resp = self._client.delete( + "/linode/instances/{}/ips/{}".format(self.linode_id, self.address), + model=self, + ) + + if "error" in resp: + return False + self.invalidate() + return True + @dataclass class VPCIPAddress(JSONObject): diff --git a/test/fixtures/linode_types.json b/test/fixtures/linode_types.json index 819867b79..dee3209ee 100644 --- a/test/fixtures/linode_types.json +++ b/test/fixtures/linode_types.json @@ -1,9 +1,10 @@ { - "results": 4, + "results": 5, "pages": 1, "page": 1, "data": [ { + "accelerated_devices": 0, "disk": 20480, "memory": 1024, "transfer": 1000, @@ -52,6 +53,7 @@ "successor": null }, { + "accelerated_devices": 0, "disk": 20480, "memory": 16384, "transfer": 5000, @@ -100,6 +102,7 @@ "successor": null }, { + "accelerated_devices": 0, "disk": 30720, "memory": 2048, "transfer": 2000, @@ -148,6 +151,7 @@ "successor": null }, { + "accelerated_devices": 0, "disk": 49152, "memory": 4096, "transfer": 3000, @@ -194,6 +198,33 @@ } ], "successor": null + }, + { + "id": "g1-accelerated-netint-vpu-t1u1-m", + "label": "Netint Quadra T1U x1 Medium", + "price": { + "hourly": 0.0, + "monthly": 0.0 + }, + "region_prices": [], + "addons": { + "backups": { + "price": { + "hourly": 0.0, + "monthly": 0.0 + }, + "region_prices": [] + } + }, + "memory": 24576, + "disk": 307200, + "transfer": 0, + "vcpus": 12, + "gpus": 0, + "network_out": 16000, + "class": "accelerated", + "successor": null, + "accelerated_devices": 1 } ] } \ No newline at end of file diff --git a/test/fixtures/lke_clusters.json b/test/fixtures/lke_clusters.json index 787a2fae5..1a932c8ec 100644 --- a/test/fixtures/lke_clusters.json +++ b/test/fixtures/lke_clusters.json @@ -6,5 +6,6 @@ "label": "example-cluster", "region": "ap-west", "k8s_version": "1.19", - "tags": [] + "tags": [], + "apl_enabled": true } diff --git a/test/fixtures/lke_clusters_18881.json b/test/fixtures/lke_clusters_18881.json index 755d11c58..bb5807c18 100644 --- a/test/fixtures/lke_clusters_18881.json +++ b/test/fixtures/lke_clusters_18881.json @@ -9,5 +9,6 @@ "tags": [], "control_plane": { "high_availability": true - } + }, + "apl_enabled": true } \ No newline at end of file diff --git a/test/integration/conftest.py b/test/integration/conftest.py index ba4a2ee14..8c7d44a57 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -184,7 +184,7 @@ def create_linode_for_pass_reset(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label, firewall=e2e_test_firewall, ) @@ -502,3 +502,22 @@ def pytest_configure(config): "markers", "smoke: mark test as part of smoke test suite", ) + + +@pytest.fixture(scope="session") +def linode_for_vlan_tests(test_linode_client, e2e_test_firewall): + client = test_linode_client + region = get_region(client, {"Linodes", "Vlans"}, site_type="core") + label = get_test_label(length=8) + + linode_instance, password = client.linode.instance_create( + "g6-nanode-1", + region, + image="linode/debian12", + label=label, + firewall=e2e_test_firewall, + ) + + yield linode_instance + + linode_instance.delete() diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 2802c90f9..eb1b06369 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -19,7 +19,7 @@ def setup_client_and_linode(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label, firewall=e2e_test_firewall, ) @@ -250,7 +250,7 @@ def test_create_linode_instance_without_image(test_linode_client): def test_create_linode_instance_with_image(setup_client_and_linode): linode = setup_client_and_linode[1] - assert re.search("linode/debian10", str(linode.image)) + assert re.search("linode/debian12", str(linode.image)) def test_create_linode_with_interfaces(test_linode_client): @@ -262,7 +262,7 @@ def test_create_linode_with_interfaces(test_linode_client): "g6-nanode-1", region, label=label, - image="linode/debian10", + image="linode/debian12", interfaces=[ {"purpose": "public"}, ConfigInterface( diff --git a/test/integration/models/account/test_account.py b/test/integration/models/account/test_account.py index 8bdc8c60e..decad434f 100644 --- a/test/integration/models/account/test_account.py +++ b/test/integration/models/account/test_account.py @@ -71,7 +71,7 @@ def test_latest_get_event(test_linode_client, e2e_test_firewall): linode, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label, firewall=e2e_test_firewall, ) diff --git a/test/integration/models/database/test_database.py b/test/integration/models/database/test_database.py index b9502abdc..5d8f74b41 100644 --- a/test/integration/models/database/test_database.py +++ b/test/integration/models/database/test_database.py @@ -1,4 +1,4 @@ -import re +import os import time from test.integration.helpers import ( get_test_label, @@ -35,9 +35,6 @@ def get_postgres_db_status(client: LinodeClient, db_id, status: str): @pytest.fixture(scope="session") 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-ord" @@ -65,9 +62,6 @@ def get_db_status(): @pytest.fixture(scope="session") 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-ord" @@ -93,46 +87,90 @@ def get_db_status(): send_request_when_resource_available(300, db.delete) -# ------- SQL DB Test cases ------- -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)" +@pytest.mark.skipif( + os.getenv("RUN_DB_FORK_TESTS", "").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_FORK_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) +def test_fork_sql_db(test_linode_client, test_create_sql_db): + client = test_linode_client + db_fork = client.database.mysql_fork( + test_create_sql_db.id, test_create_sql_db.updated + ) + + def get_db_fork_status(): + return db_fork.status == "active" + + # TAKES 15-30 MINUTES TO FULLY PROVISION DB + wait_for_condition(60, 2000, get_db_fork_status) + + assert db_fork.fork.source == test_create_sql_db.id + + db_fork.delete() + + +@pytest.mark.skipif( + os.getenv("RUN_DB_FORK_TESTS", "").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_FORK_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) +def test_fork_postgres_db(test_linode_client, test_create_postgres_db): + client = test_linode_client + db_fork = client.database.postgresql_fork( + test_create_postgres_db.id, test_create_postgres_db.updated ) + + def get_db_fork_status(): + return db_fork.status == "active" + + # TAKES 15-30 MINUTES TO FULLY PROVISION DB + wait_for_condition(60, 2000, get_db_fork_status) + + assert db_fork.fork.source == test_create_postgres_db.id + + db_fork.delete() + + +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) +def test_get_types(test_linode_client): client = test_linode_client types = client.database.types() assert "nanode" in types[0].type_class assert "g6-nanode-1" in types[0].id - assert types[0].engines.mongodb[0].price.monthly == 15 +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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: assert e.engine in ["mysql", "postgresql"] - assert re.search("[0-9]+.[0-9]+", e.version) + # assert re.search("[0-9]+.[0-9]+", e.version) assert e.id == e.engine + "/" + e.version +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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 ------- +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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: @@ -143,13 +181,14 @@ def test_get_sql_db_instance(test_linode_client, test_create_sql_db): assert str(test_create_sql_db.label) == str(database.label) assert database.cluster_size == 1 assert database.engine == "mysql" - assert "-mysql-primary.servers.linodedb.net" in database.hosts.primary + assert ".g2a.akamaidb.net" in database.hosts.primary +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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"] @@ -161,8 +200,6 @@ def test_update_sql_db(test_linode_client, test_create_sql_db): res = db.save() - database = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) - wait_for_condition( 30, 300, @@ -172,111 +209,29 @@ def test_update_sql_db(test_linode_client, test_create_sql_db): "active", ) + database = test_linode_client.load(MySQLDatabase, test_create_sql_db.id) + assert res assert database.allow_list == new_allow_list - assert database.label == label + # assert database.label == label assert database.updates.day_of_week == 2 -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, - test_linode_client, - test_create_sql_db.id, - "active", - ) - - db.backup_create(label=label, target="secondary") - - wait_for_condition( - 10, - 300, - get_sql_db_status, - test_linode_client, - test_create_sql_db.id, - "backing_up", - ) - - assert db.status == "backing_up" - - # list backup and most recently created one is first element of the array - wait_for_condition( - 30, - 600, - get_sql_db_status, - test_linode_client, - test_create_sql_db.id, - "active", - ) - - backup = db.backups[0] - - assert backup.label == label - assert backup.database_id == test_create_sql_db.id - - assert db.status == "active" - - backup.delete() - - -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: - pytest.skip( - "Skipping this test. Reason: Couldn't find db backup instance" - ) - - backup.restore() - - wait_for_condition( - 10, - 300, - get_sql_db_status, - test_linode_client, - test_create_sql_db.id, - "restoring", - ) - - assert db.status == "restoring" - - wait_for_condition( - 30, - 1000, - get_sql_db_status, - test_linode_client, - test_create_sql_db.id, - "active", - ) - - assert db.status == "active" - - +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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) +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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() @@ -304,20 +259,22 @@ def test_sql_patch(test_linode_client, test_create_sql_db): assert db.status == "active" +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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.username == "akmadmin" assert db.credentials.password +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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) @@ -327,17 +284,20 @@ def test_reset_sql_credentials(test_linode_client, test_create_sql_db): time.sleep(5) - assert db.credentials.username == "linroot" + assert db.credentials.username == "akmadmin" assert db.credentials.password != old_pass # ------- POSTGRESQL DB Test cases ------- +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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() + database = None + for db in dbs: if db.id == test_create_postgres_db.id: database = db @@ -346,13 +306,14 @@ def test_get_postgres_db_instance(test_linode_client, test_create_postgres_db): assert str(test_create_postgres_db.label) == str(database.label) assert database.cluster_size == 1 assert database.engine == "postgresql" - assert "pgsql-primary.servers.linodedb.net" in database.hosts.primary + assert "g2a.akamaidb.net" in database.hosts.primary +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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"] @@ -364,10 +325,6 @@ def test_update_postgres_db(test_linode_client, test_create_postgres_db): res = db.save() - database = test_linode_client.load( - PostgreSQLDatabase, test_create_postgres_db.id - ) - wait_for_condition( 30, 1000, @@ -377,111 +334,31 @@ def test_update_postgres_db(test_linode_client, test_create_postgres_db): "active", ) + database = test_linode_client.load( + PostgreSQLDatabase, test_create_postgres_db.id + ) + assert res assert database.allow_list == new_allow_list assert database.label == label assert database.updates.day_of_week == 2 -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 = test_linode_client.load(PostgreSQLDatabase, test_create_postgres_db.id) - label = "database_backup_test" - - wait_for_condition( - 30, - 1000, - get_postgres_db_status, - test_linode_client, - test_create_postgres_db.id, - "active", - ) - - db.backup_create(label=label, target="secondary") - - # list backup and most recently created one is first element of the array - wait_for_condition( - 10, - 300, - get_sql_db_status, - test_linode_client, - test_create_postgres_db.id, - "backing_up", - ) - - assert db.status == "backing_up" - - # list backup and most recently created one is first element of the array - wait_for_condition( - 30, - 600, - get_sql_db_status, - test_linode_client, - test_create_postgres_db.id, - "active", - ) - - # list backup and most recently created one is first element of the array - backup = db.backups[0] - - assert backup.label == label - assert backup.database_id == 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] - except IndexError as e: - pytest.skip( - "Skipping this test. Reason: Couldn't find db backup instance" - ) - - backup.restore() - - wait_for_condition( - 30, - 1000, - get_postgres_db_status, - test_linode_client, - test_create_postgres_db.id, - "restoring", - ) - - wait_for_condition( - 30, - 1000, - get_postgres_db_status, - test_linode_client, - test_create_postgres_db.id, - "active", - ) - - assert db.status == "active" - - +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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) +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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() @@ -509,22 +386,24 @@ def test_postgres_patch(test_linode_client, test_create_postgres_db): assert db.status == "active" +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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.username == "akmadmin" assert db.credentials.password +@pytest.mark.skipif( + os.getenv("RUN_DB_TESTS").strip().lower() not in {"yes", "true"}, + reason="RUN_DB_TESTS environment variable must be set to 'yes' or 'true' (case insensitive)", +) 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) @@ -533,5 +412,5 @@ def test_reset_postgres_credentials( time.sleep(5) - assert db.credentials.username == "linpostgres" + assert db.credentials.username == "akmadmin" assert db.credentials.password != old_pass diff --git a/test/integration/models/firewall/test_firewall.py b/test/integration/models/firewall/test_firewall.py index 6a9f6f079..16805f3b8 100644 --- a/test/integration/models/firewall/test_firewall.py +++ b/test/integration/models/firewall/test_firewall.py @@ -14,7 +14,7 @@ def linode_fw(test_linode_client): label = get_test_label() linode_instance, password = client.linode.instance_create( - "g6-nanode-1", region, image="linode/debian10", label=label + "g6-nanode-1", region, image="linode/debian12", label=label ) yield linode_instance diff --git a/test/integration/models/image/test_image.py b/test/integration/models/image/test_image.py index 94c819709..9124ddf97 100644 --- a/test/integration/models/image/test_image.py +++ b/test/integration/models/image/test_image.py @@ -32,8 +32,8 @@ def image_upload_url(test_linode_client): @pytest.fixture(scope="session") def test_uploaded_image(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" + 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" diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index a8ba2b21e..d97a8294a 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -38,7 +38,7 @@ def linode_with_volume_firewall(test_linode_client): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label + "_modlinode", ) @@ -76,7 +76,27 @@ def linode_for_network_interface_tests(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", + label=label, + firewall=e2e_test_firewall, + ) + + yield linode_instance + + linode_instance.delete() + + +@pytest.fixture(scope="session") +def linode_for_vpu_tests(test_linode_client, e2e_test_firewall): + client = test_linode_client + region = "us-lax" + + label = get_test_label(length=8) + + linode_instance, password = client.linode.instance_create( + "g1-accelerated-netint-vpu-t1u1-s", + region, + image="linode/debian12", label=label, firewall=e2e_test_firewall, ) @@ -147,7 +167,7 @@ def create_linode_for_long_running_tests(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label + "_long_tests", firewall=e2e_test_firewall, ) @@ -169,7 +189,7 @@ def linode_with_disk_encryption(test_linode_client, request): linode_instance, password = client.linode.instance_create( "g6-nanode-1", target_region, - image="linode/ubuntu23.10", + image="linode/ubuntu24.10", label=label, booted=False, disk_encryption=disk_encryption, @@ -196,6 +216,13 @@ def test_get_linode(test_linode_client, linode_with_volume_firewall): assert linode.id == linode_with_volume_firewall.id +def test_get_vpu(test_linode_client, linode_for_vpu_tests): + linode = test_linode_client.load(Instance, linode_for_vpu_tests.id) + + assert linode.label == linode_for_vpu_tests.label + assert hasattr(linode.specs, "accelerated_devices") + + def test_linode_transfer(test_linode_client, linode_with_volume_firewall): linode = test_linode_client.load(Instance, linode_with_volume_firewall.id) @@ -214,7 +241,7 @@ def test_linode_rebuild(test_linode_client): label = get_test_label() + "_rebuild" linode, password = client.linode.instance_create( - "g6-nanode-1", region, image="linode/debian10", label=label + "g6-nanode-1", region, image="linode/debian12", label=label ) wait_for_condition(10, 100, get_status, linode, "running") @@ -222,14 +249,14 @@ def test_linode_rebuild(test_linode_client): retry_sending_request( 3, linode.rebuild, - "linode/debian10", + "linode/debian12", disk_encryption=InstanceDiskEncryptionType.disabled, ) wait_for_condition(10, 100, get_status, linode, "rebuilding") assert linode.status == "rebuilding" - assert linode.image.id == "linode/debian10" + assert linode.image.id == "linode/debian12" assert linode.disk_encryption == InstanceDiskEncryptionType.disabled @@ -272,7 +299,7 @@ def test_delete_linode(test_linode_client): linode_instance, password = client.linode.instance_create( "g6-nanode-1", region, - image="linode/debian10", + image="linode/debian12", label=label + "_linode", ) @@ -426,6 +453,14 @@ def test_linode_firewalls(linode_with_volume_firewall): assert "firewall" in firewalls[0].label +def test_linode_apply_firewalls(linode_with_volume_firewall): + linode = linode_with_volume_firewall + + result = linode.apply_firewalls() + + assert result + + def test_linode_volumes(linode_with_volume_firewall): linode = linode_with_volume_firewall @@ -591,6 +626,9 @@ def test_get_linode_types(test_linode_client): assert len(types) > 0 assert "g6-nanode-1" in ids + for linode_type in types: + assert hasattr(linode_type, "accelerated_devices") + def test_get_linode_types_overrides(test_linode_client): types = test_linode_client.linode.types() @@ -691,6 +729,9 @@ def test_create_vlan(self, linode_for_network_interface_tests): assert interface.label == "testvlan" assert interface.ipam_address == "10.0.0.2/32" + def test_create_vpu(self, test_linode_client, linode_for_vpu_tests): + assert hasattr(linode_for_vpu_tests.specs, "accelerated_devices") + def test_create_vpc( self, test_linode_client, diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index 4a3ba6c7e..f2fb3f2e5 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -110,6 +110,32 @@ def lke_cluster_with_labels_and_taints(test_linode_client): cluster.delete() +@pytest.fixture(scope="session") +def lke_cluster_with_apl(test_linode_client): + version = test_linode_client.lke.versions()[0] + + region = get_region(test_linode_client, {"Kubernetes", "Disk Encryption"}) + + # NOTE: g6-dedicated-4 is the minimum APL-compatible Linode type + node_pools = test_linode_client.lke.node_pool("g6-dedicated-4", 3) + label = get_test_label() + "_cluster" + + cluster = test_linode_client.lke.cluster_create( + region, + label, + node_pools, + version, + control_plane=LKEClusterControlPlaneOptions( + high_availability=True, + ), + apl_enabled=True, + ) + + yield cluster + + cluster.delete() + + def get_cluster_status(cluster: LKECluster, status: str): return cluster._raw_json["status"] == status @@ -328,6 +354,19 @@ def test_lke_cluster_labels_and_taints(lke_cluster_with_labels_and_taints): assert LKENodePoolTaint.from_json(updated_taints[1]) in pool.taints +@pytest.mark.flaky(reruns=3, reruns_delay=2) +def test_lke_cluster_with_apl(lke_cluster_with_apl): + assert lke_cluster_with_apl.apl_enabled == True + assert ( + lke_cluster_with_apl.apl_console_url + == f"https://console.lke{lke_cluster_with_apl.id}.akamai-apl.net" + ) + assert ( + lke_cluster_with_apl.apl_health_check_url + == f"https://auth.lke{lke_cluster_with_apl.id}.akamai-apl.net/ready" + ) + + def test_lke_types(test_linode_client): types = test_linode_client.lke.types() diff --git a/test/integration/models/networking/test_networking.py b/test/integration/models/networking/test_networking.py index 430bd94b9..032436246 100644 --- a/test/integration/models/networking/test_networking.py +++ b/test/integration/models/networking/test_networking.py @@ -4,11 +4,15 @@ get_region, get_token, ) -from test.integration.helpers import get_test_label +from test.integration.helpers import ( + get_test_label, + retry_sending_request, + wait_for_condition, +) import pytest -from linode_api4 import LinodeClient +from linode_api4 import Instance, LinodeClient from linode_api4.objects import Config, ConfigInterfaceIPv4, Firewall, IPAddress from linode_api4.objects.networking import NetworkTransferPrice, Price @@ -150,3 +154,58 @@ def test_network_transfer_prices(test_linode_client): transfer_prices[0].price is None or transfer_prices[0].price.hourly >= 0 ) + + +def test_allocate_and_delete_ip(test_linode_client, create_linode): + linode = create_linode + ip = test_linode_client.networking.ip_allocate(linode.id) + linode.invalidate() + + assert ip.linode_id == linode.id + assert ip.address in linode.ipv4 + + is_deleted = ip.delete() + + assert is_deleted is True + + +def get_status(linode: Instance, status: str): + return linode.status == status + + +def test_create_and_delete_vlan(test_linode_client, linode_for_vlan_tests): + linode = linode_for_vlan_tests + + config: Config = linode.configs[0] + + config.interfaces = [] + config.save() + + vlan_label = "testvlan" + interface = config.interface_create_vlan( + label=vlan_label, ipam_address="10.0.0.2/32" + ) + + config.invalidate() + + assert interface.id == config.interfaces[0].id + assert interface.purpose == "vlan" + assert interface.label == vlan_label + + # Remove the VLAN interface and reboot Linode + config.interfaces = [] + config.save() + + wait_for_condition(3, 100, get_status, linode, "running") + + retry_sending_request(3, linode.reboot) + + wait_for_condition(3, 100, get_status, linode, "rebooting") + assert linode.status == "rebooting" + + # Delete the VLAN + is_deleted = test_linode_client.networking.delete_vlan( + vlan_label, linode.region + ) + + assert is_deleted is True diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index d8a8a53b1..21f4d0322 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -36,7 +36,7 @@ def linode_with_private_ip(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", TEST_REGION, - image="linode/debian10", + image="linode/debian12", label=label, private_ip=True, firewall=e2e_test_firewall, diff --git a/test/integration/models/volume/test_volume.py b/test/integration/models/volume/test_volume.py index 6588d92a7..56395d203 100644 --- a/test/integration/models/volume/test_volume.py +++ b/test/integration/models/volume/test_volume.py @@ -49,7 +49,7 @@ def linode_for_volume(test_linode_client, e2e_test_firewall): linode_instance, password = client.linode.instance_create( "g6-nanode-1", TEST_REGION, - image="linode/debian10", + image="linode/debian12", label=label, firewall=e2e_test_firewall, ) diff --git a/test/unit/groups/database_test.py b/test/unit/groups/database_test.py new file mode 100644 index 000000000..09d842b77 --- /dev/null +++ b/test/unit/groups/database_test.py @@ -0,0 +1,192 @@ +import logging +from test.unit.base import ClientBaseCase + +from linode_api4.objects import MySQLDatabase + +logger = logging.getLogger(__name__) + + +class DatabaseTest(ClientBaseCase): + """ + Tests methods of the DatabaseGroup class + """ + + def test_get_types(self): + """ + Test that database types are properly handled + """ + types = self.client.database.types() + + self.assertEqual(len(types), 1) + self.assertEqual(types[0].type_class, "nanode") + self.assertEqual(types[0].id, "g6-nanode-1") + self.assertEqual(types[0].engines.mysql[0].price.monthly, 20) + + def test_get_engines(self): + """ + Test that database engines are properly handled + """ + engines = self.client.database.engines() + + self.assertEqual(len(engines), 2) + + self.assertEqual(engines[0].engine, "mysql") + self.assertEqual(engines[0].id, "mysql/8.0.26") + self.assertEqual(engines[0].version, "8.0.26") + + self.assertEqual(engines[1].engine, "postgresql") + self.assertEqual(engines[1].id, "postgresql/10.14") + self.assertEqual(engines[1].version, "10.14") + + def test_get_databases(self): + """ + Test that databases are properly handled + """ + dbs = self.client.database.instances() + + self.assertEqual(len(dbs), 1) + self.assertEqual(dbs[0].allow_list[1], "192.0.1.0/24") + self.assertEqual(dbs[0].cluster_size, 3) + self.assertEqual(dbs[0].encrypted, False) + self.assertEqual(dbs[0].engine, "mysql") + self.assertEqual( + dbs[0].hosts.primary, + "lin-123-456-mysql-mysql-primary.servers.linodedb.net", + ) + self.assertEqual( + dbs[0].hosts.secondary, + "lin-123-456-mysql-primary-private.servers.linodedb.net", + ) + self.assertEqual(dbs[0].id, 123) + self.assertEqual(dbs[0].region, "us-east") + self.assertEqual(dbs[0].updates.duration, 3) + self.assertEqual(dbs[0].version, "8.0.26") + + def test_database_instance(self): + """ + Ensures that the .instance attribute properly translates database types + """ + + dbs = self.client.database.instances() + db_translated = dbs[0].instance + + self.assertTrue(isinstance(db_translated, MySQLDatabase)) + self.assertEqual(db_translated.ssl_connection, True) + + +class MySQLDatabaseTest(ClientBaseCase): + """ + Tests methods of the MySQLDatabase class + """ + + def test_get_instances(self): + """ + Test that database types are properly handled + """ + dbs = self.client.database.mysql_instances() + + self.assertEqual(len(dbs), 1) + self.assertEqual(dbs[0].allow_list[1], "192.0.1.0/24") + self.assertEqual(dbs[0].cluster_size, 3) + self.assertEqual(dbs[0].encrypted, False) + self.assertEqual(dbs[0].engine, "mysql") + self.assertEqual( + dbs[0].hosts.primary, + "lin-123-456-mysql-mysql-primary.servers.linodedb.net", + ) + self.assertEqual( + dbs[0].hosts.secondary, + "lin-123-456-mysql-primary-private.servers.linodedb.net", + ) + self.assertEqual(dbs[0].id, 123) + self.assertEqual(dbs[0].region, "us-east") + self.assertEqual(dbs[0].updates.duration, 3) + self.assertEqual(dbs[0].version, "8.0.26") + + def test_create(self): + """ + Test that MySQL databases can be created + """ + + with self.mock_post("/databases/mysql/instances") as m: + # We don't care about errors here; we just want to + # validate the request. + try: + self.client.database.mysql_create( + "cool", + "us-southeast", + "mysql/8.0.26", + "g6-standard-1", + cluster_size=3, + ) + except Exception as e: + logger.warning( + "An error occurred while validating the request: %s", e + ) + + self.assertEqual(m.method, "post") + self.assertEqual(m.call_url, "/databases/mysql/instances") + self.assertEqual(m.call_data["label"], "cool") + self.assertEqual(m.call_data["region"], "us-southeast") + self.assertEqual(m.call_data["engine"], "mysql/8.0.26") + self.assertEqual(m.call_data["type"], "g6-standard-1") + self.assertEqual(m.call_data["cluster_size"], 3) + + +class PostgreSQLDatabaseTest(ClientBaseCase): + """ + Tests methods of the PostgreSQLDatabase class + """ + + def test_get_instances(self): + """ + Test that database types are properly handled + """ + dbs = self.client.database.postgresql_instances() + + self.assertEqual(len(dbs), 1) + self.assertEqual(dbs[0].allow_list[1], "192.0.1.0/24") + self.assertEqual(dbs[0].cluster_size, 3) + self.assertEqual(dbs[0].encrypted, False) + self.assertEqual(dbs[0].engine, "postgresql") + self.assertEqual( + dbs[0].hosts.primary, + "lin-0000-000-pgsql-primary.servers.linodedb.net", + ) + self.assertEqual( + dbs[0].hosts.secondary, + "lin-0000-000-pgsql-primary-private.servers.linodedb.net", + ) + self.assertEqual(dbs[0].id, 123) + self.assertEqual(dbs[0].region, "us-east") + self.assertEqual(dbs[0].updates.duration, 3) + self.assertEqual(dbs[0].version, "13.2") + + def test_create(self): + """ + Test that PostgreSQL databases can be created + """ + + with self.mock_post("/databases/postgresql/instances") as m: + # We don't care about errors here; we just want to + # validate the request. + try: + self.client.database.postgresql_create( + "cool", + "us-southeast", + "postgresql/13.2", + "g6-standard-1", + cluster_size=3, + ) + except Exception as e: + logger.warning( + "An error occurred while validating the request: %s", e + ) + + self.assertEqual(m.method, "post") + self.assertEqual(m.call_url, "/databases/postgresql/instances") + self.assertEqual(m.call_data["label"], "cool") + self.assertEqual(m.call_data["region"], "us-southeast") + self.assertEqual(m.call_data["engine"], "postgresql/13.2") + self.assertEqual(m.call_data["type"], "g6-standard-1") + self.assertEqual(m.call_data["cluster_size"], 3) diff --git a/test/unit/groups/image_test.py b/test/unit/groups/image_test.py new file mode 100644 index 000000000..e2aab386b --- /dev/null +++ b/test/unit/groups/image_test.py @@ -0,0 +1,37 @@ +from test.unit.base import ClientBaseCase + + +class ImageTest(ClientBaseCase): + """ + Tests methods of the Image class + """ + + def test_image_create_cloud_init(self): + """ + Test that an image can be created successfully with cloud-init. + """ + + with self.mock_post("images/private/123") as m: + self.client.images.create( + "Test Image", + "us-southeast", + description="very real image upload.", + cloud_init=True, + ) + + self.assertTrue(m.call_data["cloud_init"]) + + def test_image_create_upload_cloud_init(self): + """ + Test that an image upload URL can be created successfully with cloud-init. + """ + + with self.mock_post("images/upload") as m: + self.client.images.create_upload( + "Test Image", + "us-southeast", + description="very real image upload.", + cloud_init=True, + ) + + self.assertTrue(m.call_data["cloud_init"]) diff --git a/test/unit/groups/linode_test.py b/test/unit/groups/linode_test.py new file mode 100644 index 000000000..8112a5d93 --- /dev/null +++ b/test/unit/groups/linode_test.py @@ -0,0 +1,118 @@ +from test.unit.base import ClientBaseCase + +from linode_api4 import InstancePlacementGroupAssignment +from linode_api4.objects import ConfigInterface + + +class LinodeTest(ClientBaseCase): + """ + Tests methods of the Linode class + """ + + def test_instance_create_with_user_data(self): + """ + Tests that the metadata field is populated on Linode create. + """ + + with self.mock_post("linode/instances/123") as m: + self.client.linode.instance_create( + "g6-nanode-1", + "us-southeast", + metadata=self.client.linode.build_instance_metadata( + user_data="cool" + ), + ) + + self.assertEqual( + m.call_data, + { + "region": "us-southeast", + "type": "g6-nanode-1", + "metadata": {"user_data": "Y29vbA=="}, + }, + ) + + def test_instance_create_with_interfaces(self): + """ + Tests that user can pass a list of interfaces on Linode create. + """ + interfaces = [ + {"purpose": "public"}, + ConfigInterface( + purpose="vlan", label="cool-vlan", ipam_address="10.0.0.4/32" + ), + ] + with self.mock_post("linode/instances/123") as m: + self.client.linode.instance_create( + "us-southeast", + "g6-nanode-1", + interfaces=interfaces, + ) + + self.assertEqual( + m.call_data["interfaces"], + [ + {"purpose": "public"}, + { + "purpose": "vlan", + "label": "cool-vlan", + "ipam_address": "10.0.0.4/32", + }, + ], + ) + + def test_build_instance_metadata(self): + """ + Tests that the metadata field is built correctly. + """ + self.assertEqual( + self.client.linode.build_instance_metadata(user_data="cool"), + {"user_data": "Y29vbA=="}, + ) + + self.assertEqual( + self.client.linode.build_instance_metadata( + user_data="cool", encode_user_data=False + ), + {"user_data": "cool"}, + ) + + def test_create_with_placement_group(self): + """ + Tests that you can create a Linode with a Placement Group + """ + + with self.mock_post("linode/instances/123") as m: + self.client.linode.instance_create( + "g6-nanode-1", + "eu-west", + placement_group=InstancePlacementGroupAssignment( + id=123, + compliant_only=True, + ), + ) + + self.assertEqual( + m.call_data["placement_group"], {"id": 123, "compliant_only": True} + ) + + +class TypeTest(ClientBaseCase): + def test_get_types(self): + """ + Tests that Linode types can be returned + """ + types = self.client.linode.types() + + self.assertEqual(len(types), 5) + for t in types: + self.assertTrue(t._populated) + self.assertIsNotNone(t.id) + self.assertIsNotNone(t.label) + self.assertIsNotNone(t.disk) + self.assertIsNotNone(t.type_class) + self.assertIsNotNone(t.gpus) + self.assertIsNone(t.successor) + self.assertIsNotNone(t.region_prices) + self.assertIsNotNone(t.addons.backups.region_prices) + self.assertIsNotNone(t.accelerated_devices) diff --git a/test/unit/groups/lke_test.py b/test/unit/groups/lke_test.py new file mode 100644 index 000000000..a39db81a6 --- /dev/null +++ b/test/unit/groups/lke_test.py @@ -0,0 +1,43 @@ +from test.unit.base import ClientBaseCase + +from linode_api4.objects import ( + LKEClusterControlPlaneACLAddressesOptions, + LKEClusterControlPlaneACLOptions, + LKEClusterControlPlaneOptions, +) + + +class LKETest(ClientBaseCase): + """ + Tests methods of the LKE class + """ + + def test_cluster_create_with_acl(self): + """ + Tests that an LKE cluster can be created with a control plane ACL configuration. + """ + + with self.mock_post("lke/clusters") as m: + self.client.lke.cluster_create( + "us-mia", + "test-acl-cluster", + [self.client.lke.node_pool("g6-nanode-1", 3)], + "1.29", + control_plane=LKEClusterControlPlaneOptions( + acl=LKEClusterControlPlaneACLOptions( + enabled=True, + addresses=LKEClusterControlPlaneACLAddressesOptions( + ipv4=["10.0.0.1/32"], ipv6=["1234::5678"] + ), + ) + ), + ) + + assert "high_availability" not in m.call_data["control_plane"] + assert m.call_data["control_plane"]["acl"]["enabled"] + assert m.call_data["control_plane"]["acl"]["addresses"]["ipv4"] == [ + "10.0.0.1/32" + ] + assert m.call_data["control_plane"]["acl"]["addresses"]["ipv6"] == [ + "1234::5678" + ] diff --git a/test/unit/groups/placement_test.py b/test/unit/groups/placement_test.py new file mode 100644 index 000000000..3c8337845 --- /dev/null +++ b/test/unit/groups/placement_test.py @@ -0,0 +1,68 @@ +from test.unit.base import ClientBaseCase + +from linode_api4 import PlacementGroupPolicy +from linode_api4.objects import ( + MigratedInstance, + PlacementGroup, + PlacementGroupMember, + PlacementGroupType, +) + + +class PlacementTest(ClientBaseCase): + """ + Tests methods of the Placement Group + """ + + def test_list_pgs(self): + """ + Tests that you can list PGs. + """ + + pgs = self.client.placement.groups() + + self.validate_pg_123(pgs[0]) + assert pgs[0]._populated + + def test_create_pg(self): + """ + Tests that you can create a Placement Group. + """ + + with self.mock_post("/placement/groups/123") as m: + pg = self.client.placement.group_create( + "test", + "eu-west", + PlacementGroupType.anti_affinity_local, + PlacementGroupPolicy.strict, + ) + + assert m.call_url == "/placement/groups" + + self.assertEqual( + m.call_data, + { + "label": "test", + "region": "eu-west", + "placement_group_type": str( + PlacementGroupType.anti_affinity_local + ), + "placement_group_policy": PlacementGroupPolicy.strict, + }, + ) + + assert pg._populated + self.validate_pg_123(pg) + + def validate_pg_123(self, pg: PlacementGroup): + assert pg.id == 123 + assert pg.label == "test" + assert pg.region.id == "eu-west" + assert pg.placement_group_type == "anti_affinity:local" + assert pg.placement_group_policy == "strict" + assert pg.is_compliant + assert pg.members[0] == PlacementGroupMember( + linode_id=123, is_compliant=True + ) + assert pg.migrations.inbound[0] == MigratedInstance(linode_id=123) + assert pg.migrations.outbound[0] == MigratedInstance(linode_id=456) diff --git a/test/unit/objects/polling_test.py b/test/unit/groups/polling_test.py similarity index 100% rename from test/unit/objects/polling_test.py rename to test/unit/groups/polling_test.py diff --git a/test/unit/groups/region_test.py b/test/unit/groups/region_test.py new file mode 100644 index 000000000..fe44c13ab --- /dev/null +++ b/test/unit/groups/region_test.py @@ -0,0 +1,51 @@ +import json +from test.unit.base import ClientBaseCase + +from linode_api4.objects.region import RegionAvailabilityEntry + + +class RegionTest(ClientBaseCase): + """ + Tests methods of the Region class + """ + + 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"}] + } diff --git a/test/unit/groups/vpc_test.py b/test/unit/groups/vpc_test.py new file mode 100644 index 000000000..7b8c985d2 --- /dev/null +++ b/test/unit/groups/vpc_test.py @@ -0,0 +1,107 @@ +import datetime +from test.unit.base import ClientBaseCase + +from linode_api4 import DATE_FORMAT, VPC, VPCSubnet + + +class VPCTest(ClientBaseCase): + """ + Tests methods of the VPC Group + """ + + 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_list_ips(self): + """ + Validates that all VPC IPs can be listed. + """ + + with self.mock_get("/vpcs/ips") as m: + result = self.client.vpcs.ips() + + assert m.call_url == "/vpcs/ips" + assert len(result) == 1 + + ip = result[0] + assert ip.address == "10.0.0.2" + assert ip.address_range is None + assert ip.vpc_id == 123 + assert ip.subnet_id == 456 + assert ip.region == "us-mia" + assert ip.linode_id == 123 + assert ip.config_id == 456 + assert ip.interface_id == 789 + assert ip.active + assert ip.nat_1_1 == "172.233.179.133" + assert ip.gateway == "10.0.0.1" + assert ip.prefix == 24 + assert ip.subnet_mask == "255.255.255.0" + + def validate_vpc_123456(self, vpc: VPC): + expected_dt = datetime.datetime.strptime( + "2018-01-01T00:01:01", DATE_FORMAT + ) + + 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/test/unit/objects/database_test.py b/test/unit/objects/database_test.py index d5b84cebb..11b2379aa 100644 --- a/test/unit/objects/database_test.py +++ b/test/unit/objects/database_test.py @@ -1,8 +1,11 @@ +import logging from test.unit.base import ClientBaseCase from linode_api4 import PostgreSQLDatabase from linode_api4.objects import MySQLDatabase +logger = logging.getLogger(__name__) + class DatabaseTest(ClientBaseCase): """ @@ -106,6 +109,8 @@ def test_create(self): Test that MySQL databases can be created """ + logger = logging.getLogger(__name__) + with self.mock_post("/databases/mysql/instances") as m: # We don't care about errors here; we just want to # validate the request. @@ -117,8 +122,10 @@ def test_create(self): "g6-standard-1", cluster_size=3, ) - except Exception: - pass + except Exception as e: + logger.warning( + "An error occurred while validating the request: %s", e + ) self.assertEqual(m.method, "post") self.assertEqual(m.call_url, "/databases/mysql/instances") @@ -178,8 +185,10 @@ def test_create_backup(self): # validate the request. try: db.backup_create("mybackup", target="secondary") - except Exception: - pass + except Exception as e: + logger.warning( + "An error occurred while validating the request: %s", e + ) self.assertEqual(m.method, "post") self.assertEqual( @@ -361,8 +370,10 @@ def test_create_backup(self): # validate the request. try: db.backup_create("mybackup", target="secondary") - except Exception: - pass + except Exception as e: + logger.warning( + "An error occurred while validating the request: %s", e + ) self.assertEqual(m.method, "post") self.assertEqual( diff --git a/test/unit/objects/image_test.py b/test/unit/objects/image_test.py index 5d1ce42d5..0869919d6 100644 --- a/test/unit/objects/image_test.py +++ b/test/unit/objects/image_test.py @@ -8,8 +8,8 @@ # A minimal gzipped image that will be accepted by the API 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" + 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" ) @@ -114,36 +114,6 @@ def put_mock(url: str, data: Optional[BinaryIO] = None, **kwargs): self.assertEqual(image.tags[0], "test_tag") self.assertEqual(image.tags[1], "test2") - def test_image_create_cloud_init(self): - """ - Test that an image can be created successfully with cloud-init. - """ - - with self.mock_post("images/private/123") as m: - self.client.images.create( - "Test Image", - "us-southeast", - description="very real image upload.", - cloud_init=True, - ) - - self.assertTrue(m.call_data["cloud_init"]) - - def test_image_create_upload_cloud_init(self): - """ - Test that an image upload URL can be created successfully with cloud-init. - """ - - with self.mock_post("images/upload") as m: - self.client.images.create_upload( - "Test Image", - "us-southeast", - description="very real image upload.", - cloud_init=True, - ) - - self.assertTrue(m.call_data["cloud_init"]) - def test_image_replication(self): """ Test that image can be replicated. diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 700e5d0db..6016d2776 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -1,11 +1,7 @@ from datetime import datetime from test.unit.base import ClientBaseCase -from linode_api4 import ( - InstanceDiskEncryptionType, - InstancePlacementGroupAssignment, - NetworkInterface, -) +from linode_api4 import InstanceDiskEncryptionType, NetworkInterface from linode_api4.objects import ( Config, ConfigInterface, @@ -284,6 +280,19 @@ def test_firewalls(self): self.assertEqual(m.call_url, "/linode/instances/123/firewalls") self.assertEqual(len(result), 1) + def test_apply_firewalls(self): + """ + Tests that you can submit a correct apply firewalls api request + """ + linode = Instance(self.client, 123) + + with self.mock_post({}) as m: + result = linode.apply_firewalls() + self.assertEqual( + m.call_url, "/linode/instances/123/firewalls/apply" + ) + self.assertEqual(result, True) + def test_volumes(self): """ Tests that you can submit a correct volumes api request @@ -413,7 +422,7 @@ def test_create_disk(self): 1234, label="test", authorized_users=["test"], - image="linode/debian10", + image="linode/debian12", ) self.assertEqual(m.call_url, "/linode/instances/123/disks") self.assertEqual( @@ -422,7 +431,7 @@ def test_create_disk(self): "size": 1234, "label": "test", "root_pass": gen_pass, - "image": "linode/debian10", + "image": "linode/debian12", "authorized_users": ["test"], "read_only": False, }, @@ -431,74 +440,6 @@ def test_create_disk(self): assert disk.id == 12345 assert disk.disk_encryption == InstanceDiskEncryptionType.disabled - def test_instance_create_with_user_data(self): - """ - Tests that the metadata field is populated on Linode create. - """ - - with self.mock_post("linode/instances/123") as m: - self.client.linode.instance_create( - "g6-nanode-1", - "us-southeast", - metadata=self.client.linode.build_instance_metadata( - user_data="cool" - ), - ) - - self.assertEqual( - m.call_data, - { - "region": "us-southeast", - "type": "g6-nanode-1", - "metadata": {"user_data": "Y29vbA=="}, - }, - ) - - def test_instance_create_with_interfaces(self): - """ - Tests that user can pass a list of interfaces on Linode create. - """ - interfaces = [ - {"purpose": "public"}, - ConfigInterface( - purpose="vlan", label="cool-vlan", ipam_address="10.0.0.4/32" - ), - ] - with self.mock_post("linode/instances/123") as m: - self.client.linode.instance_create( - "us-southeast", - "g6-nanode-1", - interfaces=interfaces, - ) - - self.assertEqual( - m.call_data["interfaces"], - [ - {"purpose": "public"}, - { - "purpose": "vlan", - "label": "cool-vlan", - "ipam_address": "10.0.0.4/32", - }, - ], - ) - - def test_build_instance_metadata(self): - """ - Tests that the metadata field is built correctly. - """ - self.assertEqual( - self.client.linode.build_instance_metadata(user_data="cool"), - {"user_data": "Y29vbA=="}, - ) - - self.assertEqual( - self.client.linode.build_instance_metadata( - user_data="cool", encode_user_data=False - ), - {"user_data": "cool"}, - ) - def test_get_placement_group(self): """ Tests that you can get the placement group for a Linode @@ -522,25 +463,6 @@ def test_get_placement_group(self): assert pg.label == "test" assert pg.placement_group_type == "anti_affinity:local" - def test_create_with_placement_group(self): - """ - Tests that you can create a Linode with a Placement Group - """ - - with self.mock_post("linode/instances/123") as m: - self.client.linode.instance_create( - "g6-nanode-1", - "eu-west", - placement_group=InstancePlacementGroupAssignment( - id=123, - compliant_only=True, - ), - ) - - self.assertEqual( - m.call_data["placement_group"], {"id": 123, "compliant_only": True} - ) - class DiskTest(ClientBaseCase): """ @@ -650,23 +572,6 @@ def test_get_stackscript(self): class TypeTest(ClientBaseCase): - def test_get_types(self): - """ - Tests that Linode types can be returned - """ - types = self.client.linode.types() - - self.assertEqual(len(types), 4) - for t in types: - self.assertTrue(t._populated) - self.assertIsNotNone(t.id) - self.assertIsNotNone(t.label) - self.assertIsNotNone(t.disk) - self.assertIsNotNone(t.type_class) - self.assertIsNotNone(t.gpus) - self.assertIsNone(t.successor) - self.assertIsNotNone(t.region_prices) - self.assertIsNotNone(t.addons.backups.region_prices) def test_get_type_by_id(self): """ diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index 100f36487..1a39b69bc 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -39,6 +39,7 @@ def test_get_cluster(self): self.assertEqual(cluster.region.id, "ap-west") self.assertEqual(cluster.k8s_version.id, "1.19") self.assertTrue(cluster.control_plane.high_availability) + self.assertTrue(cluster.apl_enabled) def test_get_pool(self): """ @@ -166,36 +167,6 @@ def test_load_node_pool(self): self.assertIsNotNone(pool.autoscaler) self.assertIsNotNone(pool.tags) - def test_cluster_create_with_acl(self): - """ - Tests that an LKE cluster can be created with a control plane ACL configuration. - """ - - with self.mock_post("lke/clusters") as m: - self.client.lke.cluster_create( - "us-mia", - "test-acl-cluster", - [self.client.lke.node_pool("g6-nanode-1", 3)], - "1.29", - control_plane=LKEClusterControlPlaneOptions( - acl=LKEClusterControlPlaneACLOptions( - enabled=True, - addresses=LKEClusterControlPlaneACLAddressesOptions( - ipv4=["10.0.0.1/32"], ipv6=["1234::5678"] - ), - ) - ), - ) - - assert "high_availability" not in m.call_data["control_plane"] - assert m.call_data["control_plane"]["acl"]["enabled"] - assert m.call_data["control_plane"]["acl"]["addresses"]["ipv4"] == [ - "10.0.0.1/32" - ] - assert m.call_data["control_plane"]["acl"]["addresses"]["ipv6"] == [ - "1234::5678" - ] - def test_cluster_get_acl(self): """ Tests that an LKE cluster can be created with a control plane ACL configuration. @@ -352,6 +323,40 @@ def test_cluster_create_with_labels_and_taints(self): ], } + def test_cluster_create_with_apl(self): + """ + Tests that an LKE cluster can be created with APL enabled. + """ + + with self.mock_post("lke/clusters") as m: + cluster = self.client.lke.cluster_create( + "us-mia", + "test-aapl-cluster", + [ + self.client.lke.node_pool( + "g6-dedicated-4", + 3, + ) + ], + "1.29", + apl_enabled=True, + control_plane=LKEClusterControlPlaneOptions( + high_availability=True, + ), + ) + + assert m.call_data["apl_enabled"] == True + assert m.call_data["control_plane"]["high_availability"] == True + + assert ( + cluster.apl_console_url == "https://console.lke18881.akamai-apl.net" + ) + + assert ( + cluster.apl_health_check_url + == "https://auth.lke18881.akamai-apl.net/ready" + ) + def test_populate_with_taints(self): """ Tests that LKENodePool correctly handles a list of LKENodePoolTaint and Dict objects. diff --git a/test/unit/objects/networking_test.py b/test/unit/objects/networking_test.py index dabf1ee2b..d12167d8c 100644 --- a/test/unit/objects/networking_test.py +++ b/test/unit/objects/networking_test.py @@ -1,6 +1,6 @@ from test.unit.base import ClientBaseCase -from linode_api4 import ExplicitNullValue +from linode_api4 import VLAN, ExplicitNullValue, Instance, Region from linode_api4.objects import Firewall, IPAddress, IPv6Range @@ -83,3 +83,28 @@ def test_vpc_nat_1_1(self): 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") + + def test_delete_ip(self): + """ + Tests that deleting an IP creates the correct api request + """ + with self.mock_delete() as m: + ip = IPAddress(self.client, "127.0.0.1") + ip.to(Instance(self.client, 123)) + ip.delete() + + self.assertEqual(m.call_url, "/linode/instances/123/ips/127.0.0.1") + + def test_delete_vlan(self): + """ + Tests that deleting a VLAN creates the correct api request + """ + with self.mock_delete() as m: + self.client.networking.delete_vlan( + VLAN(self.client, "vlan-test"), + Region(self.client, "us-southeast"), + ) + + self.assertEqual( + m.call_url, "/networking/vlans/us-southeast/vlan-test" + ) diff --git a/test/unit/objects/placement_test.py b/test/unit/objects/placement_test.py index 4e5960e7b..08fcdc1e4 100644 --- a/test/unit/objects/placement_test.py +++ b/test/unit/objects/placement_test.py @@ -1,11 +1,9 @@ from test.unit.base import ClientBaseCase -from linode_api4 import PlacementGroupPolicy from linode_api4.objects import ( MigratedInstance, PlacementGroup, PlacementGroupMember, - PlacementGroupType, ) @@ -25,46 +23,6 @@ def test_get_placement_group(self): self.validate_pg_123(pg) assert pg._populated - def test_list_pgs(self): - """ - Tests that you can list PGs. - """ - - pgs = self.client.placement.groups() - - self.validate_pg_123(pgs[0]) - assert pgs[0]._populated - - def test_create_pg(self): - """ - Tests that you can create a Placement Group. - """ - - with self.mock_post("/placement/groups/123") as m: - pg = self.client.placement.group_create( - "test", - "eu-west", - PlacementGroupType.anti_affinity_local, - PlacementGroupPolicy.strict, - ) - - assert m.call_url == "/placement/groups" - - self.assertEqual( - m.call_data, - { - "label": "test", - "region": "eu-west", - "placement_group_type": str( - PlacementGroupType.anti_affinity_local - ), - "placement_group_policy": PlacementGroupPolicy.strict, - }, - ) - - assert pg._populated - self.validate_pg_123(pg) - def test_pg_assign(self): """ Tests that you can assign to a PG. diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index a7fcc2694..0bc1afa9e 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -1,8 +1,6 @@ -import json from test.unit.base import ClientBaseCase from linode_api4.objects import Region -from linode_api4.objects.region import RegionAvailabilityEntry class RegionTest(ClientBaseCase): @@ -30,47 +28,6 @@ def test_get_region(self): region.placement_group_limits.maximum_linodes_per_pg, 5 ) - 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. diff --git a/test/unit/objects/vpc_test.py b/test/unit/objects/vpc_test.py index 4d80716d4..5e7be1b69 100644 --- a/test/unit/objects/vpc_test.py +++ b/test/unit/objects/vpc_test.py @@ -30,55 +30,6 @@ def test_list_vpcs(self): 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. @@ -138,7 +89,7 @@ def test_list_ips(self): ip = result[0] assert ip.address == "10.0.0.2" - assert ip.address_range == None + assert ip.address_range is None assert ip.vpc_id == 123 assert ip.subnet_id == 456 assert ip.region == "us-mia"