Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Add support for region and type migrations; add migration_type field #443

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/inventory/instance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ Parameters
**default_value (type=str):**
\• The default value when the host variable's value is an empty string.

\• This option is mutually exclusive with \ :literal:`trailing\_separator`\ .
\• This option is mutually exclusive with \ :literal:`keyed\_groups[].trailing\_separator`\ .


**trailing_separator (type=bool, default=True):**
\• Set this option to \ :emphasis:`False`\ to omit the \ :literal:`separator`\ after the host variable when the value is an empty string.
\• Set this option to \ :literal:`False`\ to omit the \ :literal:`keyed\_groups[].separator`\ after the host variable when the value is an empty string.

\• This option is mutually exclusive with \ :literal:`default\_value`\ .
\• This option is mutually exclusive with \ :literal:`keyed\_groups[].default\_value`\ .



Expand Down
4 changes: 3 additions & 1 deletion docs/modules/instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ Manage Linode Instances, Configs, and Disks.
| [`metadata` (sub-options)](#metadata) | <center>`dict`</center> | <center>Optional</center> | Fields relating to the Linode Metadata service. |
| `backups_enabled` | <center>`bool`</center> | <center>Optional</center> | Enroll Instance in Linode Backup service. |
| `wait` | <center>`bool`</center> | <center>Optional</center> | Wait for the instance to have status "running" before returning. **(Default: `True`)** |
| `wait_timeout` | <center>`int`</center> | <center>Optional</center> | The amount of time, in seconds, to wait for an instance to have status "running". **(Default: `240`)** |
| `wait_timeout` | <center>`int`</center> | <center>Optional</center> | The amount of time, in seconds, to wait for an instance to have status "running". **(Default: `1500`)** |
| [`additional_ipv4` (sub-options)](#additional_ipv4) | <center>`list`</center> | <center>Optional</center> | Additional ipv4 addresses to allocate. |
| `rebooted` | <center>`bool`</center> | <center>Optional</center> | If true, the Linode Instance will be rebooted. NOTE: The instance will only be rebooted if it was previously in a running state. To ensure your Linode will always be rebooted, consider also setting the `booted` field. **(Default: `False`)** |
| `migration_type` | <center>`str`</center> | <center>Optional</center> | The type of migration to use for Region and Type migrations. **(Choices: `cold`, `warm`; Default: `cold`)** |
| `auto_disk_resize` | <center>`bool`</center> | <center>Optional</center> | Whether implicitly created disks should be resized during a type change operation. **(Default: `False`)** |

### configs

Expand Down
133 changes: 132 additions & 1 deletion plugins/modules/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from typing import Any, Dict, List, Optional, Union, cast

import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.instance as docs
import linode_api4
import polling
from ansible_collections.linode.cloud.plugins.module_utils.linode_common import (
LinodeModuleBase,
)
Expand All @@ -23,6 +25,7 @@
filter_null_values_recursive,
paginated_list_to_json,
parse_linode_types,
poll_condition,
)
from ansible_specdoc.objects import (
FieldType,
Expand Down Expand Up @@ -447,7 +450,7 @@
),
"wait_timeout": SpecField(
type=FieldType.integer,
default=240,
default=1500,
description=[
"The amount of time, in seconds, to wait for an instance to "
'have status "running".'
Expand All @@ -471,6 +474,21 @@
],
default=False,
),
"migration_type": SpecField(
type=FieldType.string,
description=[
"The type of migration to use for Region and Type migrations."
],
choices=["cold", "warm"],
default="cold",
),
"auto_disk_resize": SpecField(
type=FieldType.bool,
description=[
"Whether implicitly created disks should be resized during a type change operation."
],
default=False,
),
}

SPECDOC_META = SpecDocMeta(
Expand Down Expand Up @@ -864,6 +882,111 @@ def _update_firewall(self) -> None:
"and 'firewall_device' modules."
)

def _wait_for_instance_status(self, status: str) -> None:
def poll_func() -> bool:
self._instance.invalidate()
return self._instance.status == status

try:
poll_condition(poll_func, 4, self._timeout_ctx.seconds_remaining)
except polling.TimeoutException:
self.fail(
f"failed to wait for instance to reach status {status}: timeout period expired"
)

def _update_type(self) -> None:
"""
Handles updates on the type field.
"""

new_type = self.module.params.get("type")
auto_disk_resize = self.module.params.get("auto_disk_resize")
migration_type = self.module.params.get("migration_type")

# Graceful handling for a potential edge case
# where the type is stored as a string rather than
# an instance of the Type class.
current_type = self._instance.type
if isinstance(current_type, linode_api4.Type):
current_type = current_type.id

previously_booted = self._instance.status == "running"

if new_type is None or new_type == current_type:
return

resize_poller = self.client.polling.event_poller_create(
"linode", "linode_resize", entity_id=self._instance.id
)

self.client.polling.wait_for_entity_free(
"linode",
self._instance.id,
timeout=self._timeout_ctx.seconds_remaining,
)

self._instance.resize(
new_type=new_type,
allow_auto_disk_resize=auto_disk_resize,
migration_type=migration_type,
)

self.register_action(
f"Resized instance from type {self._instance.type.id} to {new_type}"
)

