---
# SPDX-License-Identifier: GPL-3.0-only

# if inputs are given and rsyslog_enabled is true, rsyslog.conf will be
# overwritten, so no need to reinstall package
- name: Reinstall package to restore rsyslog config if purging
  when:
    - logging_purge_confs | bool | d(false)
    - not (rsyslog_inputs and __rsyslog_enabled | bool)
    - not __logging_is_ostree | d(false)
  vars:
    __rsyslog_only_packages: "{{ __rsyslog_base_packages | select('search', 'rsyslog') | list }}"
  block:
    # it is assumed that the only packages providing config files that might
    # be modified are the base packages (but not others like iproute);
    # if this is not so, then additional packages will need to be added to this check
    - name: Get status of rsyslog packages
      command: rpm -V {{ item }}  # noqa command-instead-of-module
      loop: "{{ __rsyslog_only_packages }}"
      register: __rsyslog_package_status
      failed_when: false
      changed_when: false

    - name: Reset original confs - logging package is absent
      # we can't do this with "package", dnf5 does not remove reverse
      # dependencies (such as rsyslog-gnutls) any more
      command: "{{ ansible_pkg_mgr }} remove -y {{ __rsyslog_only_packages | join(' ') }}"
      # we know it's a change here, as we just rpm -V'ed it above
      changed_when: true
      register: __rsyslog_erased
      when: __rsyslog_package_status.results |
        rejectattr('stdout', 'match', '^package .* is not installed') |
        selectattr('stdout', 'search', ' /etc/rsyslog[.]conf($|\n)') |
        list | length > 0

- name: Install/Update required packages
  package:
    name: "{{ __rsyslog_base_packages + (__rsyslog_tls_packages
      if (logging_pki_files | d([]) | length > 0) else []) +
      (rsyslog_extra_packages | flatten) }}"
    state: present
    use: "{{ (__logging_is_ostree | d(false)) |
             ternary('ansible.posix.rhel_rpm_ostree', omit) }}"
  when:
    - __rsyslog_enabled | bool or
      __rsyslog_erased is success

# make sure we do not pollute the global namespace for subsequent runs
# of this role
- name: Reset erased flag
  set_fact:
    __rsyslog_erased: false

- name: Gather package facts
  package_facts:
    manager: auto

- name: Get rsyslog version
  set_fact:
    __rsyslog_version: >-
      {{ ansible_facts.packages['rsyslog'][0].version
         if 'rsyslog' in ansible_facts.packages
         else '0.0.0' }}

- name: Check rsyslog version 1
  debug:
    msg: "Rsyslog_version is {{ __rsyslog_version }},
      which is older than \"8.37.0-7.2\"."
  when: __rsyslog_version is version('8.37.0-7.2', '<')

- name: Check rsyslog version 2
  debug:
    msg: "Rsyslog_version is {{ __rsyslog_version }},
      which is newer than \"8.37.0-7.2\"."
  when: __rsyslog_version is version('8.37.0-7.2', '>')

- name: Set failed validation flag
  when: rsyslog_in_image | bool
  block:
    - name: Set __rsyslog_failed_validation
      set_fact:
        __rsyslog_failed_validation: true
      when: __rsyslog_version is version('8.37.0-7.2', '<')

