--- - 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: | SSLVerifyClient require SSLVerifyDepth 10 SSLOptions +StdEnvVars 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: '()' replace: '\1\n ' 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