Claude assisted cleanup

This commit is contained in:
2026-02-23 23:44:21 -05:00
parent d11167b345
commit 995b7c4070
34 changed files with 925 additions and 282 deletions

View File

@@ -25,12 +25,6 @@
state: present
when: ansible_os_family == "RedHat"
- name: Set up Basic Lab Packages
hosts: all
become: yes
roles:
- role: toal-common
- name: Packages
hosts: all
become: yes

View File

@@ -0,0 +1,321 @@
---
# Configure OIDC authentication on SNO OpenShift using Keycloak as identity provider.
#
# This playbook creates a Keycloak client for OpenShift and configures the
# cluster's OAuth resource to use Keycloak as an OpenID Connect identity provider.
#
# Prerequisites:
# - SNO cluster is installed and accessible (see deploy_openshift.yml)
# - Keycloak server is running and accessible
# - oc binary available in the EE PATH (override with oc_binary if running outside EE)
# - middleware_automation.keycloak collection installed (see collections/requirements.yml)
#
# Inventory requirements:
# openshift group (e.g. sno.openshift.toal.ca)
# host_vars: ocp_cluster_name, ocp_base_domain, sno_install_dir
# secrets: vault_keycloak_admin_password
# vault_oidc_client_secret (optional — auto-generated if omitted; save the output!)
#
# Key variables:
# keycloak_url - Keycloak base URL (e.g. https://keycloak.toal.ca)
# keycloak_realm - Keycloak realm name (e.g. toallab)
# keycloak_admin_user - Keycloak admin username (default: admin)
# keycloak_context - URL prefix for Keycloak API:
# "" for Quarkus/modern Keycloak (default)
# "/auth" for legacy JBoss/WildFly Keycloak
# vault_keycloak_admin_password - Keycloak admin password (vaulted)
# vault_oidc_client_secret - OIDC client secret (vaulted, optional — auto-generated if omitted)
# oidc_provider_name - IdP name shown in OpenShift login (default: keycloak)
# oidc_client_id - Client ID in Keycloak (default: openshift)
#
# Optional variables:
# oidc_admin_groups - List of Keycloak groups to grant cluster-admin (default: [])
# oidc_ca_cert_file - Local path to CA cert PEM for Keycloak TLS (private CA only)
# oc_binary - Path to oc binary (default: oc from EE PATH)
# oc_kubeconfig - Path to kubeconfig (default: sno_install_dir/auth/kubeconfig)
#
# Usage:
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags keycloak
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags openshift
# ---------------------------------------------------------------------------
# Play 1: Configure Keycloak OIDC client for OpenShift
# ---------------------------------------------------------------------------
- name: Configure Keycloak OIDC client for OpenShift
hosts: openshift
gather_facts: false
connection: local
tags: keycloak
vars:
# Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+)
keycloak_context: ""
oidc_provider_name: keycloak
oidc_client_id: openshift
oidc_redirect_uri: "https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/oauth2callback/{{ oidc_provider_name }}"
__oidc_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context }}"
module_defaults:
middleware_automation.keycloak.keycloak_realm:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
middleware_automation.keycloak.keycloak_client:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
tasks:
# Generate a random 32-char alphanumeric secret if vault_oidc_client_secret is not supplied.
# The generated value is stored in __oidc_client_secret and displayed after the play so it
# can be vaulted and re-used on subsequent runs.
- name: Set OIDC client secret (use vault value or generate random)
ansible.builtin.set_fact:
__oidc_client_secret: "{{ vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
__oidc_secret_generated: "{{ vault_oidc_client_secret is not defined }}"
no_log: true
- name: Ensure Keycloak realm exists
middleware_automation.keycloak.keycloak_realm:
realm: "{{ keycloak_realm }}"
id: "{{ keycloak_realm }}"
display_name: "{{ keycloak_realm_display_name | default(keycloak_realm | title) }}"
enabled: true
state: present
no_log: "{{ keycloak_no_log | default(true) }}"
- name: Create OpenShift OIDC client in Keycloak
middleware_automation.keycloak.keycloak_client:
realm: "{{ keycloak_realm }}"
client_id: "{{ oidc_client_id }}"
name: "OpenShift - {{ ocp_cluster_name }}"
description: "OIDC client for OpenShift cluster {{ ocp_cluster_name }}.{{ ocp_base_domain }}"
enabled: true
protocol: openid-connect
public_client: false
standard_flow_enabled: true
implicit_flow_enabled: false
direct_access_grants_enabled: false
service_accounts_enabled: false
secret: "{{ __oidc_client_secret }}"
redirect_uris:
- "{{ oidc_redirect_uri }}"
web_origins:
- "+"
protocol_mappers:
- name: groups
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
config:
full.path: "false"
id.token.claim: "true"
access.token.claim: "true"
userinfo.token.claim: "true"
claim.name: groups
state: present
no_log: "{{ keycloak_no_log | default(true) }}"
- name: Display generated client secret (save this to vault!)
ansible.builtin.debug:
msg:
- "*** GENERATED OIDC CLIENT SECRET — SAVE THIS TO VAULT ***"
- "vault_oidc_client_secret: {{ __oidc_client_secret }}"
- ""
- "Set this in host_vars or pass as --extra-vars on future runs."
when: __oidc_secret_generated | bool
- name: Display Keycloak configuration summary
ansible.builtin.debug:
msg:
- "Keycloak OIDC client configured:"
- " Realm : {{ keycloak_realm }}"
- " Client : {{ oidc_client_id }}"
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
- " Redirect: {{ oidc_redirect_uri }}"
verbosity: 1
# ---------------------------------------------------------------------------
# Play 2: Configure OpenShift OAuth with Keycloak OIDC
# ---------------------------------------------------------------------------
- name: Configure OpenShift OAuth with Keycloak OIDC
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
tags: openshift
vars:
# Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+)
keycloak_context: ""
oidc_provider_name: keycloak
oidc_client_id: openshift
oidc_admin_groups: []
__oidc_secret_name: keycloak-oidc-client-secret
__oidc_ca_configmap_name: keycloak-oidc-ca-bundle
__oidc_oc: "{{ oc_binary | default('oc') }}"
# Prefer the fact set by Play 1; fall back to vault var when running --tags openshift alone
__oidc_client_secret_value: "{{ hostvars[inventory_hostname]['__oidc_client_secret'] | default(vault_oidc_client_secret) }}"
tasks:
- name: Create temp directory for manifests
ansible.builtin.tempfile:
state: directory
suffix: .oidc
register: __oidc_tmpdir
# ------------------------------------------------------------------
# Secret: Keycloak client secret in openshift-config namespace
# ------------------------------------------------------------------
- name: Write Keycloak client secret manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/keycloak-secret.yaml"
mode: "0600"
content: |
apiVersion: v1
kind: Secret
metadata:
name: {{ __oidc_secret_name }}
namespace: openshift-config
type: Opaque
stringData:
clientSecret: {{ __oidc_client_secret_value }}
no_log: true
- name: Apply Keycloak client secret
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-secret.yaml"
register: __oidc_secret_apply
changed_when: "'configured' in __oidc_secret_apply.stdout or 'created' in __oidc_secret_apply.stdout"
# ------------------------------------------------------------------
# CA bundle: only needed when Keycloak uses a private/internal CA
# ------------------------------------------------------------------
- name: Configure CA bundle ConfigMap for Keycloak TLS
when: oidc_ca_cert_file is defined
block:
- name: Write CA bundle ConfigMap manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/keycloak-ca.yaml"
mode: "0644"
content: |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ __oidc_ca_configmap_name }}
namespace: openshift-config
data:
ca.crt: |
{{ lookup('ansible.builtin.file', oidc_ca_cert_file) | indent(4) }}
- name: Apply CA bundle ConfigMap
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-ca.yaml"
register: __oidc_ca_apply
changed_when: "'configured' in __oidc_ca_apply.stdout or 'created' in __oidc_ca_apply.stdout"
# ------------------------------------------------------------------
# OAuth cluster resource: add/replace Keycloak IdP entry
# Reads the current config, replaces any existing entry with the same
# name, and applies the merged result — preserving other IdPs.
# ------------------------------------------------------------------
- name: Get current OAuth cluster configuration
ansible.builtin.command:
cmd: "{{ __oidc_oc }} get oauth cluster -o json"
register: __oidc_current_oauth
changed_when: false
- name: Parse current OAuth configuration
ansible.builtin.set_fact:
__oidc__oidc_current_oauth_obj: "{{ __oidc_current_oauth.stdout | from_json }}"
- name: Build Keycloak OIDC identity provider definition
ansible.builtin.set_fact:
__oidc_new_idp: >-
{{
{
'name': oidc_provider_name,
'mappingMethod': 'claim',
'type': 'OpenID',
'openID': (
{
'clientID': oidc_client_id,
'clientSecret': {'name': __oidc_secret_name},
'issuer': keycloak_url ~ keycloak_context ~ '/realms/' ~ keycloak_realm,
'claims': {
'preferredUsername': ['preferred_username'],
'name': ['name'],
'email': ['email'],
'groups': ['groups']
}
} | combine(
oidc_ca_cert_file is defined | ternary(
{'ca': {'name': __oidc_ca_configmap_name}}, {}
)
)
)
}
}}
- name: Build updated identity providers list
ansible.builtin.set_fact:
__oidc_updated_idps: >-
{{
(__oidc__oidc_current_oauth_obj.spec.identityProviders | default([])
| selectattr('name', '!=', oidc_provider_name) | list)
+ [__oidc_new_idp]
}}
- name: Write updated OAuth cluster manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/oauth-cluster.yaml"
mode: "0644"
content: "{{ __oidc__oidc_current_oauth_obj | combine({'spec': {'identityProviders': __oidc_updated_idps}}, recursive=true) | to_nice_yaml }}"
- name: Apply updated OAuth cluster configuration
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/oauth-cluster.yaml"
register: __oidc_oauth_apply
changed_when: "'configured' in __oidc_oauth_apply.stdout or 'created' in __oidc_oauth_apply.stdout"
- name: Wait for OAuth deployment to roll out
ansible.builtin.command:
cmd: "{{ __oidc_oc }} rollout status deployment/oauth-openshift -n openshift-authentication --timeout=300s"
changed_when: false
# ------------------------------------------------------------------
# Optional: grant cluster-admin to specified Keycloak groups
# ------------------------------------------------------------------
- name: Grant cluster-admin to OIDC admin groups
ansible.builtin.command:
cmd: "{{ __oidc_oc }} adm policy add-cluster-role-to-group cluster-admin {{ item }}"
loop: "{{ oidc_admin_groups }}"
changed_when: true
when: oidc_admin_groups | length > 0
- name: Remove temp directory
ansible.builtin.file:
path: "{{ __oidc_tmpdir.path }}"
state: absent
- name: Display post-configuration summary
ansible.builtin.debug:
msg:
- "OpenShift OIDC configuration complete!"
- " Provider : {{ oidc_provider_name }}"
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
- " Console : https://console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- " Login : https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- ""
- "Note: OAuth pods are restarting — login may be unavailable for ~2 minutes."
verbosity: 1