- name: Deploy rsyslog
  when: not __rsyslog_failed_validation | d(false)
  block:
    - name: Create the config directory if it does not exist
      file:
        state: directory
        path: '{{ __rsyslog_config_dir }}'
        mode: '0755'

    # Creating a work directory
    - name: Create a work directory
      file:
        state: directory
        path: '{{ __rsyslog_work_dir }}'
        mode: '0700'

    - name: "Create logging directory if it does not exist or
        the ownership and/or modes are different."
      file:
        state: directory
        path: '{{ __rsyslog_system_log_dir }}'
        owner: 'root'
        group: 'root'
        mode: '0755'

    - name: Generate global rule to add to __rsyslog_common_rules
      vars:
        __rsyslog_global_common_rule:
          - filename: '00-global.conf'
            comment: 'Global options'
            options: "{{ lookup('template', 'global.j2') }}"
          - name: 'local-modules'
            type: 'modules'
            state: '{{ "present" if logging_mark else "absent" }}'
            sections:
              - options: |-
                  # Mark message periodically by immark, if set to true.
                  module(load="immark" interval="{{ logging_mark_interval }}")
          - name: 'common-defaults'
            type: 'global'
            sections:
              - comment: 'Log every message'
                options: |-
                  $RepeatedMsgReduction {{ "on"
                  if rsyslog_message_reduction | bool else "off" }}
          - name: 'templates'
            type: 'templates'
            sections:
              - comment: 'User provided output templates'
                options: "{{ lookup('template', 'custom_templates.j2') }}"
      set_fact:
        __rsyslog_common_rules: "{{ __rsyslog_global_common_rule }}"

    - name: Generate common rsyslog configuration files in rsyslog.d
      template:
        src: 'rules.conf.j2'
        dest: |-
          {{ __rsyslog_config_dir }}/{{ inner_item.filename |
          d((inner_item.weight if inner_item.weight | d() else
          rsyslog_weight_map[inner_item.type | d("rules")]) +
          "-" + (inner_item.name | d("rules")) +
          "." + (inner_item.suffix | d("conf"))) }}
        owner: '{{ inner_item.owner | d("root") }}'
        group: '{{ inner_item.group | d("root") }}'
        mode: '{{ inner_item.mode | d("0644") }}'
      loop: '{{ __rsyslog_common_rules | flatten }}'
      loop_control:
        loop_var: inner_item
      when:
        - __rsyslog_enabled | bool
        - rsyslog_inputs | d([])
        - inner_item.filename | d() or inner_item.name | d()
        - inner_item.state is undefined or inner_item.state != 'absent'
        - inner_item.options | d() or inner_item.sections | d()
      notify: Restart rsyslogd
      register: __rsyslog_templates

    - name: Initialize list of template results
      set_fact:
        __rsyslog_template_results: "{{ [__rsyslog_templates] }}"

    - name: Remove common config files in rsyslog.d
      file:
        path: |-
          {{ __rsyslog_config_dir }}/{{ inner_item.filename |
          d((inner_item.weight if inner_item.weight | d() else
          rsyslog_weight_map[inner_item.type | d("rules")]) +
          "-" + (inner_item.name | d("rules")) + "." +
          (inner_item.suffix | d("conf"))) }}
        state: absent
      loop: '{{ __rsyslog_common_rules | flatten }}'
      loop_control:
        loop_var: inner_item
      when:
        - not __rsyslog_enabled | bool
        - inner_item.filename | d() or inner_item.name | d()
        - inner_item.options | d() or inner_item.sections | d()
      notify: Stop rsyslogd

    - name: Include input sub-vars
      include_vars:
        file: "{{ role_path }}/vars/{{ varfile }}"
      vars:
        varfile: "inputs/{{ input_item.type }}/main.yml"
      loop: "{{ rsyslog_inputs }}"
      loop_control:
        loop_var: input_item
      when:
        - input_item | d([])

    - name: Run input sub-tasks
      include_tasks:
        file: "{{ task_file }}"
      vars:
        task_file: "{{ role_path }}/tasks/inputs/{{ input_item.type }}/main.yml"
        __rsyslog_input: "{{ input_item }}"
      loop: '{{ rsyslog_inputs | sort(attribute="type") }}'
      loop_control:
        extended: true
        loop_var: input_item
      when:
        - input_item | d([])
        - input_item.type | d()
        - input_item.type != "basics" or
          (input_item.type == "basics" and
           (ansible_loop.previtem is not defined or
            (ansible_loop.previtem is defined and
             ansible_loop.previtem.type != 'basics')))

    - name: Include output sub-vars
      include_vars:
        file: "{{ role_path }}/vars/{{ varfile }}"
      vars:
        varfile: "outputs/{{ output_item.type }}/main.yml"
      loop: "{{ rsyslog_outputs }}"
      loop_control:
        loop_var: output_item
      when:
        - output_item | d([])
        - output_item.type | d()

    - name: Run output sub-tasks
      include_tasks:
        file: "{{ task_file }}"
      vars:
        task_file: >-
          {{ role_path }}/tasks/outputs/{{ output_item.type }}/main.yml
        __rsyslog_output: "{{ output_item }}"
      loop: "{{ rsyslog_outputs }}"
      loop_control:
        loop_var: output_item
      when:
        - output_item | d([])

    - name: Get rsyslog config files not owned by any package
      shell: |
        set -euo pipefail
        for conf in $( ls "{{ __rsyslog_config_dir }}" ); do
          rstr=$( rpm -qf "{{ __rsyslog_config_dir }}/$conf" 2>&1 || : )
          if [[ "$rstr" == *"not owned by any package"* ]]; then
            echo "{{ __rsyslog_config_dir }}/$conf"
          fi
        done
      register: __rsyslog_confs
      failed_when: false
      changed_when: false
      when: logging_purge_confs | bool | d(false)

    # If logging_purge_confs is set to true, remove files in rsyslog.d
    # which do not belong to any rpm packages. That includes config
    # files generated by the previous logging role run.
    - name: Purge - remove files not generated by current state
      file:
        path: "{{ item }}"
        state: absent
      loop: "{{ __rsyslog_files_to_remove }}"
      notify: "{{ 'Restart rsyslogd' if logging_enabled else 'Stop rsyslogd' }}"
      when:
        - logging_purge_confs | bool | d(false)
        - __rsyslog_files_to_remove | length > 0
      vars:
        __rsyslog_current_files: "{{ __rsyslog_template_results |
          selectattr('results', 'defined') | map(attribute='results') |
          flatten | selectattr('dest', 'defined') | map(attribute='dest') |
          list }}"
        __rsyslog_files_to_remove: "{{
          __rsyslog_confs.stdout_lines | difference(__rsyslog_current_files) }}"

    # How to set rsyslog_custom_config_files:
    # rsyslog_custom_config_files:
    #   - /path/to/custom0.conf
    #   - /path/to/custom1.conf
    # The specified custom config files are copied to /etc/rsyslog.d.
    # If the array contains non-existing file, the deployment stops
    # there with an error.
    - name: "Copy custom config files if they are specified in
        rsyslog_custom_config_files variable array."
      copy:
        src: '{{ item }}'
        dest: '{{ __rsyslog_config_dir }}'
        owner: '{{ item.owner | d("root") }}'
        group: '{{ item.group | d("root") }}'
        mode: '{{ item.mode | d("0644") }}'
      loop: '{{ rsyslog_custom_config_files | flatten }}'
      when: __rsyslog_enabled | bool
      notify: Restart rsyslogd

    - name: Configure rsyslog for TLS
      vars:
        __logging_forwards_tls: "{{ rsyslog_outputs |
                                  selectattr('target', 'defined') |
                                  selectattr('type', 'match', '^forwards$') |
                                  selectattr('tls', 'defined') |
                                  selectattr('tls') | list }}"
        __logging_remote_tls: "{{ rsyslog_inputs |
                                  selectattr('type', 'match', '^remote$') |
                                  selectattr('tcp_ports', 'defined') |
                                  selectattr('tls', 'defined') |
                                  selectattr('tls') | list }}"
      when:
        - __rsyslog_enabled | bool
      block:
        # Check if tls is enabled in forwards output or remote input and
        #          logging_pki_files.{ca_cert_src,ca_cert} is defined
        # Otherwise it fails.
        - name: "Check tls is enabled in forwards output or
            remote input and logging_pki_files"
          fail:
            msg: "Error: tls is enabled in {{ item.0.name }};
              you must specify logging_pki_files ca_cert_src and/or
              ca_cert in the playbook var section."
          loop: "{{ [__logging_forwards_tls, __logging_remote_tls] }}"
          when:
            - item | length > 0
            - not (logging_pki_files.0.ca_cert_src | d() or
                logging_pki_files.0.ca_cert | d())

        - name: Copy local ca_cert file to the target if needed
          vars:
            __pki_file: '{{ logging_pki_files.0.ca_cert_src | basename }}'
          copy:
            src: '{{ logging_pki_files.0.ca_cert_src }}'
            dest: '{{ logging_pki_files.0.ca_cert
              if logging_pki_files.0.ca_cert is defined
              else (__rsyslog_default_pki_path +
                __rsyslog_default_pki_cert_dir + __pki_file) }}'
            mode: "0600"
          when:
            - __logging_forwards_tls | length > 0 or
              __logging_remote_tls | length > 0
            - logging_pki_files.0.ca_cert_src | d("")
          notify: Restart rsyslogd

        - name: Copy local cert file to the target if needed
          vars:
            __pki_file: '{{ logging_pki_files.0.cert_src | basename }}'
          copy:
            src: '{{ logging_pki_files.0.cert_src }}'
            dest: '{{ logging_pki_files.0.cert
              if logging_pki_files.0.cert is defined
              else (__rsyslog_default_pki_path +
                __rsyslog_default_pki_cert_dir + __pki_file) }}'
            mode: "0600"
          when:
            - __logging_forwards_tls | length > 0 or
              __logging_remote_tls | length > 0
            - logging_pki_files.0.cert_src | d("")
          notify: Restart rsyslogd

        - name: Copy local key file to the target if needed
          vars:
            __pki_file: '{{ logging_pki_files.0.private_key_src | basename }}'
          copy:
            src: '{{ logging_pki_files.0.private_key_src }}'
            dest: '{{ logging_pki_files.0.private_key
              if logging_pki_files.0.private_key is defined
              else (__rsyslog_default_pki_path +
                __rsyslog_default_pki_key_dir + __pki_file) }}'
            mode: "0400"
          when:
            - __logging_forwards_tls | length > 0 or
              __logging_remote_tls | length > 0
            - logging_pki_files.0.private_key_src | d("")
          notify: Restart rsyslogd

    - name: Enable rsyslog service
      service:
        name: rsyslog
        enabled: true
      when:
        - __rsyslog_enabled | bool
        - not rsyslog_in_image | default(false) | bool

    - name: Disable rsyslog service
      systemd:
        name: rsyslog.service
        enabled: false
        state: "{{ 'stopped' if __logging_is_booted else omit }}"
      when:
        - not __rsyslog_enabled | bool
        - not rsyslog_in_image | default(false) | bool

