From 06069f99c7b0616e72ca9716552b7eaae0d05028 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 6 Apr 2026 12:53:18 -0400 Subject: [PATCH 1/2] feat(mtv-warm-cutover): add warm migration cutover role Add a new role and playbook to trigger cutover on active warm migrations. Patches the Migration resource with a cutover timestamp to initiate the final CBT sync, source VM shutdown, and target VM boot. Supports targeting by migration name, label selector, or plan name, with optional wait-for-completion verification. Resolves: MFG-216 Co-Authored-By: Claude Opus 4.6 --- playbooks/mtv_warm_cutover.yml | 11 ++ roles/mtv_warm_cutover/defaults/main.yml | 35 ++++++ roles/mtv_warm_cutover/meta/main.yml | 10 ++ .../mtv_warm_cutover/tasks/_apply_cutover.yml | 69 ++++++++++++ .../tasks/_process_cutover.yml | 100 ++++++++++++++++++ roles/mtv_warm_cutover/tasks/main.yml | 20 ++++ roles/mtv_warm_cutover/tests/inventory | 1 + roles/mtv_warm_cutover/tests/test.yml | 7 ++ 8 files changed, 253 insertions(+) create mode 100644 playbooks/mtv_warm_cutover.yml create mode 100644 roles/mtv_warm_cutover/defaults/main.yml create mode 100644 roles/mtv_warm_cutover/meta/main.yml create mode 100644 roles/mtv_warm_cutover/tasks/_apply_cutover.yml create mode 100644 roles/mtv_warm_cutover/tasks/_process_cutover.yml create mode 100644 roles/mtv_warm_cutover/tasks/main.yml create mode 100644 roles/mtv_warm_cutover/tests/inventory create mode 100644 roles/mtv_warm_cutover/tests/test.yml diff --git a/playbooks/mtv_warm_cutover.yml b/playbooks/mtv_warm_cutover.yml new file mode 100644 index 0000000..f38da45 --- /dev/null +++ b/playbooks/mtv_warm_cutover.yml @@ -0,0 +1,11 @@ +--- + +- name: Finish Warm Migration Cutover + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Invoke Warm Migration Cutover + ansible.builtin.include_role: + name: infra.openshift_virtualization_migration.mtv_warm_cutover +... diff --git a/roles/mtv_warm_cutover/defaults/main.yml b/roles/mtv_warm_cutover/defaults/main.yml new file mode 100644 index 0000000..81b0a51 --- /dev/null +++ b/roles/mtv_warm_cutover/defaults/main.yml @@ -0,0 +1,35 @@ +--- +# defaults file for mtv_warm_cutover + +# title: Warm Cutover Request +# required: True +# description: List of warm migration cutover requests +mtv_warm_cutover_request: [] +# - namespace: # Namespace containing the Migration resources. +# names: # List of Migration resource names to trigger cutover for. \ +# Optional when using label_selectors or plan_name. +# - +# label_selectors: # Label selectors to match Migration resources. +# - = +# plan_name: # Name of the Plan to find associated Migrations for cutover. +# cutover: # ISO 8601 timestamp for scheduled cutover \ +# (e.g., '2026-04-07T02:00:00Z'). If empty, uses current time. + +# title: MTV Namespace +# required: False +# description: Default namespace for MTV resources +mtv_warm_cutover_default_namespace: openshift-mtv +# title: Verify Cutover Complete +# required: False +# description: Wait until cutover migrations complete +mtv_warm_cutover_verify_complete: true +# title: Verify Complete Retries +# required: False +# description: Number of retries when waiting for cutover completion +mtv_warm_cutover_verify_retries: 360 +# title: Verify Complete Delay +# required: False +# description: Seconds to wait between retries +mtv_warm_cutover_verify_delay: 20 + +... diff --git a/roles/mtv_warm_cutover/meta/main.yml b/roles/mtv_warm_cutover/meta/main.yml new file mode 100644 index 0000000..492e78b --- /dev/null +++ b/roles/mtv_warm_cutover/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: "" + description: Trigger cutover for warm migrations to finish the migration process. + company: Red Hat + license: GPL-3.0-only + min_ansible_version: 2.15.0 + galaxy_tags: [] +dependencies: [] +... diff --git a/roles/mtv_warm_cutover/tasks/_apply_cutover.yml b/roles/mtv_warm_cutover/tasks/_apply_cutover.yml new file mode 100644 index 0000000..8dddf9a --- /dev/null +++ b/roles/mtv_warm_cutover/tasks/_apply_cutover.yml @@ -0,0 +1,69 @@ +--- + +- name: _apply_cutover | Verify Migration Is a Warm Migration + ansible.builtin.assert: + that: + - >- + mtv_warm_cutover_migration.spec.cutover is not defined or + mtv_warm_cutover_migration.spec.cutover | default("", true) | length == 0 + fail_msg: >- + Migration '{{ mtv_warm_cutover_migration.metadata.name }}' already has + a cutover timestamp set + ('{{ mtv_warm_cutover_migration.spec.cutover | default("") }}') + quiet: true + +- name: _apply_cutover | Patch Migration with Cutover Timestamp + kubernetes.core.k8s_json_patch: + api_version: forklift.konveyor.io/v1beta1 + kind: Migration + namespace: "{{ mtv_warm_cutover_migration.metadata.namespace }}" + name: "{{ mtv_warm_cutover_migration.metadata.name }}" + patch: + - op: add + path: /spec/cutover + value: "{{ mtv_warm_cutover_timestamp }}" + register: mtv_warm_cutover_patch_result + +- name: _apply_cutover | Report Cutover Triggered + ansible.builtin.debug: + msg: >- + Cutover triggered for Migration + '{{ mtv_warm_cutover_migration.metadata.name }}' + at {{ mtv_warm_cutover_timestamp }} + +- name: _apply_cutover | Wait for Migration to Complete + when: mtv_warm_cutover_verify_complete | bool + kubernetes.core.k8s_info: + api_version: forklift.konveyor.io/v1beta1 + kind: Migration + namespace: "{{ mtv_warm_cutover_migration.metadata.namespace }}" + name: "{{ mtv_warm_cutover_migration.metadata.name }}" + register: mtv_warm_cutover_status + until: > + mtv_warm_cutover_status is defined and + 'resources' in mtv_warm_cutover_status and + mtv_warm_cutover_status.resources | length == 1 and + 'status' in mtv_warm_cutover_status.resources | first and + 'completed' in (mtv_warm_cutover_status.resources | first).status and + (mtv_warm_cutover_status.resources | first).status.completed + | trim | length > 0 + retries: "{{ mtv_warm_cutover_verify_retries }}" + delay: "{{ mtv_warm_cutover_verify_delay }}" + +- name: _apply_cutover | Verify Migration Succeeded + when: mtv_warm_cutover_verify_complete | bool + ansible.builtin.assert: + that: + - >- + (mtv_warm_cutover_status.resources | first).status.conditions + | selectattr('type', 'defined') + | selectattr('status', 'defined') + | selectattr('type', 'equalto', 'Succeeded') + | selectattr('status', 'equalto', 'True') + | list | length == 1 + fail_msg: >- + Migration '{{ mtv_warm_cutover_migration.metadata.name }}' + cutover did not succeed + quiet: true + +... diff --git a/roles/mtv_warm_cutover/tasks/_process_cutover.yml b/roles/mtv_warm_cutover/tasks/_process_cutover.yml new file mode 100644 index 0000000..225b972 --- /dev/null +++ b/roles/mtv_warm_cutover/tasks/_process_cutover.yml @@ -0,0 +1,100 @@ +--- + +- name: _process_cutover | Set Working Namespace + ansible.builtin.set_fact: + mtv_warm_cutover_namespace: >- + {{ mtv_warm_cutover_item.namespace + | default(mtv_warm_cutover_default_namespace) }} + +- name: _process_cutover | Query Migrations by Name + when: mtv_warm_cutover_item.names | default([], true) | length > 0 + kubernetes.core.k8s_info: + api_version: forklift.konveyor.io/v1beta1 + kind: Migration + namespace: "{{ mtv_warm_cutover_namespace }}" + name: "{{ mtv_warm_cutover_migration_name }}" + register: mtv_warm_cutover_by_name + loop: "{{ mtv_warm_cutover_item.names }}" + loop_control: + loop_var: mtv_warm_cutover_migration_name + label: "{{ mtv_warm_cutover_migration_name }}" + +- name: _process_cutover | Query Migrations by Label Selector + when: + - mtv_warm_cutover_item.names | default([], true) | length == 0 + - mtv_warm_cutover_item.label_selectors | default([], true) | length > 0 + kubernetes.core.k8s_info: + api_version: forklift.konveyor.io/v1beta1 + kind: Migration + namespace: "{{ mtv_warm_cutover_namespace }}" + label_selectors: "{{ mtv_warm_cutover_item.label_selectors }}" + register: mtv_warm_cutover_by_selector + +- name: _process_cutover | Query Migrations by Plan Name + when: + - mtv_warm_cutover_item.names | default([], true) | length == 0 + - mtv_warm_cutover_item.label_selectors | default([], true) | length == 0 + - mtv_warm_cutover_item.plan_name | default("", true) | length > 0 + kubernetes.core.k8s_info: + api_version: forklift.konveyor.io/v1beta1 + kind: Migration + namespace: "{{ mtv_warm_cutover_namespace }}" + register: mtv_warm_cutover_by_plan_query + +- name: _process_cutover | Filter Migrations for Plan + when: mtv_warm_cutover_by_plan_query is not skipped + ansible.builtin.set_fact: + mtv_warm_cutover_by_plan: + resources: >- + {{ mtv_warm_cutover_by_plan_query.resources + | selectattr('spec.plan.name', 'equalto', mtv_warm_cutover_item.plan_name) + | list }} + +- name: _process_cutover | Build Migration List from Named Queries + when: mtv_warm_cutover_by_name is not skipped + ansible.builtin.set_fact: + mtv_warm_cutover_migrations: >- + {{ mtv_warm_cutover_by_name.results + | selectattr('resources', 'defined') + | map(attribute='resources') + | flatten }} + +- name: _process_cutover | Build Migration List from Selector + when: mtv_warm_cutover_by_selector is not skipped + ansible.builtin.set_fact: + mtv_warm_cutover_migrations: >- + {{ mtv_warm_cutover_by_selector.resources | default([]) }} + +- name: _process_cutover | Build Migration List from Plan + when: + - mtv_warm_cutover_by_plan is defined + - mtv_warm_cutover_by_plan is not skipped + ansible.builtin.set_fact: + mtv_warm_cutover_migrations: >- + {{ mtv_warm_cutover_by_plan.resources | default([]) }} + +- name: _process_cutover | Verify Migrations Found + ansible.builtin.assert: + that: + - mtv_warm_cutover_migrations | default([], true) | length > 0 + fail_msg: >- + No Migration resources found in namespace + '{{ mtv_warm_cutover_namespace }}' + matching the provided criteria + quiet: true + +- name: _process_cutover | Determine Cutover Timestamp + ansible.builtin.set_fact: + mtv_warm_cutover_timestamp: >- + {{ mtv_warm_cutover_item.cutover + | default(now(utc=true, fmt='%Y-%m-%dT%H:%M:%SZ')) }} + +- name: _process_cutover | Trigger Cutover on Migrations + ansible.builtin.include_tasks: + file: _apply_cutover.yml + loop: "{{ mtv_warm_cutover_migrations }}" + loop_control: + loop_var: mtv_warm_cutover_migration + label: "{{ mtv_warm_cutover_migration.metadata.name }}" + +... diff --git a/roles/mtv_warm_cutover/tasks/main.yml b/roles/mtv_warm_cutover/tasks/main.yml new file mode 100644 index 0000000..7134ffc --- /dev/null +++ b/roles/mtv_warm_cutover/tasks/main.yml @@ -0,0 +1,20 @@ +--- + +- name: Verify mtv_warm_cutover_request Variable Provided + ansible.builtin.assert: + that: + - mtv_warm_cutover_request | default("", true) | length > 0 + fail_msg: "'mtv_warm_cutover_request' Variable Not Provided" + quiet: true + +- name: Process Warm Cutover Requests + ansible.builtin.include_tasks: + file: _process_cutover.yml + loop: "{{ mtv_warm_cutover_request }}" + loop_control: + loop_var: mtv_warm_cutover_item + label: >- + Namespace: {{ mtv_warm_cutover_item.namespace + | default(mtv_warm_cutover_default_namespace) }} + +... diff --git a/roles/mtv_warm_cutover/tests/inventory b/roles/mtv_warm_cutover/tests/inventory new file mode 100644 index 0000000..2fbb50c --- /dev/null +++ b/roles/mtv_warm_cutover/tests/inventory @@ -0,0 +1 @@ +localhost diff --git a/roles/mtv_warm_cutover/tests/test.yml b/roles/mtv_warm_cutover/tests/test.yml new file mode 100644 index 0000000..fa741e2 --- /dev/null +++ b/roles/mtv_warm_cutover/tests/test.yml @@ -0,0 +1,7 @@ +--- +- name: Test + hosts: localhost + remote_user: root + roles: + - mtv_warm_cutover +... From eb9803e708429f0cd24c69e022613f58fa8a7698 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Wed, 22 Apr 2026 13:50:40 -0400 Subject: [PATCH 2/2] fix(mtv-warm-cutover): address review feedback from spyrexd - Update min_ansible_version to 2.16.0 - Skip migrations that already have a cutover timestamp instead of failing - Add argument_specs.yml declaring all role inputs Co-Authored-By: Claude Opus 4.6 --- .../mtv_warm_cutover/meta/argument_specs.yml | 49 +++++++++++++++++++ roles/mtv_warm_cutover/meta/main.yml | 2 +- .../mtv_warm_cutover/tasks/_apply_cutover.yml | 34 ++++++++----- 3 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 roles/mtv_warm_cutover/meta/argument_specs.yml diff --git a/roles/mtv_warm_cutover/meta/argument_specs.yml b/roles/mtv_warm_cutover/meta/argument_specs.yml new file mode 100644 index 0000000..56a45a4 --- /dev/null +++ b/roles/mtv_warm_cutover/meta/argument_specs.yml @@ -0,0 +1,49 @@ +--- +argument_specs: + main: + short_description: MTV Warm Cutover - Trigger cutover for warm migrations + options: + mtv_warm_cutover_request: + description: List of warm migration cutover requests + type: list + required: true + elements: dict + options: + namespace: + description: Namespace containing the Migration resources + type: str + names: + description: >- + List of Migration resource names to trigger cutover for. + Optional when using label_selectors or plan_name. + type: list + elements: str + label_selectors: + description: Label selectors to match Migration resources + type: list + elements: str + plan_name: + description: Name of the Plan to find associated Migrations for cutover + type: str + cutover: + description: >- + ISO 8601 timestamp for scheduled cutover + (e.g., '2026-04-07T02:00:00Z'). If omitted, uses current time. + type: str + mtv_warm_cutover_default_namespace: + description: Default namespace for MTV resources + type: str + default: openshift-mtv + mtv_warm_cutover_verify_complete: + description: Wait until cutover migrations complete + type: bool + default: true + mtv_warm_cutover_verify_retries: + description: Number of retries when waiting for cutover completion + type: int + default: 360 + mtv_warm_cutover_verify_delay: + description: Seconds to wait between retries + type: int + default: 20 +... diff --git a/roles/mtv_warm_cutover/meta/main.yml b/roles/mtv_warm_cutover/meta/main.yml index 492e78b..1829f4b 100644 --- a/roles/mtv_warm_cutover/meta/main.yml +++ b/roles/mtv_warm_cutover/meta/main.yml @@ -4,7 +4,7 @@ galaxy_info: description: Trigger cutover for warm migrations to finish the migration process. company: Red Hat license: GPL-3.0-only - min_ansible_version: 2.15.0 + min_ansible_version: 2.16.0 galaxy_tags: [] dependencies: [] ... diff --git a/roles/mtv_warm_cutover/tasks/_apply_cutover.yml b/roles/mtv_warm_cutover/tasks/_apply_cutover.yml index 8dddf9a..d62c139 100644 --- a/roles/mtv_warm_cutover/tasks/_apply_cutover.yml +++ b/roles/mtv_warm_cutover/tasks/_apply_cutover.yml @@ -1,18 +1,21 @@ --- -- name: _apply_cutover | Verify Migration Is a Warm Migration - ansible.builtin.assert: - that: - - >- - mtv_warm_cutover_migration.spec.cutover is not defined or - mtv_warm_cutover_migration.spec.cutover | default("", true) | length == 0 - fail_msg: >- - Migration '{{ mtv_warm_cutover_migration.metadata.name }}' already has - a cutover timestamp set - ('{{ mtv_warm_cutover_migration.spec.cutover | default("") }}') - quiet: true +- name: _apply_cutover | Check if Cutover Already Defined + ansible.builtin.set_fact: + mtv_warm_cutover_already_set: >- + {{ mtv_warm_cutover_migration.spec.cutover is defined and + mtv_warm_cutover_migration.spec.cutover | default("", true) | length > 0 }} + +- name: _apply_cutover | Skip Already-Cutover Migration + when: mtv_warm_cutover_already_set | bool + ansible.builtin.debug: + msg: >- + Skipping Migration '{{ mtv_warm_cutover_migration.metadata.name }}' + — cutover already set to + '{{ mtv_warm_cutover_migration.spec.cutover }}' - name: _apply_cutover | Patch Migration with Cutover Timestamp + when: not (mtv_warm_cutover_already_set | bool) kubernetes.core.k8s_json_patch: api_version: forklift.konveyor.io/v1beta1 kind: Migration @@ -25,6 +28,7 @@ register: mtv_warm_cutover_patch_result - name: _apply_cutover | Report Cutover Triggered + when: not (mtv_warm_cutover_already_set | bool) ansible.builtin.debug: msg: >- Cutover triggered for Migration @@ -32,7 +36,9 @@ at {{ mtv_warm_cutover_timestamp }} - name: _apply_cutover | Wait for Migration to Complete - when: mtv_warm_cutover_verify_complete | bool + when: + - not (mtv_warm_cutover_already_set | bool) + - mtv_warm_cutover_verify_complete | bool kubernetes.core.k8s_info: api_version: forklift.konveyor.io/v1beta1 kind: Migration @@ -51,7 +57,9 @@ delay: "{{ mtv_warm_cutover_verify_delay }}" - name: _apply_cutover | Verify Migration Succeeded - when: mtv_warm_cutover_verify_complete | bool + when: + - not (mtv_warm_cutover_already_set | bool) + - mtv_warm_cutover_verify_complete | bool ansible.builtin.assert: that: - >-