resize_poller.wait_for_next_event_finished(
timeout=self._timeout_ctx.seconds_remaining
)

# The boot process for the instance is handled implicitly by the resize operation,
# so we wait for the instance to reach running status if necessary.
if previously_booted:
self._wait_for_instance_status("running")

def _update_region(self) -> None:
"""
Handles updates on the region field.
"""

new_region = self.module.params.get("region")
migration_type = self.module.params.get("migration_type")

# Graceful handling for a potential edge case
# where the region is stored as a string rather than
# an instance of the Region class.
current_region = self._instance.region
if isinstance(current_region, linode_api4.Region):
current_region = current_region.id

if new_region is None or new_region == current_region:
return

migration_poller = self.client.polling.event_poller_create(
"linode", "linode_migrate_datacenter", entity_id=self._instance.id
)

self.client.polling.wait_for_entity_free(
"linode",
self._instance.id,
timeout=self._timeout_ctx.seconds_remaining,
)

# TODO: Include type change in request if necessary
# so only one migration needs to be run.
self._instance.initiate_migration(
region=new_region,
migration_type=migration_type,
)

self.register_action(
f"Migrated Instance from {self._instance.region.id} to {new_region}"
)

migration_poller.wait_for_next_event_finished(
timeout=self._timeout_ctx.seconds_remaining
)

def _update_config(
self, config: Config, config_params: Dict[str, Any]
) -> None:
Expand Down Expand Up @@ -1037,6 +1160,8 @@ def _update_instance(self) -> None:
"boot_config_label",
"reboot",
"backups_enabled",
"type",
"region",
):
continue

Expand Down Expand Up @@ -1094,6 +1219,12 @@ def _update_instance(self) -> None:
# Handle updating on the target Firewall ID
self._update_firewall()

# Handle migrating the instance if necessary
self._update_region()

# Handle updating the instance type
self._update_type()

def _handle_instance_boot(self) -> None:
boot_status = self.module.params.get("booted")
should_poll = self.module.params.get("wait")
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ disable = [
"missing-timeout",
"use-sequence-for-iteration",
"broad-exception-raised",
"fixme"
]
py-version = "3.8"
extension-pkg-whitelist = "math"
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
boto3>=1.26.0
botocore>=1.29.0
pylint>=2.15.5
ansible-doc-extractor==0.1.10
ansible-doc-extractor>=0.1.10
mypy>=1.3.0
ansible>=7.5.0
Jinja2>=3.0.1
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
linode-api4>=5.10.0
# TODO: Revert once Unified Migration support is released in linode_api4
# linode-api4>=5.10.0
git+https://github.com/linode/linode_api4-python@proj/unified-migrations
polling>=0.3.2
types-requests==2.31.0.10
ansible-specdoc>=0.0.14
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
- name: instance_region_migrations
block:
- set_fact:
r: "{{ 1000000000 | random }}"

- linode.cloud.instance:
label: 'ansible-test-{{ r }}'
region: us-mia
type: g6-nanode-1
image: linode/ubuntu23.10
state: present
register: create

- assert:
that:
- create.changed
- create.instance.region == "us-mia"

- linode.cloud.instance:
label: 'ansible-test-{{ r }}'
region: us-iad
type: g6-nanode-1
image: linode/ubuntu23.10
migration_type: warm
state: present
register: migrated

- assert:
that:
- migrated.changed
- migrated.instance.region == "us-iad"

always:
- ignore_errors: yes
block:
- linode.cloud.instance:
label: '{{ create.instance.label }}'
state: absent
register: delete

- assert:
that:
- delete.changed
- delete.instance.id == create.instance.id

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

77 changes: 77 additions & 0 deletions tests/integration/targets/instance_type_change/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
- name: instance_type_change
block:
- set_fact:
r: "{{ 1000000000 | random }}"

- linode.cloud.instance:
label: 'ansible-test-{{ r }}'
region: us-mia
type: g6-nanode-1
image: linode/ubuntu23.10
state: present
register: create

- assert:
that:
- create.changed
- create.instance.type == "g6-nanode-1"

- linode.cloud.type_list:
filters:
- name: label
values: Linode 2GB
register: type_info

- name: Resize the instance with resizing disks
linode.cloud.instance:
label: 'ansible-test-{{ r }}'
region: us-mia
type: g6-standard-1
image: linode/ubuntu23.10
auto_disk_resize: true
state: present
register: resize_disks

- assert:
that:
- resize_disks.changed
- resize_disks.instance.type == "g6-standard-1"
- resize_disks.disks[0].size == type_info.types[0].disk - 512

- name: Run a warm resize without resizing disks
linode.cloud.instance:
label: 'ansible-test-{{ r }}'
region: us-mia
type: g6-standard-2
image: linode/ubuntu23.10
migration_type: warm
state: present
register: resize_disks

- assert:
that:
- resize_disks.changed
- resize_disks.instance.type == "g6-standard-2"
- resize_disks.disks[0].size == type_info.types[0].disk - 512


always:
- ignore_errors: yes
block:
- linode.cloud.instance:
label: '{{ create.instance.label }}'
state: absent
register: delete

- assert:
that:
- delete.changed
- delete.instance.id == create.instance.id

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