ansible-koji/roles/koji_hub/tasks/main.yaml
Rénich Bon Ćirić f30e5f9e03
fix(branding): improve table width, page font sizes, and buildinfo alignment
- Scope table responsiveness (display: block) strictly within the mobile media query. This restores table widths back to 100% on desktop layout.
- Refine table styles by differentiating layout tables (table.data-list, table.list) from vertical key-value information tables (table th), giving them a clean layout with right-aligned headers and Slate 100 background.
- Increase page font-size base, padding, and add a drop-shadowed white container wrapper for a modern, clean visual presentation.
- Theme h4 table headers with Slate 900 and a 4px Emerald border accent to override Koji's default blue.

Co-developed-by: Gemini AI <renich+gemini@woralelandia.com>
Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>
2026-06-16 11:19:42 -06:00

792 lines
24 KiB
YAML

---
- name: Install Koji Hub, Web, httpd, and crypto packages
ansible.builtin.dnf:
name:
- koji-hub
- koji-web
- httpd
- mod_ssl
- mod_wsgi
- python3-cryptography
- nmap-ncat
- postgresql
- policycoreutils-python-utils
- koji-utils
- createrepo_c
- tree
- python3-koji-hub-plugins
state: present
- name: Create directories for certs and client configs
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: apache
group: apache
mode: "0700"
loop:
- "{{ koji_top_dir }}/certs"
- "{{ koji_top_dir }}/clients"
- "{{ koji_top_dir }}/packages"
- "{{ koji_top_dir }}/repos"
- "{{ koji_top_dir }}/work"
- "{{ koji_top_dir }}/scratch"
- "{{ koji_top_dir }}/repos-dist"
- name: Create kojira group
ansible.builtin.group:
name: kojira
state: present
- name: Create kojira system user
ansible.builtin.user:
name: kojira
group: kojira
home: /var/lib/kojira
shell: /sbin/nologin
system: true
state: present
- name: Ensure admin user exists
ansible.builtin.user:
name: "{{ koji_admin_user }}"
state: present
create_home: true
- name: Add kojiadmin to wheel group for sudo access
ansible.builtin.user:
name: "{{ koji_admin_user }}"
groups: wheel
append: true
state: present
# This is for demonstration purposes only. In a production environment,
# it is recommended to configure sudo access with a more restrictive policy.
- name: Ensure wheel group has NOPASSWD sudo access
ansible.builtin.lineinfile:
path: /etc/sudoers
regexp: '^# %wheel\s+ALL=\(ALL\)\s+NOPASSWD:\s+ALL$'
line: '%wheel ALL=(ALL) NOPASSWD: ALL'
validate: 'visudo -cf %s'
state: present
- name: Ensure .koji directory exists for '{{ koji_admin_user }}'
ansible.builtin.file:
path: "{{ koji_hub_admin_user_koji_dir }}"
state: directory
owner: "{{ koji_admin_user }}"
group: "{{ koji_admin_user }}"
mode: "{{ koji_client_dir_mode }}"
- name: Configure Koji client for admin user
ansible.builtin.template:
src: koji.conf.j2
dest: "{{ koji_hub_admin_user_koji_dir }}/config"
owner: "{{ koji_admin_user }}"
group: "{{ koji_admin_user }}"
mode: "{{ koji_client_file_mode }}"
- name: Copy client certificates for '{{ koji_admin_user }}'
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ koji_hub_admin_user_koji_dir }}/{{ item.dest }}"
owner: "{{ koji_admin_user }}"
group: "{{ koji_admin_user }}"
mode: "{{ koji_client_file_mode }}"
loop:
- src: "{{ koji_pki_dir }}/kojiadmin.pem"
dest: client.pem
- src: "{{ koji_pki_dir }}/kojiadmin.key"
dest: client.key
- src: "{{ koji_pki_dir }}/{{ koji_ca_name }}_cert.crt"
dest: clientca.crt
- src: "{{ koji_pki_dir }}/{{ koji_ca_name }}_cert.crt"
dest: serverca.crt
- name: Create Koji PKI directory on the hub
ansible.builtin.file:
path: "{{ koji_remote_pki_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Copy Koji certificates to the hub
ansible.builtin.copy:
src: "{{ koji_pki_dir }}/{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- { src: "{{ koji_ca_name }}_cert.crt", dest: "{{ koji_remote_ca_trust_path }}", mode: "0644" }
- { src: "kojihub.crt", dest: "{{ koji_hub_cert_path }}", mode: "0644", group: "apache" }
- { src: "kojihub.key", dest: "{{ koji_hub_key_path }}", mode: "0640", group: "apache" }
- { src: "kojihub.pem", dest: "/etc/pki/koji/kojihub.pem", mode: "0644", group: "apache" }
- { src: "kojiadmin.pem", dest: "{{ koji_admin_cert_path }}", mode: "0644" }
- { src: "kojira.pem", dest: "/etc/pki/koji/kojira.pem", mode: "0640", group: "kojira" }
- { src: "kojiweb.pem", dest: "/etc/pki/koji/kojiweb.pem", mode: "0640", group: "apache" }
- name: Update CA trust store
ansible.builtin.command: update-ca-trust
changed_when: false
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Create Koji Hub and Web config directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ koji_hub_config_dir }}"
- "{{ koji_hub_koji_web_config_dir }}"
- name: Configure httpd for Koji
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: true
loop:
- { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^SSLCertificateFile", line: "SSLCertificateFile {{ koji_hub_cert_path }}" }
- { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^SSLCertificateKeyFile", line: "SSLCertificateKeyFile {{ koji_hub_key_path }}" }
- { path: "{{ koji_hub_httpd_ssl_config_file }}", regexp: "^#?SSLCACertificateFile", line: "SSLCACertificateFile {{ koji_remote_ca_trust_path }}" }
notify: Restart httpd
- name: Enable SSL for Koji Hub
ansible.builtin.blockinfile:
path: "{{ koji_hub_httpd_config_file }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK"
block: |
<Location /kojihub/ssllogin>
SSLVerifyClient require
SSLVerifyDepth 10
SSLOptions +StdEnvVars
</Location>
notify: Restart httpd
- name: Template Koji Hub and Web configs
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
group: apache
mode: "0640"
loop:
- { src: "00-main.conf.j2", dest: "{{ koji_hub_config_dir }}/00-main.conf" }
- { src: "db.conf.j2", dest: "{{ koji_hub_config_dir }}/db.conf" }
- { src: "admin.conf.j2", dest: "{{ koji_hub_config_dir }}/admin.conf" }
- { src: "web.conf.j2", dest: "{{ koji_hub_koji_web_config_dir }}/web.conf" }
- { src: "policies.conf.j2", dest: "{{ koji_hub_config_dir }}/policies.conf" }
- { src: "99-local-policies.conf.j2", dest: "{{ koji_hub_config_dir }}/99-local-policies.conf" }
notify: Restart httpd
- name: Set SELinux booleans for Apache
ansible.posix.seboolean:
name: "{{ item }}"
state: true
persistent: true
loop:
- httpd_can_network_connect_db
- httpd_can_network_connect
- allow_httpd_anon_write
- httpd_use_nfs
notify: Restart httpd
- name: Configure httpd MaxConnectionsPerChild to prevent memory leaks
ansible.builtin.lineinfile:
path: /etc/httpd/conf/httpd.conf
regexp: '^#?\s*MaxConnectionsPerChild\s+\d+'
line: 'MaxConnectionsPerChild 100'
state: present
backup: true
notify: Restart httpd
- name: Start and enable httpd
ansible.builtin.service:
name: httpd
state: started
enabled: true
- name: Open firewall for http/https
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
state: enabled
loop: ["http", "https"]
notify: Reload firewalld
- name: Set SELinux context for Koji topdir
ansible.builtin.command: semanage fcontext -a -t httpd_sys_rw_content_t "/mnt/koji(/.*)?"
changed_when: false
- name: Apply SELinux context
ansible.builtin.command: restorecon -r -v /mnt/koji
changed_when: false
- name: Add database host to /etc/hosts
ansible.builtin.lineinfile:
path: /etc/hosts
line: "{{ hostvars[groups['kojidb'][0]].ansible_default_ipv4.address }} {{ koji_db_host }}"
regexp: ".*{{ koji_db_host }}"
state: present
- name: Add hub host to /etc/hosts
ansible.builtin.lineinfile:
path: /etc/hosts
line: "{{ ansible_default_ipv4.address }} {{ koji_hub_fqdn }}"
regexp: ".*{{ koji_hub_fqdn }}"
state: present
- name: Wait for database to be available
ansible.builtin.wait_for:
host: "{{ koji_db_host }}"
port: 5432
timeout: 120
- name: Check if Koji DB schema is loaded
community.postgresql.postgresql_query:
login_db: "{{ koji_db_name }}"
query: "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users');"
login_host: "{{ koji_db_host }}"
login_user: "{{ koji_db_user }}"
login_password: "{{ koji_db_password }}"
register: koji_hub_schema_check
changed_when: false
- name: Load Koji DB Schema
community.postgresql.postgresql_script:
db: "{{ koji_db_name }}"
path: /usr/share/koji/schema.sql
login_host: "{{ koji_db_host }}"
login_user: "{{ koji_db_user }}"
login_password: "{{ koji_db_password }}"
when: not koji_hub_schema_check.query_result[0].exists
- name: Create kojiadmin user in database
community.postgresql.postgresql_query:
login_db: "{{ koji_db_name }}"
query: "INSERT INTO users (name, status, usertype) VALUES ('{{ koji_admin_user }}', 0, 0) ON CONFLICT DO NOTHING"
login_host: "{{ koji_db_host }}"
login_user: "{{ koji_db_user }}"
login_password: "{{ koji_db_password }}"
- name: Configure Koji client via ansible-settings.conf
ansible.builtin.template:
src: koji.conf.j2
dest: "{{ koji_hub_koji_client_config_file }}"
group: apache
mode: "0644"
- name: Grant admin privileges to kojiadmin user
community.postgresql.postgresql_query:
login_db: "{{ koji_db_name }}"
query: >
INSERT INTO user_perms (user_id, perm_id, creator_id)
VALUES (
(SELECT id FROM users WHERE name = '{{ koji_admin_user }}'),
(SELECT id FROM permissions WHERE name = 'admin'),
(SELECT id FROM users WHERE name = '{{ koji_admin_user }}')
) ON CONFLICT DO NOTHING
login_host: "{{ koji_db_host }}"
login_user: "{{ koji_db_user }}"
login_password: "{{ koji_db_password }}"
- name: Configure Koji Hub Main Options (hub.conf)
ansible.builtin.template:
src: hub.conf.j2
dest: /etc/koji-hub/hub.conf
owner: root
group: root
mode: "0644"
notify: Restart httpd
- name: Copy Sigul signing plugin to Koji Hub plugins directory
ansible.builtin.copy:
src: sigul_sign.py
dest: /usr/lib/koji-hub-plugins/sigul_sign.py
owner: root
group: root
mode: "0644"
notify: Restart httpd
- name: Create spool directories for Koji signing
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: apache
group: apache
mode: "0770"
loop:
- /var/spool/koji-sign
- /var/spool/koji-sign/queue
- /var/spool/koji-sign/failed
- name: Copy Koji sign spooler daemon to libexec
ansible.builtin.copy:
src: koji-sign-spooler
dest: /usr/libexec/koji-sign-spooler
owner: root
group: root
mode: "0755"
notify: Restart koji-sign-spooler
- name: Copy Koji sign spooler systemd service
ansible.builtin.copy:
src: koji-sign-spooler.service
dest: /usr/lib/systemd/system/koji-sign-spooler.service
owner: root
group: root
mode: "0644"
notify: Restart koji-sign-spooler
- name: Enable and start koji-sign-spooler service
ansible.builtin.systemd:
name: koji-sign-spooler
state: started
enabled: true
daemon_reload: true
- name: Template Koji callback script
ansible.builtin.template:
src: koji_callbacks.py.j2
dest: "{{ koji_callbacks_script_path }}"
mode: "0755"
- name: Configure Koji callbacks
ansible.builtin.template:
src: callbacks.conf.j2
dest: "{{ koji_callbacks_config_path }}"
mode: "0640"
notify: Restart httpd
- name: Template Koji cleanup script
ansible.builtin.template:
src: koji_cleanup.sh.j2
dest: "{{ koji_cleanup_script_path }}"
mode: "0755"
- name: Create systemd service for Koji cleanup
ansible.builtin.copy:
src: koji-cleanup.service
dest: "{{ koji_cleanup_service_path }}"
mode: "0644"
- name: Create systemd timer for Koji cleanup
ansible.builtin.copy:
src: koji-cleanup.timer
dest: "{{ koji_cleanup_timer_path }}"
mode: "0644"
- name: Enable and start Koji cleanup timer
ansible.builtin.systemd:
name: koji-cleanup.timer
state: started
enabled: true
daemon_reload: true
- name: Enable and start Koji DB sweeper timer
ansible.builtin.service:
name: koji-sweep-db.timer
state: started
enabled: true
- name: "Generate: MxOS Theme Graphic Assets on Localhost"
delegate_to: localhost
run_once: true
block:
- name: Check if rsvg-convert is installed on localhost
ansible.builtin.command: which rsvg-convert
register: rsvg_check
ignore_errors: true
changed_when: false
- name: Ensure librsvg2-tools is installed on localhost (Fedora)
ansible.builtin.dnf:
name: librsvg2-tools
state: present
become: true
when: rsvg_check.rc != 0
- name: Convert main logo SVG to PNG
ansible.builtin.command:
cmd: "rsvg-convert -h 50 --keep-aspect-ratio -o /tmp/mxos-koji.png /home/renich/Projects/MxOS/sitio/static/images/logo.svg"
creates: /tmp/mxos-koji.png
- name: Convert favicon logo SVG to PNG
ansible.builtin.command:
cmd: "rsvg-convert -w 32 --keep-aspect-ratio -o /tmp/mxos-favicon.png /home/renich/Projects/MxOS/mxos-reducido.svg"
creates: /tmp/mxos-favicon.png
- name: "Deploy: Koji Web MxOS Branding Theme"
block:
- name: Check if viewport is present in Koji Web header template
ansible.builtin.command: grep -q "viewport" /usr/share/koji-web/scripts/includes/header.chtml
register: koji_viewport_check
failed_when: false
changed_when: false
- name: Inject viewport meta tag in Koji Web header template
ansible.builtin.replace:
path: /usr/share/koji-web/scripts/includes/header.chtml
regexp: '(<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>)'
replace: '\1\n <meta name="viewport" content="width=device-width, initial-scale=1"/>'
when: koji_viewport_check.rc != 0
notify: Restart httpd
- name: Create MxOS theme directories on Hub
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- /usr/share/koji-web/static/themes/mxos
- /usr/share/koji-web/static/themes/mxos/images
- name: Copy logo PNG to theme images directory
ansible.builtin.copy:
src: /tmp/mxos-koji.png
dest: /usr/share/koji-web/static/themes/mxos/images/koji.png
owner: root
group: root
mode: "0644"
notify: Restart httpd
- name: Copy favicon PNG to theme images directory as favicon.ico
ansible.builtin.copy:
src: /tmp/mxos-favicon.png
dest: /usr/share/koji-web/static/themes/mxos/images/favicon.ico
owner: root
group: root
mode: "0644"
notify: Restart httpd
- name: Deploy MxOS koji.css style overrides
ansible.builtin.copy:
dest: /usr/share/koji-web/static/themes/mxos/koji.css
owner: root
group: root
mode: "0644"
content: |
@import url("../../koji.css");
/* Reset de fuente base y legibilidad global */
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #334155; /* Slate 700 */
background-color: #f8fafc;
}
#wrap {
background-color: #ffffff;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
border-radius: 8px;
margin: 20px auto;
max-width: 1400px;
padding: 10px 0;
}
#innerwrap {
padding: 0;
}
h2, h3 {
color: #0f172a;
font-weight: 700;
margin-top: 25px;
margin-bottom: 15px;
}
/* Encabezados de tablas/sección de Koji */
h4 {
background-color: #0f172a !important; /* Slate 900 */
color: #f8fafc !important;
border-left: 4px solid #10b981; /* Detalle verde esmeralda */
padding: 10px 15px;
font-weight: 600;
font-size: 15px;
border-radius: 4px;
margin: 20px 0 15px 0;
}
h4 a {
color: #10b981 !important;
}
h4 a:hover {
color: #059669 !important;
}
#header {
background-color: #0f172a; /* Slate 900 */
border-bottom: 3px solid #10b981; /* Verde esmeralda de MxOS */
height: 60px; /* Altura ajustada del header */
clear: left;
}
#kojiLogo {
float: left;
height: 50px; /* Logotipo redondo de 50px */
width: auto;
margin-top: 5px;
margin-left: 15px;
}
#headerSearch {
float: right;
margin-right: 15px;
height: 50px;
margin-top: 5px; /* Centrado vertical en el header de 60px */
display: flex;
align-items: center;
gap: 8px;
}
/* Estilo para los inputs de búsqueda */
#headerSearch select,
#headerSearch input[type="text"] {
background-color: #1e293b; /* Slate 800 */
border: 1px solid #334155; /* Slate 700 */
color: #f8fafc; /* Slate 50 */
border-radius: 4px;
padding: 4px 8px;
height: 28px;
font-size: 13px;
box-sizing: border-box;
}
#headerSearch select:focus,
#headerSearch input[type="text"]:focus {
border-color: #10b981;
outline: none;
}
#headerSearch input[type="submit"] {
background-color: #10b981;
color: white;
border: none;
border-radius: 4px;
padding: 0 16px;
height: 28px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
box-sizing: border-box;
}
#headerSearch input[type="submit"]:hover {
background-color: #059669;
}
/* Estilización del Menú de Pestañas (#mainNav) */
#mainNav {
background-color: #1e293b; /* Slate 800 */
border-bottom: 2px solid #10b981; /* Borde verde esmeralda */
font-weight: bold;
height: 2.1em;
}
#mainNav ul li {
background-color: #1e293b;
border-color: #1e293b;
}
#mainNav ul li a {
color: #f8fafc; /* Texto claro */
}
#mainNav ul li:hover {
background-color: #334155; /* Slate 700 */
border-color: #10b981;
}
/* Pestaña activa de la página actual */
body#summary #mainNav li#summaryTab a,
body#tasks #mainNav li#tasksTab a,
body#tags #mainNav li#tagsTab a,
body#builds #mainNav li#buildsTab a,
body#packages #mainNav li#packagesTab a,
body#users #mainNav li#usersTab a,
body#hosts #mainNav li#hostsTab a,
body#buildtargets #mainNav li#buildtargetsTab a,
body#reports #mainNav li#reportsTab a,
body#search #mainNav li#searchTab a,
body#api #mainNav li#apiTab a {
background-color: #10b981; /* Emerald 500 */
color: white;
}
/* Cambiar colores de enlaces y botones globales */
a {
color: #10b981;
}
a:hover {
color: #059669; /* Verde oscuro */
}
/* --- ESTILOS DE TABLAS MEJORADOS --- */
/* Estilo general para todas las tablas (Información vertical y Listas) */
table {
width: 100% !important;
margin-bottom: 20px;
border-collapse: collapse;
}
/* Tablas de Información (Estructura Vertical: th a la izquierda, td a la derecha) */
table th {
font-weight: 600;
color: #0f172a; /* Slate 900 */
background-color: #f1f5f9; /* Slate 100 */
border-bottom: 1px solid #e2e8f0;
border-right: 1px solid #e2e8f0;
text-align: right;
padding: 8px 15px;
width: 180px;
font-size: 13px;
}
table td {
padding: 8px 15px;
border-bottom: 1px solid #e2e8f0;
color: #334155;
font-size: 13px;
vertical-align: middle;
}
/* Tablas de Listas de Datos (Estructura Horizontal: th arriba, td abajo) */
table.data-list,
table.list {
width: 100% !important;
}
table.data-list th,
table.list th,
tr.list-header th {
background-color: #1e293b !important; /* Slate 800 */
color: #f8fafc !important;
font-weight: 600;
text-align: left;
border-bottom: 2px solid #10b981 !important; /* Borde inferior verde */
border-right: none;
padding: 10px 15px;
font-size: 13px;
}
table.data-list td,
table.list td {
border-bottom: 1px solid #e2e8f0;
border-right: none;
text-align: left;
padding: 10px 15px;
}
/* Filas alternas y hover en listas */
table.data-list tr:nth-child(even),
table.list tr:nth-child(even) {
background-color: #f8fafc;
}
table.data-list tr:hover,
table.list tr:hover {
background-color: #f1f5f9;
}
/* Botones */
.button, input[type="submit"], input[type="button"] {
background-color: #10b981;
color: white;
border: none;
border-radius: 4px;
padding: 6px 16px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.button:hover, input[type="submit"]:hover, input[type="button"]:hover {
background-color: #059669;
}
/* --- ESTILOS RESPONSIVOS PARA MÓVILES (Media Queries) --- */
@media (max-width: 768px) {
#wrap {
margin: 5px;
padding: 5px;
}
#header {
height: auto;
min-height: 110px;
padding: 10px;
text-align: center;
}
#kojiLogo {
float: none;
display: block;
margin: 0 auto 10px auto;
height: 40px;
}
#headerSearch {
float: none;
margin: 10px auto 0 auto;
justify-content: center;
height: auto;
flex-wrap: wrap;
gap: 6px;
}
#mainNav {
height: auto;
}
#mainNav ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 0;
list-style-type: none;
}
#mainNav ul li {
float: none;
flex: 1 1 30%; /* Ocupa al menos el 30% del ancho, fluyendo en filas */
text-align: center;
border-width: 1px;
}
#mainNav ul li a {
padding: 8px 5px;
font-size: 12px;
height: auto;
}
#loginInfo {
float: none;
display: block;
text-align: center;
margin: 10px 0;
}
/* Tablas Responsivas en Móvil (convierten a scroll horizontal) */
table,
table.data-list,
table.list {
display: block;
width: 100% !important;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
notify: Restart httpd