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 "" }}'