diff --git a/playbooks/sample-sap-vm-provision-redhat-ocpv.yml b/playbooks/sample-sap-vm-provision-redhat-ocpv.yml new file mode 100644 index 0000000..d052083 --- /dev/null +++ b/playbooks/sample-sap-vm-provision-redhat-ocpv.yml @@ -0,0 +1,128 @@ +--- +- name: Preparation Ansible Play for SAP VM provisioning on Red Hat OpenShift Virtualization + hosts: all + gather_facts: false + serial: 1 + vars: + sap_vm_provision_iac_type: ansible + sap_vm_provision_iac_platform: kubevirt_vm + pre_tasks: + # Alternative to executing ansible-playbook with -e for Ansible Extravars file +# - name: Include sample variables for Red Hat Openshift Virtualization +# ansible.builtin.include_vars: ./vars/sample-variables-sap-vm-provision-redhat-ocpv.yml + tasks: + + - name: Save inventory_host as jumphost + ansible.builtin.set_fact: + sap_vm_provision_jumphost: "{{ inventory_hostname }}" + + - name: Save ansible_user as jumphost user + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_jumphost_user: "{{ ansible_user }}" + + - name: Use kubeconfig file specified in environment variable K8S_AUTH_KUBECONFIG if sap_vm_provision_kubevirt_vm_kubeconfig_path is not defined + when: > + sap_vm_provision_kubevirt_vm_kubeconfig_path is not defined or + sap_vm_provision_kubevirt_vm_kubeconfig_path == None or + sap_vm_provision_kubevirt_vm_kubeconfig_path == '' + ansible.builtin.set_fact: + sap_vm_provision_kubevirt_vm_kubeconfig_path: "{{ lookup('env', 'K8S_AUTH_KUBECONFIG') | default(None) }}" + + - name: Create Tempdir + ansible.builtin.tempfile: + state: directory + suffix: "_sap_vm_provision_kubevirt_vm" + register: __sap_vm_provision_kubevirt_vm_register_tmpdir + + - name: Set kubeconfig file variable + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_kubeconfig: "{{ __sap_vm_provision_kubevirt_vm_register_tmpdir.path }}/kubeconfig" + + - name: Read content of kubeconfig file + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_kubeconfig_data: + "{{ lookup('file', sap_vm_provision_kubevirt_vm_kubeconfig_path) | from_yaml }}" + + - name: Read cluster endpoint and CA certificate from kubeconfig if either is not defined + when: sap_vm_provision_kubevirt_vm_extract_kubeconfig + block: + + - name: Set sap_vm_provision_kubevirt_vm_api_endpoint from kubeconfig + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_api_endpoint: + "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig_data['clusters'][0]['cluster']['server'] }}" + + - name: Write the certificate-authority-data to temp dir + ansible.builtin.copy: + content: "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig_data['clusters'][0]['cluster']['certificate-authority-data'] | b64decode }}" + dest: "{{ __sap_vm_provision_kubevirt_vm_register_tmpdir.path }}/cluster-ca-cert.pem" + mode: "0600" + + - name: Set CA file variable + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_ca_cert: "{{ __sap_vm_provision_kubevirt_vm_register_tmpdir.path }}/cluster-ca-cert.pem" + + - name: Use predefined CA cert and API endpoint + when: not sap_vm_provision_kubevirt_vm_extract_kubeconfig + block: + - name: Set predefined OCP API Endpoint + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_api_endpoint: "{{ sap_vm_provision_kubevirt_vm_api_endpoint }}" + + - name: Set predefined CA file + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_ca_cert: "{{ sap_vm_provision_kubevirt_vm_ca_cert }}" + + - name: Log into Red Hat OpenShift cluster (obtain access token) + community.okd.openshift_auth: + host: "{{ __sap_vm_provision_kubevirt_vm_register_api_endpoint }}" + username: "{{ sap_vm_provision_kubevirt_vm_admin_username }}" + password: "{{ sap_vm_provision_kubevirt_vm_admin_password }}" + ca_cert: "{{ __sap_vm_provision_kubevirt_vm_register_ca_cert }}" + register: __sap_vm_provision_kubevirt_vm_register_kubevirt_vm_auth_results + + - name: Set token in kubeconfig + ansible.builtin.set_fact: + __sap_vm_provision_kubevirt_vm_register_kubeconfig_data: >- + {{ + __sap_vm_provision_kubevirt_vm_register_kubeconfig_data | combine({ + 'users': __sap_vm_provision_kubevirt_vm_register_kubeconfig_data.users | map('combine', [{'user': {'token': __sap_vm_provision_kubevirt_vm_register_kubevirt_vm_auth_results.openshift_auth.api_key }}] ) + }, recursive=True) + }} + + - name: Write the updated kubeconfig + ansible.builtin.copy: + content: "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig_data | to_nice_yaml }}" + dest: "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig }}" + mode: "0600" + + - name: Create dynamic inventory group for Ansible Role sap_vm_provision and provide jumphost and api token + ansible.builtin.add_host: + name: "{{ item }}" + group: sap_vm_provision_target_inventory_group + sap_vm_provision_jumphost: "{{ sap_vm_provision_jumphost }}" + __sap_vm_provision_kubevirt_vm_register_jumphost_user: "{{ __sap_vm_provision_kubevirt_vm_register_jumphost_user }}" + __sap_vm_provision_kubevirt_vm_register_tmpdir: "{{ __sap_vm_provision_kubevirt_vm_register_tmpdir }}" + __sap_vm_provision_kubevirt_vm_register_kubeconfig: "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig }}" + loop: "{{ sap_vm_provision_kubevirt_vm_host_specifications_dictionary[sap_vm_provision_host_specification_plan].keys() }}" + +- name: Ansible Play to provision VMs for SAP + hosts: sap_vm_provision_target_inventory_group # Ansible Play target hosts pattern, use Inventory Group created by previous Ansible Task (add_host) + gather_facts: false + environment: + K8S_AUTH_KUBECONFIG: "{{ __sap_vm_provision_kubevirt_vm_register_kubeconfig }}" + tasks: + + - name: Execute Ansible Role sap_vm_provision + when: sap_vm_provision_iac_type == "ansible" or sap_vm_provision_iac_type == "ansible_to_terraform" + block: + - name: Include sap_vm_provision Ansible Role + ansible.builtin.include_role: + name: community.sap_infrastructure.sap_vm_provision + + always: + - name: Remove temporary directory on jumphost + delegate_to: "{{ sap_vm_provision_jumphost }}" + ansible.builtin.file: + state: absent + path: "{{ __sap_vm_provision_kubevirt_vm_register_tmpdir.path }}" diff --git a/playbooks/vars/sample-variables-sap-vm-provision-redhat-ocpv.yml b/playbooks/vars/sample-variables-sap-vm-provision-redhat-ocpv.yml new file mode 100644 index 0000000..0dd8a39 --- /dev/null +++ b/playbooks/vars/sample-variables-sap-vm-provision-redhat-ocpv.yml @@ -0,0 +1,104 @@ +--- +############################################ +# Red Hat OpenShift Virtualization # +############################################ + +# Namespace where the VM should be created in +sap_vm_provision_kubevirt_vm_target_namespace: sap + +# Username to be created on guest +sap_vm_provision_kubevirt_vm_os_user: cloud-user + +# Password for the above user +sap_vm_provision_kubevirt_vm_os_user_password: "" + +# how to authenticate to the guest vm [password|private_key|private_key_data] +# password: uses provided password in sap_vm_provision_kubevirt_vm_os_user_password, make sure your ssh config allows password authentication +# private_key: use the private ssh key at the location defined by sap_vm_provision_ssh_host_private_key_file_path +# private_key_data: use the private ssh key provided in sap_vm_provision_ssh_host_private_key_data and write it to the location defined in sap_vm_provision_ssh_host_private_key_file_path +sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism: private-key + +# Private SSH key file, must be accessible on the ansible controller +# sap_vm_provision_ssh_host_private_key_file_path: + +# private ssh key, make sure the indentation is correct, here it's two spaces at the beginning of every line +# sap_vm_provision_ssh_host_private_key_data: | +# < your key data> + +# Should the CA cert and the API endpoint be extracted from the kubeconfig file? +sap_vm_provision_kubevirt_vm_extract_kubeconfig: true + +# Should an existing VM be overwritten? +sap_vm_provision_kubevirt_vm_overwrite_vm: false + +# Kubeconfig file for cluster where VMs should be created +sap_vm_provision_kubevirt_vm_kubeconfig_path: /path/to/clusterconfigs/kubeconfig + +# In order to use secured communication, provide the CA cert bundle for the cluster. +# This can be extracted from the kubeconfig file with the following command from the +# kubeconfig file: +# grep certificate-authority-data ${KUBECONFIG} | awk '{ print $2 }' | base64 --decode > cluster-ca-cert.pem +# This variable will not be used if sap_vm_provision_kubevirt_vm_extract_kubeconfig = true +# sap_vm_provision_kubevirt_vm_ca_cert: /path/to/clusterconfigs/cluster-ca-cert.pem + +# API endpoint of the cluster +# This variable will not be used if sap_vm_provision_kubevirt_vm_extract_kubeconfig = true +# sap_vm_provision_kubevirt_vm_api_endpoint: https://api.cluster.domain.tld:6443 + +# Admin username for the cluster communication +sap_vm_provision_kubevirt_vm_admin_username: kubeadmin + +# Password for the above admin user +sap_vm_provision_kubevirt_vm_admin_password: AAAAA-BBBBB-CCCCC-DDDDD + +# RAM Overhead [GiB] for virt-launcher container, this can be small for VMs < 1 TB and without SRIOV but should be increased to 16 or more for VMs > 1TB +sap_vm_provision_kubevirt_vm_container_memory_overhead: 1 + +# hostname of the ansible controller +sap_vm_provision_kubevirt_vm_ansible_controller: localhost # on AAP, this is localhost + +sap_vm_provision_kubevirt_vm_host_specifications_dictionary: + example_host_specification_plan: + host1: # Hostname, must be 13 characters or less + # SMT-2 (i.e. 2 CPU Threads per CPU Core) is default for Intel CPU Hyper-Threading, optionally can be altered to SMT-1 + kubevirt_vm_cpu_smt: 2 + kubevirt_vm_cpu_cores: 2 + kubevirt_vm_memory_gib: 24 + sap_system_type: project_dev # project_dev, project_tst, project_prd + sap_host_type: hana_primary # hana_primary, hana_secondary, anydb_primary, anydb_secondary, nwas_ascs, nwas_ers, nwas_pas, nwas_aas + # Provide either an existing PVC or a URL for an OS image + os_image: # either url or source_pvc_name have to be provided + # URL for an image to be used + url: "docker://registry.redhat.io/rhel8/rhel-guest-image:8.8.0" + # Name for a PVC to be cloned + # source_pvc_name: "rhel-8.8" + namespace: openshift-virtualization-os-images + size: "50Gi" + network_definition: + - name: sapbridge + type: bridge + networkName: sapbridge-network-definition + model: virtio + storage_definition: + - name: hana + mountpoint: /hana + disk_count: 1 # default: 1 + disk_size: 2048 # size in GB, integer + disk_type: nas # KubeVirt Storage Class + cloudinit: + userData: |- + #cloud-config + timezone: Europe/Berlin + hostname: "{{ scaleout_origin_host_spec }}" + user: {{ sap_vm_provision_kubevirt_vm_os_user if sap_vm_provision_kubevirt_vm_os_user is defined }} + password: {{ sap_vm_provision_kubevirt_vm_os_user_password if sap_vm_provision_kubevirt_vm_os_user_password is defined }} + chpasswd: + expire: false + ssh_authorized_keys: + - "{{ lookup('ansible.builtin.file', sap_vm_provision_ssh_host_public_key_file_path ) }}" + networkData: |- + network: + version: 2 + ethernets: + eth0: + dhcp4: true diff --git a/requirements.yml b/requirements.yml index 03131ca..93073f7 100644 --- a/requirements.yml +++ b/requirements.yml @@ -28,10 +28,14 @@ collections: version: 2.1.0 - name: kubevirt.core type: galaxy - version: 1.1.0 + version: 1.5.0 - name: vmware.vmware_rest type: galaxy version: 3.0.0 - name: cloud.common type: galaxy version: 3.0.0 + # For Red Hat OpenShift + - name: community.okd + type: galaxy + version: 3.0.1 diff --git a/roles/sap_vm_provision/PLATFORM_GUIDANCE.md b/roles/sap_vm_provision/PLATFORM_GUIDANCE.md index 4ae3b8e..15d72e3 100644 --- a/roles/sap_vm_provision/PLATFORM_GUIDANCE.md +++ b/roles/sap_vm_provision/PLATFORM_GUIDANCE.md @@ -99,6 +99,31 @@ See below for the drop-down list of required environment resources on an Infrast +
+Red Hat OpenShift Virtualization (kubevirt_vm) + +- IMPORTANT: The playbook has to run with the environment variable `ANSIBLE_JINJA2_NATIVE=true` otherwise you will see an unmarshalling error when the VM is created. On Ansible Automation Platform Controller (AAPC) you have to set this in Settings --> Job Settings --> Extra Environment Variables, e.g. +``` +{ + "ANSIBLE_JINJA2_NATIVE": "true", + "HOME": "/var/lib/awx" +} +``` + +- Kubeconfig file, kubeadmin user and password for the cluster you want to deploy. Default behavior is to extract CA certificate and API endpoint from kubeconfig (`sap_vm_provision_kubevirt_vm_extract_kubeconfig: true`). Kubeconfig location will be read from `sap_vm_provision_kubevirt_vm_kubeconfig_path` and if that variable is not defined from environment variable `K8S_AUTH_KUBECONFIG`. + +- SSH Key Pair for VMs or provide a password + - `sap_vm_provision_ocp_guest_ssh_auth_mechanism`: Authentication mechanism to be used to connect to the guest. Possible options are: + - `password`: Make sure to set password in `sap_vm_provision_ocp_os_user_password`. + - `private_key`: Use the private ssh key at the location defined by `sap_vm_provision_ssh_host_private_key_file_path`. + - `private_key_data`: use the private ssh key provided in `sap_vm_provision_ssh_host_private_key_data` and write it to the location defined in `sap_vm_provision_ssh_host_private_key_file_path`. + +- Optional: Jumphost with access to OpenShift cluster. + +- Native Kubernetes with KubeVirt has not been tested. + +
+
KubeVirt: diff --git a/roles/sap_vm_provision/README.md b/roles/sap_vm_provision/README.md index b691e9f..63ad4a6 100644 --- a/roles/sap_vm_provision/README.md +++ b/roles/sap_vm_provision/README.md @@ -32,7 +32,8 @@ The code modularity and commonality of provisioning enables a wide gamut of SAP - Microsoft Azure Virtual Machine/s - IBM PowerVM Virtual Machine/s _(formerly LPAR/s)_ - OVirt Virtual Machine/s (e.g. Red Hat Enterprise Linux KVM) -- KubeVirt Virtual Machine/s (e.g. Red Hat OpenShift Virtualization, SUSE Rancher with Harvester HCI) `[Experimental]` +- KubeVirt Virtual Machine/s (e.g. SUSE Rancher with Harvester HCI) `[Experimental]` +- Red Hat OpenShift Virtualization `[Experimental]` - VMware vSphere Virtual Machine/s `[Beta]` ### Known issues @@ -71,6 +72,7 @@ For a list of requirements and recommended authorizations on each Infrastructure - `openstacksdk` for IBM PowerVM - `ovirt-engine-sdk-python` for OVirt - `aiohttp` for VMware + - `kubernetes` for Kubernetes based platforms such as Red Hat OpenShift Virtualization - Ansible - Ansible Core 2.12.0+ - Ansible Collections: @@ -82,10 +84,13 @@ For a list of requirements and recommended authorizations on each Infrastructure - `google.cloud` - `ibm.cloudcollection` - _(legacy, to be replaced with `ibm.cloud` in future)_ - - `kubevirt.core` + - `kubevirt.core` for kubevirt_vm or Red Hat OpenShift Virtualization - `openstack.cloud` - `ovirt.ovirt` - `vmware.vmware_rest` _(requires `cloud.common`)_ + - `community.okd` for Red Hat OpenShift Virtualization + +TODO: Split up above dependencies per platform. ## Execution @@ -182,6 +187,7 @@ Apache 2.0 ## Authors Sean Freeman +Nils Koenig (nkoenig@redhat.com) kubevirt_vm / Red Hat OpenShift Virtualization --- diff --git a/roles/sap_vm_provision/defaults/main.yml b/roles/sap_vm_provision/defaults/main.yml index c995987..1e50ce0 100644 --- a/roles/sap_vm_provision/defaults/main.yml +++ b/roles/sap_vm_provision/defaults/main.yml @@ -10,6 +10,8 @@ sap_vm_provision_iac_type: "" # aws_ec2_vs , gcp_ce_vm , ibmcloud_vs , ibmcloud_powervs , msazure_vm , ibmpowervm_vm , kubevirt_vm , ovirt_vm , vmware_vm sap_vm_provision_iac_platform: "" +# jumphost where ansible playbook will delegate_to +sap_vm_provision_jumphost: "localhost" #### # VM Provision Infrastructure-as-Code (IaC) Configuration - Ansible provisioning - Cloud Hyperscaler @@ -563,23 +565,109 @@ sap_vm_provision_ibmpowervm_vm_host_specifications_dictionary: disk_size: 512 # size in GB, integer -# KubeVirt +####################################################### +# kubevirt / Red Hat OpenShift Virtualization # +####################################################### + +# Namespace where the VM should be created in +sap_vm_provision_kubevirt_vm_target_namespace: sap + +# Username to be created on guest +sap_vm_provision_kubevirt_vm_os_user: cloud-user + +# Password for the above user +sap_vm_provision_kubevirt_vm_os_user_password: "" + +# how to authenticate to the guest vm [password|private_key|private_key_data] +# password: uses provided password in sap_vm_provision_kubevirt_vm_os_user_password, make sure your ssh config allows password authentication +# private_key: use the private ssh key at the location defined by sap_vm_provision_ssh_host_private_key_file_path +# private_key_data: use the private ssh key provided in sap_vm_provision_ssh_host_private_key_data and write it to the location defined in sap_vm_provision_ssh_host_private_key_file_path +sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism: private-key + +# Private SSH key file, must be accessible on the ansible controller +# sap_vm_provision_ssh_host_private_key_file_path: + +# private ssh key, make sure the indentation is correct, here it's two spaces at the beginning of every line +# sap_vm_provision_ssh_host_private_key_data: | +# < your key data> + +# Should the CA cert and the API endpoint be extracted from the kubeconfig file? +sap_vm_provision_kubevirt_vm_extract_kubeconfig: true + +# Should an existing VM be overwritten? +sap_vm_provision_kubevirt_vm_overwrite_vm: false + +# Kubeconfig file for cluster where VMs should be created +sap_vm_provision_kubevirt_vm_kubeconfig_path: /path/to/clusterconfigs/kubeconfig + +# In order to use secured communication, provide the CA cert bundle for the cluster. +# This can be extracted from the kubeconfig file with the following command from the +# kubeconfig file: +# grep certificate-authority-data ${KUBECONFIG} | awk '{ print $2 }' | base64 --decode > cluster-ca-cert.pem +# This variable will not be used if sap_vm_provision_kubevirt_vm_extract_kubeconfig = true +# sap_vm_provision_kubevirt_vm_ca_cert: /path/to/clusterconfigs/cluster-ca-cert.pem + +# API endpoint of the cluster +# This variable will not be used if sap_vm_provision_kubevirt_vm_extract_kubeconfig = true +# sap_vm_provision_kubevirt_api_vm_endpoint: https://api.cluster.domain.tld:6443 + +# Admin username for the cluster communication +sap_vm_provision_kubevirt_vm_admin_username: kubeadmin + +# Password for the above admin user +sap_vm_provision_kubevirt_vm_admin_password: AAAAA-BBBBB-CCCCC-DDDDD + +# RAM Overhead [GiB] for virt-launcher container, this can be small for VMs < 1 TB and without SRIOV but should be increased to 16 or more for VMs > 1TB +sap_vm_provision_kubevirt_vm_container_memory_overhead: 1 + +# hostname of the ansible controller +sap_vm_provision_kubevirt_vm_ansible_controller: localhost # on AAP, this is localhost + sap_vm_provision_kubevirt_vm_host_specifications_dictionary: example_host_specification_plan: host1: # Hostname, must be 13 characters or less # SMT-2 (i.e. 2 CPU Threads per CPU Core) is default for Intel CPU Hyper-Threading, optionally can be altered to SMT-1 kubevirt_vm_cpu_smt: 2 - kubevirt_vm_cpu_threads: 32 - kubevirt_vm_memory_gib: 256 - #sap_system_type: project_dev # project_dev, project_tst, project_prd + kubevirt_vm_cpu_cores: 2 + kubevirt_vm_memory_gib: 24 + sap_system_type: project_dev # project_dev, project_tst, project_prd sap_host_type: hana_primary # hana_primary, hana_secondary, anydb_primary, anydb_secondary, nwas_ascs, nwas_ers, nwas_pas, nwas_aas + # Provide either an existing PVC or a URL for an OS image + os_image: # either url or source_pvc_name have to be provided + # URL for an image to be used + url: "docker://registry.redhat.io/rhel8/rhel-guest-image:8.8.0" + # Name for a PVC to be cloned + # source_pvc_name: "rhel-8.8" + namespace: openshift-virtualization-os-images + size: "50Gi" + network_definition: + - name: sapbridge + type: bridge + networkName: sapbridge-network-definition + model: virtio storage_definition: - - name: data_0 - mountpoint: /data0 + - name: hana + mountpoint: /hana disk_count: 1 # default: 1 - disk_size: 512 # size in GB, integer - disk_type: nas # KubeVirt Storage Clas - + disk_size: 2048 # size in GB, integer + disk_type: nas # KubeVirt Storage Class + cloudinit: + userData: |- + #cloud-config + timezone: Europe/Berlin + hostname: "{{ scaleout_origin_host_spec }}" + user: {{ sap_vm_provision_kubevirt_vm_os_user if sap_vm_provision_kubevirt_vm_os_user is defined }} + password: {{ sap_vm_provision_kubevirt_vm_os_user_password if sap_vm_provision_kubevirt_vm_os_user_password is defined }} + chpasswd: + expire: false + ssh_authorized_keys: + - "{{ lookup('ansible.builtin.file', sap_vm_provision_ssh_host_public_key_file_path ) }}" + networkData: |- + network: + version: 2 + ethernets: + eth0: + dhcp4: true # OVirt sap_vm_provision_ovirt_vm_boot_menu: false diff --git a/roles/sap_vm_provision/tasks/main.yml b/roles/sap_vm_provision/tasks/main.yml index 0db4d41..7a1d42f 100644 --- a/roles/sap_vm_provision/tasks/main.yml +++ b/roles/sap_vm_provision/tasks/main.yml @@ -3,7 +3,7 @@ #### Provision host/s for Deployment of SAP Software (as part of an SAP Software Solution Scenario e.g. SAP S/4HANA Distributed HA) #### - name: Begin execution - delegate_to: localhost + delegate_to: "{{ sap_vm_provision_jumphost }}" delegate_facts: false # keep facts with the original play hosts, not the delegated host block: @@ -15,7 +15,7 @@ #### Post Deployment of SAP - tasks for GCP, IBM Cloud, MS Azure #### - name: Begin execution - delegate_to: localhost + delegate_to: "{{ sap_vm_provision_jumphost }}" delegate_facts: false # keep facts with the original play hosts, not the delegated host block: diff --git a/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_main.yml b/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_main.yml index 581073c..b3f8129 100644 --- a/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_main.yml +++ b/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_main.yml @@ -1,4 +1,10 @@ --- +- name: Fail if sap_vm_provision_kubevirt_vm_os_user_password is not set and sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism is set to password + ansible.builtin.fail: + msg: Password is not allowed to be empty or undefined (sap_vm_provision_kubevirt_vm_os_user_password). + when: + - sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism == "password" + - sap_vm_provision_kubevirt_vm_os_user_password == "" or sap_vm_provision_kubevirt_vm_os_user_password == null - name: Ansible Task block for looped provisioning of KubeVirt Virtual Machines any_errors_fatal: true @@ -11,38 +17,6 @@ ansible.builtin.set_fact: register_provisioned_host_all: [] - - name: Set fact for auth - defaults - ansible.builtin.set_fact: - api_version: "kubevirt.io/v1" - validate_certs: "{{ default(lookup('env', 'K8S_AUTH_VERIFY_SSL')) | default(false) }}" - persist_config: "{{ default(lookup('env', 'K8S_AUTH_PERSIST_CONFIG')) | default(true) }}" - host: "{{ sap_vm_provision_kubevirt_cluster_url | default(lookup('env', 'K8S_AUTH_HOST')) | default(omit) }}" # Target Hypervisor Node - - - name: Set fact for auth - Kubeconfig - no_log: "{{ __sap_vm_provision_no_log }}" - ansible.builtin.set_fact: - kubeconfig: "{{ sap_vm_provision_kubevirt_kubeconfig_path | default(lookup('env', 'K8S_AUTH_KUBECONFIG')) | default(lookup('env', 'KUBECONFIG')) | default(omit) }}" - - - name: Set fact for auth - API Key - no_log: "{{ __sap_vm_provision_no_log }}" - ansible.builtin.set_fact: - api_key: "{{ sap_vm_provision_kubevirt_api_key | default(lookup('env', 'K8S_AUTH_API_KEY')) | default(omit) }}" - when: kubeconfig is defined - - - name: Set fact for auth - Username and Passwords - no_log: "{{ __sap_vm_provision_no_log }}" - ansible.builtin.set_fact: - username: "{{ sap_vm_provision_kubevirt_username | default(lookup('env', 'K8S_AUTH_USERNAME')) | default(omit) }}" - password: "{{ sap_vm_provision_kubevirt_username | default(lookup('env', 'K8S_AUTH_PASSWORD')) | default(omit) }}" - - # - name: Set fact for auth - Alternative - # no_log: "{{ __sap_vm_provision_no_log }}" - # ansible.builtin.set_fact: - # ca_cert: "{{ default(lookup('env', 'K8S_AUTH_SSL_CA_CERT')) | default(omit) }}" - # client_cert: "{{ default(lookup('env', 'K8S_AUTH_CERT_FILE')) | default(omit) }}" - # client_key: "{{ default(lookup('env', 'K8S_AUTH_KEY_FILE')) | default(omit) }}" - # context: "{{ default(lookup('env', 'K8S_AUTH_CONTEXT')) | default(omit) }}" - - name: Provision hosts to KubeVirt register: __sap_vm_provision_task_provision_host_all_run ansible.builtin.include_tasks: @@ -53,31 +27,25 @@ ansible.builtin.add_host: name: "{{ add_item[0].host_node }}" groups: "{{ add_item[0].sap_system_type + '_' if (add_item[0].sap_system_type != '') }}{{ add_item[0].sap_host_type }}" - ansible_host: "{{ add_item[0].reported_devices[0].ips[0].address }}" - ansible_user: "root" - ansible_ssh_private_key_file: "{{ sap_vm_provision_ssh_host_private_key_file_path }}" - ansible_ssh_common_args: -o ConnectTimeout=180 -o ControlMaster=auto -o ControlPersist=3600s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ForwardX11=no - loop: "{{ ansible_play_hosts | map('extract', hostvars, 'register_provisioned_host_all') }}" + ansible_host: "{{ add_item[0].provisioned_private_ip }}" + ansible_user: "{{ sap_vm_provision_kubevirt_vm_os_user }}" + loop: "{{ ansible_play_hosts | map('extract', hostvars, 'register_provisioned_host_all') }}" loop_control: label: "{{ add_item[0].host_node }}" loop_var: add_item - # Cannot override any variables from extravars input, see https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#understanding-variable-precedence # Ensure no default value exists for any prompted variable before execution of Ansible Playbook - name: Set fact to hold all inventory hosts in all groups ansible.builtin.set_fact: - groups_merged_list: "{{ [ [ groups['hana_primary'] | default([]) ] , [ groups['hana_secondary'] | default([]) ] , [ groups['anydb_primary'] | default([]) ] , [ groups['anydb_secondary'] | default([]) ] , [ groups['nwas_ascs'] | default([]) ] , [ groups['nwas_ers'] | default([]) ] , [ groups['nwas_pas'] | default([]) ] , [ groups['nwas_aas'] | default([]) ] ] | flatten | select() }}" + groups_merged_list: "{{ [ [ groups['hana_primary'] | default([]) ] , [ groups['hana_secondary'] | default([]) ] , [ groups['nwas_ascs'] | default([]) ] , [ groups['nwas_ers'] | default([]) ] , [ groups['nwas_pas'] | default([]) ] , [ groups['nwas_aas'] | default([]) ] ] | flatten | select() }}" - name: Set Ansible Vars register: __sap_vm_provision_task_ansible_vars_set ansible.builtin.include_tasks: file: common/set_ansible_vars.yml - # - ansible.builtin.debug: - # var: __sap_vm_provision_task_provision_host_all_add.results - rescue: # This requires no_log set on each Ansible Task, and not set on the Ansible Task Block # This requires an Ansible Task Block containing the Ansible Tasks for calling @@ -100,9 +68,25 @@ - not lookup('ansible.builtin.vars', loop_item, default='') is skipped - lookup('ansible.builtin.vars', loop_item, default='') is failed +- name: Write private ssh key to ansible_controller + delegate_to: "{{ sap_vm_provision_kubevirt_vm_ansible_controller }}" + no_log: true + ansible.builtin.copy: + dest: "{{ sap_vm_provision_ssh_host_private_key_file_path }}" + content: "{{ sap_vm_provision_ssh_host_private_key_data }}" + mode: "0600" + when: sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism == "private_key_data" - name: Ansible Task block to execute on target inventory hosts + remote_user: "{{ sap_vm_provision_kubevirt_vm_os_user }}" + become: true + become_user: root delegate_to: "{{ inventory_hostname }}" + vars: + ansible_password: "{{ sap_vm_provision_kubevirt_vm_os_user_password }}" + ansible_ssh_private_key_file: "{{ sap_vm_provision_ssh_host_private_key_file_path }}" + ansible_ssh_common_args: "-o ConnectTimeout=180 -o ControlMaster=auto -o ControlPersist=3600s -o UserKnownHostsFile=/dev/null -o ForwardX11=no -o ProxyJump={{ __sap_vm_provision_kubevirt_vm_register_jumphost_user }}@{{ sap_vm_provision_jumphost }}" + block: # Required to collect the remote host's facts for further processing @@ -148,3 +132,13 @@ - name: Register Package Repositories ansible.builtin.include_tasks: file: common/register_os.yml + + always: + + - name: Delete private ssh key from ansible_controller + delegate_to: "{{ sap_vm_provision_register_ansible_controller }}" + become: no + ansible.builtin.file: + path: "{{ sap_vm_provision_ssh_host_private_key_file_path }}" + state: absent + when: sap_vm_provision_kubevirt_vm_guest_ssh_auth_mechanism == "private_key_data" diff --git a/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_provision.yml b/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_provision.yml index 165c60c..6ee2920 100644 --- a/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_provision.yml +++ b/roles/sap_vm_provision/tasks/platform_ansible/kubevirt_vm/execute_provision.yml @@ -1,6 +1,4 @@ --- -# The tasks in this file are executed in a loop over the defined hosts - - name: Ensure short hostname is not longer than 13 characters (see SAP Note 611361) ansible.builtin.assert: that: (inventory_hostname | length | int) <= (13 | int) @@ -11,32 +9,35 @@ ansible.builtin.set_fact: scaleout_origin_host_spec: "{{ inventory_hostname | regex_replace('^(.+?)\\d*$', '\\1') }}" when: - - sap_vm_provision_calculate_sap_hana_scaleout_active_coordinator is defined - - not inventory_hostname in lookup('ansible.builtin.vars', 'sap_vm_provision_' + sap_vm_provision_iac_platform + '_host_specifications_dictionary')[sap_vm_provision_host_specification_plan].keys() + - sap_hana_scaleout_active_coordinator is defined + - not item in lookup('ansible.builtin.vars', 'sap_vm_provision_' + sap_vm_provision_iac_platform + '_host_specifications_dictionary')[sap_vm_provision_host_specification_plan].keys() -- name: Set fact for host specifications of the provision target +- name: Set fact for VM name ansible.builtin.set_fact: - target_provision_host_spec: "{{ lookup('ansible.builtin.vars', 'sap_vm_provision_' + sap_vm_provision_iac_platform + '_host_specifications_dictionary')[sap_vm_provision_host_specification_plan][scaleout_origin_host_spec | default(inventory_hostname)] }}" + __sap_vm_provision_register_vm_name: "{{ inventory_hostname }}" +- name: Set fact for VM config + ansible.builtin.set_fact: + __sap_vm_provision_register_vm_config: "{{ (lookup('ansible.builtin.vars', 'sap_vm_provision_' + sap_vm_provision_iac_platform + '_host_specifications_dictionary')[sap_vm_provision_host_specification_plan][__sap_vm_provision_register_vm_name]) }}" -- name: Set fact for downloaded OS Image +- name: Set fact for download OS Image ansible.builtin.set_fact: - os_image_downloaded: |- + os_image: |- {%- set disks_map = [ { - 'metadata': { 'name': (inventory_hostname + '-boot' | replace('_', '-')) }, + 'metadata': { 'name': (__sap_vm_provision_register_vm_name + '-boot' | replace('_', '-')) }, 'spec' : { 'source' : { 'registry' : { - 'url': sap_vm_provision_kubevirt_vm_host_os_image_url, + 'url': __sap_vm_provision_register_vm_config.os_image.url, 'pullMethod': 'node' }, }, 'storage' : { - 'accessModes': ['ReadWriteOnce'], + 'accessModes': ['ReadWriteMany'], 'resources': { 'requests': { - 'storage': '50Gi' + 'storage': __sap_vm_provision_register_vm_config.os_image.size } } } @@ -44,46 +45,45 @@ } ] -%} {{ disks_map }} + when: __sap_vm_provision_register_vm_config.os_image.url is defined -# - name: Set fact for existing OS Image -# ansible.builtin.set_fact: -# os_image_existing: | -# {%- set disks_map = [ -# { -# 'metadata': { 'name': (inventory_hostname + '-boot' | replace('_', '-')) }, -# 'spec' : { -# 'source' : { -# 'pvc' : { -# 'name': (inventory_hostname + '-boot' | replace('_', '-')), -# 'namespace': sap_vm_provision_kubevirt_target_namespace -# }, -# }, -# 'storage' : { -# 'accessModes': ['ReadWriteOnce'], -# 'resources': { -# 'requests': { -# 'storage': '25Gi' -# } -# } -# } -# } -# } -# ] -%} -# {{ disks_map }} - +- name: Set fact for existing OS Image + ansible.builtin.set_fact: + os_image: | + {%- set disks_map = [ + { + 'metadata': { 'name': (__sap_vm_provision_register_vm_name + '-boot' | replace('_', '-')) }, + 'spec' : { + 'source' : { + 'pvc' : { + 'name': __sap_vm_provision_register_vm_config.os_image.source_pvc_name, + 'namespace': __sap_vm_provision_register_vm_config.os_image.namespace + }, + }, + 'storage' : { + 'accessModes': ['ReadWriteMany'], + 'resources': { + 'requests': { + 'storage': __sap_vm_provision_register_vm_config.os_image.size + } + } + } + } + } + ] -%} + {{ disks_map }} + when: + - __sap_vm_provision_register_vm_config.os_image.source_pvc_name is defined + - __sap_vm_provision_register_vm_config.os_image.namespace is defined - name: Set fact for storage volume template map ansible.builtin.set_fact: storage_disks_map: |- {% set disks_map = [] -%} - {% for storage_item in target_provision_host_spec.storage_definition -%} - {% for idx in range(0, storage_item.disk_count | default(1)) -%} - {% if (storage_item.filesystem_type is defined) -%} - {% if ('swap' in storage_item.filesystem_type and storage_item.swap_path is not defined) - or ('swap' not in storage_item.filesystem_type and storage_item.nfs_path is not defined) -%} + {% for storage_item in __sap_vm_provision_register_vm_config.storage_definition -%} {% set vol = disks_map.extend([ { - 'metadata': { 'name': (inventory_hostname + '-' + storage_item.name + (idx | string) | replace('_', '-')) }, + 'metadata': { 'name': (__sap_vm_provision_register_vm_name + '-' + storage_item.name | replace('_', '-')) }, 'spec' : { 'source' : { 'blank' : {} @@ -99,18 +99,36 @@ } } }]) %} - {%- endif %} - {%- endif %} - {%- endfor %} {%- endfor %} {{ disks_map }} +- name: Set fact for storage volumes attachment list + ansible.builtin.set_fact: + storage_disk_name_list: |- + {% set disks_simple_map = [] -%} + {% for list_item in os_image -%} + {% set vol = disks_simple_map.extend([ + { + 'name': list_item.metadata.name, + 'dataVolume': { 'name': list_item.metadata.name }, + } + ]) %} + {%- endfor %} + {% for list_item in storage_disks_map -%} + {% set vol = disks_simple_map.extend([ + { + 'name': list_item.metadata.name, + 'dataVolume': { 'name': list_item.metadata.name }, + } + ]) %} + {%- endfor %} + {{ disks_simple_map }} - name: Set fact for storage volumes attachment list ansible.builtin.set_fact: storage_disk_name_list: |- {% set disks_simple_map = [] -%} - {% for list_item in os_image_downloaded -%} + {% for list_item in os_image -%} {% set vol = disks_simple_map.extend([ { 'name': list_item.metadata.name, @@ -128,88 +146,104 @@ {%- endfor %} {{ disks_simple_map }} +- name: Set fact for disk list + ansible.builtin.set_fact: + storage_disk_list: |- + {% set disks_list_simple = [] -%} + {% set vol = disks_list_simple.extend([ + { + 'name': __sap_vm_provision_register_vm_name + '-boot' | replace('_', '-'), + 'bootOrder': 1, + 'disk': { + 'bus': 'virtio', + 'io': 'native' + } + }, + { + 'name': 'cloudinit', + 'io': 'native', + 'disk': { + 'bus': 'virtio' + } + }, + ]) %} + {% for list_item in storage_disks_map -%} + {% set vol = disks_list_simple.extend([ + { + 'name': list_item.metadata.name, + 'io': 'native', + 'disk': { + 'bus': 'virtio' + } + } + ]) %} + {%- endfor %} + {{ disks_list_simple }} + - name: Set fact for cloud-init volume ansible.builtin.set_fact: cloud_init_volume: - name: cloudinit - cloudInitNoCloud: - userData: |- - #cloud-config - hostname: "{{ inventory_hostname_short }}" - "{{ 'user: ' + sap_vm_provision_kubevirt_os_user if sap_vm_provision_kubevirt_os_user is defined }}" - "{{ 'password: ' + sap_vm_provision_kubevirt_os_user_password if sap_vm_provision_kubevirt_os_user_password is defined }}" - chpasswd: - expire: false - ssh_authorized_keys: - - "{{ lookup('ansible.builtin.file', sap_vm_provision_ssh_host_public_key_file_path ) }}" - network: - version: 2 - ethernets: - eth0: - dhcp4: true + disk: + bus: virtio + cloudInitNoCloud: "{{ __sap_vm_provision_register_vm_config.cloudinit }}" +- name: Set fact for network interfaces + ansible.builtin.set_fact: + __sap_vm_provision_register_network_interfaces: |- + {% set netifs = [] -%} + {% for list_item in __sap_vm_provision_register_vm_config.network_definition -%} + {% set ifs = netifs.extend([ + { + list_item.type: {}, + 'model': list_item.model, + 'name': list_item.name, + } + ]) %} + {%- endfor %} + {{ netifs }} -- name: Provision KubeVirt Virtual Machine - no_log: "{{ __sap_vm_provision_no_log }}" - register: __sap_vm_provision_task_provision_host_single - kubevirt.core.kubevirt_vm: - - ## Hypervisor Control Plane definition and credentials - api_version: "{{ api_version | default(omit) }}" - validate_certs: "{{ validate_certs | default(omit) }}" - persist_config: "{{ persist_config | default(omit) }}" - host: "{{ host | default(omit) }}" # Target Hypervisor Node - - kubeconfig: "{{ kubeconfig | default(omit) }}" - api_key: "{{ api_key | default(omit) }}" - username: "{{ username | default(omit) }}" - password: "{{ password | default(omit) }}" - - # ca_cert: "{{ ca_cert | default(omit) }}" - # client_cert: "{{ client_cert | default(omit) }}" - # client_key: "{{ client_key | default(omit) }}" - # context: "{{ context | default(omit) }}" - - ## Virtual Machine target Hypervisor definition - namespace: "{{ sap_vm_provision_kubevirt_target_namespace }}" # Target namespace - - ## Virtual Machine definition - state: present - running: true - wait: true # ensure Virtual Machine in ready state before exiting Ansible Task - wait_sleep: 30 # 30 second poll for ready state - wait_timeout: 600 # 10 minute wait for ready state - force: false # Do not replace existing Virtual Machine with same name - name: "{{ inventory_hostname }}" - labels: - app: "{{ inventory_hostname }}" - - # Virtual Disk volume definitions - data_volume_templates: "{{ storage_disks_map }}" +- name: Set fact for networks definition + ansible.builtin.set_fact: + __sap_vm_provision_register_networks_definition: |- + {% set networks = [] -%} + {% for list_item in __sap_vm_provision_register_vm_config.network_definition -%} + {% set ifs = networks.extend([ + { + 'name': list_item.name, + 'multus': { 'networkName': list_item.networkName} + } + ]) %} + {%- endfor %} + {{ networks }} - # Virtual Machine configuration - #preference: - # name: fedora # OS Image, not used when data volume template and spec contains volume using registry OS Image - #instancetype: - # name: u1.medium # VM Template Size, not used when spec contains cpu and memory configuration - spec: +- name: Set fact for VM deploy config + ansible.builtin.set_fact: + __sap_vm_provision_register_vm_deploy_config: + volumes: "{{ storage_disk_name_list + cloud_init_volume }}" + networks: "{{ __sap_vm_provision_register_networks_definition }}" domain: - ioThreadsPolicy: auto - hostname: "{{ item }}" + # shared | auto, auto prevents live migration + ioThreadsPolicy: shared + hostname: "{{ __sap_vm_provision_register_vm_name }}" evictionStrategy: LiveMigrate terminationGracePeriodSeconds: 1800 # 30 minutes after stop request before VM is force terminated - resources: requests: - memory: "{{ (target_provision_host_spec.kubevirt_vm_memory_gib) + 16 }}Gi" # Static 16GB DRAM overhead for container runtime + memory: "{{ __sap_vm_provision_register_vm_config.kubevirt_vm_memory_gib + sap_vm_provision_kubevirt_vm_container_memory_overhead }}Gi" # memory + overhead for container runtime) - devices: {} + devices: + downwardMetrics: {} + networkInterfaceMultiqueue: true + blockMultiQueue: true + autoattachMemBalloon: false + disks: "{{ storage_disk_list }}" + interfaces: "{{ __sap_vm_provision_register_network_interfaces }}" cpu: - cores: "{{ (target_provision_host_spec.kubevirt_vm_cpu_threads) / kubevirt_vm_cpu_smt }}" - threads: "{{ target_provision_host_spec.kubevirt_vm_cpu_threads }}" + cores: "{{ __sap_vm_provision_register_vm_config.kubevirt_vm_cpu_cores }}" + threads: "{{ __sap_vm_provision_register_vm_config.kubevirt_vm_cpu_smt }}" dedicatedCpuPlacement: true - isolateEmulatorThread: true model: host-passthrough numa: guestMappingPassthrough: {} @@ -222,62 +256,57 @@ policy: require memory: - guest: "{{ target_provision_host_spec.kubevirt_vm_memory_gib }}Gi" + guest: "{{ __sap_vm_provision_register_vm_config.kubevirt_vm_memory_gib }}Gi" hugepages: pageSize: 1Gi - networks: - - name: bridge-network-definition - multus: - networkName: iface-bridge-sriov - - name: storage-network-definition - multus: - networkName: iface-storage-sriov - - name: multi-network-definition - multus: - networkName: iface-multi-sriov +- name: Provision KubeVirt Virtual Machine + kubevirt.core.kubevirt_vm: + api_version: "{{ api_version | default(omit) }}" + persist_config: "{{ persist_config | default(omit) }}" - volumes: "{{ storage_disk_name_list + cloud_init_volume }}" + ## Virtual Machine target Hypervisor definition + namespace: "{{ sap_vm_provision_kubevirt_vm_target_namespace }}" # Target namespace + ## Virtual Machine definition + state: present + running: true + wait: true # ensure Virtual Machine in ready state before exiting Ansible Task + wait_sleep: 30 # 30 second poll for ready state + wait_timeout: 600 # 10 minute wait for ready state + force: "{{ sap_vm_provision_overwrite_vm | default(false) }}" # Do not replace existing Virtual Machine with same name + name: "{{ __sap_vm_provision_register_vm_name }}" + labels: + app: "{{ __sap_vm_provision_register_vm_name }}" + # Virtual Disk volume definitions + data_volume_templates: "{{ storage_disks_map + os_image }}" + spec: "{{ __sap_vm_provision_register_vm_deploy_config }}" - name: Check VM status - no_log: "{{ __sap_vm_provision_no_log }}" - register: __sap_vm_provision_task_provision_host_single_info + register: register_provisioned_host_single_info kubevirt.core.kubevirt_vm_info: - name: "{{ inventory_hostname }}" - namespace: "{{ sap_vm_provision_kubevirt_target_namespace }}" - + name: "{{ __sap_vm_provision_register_vm_name }}" + namespace: "{{ sap_vm_provision_kubevirt_vm_target_namespace }}" + +- name: Get VMI details + kubernetes.core.k8s_info: + api_version: kubevirt.io/v1 + kind: VirtualMachineInstance + namespace: "{{ sap_vm_provision_kubevirt_vm_target_namespace }}" + name: "{{ __sap_vm_provision_register_vm_name }}" + register: vmi_info + until: vmi_info.resources[0].status.interfaces[0].ipAddress is defined + retries: 10 + delay: 30 - name: Create fact for delegate host IP ansible.builtin.set_fact: - provisioned_private_ip: "{{ __sap_vm_provision_task_provision_host_single_info.spec.UNKNOWN_VARIABLE_FOR_PRIVATE_IP_HERE }}" - - -- name: Collect only facts about hardware - register: __sap_vm_provision_task_ansible_facts_host_disks_info - ansible.builtin.setup: - gather_subset: - - hardware - remote_user: root - become: true - become_user: root - delegate_to: "{{ provisioned_private_ip }}" - delegate_facts: false - vars: - ansible_ssh_private_key_file: "{{ sap_vm_provision_ssh_host_private_key_file_path }}" - -#- name: Output disks -# ansible.builtin.debug: -# var: hostvars[inventory_hostname].ansible_devices.keys() | list - -#- name: Debug Ansible Facts devices used list -# ansible.builtin.debug: -# msg: "{{ __sap_vm_provision_task_ansible_facts_host_disks_info.ansible_facts.ansible_device_links.ids.keys() | list }}" - + provisioned_private_ip: "{{ vmi_info.resources[0].status.interfaces[0].ipAddress }}" +# How should this datastructure look like? when just using the provisioned_private_ip there is no need to use combine (would need two dicts). Using VMI info for now - name: Append loop value to register ansible.builtin.set_fact: - __sap_vm_provision_task_provision_host_single: "{{ __sap_vm_provision_task_provision_host_single_info.spec.UKNOWN_VARIABLE_HERE | combine( { 'host_node' : inventory_hostname } , { 'sap_host_type' : target_provision_host_spec.sap_host_type } , { 'sap_system_type' : (target_provision_host_spec.sap_system_type | default('')) } ) }}" + __sap_vm_provision_task_provision_host_single: "{{ vmi_info | combine( { 'host_node' : inventory_hostname } , { 'sap_host_type' : __sap_vm_provision_register_vm_config.sap_host_type } , { 'sap_system_type' : (__sap_vm_provision_register_vm_config.sap_system_type | default('')) }, {'provisioned_private_ip': provisioned_private_ip } ) }}" - name: Append output to merged register ansible.builtin.set_fact: