diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fa18c9e5e..fc36b138b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,7 +18,7 @@ jobs: path: .ansible/collections/ansible_collections/linode/cloud - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} @@ -55,7 +55,7 @@ jobs: path: .ansible/collections/ansible_collections/linode/cloud - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} diff --git a/.github/workflows/integration-tests-pr.yml b/.github/workflows/integration-tests-pr.yml index d880c9689..c5a35b9bb 100644 --- a/.github/workflows/integration-tests-pr.yml +++ b/.github/workflows/integration-tests-pr.yml @@ -40,7 +40,7 @@ jobs: submodules: 'recursive' - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' @@ -83,7 +83,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 id: update-check-run if: ${{ inputs.pull_request_number != '' && fromJson(steps.commit-hash.outputs.data).repository.pullRequest.headRef.target.oid == inputs.sha }} env: @@ -144,7 +144,7 @@ jobs: steps: - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0ca9d3461..042d3048c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -41,7 +41,7 @@ jobs: submodules: 'recursive' - name: Setup Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ inputs.run-eol-python-version == 'true' && env.EOL_PYTHON_VERSION || inputs.python-version || env.DEFAULT_PYTHON_VERSION }} @@ -89,7 +89,7 @@ jobs: submodules: 'recursive' - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' @@ -174,7 +174,7 @@ jobs: steps: - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a5188c17e..836c5a005 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: uses: actions/checkout@v5 - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index 06e99e9df..2f6dae602 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -29,7 +29,7 @@ jobs: submodules: 'recursive' - name: Setup Python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ inputs.run-eol-python-version == 'true' && env.EOL_PYTHON_VERSION || inputs.python-version || env.DEFAULT_PYTHON_VERSION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f51abab..e7933c8b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: path: .ansible/collections/ansible_collections/linode/cloud - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7363dfc34..c9fa4bb93 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -21,7 +21,7 @@ jobs: path: .ansible/collections/ansible_collections/linode/cloud - name: setup python 3 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/README.md b/README.md index 738dd893f..f49003520 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,11 @@ Name | Description | [linode.cloud.volume_list](./docs/modules/volume_list.md)|List and filter on Linode Volumes.| [linode.cloud.volume_type_list](./docs/modules/volume_type_list.md)|List and filter on Volume Types.| [linode.cloud.vpc_ip_list](./docs/modules/vpc_ip_list.md)|List and filter on VPC IP Addresses.| +[linode.cloud.vpc_ipv6_list](./docs/modules/vpc_ipv6_list.md)|List and filter on all VPC IPv6 addresses for a given VPC.| [linode.cloud.vpc_list](./docs/modules/vpc_list.md)|List and filter on VPCs.| [linode.cloud.vpc_subnet_list](./docs/modules/vpc_subnet_list.md)|List and filter on VPC Subnets.| [linode.cloud.vpcs_ip_list](./docs/modules/vpcs_ip_list.md)|List and filter on all VPC IP Addresses.| +[linode.cloud.vpcs_ipv6_list](./docs/modules/vpcs_ipv6_list.md)|List and filter on all VPC IPv6 addresses.| ### Inventory Plugins diff --git a/docs/modules/database_list.md b/docs/modules/database_list.md index e1816d9a2..770a26b3e 100644 --- a/docs/modules/database_list.md +++ b/docs/modules/database_list.md @@ -67,6 +67,11 @@ List and filter on Linode Managed Databases. "id": 123, "instance_uri": "/v4/databases/mysql/instances/123", "label": "example-db", + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "us-east", "status": "active", "type": "g6-dedicated-2", diff --git a/docs/modules/database_mysql_v2.md b/docs/modules/database_mysql_v2.md index 93fdf3e52..42ce4b268 100644 --- a/docs/modules/database_mysql_v2.md +++ b/docs/modules/database_mysql_v2.md @@ -70,6 +70,20 @@ Create, read, and update a Linode MySQL database. state: present ``` +```yaml +- name: Create a MySQL database attached to a VPC + linode.cloud.database_mysql_v2: + label: my-db + region: us-mia + engine: mysql/8 + type: g6-nanode-1 + private_network: + vpc_id: 123 + subnet_id: 456 + public_access: true + state: present +``` + ```yaml - name: Delete a MySQL database linode.cloud.database_mysql_v2: @@ -88,6 +102,8 @@ Create, read, and update a Linode MySQL database. | `engine` |
`str`
|
Optional
| The Managed Database engine in engine/version format. **(Updatable)** | | [`engine_config` (sub-options)](#engine_config) |
`dict`
|
Optional
| Various parameters used to configure this database's underlying engine. NOTE: If a configuration parameter is not current accepted by this field, configure using the linode.cloud.api_request module. **(Updatable)** | | `label` |
`str`
|
Optional
| The label of the Managed Database. | +| `detach_private_network` |
`bool`
|
Optional
| If true, the Managed Database will be detached from its current private network when `private_network` is null. If the Managed Database is not currently attached to a private network or the private_network field is specified, this option has no effect. This is not necessary when switching between VPC subnets. **(Default: `False`)** | +| [`private_network` (sub-options)](#private_network) |
`dict`
|
Optional
| Restricts access to this database using a virtual private cloud (VPC) that you've configured in the region where the database will live. **(Updatable)** | | `region` |
`str`
|
Optional
| The region of the Managed Database. | | `type` |
`str`
|
Optional
| The Linode Instance type used by the Managed Database for its nodes. **(Updatable)** | | [`fork` (sub-options)](#fork) |
`dict`
|
Optional
| Information about a database to fork from. | @@ -133,6 +149,14 @@ Create, read, and update a Linode MySQL database. | `tmp_table_size` |
`int`
|
Optional
| Limits the size of internal in-memory tables. Also sets max_heap_table_size. Default is 16777216 (16M). | | `wait_timeout` |
`int`
|
Optional
| The number of seconds the server waits for activity on a noninteractive connection before closing it. | +### private_network + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `vpc_id` |
`int`
|
**Required**
| The ID of the virtual private cloud (VPC) to restrict access to this database using | +| `subnet_id` |
`int`
|
**Required**
| The ID of the VPC subnet to restrict access to this database using. | +| `public_access` |
`bool`
|
Optional
| Set to `true` to allow clients outside of the VPC to connect to the database using a public IP address. **(Default: `False`)** | + ### fork | Field | Type | Required | Description | @@ -183,6 +207,11 @@ Create, read, and update a Linode MySQL database. "oldest_restore_time": "2025-02-10T20:15:07", "platform": "rdbms-default", "port": 11876, + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "ap-west", "ssl_connection": true, "status": "active", diff --git a/docs/modules/database_postgresql_v2.md b/docs/modules/database_postgresql_v2.md index 3528e7dc5..efe6aa913 100644 --- a/docs/modules/database_postgresql_v2.md +++ b/docs/modules/database_postgresql_v2.md @@ -70,6 +70,20 @@ Create, read, and update a Linode PostgreSQL database. state: present ``` +```yaml +- name: Create a PostgreSQL database attached to a VPC + linode.cloud.database_postgresql_v2: + label: my-db + region: us-mia + engine: postgresql/16 + type: g6-nanode-1 + private_network: + vpc_id: 123 + subnet_id: 456 + public_access: true + state: present +``` + ```yaml - name: Delete a PostgreSQL database linode.cloud.database_postgresql_v2: @@ -88,6 +102,8 @@ Create, read, and update a Linode PostgreSQL database. | `engine` |
`str`
|
Optional
| The Managed Database engine in engine/version format. **(Updatable)** | | [`engine_config` (sub-options)](#engine_config) |
`dict`
|
Optional
| Various parameters used to configure this database's underlying engine. NOTE: If a configuration parameter is not current accepted by this field, configure using the linode.cloud.api_request module. **(Updatable)** | | `label` |
`str`
|
Optional
| The label of the Managed Database. | +| `detach_private_network` |
`bool`
|
Optional
| If true, the Managed Database will be detached from its current private network when `private_network` is null. If the Managed Database is not currently attached to a private network or the private_network field is specified, this option has no effect. This is not necessary when switching between VPC subnets. **(Default: `False`)** | +| [`private_network` (sub-options)](#private_network) |
`dict`
|
Optional
| Restricts access to this database using a virtual private cloud (VPC) that you've configured in the region where the database will live. **(Updatable)** | | `region` |
`str`
|
Optional
| The region of the Managed Database. | | `type` |
`str`
|
Optional
| The Linode Instance type used by the Managed Database for its nodes. **(Updatable)** | | [`fork` (sub-options)](#fork) |
`dict`
|
Optional
| Information about a database to fork from. | @@ -158,6 +174,14 @@ Create, read, and update a Linode PostgreSQL database. |-----------|------|----------|------------------------------------------------------------------------------| | `max_failover_replication_time_lag` |
`int`
|
Optional
| Number of seconds of master unavailability before triggering database failover to standby. | +### private_network + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `vpc_id` |
`int`
|
**Required**
| The ID of the virtual private cloud (VPC) to restrict access to this database using | +| `subnet_id` |
`int`
|
**Required**
| The ID of the VPC subnet to restrict access to this database using. | +| `public_access` |
`bool`
|
Optional
| Set to `true` to allow clients outside of the VPC to connect to the database using a public IP address. **(Default: `False`)** | + ### fork | Field | Type | Required | Description | @@ -208,6 +232,11 @@ Create, read, and update a Linode PostgreSQL database. "oldest_restore_time": "2025-02-10T20:15:07", "platform": "rdbms-default", "port": 11876, + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "ap-west", "ssl_connection": true, "status": "active", diff --git a/docs/modules/instance.md b/docs/modules/instance.md index 6631b29d2..166c656f1 100644 --- a/docs/modules/instance.md +++ b/docs/modules/instance.md @@ -107,6 +107,43 @@ Manage Linode Instances, Configs, and Disks. state: present ``` +```yaml +- name: Create a Linode Instance with a VPC interface and a NAT 1-1 mapping to its public IPv4 address. + linode.cloud.instance: + label: my-vpc-instance + region: us-mia + type: g6-nanode-1 + image: linode/alpine3.21 + booted: true + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv4: + nat_1_1: any + state: present +``` + +```yaml +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a Linode Instance with a public VPC interface, assigning one IPv6 SLAAC prefix and one additional IPv6 range. + linode.cloud.instance: + label: my-vpc-ipv6-instance + region: us-mia + type: g6-nanode-1 + image: linode/alpine3.21 + booted: true + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv6: + is_public: true + slaac: + - range: auto + ranges: + - range: auto + state: present +``` + ```yaml - name: Delete a Linode instance. linode.cloud.instance: @@ -262,11 +299,39 @@ Manage Linode Instances, Configs, and Disks. | `purpose` |
`str`
|
**Required**
| The type of interface. **(Choices: `public`, `vlan`, `vpc`)** | | `primary` |
`bool`
|
Optional
| Whether this is a primary interface **(Default: `False`)** | | `subnet_id` |
`int`
|
Optional
| The ID of the VPC subnet to assign this interface to. | -| `ipv4` |
`dict`
|
Optional
| The IPv4 configuration for this interface. (VPC only) | +| [`ipv4` (sub-options)](#ipv4) |
`dict`
|
Optional
| The IPv4 configuration for this interface. (VPC only) | +| [`ipv6` (sub-options)](#ipv6) |
`dict`
|
Optional
| The IPv6 configuration for this interface. (VPC only) NOTE: IPv6 VPCs may not currently be available to all users. | | `label` |
`str`
|
Optional
| The name of this interface. Required for vlan purpose interfaces. Must be an empty string or null for public purpose interfaces. | | `ipam_address` |
`str`
|
Optional
| This Network Interface’s private IP address in Classless Inter-Domain Routing (CIDR) notation. | | `ip_ranges` |
`list`
|
Optional
| Packets to these CIDR ranges are routed to the VPC network interface. (VPC only) | +### ipv4 + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `vpc` |
`str`
|
Optional
| The IP from the VPC subnet to use for this interface. | +| `nat_1_1` |
`str`
|
Optional
| The public IPv4 address assigned to the Linode will be 1:1 with the VPC IPv4 address. | + +### ipv6 + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `is_public` |
`bool`
|
Optional
| If true, connections from the interface to IPv6 addresses outside the VPC, and connections from IPv6 addresses outside the VPC to the interface will be permitted. | +| [`slaac` (sub-options)](#slaac) |
`list`
|
Optional
| An array of SLAAC prefixes to use for this interface. | +| [`ranges` (sub-options)](#ranges) |
`list`
|
Optional
| An array of SLAAC prefixes to use for this interface. | + +### slaac + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `range` |
`str`
|
Optional
| A SLAAC prefix to add to this interface, or `auto` for a new IPv6 prefix to be automatically allocated. | + +### ranges + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `range` |
`str`
|
Optional
| A prefix to add to this interface, or `auto` for a new IPv6 prefix to be automatically allocated. | + ### disks | Field | Type | Required | Description | @@ -419,6 +484,28 @@ Manage Linode Instances, Configs, and Disks. "ipam_address": "10.0.0.1/24", "label": "example-interface", "purpose": "vlan" + }, + { + "ip_ranges": null, + "ipam_address": null, + "ipv4": null, + "ipv6": { + "is_public": null, + "ranges": [ + { + "range": "auto" + } + ], + "slaac": [ + { + "range": "auto" + } + ] + }, + "label": null, + "primary": false, + "purpose": "vpc", + "subnet_id": 271176 } ], "kernel": "linode/latest-64bit", @@ -511,6 +598,25 @@ Manage Linode Instances, Configs, and Disks. "subnet_mask": "255.255.255.0", "type": "ipv4" } + ], + "vpc": [ + { + "active": true, + "address": "10.0.0.2", + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": "10.0.0.1", + "interface_id": 12345, + "linode_id": 12345, + "nat_1_1": null, + "nodebalancer_id": null, + "prefix": 24, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "255.255.255.0", + "vpc_id": 12345 + } ] }, "ipv6": { @@ -541,6 +647,54 @@ Manage Linode Instances, Configs, and Disks. "region": "us-east", "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "type": "ipv6" + }, + "vpc": { + "vpc": [ + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + }, + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:2::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + } + ] } } } diff --git a/docs/modules/instance_info.md b/docs/modules/instance_info.md index 143fc22b8..284e2d2f4 100644 --- a/docs/modules/instance_info.md +++ b/docs/modules/instance_info.md @@ -151,6 +151,28 @@ Get info about a Linode Instance. "ipam_address": "10.0.0.1/24", "label": "example-interface", "purpose": "vlan" + }, + { + "ip_ranges": null, + "ipam_address": null, + "ipv4": null, + "ipv6": { + "is_public": null, + "ranges": [ + { + "range": "auto" + } + ], + "slaac": [ + { + "range": "auto" + } + ] + }, + "label": null, + "primary": false, + "purpose": "vpc", + "subnet_id": 271176 } ], "kernel": "linode/latest-64bit", @@ -243,6 +265,25 @@ Get info about a Linode Instance. "subnet_mask": "255.255.255.0", "type": "ipv4" } + ], + "vpc": [ + { + "active": true, + "address": "10.0.0.2", + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": "10.0.0.1", + "interface_id": 12345, + "linode_id": 12345, + "nat_1_1": null, + "nodebalancer_id": null, + "prefix": 24, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "255.255.255.0", + "vpc_id": 12345 + } ] }, "ipv6": { @@ -273,6 +314,54 @@ Get info about a Linode Instance. "region": "us-east", "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "type": "ipv6" + }, + "vpc": { + "vpc": [ + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + }, + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:2::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + } + ] } } } diff --git a/docs/modules/lke_cluster.md b/docs/modules/lke_cluster.md index c472f6cca..cf84cf044 100644 --- a/docs/modules/lke_cluster.md +++ b/docs/modules/lke_cluster.md @@ -89,6 +89,7 @@ Manage Linode LKE clusters. |-----------|------|----------|------------------------------------------------------------------------------| | `count` |
`int`
|
**Required**
| The number of nodes in the Node Pool. **(Updatable)** | | `type` |
`str`
|
**Required**
| The Linode Type for all of the nodes in the Node Pool. | +| `label` |
`str`
|
Optional
| A unique label for this Node Pool. **(Updatable)** | | [`autoscaler` (sub-options)](#autoscaler) |
`dict`
|
Optional
| When enabled, the number of nodes autoscales within the defined minimum and maximum values. **(Updatable)** | | `labels` |
`dict`
|
Optional
| Key-value pairs added as labels to nodes in the node pool. Labels help classify your nodes and to easily select subsets of objects. **(Updatable)** | | [`taints` (sub-options)](#taints) |
`list`
|
Optional
| Kubernetes taints to add to node pool nodes. Taints help control how pods are scheduled onto nodes, specifically allowing them to repel certain pods. **(Updatable)** | diff --git a/docs/modules/lke_node_pool.md b/docs/modules/lke_node_pool.md index 18787a4f4..a24df30d2 100644 --- a/docs/modules/lke_node_pool.md +++ b/docs/modules/lke_node_pool.md @@ -57,6 +57,7 @@ Manage Linode LKE cluster node pools. | `state` |
`str`
|
**Required**
| The desired state of the target. **(Choices: `present`, `absent`)** | | [`autoscaler` (sub-options)](#autoscaler) |
`dict`
|
Optional
| When enabled, the number of nodes autoscales within the defined minimum and maximum values. **(Updatable)** | | `count` |
`int`
|
Optional
| The number of nodes in the Node Pool. **(Updatable)** | +| `label` |
`str`
|
Optional
| A unique label for this Node Pool. **(Updatable)** | | [`disks` (sub-options)](#disks) |
`list`
|
Optional
| This Node Pool’s custom disk layout. Each item in this array will create a new disk partition for each node in this Node Pool. | | `type` |
`str`
|
Optional
| The Linode Type for all of the nodes in the Node Pool. Required if `state` == `present`. | | `skip_polling` |
`bool`
|
Optional
| If true, the module will not wait for all nodes in the node pool to be ready. **(Default: `False`)** | diff --git a/docs/modules/vpc.md b/docs/modules/vpc.md index 9ec0a996f..c781a5bd3 100644 --- a/docs/modules/vpc.md +++ b/docs/modules/vpc.md @@ -23,6 +23,17 @@ Create, read, and update a Linode VPC. state: present ``` +```yaml +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a VPC with an auto-allocated IPv6 range + linode.cloud.vpc: + label: my-vpc + region: us-east + ipv6: + - range: auto + state: present +``` + ```yaml - name: Delete a VPC linode.cloud.vpc: @@ -39,6 +50,14 @@ Create, read, and update a Linode VPC. | `state` |
`str`
|
**Required**
| The state of this token. **(Choices: `present`, `absent`)** | | `description` |
`str`
|
Optional
| A description describing this VPC. | | `region` |
`str`
|
Optional
| The region this VPC is located in. | +| [`ipv6` (sub-options)](#ipv6) |
`list`
|
Optional
| A list of IPv6 ranges in CIDR notation. NOTE: IPv6 VPCs may not currently be available to all users. | + +### ipv6 + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `range` |
`str`
|
Optional
| The IPv6 range assigned to this VPC. | +| `allocation_class` |
`str`
|
Optional
| The labeled IPv6 Inventory that the VPC Prefix should be allocated from. | ## Return Values @@ -50,6 +69,11 @@ Create, read, and update a Linode VPC. "created": "2023-08-31T18:35:01", "description": "A description of this VPC", "id": 344, + "ipv6": [ + { + "range": "2001:db8:acad:0::/52" + } + ], "label": "my-vpc", "region": "us-east", "subnets": [], diff --git a/docs/modules/vpc_info.md b/docs/modules/vpc_info.md index 507bf2af5..af4d79782 100644 --- a/docs/modules/vpc_info.md +++ b/docs/modules/vpc_info.md @@ -44,6 +44,11 @@ Get info about a Linode VPC. "created": "2023-08-31T18:35:01", "description": "A description of this VPC", "id": 344, + "ipv6": [ + { + "range": "2001:db8:acad:0::/52" + } + ], "label": "my-vpc", "region": "us-east", "subnets": [], diff --git a/docs/modules/vpc_ipv6_list.md b/docs/modules/vpc_ipv6_list.md new file mode 100644 index 000000000..83da75c9c --- /dev/null +++ b/docs/modules/vpc_ipv6_list.md @@ -0,0 +1,118 @@ +# vpc_ipv6_list + +List and filter on all VPC IPv6 addresses for a given VPC. + +NOTE: IPv6 VPCs may not currently be available to all users. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all IPv6 addresses for a specific VPC. + linode.cloud.vpc_ipv6_list: + vpc_id: 12345 +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `vpc_id` |
`int`
|
**Required**
| The parent VPC for the VPC IPv6 Addresses. | +| `order` |
`str`
|
Optional
| The order to list VPC IPv6 Addresses in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order VPC IPv6 Addresses by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting VPC IPv6 Addresses. | +| `count` |
`int`
|
Optional
| The number of VPC IPv6 Addresses to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-vpc-ipv6s). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `addresses` - The returned VPC IPv6 Addresses. + + - Sample Response: + ```json + [ + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 271170, + "subnet_mask": "", + "vpc_id": 262108 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-vpc-ipv6s) for a list of returned fields + + diff --git a/docs/modules/vpc_list.md b/docs/modules/vpc_list.md index 1c6357ccf..c95b2a28c 100644 --- a/docs/modules/vpc_list.md +++ b/docs/modules/vpc_list.md @@ -55,6 +55,11 @@ List and filter on VPCs. "created": "2023-08-31T18:35:01", "description": "A description of this VPC", "id": 344, + "ipv6": [ + { + "range": "2001:db8:acad:0::/52" + } + ], "label": "my-vpc", "region": "us-east", "subnets": [], diff --git a/docs/modules/vpc_subnet.md b/docs/modules/vpc_subnet.md index 87d668338..c6c5cb48e 100644 --- a/docs/modules/vpc_subnet.md +++ b/docs/modules/vpc_subnet.md @@ -15,7 +15,7 @@ Create, read, and update a Linode VPC Subnet. ## Examples ```yaml -- name: Create a VPC Subnet +- name: Create a VPC subnet linode.cloud.vpc_subnet: vpc_id: 12345 label: my-subnet @@ -23,6 +23,17 @@ Create, read, and update a Linode VPC Subnet. state: present ``` +```yaml +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a VPC subnet with an auto-allocated IPv6 range + linode.cloud.vpc_subnet: + vpc_id: 12345 + label: my-subnet + ipv6: + - range: auto + state: present +``` + ```yaml - name: Delete a VPC Subnet linode.cloud.vpc_subnet: @@ -40,6 +51,13 @@ Create, read, and update a Linode VPC Subnet. | `label` |
`str`
|
**Required**
| This VPC's unique label. | | `state` |
`str`
|
**Required**
| The state of this token. **(Choices: `present`, `absent`)** | | `ipv4` |
`str`
|
Optional
| The IPV4 range for this subnet in CIDR format. | +| [`ipv6` (sub-options)](#ipv6) |
`list`
|
Optional
| The IPv6 ranges of this subnet. NOTE: IPv6 VPCs may not currently be available to all users. | + +### ipv6 + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `range` |
`str`
|
Optional
| An existing IPv6 prefix owned by the current account or a forward slash (/) followed by a valid prefix length. If unspecified, a range with the default prefix will be allocated for this VPC. | ## Return Values @@ -51,6 +69,11 @@ Create, read, and update a Linode VPC Subnet. "created": "2023-08-31T18:53:04", "id": 271, "ipv4": "10.0.0.0/24", + "ipv6": [ + { + "range": "2001:db8:acad:300::/56" + } + ], "label": "test-subnet", "linodes": [ { @@ -58,6 +81,13 @@ Create, read, and update a Linode VPC Subnet. "interfaces": [{"active": false, "id": 654321}] } ], + "databases": [ + { + "id": 1234567, + "ipv4_range": "10.0.0.16/28", + "ipv6_range": "2001:db8:1234:1::/64" + } + ], "updated": "2023-08-31T18:53:04" } ``` diff --git a/docs/modules/vpc_subnet_info.md b/docs/modules/vpc_subnet_info.md index 9a3afbf2d..35d59ccfd 100644 --- a/docs/modules/vpc_subnet_info.md +++ b/docs/modules/vpc_subnet_info.md @@ -47,6 +47,11 @@ Get info about a Linode VPC Subnet. "created": "2023-08-31T18:53:04", "id": 271, "ipv4": "10.0.0.0/24", + "ipv6": [ + { + "range": "2001:db8:acad:300::/56" + } + ], "label": "test-subnet", "linodes": [ { @@ -54,6 +59,13 @@ Get info about a Linode VPC Subnet. "interfaces": [{"active": false, "id": 654321}] } ], + "databases": [ + { + "id": 1234567, + "ipv4_range": "10.0.0.16/28", + "ipv6_range": "2001:db8:1234:1::/64" + } + ], "updated": "2023-08-31T18:53:04" } ``` diff --git a/docs/modules/vpc_subnet_list.md b/docs/modules/vpc_subnet_list.md index c207e4e95..3ff278d4d 100644 --- a/docs/modules/vpc_subnet_list.md +++ b/docs/modules/vpc_subnet_list.md @@ -59,6 +59,11 @@ List and filter on VPC Subnets. "created": "2023-08-31T18:53:04", "id": 271, "ipv4": "10.0.0.0/24", + "ipv6": [ + { + "range": "2001:db8:acad:300::/56" + } + ], "label": "test-subnet", "linodes": [ { @@ -66,6 +71,13 @@ List and filter on VPC Subnets. "interfaces": [{"active": false, "id": 654321}] } ], + "databases": [ + { + "id": 1234567, + "ipv4_range": "10.0.0.16/28", + "ipv6_range": "2001:db8:1234:1::/64" + } + ], "updated": "2023-08-31T18:53:04" } ] diff --git a/docs/modules/vpcs_ipv6_list.md b/docs/modules/vpcs_ipv6_list.md new file mode 100644 index 000000000..90af54f23 --- /dev/null +++ b/docs/modules/vpcs_ipv6_list.md @@ -0,0 +1,116 @@ +# vpcs_ipv6_list + +List and filter on all VPC IPv6 addresses. + +NOTE: IPv6 VPCs may not currently be available to all users. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all IPv6 addresses for the current user. + linode.cloud.vpcs_ipv6_list: {} +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `order` |
`str`
|
Optional
| The order to list all VPC IPv6 Addresses in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order all VPC IPv6 Addresses by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting all VPC IPv6 Addresses. | +| `count` |
`int`
|
Optional
| The number of all VPC IPv6 Addresses to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-vpcs-ipv6s). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `addresses` - The returned all VPC IPv6 Addresses. + + - Sample Response: + ```json + [ + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 271170, + "subnet_mask": "", + "vpc_id": 262108 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-vpcs-ipv6s) for a list of returned fields + + diff --git a/plugins/inventory/instance.py b/plugins/inventory/instance.py index ba47a1b2a..404c40637 100644 --- a/plugins/inventory/instance.py +++ b/plugins/inventory/instance.py @@ -97,6 +97,7 @@ HAS_LINODE = False +# pylint: disable=too-many-ancestors class InventoryModule(BaseInventoryPlugin, Constructable): """Linode instance inventory plugin""" diff --git a/plugins/module_utils/doc_fragments/database_list.py b/plugins/module_utils/doc_fragments/database_list.py index 99b784e50..5a0f9a475 100644 --- a/plugins/module_utils/doc_fragments/database_list.py +++ b/plugins/module_utils/doc_fragments/database_list.py @@ -26,6 +26,11 @@ "id": 123, "instance_uri": "/v4/databases/mysql/instances/123", "label": "example-db", + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "us-east", "status": "active", "type": "g6-dedicated-2", diff --git a/plugins/module_utils/doc_fragments/database_mysql_v2.py b/plugins/module_utils/doc_fragments/database_mysql_v2.py index 2b4629773..328418d49 100644 --- a/plugins/module_utils/doc_fragments/database_mysql_v2.py +++ b/plugins/module_utils/doc_fragments/database_mysql_v2.py @@ -45,6 +45,17 @@ fork: source: 12345 state: present''', ''' +- name: Create a MySQL database attached to a VPC + linode.cloud.database_mysql_v2: + label: my-db + region: us-mia + engine: mysql/8 + type: g6-nanode-1 + private_network: + vpc_id: 123 + subnet_id: 456 + public_access: true + state: present''', ''' - name: Delete a MySQL database linode.cloud.database_mysql_v2: label: my-db @@ -78,6 +89,11 @@ "oldest_restore_time": "2025-02-10T20:15:07", "platform": "rdbms-default", "port": 11876, + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "ap-west", "ssl_connection": true, "status": "active", diff --git a/plugins/module_utils/doc_fragments/database_postgresql_v2.py b/plugins/module_utils/doc_fragments/database_postgresql_v2.py index c093e57f1..f87544767 100644 --- a/plugins/module_utils/doc_fragments/database_postgresql_v2.py +++ b/plugins/module_utils/doc_fragments/database_postgresql_v2.py @@ -45,6 +45,17 @@ fork: source: 12345 state: present''', ''' +- name: Create a PostgreSQL database attached to a VPC + linode.cloud.database_postgresql_v2: + label: my-db + region: us-mia + engine: postgresql/16 + type: g6-nanode-1 + private_network: + vpc_id: 123 + subnet_id: 456 + public_access: true + state: present''', ''' - name: Delete a PostgreSQL database linode.cloud.database_postgresql_v2: label: my-db @@ -78,6 +89,11 @@ "oldest_restore_time": "2025-02-10T20:15:07", "platform": "rdbms-default", "port": 11876, + "private_network": { + "public_access": true, + "subnet_id": 456, + "vpc_id": 123 + }, "region": "ap-west", "ssl_connection": true, "status": "active", diff --git a/plugins/module_utils/doc_fragments/instance.py b/plugins/module_utils/doc_fragments/instance.py index 1f17d4246..7e22cde35 100644 --- a/plugins/module_utils/doc_fragments/instance.py +++ b/plugins/module_utils/doc_fragments/instance.py @@ -78,7 +78,43 @@ placement_group: id: 123 compliant_only: false - state: present''', ''' + state: present''', +''' +- name: Create a Linode Instance with a VPC interface ''' ++ '''and a NAT 1-1 mapping to its public IPv4 address. + linode.cloud.instance: + label: my-vpc-instance + region: us-mia + type: g6-nanode-1 + image: linode/alpine3.21 + booted: true + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv4: + nat_1_1: any + state: present''', +''' +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a Linode Instance with a public VPC interface, ''' ++ '''assigning one IPv6 SLAAC prefix and one additional IPv6 range. + linode.cloud.instance: + label: my-vpc-ipv6-instance + region: us-mia + type: g6-nanode-1 + image: linode/alpine3.21 + booted: true + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv6: + is_public: true + slaac: + - range: auto + ranges: + - range: auto + state: present''', +''' - name: Delete a Linode instance. linode.cloud.instance: label: my-linode @@ -188,6 +224,28 @@ "ipam_address": "10.0.0.1/24", "label": "example-interface", "purpose": "vlan" + }, + { + "ip_ranges": null, + "ipam_address": null, + "ipv4": null, + "ipv6": { + "is_public": null, + "ranges": [ + { + "range": "auto" + } + ], + "slaac": [ + { + "range": "auto" + } + ] + }, + "label": null, + "primary": false, + "purpose": "vpc", + "subnet_id": 271176 } ], "kernel": "linode/latest-64bit", @@ -266,6 +324,25 @@ "subnet_mask": "255.255.255.0", "type": "ipv4" } + ], + "vpc": [ + { + "active": true, + "address": "10.0.0.2", + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": "10.0.0.1", + "interface_id": 12345, + "linode_id": 12345, + "nat_1_1": null, + "nodebalancer_id": null, + "prefix": 24, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "255.255.255.0", + "vpc_id": 12345 + } ] }, "ipv6": { @@ -296,6 +373,54 @@ "region": "us-east", "subnet_mask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "type": "ipv6" + }, + "vpc": { + "vpc": [ + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + }, + { + "active": true, + "address": null, + "address_range": null, + "config_id": 12345, + "database_id": null, + "gateway": null, + "interface_id": 12345, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:2::/64", + "linode_id": 12345, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-example-1", + "subnet_id": 12345, + "subnet_mask": "", + "vpc_id": 12345 + } + ] } } }'''] diff --git a/plugins/module_utils/doc_fragments/vpc.py b/plugins/module_utils/doc_fragments/vpc.py index 4fc34dba8..2d702d0ec 100644 --- a/plugins/module_utils/doc_fragments/vpc.py +++ b/plugins/module_utils/doc_fragments/vpc.py @@ -6,7 +6,17 @@ label: my-vpc region: us-east description: A description of this VPC. - state: present''', ''' + state: present''', +''' +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a VPC with an auto-allocated IPv6 range + linode.cloud.vpc: + label: my-vpc + region: us-east + ipv6: + - range: auto + state: present''', +''' - name: Delete a VPC linode.cloud.vpc: label: my-vpc @@ -16,6 +26,11 @@ "created": "2023-08-31T18:35:01", "description": "A description of this VPC", "id": 344, + "ipv6": [ + { + "range": "2001:db8:acad:0::/52" + } + ], "label": "my-vpc", "region": "us-east", "subnets": [], diff --git a/plugins/module_utils/doc_fragments/vpc_ipv6_list.py b/plugins/module_utils/doc_fragments/vpc_ipv6_list.py new file mode 100644 index 000000000..d935fa8c9 --- /dev/null +++ b/plugins/module_utils/doc_fragments/vpc_ipv6_list.py @@ -0,0 +1,76 @@ +"""Documentation fragments for the vpc_ipv6_list module""" + +specdoc_examples = [""" +- name: List all IPv6 addresses for a specific VPC. + linode.cloud.vpc_ipv6_list: + vpc_id: 12345""", +] + +result_addresses_samples = [ + """[ + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 271170, + "subnet_mask": "", + "vpc_id": 262108 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + } +]""" +] diff --git a/plugins/module_utils/doc_fragments/vpc_list.py b/plugins/module_utils/doc_fragments/vpc_list.py index 566f4babd..36b7a324c 100644 --- a/plugins/module_utils/doc_fragments/vpc_list.py +++ b/plugins/module_utils/doc_fragments/vpc_list.py @@ -14,6 +14,11 @@ "created": "2023-08-31T18:35:01", "description": "A description of this VPC", "id": 344, + "ipv6": [ + { + "range": "2001:db8:acad:0::/52" + } + ], "label": "my-vpc", "region": "us-east", "subnets": [], diff --git a/plugins/module_utils/doc_fragments/vpc_subnet.py b/plugins/module_utils/doc_fragments/vpc_subnet.py index 1073acbdf..6ae0caece 100644 --- a/plugins/module_utils/doc_fragments/vpc_subnet.py +++ b/plugins/module_utils/doc_fragments/vpc_subnet.py @@ -1,12 +1,21 @@ """Documentation fragments for the vpc module""" specdoc_examples = [''' -- name: Create a VPC Subnet +- name: Create a VPC subnet linode.cloud.vpc_subnet: vpc_id: 12345 label: my-subnet ipv4: '10.0.0.0/24' - state: present''', ''' + state: present''', +''' +# NOTE: IPv6 VPCs may not currently be available to all users. +- name: Create a VPC subnet with an auto-allocated IPv6 range + linode.cloud.vpc_subnet: + vpc_id: 12345 + label: my-subnet + ipv6: + - range: auto + state: present''',''' - name: Delete a VPC Subnet linode.cloud.vpc_subnet: vpc_id: 12345 @@ -17,6 +26,11 @@ "created": "2023-08-31T18:53:04", "id": 271, "ipv4": "10.0.0.0/24", + "ipv6": [ + { + "range": "2001:db8:acad:300::/56" + } + ], "label": "test-subnet", "linodes": [ { @@ -24,5 +38,12 @@ "interfaces": [{"active": false, "id": 654321}] } ], + "databases": [ + { + "id": 1234567, + "ipv4_range": "10.0.0.16/28", + "ipv6_range": "2001:db8:1234:1::/64" + } + ], "updated": "2023-08-31T18:53:04" }'''] diff --git a/plugins/module_utils/doc_fragments/vpc_subnet_list.py b/plugins/module_utils/doc_fragments/vpc_subnet_list.py index 50c554bce..b3008bb70 100644 --- a/plugins/module_utils/doc_fragments/vpc_subnet_list.py +++ b/plugins/module_utils/doc_fragments/vpc_subnet_list.py @@ -17,6 +17,11 @@ "created": "2023-08-31T18:53:04", "id": 271, "ipv4": "10.0.0.0/24", + "ipv6": [ + { + "range": "2001:db8:acad:300::/56" + } + ], "label": "test-subnet", "linodes": [ { @@ -24,6 +29,13 @@ "interfaces": [{"active": false, "id": 654321}] } ], + "databases": [ + { + "id": 1234567, + "ipv4_range": "10.0.0.16/28", + "ipv6_range": "2001:db8:1234:1::/64" + } + ], "updated": "2023-08-31T18:53:04" } ]'''] diff --git a/plugins/module_utils/doc_fragments/vpcs_ipv6_list.py b/plugins/module_utils/doc_fragments/vpcs_ipv6_list.py new file mode 100644 index 000000000..7bbb0a97f --- /dev/null +++ b/plugins/module_utils/doc_fragments/vpcs_ipv6_list.py @@ -0,0 +1,75 @@ +"""Documentation fragments for the vpcs_ipv6_list module""" + +specdoc_examples = [""" +- name: List all IPv6 addresses for the current user. + linode.cloud.vpcs_ipv6_list: {}""", +] + +result_addresses_samples = [ + """[ + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [ + { + "slaac_address": "2001:db8:acad:1:abcd:ef12:3456:7890" + } + ], + "ipv6_is_public": false, + "ipv6_range": "2001:db8:acad:1::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 271170, + "subnet_mask": "", + "vpc_id": 262108 + }, + { + "active": false, + "address": null, + "address_range": null, + "config_id": 123, + "database_id": null, + "gateway": null, + "interface_id": 123, + "ipv6_addresses": [], + "ipv6_is_public": false, + "ipv6_range": "2001:db8::/64", + "linode_id": 123, + "nat_1_1": "", + "nodebalancer_id": null, + "prefix": 64, + "region": "us-mia", + "subnet_id": 123, + "subnet_mask": "", + "vpc_id": 123 + } +]""" +] diff --git a/plugins/module_utils/linode_helper.py b/plugins/module_utils/linode_helper.py index 436f2d9d3..8ca4fdbcd 100644 --- a/plugins/module_utils/linode_helper.py +++ b/plugins/module_utils/linode_helper.py @@ -1,10 +1,21 @@ """This module contains helper functions for various Linode modules.""" -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, cast +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Set, + Tuple, + Union, + cast, +) import linode_api4 import polling from linode_api4 import ( + ApiError, JSONObject, LinodeClient, LKENodePool, @@ -18,6 +29,7 @@ FilterableAttribute, FilterableMetaclass, ) +from linode_api4.polling import TimeoutContext def dict_select_spec(target: dict, spec: dict) -> dict: @@ -116,10 +128,12 @@ def handle_updates( mutable_fields: set, register_func: Callable, ignore_keys: Set[str] = None, + nullable_keys: set[str] = None, ) -> Set[str]: """Handles updates for a linode_api4 object""" ignore_keys = ignore_keys or set() + nullable_keys = nullable_keys or set() obj._api_get() @@ -129,16 +143,25 @@ def handle_updates( # Update mutable values params = filter_null_values(params) + # Populate excluded nullable keys with None + for key in nullable_keys: + if key not in params: + params[key] = None + put_request = {} result = set() for key, new_value in params.items(): - if not hasattr(obj, key) or key in ignore_keys: + if ( + key not in property_metadata + or not hasattr(obj, key) + or key in ignore_keys + ): continue old_value = parse_linode_types(getattr(obj, key)) - if isinstance(new_value, dict): + if isinstance(new_value, dict) and isinstance(old_value, dict): # If this field is a dict, we only want to compare values that are # specified by the user old_value, new_value = dict_select_matching( @@ -381,3 +404,68 @@ def safe_find( return None except Exception as exception: raise Exception(f"failed to get resource: {exception}") from exception + + +def pop_and_compare_optional_attribute( + local_parent: Dict[str, Any], + remote_parent: Dict[str, Any], + key: str, + compare: Optional[Callable[[Any, Any], bool]] = None, +) -> bool: + """ + Returns whether the given key for the local and remote dicts are equivalent, + ignoring unspecified local values. + + NOTE: This helper pops the keys from their respective dicts. + """ + + if key not in local_parent: + return True + + local_value = local_parent.pop(key, None) + remote_value = remote_parent.pop(key, None) + + if compare is not None: + return compare(local_value, remote_value) + + if isinstance(local_value, dict) and isinstance(remote_value, dict): + return matching_keys_eq(local_value, remote_value) + + return local_value == remote_value + + +def matching_keys_eq( + a: Dict[str, Any], + b: Dict[str, Any], +): + """ + Compares values for matching keys in two dicts. + """ + + a, b = dict_select_matching(a, b) + return b == a + + +def retry_on_response_status( + timeout_ctx: TimeoutContext, func: Callable[[], None], *statuses: int +): + """ + Retries a given function if it raises an ApiError with specific response statuses. + """ + + def __attempt_delete() -> bool: + try: + func() + except ApiError as err: + if err.status in statuses: + return False + + raise err + + return True + + poll_condition( + __attempt_delete, + step=4, + timeout=timeout_ctx.seconds_remaining, + ) diff --git a/plugins/module_utils/linode_networking.py b/plugins/module_utils/linode_networking.py new file mode 100644 index 000000000..e450653d0 --- /dev/null +++ b/plugins/module_utils/linode_networking.py @@ -0,0 +1,37 @@ +""" +Contains various networking-related helper functions. +""" + +import ipaddress +from typing import Optional, Tuple + + +def auto_alloc_ranges_equivalent( + range1: str, + range2: str, +) -> bool: + """ + Returns whether the given auto-alloc ranges are semantically equivalent. + These ranges accept an explicit range, a prefix, or "auto". + """ + + def __parse_range_components(r: str) -> Tuple[Optional[str], int]: + segments = r.split("/") + if len(segments) != 2: + raise ValueError(f"{r} is not a valid IPv6 range") + + return segments[0] if len(segments[0]) > 0 else None, int(segments[1]) + + if "auto" in (range1, range2): + return True + + r1_address, r1_prefix = __parse_range_components(range1) + r2_address, r2_prefix = __parse_range_components(range2) + + if r1_address is None or r2_address is None: + return r1_prefix == r2_prefix + + return ( + ipaddress.ip_address(r1_address) == ipaddress.ip_address(r2_address) + and r1_prefix == r2_prefix + ) diff --git a/plugins/module_utils/linode_vpc_shared.py b/plugins/module_utils/linode_vpc_shared.py new file mode 100644 index 000000000..95509796e --- /dev/null +++ b/plugins/module_utils/linode_vpc_shared.py @@ -0,0 +1,50 @@ +""" +This file contains various helpers shared between VPC-related modules. +""" + +from itertools import chain + +from linode_api4 import VPCSubnet + + +def should_retry_subnet_delete_400s( + client: "LinodeClient", + subnet: VPCSubnet, +) -> bool: + """ + Returns whether the given subnet should be retried upon a 400 error. + + This is necessary because database deletions and detachments can + occasionally take longer than expected to propagate on VPC subnets. + """ + + account_dbs = { + db.id: db + for db in chain( + client.database.mysql_instances(), + client.database.postgresql_instances(), + ) + } + + if len(subnet.databases) < 1: + # There are no databases attached to this subnet, + # so there is nothing to retry + return False + + for subnet_db in subnet.databases: + if subnet_db.id not in account_dbs: + continue + + db = account_dbs[subnet_db.id] + db._api_get() + + if ( + db.private_network is None + or db.private_network.subnet_id != subnet.id + ): + continue + + # This database is not in the process of being detached + return False + + return True diff --git a/plugins/modules/database_mysql_v2.py b/plugins/modules/database_mysql_v2.py index 63e0c9c4c..1bc753a91 100644 --- a/plugins/modules/database_mysql_v2.py +++ b/plugins/modules/database_mysql_v2.py @@ -278,6 +278,51 @@ type=FieldType.string, description=["The label of the Managed Database."], ), + "detach_private_network": SpecField( + description=[ + "If true, the Managed Database will be detached from its current private network " + + "when `private_network` is null.", + "If the Managed Database is not currently attached to a private network or " + + "the private_network field is specified, this option has no effect.", + "This is not necessary when switching between VPC subnets.", + ], + type=FieldType.bool, + default=False, + ), + "private_network": SpecField( + description=[ + "Restricts access to this database using a virtual private cloud (VPC) " + + "that you've configured in the region where the database will live." + ], + editable=True, + type=FieldType.dict, + suboptions={ + "vpc_id": SpecField( + description=[ + "The ID of the virtual private cloud (VPC) " + + "to restrict access to this database using" + ], + type=FieldType.integer, + required=True, + ), + "subnet_id": SpecField( + description=[ + "The ID of the VPC subnet to restrict access " + + "to this database using." + ], + type=FieldType.integer, + required=True, + ), + "public_access": SpecField( + description=[ + "Set to `true` to allow clients outside of the VPC to " + + "connect to the database using a public IP address." + ], + type=FieldType.bool, + default=False, + ), + }, + ), "region": SpecField( type=FieldType.string, description=["The region of the Managed Database."], @@ -368,9 +413,7 @@ def __init__(self) -> None: "credentials": None, } - super().__init__( - module_arg_spec=self.module_arg_spec, - ) + super().__init__(module_arg_spec=self.module_arg_spec) def _create(self) -> MySQLDatabase: params = filter_null_values_recursive( @@ -385,10 +428,11 @@ def _create(self) -> MySQLDatabase: "engine_config", "fork", "label", + "private_network", "region", "type", ] - } + }, ) # This is necessary because `type` is a Python-reserved keyword @@ -457,6 +501,13 @@ def _update(self, database: MySQLDatabase) -> None: if "updates" in params and params["updates"] is not None: params["updates"]["pending"] = database.updates.pending + # We want to explicitly include keys that are nullable in the update request + # only if their corresponding "detach" parameter is True. + nullable_keys = set() + + if self.module.params.get("detach_private_network"): + nullable_keys.add("private_network") + # Apply updates updated_fields = handle_updates( database, @@ -466,11 +517,13 @@ def _update(self, database: MySQLDatabase) -> None: "allow_list", "cluster_size", "engine_config", + "private_network", "updates", "type", "version", }, self.register_action, + nullable_keys=nullable_keys, ) # NOTE: We don't poll for the database_update event here because it is not diff --git a/plugins/modules/database_postgresql_v2.py b/plugins/modules/database_postgresql_v2.py index c1fd69faa..ed0c35935 100644 --- a/plugins/modules/database_postgresql_v2.py +++ b/plugins/modules/database_postgresql_v2.py @@ -431,6 +431,51 @@ type=FieldType.string, description=["The label of the Managed Database."], ), + "detach_private_network": SpecField( + description=[ + "If true, the Managed Database will be detached from its current private network " + + "when `private_network` is null.", + "If the Managed Database is not currently attached to a private network or " + + "the private_network field is specified, this option has no effect.", + "This is not necessary when switching between VPC subnets.", + ], + type=FieldType.bool, + default=False, + ), + "private_network": SpecField( + description=[ + "Restricts access to this database using a virtual private cloud (VPC) " + + "that you've configured in the region where the database will live." + ], + editable=True, + type=FieldType.dict, + suboptions={ + "vpc_id": SpecField( + description=[ + "The ID of the virtual private cloud (VPC) " + + "to restrict access to this database using" + ], + type=FieldType.integer, + required=True, + ), + "subnet_id": SpecField( + description=[ + "The ID of the VPC subnet to restrict access " + + "to this database using." + ], + type=FieldType.integer, + required=True, + ), + "public_access": SpecField( + description=[ + "Set to `true` to allow clients outside of the VPC to " + + "connect to the database using a public IP address." + ], + type=FieldType.bool, + default=False, + ), + }, + ), "region": SpecField( type=FieldType.string, description=["The region of the Managed Database."], @@ -538,6 +583,7 @@ def _create(self) -> PostgreSQLDatabase: "engine_config", "fork", "label", + "private_network", "region", "type", ] @@ -605,6 +651,13 @@ def _update(self, database: PostgreSQLDatabase) -> None: if params.get("updates") is not None: params["updates"]["pending"] = database.updates.pending + # We want to explicitly include keys that are nullable in the update request + # only if their corresponding "detach" parameter is True. + nullable_keys = set() + + if self.module.params.get("detach_private_network"): + nullable_keys.add("private_network") + updated_fields = handle_updates( database, params, @@ -613,11 +666,13 @@ def _update(self, database: PostgreSQLDatabase) -> None: "allow_list", "cluster_size", "engine_config", + "private_network", "updates", "type", "version", }, self.register_action, + nullable_keys=nullable_keys, ) # NOTE: We don't poll for the database_update event here because it is not diff --git a/plugins/modules/instance.py b/plugins/modules/instance.py index 7997bef8c..7576bb92d 100644 --- a/plugins/modules/instance.py +++ b/plugins/modules/instance.py @@ -24,9 +24,14 @@ filter_null_values, filter_null_values_recursive, handle_updates, + matching_keys_eq, paginated_list_to_json, parse_linode_types, poll_condition, + pop_and_compare_optional_attribute, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_networking import ( + auto_alloc_ranges_equivalent, ) from ansible_specdoc.objects import ( FieldType, @@ -214,6 +219,56 @@ "ipv4": SpecField( type=FieldType.dict, description=["The IPv4 configuration for this interface. (VPC only)"], + suboptions=linode_instance_interface_ipv4_spec, + ), + "ipv6": SpecField( + type=FieldType.dict, + description=[ + "The IPv6 configuration for this interface. (VPC only)", + "NOTE: IPv6 VPCs may not currently be available to all users.", + ], + suboptions={ + "is_public": SpecField( + type=FieldType.bool, + description=[ + "If true, connections from the interface to IPv6 addresses outside the VPC, " + + "and connections from IPv6 addresses outside the VPC to the interface " + + "will be permitted." + ], + ), + "slaac": SpecField( + type=FieldType.list, + element_type=FieldType.dict, + description=[ + "An array of SLAAC prefixes to use for this interface." + ], + suboptions={ + "range": SpecField( + type=FieldType.string, + description=[ + "A SLAAC prefix to add to this interface, " + "or `auto` for a new IPv6 prefix to be automatically allocated." + ], + ) + }, + ), + "ranges": SpecField( + type=FieldType.list, + element_type=FieldType.dict, + description=[ + "An array of SLAAC prefixes to use for this interface." + ], + suboptions={ + "range": SpecField( + type=FieldType.string, + description=[ + "A prefix to add to this interface, " + "or `auto` for a new IPv6 prefix to be automatically allocated." + ], + ) + }, + ), + }, ), "label": SpecField( type=FieldType.string, @@ -738,37 +793,87 @@ def _compare_param_to_device( ) @staticmethod - def _normalize_local_interface( + def _interfaces_equivalent( local_interface: Dict[str, Any], remote_interface: Dict[str, Any] - ) -> Dict[str, Any]: + ) -> bool: """ - Normalizes the given param interface to the remote interface - for direct comparison. + Returns whether the given user-defined and remote interfaces are equivalent. """ - result = copy.deepcopy(local_interface) - # The IPv4 field will be implicitly populated if is not defined - if "ipv4" not in local_interface and "ipv4" in remote_interface: - result["ipv4"] = remote_interface.get("ipv4") + def __compare_ipv4( + local: Dict[str, Any], remote: Dict[str, Any] + ) -> bool: + local, remote = copy.deepcopy(local), copy.deepcopy(remote) - # Primary is only allowed for public and VPC purposes, so we - # should implicitly populate a default - if ( - local_interface.get("purpose") in ("public", "vpc") - and "primary" not in local_interface + local_nat = local.pop("nat_1_1", None) + remote_nat = remote.pop("nat_1_1", None) + if local_nat is not None: + if remote_nat is None: + return False + + if local_nat not in ("any", remote_nat): + return False + + return matching_keys_eq(local, remote) + + def __compare_ipv6_range( + local: Dict[str, Any], remote: Dict[str, Any] + ) -> bool: + local, remote = copy.deepcopy(local), copy.deepcopy(remote) + + # Diff `range` field with respect for semantic equality + if not pop_and_compare_optional_attribute( + local, remote, "range", auto_alloc_ranges_equivalent + ): + return False + + return matching_keys_eq(local, remote) + + def __compare_ipv6( + local: Dict[str, Any], remote: Dict[str, Any] + ) -> bool: + local, remote = copy.deepcopy(local), copy.deepcopy(remote) + + # Diff ranges + local_ranges, remote_ranges = local.pop("ranges", []), remote.pop( + "ranges", [] + ) + if len(local_ranges) != len(remote_ranges): + return False + + for local_range, remote_range in zip(local_ranges, remote_ranges): + if not __compare_ipv6_range(local_range, remote_range): + return False + + # Diff SLAAC + local_slaac, remote_slaac = local.pop("slaac", []), remote.pop( + "slaac", [] + ) + if len(local_slaac) != len(remote_slaac): + return False + + for local_slaac, remote_slaac in zip(local_slaac, remote_slaac): + if not __compare_ipv6_range(local_slaac, remote_slaac): + return False + + # Compare other matching fields + return matching_keys_eq(local, remote) + + # Root-level diff + local_interface = copy.deepcopy(local_interface) + remote_interface = copy.deepcopy(remote_interface) + + if not pop_and_compare_optional_attribute( + local_interface, remote_interface, "ipv4", __compare_ipv4 ): - result["primary"] = False + return False - # The primary field will not be returned for VLAN interfaces, - # so we should drop it from the user-configured interface. - if local_interface.get("purpose") == "vlan" and "primary" in result: - primary = result.pop("primary") + if not pop_and_compare_optional_attribute( + local_interface, remote_interface, "ipv6", __compare_ipv6 + ): + return False - # Extra validation step to make sure users aren't trying to - # set a VLAN as a primary interface. - if primary: - raise ValueError("VLAN interfaces cannot be primary interfaces") - return result + return matching_keys_eq(local_interface, remote_interface) @staticmethod def _compare_interfaces( @@ -784,11 +889,8 @@ def _compare_interfaces( for i, local_interface in enumerate(local_interfaces): remote_interface = remote_interfaces[i] - if ( - LinodeInstance._normalize_local_interface( - local_interface, remote_interface - ) - != remote_interfaces[i] + if not LinodeInstance._interfaces_equivalent( + local_interface, remote_interface ): return False diff --git a/plugins/modules/lke_cluster.py b/plugins/modules/lke_cluster.py index 518f62c5b..534123af5 100644 --- a/plugins/modules/lke_cluster.py +++ b/plugins/modules/lke_cluster.py @@ -134,6 +134,12 @@ } linode_lke_cluster_node_pool_spec = { + "label": SpecField( + type=FieldType.string, + editable=True, + description=["A unique label for this Node Pool."], + required=False, + ), "count": SpecField( type=FieldType.integer, editable=True, @@ -567,6 +573,16 @@ def _update_cluster(self, cluster: LKECluster) -> None: current_pool.labels = pool.get("labels") current_pool.save() + if "label" in pool and current_pool.label != pool["label"]: + self.register_action( + "Updated label for Node Pool {}".format( + current_pool.id + ) + ) + + current_pool.label = pool.get("label") + current_pool.save() + pools_handled[k] = True should_keep[i] = True break @@ -635,6 +651,16 @@ def _update_cluster(self, cluster: LKECluster) -> None: existing_pool.labels = pool["labels"] should_update = True + if "label" in pool and existing_pool.label != pool["label"]: + self.register_action( + "Updated label for Node Pool {}".format( + existing_pool.id + ) + ) + + existing_pool.label = pool["label"] + should_update = True + if should_update: existing_pool.save() diff --git a/plugins/modules/lke_node_pool.py b/plugins/modules/lke_node_pool.py index 910acd947..4a063465b 100644 --- a/plugins/modules/lke_node_pool.py +++ b/plugins/modules/lke_node_pool.py @@ -111,6 +111,12 @@ editable=True, description=["The number of nodes in the Node Pool."], ), + "label": SpecField( + type=FieldType.string, + editable=True, + description=["A unique label for this Node Pool."], + required=False, + ), "disks": SpecField( type=FieldType.list, element_type=FieldType.dict, @@ -319,6 +325,7 @@ def _update_pool(self, pool: LKENodePool) -> LKENodePool: new_count = params.pop("count") new_taints = params.pop("taints") if "taints" in params else None new_labels = params.pop("labels") if "labels" in params else None + new_label = params.pop("label") if "label" in params else None new_k8s_version = ( params.pop("k8s_version") if "k8s_version" in params else None ) @@ -362,6 +369,11 @@ def _update_pool(self, pool: LKENodePool) -> LKENodePool: pool.labels = new_labels should_update = True + if new_label is not None and pool.label != new_label: + self.register_action("Updated label for Node Pool") + pool.label = new_label + should_update = True + if new_k8s_version is not None and pool.k8s_version != new_k8s_version: self.register_action("Updated k8s version for Node Pool") pool.k8s_version = new_k8s_version diff --git a/plugins/modules/vpc.py b/plugins/modules/vpc.py index bbf867917..cede3f882 100644 --- a/plugins/modules/vpc.py +++ b/plugins/modules/vpc.py @@ -18,8 +18,15 @@ from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( filter_null_values, handle_updates, + retry_on_response_status, safe_find, ) +from ansible_collections.linode.cloud.plugins.module_utils.linode_networking import ( + auto_alloc_ranges_equivalent, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_vpc_shared import ( + should_retry_subnet_delete_400s, +) from ansible_specdoc.objects import ( FieldType, SpecDocMeta, @@ -49,6 +56,25 @@ type=FieldType.string, description=["The region this VPC is located in."], ), + "ipv6": SpecField( + type=FieldType.list, + element_type=FieldType.dict, + description=[ + "A list of IPv6 ranges in CIDR notation.", + "NOTE: IPv6 VPCs may not currently be available to all users.", + ], + suboptions={ + "range": SpecField( + type=FieldType.string, + description="The IPv6 range assigned to this VPC.", + ), + "allocation_class": SpecField( + type=FieldType.string, + description="The labeled IPv6 Inventory that the VPC Prefix " + + "should be allocated from.", + ), + }, + ), } SPECDOC_META = SpecDocMeta( @@ -69,7 +95,7 @@ }, ) -CREATE_FIELDS = {"label", "region", "description"} +CREATE_FIELDS = {"label", "region", "description", "ipv6"} MUTABLE_FIELDS = {"description"} DOCUMENTATION = r""" @@ -92,6 +118,27 @@ def __init__(self) -> None: required_if=[("state", "present", ["region"])], ) + def __ipv6_updated(self, vpc: VPC) -> bool: + ipv6_arg = self.module.params.get("ipv6") + ipv6_actual = vpc.ipv6 + + if len(ipv6_arg) != len(ipv6_actual): + return True + + for i, entry_arg in enumerate(ipv6_arg): + range_arg = entry_arg.get("range") + + if range_arg is None: + # The value isn't specified, so we shouldn't diff + continue + + if not auto_alloc_ranges_equivalent( + range_arg, ipv6_actual[i].range + ): + return True + + return False + def _create(self) -> Optional[VPC]: params = filter_null_values( {k: v for k, v in self.module.params.items() if k in CREATE_FIELDS} @@ -104,9 +151,16 @@ def _create(self) -> Optional[VPC]: def _update(self, vpc: VPC) -> None: handle_updates( - vpc, self.module.params, MUTABLE_FIELDS, self.register_action + vpc, + self.module.params, + MUTABLE_FIELDS, + self.register_action, + ignore_keys={"ipv6"}, ) + if vpc.ipv6 is not None and self.__ipv6_updated(vpc): + self.fail(msg="IPv6 cannot be updated after VPC creation.") + def _handle_present(self) -> None: params = self.module.params @@ -130,7 +184,19 @@ def _handle_absent(self) -> None: if vpc is not None: self.results["vpc"] = vpc._raw_json - vpc.delete() + + # If any entities attached to this VPC's subnets are + # in a transient state expected to eventually allow deletions, + # retry the delete until it succeeds. + if all( + should_retry_subnet_delete_400s(self.client, subnet) + or len(subnet.databases) == 0 + for subnet in vpc.subnets + ): + retry_on_response_status(self._timeout_ctx, vpc.delete, 400) + else: + vpc.delete() + self.register_action(f"Deleted VPC {label}") def exec_module(self, **kwargs: Any) -> Optional[dict]: diff --git a/plugins/modules/vpc_ipv6_list.py b/plugins/modules/vpc_ipv6_list.py new file mode 100644 index 000000000..c9f2ec7a7 --- /dev/null +++ b/plugins/modules/vpc_ipv6_list.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the implementation of the vpc_ipv6_list module.""" + +from __future__ import absolute_import, division, print_function + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.vpc_ipv6_list as docs +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, + ListModuleParam, +) +from ansible_specdoc.objects import FieldType + +module = ListModule( + result_display_name="VPC IPv6 Addresses", + result_field_name="addresses", + endpoint_template="/vpcs/{vpc_id}/ipv6s", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-vpc-ipv6s", + examples=docs.specdoc_examples, + result_samples=docs.result_addresses_samples, + params=[ + ListModuleParam( + display_name="VPC", + name="vpc_id", + type=FieldType.integer, + ) + ], + description=[ + "List and filter on all VPC IPv6 addresses for a given VPC.", + "NOTE: IPv6 VPCs may not currently be available to all users.", + ], +) + + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/plugins/modules/vpc_subnet.py b/plugins/modules/vpc_subnet.py index 497082ced..8b3b9200b 100644 --- a/plugins/modules/vpc_subnet.py +++ b/plugins/modules/vpc_subnet.py @@ -18,8 +18,15 @@ from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( filter_null_values, handle_updates, + retry_on_response_status, safe_find, ) +from ansible_collections.linode.cloud.plugins.module_utils.linode_networking import ( + auto_alloc_ranges_equivalent, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_vpc_shared import ( + should_retry_subnet_delete_400s, +) from ansible_specdoc.objects import ( FieldType, SpecDocMeta, @@ -49,6 +56,22 @@ type=FieldType.string, description=["The IPV4 range for this subnet in CIDR format."], ), + "ipv6": SpecField( + type=FieldType.list, + element_type=FieldType.dict, + description=[ + "The IPv6 ranges of this subnet.", + "NOTE: IPv6 VPCs may not currently be available to all users.", + ], + suboptions={ + "range": SpecField( + type=FieldType.string, + description="An existing IPv6 prefix owned by the current account " + + "or a forward slash (/) followed by a valid prefix length. " + + "If unspecified, a range with the default prefix will be allocated for this VPC.", + ), + }, + ), } @@ -70,7 +93,7 @@ }, ) -CREATE_FIELDS = {"label", "ipv4"} +CREATE_FIELDS = {"label", "ipv4", "ipv6"} DOCUMENTATION = r""" """ @@ -91,6 +114,27 @@ def __init__(self) -> None: module_arg_spec=self.module_arg_spec, ) + def __ipv6_updated(self, subnet: VPCSubnet) -> bool: + ipv6_arg = self.module.params.get("ipv6") + ipv6_actual = subnet.ipv6 + + if len(ipv6_arg) != len(ipv6_actual): + return True + + for i, entry_arg in enumerate(ipv6_arg): + range_arg = entry_arg.get("range") + + if range_arg is None: + # The value isn't specified, so we shouldn't diff + continue + + if not auto_alloc_ranges_equivalent( + range_arg, ipv6_actual[i].range + ): + return True + + return False + def _create(self, vpc: VPC) -> Optional[VPCSubnet]: params = filter_null_values( {k: v for k, v in self.module.params.items() if k in CREATE_FIELDS} @@ -103,7 +147,16 @@ def _create(self, vpc: VPC) -> Optional[VPCSubnet]: def _update(self, subnet: VPCSubnet) -> None: # VPC Subnets cannot be updated - handle_updates(subnet, self.module.params, set(), self.register_action) + handle_updates( + subnet, + self.module.params, + set(), + self.register_action, + ignore_keys={"ipv6"}, + ) + + if subnet.ipv6 is not None and self.__ipv6_updated(subnet): + self.fail(msg="IPv6 cannot be updated after VPC subnet creation.") def _handle_present(self) -> None: params = self.module.params @@ -135,7 +188,15 @@ def _handle_absent(self) -> None: ) if subnet is not None: self.results["subnet"] = subnet._raw_json - subnet.delete() + + # If any entities attached to this subnet are in a transient state + # expected to eventually allow deletions, + # retry the delete until it succeeds. + if should_retry_subnet_delete_400s(self.client, subnet): + retry_on_response_status(self._timeout_ctx, subnet.delete, 400) + else: + subnet.delete() + self.register_action(f"Deleted VPC Subnet {label}") def exec_module(self, **kwargs: Any) -> Optional[dict]: diff --git a/plugins/modules/vpcs_ipv6_list.py b/plugins/modules/vpcs_ipv6_list.py new file mode 100644 index 000000000..c7db8df71 --- /dev/null +++ b/plugins/modules/vpcs_ipv6_list.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the implementation of the vpcs_ipv6_list module.""" + +from __future__ import absolute_import, division, print_function + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.vpcs_ipv6_list as docs +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, +) + +module = ListModule( + result_display_name="all VPC IPv6 Addresses", + result_field_name="addresses", + endpoint_template="/vpcs/ipv6s", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-vpcs-ipv6s", + examples=docs.specdoc_examples, + result_samples=docs.result_addresses_samples, + description=[ + "List and filter on all VPC IPv6 addresses.", + "NOTE: IPv6 VPCs may not currently be available to all users.", + ], +) + + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/requirements.txt b/requirements.txt index 15f37cedb..6a2f3ecc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -linode-api4>=5.34.0 +linode-api4>=5.37.0 polling==0.3.2 ansible-specdoc>=0.0.19 diff --git a/tests/integration/targets/api_request_extra/tasks/main.yaml b/tests/integration/targets/api_request_extra/tasks/main.yaml index af260e0de..b36c0197f 100644 --- a/tests/integration/targets/api_request_extra/tasks/main.yaml +++ b/tests/integration/targets/api_request_extra/tasks/main.yaml @@ -8,6 +8,9 @@ - name: GET region_list request linode.cloud.region_list: register: regions + + - set_fact: + valid_region: '{{ ( regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Block Storage") | list)[0] }}' - name: POST volume request linode.cloud.api_request: @@ -17,7 +20,7 @@ { "label": "ansible-test-{{ r }}", "size": 10, - "region": "{{ regions.regions[0].id }}" + "region": "{{ valid_region.id }}" } register: response diff --git a/tests/integration/targets/database_mysql_v2_basic/tasks/main.yaml b/tests/integration/targets/database_mysql_v2_basic/tasks/main.yaml index bf56a4053..d01fff8d8 100644 --- a/tests/integration/targets/database_mysql_v2_basic/tasks/main.yaml +++ b/tests/integration/targets/database_mysql_v2_basic/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available MySQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_mysql_v2_complex/tasks/main.yaml b/tests/integration/targets/database_mysql_v2_complex/tasks/main.yaml index b8cd769fb..ea92f280b 100644 --- a/tests/integration/targets/database_mysql_v2_complex/tasks/main.yaml +++ b/tests/integration/targets/database_mysql_v2_complex/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available MySQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_mysql_v2_engine_config/tasks/main.yaml b/tests/integration/targets/database_mysql_v2_engine_config/tasks/main.yaml index ea640de04..c97b2ba37 100644 --- a/tests/integration/targets/database_mysql_v2_engine_config/tasks/main.yaml +++ b/tests/integration/targets/database_mysql_v2_engine_config/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available MySQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_mysql_v2_vpc/tasks/main.yaml b/tests/integration/targets/database_mysql_v2_vpc/tasks/main.yaml new file mode 100644 index 000000000..65e580d0c --- /dev/null +++ b/tests/integration/targets/database_mysql_v2_vpc/tasks/main.yaml @@ -0,0 +1,178 @@ +- name: database_mysql_v2_vpc + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: List regions + linode.cloud.region_list: + register: all_regions + + - set_fact: + target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | selectattr("capabilities", "search", "VPCs") | list)[0]["id"] }}' + + - name: Get an available MySQL engine + linode.cloud.database_engine_list: + filters: + - name: engine + values: mysql + register: available_engines + + - name: Assert available database_engine_list + assert: + that: + - available_engines.database_engines | length >= 1 + + - set_fact: + engine_id: "{{ available_engines.database_engines[0]['id'] }}" + engine_version: "{{ available_engines.database_engines[0]['version'] }}" + + + - name: Create a VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet-1" + ipv4: "10.0.0.0/24" + state: present + register: create_subnet_1 + + - name: Create a second subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet-2" + ipv4: "10.0.1.0/24" + state: present + register: create_subnet_2 + + - name: Create a database + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_1.subnet.id }}' + public_access: false + state: present + register: db_create + + - name: Assert database is created + assert: + that: + - db_create.changed + - db_create.database.status == "active" + - db_create.database.private_network.vpc_id == create_vpc.vpc.id + - db_create.database.private_network.subnet_id == create_subnet_1.subnet.id + - db_create.database.private_network.public_access == False + + - name: Update the database private network configuration + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_2.subnet.id }}' + public_access: true + state: present + register: db_update + + - name: Assert database is created + assert: + that: + - db_update.changed + - db_update.database.status == "active" + - db_update.database.private_network.vpc_id == create_vpc.vpc.id + - db_update.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_update.database.private_network.public_access == True + + - name: Don't change the private network configuration + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_2.subnet.id }}' + public_access: true + state: present + register: db_unchanged + + - name: Assert the database is unchanged + assert: + that: + - db_unchanged.changed == False + - db_unchanged.database.status == "active" + - db_unchanged.database.private_network.vpc_id == create_vpc.vpc.id + - db_unchanged.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_unchanged.database.private_network.public_access == True + + - name: Get information about the database by label + linode.cloud.database_mysql_info: + label: "ansible-test-{{ r }}" + register: db_info_label + + - name: Assert the database is found by label + assert: + that: + - db_info_label.database.status == "active" + - db_info_label.database.private_network.vpc_id == create_vpc.vpc.id + - db_info_label.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_info_label.database.private_network.public_access == True + + - name: Get information about the database by ID + linode.cloud.database_mysql_info: + id: "{{ db_create.database.id }}" + register: db_info_id + + - name: Assert the database is found by ID + assert: + that: + - db_info_id.database.status == "active" + - db_info_id.database.private_network.vpc_id == create_vpc.vpc.id + - db_info_id.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_info_id.database.private_network.public_access == True + + - name: Get information about the VPC subnet by ID + linode.cloud.vpc_subnet_info: + vpc_id: "{{ create_vpc.vpc.id }}" + id: '{{ create_subnet_2.subnet.id }}' + register: subnet_info + + - name: Assert the database is found by ID + assert: + that: + - subnet_info.subnet.databases[0].id == db_create.database.id + - subnet_info.subnet.databases[0].ipv4_range != None + - '"ipv6_ranges" in subnet_info.subnet.databases[0]' + always: + - ignore_errors: true + block: + - name: Delete database + linode.cloud.database_mysql_v2: + label: "{{ db_create.database.label }}" + state: absent + + - name: Delete VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + state: absent + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_API_TOKEN: "{{ api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" diff --git a/tests/integration/targets/database_mysql_v2_vpc_detach/tasks/main.yaml b/tests/integration/targets/database_mysql_v2_vpc_detach/tasks/main.yaml new file mode 100644 index 000000000..66e69b88d --- /dev/null +++ b/tests/integration/targets/database_mysql_v2_vpc_detach/tasks/main.yaml @@ -0,0 +1,125 @@ +- name: database_mysql_v2_vpc_detach + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: List regions + linode.cloud.region_list: + register: all_regions + + - set_fact: + target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | selectattr("capabilities", "search", "VPCs") | list)[0]["id"] }}' + + - name: Get an available MySQL engine + linode.cloud.database_engine_list: + filters: + - name: engine + values: mysql + register: available_engines + + - name: Assert available database_engine_list + assert: + that: + - available_engines.database_engines | length >= 1 + + - set_fact: + engine_id: "{{ available_engines.database_engines[0]['id'] }}" + engine_version: "{{ available_engines.database_engines[0]['version'] }}" + + + - name: Create a VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet" + ipv4: "10.0.0.0/24" + state: present + register: create_subnet + + - name: Create a database + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet.subnet.id }}' + public_access: false + state: present + register: db_create + + - name: Assert database is created + assert: + that: + - db_create.changed + - db_create.database.status == "active" + - db_create.database.private_network.vpc_id == create_vpc.vpc.id + - db_create.database.private_network.subnet_id == create_subnet.subnet.id + - db_create.database.private_network.public_access == False + + - name: Remove the private network configuration + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + detach_private_network: true + state: present + register: db_remove_private_network + + - name: Assert the database is unchanged + assert: + that: + - db_remove_private_network.changed + - db_remove_private_network.database.status == "active" + - db_remove_private_network.database.private_network == None + + - name: Don't make any changes + linode.cloud.database_mysql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + detach_private_network: true + state: present + register: db_remove_private_network_unchanged + + - name: Assert the database is unchanged + assert: + that: + - db_remove_private_network_unchanged.changed == False + - db_remove_private_network_unchanged.database.status == "active" + - db_remove_private_network_unchanged.database.private_network == None + + always: + - ignore_errors: true + block: + - name: Delete database + linode.cloud.database_mysql_v2: + label: "{{ db_create.database.label }}" + state: absent + + - name: Delete VPC subnet + linode.cloud.vpc_subnet: + label: "test-subnet" + vpc_id: "{{ create_vpc.vpc.id }}" + state: absent + + - name: Delete VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + state: absent + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_API_TOKEN: "{{ api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" diff --git a/tests/integration/targets/database_postgresql_v2_basic/tasks/main.yaml b/tests/integration/targets/database_postgresql_v2_basic/tasks/main.yaml index e0c5afc5d..b1f8ec095 100644 --- a/tests/integration/targets/database_postgresql_v2_basic/tasks/main.yaml +++ b/tests/integration/targets/database_postgresql_v2_basic/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available PostgreSQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_postgresql_v2_complex/tasks/main.yaml b/tests/integration/targets/database_postgresql_v2_complex/tasks/main.yaml index e3a0f8d3a..c9a72cb07 100644 --- a/tests/integration/targets/database_postgresql_v2_complex/tasks/main.yaml +++ b/tests/integration/targets/database_postgresql_v2_complex/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available PostgreSQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_postgresql_v2_engine_config/tasks/main.yaml b/tests/integration/targets/database_postgresql_v2_engine_config/tasks/main.yaml index 5a86d1b66..36fa1682e 100644 --- a/tests/integration/targets/database_postgresql_v2_engine_config/tasks/main.yaml +++ b/tests/integration/targets/database_postgresql_v2_engine_config/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' + target_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Databases") | list)[0]["id"] }}' - name: Get an available PostgreSQL engine linode.cloud.database_engine_list: diff --git a/tests/integration/targets/database_postgresql_v2_vpc/tasks/main.yaml b/tests/integration/targets/database_postgresql_v2_vpc/tasks/main.yaml new file mode 100644 index 000000000..fdc7e7471 --- /dev/null +++ b/tests/integration/targets/database_postgresql_v2_vpc/tasks/main.yaml @@ -0,0 +1,176 @@ +- name: database_postgresql_v2_vpc + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: List regions + linode.cloud.region_list: + register: all_regions + + - set_fact: + target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | selectattr("capabilities", "search", "VPCs") | list)[0]["id"] }}' + + - name: Get an available PostgreSQL engine + linode.cloud.database_engine_list: + filters: + - name: engine + values: postgresql + register: available_engines + + - name: Assert available database_engine_list + assert: + that: + - available_engines.database_engines | length >= 1 + + - set_fact: + engine_id: "{{ available_engines.database_engines[0]['id'] }}" + engine_version: "{{ available_engines.database_engines[0]['version'] }}" + + - name: Create a VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet-1" + ipv4: "10.0.0.0/24" + state: present + register: create_subnet_1 + + - name: Create a second subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet-2" + ipv4: "10.0.1.0/24" + state: present + register: create_subnet_2 + + - name: Create a database + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_1.subnet.id }}' + public_access: false + state: present + register: db_create + + - name: Assert database is created + assert: + that: + - db_create.changed + - db_create.database.status == "active" + - db_create.database.private_network.vpc_id == create_vpc.vpc.id + - db_create.database.private_network.subnet_id == create_subnet_1.subnet.id + - db_create.database.private_network.public_access == False + + - name: Update the database private network configuration + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_2.subnet.id }}' + public_access: true + state: present + register: db_update + + - name: Assert database is created + assert: + that: + - db_update.changed + - db_update.database.status == "active" + - db_update.database.private_network.vpc_id == create_vpc.vpc.id + - db_update.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_update.database.private_network.public_access == True + + - name: Don't change the private network configuration + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet_2.subnet.id }}' + public_access: true + state: present + register: db_unchanged + + - name: Assert the database is unchanged + assert: + that: + - db_unchanged.changed == False + - db_unchanged.database.status == "active" + - db_unchanged.database.private_network.vpc_id == create_vpc.vpc.id + - db_unchanged.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_unchanged.database.private_network.public_access == True + + - name: Get information about the database by label + linode.cloud.database_postgresql_info: + label: "ansible-test-{{ r }}" + register: db_info_label + + - name: Assert the database is found by label + assert: + that: + - db_info_label.database.status == "active" + - db_info_label.database.private_network.vpc_id == create_vpc.vpc.id + - db_info_label.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_info_label.database.private_network.public_access == True + + - name: Get information about the database by ID + linode.cloud.database_postgresql_info: + id: "{{ db_create.database.id }}" + register: db_info_id + + - name: Assert the database is found by ID + assert: + that: + - db_info_id.database.status == "active" + - db_info_id.database.private_network.vpc_id == create_vpc.vpc.id + - db_info_id.database.private_network.subnet_id == create_subnet_2.subnet.id + - db_info_id.database.private_network.public_access == True + + - name: Get information about the VPC subnet by ID + linode.cloud.vpc_subnet_info: + vpc_id: "{{ create_vpc.vpc.id }}" + id: '{{ create_subnet_2.subnet.id }}' + register: subnet_info + + - name: Assert the database is found by ID + assert: + that: + - subnet_info.subnet.databases[0].id == db_create.database.id + - subnet_info.subnet.databases[0].ipv4_range != None + - '"ipv6_ranges" in subnet_info.subnet.databases[0]' + always: + - ignore_errors: true + block: + - name: Delete database + linode.cloud.database_postgresql_v2: + label: "{{ db_create.database.label }}" + state: absent + + - name: Delete VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + state: absent + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_API_TOKEN: "{{ api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file diff --git a/tests/integration/targets/database_postgresql_v2_vpc_detach/tasks/main.yaml b/tests/integration/targets/database_postgresql_v2_vpc_detach/tasks/main.yaml new file mode 100644 index 000000000..11f0ab827 --- /dev/null +++ b/tests/integration/targets/database_postgresql_v2_vpc_detach/tasks/main.yaml @@ -0,0 +1,127 @@ +- name: database_postgresql_v2_vpc_detach + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: List regions + linode.cloud.region_list: + register: all_regions + + - set_fact: + target_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Databases") | selectattr("capabilities", "search", "VPCs") | list)[0]["id"] }}' + + - name: Get an available PostgreSQL engine + linode.cloud.database_engine_list: + filters: + - name: engine + values: postgresql + register: available_engines + + - name: Assert available database_engine_list + assert: + that: + - available_engines.database_engines | length >= 1 + + - set_fact: + engine_id: "{{ available_engines.database_engines[0]['id'] }}" + engine_version: "{{ available_engines.database_engines[0]['version'] }}" + + - name: Create a VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: "{{ create_vpc.vpc.id }}" + label: "test-subnet" + ipv4: "10.0.0.0/24" + state: present + register: create_subnet + + - name: Create a database + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + private_network: + vpc_id: "{{ create_vpc.vpc.id }}" + subnet_id: '{{ create_subnet.subnet.id }}' + public_access: false + state: present + register: db_create + + - name: Assert database is created + assert: + that: + - db_create.changed + - db_create.database.status == "active" + - db_create.database.private_network.vpc_id == create_vpc.vpc.id + - db_create.database.private_network.subnet_id == create_subnet.subnet.id + - db_create.database.private_network.public_access == False + + - name: Remove the private network configuration + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + detach_private_network: true + state: present + register: db_remove_private_network + + - name: Assert the database is unchanged + assert: + that: + - db_remove_private_network.changed + - db_remove_private_network.database.status == "active" + - db_remove_private_network.database.private_network == None + + - name: Don't make any changes + linode.cloud.database_postgresql_v2: + label: "ansible-test-{{ r }}" + region: "{{ target_region }}" + engine: "{{ engine_id }}" + type: g6-nanode-1 + allow_list: [] + detach_private_network: true + state: present + register: db_remove_private_network_unchanged + + - name: Assert the database is unchanged + assert: + that: + - db_remove_private_network_unchanged.changed == False + - db_remove_private_network_unchanged.database.status == "active" + - db_remove_private_network_unchanged.database.private_network == None + + always: + - ignore_errors: true + block: + - name: Delete database + linode.cloud.database_postgresql_v2: + label: "{{ db_create.database.label }}" + state: absent + + - name: Delete VPC subnet + linode.cloud.vpc_subnet: + label: "test-subnet" + vpc_id: "{{ create_vpc.vpc.id }}" + state: absent + + - name: Delete VPC + linode.cloud.vpc: + label: "ansible-test-{{ r }}" + state: absent + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_API_TOKEN: "{{ api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file diff --git a/tests/integration/targets/image_basic/tasks/main.yaml b/tests/integration/targets/image_basic/tasks/main.yaml index 349fd2375..69e6ea570 100644 --- a/tests/integration/targets/image_basic/tasks/main.yaml +++ b/tests/integration/targets/image_basic/tasks/main.yaml @@ -2,14 +2,14 @@ block: - set_fact: r: "{{ 1000000000 | random }}" - disallowed_image_regions: ["gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3"] + disallowed_image_regions: ["gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3", "no-osl-1"] - name: List regions linode.cloud.region_list: {} register: all_regions - set_fact: - capable_regions: '{{ (all_regions.regions | selectattr("capabilities", "search", "Object Storage") | selectattr("site_type", "equalto", "core") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' + capable_regions: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Object Storage") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' - name: Create an instance to image linode.cloud.instance: diff --git a/tests/integration/targets/instance_basic/tasks/main.yaml b/tests/integration/targets/instance_basic/tasks/main.yaml index 24baa32be..ef238ed2a 100644 --- a/tests/integration/targets/instance_basic/tasks/main.yaml +++ b/tests/integration/targets/instance_basic/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - pg_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Placement Group") | selectattr("capabilities", "search", "Maintenance Policy") | list)[0].id }}' + pg_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Placement Group") | selectattr("capabilities", "search", "Maintenance Policy") | list)[0].id }}' - name: Get account_settings linode.cloud.account_settings: diff --git a/tests/integration/targets/instance_config_vpc_ipv6/tasks/main.yaml b/tests/integration/targets/instance_config_vpc_ipv6/tasks/main.yaml new file mode 100644 index 000000000..ca416a1cf --- /dev/null +++ b/tests/integration/targets/instance_config_vpc_ipv6/tasks/main.yaml @@ -0,0 +1,188 @@ +- name: instance_config_vpc_ipv6 + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create a VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "auto" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "auto" + state: present + register: create_subnet + + - name: Create a Linode instance with interface + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + booted: false + disks: + - label: test-disk + filesystem: ext4 + size: 20 + configs: + - label: cool-config + devices: + sda: + disk_label: test-disk + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + primary: true + ipv6: + is_public: true + slaac: + - range: auto + ranges: + - range: auto + wait: false + state: present + firewall_id: '{{ firewall_id }}' + register: create_instance + + - name: Assert instance created + assert: + that: + - create_instance.changed + + - create_instance.configs[0].interfaces[0].purpose == 'vpc' + - create_instance.configs[0].interfaces[0].subnet_id == create_subnet.subnet.id + - create_instance.configs[0].interfaces[0].vpc_id == create_vpc.vpc.id + + - create_instance.configs[0].interfaces[0].ipv6.is_public == True + + - create_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - create_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - create_instance.configs[0].interfaces[0].ipv6.ranges | length == 1 + - create_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + + - name: Update the instance interfaces + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + booted: false + disks: + - label: test-disk + filesystem: ext4 + size: 20 + configs: + - label: cool-config + devices: + sda: + disk_label: test-disk + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + primary: true + ipv6: + is_public: false + slaac: + - range: auto + ranges: + - range: auto + - range: auto + - range: auto + wait: false + state: present + register: update_instance + + - name: Assert instance updated + assert: + that: + - update_instance.changed + - update_instance.configs[0].interfaces[0].ipv6.is_public == False + + - update_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - update_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - update_instance.configs[0].interfaces[0].ipv6.ranges | length == 3 + - update_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + - update_instance.configs[0].interfaces[0].ipv6.ranges[1].range | split('/') | length == 2 + - update_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/') | length == 2 + + - name: Don't change the instance interfaces + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + booted: false + disks: + - label: test-disk + filesystem: ext4 + size: 20 + configs: + - label: cool-config + devices: + sda: + disk_label: test-disk + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + primary: true + ipv6: + slaac: + - range: auto + ranges: + - range: auto + - range: "{{ update_instance.configs[0].interfaces[0].ipv6.ranges[1].range }}" + - range: "/{{ (update_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/'))[1] }}" + wait: false + state: present + register: unchanged_instance + + - name: Assert instance unchanged + assert: + that: + - unchanged_instance.changed == False + + - unchanged_instance.configs[0].interfaces[0].ipv6.is_public == False + + - unchanged_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - unchanged_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges | length == 3 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[1].range | split('/') | length == 2 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/') | length == 2 + + always: + - ignore_errors: true + block: + - name: Delete a Linode instance + linode.cloud.instance: + label: '{{ create_instance.instance.label }}' + state: absent + register: delete_instance + + - name: Assert instance delete succeeded + assert: + that: + - delete_instance.changed + - delete_instance.instance.id == create_instance.instance.id + + - name: Delete the VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + state: absent + register: delete_vpc + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' diff --git a/tests/integration/targets/instance_disk_encryption/tasks/main.yaml b/tests/integration/targets/instance_disk_encryption/tasks/main.yaml index efb2f019c..cc1b192a4 100644 --- a/tests/integration/targets/instance_disk_encryption/tasks/main.yaml +++ b/tests/integration/targets/instance_disk_encryption/tasks/main.yaml @@ -8,7 +8,7 @@ register: all_regions - set_fact: - lde_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "LA Disk Encryption") | list)[0].id }}' + lde_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Disk Encryption") | list)[0].id }}' - name: Create a Linode instance with disk encryption set linode.cloud.instance: diff --git a/tests/integration/targets/instance_interfaces_vpc_ipv6/tasks/main.yaml b/tests/integration/targets/instance_interfaces_vpc_ipv6/tasks/main.yaml new file mode 100644 index 000000000..cecc8b5e6 --- /dev/null +++ b/tests/integration/targets/instance_interfaces_vpc_ipv6/tasks/main.yaml @@ -0,0 +1,180 @@ +- name: instance_interfaces_vpc_ipv6 + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create a VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "auto" + state: present + register: create_vpc + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: 'auto' + state: present + register: create_subnet + + - name: Create a Linode instance with interface + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + image: linode/alpine3.21 + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + primary: true + ipv6: + is_public: true + slaac: + - range: auto + ranges: + - range: auto + wait: false + state: present + firewall_id: '{{ firewall_id }}' + register: create_instance + + - name: Assert instance created + assert: + that: + - create_instance.changed + + - create_instance.configs[0].interfaces[0].purpose == 'vpc' + - create_instance.configs[0].interfaces[0].subnet_id == create_subnet.subnet.id + - create_instance.configs[0].interfaces[0].vpc_id == create_vpc.vpc.id + + - create_instance.configs[0].interfaces[0].ipv6.is_public == True + + - create_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - create_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - create_instance.configs[0].interfaces[0].ipv6.ranges | length == 1 + - create_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + + - name: Update the instance interfaces + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + image: linode/alpine3.21 + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv6: + is_public: false + slaac: + - range: auto + ranges: + - range: auto + - range: auto + - range: auto + wait: false + state: present + register: update_instance + + - name: Assert instance updated + assert: + that: + - update_instance.changed + - update_instance.configs[0].interfaces[0].ipv6.is_public == False + + - update_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - update_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - update_instance.configs[0].interfaces[0].ipv6.ranges | length == 3 + - update_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + - update_instance.configs[0].interfaces[0].ipv6.ranges[1].range | split('/') | length == 2 + - update_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/') | length == 2 + + - name: Don't change the instance interfaces + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + type: g6-nanode-1 + image: linode/alpine3.21 + interfaces: + - purpose: vpc + subnet_id: '{{ create_subnet.subnet.id }}' + ipv6: + slaac: + - range: auto + ranges: + - range: auto + - range: "{{ update_instance.configs[0].interfaces[0].ipv6.ranges[1].range }}" + - range: "/{{ (update_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/'))[1] }}" + wait: false + state: present + register: unchanged_instance + + - name: Assert instance unchanged + assert: + that: + - unchanged_instance.changed == False + + - unchanged_instance.configs[0].interfaces[0].ipv6.is_public == False + + - unchanged_instance.configs[0].interfaces[0].ipv6.slaac | length == 1 + - unchanged_instance.configs[0].interfaces[0].ipv6.slaac[0].range | split('/') | length == 2 + + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges | length == 3 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[0].range | split('/') | length == 2 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[1].range | split('/') | length == 2 + - unchanged_instance.configs[0].interfaces[0].ipv6.ranges[2].range | split('/') | length == 2 + + - name: Get all VPC IPv6 addresses for the current account + linode.cloud.vpcs_ipv6_list: + register: global_vpc_ipv6s + + - name: Ensure VPC IPv6 addresses are retrieved + assert: + that: + - global_vpc_ipv6s.addresses | length > 0 + + - name: Get all VPC IPv6 addresses for the VPC + linode.cloud.vpc_ipv6_list: + vpc_id: '{{ create_vpc.vpc.id }}' + register: vpc_ipv6s + + - name: Ensure VPC IPv6 addresses are retrieved + assert: + that: + - vpc_ipv6s.addresses | length > 0 + - vpc_ipv6s.addresses[0].vpc_id == create_vpc.vpc.id + - vpc_ipv6s.addresses[0].subnet_id == create_subnet.subnet.id + + always: + - ignore_errors: true + block: + - name: Delete a Linode instance + linode.cloud.instance: + label: '{{ create_instance.instance.label }}' + state: absent + register: delete_instance + + - name: Assert instance delete succeeded + assert: + that: + - delete_instance.changed + - delete_instance.instance.id == create_instance.instance.id + + - name: Delete the VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + state: absent + register: delete_vpc + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' diff --git a/tests/integration/targets/lke_cluster_basic/tasks/main.yaml b/tests/integration/targets/lke_cluster_basic/tasks/main.yaml index 496694539..e097e66f3 100644 --- a/tests/integration/targets/lke_cluster_basic/tasks/main.yaml +++ b/tests/integration/targets/lke_cluster_basic/tasks/main.yaml @@ -8,20 +8,19 @@ register: get_lke_versions - set_fact: - lke_versions: '{{ get_lke_versions.lke_versions|sort(attribute="id") }}' + lke_versions: '{{ get_lke_versions.lke_versions|sort(attribute="id", reverse=True) }}' - set_fact: - old_kube_version: '{{ lke_versions[0].id }}' - # Sometimes only one LKE version is available for provisioning - kube_version: '{{ lke_versions[1].id if lke_versions|length > 1 else lke_versions[0].id }}' + old_kube_version: '{{ lke_versions[1].id if lke_versions|length > 1 else lke_versions[0].id }}' + kube_version: '{{ lke_versions[0].id }}' - name: List regions that support Disk Encryption linode.cloud.region_list: {} register: all_regions - set_fact: - lde_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "LA Disk Encryption") | list)[0].id }}' + lde_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Disk Encryption") | list)[0].id }}' - name: Create a Linode LKE cluster linode.cloud.lke_cluster: @@ -40,6 +39,7 @@ effect: NoExecute - type: g6-standard-4 count: 1 + label: pool-with-autoscaler autoscaler: enabled: true min: 1 @@ -55,6 +55,7 @@ - create_cluster.cluster.region == 'us-southeast' - create_cluster.node_pools[0].type == 'g6-standard-1' - create_cluster.node_pools[0].count == 3 + - create_cluster.node_pools[1].label == 'pool-with-autoscaler' - create_cluster.node_pools[0].labels['foo.example.com/test'] == 'bar' - create_cluster.node_pools[0].labels['foo.example.com/test2'] == 'foo' - create_cluster.node_pools[0].taints[0].key == 'foo.example.com/test2' @@ -73,6 +74,7 @@ node_pools: - type: g6-standard-1 count: 2 + label: updated-pool-label labels: foo.example.com/update: updated foo.example.com/test2: foo @@ -97,6 +99,7 @@ - update_pools.node_pools[0].type == 'g6-standard-1' - update_pools.node_pools[0].count == 2 + - update_pools.node_pools[0].label == 'updated-pool-label' - update_pools.node_pools[0].id == create_cluster.node_pools[0].id - update_pools.node_pools[0].labels['foo.example.com/update'] == 'updated' diff --git a/tests/integration/targets/lke_cluster_enterprise/tasks/main.yaml b/tests/integration/targets/lke_cluster_enterprise/tasks/main.yaml index 5da0354fc..755bccb73 100644 --- a/tests/integration/targets/lke_cluster_enterprise/tasks/main.yaml +++ b/tests/integration/targets/lke_cluster_enterprise/tasks/main.yaml @@ -10,7 +10,7 @@ register: get_lke_versions_enterprise - set_fact: - lke_versions: '{{ get_lke_versions_enterprise.lke_versions|sort(attribute="id") }}' + lke_versions: '{{ get_lke_versions_enterprise.lke_versions|sort(attribute="id", reverse=True) }}' - set_fact: kube_version: '{{ lke_versions[0].id }}' @@ -20,7 +20,7 @@ register: all_regions - set_fact: - lke_e_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Kubernetes Enterprise") | list)[1].id }}' + lke_e_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Kubernetes Enterprise") | list)[1].id }}' - name: Create a Linode LKE Enterprise cluster linode.cloud.lke_cluster: diff --git a/tests/integration/targets/lke_node_pool_basic/tasks/main.yaml b/tests/integration/targets/lke_node_pool_basic/tasks/main.yaml index f6f32da1a..7c229d27f 100644 --- a/tests/integration/targets/lke_node_pool_basic/tasks/main.yaml +++ b/tests/integration/targets/lke_node_pool_basic/tasks/main.yaml @@ -15,7 +15,7 @@ register: all_regions - set_fact: - lde_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "LA Disk Encryption") | list)[0].id }}' + lde_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Disk Encryption") | list)[0].id }}' - name: Create a minimal LKE cluster linode.cloud.lke_cluster: @@ -44,6 +44,7 @@ tags: ['my-pool'] type: g6-standard-1 count: 2 + label: new-pool-label labels: foo.example.com/test: bar foo.example.com/test2: foo @@ -58,6 +59,7 @@ assert: that: - new_pool.node_pool.count == 2 + - new_pool.node_pool.label == 'new-pool-label' - new_pool.node_pool.type == 'g6-standard-1' - new_pool.node_pool.nodes[0].status == 'ready' - new_pool.node_pool.nodes[1].status == 'ready' @@ -86,6 +88,7 @@ type: g6-standard-1 count: 1 skip_polling: true + label: updated-pool-label autoscaler: enabled: true min: 1 @@ -104,6 +107,7 @@ assert: that: - update_pool.node_pool.count == 1 + - update_pool.node_pool.label == 'updated-pool-label' - update_pool.node_pool.type == 'g6-standard-1' - update_pool.node_pool.autoscaler.enabled - update_pool.node_pool.autoscaler.min == 1 diff --git a/tests/integration/targets/lke_node_pool_enterprise/tasks/main.yaml b/tests/integration/targets/lke_node_pool_enterprise/tasks/main.yaml index cabcb5bde..bb31784ec 100644 --- a/tests/integration/targets/lke_node_pool_enterprise/tasks/main.yaml +++ b/tests/integration/targets/lke_node_pool_enterprise/tasks/main.yaml @@ -17,7 +17,7 @@ register: all_regions - set_fact: - lke_e_region: '{{ (all_regions.regions | selectattr("capabilities", "search", "Kubernetes Enterprise") | list)[1].id }}' + lke_e_region: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Kubernetes Enterprise") | list)[1].id }}' - name: Create a Linode LKE Enterprise cluster linode.cloud.lke_cluster: diff --git a/tests/integration/targets/placement_group_assign/tasks/main.yaml b/tests/integration/targets/placement_group_assign/tasks/main.yaml index 39f6e31de..81727be16 100644 --- a/tests/integration/targets/placement_group_assign/tasks/main.yaml +++ b/tests/integration/targets/placement_group_assign/tasks/main.yaml @@ -8,7 +8,7 @@ register: valid_regions - set_fact: - region: '{{ (valid_regions.regions | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' + region: '{{ (valid_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' - name: Create a Linode placement group linode.cloud.placement_group: diff --git a/tests/integration/targets/placement_group_basic/tasks/main.yaml b/tests/integration/targets/placement_group_basic/tasks/main.yaml index fdd5e4014..8347d0ad1 100644 --- a/tests/integration/targets/placement_group_basic/tasks/main.yaml +++ b/tests/integration/targets/placement_group_basic/tasks/main.yaml @@ -8,7 +8,7 @@ register: valid_regions - set_fact: - region: '{{ (valid_regions.regions | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' + region: '{{ (valid_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' - name: Create a Linode placement group linode.cloud.placement_group: diff --git a/tests/integration/targets/placement_group_info/tasks/main.yaml b/tests/integration/targets/placement_group_info/tasks/main.yaml index 1c2a5bd36..fffc2d02f 100644 --- a/tests/integration/targets/placement_group_info/tasks/main.yaml +++ b/tests/integration/targets/placement_group_info/tasks/main.yaml @@ -8,7 +8,7 @@ register: valid_regions - set_fact: - region: '{{ (valid_regions.regions | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' + region: '{{ (valid_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' - name: Create a Linode placement group linode.cloud.placement_group: diff --git a/tests/integration/targets/placement_group_list/tasks/main.yaml b/tests/integration/targets/placement_group_list/tasks/main.yaml index 9ea13ec4c..f5f21d0b5 100644 --- a/tests/integration/targets/placement_group_list/tasks/main.yaml +++ b/tests/integration/targets/placement_group_list/tasks/main.yaml @@ -8,7 +8,7 @@ register: valid_regions - set_fact: - region: '{{ (valid_regions.regions | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' + region: '{{ (valid_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Placement Group") | list)[0].id }}' - name: Create a Linode placement group linode.cloud.placement_group: diff --git a/tests/integration/targets/vpc_ipv6/tasks/main.yaml b/tests/integration/targets/vpc_ipv6/tasks/main.yaml new file mode 100644 index 000000000..60e376520 --- /dev/null +++ b/tests/integration/targets/vpc_ipv6/tasks/main.yaml @@ -0,0 +1,91 @@ +- name: vpc_ipv6 + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create a VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "/52" + state: present + register: create_vpc + + - name: Assert VPC created + assert: + that: + - create_vpc.changed + - create_vpc.vpc.label == 'ansible-test-{{ r }}' + - create_vpc.vpc.region == 'no-osl-1' + - create_vpc.vpc.ipv6 | length == 1 + - create_vpc.vpc.ipv6[0].range.endswith('/52') + + - name: Attempt to update the IPv6 range's prefix + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + description: test description updated + ipv6: + - range: "/53" + state: present + register: failed_update_prefix + failed_when: "'IPv6 cannot be updated after VPC creation.' not in failed_update_prefix.msg" + + - name: Ensure semantic equality (explicit address) + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "{{ create_vpc.vpc.ipv6[0].range }}" + state: present + register: no_update_vpc + + - name: Assert VPC not updated + assert: + that: + - no_update_vpc.changed == False + + - name: Ensure semantic equality (auto) + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "auto" + state: present + register: no_update_vpc + + - name: Assert VPC not updated + assert: + that: + - no_update_vpc.changed == False + + - name: Ensure semantic equality (None) + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: [{}] + state: present + register: no_update_vpc + + - name: Assert VPC not updated + assert: + that: + - no_update_vpc.changed == False + + always: + - ignore_errors: yes + block: + - name: Delete a VPC + linode.cloud.vpc: + label: '{{ create_vpc.vpc.label }}' + state: absent + register: delete_vpc + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + + LINODE_CA: '{{ ca_file or "" }}' diff --git a/tests/integration/targets/vpc_subnet_ipv6/tasks/main.yaml b/tests/integration/targets/vpc_subnet_ipv6/tasks/main.yaml new file mode 100644 index 000000000..d16b3f18a --- /dev/null +++ b/tests/integration/targets/vpc_subnet_ipv6/tasks/main.yaml @@ -0,0 +1,124 @@ +- name: vpc_subnet_ipv6 + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create a VPC + linode.cloud.vpc: + label: 'ansible-test-{{ r }}' + region: no-osl-1 + ipv6: + - range: "/52" + state: present + register: create_vpc + + - name: Assert VPC created + assert: + that: + - create_vpc.changed + - create_vpc.vpc.label == 'ansible-test-{{ r }}' + - create_vpc.vpc.region == 'no-osl-1' + - create_vpc.vpc.ipv6 | length == 1 + - create_vpc.vpc.ipv6[0].range.endswith('/52') + + - name: Create a subnet + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "auto" + state: present + register: create_subnet + + - name: Assert Subnet created + assert: + that: + - create_subnet.changed + - create_subnet.subnet.label == 'test-subnet' + - create_subnet.subnet.ipv4 == '10.0.0.0/24' + - create_subnet.subnet.ipv6 | length == 1 + - create_subnet.subnet.ipv6[0].range != None + - create_subnet.subnet.created != None + - create_subnet.subnet.updated != None + - create_subnet.subnet.linodes | length == 0 + + - name: Attempt to update the VPC subnet's IPv6 ranges + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "::/2" + state: present + register: invalid_subnet + failed_when: "'IPv6 cannot be updated after VPC subnet creation.' not in invalid_subnet.msg" + + - name: Ensure semantic equality (explicit address) + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "{{ create_subnet.subnet.ipv6[0].range }}" + state: present + register: unchanged + + - name: Ensure unchanged + assert: + that: + - unchanged.changed == False + + - name: Ensure semantic equality (prefix) + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "/{{ (create_subnet.subnet.ipv6[0].range | split('/'))[1] }}" + state: present + register: unchanged + + - name: Ensure unchanged + assert: + that: + - unchanged.changed == False + + - name: Ensure semantic equality (auto) + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + ipv4: '10.0.0.0/24' + ipv6: + - range: "auto" + state: present + register: unchanged + + - name: Ensure unchanged + assert: + that: + - unchanged.changed == False + + always: + - ignore_errors: yes + block: + - name: Delete a subnet + linode.cloud.vpc_subnet: + vpc_id: '{{ create_vpc.vpc.id }}' + label: 'test-subnet' + state: absent + register: delete_subnet + + - name: Delete a VPC + linode.cloud.vpc: + label: '{{ create_vpc.vpc.label }}' + state: absent + register: delete_vpc + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + + LINODE_CA: '{{ ca_file or "" }}'