From 99a0cae4f326b2a8db80a2b90e003843646192bb Mon Sep 17 00:00:00 2001 From: rut31337 Date: Wed, 1 Jul 2026 15:01:29 -0500 Subject: [PATCH 1/7] feat: add OCP agent-based and image-based installer roles - host_ocp4_agent_installer: Agent-Based Installer for disconnected OCP on Troshka - DNS forwarder extra manifest for registry resolution - Redfish virtual media boot via sushy-emulator - Pull-through registry support for oc-mirror - host_ocp4_ibi_installer: Image-Based Install for SNO rapid deployment - Seed image restore via lca-cli - Recert for cluster identity rename --- .../defaults/main.yml | 53 +++ .../tasks/boot_via_bmc.yml | 42 ++ .../tasks/eject_iso.yml | 26 ++ .../host_ocp4_agent_installer/tasks/main.yml | 325 ++++++++++++++++ .../templates/agent-config.yaml.j2 | 39 ++ .../templates/install-config.yaml.j2 | 60 +++ .../host_ocp4_ibi_installer/defaults/main.yml | 59 +++ roles/host_ocp4_ibi_installer/tasks/main.yml | 363 ++++++++++++++++++ .../image-based-installation-config.yaml.j2 | 31 ++ 9 files changed, 998 insertions(+) create mode 100644 roles/host_ocp4_agent_installer/defaults/main.yml create mode 100644 roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml create mode 100644 roles/host_ocp4_agent_installer/tasks/eject_iso.yml create mode 100644 roles/host_ocp4_agent_installer/tasks/main.yml create mode 100644 roles/host_ocp4_agent_installer/templates/agent-config.yaml.j2 create mode 100644 roles/host_ocp4_agent_installer/templates/install-config.yaml.j2 create mode 100644 roles/host_ocp4_ibi_installer/defaults/main.yml create mode 100644 roles/host_ocp4_ibi_installer/tasks/main.yml create mode 100644 roles/host_ocp4_ibi_installer/templates/image-based-installation-config.yaml.j2 diff --git a/roles/host_ocp4_agent_installer/defaults/main.yml b/roles/host_ocp4_agent_installer/defaults/main.yml new file mode 100644 index 0000000..fd71b3c --- /dev/null +++ b/roles/host_ocp4_agent_installer/defaults/main.yml @@ -0,0 +1,53 @@ +--- +# OCP Agent-Based Installer role defaults + +# OCP version — major.minor or major.minor.patch +host_ocp4_installer_version: "4.20" + +# Mirror URL for downloading openshift-install and oc +host_ocp4_installer_root_url: https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp + +# Cluster config +host_ocp4_cluster_name: "{{ ocp.cluster_name | default('ocp') }}" +host_ocp4_base_domain: "{{ ocp.base_domain | default('ocp.local') }}" +host_ocp4_api_vip: "{{ ocp.api_vip }}" +host_ocp4_ingress_vip: "{{ ocp.ingress_vip }}" + +# Pull secret — JSON string +host_ocp4_pull_secret: "{{ pull_secret | default('{}') }}" + +# SSH public key for core user access +host_ocp4_ssh_key: "{{ ssh_pub_key | default('') }}" + +# BMC credentials for Redfish virtual media boot +host_ocp4_bmc_user: admin +host_ocp4_bmc_password: "{{ common_password }}" + +# BMC IPs to boot — list of IPs, derived from topology if not set +host_ocp4_bmc_ips: [] + +# Cluster network CIDRs +host_ocp4_machine_network_cidr: "{{ networks.cluster.cidr | default('10.0.0.0/24') }}" +host_ocp4_bmc_cidr: "{{ networks.bmc.cidr | default('192.168.50.0/24') }}" +host_ocp4_cluster_network_cidr: 10.128.0.0/14 +host_ocp4_service_network_cidr: 172.30.0.0/16 + +# Install dir on bastion +host_ocp4_install_dir: "/home/{{ ansible_user }}/ocp-install" + +# HTTP server port for serving agent ISO +host_ocp4_iso_http_port: 8080 + +# Timeout for cluster install (seconds) +host_ocp4_install_timeout: 5400 + +# Additional trust bundle (registry CA cert) — set by disconnected_registry role +host_ocp4_additional_trust_bundle: "" + +# RHCOS images to download and serve via HTTP for assisted-image-service +host_ocp4_rhcos_images: [] + +# NTP sources +host_ocp4_ntp_sources: + - clock.redhat.com + - pool.ntp.org diff --git a/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml b/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml new file mode 100644 index 0000000..cc548dc --- /dev/null +++ b/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml @@ -0,0 +1,42 @@ +--- +# Boot a single node via Redfish virtual media +# Called in a loop with _bmc_ip as the loop var + +- name: Get Redfish system ID for {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + return_content: true + register: _redfish_systems + +- name: Extract system ID + ansible.builtin.set_fact: + _sys_id: "{{ _redfish_systems.json.Members[0]['@odata.id'].split('/')[-1] }}" + +- name: Insert virtual media ISO on {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: + Image: "http://{{ _bastion_ip }}:{{ host_ocp4_iso_http_port }}/agent.x86_64.iso" + Inserted: true + WriteProtected: true + status_code: [200, 204] + +- name: Reboot node {{ _bmc_ip }} from ISO + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/Actions/ComputerSystem.Reset" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: + ResetType: ForceRestart + status_code: [200, 204] diff --git a/roles/host_ocp4_agent_installer/tasks/eject_iso.yml b/roles/host_ocp4_agent_installer/tasks/eject_iso.yml new file mode 100644 index 0000000..22be20a --- /dev/null +++ b/roles/host_ocp4_agent_installer/tasks/eject_iso.yml @@ -0,0 +1,26 @@ +--- +# Eject virtual media from a BMC node after install +# Called in a loop with _bmc_ip as the loop var + +- name: Get Redfish system ID for {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + return_content: true + register: _redfish_systems_eject + ignore_errors: true + +- name: Eject virtual media on {{ _bmc_ip }} + when: _redfish_systems_eject is success + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _redfish_systems_eject.json.Members[0]['@odata.id'].split('/')[-1] }}/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: {} + status_code: [200, 204] + ignore_errors: true diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml new file mode 100644 index 0000000..40df6fb --- /dev/null +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -0,0 +1,325 @@ +--- +# OCP Agent-Based Installer — runs on bastion, boots nodes via Redfish BMC + +- name: Check if cluster already installed + ansible.builtin.command: + cmd: oc --kubeconfig={{ host_ocp4_install_dir }}/auth/kubeconfig cluster-info + register: _ocp_cluster_info + failed_when: false + changed_when: false + +- name: Install OCP cluster + when: _ocp_cluster_info.rc != 0 + block: + - name: Determine OCP version URL path + ansible.builtin.set_fact: + _ocp_version_path: >- + {{ (host_ocp4_installer_version | regex_search('^\d+\.\d+\.\d+$')) + | ternary(host_ocp4_installer_version, 'stable-' + host_ocp4_installer_version) }} + + - name: Determine OCP version URLs + ansible.builtin.set_fact: + _ocp_install_url: >- + {{ host_ocp4_installer_root_url }}/{{ _ocp_version_path }}/openshift-install-linux.tar.gz + _ocp_client_url: >- + {{ host_ocp4_installer_root_url }}/{{ _ocp_version_path }}/openshift-client-linux.tar.gz + + - name: Download openshift-install + ansible.builtin.unarchive: + src: "{{ _ocp_install_url }}" + dest: /usr/bin + remote_src: true + creates: /usr/bin/openshift-install + retries: 3 + delay: 10 + + - name: Download oc client + ansible.builtin.unarchive: + src: "{{ _ocp_client_url }}" + dest: /usr/bin + remote_src: true + creates: /usr/bin/oc + retries: 3 + delay: 10 + + - name: Compute cluster topology facts + ansible.builtin.set_fact: + _cp_count: "{{ vms | dict2items | selectattr('value.role', 'eq', 'control-plane') | list | length }}" + _worker_count: 0 + _machine_net: "{{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('network/prefix') }}" + + - name: Compute network facts + ansible.builtin.set_fact: + _prefix_len: "{{ _machine_net.split('/')[1] }}" + _gateway_ip: "{{ _machine_net | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }}" + _rendezvous_ip: "{{ host_ocp4_api_vip }}" + + - name: Get deployed topology from Troshka + troshka.cloud.project_info: + api_url: "{{ troshka_api_url }}" + api_key: "{{ troshka_api_key }}" + project_id: "{{ troshka_project_id }}" + register: _project_topo + delegate_to: localhost + become: false + + - name: Build BMC host list from deployed topology + ansible.builtin.set_fact: + _bmc_hosts: >- + {{ _bmc_hosts | default([]) + [{ + 'name': item.data.name, + 'role': 'master' if item.data.tags.AnsibleGroup | default('') == 'controllers' else 'worker', + 'ip': item.data.nics[0].ip, + 'mac': item.data.nics[0].mac, + 'bmc_ip': item.data.bmcIp | default('') + }] }} + loop: >- + {{ _project_topo.topology.nodes + | selectattr('type', 'equalto', 'vmNode') + | selectattr('data.tags.AnsibleGroup', 'defined') + | selectattr('data.tags.AnsibleGroup', 'in', ['controllers', 'workers']) + | list }} + loop_control: + label: "{{ item.data.name }}" + + - name: Build BMC IP list + ansible.builtin.set_fact: + _bmc_ip_list: "{{ host_ocp4_bmc_ips if host_ocp4_bmc_ips | length > 0 else _bmc_hosts | map(attribute='bmc_ip') | select('ne', '') | list }}" + + - name: Create install directory + ansible.builtin.file: + path: "{{ host_ocp4_install_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + + - name: Merge registry credentials into pull secret + ansible.builtin.set_fact: + host_ocp4_pull_secret: >- + {{ host_ocp4_pull_secret | from_json + | combine({'auths': (host_ocp4_pull_secret | from_json).get('auths', {}) + | combine({disconnected_registry_hostname + ':' + (disconnected_registry_port | default(8443) | string): + {'auth': (disconnected_registry_user | default('admin') + ':' + disconnected_registry_password) | b64encode}})}, + recursive=true) | to_json }} + when: disconnected_registry_hostname is defined + + - name: Generate install-config.yaml + ansible.builtin.template: + src: install-config.yaml.j2 + dest: "{{ host_ocp4_install_dir }}/install-config.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + + - name: Backup install-config.yaml + ansible.builtin.copy: + src: "{{ host_ocp4_install_dir }}/install-config.yaml" + dest: "{{ host_ocp4_install_dir }}/install-config.yaml.bak" + remote_src: true + owner: "{{ ansible_user }}" + + - name: Generate agent-config.yaml + ansible.builtin.template: + src: agent-config.yaml.j2 + dest: "{{ host_ocp4_install_dir }}/agent-config.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + + - name: Backup agent-config.yaml + ansible.builtin.copy: + src: "{{ host_ocp4_install_dir }}/agent-config.yaml" + dest: "{{ host_ocp4_install_dir }}/agent-config.yaml.bak" + remote_src: true + owner: "{{ ansible_user }}" + + - name: Inject DNS forwarder for disconnected registry resolution + when: disconnected_registry_hostname is defined + block: + - name: Ensure openshift manifests directory exists + ansible.builtin.file: + path: "{{ host_ocp4_install_dir }}/openshift" + state: directory + owner: "{{ ansible_user }}" + mode: "0755" + + - name: Create DNS forwarder extra manifest for lab domain + ansible.builtin.copy: + dest: "{{ host_ocp4_install_dir }}/openshift/99-dns-forwarder.yaml" + owner: "{{ ansible_user }}" + mode: "0644" + content: | + apiVersion: operator.openshift.io/v1 + kind: DNS + metadata: + name: default + spec: + servers: + - name: lab-forward + zones: + - {{ host_ocp4_base_domain }} + forwardPlugin: + upstreams: + - {{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }} + + - name: Create agent ISO + ansible.builtin.shell: + cmd: >- + openshift-install agent create image --dir {{ host_ocp4_install_dir }} --log-level debug + 2>&1 | tee -a ~{{ ansible_user }}/install.log + become_user: "{{ ansible_user }}" + register: _create_image + changed_when: true + + - name: Download RHCOS images for assisted-image-service + ansible.builtin.get_url: + url: "{{ item }}" + dest: "{{ host_ocp4_install_dir }}/{{ item | basename }}" + mode: "0644" + retries: 3 + delay: 10 + loop: "{{ host_ocp4_rhcos_images }}" + when: host_ocp4_rhcos_images | default([]) | length > 0 + + - name: Open HTTP port for assisted-image-service + ansible.builtin.shell: + cmd: >- + firewall-cmd --add-port={{ host_ocp4_iso_http_port }}/tcp && + firewall-cmd --add-port={{ host_ocp4_iso_http_port }}/tcp --permanent + changed_when: true + failed_when: false + + - name: Start HTTP server for agent ISO and RHCOS images + ansible.builtin.shell: + cmd: >- + cd {{ host_ocp4_install_dir }} && + nohup python3 -m http.server {{ host_ocp4_iso_http_port }} + > /tmp/ocp-iso-http.log 2>&1 & + echo $! + register: _http_pid + changed_when: true + + - name: Get bastion IP on BMC network for ISO serving + ansible.builtin.shell: + cmd: ip -4 -o addr | awk '{print $4}' | cut -d/ -f1 + register: _all_ips + changed_when: false + + - name: Select bastion IP reachable from BMC network + ansible.builtin.set_fact: + _bastion_ip: >- + {{ _all_ips.stdout_lines + | select('match', '^' + _bmc_subnet_prefix) + | first + | default(ansible_facts.default_ipv4.address | default(ansible_host)) }} + vars: + _bmc_subnet_prefix: "{{ (host_ocp4_bmc_cidr | default('192.168.50.0/24')).rsplit('.', 1)[0] }}" + + - name: Find BMC network interface + ansible.builtin.shell: + cmd: ip -4 -o addr | grep '{{ _bastion_ip }}/' | awk '{print $2}' + register: _bmc_iface + changed_when: false + + - name: Allow all traffic on BMC interface + ansible.builtin.shell: + cmd: >- + firewall-cmd --zone=trusted --change-interface={{ _bmc_iface.stdout | trim }} && + firewall-cmd --zone=trusted --change-interface={{ _bmc_iface.stdout | trim }} --permanent + when: _bmc_iface.stdout | trim | length > 0 + changed_when: true + failed_when: false + + - name: Boot nodes via Redfish BMC + ansible.builtin.include_tasks: boot_via_bmc.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + + - name: Wait for OCP install to complete + ansible.builtin.shell: + cmd: >- + openshift-install agent wait-for install-complete + --dir {{ host_ocp4_install_dir }} --log-level debug + 2>&1 | tee -a ~{{ ansible_user }}/install.log + become_user: "{{ ansible_user }}" + register: _install_result + timeout: "{{ host_ocp4_install_timeout }}" + changed_when: true + vars: + troshka_timeout: "{{ host_ocp4_install_timeout | int + 60 }}" + + - name: Eject ISO from BMC nodes + ansible.builtin.include_tasks: eject_iso.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + +- name: Set up kubeconfig + block: + - name: Create .kube directory + ansible.builtin.file: + path: "~{{ item }}/.kube" + state: directory + owner: "{{ item }}" + mode: "0700" + loop: + - "{{ ansible_user }}" + - root + + - name: Copy kubeconfig + ansible.builtin.copy: + src: "{{ host_ocp4_install_dir }}/auth/kubeconfig" + dest: "~{{ item }}/.kube/config" + remote_src: true + owner: "{{ item }}" + mode: "0600" + loop: + - "{{ ansible_user }}" + - root + + - name: Ensure student user exists + ansible.builtin.user: + name: "{{ student_name }}" + password: "{{ common_password | password_hash('sha512') }}" + groups: wheel + append: true + state: present + when: student_name is defined + + - name: Set up student user kubeconfig + when: student_name is defined + block: + - name: Create student .kube directory + ansible.builtin.file: + path: "~{{ student_name }}/.kube" + state: directory + owner: "{{ student_name }}" + mode: "0700" + + - name: Copy kubeconfig to student + ansible.builtin.copy: + src: "{{ host_ocp4_install_dir }}/auth/kubeconfig" + dest: "~{{ student_name }}/.kube/config" + remote_src: true + owner: "{{ student_name }}" + mode: "0600" + + - name: Add KUBECONFIG to bashrc + ansible.builtin.lineinfile: + path: "~{{ ansible_user }}/.bashrc" + line: "export KUBECONFIG={{ host_ocp4_install_dir }}/auth/kubeconfig" + state: present + + - name: Read kubeadmin password + ansible.builtin.slurp: + src: "{{ host_ocp4_install_dir }}/auth/kubeadmin-password" + register: _kubeadmin_pw + + - name: Display cluster info + ansible.builtin.debug: + msg: + - "OCP cluster installed successfully" + - "Console: https://console-openshift-console.apps.{{ host_ocp4_cluster_name }}.{{ host_ocp4_base_domain }}" + - "kubeadmin password: {{ _kubeadmin_pw.content | b64decode }}" diff --git a/roles/host_ocp4_agent_installer/templates/agent-config.yaml.j2 b/roles/host_ocp4_agent_installer/templates/agent-config.yaml.j2 new file mode 100644 index 0000000..a64903d --- /dev/null +++ b/roles/host_ocp4_agent_installer/templates/agent-config.yaml.j2 @@ -0,0 +1,39 @@ +apiVersion: v1beta1 +kind: AgentConfig +metadata: + name: {{ host_ocp4_cluster_name }} +rendezvousIP: {{ _rendezvous_ip }} +additionalNTPSources: +{% for ntp in host_ocp4_ntp_sources %} + - {{ ntp }} +{% endfor %} +hosts: +{% for host in _bmc_hosts %} + - hostname: {{ host.name }} + role: {{ host.role }} + interfaces: + - name: cluster-nic + macAddress: {{ host.mac }} + networkConfig: + interfaces: + - name: cluster-nic + type: ethernet + state: up + identifier: mac-address + mac-address: {{ host.mac }} + ipv4: + enabled: true + address: + - ip: {{ host.ip }} + prefix-length: {{ _prefix_len }} + dhcp: false + dns-resolver: + config: + server: + - {{ _gateway_ip }} + routes: + config: + - destination: 0.0.0.0/0 + next-hop-address: {{ _gateway_ip }} + next-hop-interface: cluster-nic +{% endfor %} diff --git a/roles/host_ocp4_agent_installer/templates/install-config.yaml.j2 b/roles/host_ocp4_agent_installer/templates/install-config.yaml.j2 new file mode 100644 index 0000000..6782ca0 --- /dev/null +++ b/roles/host_ocp4_agent_installer/templates/install-config.yaml.j2 @@ -0,0 +1,60 @@ +apiVersion: v1 +baseDomain: {{ host_ocp4_base_domain }} +metadata: + name: {{ host_ocp4_cluster_name }} +compute: + - name: worker + replicas: {{ _worker_count }} + architecture: amd64 +controlPlane: + name: master + replicas: {{ _cp_count }} + architecture: amd64 +networking: + networkType: OVNKubernetes + clusterNetwork: + - cidr: {{ host_ocp4_cluster_network_cidr }} + hostPrefix: 23 + serviceNetwork: + - {{ host_ocp4_service_network_cidr }} + machineNetwork: + - cidr: {{ host_ocp4_machine_network_cidr }} +{% if _cp_count | int == 1 and _worker_count | int == 0 %} +platform: + none: {} +{% else %} +platform: + baremetal: + apiVIPs: + - {{ host_ocp4_api_vip }} + ingressVIPs: + - {{ host_ocp4_ingress_vip }} + hosts: +{% for host in _bmc_hosts %} + - name: {{ host.name }} + role: {{ host.role }} + bootMACAddress: {{ host.mac }} +{% endfor %} +{% endif %} +{% if host_ocp4_additional_trust_bundle | length > 0 %} +additionalTrustBundle: | +{{ host_ocp4_additional_trust_bundle | indent(2, first=true) }} +{% endif %} +{% if disconnected_registry_hostname is defined %} +{% set _reg = disconnected_registry_hostname + ':' + (disconnected_registry_port | default(8443) | string) %} +imageContentSources: +- mirrors: + - {{ _reg }}/openshift-release-dev/ocp-v4.0-art-dev + - {{ _reg }}/openshift/release + source: quay.io/openshift-release-dev/ocp-v4.0-art-dev +- mirrors: + - {{ _reg }}/openshift-release-dev + source: quay.io/openshift-release-dev +- mirrors: + - {{ _reg }}/openshift/release-images + source: quay.io/openshift-release-dev/ocp-release +{% endif %} +pullSecret: '{{ host_ocp4_pull_secret }}' +{% if host_ocp4_ssh_key | length > 0 %} +sshKey: '{{ host_ocp4_ssh_key }}' +{% endif %} diff --git a/roles/host_ocp4_ibi_installer/defaults/main.yml b/roles/host_ocp4_ibi_installer/defaults/main.yml new file mode 100644 index 0000000..b366baa --- /dev/null +++ b/roles/host_ocp4_ibi_installer/defaults/main.yml @@ -0,0 +1,59 @@ +--- +# OCP IBI (Image-Based Install) role defaults + +# Seed image — OCI container image with captured SNO +host_ocp4_ibi_seed_image: "" +host_ocp4_ibi_seed_version: "" + +# Target disk for RHCOS installation +host_ocp4_ibi_installation_disk: /dev/vda + +# Container storage partition offset from end of disk +host_ocp4_ibi_extra_partition_start: "" + +# OCP version for downloading openshift-install binary +host_ocp4_installer_version: "4.22" + +# Mirror URL for downloading openshift-install +host_ocp4_installer_root_url: https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp + +# Cluster config +host_ocp4_cluster_name: "{{ ocp.cluster_name | default('sno') }}" +host_ocp4_base_domain: "{{ ocp.base_domain | default('sno.local') }}" +host_ocp4_api_vip: "{{ ocp.api_vip }}" +host_ocp4_ingress_vip: "{{ ocp.ingress_vip | default(ocp.api_vip) }}" + +# Pull secret +host_ocp4_pull_secret: "{{ pull_secret | default('{}') }}" + +# SSH public key +host_ocp4_ssh_key: "{{ ssh_pub_key | default('') }}" + +# BMC credentials +host_ocp4_bmc_user: admin +host_ocp4_bmc_password: "{{ common_password }}" +host_ocp4_bmc_ips: [] + +# Network CIDRs +host_ocp4_machine_network_cidr: "{{ networks.cluster.cidr | default('10.0.0.0/24') }}" +host_ocp4_cluster_network_cidr: 10.128.0.0/14 +host_ocp4_service_network_cidr: 172.30.0.0/16 + +# Install dir on bastion +host_ocp4_ibi_install_dir: "/home/{{ ansible_user }}/ibi-install" +host_ocp4_ibi_config_dir: "/home/{{ ansible_user }}/ibi-config" + +# HTTP server port for serving ISO +host_ocp4_iso_http_port: 8080 + +# Timeout for IBI install (seconds) — much shorter than agent install +host_ocp4_ibi_install_timeout: 1800 + +# NTP sources +host_ocp4_ntp_sources: + - clock.redhat.com + - pool.ntp.org + +# Debug root password — set to a plaintext password to enable VNC console login +# on the seed-restored RHCOS system. Leave empty for production. +host_ocp4_ibi_debug_password: "" diff --git a/roles/host_ocp4_ibi_installer/tasks/main.yml b/roles/host_ocp4_ibi_installer/tasks/main.yml new file mode 100644 index 0000000..eee8c1d --- /dev/null +++ b/roles/host_ocp4_ibi_installer/tasks/main.yml @@ -0,0 +1,363 @@ +--- +# OCP Image-Based Install (IBI) — deploys a SNO from a pre-captured seed image. +# Supported platforms: Troshka (KVM) and OCP Virt only (requires ISO boot). +# +# Flow: +# 1. Generate installation ISO from seed image +# 2. Boot target VM from ISO via Redfish BMC +# 3. Wait for RHCOS install + seed restore +# 4. Generate configuration ISO with site-specific details +# 5. Attach config ISO, reboot +# 6. Wait for cluster to come up + +- name: Validate IBI parameters + ansible.builtin.assert: + that: + - host_ocp4_ibi_seed_image | length > 0 + - host_ocp4_ibi_seed_version | length > 0 + fail_msg: "host_ocp4_ibi_seed_image and host_ocp4_ibi_seed_version are required" + +- name: Check if cluster already installed + ansible.builtin.command: + cmd: oc --kubeconfig={{ host_ocp4_ibi_install_dir }}/auth/kubeconfig cluster-info + register: _ibi_cluster_info + failed_when: false + changed_when: false + +- name: Install OCP via IBI + when: _ibi_cluster_info.rc != 0 + block: + - name: Determine OCP version URLs + ansible.builtin.set_fact: + _ocp_install_url: >- + {{ host_ocp4_installer_root_url }}/stable-{{ host_ocp4_installer_version }}/openshift-install-linux.tar.gz + _ocp_client_url: >- + {{ host_ocp4_installer_root_url }}/stable-{{ host_ocp4_installer_version }}/openshift-client-linux.tar.gz + + - name: Download openshift-install + ansible.builtin.unarchive: + src: "{{ _ocp_install_url }}" + dest: /usr/bin + remote_src: true + creates: /usr/bin/openshift-install + retries: 3 + delay: 10 + + - name: Download oc client + ansible.builtin.unarchive: + src: "{{ _ocp_client_url }}" + dest: /usr/bin + remote_src: true + creates: /usr/bin/oc + retries: 3 + delay: 10 + + - name: Get deployed topology from Troshka + troshka.cloud.project_info: + api_url: "{{ troshka_api_url }}" + api_key: "{{ troshka_api_key }}" + project_id: "{{ troshka_project_id }}" + register: _project_topo + delegate_to: localhost + become: false + + - name: Find target SNO VM in topology + ansible.builtin.set_fact: + _ibi_target_vm: >- + {{ _project_topo.topology.nodes + | selectattr('type', 'equalto', 'vmNode') + | selectattr('data.os', 'equalto', 'blank') + | selectattr('data.bmcEnabled', 'defined') + | first | default({}) }} + + - name: Set IBI target facts + ansible.builtin.set_fact: + _ibi_target_ip: "{{ _ibi_target_vm.data.nics[0].ip }}" + _ibi_target_mac: "{{ _ibi_target_vm.data.nics[0].mac }}" + _ibi_target_hostname: "{{ _ibi_target_vm.data.name | default('sno') }}" + _ibi_bmc_ip: "{{ _ibi_target_vm.data.bmcIp }}" + _ibi_bmc_port: "{{ _ibi_target_vm.data.bmcPort | default(8000) }}" + + - name: Build network config (NMState format) + ansible.builtin.set_fact: + _ibi_network_config: | + interfaces: + - name: enp1s0 + type: ethernet + state: up + mac-address: "{{ _ibi_target_mac }}" + ipv4: + dhcp: false + enabled: true + address: + - ip: {{ _ibi_target_ip }} + prefix-length: {{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('prefix') }} + ipv6: + enabled: false + dns-resolver: + config: + server: + - {{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }} + routes: + config: + - destination: 0.0.0.0/0 + next-hop-address: {{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }} + next-hop-interface: enp1s0 + + # ── Phase 1: Generate installation ISO ────────────────────────────────── + - name: Create IBI install directory + ansible.builtin.file: + path: "{{ host_ocp4_ibi_install_dir }}" + state: directory + owner: "{{ ansible_user }}" + mode: "0755" + + - name: Generate ignition config override for debugging + when: host_ocp4_ibi_debug_password | default('') | length > 0 + block: + - name: Hash debug password + ansible.builtin.shell: + cmd: >- + python3 -c "import crypt; print(crypt.crypt('{{ host_ocp4_ibi_debug_password }}', crypt.mksalt(crypt.METHOD_SHA512)))" + register: _ibi_password_hash + changed_when: false + no_log: true + + - name: Build ignition override JSON + ansible.builtin.set_fact: + _ibi_ignition_override: >- + {"ignition":{"version":"3.2.0"},"passwd":{"users":[{"name":"root","passwordHash":"{{ _ibi_password_hash.stdout }}"}]}} + no_log: true + + - name: Generate image-based-installation-config.yaml + ansible.builtin.template: + src: image-based-installation-config.yaml.j2 + dest: "{{ host_ocp4_ibi_install_dir }}/image-based-installation-config.yaml" + owner: "{{ ansible_user }}" + mode: "0644" + + - name: Create installation ISO + ansible.builtin.shell: + cmd: >- + openshift-install image-based create image + --dir {{ host_ocp4_ibi_install_dir }} --log-level debug + 2>&1 | tee -a ~{{ ansible_user }}/ibi-install.log + become_user: "{{ ansible_user }}" + register: _ibi_create_iso + changed_when: true + + - name: Copy SSH private key to bastion for SNO access + ansible.builtin.copy: + content: "{{ ssh_provision_key_content | default('') }}\n" + dest: "/home/{{ ansible_user }}/.ssh/ibi_key" + owner: "{{ ansible_user }}" + mode: "0600" + when: ssh_provision_key_content | default('') | length > 0 + + # ── Phase 2: Boot from installation ISO ───────────────────────────────── + - name: Open HTTP port + ansible.builtin.shell: + cmd: >- + firewall-cmd --add-port={{ host_ocp4_iso_http_port }}/tcp && + firewall-cmd --add-port={{ host_ocp4_iso_http_port }}/tcp --permanent + changed_when: true + failed_when: false + + - name: Start HTTP server for ISO + ansible.builtin.shell: + cmd: >- + cd {{ host_ocp4_ibi_install_dir }} && + nohup python3 -m http.server {{ host_ocp4_iso_http_port }} + > /tmp/ibi-http.log 2>&1 & + echo $! + register: _http_pid + changed_when: true + + - name: Set bastion IP for ISO serving + ansible.builtin.set_fact: + _bastion_ip: "{{ ansible_facts['default_ipv4']['address'] | default(ansible_host) }}" + + - name: Build BMC IP list + ansible.builtin.set_fact: + _bmc_ip_list: "{{ host_ocp4_bmc_ips if host_ocp4_bmc_ips | length > 0 else [_ibi_bmc_ip] }}" + + - name: Boot target VM from installation ISO via BMC + ansible.builtin.include_tasks: boot_via_bmc.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + + - name: Wait for IBI seed restore to complete + ansible.builtin.shell: + cmd: >- + ssh -i /home/{{ ansible_user }}/.ssh/ibi_key + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 + core@{{ _ibi_target_ip }} + "journalctl -u install-rhcos-and-restore-seed --no-pager -q 2>/dev/null | tail -3" + 2>&1 + register: _ibi_seed_check + until: >- + 'finished successfully' in (_ibi_seed_check.stdout | default('')) + or 'IBI preparation process finished' in (_ibi_seed_check.stdout | default('')) + retries: 80 + delay: 15 + changed_when: false + + # ── Phase 3: Generate and attach configuration ISO ────────────────────── + - name: Create IBI config directory + ansible.builtin.file: + path: "{{ host_ocp4_ibi_config_dir }}" + state: directory + owner: "{{ ansible_user }}" + mode: "0755" + + - name: Generate install-config.yaml for config ISO + ansible.builtin.template: + src: install-config.yaml.j2 + dest: "{{ host_ocp4_ibi_config_dir }}/install-config.yaml" + owner: "{{ ansible_user }}" + mode: "0644" + + - name: Generate image-based-config.yaml + ansible.builtin.template: + src: image-based-config.yaml.j2 + dest: "{{ host_ocp4_ibi_config_dir }}/image-based-config.yaml" + owner: "{{ ansible_user }}" + mode: "0644" + + - name: Create configuration ISO + ansible.builtin.shell: + cmd: >- + openshift-install image-based create config-image + --dir {{ host_ocp4_ibi_config_dir }} --log-level debug + 2>&1 | tee -a ~{{ ansible_user }}/ibi-install.log + become_user: "{{ ansible_user }}" + register: _ibi_config_iso + changed_when: true + + - name: Eject installation ISO from BMC + ansible.builtin.include_tasks: eject_iso.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + + - name: Serve config ISO via HTTP + ansible.builtin.shell: + cmd: >- + cp {{ host_ocp4_ibi_config_dir }}/imagebasedconfig.iso + {{ host_ocp4_ibi_install_dir }}/imagebasedconfig.iso + changed_when: true + + - name: Power off VM before attaching config ISO + ansible.builtin.uri: + url: "http://{{ _bmc_ip_list[0] }}:{{ _ibi_bmc_port }}/redfish/v1/Systems" + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + return_content: true + register: _sys_for_off + + - name: Send ForceOff to VM + ansible.builtin.uri: + url: "http://{{ _bmc_ip_list[0] }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ _sys_for_off.json.Members[0]['@odata.id'].split('/')[-1] }}/Actions/ComputerSystem.Reset" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: + ResetType: ForceOff + status_code: [200, 204] + failed_when: false + + - name: Wait for VM to power off + ansible.builtin.pause: + seconds: 10 + + - name: Eject any existing virtual media before config ISO + ansible.builtin.include_tasks: eject_iso.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + + - name: Insert config ISO and power on VM + ansible.builtin.include_tasks: boot_via_bmc.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + vars: + _bmc_iso_name: imagebasedconfig.iso + + # ── Phase 4: Wait for cluster ready ───────────────────────────────────── + - name: Wait for OCP cluster to come up + ansible.builtin.shell: + cmd: >- + oc --kubeconfig={{ host_ocp4_ibi_config_dir }}/auth/kubeconfig + get clusterversion version + -o jsonpath='{.status.conditions[?(@.type=="Available")].status}' + 2>/dev/null + register: _ibi_cluster_ready + until: _ibi_cluster_ready.stdout == "True" + retries: 60 + delay: 30 + changed_when: false + + - name: Eject config ISO from BMC + ansible.builtin.include_tasks: eject_iso.yml + loop: "{{ _bmc_ip_list }}" + loop_control: + loop_var: _bmc_ip + + - name: Stop HTTP server + ansible.builtin.shell: + cmd: kill {{ _http_pid.stdout | trim }} 2>/dev/null || true + changed_when: false + +# ── Set up kubeconfig (runs regardless of whether install was needed) ───── +- name: Set up kubeconfig + block: + - name: Check for kubeconfig in config dir + ansible.builtin.stat: + path: "{{ host_ocp4_ibi_config_dir }}/auth/kubeconfig" + register: _ibi_config_kubeconfig + + - name: Set kubeconfig source + ansible.builtin.set_fact: + _ibi_kubeconfig_src: >- + {{ host_ocp4_ibi_config_dir }}/auth/kubeconfig + if _ibi_config_kubeconfig.stat.exists + else host_ocp4_ibi_install_dir ~ '/auth/kubeconfig' + + - name: Create .kube directory + ansible.builtin.file: + path: "~{{ item }}/.kube" + state: directory + owner: "{{ item }}" + mode: "0700" + loop: + - "{{ ansible_user }}" + - root + + - name: Copy kubeconfig + ansible.builtin.copy: + src: "{{ _ibi_kubeconfig_src }}" + dest: "~{{ item }}/.kube/config" + remote_src: true + owner: "{{ item }}" + mode: "0600" + loop: + - "{{ ansible_user }}" + - root + + - name: Read cluster version + ansible.builtin.shell: + cmd: oc get clusterversion version -o jsonpath='{.status.desired.version}' + register: _ibi_version + changed_when: false + + - name: Display cluster info + ansible.builtin.debug: + msg: + - "IBI SNO cluster installed successfully" + - "Version: {{ _ibi_version.stdout }}" + - "Console: https://console-openshift-console.apps.{{ host_ocp4_cluster_name }}.{{ host_ocp4_base_domain }}" diff --git a/roles/host_ocp4_ibi_installer/templates/image-based-installation-config.yaml.j2 b/roles/host_ocp4_ibi_installer/templates/image-based-installation-config.yaml.j2 new file mode 100644 index 0000000..f03016a --- /dev/null +++ b/roles/host_ocp4_ibi_installer/templates/image-based-installation-config.yaml.j2 @@ -0,0 +1,31 @@ +apiVersion: v1beta1 +kind: ImageBasedInstallationConfig +metadata: + name: ibi-config +seedImage: {{ host_ocp4_ibi_seed_image }} +seedVersion: "{{ host_ocp4_ibi_seed_version }}" +installationDisk: {{ host_ocp4_ibi_installation_disk }} +pullSecret: '{{ host_ocp4_pull_secret }}' +{% if host_ocp4_ibi_extra_partition_start | default('') | length > 0 %} +extraPartitionStart: "{{ host_ocp4_ibi_extra_partition_start }}" +{% endif %} +{% if host_ocp4_ssh_key | length > 0 %} +sshKey: '{{ host_ocp4_ssh_key }}' +{% endif %} +{% if _ibi_network_config is defined %} +networkConfig: +{{ _ibi_network_config | indent(2, first=true) }} +{% endif %} +{% if _ibi_ignition_override is defined %} +ignitionConfigOverride: '{{ _ibi_ignition_override }}' +{% endif %} +{% if host_ocp4_ibi_image_digest_sources | default([]) | length > 0 %} +imageDigestSources: +{% for source in host_ocp4_ibi_image_digest_sources %} + - source: "{{ source.source }}" + mirrors: +{% for mirror in source.mirrors %} + - "{{ mirror }}" +{% endfor %} +{% endfor %} +{% endif %} From 05c1ce518c3bebebe55f3370104da9b6fec0a541 Mon Sep 17 00:00:00 2001 From: rut31337 Date: Wed, 1 Jul 2026 15:27:01 -0500 Subject: [PATCH 2/7] chore: remove DNS manifest injection (now handled by Troshka agent template) --- .../host_ocp4_agent_installer/tasks/main.yml | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml index 40df6fb..6073ab5 100644 --- a/roles/host_ocp4_agent_installer/tasks/main.yml +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -134,35 +134,6 @@ remote_src: true owner: "{{ ansible_user }}" - - name: Inject DNS forwarder for disconnected registry resolution - when: disconnected_registry_hostname is defined - block: - - name: Ensure openshift manifests directory exists - ansible.builtin.file: - path: "{{ host_ocp4_install_dir }}/openshift" - state: directory - owner: "{{ ansible_user }}" - mode: "0755" - - - name: Create DNS forwarder extra manifest for lab domain - ansible.builtin.copy: - dest: "{{ host_ocp4_install_dir }}/openshift/99-dns-forwarder.yaml" - owner: "{{ ansible_user }}" - mode: "0644" - content: | - apiVersion: operator.openshift.io/v1 - kind: DNS - metadata: - name: default - spec: - servers: - - name: lab-forward - zones: - - {{ host_ocp4_base_domain }} - forwardPlugin: - upstreams: - - {{ host_ocp4_machine_network_cidr | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }} - - name: Create agent ISO ansible.builtin.shell: cmd: >- From 8e02ce5b1f9519ad22b4616bf99d34b0b944be5f Mon Sep 17 00:00:00 2001 From: rut31337 Date: Thu, 2 Jul 2026 08:07:52 -0500 Subject: [PATCH 3/7] feat: add /etc/hosts MachineConfig for disconnected registry DNS resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DNS forwarder alone isn't enough — OVN/CNI pods need to pull images before CoreDNS starts. /etc/hosts is available at boot and resolves the registry hostname for CRI-O immediately. --- .../host_ocp4_agent_installer/tasks/main.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml index 6073ab5..7f3bdc8 100644 --- a/roles/host_ocp4_agent_installer/tasks/main.yml +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -134,6 +134,45 @@ remote_src: true owner: "{{ ansible_user }}" + - name: Inject /etc/hosts for disconnected registry resolution + when: disconnected_registry_hostname is defined + block: + - name: Ensure openshift manifests directory exists + ansible.builtin.file: + path: "{{ host_ocp4_install_dir }}/openshift" + state: directory + owner: "{{ ansible_user }}" + mode: "0755" + + - name: Resolve registry IP from bastion internal IP + ansible.builtin.set_fact: + _registry_ip: "{{ hostvars[groups.bastions | first].troshka_internal_ip | default('') }}" + + - name: Create /etc/hosts MachineConfig extra manifest + when: _registry_ip | length > 0 + ansible.builtin.copy: + dest: "{{ host_ocp4_install_dir }}/openshift/99-lab-etc-hosts.yaml" + owner: "{{ ansible_user }}" + mode: "0644" + content: | + apiVersion: machineconfiguration.openshift.io/v1 + kind: MachineConfig + metadata: + labels: + machineconfiguration.openshift.io/role: master + name: 99-lab-etc-hosts + spec: + config: + ignition: + version: 3.2.0 + storage: + files: + - path: /etc/hosts + mode: 0644 + overwrite: false + append: + - source: "data:,{{ _registry_ip | trim }}%09{{ disconnected_registry_hostname }}%0A" + - name: Create agent ISO ansible.builtin.shell: cmd: >- From 3aaafb6edc09db2d2a21d8026158d5a40dfb5b48 Mon Sep 17 00:00:00 2001 From: rut31337 Date: Thu, 9 Jul 2026 16:20:50 -0500 Subject: [PATCH 4/7] fix: /etc/hosts via systemd oneshot for disconnected registry DNS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use systemd oneshot service instead of ignition append for /etc/hosts entry. MCO doesn't support ignition 'append' directive — causes 'Append is not supported' error and blocks all MCO reconciliation. The systemd unit runs Before=crio.service so CRI-O can resolve the registry hostname at boot, before CoreDNS starts. --- .../host_ocp4_agent_installer/tasks/main.yml | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml index 7f3bdc8..aedab1f 100644 --- a/roles/host_ocp4_agent_installer/tasks/main.yml +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -165,13 +165,21 @@ config: ignition: version: 3.2.0 - storage: - files: - - path: /etc/hosts - mode: 0644 - overwrite: false - append: - - source: "data:,{{ _registry_ip | trim }}%09{{ disconnected_registry_hostname }}%0A" + systemd: + units: + - name: lab-etc-hosts.service + enabled: true + contents: | + [Unit] + Description=Add lab DNS entries to /etc/hosts + Before=crio.service kubelet.service + After=network-online.target + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=/bin/bash -c 'grep -q {{ disconnected_registry_hostname }} /etc/hosts || echo {{ _registry_ip | trim }} {{ disconnected_registry_hostname }} >> /etc/hosts' + [Install] + WantedBy=multi-user.target - name: Create agent ISO ansible.builtin.shell: From 0ad951c246a3c34856e4de867e5193d8a52b94c4 Mon Sep 17 00:00:00 2001 From: rut31337 Date: Wed, 15 Jul 2026 07:38:40 -0500 Subject: [PATCH 5/7] fix: resolve yamllint and ansible-lint CI failures - Break long URL lines (>150 chars) in IBI main.yml, eject_iso.yml, and agent installer main.yml using folded scalars and extracted variables - Fix name[template] violations: move Jinja expressions to end of task names - Add troshka.cloud.project_info to mock_modules in .ansible-lint - Copy boot_via_bmc.yml and eject_iso.yml into host_ocp4_ibi_installer role to resolve load-failure[filenotfounderror] from relative includes --- .ansible-lint | 1 + .../tasks/boot_via_bmc.yml | 6 +-- .../tasks/eject_iso.yml | 14 +++++-- .../host_ocp4_agent_installer/tasks/main.yml | 5 ++- .../tasks/boot_via_bmc.yml | 42 +++++++++++++++++++ .../tasks/eject_iso.yml | 34 +++++++++++++++ roles/host_ocp4_ibi_installer/tasks/main.yml | 9 +++- 7 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml create mode 100644 roles/host_ocp4_ibi_installer/tasks/eject_iso.yml diff --git a/.ansible-lint b/.ansible-lint index 7af1db4..42f525f 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -13,3 +13,4 @@ skip_list: mock_modules: - agnosticd.core.agnosticd_user_info - ansible.controller.license +- troshka.cloud.project_info diff --git a/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml b/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml index cc548dc..24428c2 100644 --- a/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml +++ b/roles/host_ocp4_agent_installer/tasks/boot_via_bmc.yml @@ -2,7 +2,7 @@ # Boot a single node via Redfish virtual media # Called in a loop with _bmc_ip as the loop var -- name: Get Redfish system ID for {{ _bmc_ip }} +- name: Get Redfish system ID - {{ _bmc_ip }} ansible.builtin.uri: url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" user: "{{ host_ocp4_bmc_user }}" @@ -15,7 +15,7 @@ ansible.builtin.set_fact: _sys_id: "{{ _redfish_systems.json.Members[0]['@odata.id'].split('/')[-1] }}" -- name: Insert virtual media ISO on {{ _bmc_ip }} +- name: Insert virtual media ISO - {{ _bmc_ip }} ansible.builtin.uri: url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia" method: POST @@ -29,7 +29,7 @@ WriteProtected: true status_code: [200, 204] -- name: Reboot node {{ _bmc_ip }} from ISO +- name: Reboot node from ISO - {{ _bmc_ip }} ansible.builtin.uri: url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/Actions/ComputerSystem.Reset" method: POST diff --git a/roles/host_ocp4_agent_installer/tasks/eject_iso.yml b/roles/host_ocp4_agent_installer/tasks/eject_iso.yml index 22be20a..6cc53bf 100644 --- a/roles/host_ocp4_agent_installer/tasks/eject_iso.yml +++ b/roles/host_ocp4_agent_installer/tasks/eject_iso.yml @@ -2,7 +2,7 @@ # Eject virtual media from a BMC node after install # Called in a loop with _bmc_ip as the loop var -- name: Get Redfish system ID for {{ _bmc_ip }} +- name: Get Redfish system ID for eject - {{ _bmc_ip }} ansible.builtin.uri: url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" user: "{{ host_ocp4_bmc_user }}" @@ -12,10 +12,18 @@ register: _redfish_systems_eject ignore_errors: true -- name: Eject virtual media on {{ _bmc_ip }} +- name: Extract system ID for eject + when: _redfish_systems_eject is success + ansible.builtin.set_fact: + _sys_id_eject: >- + {{ _redfish_systems_eject.json.Members[0]['@odata.id'].split('/')[-1] }} + +- name: Eject virtual media - {{ _bmc_ip }} when: _redfish_systems_eject is success ansible.builtin.uri: - url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _redfish_systems_eject.json.Members[0]['@odata.id'].split('/')[-1] }}/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia" + url: >- + http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ + _sys_id_eject }}/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia method: POST user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml index aedab1f..704f8aa 100644 --- a/roles/host_ocp4_agent_installer/tasks/main.yml +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -177,7 +177,10 @@ [Service] Type=oneshot RemainAfterExit=yes - ExecStart=/bin/bash -c 'grep -q {{ disconnected_registry_hostname }} /etc/hosts || echo {{ _registry_ip | trim }} {{ disconnected_registry_hostname }} >> /etc/hosts' + ExecStart=/bin/bash -c \ + 'grep -q {{ disconnected_registry_hostname }} /etc/hosts \ + || echo {{ _registry_ip | trim }} {{ disconnected_registry_hostname }} \ + >> /etc/hosts' [Install] WantedBy=multi-user.target diff --git a/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml b/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml new file mode 100644 index 0000000..24428c2 --- /dev/null +++ b/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml @@ -0,0 +1,42 @@ +--- +# Boot a single node via Redfish virtual media +# Called in a loop with _bmc_ip as the loop var + +- name: Get Redfish system ID - {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + return_content: true + register: _redfish_systems + +- name: Extract system ID + ansible.builtin.set_fact: + _sys_id: "{{ _redfish_systems.json.Members[0]['@odata.id'].split('/')[-1] }}" + +- name: Insert virtual media ISO - {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: + Image: "http://{{ _bastion_ip }}:{{ host_ocp4_iso_http_port }}/agent.x86_64.iso" + Inserted: true + WriteProtected: true + status_code: [200, 204] + +- name: Reboot node from ISO - {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/Actions/ComputerSystem.Reset" + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: + ResetType: ForceRestart + status_code: [200, 204] diff --git a/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml b/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml new file mode 100644 index 0000000..6cc53bf --- /dev/null +++ b/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml @@ -0,0 +1,34 @@ +--- +# Eject virtual media from a BMC node after install +# Called in a loop with _bmc_ip as the loop var + +- name: Get Redfish system ID for eject - {{ _bmc_ip }} + ansible.builtin.uri: + url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + return_content: true + register: _redfish_systems_eject + ignore_errors: true + +- name: Extract system ID for eject + when: _redfish_systems_eject is success + ansible.builtin.set_fact: + _sys_id_eject: >- + {{ _redfish_systems_eject.json.Members[0]['@odata.id'].split('/')[-1] }} + +- name: Eject virtual media - {{ _bmc_ip }} + when: _redfish_systems_eject is success + ansible.builtin.uri: + url: >- + http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ + _sys_id_eject }}/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia + method: POST + user: "{{ host_ocp4_bmc_user }}" + password: "{{ host_ocp4_bmc_password }}" + force_basic_auth: true + body_format: json + body: {} + status_code: [200, 204] + ignore_errors: true diff --git a/roles/host_ocp4_ibi_installer/tasks/main.yml b/roles/host_ocp4_ibi_installer/tasks/main.yml index eee8c1d..28bde43 100644 --- a/roles/host_ocp4_ibi_installer/tasks/main.yml +++ b/roles/host_ocp4_ibi_installer/tasks/main.yml @@ -257,9 +257,16 @@ return_content: true register: _sys_for_off + - name: Extract system ID for power-off + ansible.builtin.set_fact: + _sys_id_off: >- + {{ _sys_for_off.json.Members[0]['@odata.id'].split('/')[-1] }} + - name: Send ForceOff to VM ansible.builtin.uri: - url: "http://{{ _bmc_ip_list[0] }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ _sys_for_off.json.Members[0]['@odata.id'].split('/')[-1] }}/Actions/ComputerSystem.Reset" + url: >- + http://{{ _bmc_ip_list[0] }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ + _sys_id_off }}/Actions/ComputerSystem.Reset method: POST user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" From 454d454c595c6bd810cf0b62dea1c0ff9ca35983 Mon Sep 17 00:00:00 2001 From: rut31337 Date: Wed, 15 Jul 2026 07:40:04 -0500 Subject: [PATCH 6/7] fix: use _ibi_bmc_port variable in IBI role include_tasks and trim comment line --- roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml | 6 +++--- roles/host_ocp4_ibi_installer/tasks/eject_iso.yml | 4 ++-- roles/host_ocp4_ibi_installer/tasks/main.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml b/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml index 24428c2..aeb9bbb 100644 --- a/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml +++ b/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml @@ -4,7 +4,7 @@ - name: Get Redfish system ID - {{ _bmc_ip }} ansible.builtin.uri: - url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + url: "http://{{ _bmc_ip }}:{{ _ibi_bmc_port }}/redfish/v1/Systems" user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" force_basic_auth: true @@ -17,7 +17,7 @@ - name: Insert virtual media ISO - {{ _bmc_ip }} ansible.builtin.uri: - url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia" + url: "http://{{ _bmc_ip }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ _sys_id }}/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia" method: POST user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" @@ -31,7 +31,7 @@ - name: Reboot node from ISO - {{ _bmc_ip }} ansible.builtin.uri: - url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ _sys_id }}/Actions/ComputerSystem.Reset" + url: "http://{{ _bmc_ip }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ _sys_id }}/Actions/ComputerSystem.Reset" method: POST user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" diff --git a/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml b/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml index 6cc53bf..fcd3036 100644 --- a/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml +++ b/roles/host_ocp4_ibi_installer/tasks/eject_iso.yml @@ -4,7 +4,7 @@ - name: Get Redfish system ID for eject - {{ _bmc_ip }} ansible.builtin.uri: - url: "http://{{ _bmc_ip }}:8000/redfish/v1/Systems" + url: "http://{{ _bmc_ip }}:{{ _ibi_bmc_port }}/redfish/v1/Systems" user: "{{ host_ocp4_bmc_user }}" password: "{{ host_ocp4_bmc_password }}" force_basic_auth: true @@ -22,7 +22,7 @@ when: _redfish_systems_eject is success ansible.builtin.uri: url: >- - http://{{ _bmc_ip }}:8000/redfish/v1/Systems/{{ + http://{{ _bmc_ip }}:{{ _ibi_bmc_port }}/redfish/v1/Systems/{{ _sys_id_eject }}/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia method: POST user: "{{ host_ocp4_bmc_user }}" diff --git a/roles/host_ocp4_ibi_installer/tasks/main.yml b/roles/host_ocp4_ibi_installer/tasks/main.yml index 28bde43..c790201 100644 --- a/roles/host_ocp4_ibi_installer/tasks/main.yml +++ b/roles/host_ocp4_ibi_installer/tasks/main.yml @@ -295,7 +295,7 @@ vars: _bmc_iso_name: imagebasedconfig.iso - # ── Phase 4: Wait for cluster ready ───────────────────────────────────── + # ── Phase 4: Wait for cluster ready ──────────────────────────────── - name: Wait for OCP cluster to come up ansible.builtin.shell: cmd: >- From 96949c4a8d9427bdc0698fbdcb34d4c6a0405b0f Mon Sep 17 00:00:00 2001 From: rut31337 Date: Wed, 15 Jul 2026 07:44:00 -0500 Subject: [PATCH 7/7] fix: address risky-shell-pipe, partial-become, command-instead-of-shell, and file-permissions lint violations --- roles/host_ocp4_agent_installer/tasks/main.yml | 18 ++++++++++++++++-- roles/host_ocp4_ibi_installer/tasks/main.yml | 18 +++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/roles/host_ocp4_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml index 704f8aa..d0755fb 100644 --- a/roles/host_ocp4_agent_installer/tasks/main.yml +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -118,6 +118,7 @@ dest: "{{ host_ocp4_install_dir }}/install-config.yaml.bak" remote_src: true owner: "{{ ansible_user }}" + mode: "0644" - name: Generate agent-config.yaml ansible.builtin.template: @@ -133,6 +134,7 @@ dest: "{{ host_ocp4_install_dir }}/agent-config.yaml.bak" remote_src: true owner: "{{ ansible_user }}" + mode: "0644" - name: Inject /etc/hosts for disconnected registry resolution when: disconnected_registry_hostname is defined @@ -187,8 +189,11 @@ - name: Create agent ISO ansible.builtin.shell: cmd: >- + set -o pipefail; openshift-install agent create image --dir {{ host_ocp4_install_dir }} --log-level debug 2>&1 | tee -a ~{{ ansible_user }}/install.log + executable: /bin/bash + become: true become_user: "{{ ansible_user }}" register: _create_image changed_when: true @@ -223,7 +228,10 @@ - name: Get bastion IP on BMC network for ISO serving ansible.builtin.shell: - cmd: ip -4 -o addr | awk '{print $4}' | cut -d/ -f1 + cmd: >- + set -o pipefail; + ip -4 -o addr | awk '{print $4}' | cut -d/ -f1 + executable: /bin/bash register: _all_ips changed_when: false @@ -239,7 +247,10 @@ - name: Find BMC network interface ansible.builtin.shell: - cmd: ip -4 -o addr | grep '{{ _bastion_ip }}/' | awk '{print $2}' + cmd: >- + set -o pipefail; + ip -4 -o addr | grep '{{ _bastion_ip }}/' | awk '{print $2}' + executable: /bin/bash register: _bmc_iface changed_when: false @@ -261,9 +272,12 @@ - name: Wait for OCP install to complete ansible.builtin.shell: cmd: >- + set -o pipefail; openshift-install agent wait-for install-complete --dir {{ host_ocp4_install_dir }} --log-level debug 2>&1 | tee -a ~{{ ansible_user }}/install.log + executable: /bin/bash + become: true become_user: "{{ ansible_user }}" register: _install_result timeout: "{{ host_ocp4_install_timeout }}" diff --git a/roles/host_ocp4_ibi_installer/tasks/main.yml b/roles/host_ocp4_ibi_installer/tasks/main.yml index c790201..d973313 100644 --- a/roles/host_ocp4_ibi_installer/tasks/main.yml +++ b/roles/host_ocp4_ibi_installer/tasks/main.yml @@ -139,9 +139,12 @@ - name: Create installation ISO ansible.builtin.shell: cmd: >- + set -o pipefail; openshift-install image-based create image --dir {{ host_ocp4_ibi_install_dir }} --log-level debug 2>&1 | tee -a ~{{ ansible_user }}/ibi-install.log + executable: /bin/bash + become: true become_user: "{{ ansible_user }}" register: _ibi_create_iso changed_when: true @@ -190,11 +193,13 @@ - name: Wait for IBI seed restore to complete ansible.builtin.shell: cmd: >- + set -o pipefail; ssh -i /home/{{ ansible_user }}/.ssh/ibi_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 core@{{ _ibi_target_ip }} "journalctl -u install-rhcos-and-restore-seed --no-pager -q 2>/dev/null | tail -3" 2>&1 + executable: /bin/bash register: _ibi_seed_check until: >- 'finished successfully' in (_ibi_seed_check.stdout | default('')) @@ -228,9 +233,12 @@ - name: Create configuration ISO ansible.builtin.shell: cmd: >- + set -o pipefail; openshift-install image-based create config-image --dir {{ host_ocp4_ibi_config_dir }} --log-level debug 2>&1 | tee -a ~{{ ansible_user }}/ibi-install.log + executable: /bin/bash + become: true become_user: "{{ ansible_user }}" register: _ibi_config_iso changed_when: true @@ -242,11 +250,11 @@ loop_var: _bmc_ip - name: Serve config ISO via HTTP - ansible.builtin.shell: - cmd: >- - cp {{ host_ocp4_ibi_config_dir }}/imagebasedconfig.iso - {{ host_ocp4_ibi_install_dir }}/imagebasedconfig.iso - changed_when: true + ansible.builtin.copy: + src: "{{ host_ocp4_ibi_config_dir }}/imagebasedconfig.iso" + dest: "{{ host_ocp4_ibi_install_dir }}/imagebasedconfig.iso" + remote_src: true + mode: "0644" - name: Power off VM before attaching config ISO ansible.builtin.uri: