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/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..24428c2 --- /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 - {{ _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_agent_installer/tasks/eject_iso.yml b/roles/host_ocp4_agent_installer/tasks/eject_iso.yml new file mode 100644 index 0000000..6cc53bf --- /dev/null +++ b/roles/host_ocp4_agent_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_agent_installer/tasks/main.yml b/roles/host_ocp4_agent_installer/tasks/main.yml new file mode 100644 index 0000000..d0755fb --- /dev/null +++ b/roles/host_ocp4_agent_installer/tasks/main.yml @@ -0,0 +1,360 @@ +--- +# 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 }}" + mode: "0644" + + - 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 }}" + mode: "0644" + + - 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 + 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: + 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 + + - 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: >- + set -o pipefail; + ip -4 -o addr | awk '{print $4}' | cut -d/ -f1 + executable: /bin/bash + 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: >- + set -o pipefail; + ip -4 -o addr | grep '{{ _bastion_ip }}/' | awk '{print $2}' + executable: /bin/bash + 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: >- + 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 }}" + 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/boot_via_bmc.yml b/roles/host_ocp4_ibi_installer/tasks/boot_via_bmc.yml new file mode 100644 index 0000000..aeb9bbb --- /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 }}:{{ _ibi_bmc_port }}/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 }}:{{ _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 }}" + 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 }}:{{ _ibi_bmc_port }}/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..fcd3036 --- /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 }}:{{ _ibi_bmc_port }}/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 }}:{{ _ibi_bmc_port }}/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 new file mode 100644 index 0000000..d973313 --- /dev/null +++ b/roles/host_ocp4_ibi_installer/tasks/main.yml @@ -0,0 +1,378 @@ +--- +# 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: >- + 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 + + - 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: >- + 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('')) + 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: >- + 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 + + - 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.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: + 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: 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_id_off }}/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 %}