From e8c7c657bd33c20555b63f404501f8fdeca13b3a Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 6 Apr 2026 12:37:23 -0400 Subject: [PATCH] feat(vm-gitops-export): capture VM definitions for GitOps Add a new role and playbook to capture running VirtualMachine definitions from OpenShift, clean runtime fields from manifests, and export the resulting YAML to a Git repository. Supports SSH and HTTPS token auth, named VM targeting and label selectors for bulk capture. Resolves: MFG-273 Co-Authored-By: Claude Opus 4.6 --- playbooks/vm_gitops_export.yml | 15 +++ roles/vm_gitops_export/defaults/main.yml | 71 ++++++++++ roles/vm_gitops_export/meta/main.yml | 10 ++ .../tasks/_clean_manifest.yml | 61 +++++++++ .../vm_gitops_export/tasks/_export_to_git.yml | 121 ++++++++++++++++++ roles/vm_gitops_export/tasks/main.yml | 78 +++++++++++ roles/vm_gitops_export/tests/inventory | 1 + roles/vm_gitops_export/tests/test.yml | 7 + 8 files changed, 364 insertions(+) create mode 100644 playbooks/vm_gitops_export.yml create mode 100644 roles/vm_gitops_export/defaults/main.yml create mode 100644 roles/vm_gitops_export/meta/main.yml create mode 100644 roles/vm_gitops_export/tasks/_clean_manifest.yml create mode 100644 roles/vm_gitops_export/tasks/_export_to_git.yml create mode 100644 roles/vm_gitops_export/tasks/main.yml create mode 100644 roles/vm_gitops_export/tests/inventory create mode 100644 roles/vm_gitops_export/tests/test.yml diff --git a/playbooks/vm_gitops_export.yml b/playbooks/vm_gitops_export.yml new file mode 100644 index 0000000..58c0428 --- /dev/null +++ b/playbooks/vm_gitops_export.yml @@ -0,0 +1,15 @@ +--- + +- name: Export VM Definitions to GitOps Repository + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Invoke VM GitOps Export + ansible.builtin.include_role: + name: infra.openshift_virtualization_migration.vm_gitops_export + vars: + openshift_host: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_HOST', default=Undefined) | default('', True) }}" + openshift_api_key: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_API_KEY', default=Undefined) | default('', True) }}" # noqa: yaml[line-length] + openshift_verify_ssl: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_VERIFY_SSL', default='') | default(false) | bool }}" # noqa: yaml[line-length] +... diff --git a/roles/vm_gitops_export/defaults/main.yml b/roles/vm_gitops_export/defaults/main.yml new file mode 100644 index 0000000..acb7b41 --- /dev/null +++ b/roles/vm_gitops_export/defaults/main.yml @@ -0,0 +1,71 @@ +--- +# defaults file for vm_gitops_export + +# title: GitOps Export Request +# required: True +# description: List of VirtualMachine GitOps Export Requests +vm_gitops_export_request: [] +# - namespace: # Namespace containing VMs to capture. +# names: # List of VM names within a namespace. \ +# Optional when using label_selectors. +# - +# label_selectors: # Label selectors to match VMs. \ +# Cannot be used with list of VM names. +# - = + +# title: Git Repository URL +# required: True +# description: URL of the Git repository to store VM manifests +vm_gitops_export_git_repo_url: "" +# title: Git Branch +# required: False +# description: Git branch to push manifests to +vm_gitops_export_git_branch: "main" +# title: Git Path +# required: False +# description: Directory path within the Git repository to store manifests +vm_gitops_export_git_path: "virtualmachines" +# title: Git User Name +# required: False +# description: Git commit author name +vm_gitops_export_git_user_name: "Ansible Automation" +# title: Git User Email +# required: False +# description: Git commit author email +vm_gitops_export_git_user_email: "ansible@automation.local" +# title: Git SSH Key Path +# required: False +# description: Path to SSH private key for Git authentication +vm_gitops_export_git_ssh_key: "" +# title: Git Token +# required: False +# description: Token for HTTPS Git authentication +vm_gitops_export_git_token: "" +# title: Git Commit Message +# required: False +# description: Commit message for exported manifests +vm_gitops_export_git_commit_message: "Export VirtualMachine definitions via Ansible" + +# title: OpenShift Host +# required: True +# description: OpenShift Host +vm_gitops_export_openshift_host: "{{ openshift_host }}" +# title: OpenShift API Key +# required: True +# description: OpenShift API Key +vm_gitops_export_openshift_api_key: "{{ openshift_api_key }}" +# title: Verify SSL Certificate +# required: True +# description: Verify SSL Certificate +vm_gitops_export_openshift_verify_ssl: "{{ openshift_verify_ssl }}" +# title: KubeVirt API Version +# required: True +# description: KubeVirt API Version +vm_gitops_export_kubevirt_api_version: kubevirt.io/v1 + +# title: Cleanup Working Directory +# required: False +# description: Remove local working directory after export +vm_gitops_export_cleanup: true + +... diff --git a/roles/vm_gitops_export/meta/main.yml b/roles/vm_gitops_export/meta/main.yml new file mode 100644 index 0000000..aa323fb --- /dev/null +++ b/roles/vm_gitops_export/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: "" + description: Capture VirtualMachine definitions and export to a GitOps repository. + company: Red Hat + license: GPL-3.0-only + min_ansible_version: 2.15.0 + galaxy_tags: [] +dependencies: [] +... diff --git a/roles/vm_gitops_export/tasks/_clean_manifest.yml b/roles/vm_gitops_export/tasks/_clean_manifest.yml new file mode 100644 index 0000000..d415a19 --- /dev/null +++ b/roles/vm_gitops_export/tasks/_clean_manifest.yml @@ -0,0 +1,61 @@ +--- + +- name: _clean_manifest | Build Cleaned Manifest + ansible.builtin.set_fact: + vm_gitops_export_cleaned_manifests: >- + {{ vm_gitops_export_cleaned_manifests + [vm_gitops_export_cleaned_vm] }} + vars: + vm_gitops_export_source: "{{ vm_gitops_export_vm.response_obj }}" + vm_gitops_export_clean_metadata: >- + {{ + vm_gitops_export_source.metadata + | dict2items + | rejectattr('key', 'in', vm_gitops_export_metadata_remove_keys) + | items2dict + }} + vm_gitops_export_metadata_remove_keys: + - uid + - resourceVersion + - generation + - creationTimestamp + - deletionTimestamp + - deletionGracePeriodSeconds + - managedFields + - ownerReferences + - finalizers + - selfLink + vm_gitops_export_clean_annotations: >- + {{ + vm_gitops_export_clean_metadata.annotations | default({}) + | dict2items + | rejectattr('key', 'match', 'kubectl.kubernetes.io/') + | rejectattr('key', 'match', 'kubevirt.io/latest-observed-api-version') + | rejectattr('key', 'match', 'kubevirt.io/storage-observed-api-version') + | items2dict + }} + vm_gitops_export_final_metadata: >- + {{ + vm_gitops_export_clean_metadata + | ansible.builtin.combine( + {'annotations': vm_gitops_export_clean_annotations} + if vm_gitops_export_clean_annotations | length > 0 + else {}, + recursive=True) + }} + vm_gitops_export_cleaned_vm: + apiVersion: "{{ vm_gitops_export_source.apiVersion }}" + kind: "{{ vm_gitops_export_source.kind }}" + metadata: >- + {{ + vm_gitops_export_final_metadata + | dict2items + | rejectattr('key', 'equalto', 'annotations') + | items2dict + | ansible.builtin.combine( + {'annotations': vm_gitops_export_clean_annotations} + if vm_gitops_export_clean_annotations | length > 0 + else {}) + }} + spec: "{{ vm_gitops_export_source.spec }}" + +... diff --git a/roles/vm_gitops_export/tasks/_export_to_git.yml b/roles/vm_gitops_export/tasks/_export_to_git.yml new file mode 100644 index 0000000..db8bf49 --- /dev/null +++ b/roles/vm_gitops_export/tasks/_export_to_git.yml @@ -0,0 +1,121 @@ +--- + +- name: _export_to_git | Create Working Directory + ansible.builtin.file: + path: "{{ vm_gitops_export_work_dir }}" + state: directory + mode: "0700" + +- name: _export_to_git | Configure Git SSH Command + when: vm_gitops_export_git_ssh_key | default("", true) | length > 0 + ansible.builtin.set_fact: + vm_gitops_export_git_env: + GIT_SSH_COMMAND: >- + ssh -i {{ vm_gitops_export_git_ssh_key }} -o StrictHostKeyChecking=no + +- name: _export_to_git | Build HTTPS URL with Token + when: vm_gitops_export_git_token | default("", true) | length > 0 + ansible.builtin.set_fact: + vm_gitops_export_git_clone_url: >- + {{ vm_gitops_export_git_repo_url + | regex_replace('^https://', + 'https://oauth2:' + vm_gitops_export_git_token + '@') }} + no_log: true + +- name: _export_to_git | Set Clone URL for SSH + when: vm_gitops_export_git_token | default("", true) | length == 0 + ansible.builtin.set_fact: + vm_gitops_export_git_clone_url: "{{ vm_gitops_export_git_repo_url }}" + +- name: _export_to_git | Clone Git Repository + ansible.builtin.git: + repo: "{{ vm_gitops_export_git_clone_url }}" + dest: "{{ vm_gitops_export_work_dir }}/repo" + version: "{{ vm_gitops_export_git_branch }}" + depth: 1 + single_branch: true + key_file: >- + {{ vm_gitops_export_git_ssh_key + if (vm_gitops_export_git_ssh_key | default("", true) | length > 0) + else omit }} + accept_newhostkey: true + no_log: true + +- name: _export_to_git | Create Export Directory + ansible.builtin.file: + path: "{{ vm_gitops_export_work_dir }}/repo/{{ vm_gitops_export_git_path }}" + state: directory + mode: "0755" + +- name: _export_to_git | Write VM Manifests + ansible.builtin.copy: + content: >- + {{ vm_gitops_export_manifest | to_nice_yaml(indent=2, width=200) }} + dest: >- + {{ vm_gitops_export_work_dir }}/repo/{{ vm_gitops_export_git_path }}/{{ + vm_gitops_export_manifest.metadata.namespace }}_{{ + vm_gitops_export_manifest.metadata.name }}.yml + mode: "0644" + loop: "{{ vm_gitops_export_cleaned_manifests }}" + loop_control: + loop_var: vm_gitops_export_manifest + label: >- + {{ vm_gitops_export_manifest.metadata.namespace }}/{{ + vm_gitops_export_manifest.metadata.name }} + register: vm_gitops_export_write_result + +- name: _export_to_git | Configure Git User # noqa: command-instead-of-module + ansible.builtin.command: + cmd: >- + git config user.{{ vm_gitops_export_git_config_item.key }} + '{{ vm_gitops_export_git_config_item.value }}' + chdir: "{{ vm_gitops_export_work_dir }}/repo" + loop: + - key: name + value: "{{ vm_gitops_export_git_user_name }}" + - key: email + value: "{{ vm_gitops_export_git_user_email }}" + loop_control: + loop_var: vm_gitops_export_git_config_item + label: "{{ vm_gitops_export_git_config_item.key }}" + changed_when: true + +- name: _export_to_git | Stage Changes # noqa: command-instead-of-module + ansible.builtin.command: + cmd: "git add {{ vm_gitops_export_git_path }}/" + chdir: "{{ vm_gitops_export_work_dir }}/repo" + changed_when: true + +- name: _export_to_git | Check for Changes # noqa: command-instead-of-module + ansible.builtin.command: + cmd: git diff --cached --quiet + chdir: "{{ vm_gitops_export_work_dir }}/repo" + register: vm_gitops_export_git_diff + changed_when: false + failed_when: false + +- name: _export_to_git | Commit Changes # noqa: command-instead-of-module + when: vm_gitops_export_git_diff.rc == 1 + ansible.builtin.command: + cmd: >- + git commit -m '{{ vm_gitops_export_git_commit_message }}' + chdir: "{{ vm_gitops_export_work_dir }}/repo" + changed_when: true + +- name: _export_to_git | Push Changes # noqa: command-instead-of-module + when: vm_gitops_export_git_diff.rc == 1 + ansible.builtin.command: + cmd: "git push origin {{ vm_gitops_export_git_branch }}" + chdir: "{{ vm_gitops_export_work_dir }}/repo" + environment: "{{ vm_gitops_export_git_env | default({}) }}" + changed_when: true + no_log: true + +- name: _export_to_git | Report No Changes + when: vm_gitops_export_git_diff.rc == 0 + ansible.builtin.debug: + msg: >- + No changes detected in VirtualMachine manifests. + Nothing to commit. + +... diff --git a/roles/vm_gitops_export/tasks/main.yml b/roles/vm_gitops_export/tasks/main.yml new file mode 100644 index 0000000..bbd5838 --- /dev/null +++ b/roles/vm_gitops_export/tasks/main.yml @@ -0,0 +1,78 @@ +--- + +- name: Verify vm_gitops_export_request Variable Provided + ansible.builtin.assert: + that: + - vm_gitops_export_request | default("", true) | length > 0 + fail_msg: "'vm_gitops_export_request' Variable Not Provided" + quiet: true + +- name: Verify Required Properties Provided + ansible.builtin.assert: + that: + - vm_gitops_export_request | selectattr('namespace', 'undefined') | list | length == 0 + fail_msg: "Required property 'namespace' in 'vm_gitops_export_request' Variable Not Provided" + quiet: true + +- name: Verify Git Repository URL Provided + ansible.builtin.assert: + that: + - vm_gitops_export_git_repo_url | default("", true) | length > 0 + fail_msg: "'vm_gitops_export_git_repo_url' Variable Not Provided" + quiet: true + +- name: Verify Git Authentication Provided + ansible.builtin.assert: + that: + - >- + (vm_gitops_export_git_ssh_key | default("", true) | length > 0) or + (vm_gitops_export_git_token | default("", true) | length > 0) + fail_msg: "Either 'vm_gitops_export_git_ssh_key' or 'vm_gitops_export_git_token' must be provided" + quiet: true + +- name: Initialize Variables + ansible.builtin.set_fact: + vm_gitops_export_vms: [] + vm_gitops_export_cleaned_manifests: [] + vm_gitops_export_work_dir: "/tmp/vm_gitops_export_{{ ansible_date_time.epoch }}" + +- name: Invoke Collect VM Role + ansible.builtin.include_role: + name: infra.openshift_virtualization_migration.vm_collect + vars: + vm_collect_openshift_api_key: "{{ vm_gitops_export_openshift_api_key }}" + vm_collect_openshift_host: "{{ vm_gitops_export_openshift_host }}" + vm_collect_verify_ssl: "{{ vm_gitops_export_openshift_verify_ssl }}" + vm_collect_vms_var: vm_gitops_export_vms + loop: "{{ vm_gitops_export_request }}" + loop_control: + loop_var: vm_collect_request_instance + +- name: Verify VirtualMachines Found + ansible.builtin.assert: + that: + - vm_gitops_export_vms | length > 0 + fail_msg: "No VirtualMachines found matching the provided criteria" + quiet: true + +- name: Clean VM Manifests + ansible.builtin.include_tasks: + file: _clean_manifest.yml + loop: "{{ vm_gitops_export_vms }}" + loop_control: + loop_var: vm_gitops_export_vm + label: >- + Namespace: {{ vm_gitops_export_vm.response_obj.metadata.namespace }} + - Name: {{ vm_gitops_export_vm.response_obj.metadata.name }} + +- name: Export Manifests to Git Repository + ansible.builtin.include_tasks: + file: _export_to_git.yml + +- name: Cleanup Working Directory + when: vm_gitops_export_cleanup | bool + ansible.builtin.file: + path: "{{ vm_gitops_export_work_dir }}" + state: absent + +... diff --git a/roles/vm_gitops_export/tests/inventory b/roles/vm_gitops_export/tests/inventory new file mode 100644 index 0000000..2fbb50c --- /dev/null +++ b/roles/vm_gitops_export/tests/inventory @@ -0,0 +1 @@ +localhost diff --git a/roles/vm_gitops_export/tests/test.yml b/roles/vm_gitops_export/tests/test.yml new file mode 100644 index 0000000..372b803 --- /dev/null +++ b/roles/vm_gitops_export/tests/test.yml @@ -0,0 +1,7 @@ +--- +- name: Test + hosts: localhost + remote_user: root + roles: + - vm_gitops_export +...