View File

@@ -63,50 +63,27 @@
ssl_verify: "{{ opnsense_ssl_verify | default(false) }}"
api_port: "{{ opnsense_api_port | default(omit) }}"
vars:
__deploy_ocp_cluster_name: "{{ hostvars['sno.openshift.toal.ca']['ocp_cluster_name'] }}"
__deploy_ocp_base_domain: "{{ hostvars['sno.openshift.toal.ca']['ocp_base_domain'] }}"
__deploy_sno_ip: "{{ hostvars['sno.openshift.toal.ca']['sno_ip'] }}"
tags: opnsense
tasks:
- name: Add Unbound host override for OCP API
oxlorg.opnsense.unbound_host:
hostname: "api.{{ ocp_cluster_name }}"
domain: "{{ ocp_base_domain }}"
value: "{{ sno_ip }}"
match_fields:
- hostname
- domain
state: present
delegate_to: localhost
vars:
ocp_cluster_name: "{{ hostvars['sno.openshift.toal.ca']['ocp_cluster_name'] }}"
ocp_base_domain: "{{ hostvars['sno.openshift.toal.ca']['ocp_base_domain'] }}"
sno_ip: "{{ hostvars['sno.openshift.toal.ca']['sno_ip'] }}"
- name: Add Unbound host override for OCP API internal
oxlorg.opnsense.unbound_host:
hostname: "api-int.{{ ocp_cluster_name }}"
domain: "{{ ocp_base_domain }}"
value: "{{ sno_ip }}"
match_fields:
- hostname
- domain
state: present
delegate_to: localhost
vars:
ocp_cluster_name: "{{ hostvars['sno.openshift.toal.ca']['ocp_cluster_name'] }}"
ocp_base_domain: "{{ hostvars['sno.openshift.toal.ca']['ocp_base_domain'] }}"
sno_ip: "{{ hostvars['sno.openshift.toal.ca']['sno_ip'] }}"
- name: Forward apps wildcard domain to SNO ingress
oxlorg.opnsense.unbound_forward:
domain: "apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
target: "{{ sno_ip }}"
state: present
delegate_to: localhost
vars:
ocp_cluster_name: "{{ hostvars['sno.openshift.toal.ca']['ocp_cluster_name'] }}"
ocp_base_domain: "{{ hostvars['sno.openshift.toal.ca']['ocp_base_domain'] }}"
sno_ip: "{{ hostvars['sno.openshift.toal.ca']['sno_ip'] }}"
roles:
- role: opnsense_dns_override
opnsense_dns_override_entries:
- hostname: "api.{{ __deploy_ocp_cluster_name }}"
domain: "{{ __deploy_ocp_base_domain }}"
value: "{{ __deploy_sno_ip }}"
type: host
- hostname: "api-int.{{ __deploy_ocp_cluster_name }}"
domain: "{{ __deploy_ocp_base_domain }}"
value: "{{ __deploy_sno_ip }}"
type: host
- domain: "apps.{{ __deploy_ocp_cluster_name }}.{{ __deploy_ocp_base_domain }}"
value: "{{ __deploy_sno_ip }}"
type: forward
# ---------------------------------------------------------------------------
# Play 3: Configure Public DNS Records in DNS Made Easy
@@ -116,35 +93,26 @@
gather_facts: false
connection: local
vars:
__deploy_public_ip: "{{ hostvars['gate.toal.ca']['haproxy_public_ip'] }}"
tags: dns
tasks:
- name: Create A record for OpenShift API endpoint
community.general.dnsmadeeasy:
account_key: "{{ dme_account_key }}"
account_secret: "{{ dme_account_secret }}"
domain: "{{ ocp_base_domain }}"
record_name: "api.{{ ocp_cluster_name }}"
record_type: A
record_value: "{{ hostvars['gate.toal.ca']['haproxy_public_ip'] }}"
record_ttl: "{{ ocp_dns_ttl }}"
port: 443
protocol: HTTPS
state: present
- name: Create A record for OpenShift apps wildcard
community.general.dnsmadeeasy:
account_key: "{{ dme_account_key }}"
account_secret: "{{ dme_account_secret }}"
domain: "{{ ocp_base_domain }}"
record_name: "*.apps.{{ ocp_cluster_name }}"
record_type: A
record_value: "{{ hostvars['gate.toal.ca']['haproxy_public_ip'] }}"
record_ttl: "{{ ocp_dns_ttl }}"
port: 443
protocol: HTTPS
state: present
roles:
- role: dnsmadeeasy_record
dnsmadeeasy_record_account_key: "{{ dme_account_key }}"
dnsmadeeasy_record_account_secret: "{{ dme_account_secret }}"
dnsmadeeasy_record_entries:
- domain: "{{ ocp_base_domain }}"
record_name: "api.{{ ocp_cluster_name }}"
record_type: A
record_value: "{{ __deploy_public_ip }}"
record_ttl: "{{ ocp_dns_ttl }}"
- domain: "{{ ocp_base_domain }}"
record_name: "*.apps.{{ ocp_cluster_name }}"
record_type: A
record_value: "{{ __deploy_public_ip }}"
record_ttl: "{{ ocp_dns_ttl }}"
# ---------------------------------------------------------------------------
# Play 4: Generate Agent ISO and deploy SNO (agent-based installer)
@@ -184,18 +152,18 @@
name: "{{ sno_vm_name }}"
type: qemu
config: current
register: _sno_vm_info
register: __sno_vm_info
when: (sno_vm_id | default('')) == '' or (sno_mac | default('')) == ''
- name: Set sno_vm_id and sno_mac from live Proxmox query
ansible.builtin.set_fact:
sno_vm_id: "{{ _sno_vm_info.proxmox_vms[0].vmid }}"
sno_vm_id: "{{ __sno_vm_info.proxmox_vms[0].vmid }}"
sno_mac: >-
{{ _sno_vm_info.proxmox_vms[0].config.net0
{{ __sno_vm_info.proxmox_vms[0].config.net0
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
| first }}
cacheable: true
when: _sno_vm_info is not skipped
when: __sno_vm_info is not skipped
- name: Ensure local install directories exist
ansible.builtin.file:
@@ -217,27 +185,27 @@
path: "{{ proxmox_iso_dir }}/{{ sno_iso_filename }}"
get_checksum: false
delegate_to: proxmox_host
register: proxmox_iso_stat
register: __proxmox_iso_stat
- name: Check if local openshift-install state directory exists
ansible.builtin.stat:
path: "{{ sno_install_dir }}/.openshift_install_state"
get_checksum: false
register: install_state_stat
register: __install_state_stat
- name: Set fact - skip ISO build if recent ISO exists on Proxmox and local state is intact
ansible.builtin.set_fact:
sno_iso_fresh: >-
__sno_iso_fresh: >-
{{
proxmox_iso_stat.stat.exists and
(now(utc=true).timestamp() | int - proxmox_iso_stat.stat.mtime | int) < 86400 and
install_state_stat.stat.exists
__proxmox_iso_stat.stat.exists and
(now(utc=true).timestamp() | int - __proxmox_iso_stat.stat.mtime | int) < 86400 and
__install_state_stat.stat.exists
}}
# ------------------------------------------------------------------
# Step 2: Get openshift-install binary
# Always ensure the binary is present — needed for both ISO generation
# and wait-for-install-complete regardless of sno_iso_fresh.
# and wait-for-install-complete regardless of __sno_iso_fresh.
# Binaries are stored in sno_install_dir so they survive across runs
# when sno_install_dir is a mounted volume in an EE.
# ------------------------------------------------------------------
@@ -247,7 +215,7 @@
dest: "{{ sno_install_dir }}/openshift-install-{{ ocp_version }}.tar.gz"
mode: "0644"
checksum: "{{ ocp_install_checksum | default(omit) }}"
register: ocp_install_tarball
register: __ocp_install_tarball
- name: Extract openshift-install binary
ansible.builtin.unarchive:
@@ -256,7 +224,7 @@
remote_src: false
include:
- openshift-install
when: ocp_install_tarball.changed or not (sno_install_dir ~ '/openshift-install') is file
when: __ocp_install_tarball.changed or not (sno_install_dir ~ '/openshift-install') is file
- name: Download openshift-client tarball
ansible.builtin.get_url:
@@ -264,7 +232,7 @@
dest: "{{ sno_install_dir }}/openshift-client-{{ ocp_version }}.tar.gz"
mode: "0644"
checksum: "{{ ocp_client_checksum | default(omit) }}"
register: ocp_client_tarball
register: __ocp_client_tarball
- name: Extract oc binary
ansible.builtin.unarchive:
@@ -273,7 +241,7 @@
remote_src: false
include:
- oc
when: ocp_client_tarball.changed or not (sno_install_dir ~ '/oc') is file
when: __ocp_client_tarball.changed or not (sno_install_dir ~ '/oc') is file
# ------------------------------------------------------------------
# Step 3: Template agent installer config files (skipped if ISO is fresh)
@@ -283,14 +251,15 @@
src: templates/install-config.yaml.j2
dest: "{{ sno_install_dir }}/install-config.yaml"
mode: "0640"
when: not sno_iso_fresh
when: not __sno_iso_fresh
no_log: true
- name: Template agent-config.yaml
ansible.builtin.template:
src: templates/agent-config.yaml.j2
dest: "{{ sno_install_dir }}/agent-config.yaml"
mode: "0640"
when: not sno_iso_fresh
when: not __sno_iso_fresh
# ------------------------------------------------------------------
# Step 4: Generate discovery ISO (skipped if ISO is fresh)
@@ -300,7 +269,7 @@
- name: Generate agent-based installer ISO
ansible.builtin.command:
cmd: "{{ sno_install_dir }}/openshift-install agent create image --dir {{ sno_install_dir }}"
when: not sno_iso_fresh
when: not __sno_iso_fresh
# ------------------------------------------------------------------
# Step 5: Upload ISO to Proxmox and attach to VM
@@ -311,7 +280,7 @@
dest: "{{ proxmox_iso_dir }}/{{ sno_iso_filename }}"
mode: "0644"
delegate_to: proxmox_host
when: not sno_iso_fresh
when: not __sno_iso_fresh
- name: Attach ISO to VM as CDROM
ansible.builtin.command:
@@ -403,3 +372,4 @@
- "Console : https://console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- "Kubeconfig : {{ sno_credentials_dir }}/kubeconfig (on proxmox_host)"
- "kubeadmin pass : {{ sno_credentials_dir }}/kubeadmin-password (on proxmox_host)"
verbosity: 1

View File

@@ -272,12 +272,6 @@
#TODO Automatically set up DNS GSSAPI per: https://access.redhat.com/documentation/en-us/red_hat_satellite/6.8/html/installing_satellite_server_from_a_connected_network/configuring-external-services#configuring-external-idm-dns_satellite
- name: Set up Basic Lab Packages
hosts: "{{ vm_name }}"
become: yes
roles:
- role: toal-common
- name: Install Satellite Servers
hosts: "{{ vm_name }}"
become: true

View File

@@ -6,7 +6,6 @@
- name: linux-system-roles.network
when: network_connections is defined
- name: toal-common
- name: Set Network OS from Netbox info.
gather_facts: no