- name: Check rsyslog version
  debug:
    msg: "Rsyslog version must be >= 8.37.0-7.2"
  when:
    - __rsyslog_failed_validation | d(false)
    - rsyslog_in_image | default(false) | bool

- name: Create or recreate main config file
  vars:
    __rsyslog_generate_conf: "{{ (__logging_is_ostree | d(false)) |
      ternary(true,
              __rsyslog_enabled and rsyslog_inputs | d([]) | length > 0) }}"
  when: __rsyslog_generate_conf | bool
  block:
    - name: See if there are any config files
      find:
        paths: "{{ __rsyslog_config_dir }}"
        patterns: "*.conf"
      register: __rsyslog_find_result
      when: __logging_is_ostree | d(false)

    - name: Get mode of rsyslog.conf if it exists
      stat:
        path: /etc/rsyslog.conf
      register: __rsyslog_register_stat_conf
      when: __rsyslog_generate_conf | bool

    - name: Generate main rsyslog configuration
      template:
        src: 'rsyslog.conf.j2'
        dest: '/etc/rsyslog.conf'
        mode: "{{ __rsyslog_register_stat_conf.stat.mode | d('0700') }}"
      notify: Restart rsyslogd
      when: __rsyslog_generate_conf | bool
      vars:
        __rsyslog_has_config_files: "{{ __rsyslog_find_result.matched > 0
          if __logging_is_ostree | d(false) else true }}"
