From a1d7c17a9d029d37582e5d2fac90f8be2acafbed Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 15 Jan 2026 21:20:19 -0500 Subject: [PATCH 1/2] initial release of verify and certify --- playbooks/verify.yml | 180 +++++++ plugins/modules/gather_host_information.py | 154 ++++++ roles/redis/tasks/certify-redis.yml | 542 +++++++++++++++++++++ roles/redis/templates/certify-report-md.j2 | 305 ++++++++++++ 4 files changed, 1181 insertions(+) create mode 100644 playbooks/verify.yml create mode 100644 plugins/modules/gather_host_information.py create mode 100644 roles/redis/tasks/certify-redis.yml create mode 100644 roles/redis/templates/certify-report-md.j2 diff --git a/playbooks/verify.yml b/playbooks/verify.yml new file mode 100644 index 00000000..60856fc8 --- /dev/null +++ b/playbooks/verify.yml @@ -0,0 +1,180 @@ +--- +- name: Gather System Facts + hosts: all + gather_facts: true + + vars: + # These are production specs for Itential P6 + hardware_specs: + mongodb: + cpu_count: 16 + ram_size: 128 + disk_size: 1000 + platform: + cpu_count: 16 + ram_size: 64 + disk_size: 250 + redis: + cpu_count: 16 + ram_size: 32 + disk_size: 100 + + tasks: + # OS and Architecture validation + - name: Check OS compatibility + ansible.builtin.set_fact: + os_valid: >- + {{ + (ansible_distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or + (ansible_distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or + (ansible_distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or + (ansible_distribution == 'Amazon' and ansible_distribution_version == '2023') + }} + + - name: Assert that this is a supported OS + ansible.builtin.assert: + that: "{{ os_valid }} == true" + fail_msg: "{{ ansible_distribution }} {{ ansible_distribution_major_version }} is not a supported OS!" + success_msg: "OS validation passed!" + quiet: true + + - name: Check architecture compatibility + ansible.builtin.set_fact: + arch_valid: "{{ ansible_architecture in ['x86_64', 'aarch64'] }}" + + - name: Assert that this is a supported Architecture + ansible.builtin.assert: + that: "{{ arch_valid }} == true" + fail_msg: "{{ ansible_architecture }} is not a supported architecture!" + success_msg: "Architecture validation passed!" + quiet: true + + # Hardware spec validation + - name: Determine which hardware spec applies to this host + ansible.builtin.set_fact: + applicable_spec: >- + {%- if 'mongodb' in group_names -%} + mongodb + {%- elif 'platform' in group_names -%} + platform + {%- elif 'redis' in group_names -%} + redis + {%- else -%} + none + {%- endif -%} + + - name: Get root partition size + ansible.builtin.set_fact: + root_disk_size_gb: "{{ (ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_total') | first / 1024 / 1024 / 1024) | round(2) }}" + when: ansible_mounts | selectattr('mount', 'equalto', '/') | list | length > 0 + + - name: Validate hardware specs against requirements + ansible.builtin.set_fact: + hardware_validation: + applicable_spec: "{{ applicable_spec }}" + required: + cpu_count: "{{ hardware_specs[applicable_spec].cpu_count if applicable_spec != 'none' else 'N/A' }}" + ram_size_gb: "{{ hardware_specs[applicable_spec].ram_size if applicable_spec != 'none' else 'N/A' }}" + disk_size_gb: "{{ hardware_specs[applicable_spec].disk_size if applicable_spec != 'none' else 'N/A' }}" + actual: + cpu_count: "{{ ansible_processor_vcpus }}" + ram_size_gb: "{{ (ansible_memtotal_mb / 1024) | round(2) }}" + disk_size_gb: "{{ root_disk_size_gb | default('N/A') }}" + validation: + cpu_valid: "{{ (applicable_spec == 'none') or (ansible_processor_vcpus >= hardware_specs[applicable_spec].cpu_count) }}" + ram_valid: "{{ (applicable_spec == 'none') or ((ansible_memtotal_mb / 1024) >= hardware_specs[applicable_spec].ram_size) }}" + disk_valid: "{{ (applicable_spec == 'none') or ((root_disk_size_gb | default(0) | float) >= hardware_specs[applicable_spec].disk_size) }}" + all_valid: "{{ (applicable_spec == 'none') or ((ansible_processor_vcpus >= hardware_specs[applicable_spec].cpu_count) and ((ansible_memtotal_mb / 1024) >= hardware_specs[applicable_spec].ram_size) and ((root_disk_size_gb | default(0) | float) >= hardware_specs[applicable_spec].disk_size)) }}" + + + # Network interface IP version check + - name: Check network interface IP support + ansible.builtin.set_fact: + interface_info: >- + {{ + interface_info | default([]) + [{ + 'interface': item, + 'ipv4_enabled': hostvars[inventory_hostname]['ansible_' + item].ipv4 is defined, + 'ipv6_enabled': hostvars[inventory_hostname]['ansible_' + item].ipv6 is defined and + (hostvars[inventory_hostname]['ansible_' + item].ipv6 | length > 0), + 'ipv4_address': hostvars[inventory_hostname]['ansible_' + item].ipv4.address | default('N/A'), + 'ipv6_addresses': hostvars[inventory_hostname]['ansible_' + item].ipv6 | map(attribute='address') | list | default([]) + }] + }} + loop: "{{ ansible_interfaces }}" + when: + - item != 'lo' + - not item.startswith('docker') + - not item.startswith('veth') + + - name: Determine dual stack support + ansible.builtin.set_fact: + has_dual_stack: "{{ interface_info | selectattr('ipv4_enabled') | selectattr('ipv6_enabled') | list | length > 0 }}" + has_ipv4: "{{ interface_info | selectattr('ipv4_enabled') | list | length > 0 }}" + has_ipv6: "{{ interface_info | selectattr('ipv6_enabled') | list | length > 0 }}" + + - name: Build simplified disk list + ansible.builtin.set_fact: + disk_list: "{{ disk_list | default([]) + [{'mount': item.mount, 'size_gb': (item.size_total / 1024 / 1024 / 1024) | round(2)}] }}" + loop: "{{ ansible_mounts | selectattr('size_total', 'defined') | list }}" + + - name: Build host information dictionary + ansible.builtin.set_fact: + host_info: + hostname: "{{ inventory_hostname }}" + groups: "{{ group_names }}" + validation: + os_valid: "{{ os_valid }}" + os_details: "{{ ansible_distribution }} {{ ansible_distribution_version }}" + arch_valid: "{{ arch_valid }}" + arch_details: "{{ ansible_architecture }}" + hardware: "{{ hardware_validation }}" + networking: + has_ipv4: "{{ has_ipv4 }}" + has_ipv6: "{{ has_ipv6 }}" + has_dual_stack: "{{ has_dual_stack }}" + interfaces: "{{ interface_info }}" + cpu: + physical_cpus: "{{ ansible_processor_count }}" + cores_per_cpu: "{{ ansible_processor_cores }}" + total_vcpus: "{{ ansible_processor_vcpus }}" + threads_per_core: "{{ ansible_processor_threads_per_core }}" + processor_model: "{{ ansible_processor[2] | default('N/A') }}" + memory: + total_mb: "{{ ansible_memtotal_mb }}" + total_gb: "{{ (ansible_memtotal_mb / 1024) | round(2) }}" + free_mb: "{{ ansible_memfree_mb }}" + swap_total_mb: "{{ ansible_swaptotal_mb }}" + disks: "{{ disk_list }}" + selinux: "{{ ansible_selinux | default({'status': 'not available'}) }}" + firewalld: "{{ ansible_facts.services['firewalld.service'] | default(ansible_facts.services['firewalld'] | default({'state': 'not installed', 'status': 'not installed'})) }}" + os: + distribution: "{{ ansible_distribution }}" + version: "{{ ansible_distribution_version }}" + family: "{{ ansible_os_family }}" + kernel: "{{ ansible_kernel }}" + architecture: "{{ ansible_architecture }}" + hostname: "{{ ansible_hostname }}" + fqdn: "{{ ansible_fqdn }}" + + - name: Gather host information + itential.deployer.gather_host_information: + register: host_info + + - name: Debug host info + ansible.builtin.debug: + msg: "{{ host_info }}" + +- name: Aggregate Results + hosts: localhost + gather_facts: false + + tasks: + - name: Collect all host information + ansible.builtin.set_fact: + all_systems_info: "{{ all_systems_info | default([]) + [hostvars[item].host_info] }}" + loop: "{{ groups['all'] }}" + + - name: Display aggregated information + ansible.builtin.debug: + var: all_systems_info diff --git a/plugins/modules/gather_host_information.py b/plugins/modules/gather_host_information.py new file mode 100644 index 00000000..e9828b2a --- /dev/null +++ b/plugins/modules/gather_host_information.py @@ -0,0 +1,154 @@ +#!/usr/bin/python + +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: gather_host_information + +short_description: Inspect facts and gather interesting data + +version_added: "3.0.0" + +description: This module will inspect the host facts and gather interesting data to be used in the + verification and certification of environments. + +author: + - Steven Schattenberg (@steven-schattenberg-itential) +''' + +EXAMPLES = r''' +- name: Gather standard facts + itential.deployer.gather_host_information: +''' + +RETURN = r''' +details: + description: Details from the host + type: object + returned: always + sample: false +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.facts.compat import ansible_facts + +def build_disk_list(ansible_mounts): + """Build simplified disk list from ansible_mounts data""" + disk_list = [] + + for item in ansible_mounts: + if 'size_total' in item: + disk_list.append({ + 'mount': item['mount'], + 'size_gb': round(item['size_total'] / 1024 / 1024 / 1024, 2) + }) + + return disk_list + +def build_interface_list(facts): + """Build simplified interface information""" + interfaces = [] + + # Get list of all interfaces + interface_names = facts.get('interfaces', []) + + for iface_name in interface_names: + # Skip loopback + if iface_name == 'lo': + continue + + # Get the interface details + iface_data = facts.get(iface_name, {}) + + if not iface_data or not isinstance(iface_data, dict): + continue + + interface_info = { + 'name': iface_name, + 'active': iface_data.get('active', False), + 'type': iface_data.get('type', 'unknown'), + 'ipv4': iface_data.get('ipv4', {}), + 'ipv6': iface_data.get('ipv6', []) + } + + interfaces.append(interface_info) + + return interfaces + +def run_module(): + # define available arguments/parameters a user can pass to the module + module_args = dict() + + # seed the result dict in the object + result = dict( + changed=False, + details=False, + ) + + # the AnsibleModule object will be our abstraction working with Ansible + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # if the user is working with this module in only check mode we do not + # want to make any changes to the environment, just return the current + # state with no modifications + if module.check_mode: + module.exit_json(**result) + + # Get the facts from the host + facts = ansible_facts(module) + + # Gather OS information... + result["os"] = {} + result["os"]["distribution"] = facts.get("distribution", "unknown") + result["os"]["distribution_version"] = facts.get("distribution_version", "unknown") + result["os"]["os_family"] = facts.get("os_family", "unknown") + result["os"]["kernel"] = facts.get("kernel", "unknown") + result["os"]["architecture"] = facts.get("architecture", "unknown") + result["os"]["hostname"] = facts.get("hostname", "unknown") + result["os"]["fqdn"] = facts.get("fqdn", "unknown") + + # Gather hardware information... + result["hardware"] = {} + result["hardware"]["cpu"] = {} + result["hardware"]["cpu"]["processor_count"] = facts.get("processor_count", 0) + result["hardware"]["cpu"]["processor_cores"] = facts.get("processor_cores", 0) + result["hardware"]["cpu"]["processor_vcpus"] = facts.get("processor_vcpus", 0) + result["hardware"]["cpu"]["processor_threads_per_core"] = facts.get("processor_threads_per_core", 0) + result["hardware"]["cpu"]["processor"] = facts.get("processor", []) + result["hardware"]["memory"] = {} + result["hardware"]["memory"]["memtotal_mb"] = facts.get("memtotal_mb", 0) + result["hardware"]["memory"]["memfree_mb"] = facts.get("memfree_mb", 0) + result["hardware"]["memory"]["swaptotal_mb"] = facts.get("swaptotal_mb", 0) + result["hardware"]["disk"] = build_disk_list(facts.get("mounts", [])) + + # Gather security information... + result["security"] = {} + result["security"]["selinux"] = facts.get("selinux", {"status": "not available"}) + + # Is firewalld running? + firewalld = facts.get('services', {}).get('firewalld.service') + if firewalld: + result["security"]["firewalld"] = firewalld + + # Gather networking information... + result["networking"] = {} + result["networking"]["interfaces"] = build_interface_list(facts) + result["networking"]["default_ipv4"] = facts.get("default_ipv4", {}) + result["networking"]["default_ipv6"] = facts.get("default_ipv6", {}) + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + +def main(): + run_module() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml new file mode 100644 index 00000000..2c0f52dc --- /dev/null +++ b/roles/redis/tasks/certify-redis.yml @@ -0,0 +1,542 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Ensure report directory exists + ansible.builtin.file: + path: "{{ redis_report_dir }}" + state: directory + owner: "{{ redis_owner }}" + group: "{{ redis_group }}" + mode: "0755" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_info + +- name: Check if Redis service exists + ansible.builtin.systemd: + name: redis + register: redis_service_check + failed_when: false + changed_when: false + +- name: Get Redis service status + ansible.builtin.systemd: + name: redis + register: redis_service_status + when: redis_service_check.status is defined + +- name: Check Redis process + ansible.builtin.shell: set -o pipefail && ps aux | grep redis-server | grep -v grep + register: redis_process + failed_when: false + changed_when: false + +- name: Test Redis connectivity + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user admin \ + -p {{ redis_port }} \ + -a "{{ redis_user_admin_password }}" \ + --no-auth-warning \ + PING + register: redis_ping + failed_when: false + changed_when: false + +- name: Get Redis version + ansible.builtin.shell: set -o pipefail && redis-server --version + register: redis_version + changed_when: false + +- name: Get Redis INFO + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user admin \ + -p {{ redis_port }} \ + -a "{{ redis_user_admin_password }}" \ + --no-auth-warning \ + INFO + register: redis_info + when: redis_ping.rc == 0 + failed_when: false + changed_when: false + +- name: Get Redis configuration + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_port }} \ + -a "{{ redis_user_admin_password }}" \ + --no-auth-warning \ + CONFIG GET '*' + register: redis_config + when: redis_ping.rc == 0 + failed_when: false + changed_when: false + +- name: Check Redis configuration file + ansible.builtin.stat: + path: /etc/redis/redis.conf + register: redis_conf_file + +- name: Get Redis configuration file permissions + ansible.builtin.shell: set -o pipefail && ls -lh /etc/redis/redis.conf + register: redis_conf_permissions + when: redis_conf_file.stat.exists + changed_when: false + +- name: Check if Redis is using systemd + ansible.builtin.stat: + path: /usr/lib/systemd/system/redis.service + register: redis_systemd_file + +- name: Get listening ports + ansible.builtin.shell: set -o pipefail && netstat -tlnp | grep redis | ss -tlnp | grep redis + register: redis_ports + failed_when: false + changed_when: false + +- name: Check Redis log file + ansible.builtin.shell: | + set -o pipefail && + if [ -f /var/log/redis/redis.log ]; then + tail -50 /var/log/redis/redis.log + else + echo "Log file not found in standard location" + fi + register: redis_logs + changed_when: false + +- name: Parse Redis INFO for key metrics + ansible.builtin.set_fact: + redis_metrics: + version: "{{ redis_info.stdout | regex_search('redis_version:([^\\r\\n]+)', '\\1') }}" + os: "{{ redis_info.stdout | regex_search('os:([^\\r\\n]+)', '\\1') }}" + executable: "{{ redis_info.stdout | regex_search('executable:([^\\r\\n]+)', '\\1') }}" + config_file: "{{ redis_info.stdout | regex_search('config_file:([^\\r\\n]+)', '\\1') }}" + port: "{{ redis_info.stdout | regex_search('tcp_port:([^\\r\\n]+)', '\\1') }}" + role: "{{ redis_info.stdout | regex_search('role:([^\\r\\n]+)', '\\1') }}" + mode: "{{ redis_info.stdout | regex_search('redis_mode:([^\\r\\n]+)', '\\1') }}" + slaves: "{{ redis_info.stdout | regex_search('connected_slaves:([^\\r\\n]+)', '\\1') }}" + master_host: "{{ redis_info.stdout | regex_search('master_host:([^\\r\\n]+)', '\\1') }}" + master_port: "{{ redis_info.stdout | regex_search('master_port:([^\\r\\n]+)', '\\1') }}" + master_link: "{{ redis_info.stdout | regex_search('master_link_status:([^\\r\\n]+)', '\\1') }}" + clients: "{{ redis_info.stdout | regex_search('connected_clients:([^\\r\\n]+)', '\\1') }}" + bind_address: "{{ redis_config.results[2].stdout_lines[1] | default(['0.0.0.0'], true) }}" + users: "{{ redis_config.results[0].stdout_lines | default(['N/A'], true) }}" + when: redis_info.rc is defined and redis_info.rc == 0 + +- name: Get list of configured Redis users + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_port }} \ + -a "{{ redis_user_admin_password }}" \ + --no-auth-warning \ + ACL LIST + when: redis_info.rc is defined and redis_info.rc == 0 + register: redis_acl_list + no_log: false + failed_when: false + changed_when: false + +- name: Parse Redis ACL list into structured format + ansible.builtin.set_fact: + redis_users: >- + {%- set result = [] -%} + {%- for acl_entry in (redis_acl_list.stdout | from_json) -%} + {%- set parts = acl_entry.split() -%} + {%- if parts | length >= 3 and parts[0] == 'user' -%} + {%- set user_obj = {'user': parts[1], 'enabled': (parts[2] == 'on')} -%} + {%- set _ = result.append(user_obj) -%} + {%- endif -%} + {%- endfor -%} + {{ result }} + when: redis_info.rc is defined and redis_info.rc == 0 + +- name: Confirm "itential" user login + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user itential \ + -p {{ redis_port }} \ + -a "{{ redis_user_itential_password }}" \ + --no-auth-warning \ + PING + register: redis_itential_user_ping + failed_when: false + changed_when: false + +- name: Confirm "repluser" user login + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user repluser \ + -p {{ redis_port }} \ + -a "{{ redis_user_repluser_password }}" \ + --no-auth-warning \ + PING + register: redis_repl_user_ping + failed_when: false + changed_when: false + +- name: Confirm "sentineluser" user login + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user sentineluser \ + -p {{ redis_port }} \ + -a "{{ redis_user_sentineluser_password }}" \ + --no-auth-warning \ + PING + register: redis_sentineluser_user_ping + failed_when: false + changed_when: false + +- name: Confirm "prometheus" user login + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user prometheus \ + -p {{ redis_port }} \ + -a "{{ redis_user_prometheus_password }}" \ + --no-auth-warning \ + PING + register: redis_prometheus_user_ping + failed_when: false + changed_when: false + +# ========================================================================= +# SENTINEL DETECTION +# ========================================================================= + +- name: Check if Sentinel service exists + ansible.builtin.systemd: + name: redis-sentinel + register: sentinel_service_check + failed_when: false + changed_when: false + +- name: Check Sentinel process + ansible.builtin.shell: set -o pipefail && ps aux | grep redis-sentinel | grep -v grep + register: sentinel_process + failed_when: false + changed_when: false + +- name: Test Sentinel connectivity + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + PING + register: sentinel_ping + failed_when: false + changed_when: false + +- name: Set Sentinel detection fact + ansible.builtin.set_fact: + sentinel_is_running: "{{ sentinel_ping.rc == 0 and sentinel_process.rc == 0 }}" + +- name: Display Sentinel detection status + ansible.builtin.debug: + msg: "Sentinel detected: {{ sentinel_is_running }}" + +# ========================================================================= +# SENTINEL-SPECIFIC TASKS (Only run if Sentinel is detected) +# ========================================================================= + +- name: Get Sentinel service status + ansible.builtin.systemd: + name: redis-sentinel + register: sentinel_service_status + when: + - sentinel_is_running | bool + - sentinel_service_check.status is defined + failed_when: false + +- name: Get Sentinel INFO + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + INFO + register: sentinel_info + when: sentinel_is_running | bool + changed_when: false + +- name: Get Sentinel masters + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + SENTINEL MASTERS + register: sentinel_masters + when: sentinel_is_running | bool + changed_when: false + +- name: Capture itentialmaster + ansible.builtin.set_fact: + itential_master: "{{ (sentinel_masters.stdout | from_json)[0] }}" + +- name: Get details for each monitored master + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + SENTINEL MASTER {{ itential_master.name }} + register: monitored_master + # loop: "{{ master_names.stdout_lines | default([]) }}" + when: sentinel_is_running | bool + changed_when: false + +- name: Capture monitored master details + ansible.builtin.set_fact: + monitored_master_details: "{{ (monitored_master.stdout | from_json) }}" + +- name: Get known sentinels for each master + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + SENTINEL SENTINELS {{ itential_master.name }} + register: known_sentinels + when: sentinel_is_running | bool + failed_when: false + changed_when: false + +- name: Capture known sentinel details + ansible.builtin.set_fact: + known_sentinel_details: "{{ (known_sentinels.stdout | from_json) }}" + +- name: Get known replicas for each master + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + SENTINEL REPLICAS {{ itential_master.name }} + register: known_replicas + when: sentinel_is_running | bool + failed_when: false + changed_when: false + +- name: Capture known replica details + ansible.builtin.set_fact: + known_replica_details: "{{ (known_replicas.stdout | from_json) }}" + +- name: Check master status + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + SENTINEL CKQUORUM {{ itential_master.name }} + register: quorum_check + when: sentinel_is_running | bool + failed_when: false + changed_when: false + +- name: Capture known replica details + ansible.builtin.set_fact: + quorum_check_details: "{{ (quorum_check.stdout | from_json) }}" + +- name: Get Sentinel configuration + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + CONFIG GET '*' + register: sentinel_config + when: sentinel_is_running | bool + failed_when: false + changed_when: false + +- name: Check Sentinel configuration file + ansible.builtin.stat: + path: /etc/redis/sentinel.conf + register: sentinel_conf_file + when: sentinel_is_running | bool + +- name: Get Sentinel configuration file permissions + ansible.builtin.shell: set -o pipefail && ls -lh /etc/redis/sentinel.conf + register: sentinel_conf_permissions + when: + - sentinel_is_running | bool + - sentinel_conf_file.stat.exists | default(false) + changed_when: false + +- name: Check if Sentinel is using systemd + ansible.builtin.stat: + path: /usr/lib/systemd/system/redis-sentinel.service + register: sentinel_systemd_file + when: sentinel_is_running | bool + +- name: Get Sentinel listening ports + ansible.builtin.shell: | + set -o pipefail && netstat -tlnp | grep sentinel | ss -tlnp | grep sentinel + register: redis_sentinel_ports + when: sentinel_is_running | bool + failed_when: false + changed_when: false + +- name: Check Sentinel log file + ansible.builtin.shell: | + set -o pipefail && + if [ -f /var/log/redis/sentinel.log ]; then + tail -50 /var/log/redis/sentinel.log + else + echo "Log file not found in standard location" + fi + register: sentinel_logs + when: sentinel_is_running | bool + changed_when: false + +# For unknown reasons there are control characters (^M) at the end of the +# SENTINEL INFO values. This task will remove those characters. +- name: Remove control characters from output + ansible.builtin.set_fact: + sentinel_info_clean: "{{ sentinel_info.stdout | regex_replace('\\r', '') }}" + when: + - sentinel_is_running | bool + - sentinel_info.rc is defined + - sentinel_info.rc == 0 + +- name: Parse Sentinel INFO for key metrics + ansible.builtin.set_fact: + sentinel_metrics: + version: "{{ sentinel_info_clean | regex_search('redis_version:(.+)', '\\1') }}" + mode: "{{ sentinel_info_clean | regex_search('redis_mode:(.+)', '\\1') }}" + masters: "{{ sentinel_info_clean | regex_search('sentinel_masters:(.+)', '\\1') }}" + when: + - sentinel_is_running | bool + - sentinel_info.rc is defined + - sentinel_info.rc == 0 + +# ========================================================================= +# Confirm all expected Sentinel users +# ========================================================================= +- name: Get list of configured Sentinel users + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --json \ + --user admin \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineladmin_password }}" \ + --no-auth-warning \ + ACL LIST + when: + - sentinel_is_running | bool + - sentinel_info.rc is defined + - sentinel_info.rc == 0 + register: sentinel_acl_list + no_log: true + failed_when: false + changed_when: false + +- name: Parse Sentinel ACL list into structured format + ansible.builtin.set_fact: + sentinel_users: >- + {%- set result = [] -%} + {%- for acl_entry in (sentinel_acl_list.stdout | from_json) -%} + {%- set parts = acl_entry.split() -%} + {%- if parts | length >= 3 and parts[0] == 'user' -%} + {%- set user_obj = {'user': parts[1], 'enabled': (parts[2] == 'on')} -%} + {%- set _ = result.append(user_obj) -%} + {%- endif -%} + {%- endfor -%} + {{ result }} + when: + - sentinel_is_running | bool + - sentinel_acl_list.rc is defined + - sentinel_acl_list.rc == 0 + +- name: Verify the Sentinel user can login (not admin) + ansible.builtin.shell: | + set -o pipefail && + redis-cli \ + --user sentineluser \ + -p {{ redis_sentinel_port }} \ + -h "{{ inventory_hostname }}" \ + -a "{{ redis_user_sentineluser_password }}" \ + --no-auth-warning \ + PING + register: sentinel_user_ping + no_log: true + failed_when: false + changed_when: false + when: + - sentinel_is_running | bool + - sentinel_acl_list.rc is defined + - sentinel_acl_list.rc == 0 + +# ========================================================================= +# Generate the report +# ========================================================================= +- name: Generate validation report + ansible.builtin.template: + backup: true + dest: "{{ redis_report_file }}" + group: "{{ redis_group }}" + mode: "0665" + owner: "{{ redis_owner }}" + # src: certify-report.j2 + src: certify-report-alt.j2 + +- name: Display report summary + ansible.builtin.debug: + msg: + - "Redis validation complete for {{ inventory_hostname }}" + - "Overall Status: {{ 'PASSED ✓' if (redis_ping.rc == 0 and redis_process.rc == 0) else 'FAILED ✗' }}" + - "Report saved to: {{ redis_report_file }}" + +- name: Display report location + ansible.builtin.debug: + msg: "Full report available at: {{ redis_report_file }}" + # run_once: no diff --git a/roles/redis/templates/certify-report-md.j2 b/roles/redis/templates/certify-report-md.j2 new file mode 100644 index 00000000..d3cfb08f --- /dev/null +++ b/roles/redis/templates/certify-report-md.j2 @@ -0,0 +1,305 @@ +# Redis Installation Validation Report + +**Generated:** {{ ansible_date_time.iso8601 | default('Unknown') }} +**Hostname:** {{ inventory_hostname | default('Unknown') }} +**IP Address:** {{ ansible_default_ipv4.address | default('N/A') }} +**OS:** {{ ansible_distribution | default('Unknown') }} {{ ansible_distribution_version | default('') }} + +--- + +## Service Status + +{% if redis_service_status is defined and redis_service_status.status is defined %} +- **Service Name:** {{ redis_service_status.name | default('Unknown') }} +- **Service State:** {{ redis_service_status.status.ActiveState | default('Unknown') }} +- **Service SubState:** {{ redis_service_status.status.SubState | default('Unknown') }} +- **Service Enabled:** {{ redis_service_status.status.UnitFileState | default('Unknown') }} +{% else %} +- **Service Status:** Could not determine (service may not exist) +{% endif %} + +**Process Running:** {{ 'YES' if (redis_process is defined and redis_process.rc == 0) else 'NO' }} + +{% if redis_process is defined and redis_process.rc == 0 %} +**Process Details:** +``` +{{ redis_process.stdout | default('N/A') }} +``` +{% endif %} + +--- + +## Connectivity + +- **Redis Port:** {{ redis_port | default('6379') }} +- **Ping Response:** {{ redis_ping.stdout | default('FAILED') if redis_ping is defined else 'FAILED' }} +- **Connection Status:** {{ 'SUCCESS' if (redis_ping is defined and redis_ping.rc == 0) else 'FAILED' }} + +**Listening Ports:** +``` +{{ redis_ports.stdout | default('Could not determine') if redis_ports is defined else 'Could not determine' }} +``` + +--- + +## Version Information + +``` +{{ redis_version.stdout | default('Version information not available') if redis_version is defined else 'Version information not available' }} +``` + +--- + +## Redis Metrics (from INFO) + +{% if redis_metrics is defined %} +- **OS:** {{ redis_metrics.os | default(['N/A'], true) | first }} +- **Redis Version:** {{ redis_metrics.version | default(['N/A'], true) | first }} +- **Executable:** {{ redis_metrics.executable | default(['N/A'], true) | first }} +- **Redis Mode:** {{ redis_metrics.mode | default(['N/A'], true) | first }} +- **Role:** {{ redis_metrics.role | default(['N/A'], true) | first }} +- **Connected Clients:** {{ redis_metrics.clients | default(['N/A'], true) | first }} +- **Connected Slaves:** {{ redis_metrics.slaves | default(['0'], true) | first }} +- **Master Host:** {{ redis_metrics.master_host | default(['N/A'], true) | first }} +- **Master Port:** {{ redis_metrics.master_port | default(['N/A'], true) | first }} +- **Master Connection:** {{ redis_metrics.master_link | default(['N/A'], true) | first }} +{% else %} +Redis INFO not available - check connectivity and authentication +{% endif %} + +--- + +## Configuration File + +- **Config File Exists:** {{ redis_conf_file.stat.exists | default('Unknown') if redis_conf_file is defined else 'Unknown' }} +{% if redis_conf_file is defined and redis_conf_file.stat.exists %} +- **Config File Path:** `/etc/redis/redis.conf` +- **Permissions:** {{ redis_conf_permissions.stdout | default('Unknown') if redis_conf_permissions is defined else 'Unknown' }} +{% endif %} + +- **Systemd Unit File:** {{ redis_systemd_file.stat.exists | default('Unknown') if redis_systemd_file is defined else 'Unknown' }} +{% if redis_systemd_file is defined and redis_systemd_file.stat.exists %} +- **Unit File Path:** `/etc/systemd/system/redis.service` +{% endif %} + +--- + +## Redis User Auth Tests + +**The following users were found:** + +{% if redis_users is defined and redis_users | length > 0 %} +{% for redis in redis_users %} +### User: {{ redis.user | default('Unknown') }} +- **Enabled:** {{ redis.enabled | default(false) }} +{% endfor %} + +### User Connection Test Results: +- **admin:** {{ 'PASSED ✓' if (redis_ping is defined and redis_ping.rc == 0) else 'FAILED ✗' }} +- **itential:** {{ 'PASSED ✓' if (redis_itential_user_ping is defined and redis_itential_user_ping.rc == 0) else 'FAILED ✗' }} +- **repluser:** {{ 'PASSED ✓' if (redis_repl_user_ping is defined and redis_repl_user_ping.rc == 0) else 'FAILED ✗' }} +- **sentineluser:** {{ 'PASSED ✓' if (redis_sentineluser_user_ping is defined and redis_sentineluser_user_ping.rc == 0) else 'FAILED ✗' }} +- **prometheus:** {{ 'PASSED ✓' if (redis_prometheus_user_ping is defined and redis_prometheus_user_ping.rc == 0) else 'FAILED ✗' }} +{% else %} +No Redis users were found! +{% endif %} + +--- + +## Recent Log Entries (Last 50 lines) + +``` +{{ redis_logs.stdout | default('Log entries not available') if redis_logs is defined else 'Log entries not available' }} +``` + +{% if sentinel_service_status is defined and sentinel_service_status.status is defined %} +--- + +## Sentinel Service Status + +- **Service Name:** {{ sentinel_service_status.name | default('Unknown') }} +- **Service State:** {{ sentinel_service_status.status.ActiveState | default('Unknown') }} +- **Service SubState:** {{ sentinel_service_status.status.SubState | default('Unknown') }} +- **Service Enabled:** {{ sentinel_service_status.status.UnitFileState | default('Unknown') }} + +**Process Running:** {{ 'YES' if (sentinel_process is defined and sentinel_process.rc == 0) else 'NO' }} + +{% if sentinel_process is defined and sentinel_process.rc == 0 %} +**Process Details:** +``` +{{ sentinel_process.stdout | default('N/A') }} +``` +{% endif %} + +--- + +## Sentinel Connectivity + +- **Sentinel Port:** {{ redis_sentinel_port | default('26379') }} +- **Ping Response:** {{ sentinel_ping.stdout | default('FAILED') if sentinel_ping is defined else 'FAILED' }} +- **Connection Status:** {{ 'SUCCESS' if (sentinel_ping is defined and sentinel_ping.rc == 0) else 'FAILED' }} + +**Listening Ports:** +``` +{{ sentinel_ports.stdout | default('Could not determine') if sentinel_ports is defined else 'Could not determine' }} +``` + +--- + +## Sentinel Metrics (from INFO) + +{% if sentinel_metrics is defined %} +- **Redis Version:** {{ sentinel_metrics.version | default(['N/A'], true) | first }} +- **Redis Mode:** {{ sentinel_metrics.mode | default(['N/A'], true) | first }} +- **Monitored Masters:** {{ sentinel_metrics.masters | default(['N/A'], true) | first }} +{% else %} +Sentinel INFO not available +{% endif %} + +--- + +## Sentinel Master + +{% if itential_master is defined and itential_master.name is defined and itential_master.name == "itentialmaster" %} +**Number of Masters:** 1 +**Master Name:** {{ itential_master.name }} + +{% if monitored_master_details is defined %} +### Master Details: +- **IP:** {{ monitored_master_details.ip | default('N/A') }} +- **Connected Slaves:** {{ monitored_master_details["num-slaves"] | default('N/A') }} +- **Port:** {{ monitored_master_details.port | default('N/A') }} +- **Quorum:** {{ monitored_master_details.quorum | default('N/A') }} +- **Down After (ms):** {{ monitored_master_details["down-after-milliseconds"] | default('N/A') }} +- **Failover Timeout:** {{ monitored_master_details["failover-timeout"] | default('N/A') }} +- **Parallel Syncs:** {{ monitored_master_details["parallel-syncs"] | default('N/A') }} +{% endif %} +{% else %} +No masters are being monitored +{% endif %} + +--- + +## Sentinel Quorum Status + +{% if quorum_check_details is defined and quorum_check_details %} +- **Master:** {{ itential_master.name | default('Unknown') if itential_master is defined else 'Unknown' }} +- **Status:** {{ quorum_check_details }} +{% else %} +No quorum was found! +{% endif %} + +--- + +## Sentinel Known Sentinels + +**This Sentinel is aware of the following other Sentinels:** + +{% if known_sentinel_details is defined and known_sentinel_details | length > 0 %} +{% for sentinel in known_sentinel_details %} +### Sentinel: {{ sentinel.name | default('N/A') }} +- **IP:** {{ sentinel.ip | default('N/A') }} +- **Runid:** {{ sentinel.runid | default('N/A') }} +- **Port:** {{ sentinel.port | default('N/A') }} +- **Flags:** {{ sentinel.flags | default('N/A') }} + +{% endfor %} +{% else %} +No other Sentinels were found! +{% endif %} + +--- + +## Sentinel Known Replicas + +**This Sentinel is aware of the following Replicas:** + +{% if known_replica_details is defined and known_replica_details | length > 0 %} +{% for replica in known_replica_details %} +### Replica: {{ replica.name | default('N/A') }} +- **Master:** {{ replica["master-host"] | default('N/A') }}:{{ replica["master-port"] | default('N/A') }} +- **Master Connectivity:** {{ replica["master-link-status"] | default('N/A') }} +- **Replica Role:** {{ replica["role-reported"] | default('N/A') }} +- **Flags:** {{ replica.flags | default('N/A') }} + +{% endfor %} +{% else %} +No other Replicas were found! +{% endif %} + +--- + +## Sentinel Configuration File + +- **Config File Exists:** {{ sentinel_conf_file.stat.exists | default('Unknown') if sentinel_conf_file is defined else 'Unknown' }} +{% if sentinel_conf_file is defined and sentinel_conf_file.stat.exists | default(false) %} +- **Config File Path:** `/etc/redis/sentinel.conf` +- **Permissions:** {{ sentinel_conf_permissions.stdout | default('Unknown') if sentinel_conf_permissions is defined else 'Unknown' }} +{% endif %} + +- **Systemd Unit File:** {{ sentinel_systemd_file.stat.exists | default('Unknown') if sentinel_systemd_file is defined else 'Unknown' }} +{% if sentinel_systemd_file is defined and sentinel_systemd_file.stat.exists | default(false) %} +- **Unit File Path:** `/etc/systemd/system/redis-sentinel.service` +{% endif %} + +--- + +## Sentinel User Auth Tests + +**The following users were found:** + +{% if sentinel_users is defined and sentinel_users | length > 0 %} +{% for sentinel in sentinel_users %} +### User: {{ sentinel.user | default('Unknown') }} +- **Enabled:** {{ sentinel.enabled | default(false) }} +{% endfor %} + +### Connection Test Results: +- **admin:** {{ 'PASSED ✓' if (sentinel_ping is defined and sentinel_ping.rc == 0) else 'FAILED ✗' }} +- **sentineluser:** {{ 'PASSED ✓' if (sentinel_user_ping is defined and sentinel_user_ping.rc == 0) else 'FAILED ✗' }} +{% else %} +No Sentinel users were found! +{% endif %} + +--- + +## Sentinel Recent Log Entries (Last 50 lines) + +``` +{{ sentinel_logs.stdout | default('Log entries not available') if sentinel_logs is defined else 'Log entries not available' }} +``` + +{% endif %} + +--- + +## Validation Summary + +**Overall Status:** {{ 'PASSED' if (redis_ping is defined and redis_ping.rc == 0 and redis_process is defined and redis_process.rc == 0) else 'FAILED' }} + +### Checks: + +{% if redis_service_status is defined and redis_service_status.status is defined %} +- **Redis Service Exists:** YES ✓ +- **Redis Service Active:** {{ redis_service_status.status.ActiveState | default('unknown') | upper }} {{ '✓' if redis_service_status.status.ActiveState == 'active' else '✗' }} +{% else %} +- **Redis Service Exists:** NO ✗ +{% endif %} +- **Redis Process Running:** {{ 'YES' if (redis_process is defined and redis_process.rc == 0) else 'NO' }} {{ '✓' if (redis_process is defined and redis_process.rc == 0) else '✗' }} +- **Redis Responding:** {{ 'YES' if (redis_ping is defined and redis_ping.rc == 0) else 'NO' }} {{ '✓' if (redis_ping is defined and redis_ping.rc == 0) else '✗' }} +- **Redis Config File Present:** {{ 'YES' if (redis_conf_file is defined and redis_conf_file.stat.exists) else 'NO' }} {{ '✓' if (redis_conf_file is defined and redis_conf_file.stat.exists) else '✗' }} +{% if sentinel_service_status is defined and sentinel_service_status.status is defined %} +- **Sentinel Service Exists:** YES ✓ +- **Sentinel Service Active:** {{ sentinel_service_status.status.ActiveState | default('unknown') | upper }} {{ '✓' if sentinel_service_status.status.ActiveState == 'active' else '✗' }} +- **Sentinel Process Running:** {{ 'YES' if (sentinel_process is defined and sentinel_process.rc == 0) else 'NO' }} {{ '✓' if (sentinel_process is defined and sentinel_process.rc == 0) else '✗' }} +- **Sentinel Responding:** {{ 'YES' if (sentinel_ping is defined and sentinel_ping.rc == 0) else 'NO' }} {{ '✓' if (sentinel_ping is defined and sentinel_ping.rc == 0) else '✗' }} +- **Sentinel Config File Present:** {{ 'YES' if (sentinel_conf_file is defined and sentinel_conf_file.stat.exists | default(false)) else 'NO' }} {{ '✓' if (sentinel_conf_file is defined and sentinel_conf_file.stat.exists | default(false)) else '✗' }} +- **Sentinel Monitoring:** {{ ([itential_master.name] if itential_master is defined and itential_master.name is defined else []) | length }} master(s) {{ '✓' if (itential_master is defined and itential_master.name is defined) else '✗' }} +- **Sentinel Quorum:** {{ 'OK' if (quorum_check_details is defined and 'OK' in quorum_check_details) else 'FAILED' }} {{ '✓' if (quorum_check_details is defined and 'OK' in quorum_check_details) else '✗' }} +{% else %} +- **Sentinel Service:** NOT RUNNING OR NOT DETECTED +{% endif %} + +--- + +**End of Report** \ No newline at end of file From 8835c3e75bec44fc6b5a2c1f4faa216978244908 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Fri, 16 Jan 2026 07:37:15 -0500 Subject: [PATCH 2/2] Fix template issues --- roles/redis/defaults/main/install.yml | 4 ++ roles/redis/tasks/certify-redis.yml | 8 ++- roles/redis/templates/certify-report-md.j2 | 60 ++++++++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/roles/redis/defaults/main/install.yml b/roles/redis/defaults/main/install.yml index ac62dd8b..8323f581 100644 --- a/roles/redis/defaults/main/install.yml +++ b/roles/redis/defaults/main/install.yml @@ -27,3 +27,7 @@ redis_remi_repo_url: "http://rpms.remirepo.net/enterprise/remi-release-\ {{ ansible_distribution_version }}.rpm" redis_epel_repo_url: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-\ {{ ansible_distribution_major_version }}.noarch.rpm" + +# The name and location of the certification report +redis_report_dir: "/tmp/itential-reports" +redis_report_file: "{{ redis_report_dir }}/redis_report_{{ inventory_hostname }}.md" \ No newline at end of file diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 2c0f52dc..639f5410 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -12,7 +12,7 @@ - name: Gather host information itential.deployer.gather_host_information: - register: host_info + register: host_details - name: Check if Redis service exists ansible.builtin.systemd: @@ -527,7 +527,11 @@ mode: "0665" owner: "{{ redis_owner }}" # src: certify-report.j2 - src: certify-report-alt.j2 + src: certify-report-md.j2 + +- name: debug + debug: + msg: "{{ host_details }}" - name: Display report summary ansible.builtin.debug: diff --git a/roles/redis/templates/certify-report-md.j2 b/roles/redis/templates/certify-report-md.j2 index d3cfb08f..0a4353c7 100644 --- a/roles/redis/templates/certify-report-md.j2 +++ b/roles/redis/templates/certify-report-md.j2 @@ -7,6 +7,56 @@ --- +## Host Details + +{% if host_details is defined %} +### Operating System +- **Distribution:** {{ host_details.os.distribution | default('Unknown') }} {{ host_details.os.distribution_version | default('') }} +- **OS Family:** {{ host_details.os.os_family | default('Unknown') }} +- **Kernel:** {{ host_details.os.kernel | default('Unknown') }} +- **Architecture:** {{ host_details.os.architecture | default('Unknown') }} +- **Hostname:** {{ host_details.os.hostname | default('Unknown') }} +- **FQDN:** {{ host_details.os.fqdn | default('Unknown') }} + +### Hardware +- **CPU Count:** {{ host_details.hardware.cpu.processor_count | default('Unknown') }} +- **CPU Cores:** {{ host_details.hardware.cpu.processor_cores | default('Unknown') }} +- **CPU vCPUs:** {{ host_details.hardware.cpu.processor_vcpus | default('Unknown') }} +- **Threads per Core:** {{ host_details.hardware.cpu.processor_threads_per_core | default('Unknown') }} +- **Total Memory:** {{ host_details.hardware.memory.memtotal_mb | default('Unknown') }} MB +- **Free Memory:** {{ host_details.hardware.memory.memfree_mb | default('Unknown') }} MB +- **Swap Total:** {{ host_details.hardware.memory.swaptotal_mb | default('Unknown') }} MB + +### Disk Mounts +{% if host_details.hardware.disk is defined and host_details.hardware.disk | length > 0 %} +{% for disk in host_details.hardware.disk %} +- **{{ disk.mount }}:** {{ disk.size_gb }} GB +{% endfor %} +{% else %} +- No disk information available +{% endif %} + +### Networking +- **Primary IP:** {{ host_details.networking.default_ipv4.address | default('N/A') }} +- **Gateway:** {{ host_details.networking.default_ipv4.gateway | default('N/A') }} +- **Interface:** {{ host_details.networking.default_ipv4.interface | default('N/A') }} +- **MAC Address:** {{ host_details.networking.default_ipv4.macaddress | default('N/A') }} + +### Security +{% if host_details.security.selinux is defined %} +- **SELinux Status:** {{ host_details.security.selinux.status | default('Unknown') }} +- **SELinux Mode:** {{ host_details.security.selinux.mode | default('Unknown') }} +- **SELinux Type:** {{ host_details.security.selinux.type | default('Unknown') }} +{% endif %} +{% if host_details.security.firewalld is defined %} +- **Firewalld:** {{ host_details.security.firewalld.state | default('Unknown') }} +{% endif %} +{% else %} +Host details not available +{% endif %} + +--- + ## Service Status {% if redis_service_status is defined and redis_service_status.status is defined %} @@ -18,7 +68,7 @@ - **Service Status:** Could not determine (service may not exist) {% endif %} -**Process Running:** {{ 'YES' if (redis_process is defined and redis_process.rc == 0) else 'NO' }} +**Process Running:** {{ 'YES ✓' if (redis_process is defined and redis_process.rc == 0) else 'NO' }} {% if redis_process is defined and redis_process.rc == 0 %} **Process Details:** @@ -33,7 +83,7 @@ - **Redis Port:** {{ redis_port | default('6379') }} - **Ping Response:** {{ redis_ping.stdout | default('FAILED') if redis_ping is defined else 'FAILED' }} -- **Connection Status:** {{ 'SUCCESS' if (redis_ping is defined and redis_ping.rc == 0) else 'FAILED' }} +- **Connection Status:** {{ 'SUCCESS ✓' if (redis_ping is defined and redis_ping.rc == 0) else 'FAILED' }} **Listening Ports:** ``` @@ -122,7 +172,7 @@ No Redis users were found! - **Service SubState:** {{ sentinel_service_status.status.SubState | default('Unknown') }} - **Service Enabled:** {{ sentinel_service_status.status.UnitFileState | default('Unknown') }} -**Process Running:** {{ 'YES' if (sentinel_process is defined and sentinel_process.rc == 0) else 'NO' }} +**Process Running:** {{ 'YES ✓' if (sentinel_process is defined and sentinel_process.rc == 0) else 'NO' }} {% if sentinel_process is defined and sentinel_process.rc == 0 %} **Process Details:** @@ -137,7 +187,7 @@ No Redis users were found! - **Sentinel Port:** {{ redis_sentinel_port | default('26379') }} - **Ping Response:** {{ sentinel_ping.stdout | default('FAILED') if sentinel_ping is defined else 'FAILED' }} -- **Connection Status:** {{ 'SUCCESS' if (sentinel_ping is defined and sentinel_ping.rc == 0) else 'FAILED' }} +- **Connection Status:** {{ 'SUCCESS ✓' if (sentinel_ping is defined and sentinel_ping.rc == 0) else 'FAILED' }} **Listening Ports:** ``` @@ -275,7 +325,7 @@ No Sentinel users were found! ## Validation Summary -**Overall Status:** {{ 'PASSED' if (redis_ping is defined and redis_ping.rc == 0 and redis_process is defined and redis_process.rc == 0) else 'FAILED' }} +**Overall Status:** {{ 'PASSED ✓' if (redis_ping is defined and redis_ping.rc == 0 and redis_process is defined and redis_process.rc == 0) else 'FAILED' }} ### Checks: