From a1d7c17a9d029d37582e5d2fac90f8be2acafbed Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 15 Jan 2026 21:20:19 -0500 Subject: [PATCH 01/11] 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 02/11] 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: From f3be7ebd895b5084da8f74d337059a2653979384 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Fri, 16 Jan 2026 07:39:21 -0500 Subject: [PATCH 03/11] Fix lint issues --- roles/redis/defaults/main/install.yml | 2 +- roles/redis/tasks/certify-redis.yml | 4 ---- roles/redis/templates/certify-report-md.j2 | 8 ++++---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/roles/redis/defaults/main/install.yml b/roles/redis/defaults/main/install.yml index 8323f581..894a6afd 100644 --- a/roles/redis/defaults/main/install.yml +++ b/roles/redis/defaults/main/install.yml @@ -30,4 +30,4 @@ redis_epel_repo_url: "https://dl.fedoraproject.org/pub/epel/epel-release-latest- # 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 +redis_report_file: "{{ redis_report_dir }}/redis_report_{{ inventory_hostname }}.md" diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 639f5410..34b12717 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -529,10 +529,6 @@ # src: certify-report.j2 src: certify-report-md.j2 -- name: debug - debug: - msg: "{{ host_details }}" - - name: Display report summary ansible.builtin.debug: msg: diff --git a/roles/redis/templates/certify-report-md.j2 b/roles/redis/templates/certify-report-md.j2 index 0a4353c7..665cce92 100644 --- a/roles/redis/templates/certify-report-md.j2 +++ b/roles/redis/templates/certify-report-md.j2 @@ -1,9 +1,9 @@ # 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('') }} +- **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('') }} --- From 17fe161a30a7f7551fe914731293cbc00956c31f Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 20 Jan 2026 12:14:26 -0500 Subject: [PATCH 04/11] Add validation report for mongodb --- roles/mongodb/defaults/main/install.yml | 4 + roles/mongodb/tasks/certify-mongodb.yml | 382 ++++++++++++++++++ .../templates/mongodb-validation-report.md.j2 | 353 ++++++++++++++++ roles/redis/tasks/certify-redis.yml | 3 +- ...rt-md.j2 => redis-validation-report.md.j2} | 0 5 files changed, 740 insertions(+), 2 deletions(-) create mode 100644 roles/mongodb/tasks/certify-mongodb.yml create mode 100644 roles/mongodb/templates/mongodb-validation-report.md.j2 rename roles/redis/templates/{certify-report-md.j2 => redis-validation-report.md.j2} (100%) diff --git a/roles/mongodb/defaults/main/install.yml b/roles/mongodb/defaults/main/install.yml index d8b46c46..6444e3d0 100644 --- a/roles/mongodb/defaults/main/install.yml +++ b/roles/mongodb/defaults/main/install.yml @@ -24,3 +24,7 @@ mongodb_mongod_service_delay: 10 # MongoDB status settings mongodb_status_poll: 3 mongodb_status_interval: 10 + +# The name and location of the certification report +mongodb_report_dir: "/tmp/itential-reports" +mongodb_report_file: "{{ mongodb_report_dir }}/mongodb_report_{{ inventory_hostname }}.md" diff --git a/roles/mongodb/tasks/certify-mongodb.yml b/roles/mongodb/tasks/certify-mongodb.yml new file mode 100644 index 00000000..30a72f6c --- /dev/null +++ b/roles/mongodb/tasks/certify-mongodb.yml @@ -0,0 +1,382 @@ +--- +# MongoDB Validation Tasks +# This playbook gathers information to validate MongoDB installation + +- name: Check if MongoDB service exists + ansible.builtin.systemd: + name: mongod + register: mongodb_service_status + ignore_errors: true + +- name: Check if MongoDB process is running + ansible.builtin.shell: ps aux | grep -v grep | grep mongod + register: mongodb_process + ignore_errors: true + changed_when: false + +- name: Check MongoDB listening ports + ansible.builtin.shell: ss -tulpn | grep mongod + register: mongodb_ports + ignore_errors: true + changed_when: false + +- name: Get MongoDB version + ansible.builtin.shell: mongod --version | head -n 1 + register: mongodb_version + ignore_errors: true + changed_when: false + +- name: Check MongoDB config file exists + ansible.builtin.stat: + path: /etc/mongod.conf + register: mongodb_conf_file + +- name: Get MongoDB config file permissions + ansible.builtin.command: ls -la /etc/mongod.conf + register: mongodb_conf_permissions + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Read MongoDB configuration file + ansible.builtin.slurp: + src: /etc/mongod.conf + register: mongodb_config_content + when: mongodb_conf_file.stat.exists + ignore_errors: true + +- name: Parse MongoDB config for data directory + ansible.builtin.shell: grep -E '^\s*dbPath:' /etc/mongod.conf | awk '{print $2}' + register: mongodb_data_dir + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Parse MongoDB config for log path + ansible.builtin.shell: grep -E '^\s*path:' /etc/mongod.conf | awk '{print $2}' + register: mongodb_log_path + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Check MongoDB systemd unit file + ansible.builtin.stat: + path: /usr/lib/systemd/system/mongod.service + register: mongodb_systemd_file + +- name: Check data directory exists and permissions + ansible.builtin.stat: + path: "{{ mongodb_data_dir.stdout }}" + register: mongodb_data_dir_stat + when: mongodb_data_dir.stdout is defined and mongodb_data_dir.stdout != "" + ignore_errors: true + +- name: Get data directory size + ansible.builtin.command: du -sh {{ mongodb_data_dir.stdout }} + register: mongodb_data_size + ignore_errors: true + changed_when: false + when: mongodb_data_dir.stdout is defined and mongodb_data_dir.stdout != "" + +# ============================================================================ +# CONNECTIVITY TESTS +# ============================================================================ + +- name: Test basic MongoDB connection (no auth) + ansible.builtin.shell: | + mongosh --quiet --eval "db.adminCommand('ping')" 2>&1 + register: mongodb_ping_noauth + ignore_errors: true + changed_when: false + +- name: Test MongoDB connection with admin user + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "db.adminCommand('ping')" + register: mongodb_ping_admin + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +# ============================================================================ +# TLS/SSL CHECKS +# ============================================================================ + +- name: Check if TLS is enabled in config + ansible.builtin.shell: | + grep -E '^\s*mode:\s*requireTLS|^\s*mode:\s*preferTLS|^\s*mode:\s*allowTLS' /etc/mongod.conf + register: mongodb_tls_mode + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Get TLS certificate path from config + ansible.builtin.shell: | + grep -E '^\s*certificateKeyFile:' /etc/mongod.conf | awk '{print $2}' + register: mongodb_tls_cert_path + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Check TLS certificate file exists + ansible.builtin.stat: + path: "{{ mongodb_tls_cert_path.stdout }}" + register: mongodb_tls_cert_file + when: mongodb_tls_cert_path.stdout is defined and mongodb_tls_cert_path.stdout != "" + ignore_errors: true + +- name: Get TLS certificate expiration + ansible.builtin.shell: | + openssl x509 -in {{ mongodb_tls_cert_path.stdout }} -noout -enddate + register: mongodb_tls_cert_expiry + ignore_errors: true + changed_when: false + when: + - mongodb_tls_cert_path.stdout is defined + - mongodb_tls_cert_path.stdout != "" + - mongodb_tls_cert_file.stat.exists | default(false) + +- name: Test MongoDB connection with TLS + ansible.builtin.shell: | + mongosh --tls --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "db.adminCommand('ping')" + register: mongodb_ping_tls + ignore_errors: true + changed_when: false + no_log: true + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined + - mongodb_tls_mode.rc == 0 + +# ============================================================================ +# SERVER STATUS AND METRICS +# ============================================================================ + +- name: Get MongoDB server status + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.serverStatus())" + register: mongodb_server_status + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +- name: Get MongoDB build info + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.serverBuildInfo())" + register: mongodb_build_info + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +# ============================================================================ +# REPLICA SET STATUS +# ============================================================================ + +- name: Check if replica set is configured + ansible.builtin.shell: | + grep -E '^\s*replSetName:' /etc/mongod.conf | awk '{print $2}' + register: mongodb_replset_name + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Get replica set status + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(rs.status())" + register: mongodb_replset_status + ignore_errors: true + changed_when: false + no_log: true + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined + - mongodb_replset_name.stdout is defined + - mongodb_replset_name.stdout != "" + +- name: Get replica set configuration + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(rs.conf())" + register: mongodb_replset_config + ignore_errors: true + changed_when: false + no_log: true + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined + - mongodb_replset_name.stdout is defined + - mongodb_replset_name.stdout != "" + +- name: Get replica set member details + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(rs.isMaster())" + register: mongodb_replset_ismaster + ignore_errors: true + changed_when: false + no_log: true + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined + - mongodb_replset_name.stdout is defined + - mongodb_replset_name.stdout != "" + +# ============================================================================ +# USER AUTHENTICATION TESTS +# ============================================================================ + +- name: Get list of MongoDB users + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin admin --eval "JSON.stringify(db.getUsers())" + register: mongodb_users_list + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +- name: Parse MongoDB users + ansible.builtin.set_fact: + mongodb_users: "{{ (mongodb_users_list.stdout | from_json).users | default([]) }}" + when: + - mongodb_users_list is defined + - mongodb_users_list.rc == 0 + - mongodb_users_list.stdout is defined + failed_when: false + +# Test individual users +- name: Test connection with itential user + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_itential }} -p {{ mongodb_user_itential_password }} \ + --authenticationDatabase "itential" --eval "db.adminCommand('ping')" + register: mongodb_user_itential_ping + ignore_errors: true + changed_when: false + # no_log: true + when: + - mongodb_user_itential is defined + - mongodb_user_itential_password is defined + +# ============================================================================ +# DATABASE INFORMATION +# ============================================================================ + +- name: List all databases + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.adminCommand('listDatabases'))" + register: mongodb_databases + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +- name: Get database stats + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.stats())" + register: mongodb_db_stats + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +# ============================================================================ +# LOGS +# ============================================================================ + +- name: Get recent MongoDB log entries + ansible.builtin.shell: | + if [ -f "{{ mongodb_log_path.stdout }}" ]; then + tail -n 100 {{ mongodb_log_path.stdout }} + else + journalctl -u mongod -n 100 --no-pager + fi + register: mongodb_logs + ignore_errors: true + changed_when: false + when: mongodb_log_path.stdout is defined or mongodb_service_status.status is defined + +# ============================================================================ +# SECURITY CHECKS +# ============================================================================ + +- name: Check if authentication is enabled + ansible.builtin.shell: | + grep -E '^\s*authorization:\s*enabled' /etc/mongod.conf + register: mongodb_auth_enabled + ignore_errors: true + changed_when: false + when: mongodb_conf_file.stat.exists + +- name: Check security settings + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.adminCommand({getCmdLineOpts: 1}))" + register: mongodb_security_settings + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +# ============================================================================ +# PERFORMANCE METRICS +# ============================================================================ + +- name: Get current operations + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "JSON.stringify(db.currentOp())" + register: mongodb_current_ops + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +- name: Get connection count + ansible.builtin.shell: | + mongosh --quiet -u {{ mongodb_user_admin }} -p {{ mongodb_user_admin_password }} \ + --authenticationDatabase admin --eval "db.serverStatus().connections" + register: mongodb_connections + ignore_errors: true + changed_when: false + no_log: true + when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + +# ============================================================================ +# GATHER HOST INFORMATION +# ============================================================================ + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_details + +# ============================================================================ +# GENERATE REPORT +# ============================================================================ +- name: Ensure report directory exists + ansible.builtin.file: + path: "/tmp/itential-reports" + state: directory + owner: "{{ mongodb_owner }}" + group: "{{ mongodb_group }}" + mode: "0755" + +- name: Generate MongoDB validation report + ansible.builtin.template: + src: mongodb-validation-report.md.j2 + dest: "{{ mongodb_report_file }}" + mode: '0644' + owner: "{{ mongodb_owner }}" + group: "{{ mongodb_group }}" + +- name: Display report location + ansible.builtin.debug: + msg: "MongoDB validation report generated at: {{ mongodb_report_file }}" diff --git a/roles/mongodb/templates/mongodb-validation-report.md.j2 b/roles/mongodb/templates/mongodb-validation-report.md.j2 new file mode 100644 index 00000000..bfddc215 --- /dev/null +++ b/roles/mongodb/templates/mongodb-validation-report.md.j2 @@ -0,0 +1,353 @@ +# MongoDB 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('') }} + +--- + +## 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 mongodb_service_status is defined and mongodb_service_status.status is defined %} +- **Service Name:** {{ mongodb_service_status.name | default('Unknown') }} +- **Service State:** {{ mongodb_service_status.status.ActiveState | default('Unknown') }} +- **Service SubState:** {{ mongodb_service_status.status.SubState | default('Unknown') }} +- **Service Enabled:** {{ mongodb_service_status.status.UnitFileState | default('Unknown') }} +{% else %} +- **Service Status:** Could not determine (service may not exist) +{% endif %} + +**Process Running:** {{ 'YES ✓' if (mongodb_process is defined and mongodb_process.rc == 0) else 'NO ✗' }} + +{% if mongodb_process is defined and mongodb_process.rc == 0 %} +**Process Details:** +``` +{{ mongodb_process.stdout | default('N/A') }} +``` +{% endif %} + +--- + +## Connectivity + +**Connection Tests:** +- **No Auth:** {{ 'SUCCESS ✓' if (mongodb_ping_noauth is defined and mongodb_ping_noauth.rc == 0) else 'FAILED ✗' }} +- **Admin User:** {{ 'SUCCESS ✓' if (mongodb_ping_admin is defined and mongodb_ping_admin.rc == 0) else 'FAILED ✗' }} +{% if mongodb_tls_mode.rc == 0 %} +- **TLS Connection:** {{ 'SUCCESS ✓' if (mongodb_ping_tls is defined and mongodb_ping_tls.rc == 0) else 'FAILED ✗' }} +{% endif %} + +**Listening Ports:** +``` +{{ mongodb_ports.stdout | default('Could not determine') if mongodb_ports is defined else 'Could not determine' }} +``` + +--- + +## Version Information + +``` +{{ mongodb_version.stdout | default('Version information not available') if mongodb_version is defined else 'Version information not available' }} +``` + +{% if mongodb_build_info is defined and mongodb_build_info.rc == 0 %} +**Build Details:** +{% set build = mongodb_build_info.stdout | from_json %} +- **Version:** {{ build.version | default('Unknown') }} +- **Git Version:** {{ build.gitVersion | default('Unknown') }} +- **OpenSSL Version:** {{ build.openssl.running | default('Unknown') }} +{% endif %} + +--- + +## Configuration Files + +- **Config File Exists:** {{ 'YES ✓' if (mongodb_conf_file is defined and mongodb_conf_file.stat.exists) else 'NO ✗' }} +{% if mongodb_conf_file is defined and mongodb_conf_file.stat.exists %} +- **Config File Path:** `/etc/mongod.conf` +- **Permissions:** {{ mongodb_conf_permissions.stdout | default('Unknown') if mongodb_conf_permissions is defined else 'Unknown' }} +{% endif %} + +- **Systemd Unit File:** {{ 'YES ✓' if (mongodb_systemd_file is defined and mongodb_systemd_file.stat.exists) else 'NO ✗' }} +{% if mongodb_systemd_file is defined and mongodb_systemd_file.stat.exists %} +- **Unit File Path:** `/etc/systemd/system/mongod.service` +{% endif %} + +--- + +## Data Directory + +{% if mongodb_data_dir.stdout is defined and mongodb_data_dir.stdout != "" %} +- **Data Directory:** `{{ mongodb_data_dir.stdout }}` +- **Directory Exists:** {{ 'YES ✓' if (mongodb_data_dir_stat is defined and mongodb_data_dir_stat.stat.exists) else 'NO ✗' }} +{% if mongodb_data_size is defined and mongodb_data_size.rc == 0 %} +- **Data Size:** {{ mongodb_data_size.stdout }} +{% endif %} +{% else %} +Data directory not configured or could not be determined +{% endif %} + +--- + +## Log Configuration + +{% if mongodb_log_path.stdout is defined and mongodb_log_path.stdout != "" %} +- **Log Path:** `{{ mongodb_log_path.stdout }}` +{% else %} +Log path not configured or could not be determined (likely using journald) +{% endif %} + +--- + +## Security Configuration + +{% if mongodb_auth_enabled is defined and mongodb_auth_enabled.rc == 0 %} +- **Authentication:** ENABLED ✓ +{% else %} +- **Authentication:** DISABLED ✗ +{% endif %} + +### TLS/SSL Configuration + +{% if mongodb_tls_mode is defined and mongodb_tls_mode.rc == 0 %} +- **TLS Mode:** {{ mongodb_tls_mode.stdout | default('Not configured') }} +{% if mongodb_tls_cert_path.stdout is defined and mongodb_tls_cert_path.stdout != "" %} +- **Certificate Path:** `{{ mongodb_tls_cert_path.stdout }}` +- **Certificate Exists:** {{ 'YES ✓' if (mongodb_tls_cert_file is defined and mongodb_tls_cert_file.stat.exists) else 'NO' }} +{% if mongodb_tls_cert_expiry is defined and mongodb_tls_cert_expiry.rc == 0 %} +- **Certificate Expiration:** {{ mongodb_tls_cert_expiry.stdout }} +{% endif %} +{% endif %} +{% else %} +- **TLS:** Not configured ✗ +{% endif %} + +--- + +## User Authentication Tests + +{% if mongodb_users is defined and mongodb_users | length > 0 %} +**Configured Users:** +{% for user in mongodb_users %} +### User: {{ user.user | default('Unknown') }} +- **Database:** {{ user.db | default('Unknown') }} +- **Roles:** {{ user.roles | map(attribute='role') | list | join(', ') if user.roles is defined else 'None' }} +{% endfor %} + +**Connection Test Results:** +- **admin:** {{ 'PASSED ✓' if (mongodb_ping_admin is defined and mongodb_ping_admin.rc == 0) else 'FAILED ✗' }} +- **itential:** {{ 'PASSED ✓' if (mongodb_user_itential_ping is defined and mongodb_user_itential_ping.rc == 0) else 'FAILED ✗' }} +{% else %} +No users found or unable to retrieve user list +{% endif %} + +--- + +## Replica Set Configuration + +{% if mongodb_replset_name.stdout is defined and mongodb_replset_name.stdout != "" %} +**Replica Set Name:** {{ mongodb_replset_name.stdout }} + +{% if mongodb_replset_status is defined and mongodb_replset_status.rc == 0 %} +{% set rs_status = mongodb_replset_status.stdout | from_json %} +### Replica Set Status +- **Set Name:** {{ rs_status.set | default('Unknown') }} +{% if rs_status.date is defined %} +- **Date:** {{ rs_status.date }} +{% endif %} +- **My State:** {{ rs_status.myState | default('Unknown') }} + +### Members: +{% if rs_status.members is defined %} +{% for member in rs_status.members %} +#### {{ member.name | default('Unknown') }} +- **State:** {{ member.stateStr | default('Unknown') }} +- **Health:** {{ member.health | default('Unknown') }} +- **Uptime:** {{ member.uptime | default('Unknown') }} seconds +{% if member.optime is defined and member.optime.ts is defined %} +- **Optime:** {{ member.optime.ts }} +{% endif %} +{% if member.stateStr == 'PRIMARY' %} +- **Role:** PRIMARY ⭐ +{% elif member.stateStr == 'SECONDARY' %} +- **Role:** SECONDARY +{% endif %} + +{% endfor %} +{% endif %} +{% endif %} + +{% if mongodb_replset_config is defined and mongodb_replset_config.rc == 0 %} +{% set rs_config = mongodb_replset_config.stdout | from_json %} +### Replica Set Configuration +- **Config Version:** {{ rs_config.version | default('Unknown') }} +- **Protocol Version:** {{ rs_config.protocolVersion | default('Unknown') }} + +**Settings:** +- **Heartbeat Timeout:** {{ rs_config.settings.heartbeatTimeoutSecs | default('Unknown') }} seconds +- **Election Timeout:** {{ rs_config.settings.electionTimeoutMillis | default('Unknown') }} ms +- **Catchup Timeout:** {{ rs_config.settings.catchUpTimeoutMillis | default('Unknown') }} ms +{% if rs_config.members is defined %} +{% for member in rs_config.members %} +#### {{ member.host | default('Unknown') }} +- **arbiterOnly:** {{ member.arbiterOnly | default(false) }} +- **priority:** {{ member.priority | default(0) }} +- **hidden:** {{ member.hidden | default(false) }} +- **votes:** {{ member.votes | default(1) }} +{% endfor %} +{% endif %} +{% endif %} + +{% else %} +Replica set not configured +{% endif %} + +--- + +## Database Information + +{% if mongodb_databases is defined and mongodb_databases.rc == 0 %} +{% set db_list = mongodb_databases.stdout | from_json %} +**Total Databases:** {{ db_list.databases | length if db_list.databases is defined else 0 }} +{% if db_list.totalSize is defined %} +{% if db_list.totalSize is number %} +**Total Size:** {{ (db_list.totalSize / 1024 / 1024 / 1024) | round(2) }} GB +{% else %} +**Total Size:** Unknown +{% endif %} +{% endif %} + +### Databases: +{% if db_list.databases is defined %} +{% for db in db_list.databases %} +{% if db.sizeOnDisk is defined and db.sizeOnDisk is number %} +- **{{ db.name }}:** {{ (db.sizeOnDisk / 1024 / 1024) | round(2) }} MB +{% else %} +- **{{ db.name }}:** Size unknown +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + +--- + +## Server Metrics + +{% if mongodb_server_status is defined and mongodb_server_status.rc == 0 %} +{% set server_status = mongodb_server_status.stdout | from_json %} + +### Connections +- **Current:** {{ server_status.connections.current | default('Unknown') }} +- **Available:** {{ server_status.connections.available | default('Unknown') }} +- **Total Created:** {{ server_status.connections.totalCreated | default('Unknown') }} + +### Memory +- **Resident:** {{ (server_status.mem.resident | default(0)) }} MB +- **Virtual:** {{ (server_status.mem.virtual | default(0)) }} MB + +### Network +{% if server_status.network.bytesIn is defined and server_status.network.bytesIn is number %} +- **Bytes In:** {{ (server_status.network.bytesIn / 1024 / 1024) | round(2) }} MB +{% else %} +- **Bytes In:** Unknown +{% endif %} +{% if server_status.network.bytesOut is defined and server_status.network.bytesOut is number %} +- **Bytes Out:** {{ (server_status.network.bytesOut / 1024 / 1024) | round(2) }} MB +{% else %} +- **Bytes Out:** Unknown +{% endif %} +{% endif %} + +--- + +## Recent Log Entries (Last 100 lines) + +``` +{{ mongodb_logs.stdout | default('Log entries not available') if mongodb_logs is defined else 'Log entries not available' }} +``` + +--- + +## Validation Summary + +**Overall Status:** {{ 'PASSED ✓' if (mongodb_ping_admin is defined and mongodb_ping_admin.rc == 0 and mongodb_process is defined and mongodb_process.rc == 0) else 'FAILED ✗' }} + +### Checks: + +{% if mongodb_service_status is defined and mongodb_service_status.status is defined %} +- **MongoDB Service Exists:** YES ✓ +- **MongoDB Service Active:** {{ mongodb_service_status.status.ActiveState | default('unknown') | upper }} {{ '✓' if mongodb_service_status.status.ActiveState == 'active' else '✗' }} +{% else %} +- **MongoDB Service Exists:** NO ✗ +{% endif %} +- **MongoDB Process Running:** {{ 'YES ✓' if (mongodb_process is defined and mongodb_process.rc == 0) else 'NO ✗' }} +- **MongoDB Responding:** {{ 'YES ✓' if (mongodb_ping_admin is defined and mongodb_ping_admin.rc == 0) else 'NO ✗' }} +- **Config File Present:** {{ 'YES ✓' if (mongodb_conf_file is defined and mongodb_conf_file.stat.exists) else 'NO ✗' }} +- **Authentication Enabled:** {{ 'YES ✓' if (mongodb_auth_enabled is defined and mongodb_auth_enabled.rc == 0) else 'NO ✗' }} +{% if mongodb_tls_mode is defined and mongodb_tls_mode.rc == 0 %} +- **TLS Configured:** YES ✓ +- **TLS Connection:** {{ 'SUCCESS ✓' if (mongodb_ping_tls is defined and mongodb_ping_tls.rc == 0) else 'FAILED ✗' }} +{% else %} +- **TLS Configured:** NO ✗ +{% endif %} +{% if mongodb_replset_name.stdout is defined and mongodb_replset_name.stdout != "" %} +- **Replica Set Configured:** YES ✓ +- **Replica Set Status:** {{ 'OK ✓' if (mongodb_replset_status is defined and mongodb_replset_status.rc == 0) else 'FAILED ✗' }} +{% else %} +- **Replica Set Configured:** NO ✗ +{% endif %} + +--- + +**End of Report** \ No newline at end of file diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 34b12717..1e1b70bc 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -526,8 +526,7 @@ group: "{{ redis_group }}" mode: "0665" owner: "{{ redis_owner }}" - # src: certify-report.j2 - src: certify-report-md.j2 + src: redis-validation-report.md.j2 - name: Display report summary ansible.builtin.debug: diff --git a/roles/redis/templates/certify-report-md.j2 b/roles/redis/templates/redis-validation-report.md.j2 similarity index 100% rename from roles/redis/templates/certify-report-md.j2 rename to roles/redis/templates/redis-validation-report.md.j2 From 5d953c55d482bb23ef84f89cee24952660bfe566 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 20 Jan 2026 15:16:24 -0500 Subject: [PATCH 05/11] Additional changes to verify playbook --- playbooks/verify.yml | 136 +++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 83 deletions(-) diff --git a/playbooks/verify.yml b/playbooks/verify.yml index 60856fc8..88d75166 100644 --- a/playbooks/verify.yml +++ b/playbooks/verify.yml @@ -20,32 +20,41 @@ disk_size: 100 tasks: + + - name: Gather host information + itential.deployer.gather_host_information: + register: host_info + + - name: Extract OS information + ansible.builtin.set_fact: + os: "{{ host_info.os }}" + # 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') + (os.distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Amazon' and ansible_distribution_major_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!" + fail_msg: "{{ os.distribution }} {{ os.distribution_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'] }}" + arch_valid: "{{ os.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!" + fail_msg: "{{ os.architecture }} is not a supported architecture!" success_msg: "Architecture validation passed!" quiet: true @@ -86,84 +95,45 @@ 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)) }}" + - name: Validate CPU Count + ansible.builtin.assert: + that: hardware_validation.validation.cpu_valid | bool + fail_msg: "CPU validation failed" + quiet: true + ignore_errors: true + register: cpu_validation - # 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: Validate Memory Amount + ansible.builtin.assert: + that: hardware_validation.validation.memory_valid | bool + fail_msg: "Memory validation failed" + quiet: true + ignore_errors: true + register: memory_validation - - name: Debug host info - ansible.builtin.debug: - msg: "{{ host_info }}" + - name: Validate Disk Size + ansible.builtin.assert: + that: hardware_validation.validation.disk_valid | bool + fail_msg: "Disk validation failed" + quiet: true + ignore_errors: true + register: disk_validation + + # Fail at the end if any validation failed + - name: Check if any validations failed + ansible.builtin.fail: + msg: | + Hardware validation failures detected: + {% if cpu_validation is failed %} + - CPU: {{ hardware_validation.required.cpu_count }} required, {{ hardware_validation.actual.cpu_count }} found + {% endif %} + {% if memory_validation is failed %} + - Memory: {{ hardware_validation.required.ram_size_gb }}GB required, {{ hardware_validation.actual.ram_size_gb }}GB found + {% endif %} + {% if disk_validation is failed %} + - Disk: {{ hardware_validation.required.disk_size_gb }}GB required, {{ hardware_validation.actual.disk_size_gb }}GB found + {% endif %} + when: cpu_validation is failed or memory_validation is failed or disk_validation is failed - name: Aggregate Results hosts: localhost From 28b6020acf0f4432f351038a7260beb09c89e785 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 29 Jan 2026 12:00:22 -0500 Subject: [PATCH 06/11] WIP: certify playbooks call task files from roles for certification --- playbooks/certify.yml | 69 +++ playbooks/preflight.yml | 25 - playbooks/verify.yml | 60 +- roles/mongodb/defaults/main/install.yml | 4 - roles/mongodb/tasks/certify-mongodb.yml | 35 +- .../templates/mongodb-validation-report.md.j2 | 2 +- roles/mongodb/vars/platform-release-6.yml | 14 + roles/platform/tasks/certify-platform.yml | 565 ++++++++++++++++++ .../platform-validation-report.md.j2 | 187 ++++++ roles/platform/vars/platform-release-6.yml | 14 + roles/preflight/README.md | 0 roles/preflight/tasks/main.yml | 114 ---- roles/preflight/vars/dev.specs.yml | 26 - roles/preflight/vars/prod.specs.yml | 26 - roles/preflight/vars/staging.specs.yml | 26 - roles/preflight/vars/undefined.specs.yml | 4 - roles/redis/defaults/main/install.yml | 4 - roles/redis/tasks/certify-redis.yml | 25 +- roles/redis/vars/platform-release-6.yml | 14 + 19 files changed, 934 insertions(+), 280 deletions(-) create mode 100644 playbooks/certify.yml delete mode 100644 playbooks/preflight.yml create mode 100644 roles/platform/tasks/certify-platform.yml create mode 100644 roles/platform/templates/platform-validation-report.md.j2 delete mode 100644 roles/preflight/README.md delete mode 100644 roles/preflight/tasks/main.yml delete mode 100644 roles/preflight/vars/dev.specs.yml delete mode 100644 roles/preflight/vars/prod.specs.yml delete mode 100644 roles/preflight/vars/staging.specs.yml delete mode 100644 roles/preflight/vars/undefined.specs.yml diff --git a/playbooks/certify.yml b/playbooks/certify.yml new file mode 100644 index 00000000..7688d469 --- /dev/null +++ b/playbooks/certify.yml @@ -0,0 +1,69 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Run Environment Certification Tasks + hosts: all + gather_facts: true + become: true + vars: + certify_report_dir: "/tmp/itential-reports" + # certify_report_file: "{{ certify_report_dir }}/{{ group_names[0] }}_report_{{ inventory_hostname }}" + roles: + - role: itential.deployer.common_vars + tags: always + tasks: + - name: Certify Redis Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.redis + tasks_from: certify-redis + when: inventory_hostname in groups['redis'] + tags: certify-redis + + - name: Certify MongoDB Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.mongodb + tasks_from: certify-mongodb + when: inventory_hostname in groups['mongodb'] + tags: certify-mongodb + + - name: Certify Itential Platform Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.platform + tasks_from: certify-platform + when: inventory_hostname in groups['platform'] + tags: certify-platform + + - name: Remove old reports on the control node + ansible.builtin.file: + path: "{{ certify_report_dir }}" + state: absent + delegate_to: localhost + become: false + run_once: true + tags: always + + - name: Ensure the report directory exists on the control node + ansible.builtin.file: + path: "{{ certify_report_dir }}" + state: directory + mode: '0755' + delegate_to: localhost + become: false + run_once: true + tags: always + + - name: Find report files on remote host + ansible.builtin.find: + paths: "{{ certify_report_dir }}" + patterns: "*.md" + register: report_files_found + tags: always + + - name: Fetch all the report files to the control node + ansible.builtin.fetch: + fail_on_missing: false + src: "{{ item.path }}" + dest: "{{ certify_report_dir }}/{{ item.path | basename }}" + flat: true + loop: "{{ report_files_found.files }}" + tags: always diff --git a/playbooks/preflight.yml b/playbooks/preflight.yml deleted file mode 100644 index 554dbf12..00000000 --- a/playbooks/preflight.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Run Redis Preflight checks - hosts: redis, redis_secondary, mongodb, mongodb_secondary, gateway, platform, platform_secondary - become: true - roles: - - role: itential.deployer.common_vars - tags: always - -- name: Include Preflight Redis - import_playbook: itential.deployer.preflight_redis - tags: preflight_redis - -- name: Include Preflight MongoDB - import_playbook: itential.deployer.preflight_mongodb - tags: preflight_mongodb - -- name: Include Preflight Platform - import_playbook: itential.deployer.preflight_platform - tags: preflight_platform - -- name: Include Preflight Gateway - import_playbook: itential.deployer.preflight_gateway - tags: preflight_gateway diff --git a/playbooks/verify.yml b/playbooks/verify.yml index 88d75166..fea9aa44 100644 --- a/playbooks/verify.yml +++ b/playbooks/verify.yml @@ -1,26 +1,31 @@ +# Copyright (c) 2024, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Gather System Facts +- name: Run Environment Verification Tasks 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 + become: true + roles: + - role: itential.deployer.common_vars + tags: always tasks: + - name: Import role vars + ansible.builtin.include_vars: + file: "{{ item }}" + loop: + - "../roles/redis/vars/platform-release-{{ platform_release }}.yml" + - "../roles/mongodb/vars/platform-release-{{ platform_release }}.yml" + - "../roles/platform/vars/platform-release-{{ platform_release }}.yml" + + - name: Gather hardware specs + ansible.builtin.set_fact: + hardware_specs: + "mongodb": "{{ mongodb_hw_specs[env] }}" + "platform": "{{ platform_hw_specs[env] }}" + "redis": "{{ redis_hw_specs[env] }}" + - name: Gather host information itential.deployer.gather_host_information: register: host_info @@ -59,7 +64,7 @@ quiet: true # Hardware spec validation - - name: Determine which hardware spec applies to this host + - name: Determine the applicable specs based on host group ansible.builtin.set_fact: applicable_spec: >- {%- if 'mongodb' in group_names -%} @@ -80,7 +85,6 @@ - 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' }}" @@ -119,6 +123,10 @@ ignore_errors: true register: disk_validation + - name: Print host information + ansible.builtin.debug: + msg: "{{ host_info }}" + # Fail at the end if any validation failed - name: Check if any validations failed ansible.builtin.fail: @@ -134,17 +142,3 @@ - Disk: {{ hardware_validation.required.disk_size_gb }}GB required, {{ hardware_validation.actual.disk_size_gb }}GB found {% endif %} when: cpu_validation is failed or memory_validation is failed or disk_validation is failed - -- 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/roles/mongodb/defaults/main/install.yml b/roles/mongodb/defaults/main/install.yml index 6444e3d0..d8b46c46 100644 --- a/roles/mongodb/defaults/main/install.yml +++ b/roles/mongodb/defaults/main/install.yml @@ -24,7 +24,3 @@ mongodb_mongod_service_delay: 10 # MongoDB status settings mongodb_status_poll: 3 mongodb_status_interval: 10 - -# The name and location of the certification report -mongodb_report_dir: "/tmp/itential-reports" -mongodb_report_file: "{{ mongodb_report_dir }}/mongodb_report_{{ inventory_hostname }}.md" diff --git a/roles/mongodb/tasks/certify-mongodb.yml b/roles/mongodb/tasks/certify-mongodb.yml index 30a72f6c..70e0f5be 100644 --- a/roles/mongodb/tasks/certify-mongodb.yml +++ b/roles/mongodb/tasks/certify-mongodb.yml @@ -1,6 +1,25 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -# MongoDB Validation Tasks -# This playbook gathers information to validate MongoDB installation + +- name: Load variables from the MongoDB role + ansible.builtin.include_vars: + dir: "{{ item }}" + loop: + - "{{ role_path }}/../mongodb/defaults" + - "{{ role_path }}/../mongodb/vars" + +- name: Ensure report directory exists + ansible.builtin.file: + path: "{{ certify_report_dir }}" + state: directory + owner: "{{ mongodb_owner }}" + group: "{{ mongodb_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + certify_report_file: "{{ certify_report_dir }}/mongodb-report-{{ inventory_hostname }}.md" - name: Check if MongoDB service exists ansible.builtin.systemd: @@ -361,22 +380,14 @@ # ============================================================================ # GENERATE REPORT # ============================================================================ -- name: Ensure report directory exists - ansible.builtin.file: - path: "/tmp/itential-reports" - state: directory - owner: "{{ mongodb_owner }}" - group: "{{ mongodb_group }}" - mode: "0755" - - name: Generate MongoDB validation report ansible.builtin.template: src: mongodb-validation-report.md.j2 - dest: "{{ mongodb_report_file }}" + dest: "{{ certify_report_file }}" mode: '0644' owner: "{{ mongodb_owner }}" group: "{{ mongodb_group }}" - name: Display report location ansible.builtin.debug: - msg: "MongoDB validation report generated at: {{ mongodb_report_file }}" + msg: "MongoDB validation report generated at: {{ certify_report_file }}" diff --git a/roles/mongodb/templates/mongodb-validation-report.md.j2 b/roles/mongodb/templates/mongodb-validation-report.md.j2 index bfddc215..e117fa2b 100644 --- a/roles/mongodb/templates/mongodb-validation-report.md.j2 +++ b/roles/mongodb/templates/mongodb-validation-report.md.j2 @@ -250,7 +250,7 @@ No users found or unable to retrieve user list {% endif %} {% else %} -Replica set not configured +Replica set not configured ✗ {% endif %} --- diff --git a/roles/mongodb/vars/platform-release-6.yml b/roles/mongodb/vars/platform-release-6.yml index 861664c9..9efd3287 100644 --- a/roles/mongodb/vars/platform-release-6.yml +++ b/roles/mongodb/vars/platform-release-6.yml @@ -50,3 +50,17 @@ mongodb_gpgkey_url_default: - https://www.mongodb.org/static/pgp/server-{{ mongodb_version }}.asc "2023": - https://pgp.mongodb.com/server-{{ mongodb_version }}.asc + +mongodb_hw_specs: + "dev": + "cpu_count": 8 + "ram_size": 64 + "disk_size": 1000 + "test": + "cpu_count": 16 + "ram_size": 128 + "disk_size": 1000 + "production": + "cpu_count": 16 + "ram_size": 128 + "disk_size": 1000 diff --git a/roles/platform/tasks/certify-platform.yml b/roles/platform/tasks/certify-platform.yml new file mode 100644 index 00000000..e9ebe43e --- /dev/null +++ b/roles/platform/tasks/certify-platform.yml @@ -0,0 +1,565 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Load variables from the Platform role + ansible.builtin.include_vars: + dir: "{{ item }}" + loop: + - "{{ role_path }}/../platform/defaults" + - "{{ role_path }}/../platform/vars" + +- name: Ensure report directory exists + ansible.builtin.file: + path: "{{ certify_report_dir }}" + state: directory + owner: "{{ platform_user }}" + group: "{{ platform_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + certify_report_file: "{{ certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" + +- name: Check if Itential service exists + ansible.builtin.systemd: + name: itential-platform + register: iap_service_status + ignore_errors: true + +- name: Check if Itential process is running + ansible.builtin.shell: ps aux | grep -v grep | grep -i pronghorn + register: iap_process + ignore_errors: true + changed_when: false + +- name: Check Itential listening ports + ansible.builtin.shell: ss -tulpn | grep itential-platform + register: iap_ports + ignore_errors: true + changed_when: false + +- name: Check Itential systemd unit file + ansible.builtin.stat: + path: /usr/lib/systemd/system/itential-platform.service + register: iap_systemd_file + +- name: Check Itential config file exists + ansible.builtin.stat: + path: /etc/itential/platform.properties + register: iap_conf_file + +- name: Get Itential config file permissions + ansible.builtin.command: ls -la /etc/itential/platform.properties + register: iap_conf_permissions + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +# ============================================================================ +# CONFIRM CONFIGURATION SETTINGS +# ============================================================================ + +- name: Parse Itential config for server_id + ansible.builtin.shell: + cmd: > + grep '^server_id' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_server_id + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for mongo_auth_enabled + ansible.builtin.shell: + cmd: > + grep '^mongo_auth_enabled' /etc/itential/platform.properties + | awk -F'=' '{print $2}' | + xargs + register: iap_mongo_auth_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for mongo_user + ansible.builtin.shell: + cmd: > + grep '^mongo_user' /etc/itential/platform.properties + | awk -F'=' '{print $2}' | + xargs + register: iap_mongo_user + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for mongo_auth_db + ansible.builtin.shell: + cmd: > + grep '^mongo_auth_db' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_mongo_auth_db + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for mongo_db_name + ansible.builtin.shell: + cmd: > + grep '^mongo_db_name' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_mongo_db_name + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for mongo_url + ansible.builtin.shell: + cmd: > + grep '^mongo_url' /etc/itential/platform.properties | + sed 's/^mongo_url[[:space:]]*=[[:space:]]*//' | + xargs + register: iap_mongo_url + ignore_errors: true + changed_when: false + no_log: true + when: iap_conf_file.stat.exists + +- name: Mask password in MongoDB URL + ansible.builtin.set_fact: + mongo_url_masked: "{{ iap_mongo_url.stdout | regex_replace('(mongodb[^:]*://[^:]+:)([^@]+)(@.*)', '\\1****\\3') }}" + no_log: true + when: iap_mongo_url.stdout is defined and iap_mongo_url.stdout != "" + +- name: Parse Itential config for mongo_tls_enabled + ansible.builtin.shell: + cmd: > + grep '^mongo_tls_enabled' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_mongo_tls_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_username + ansible.builtin.shell: + cmd: > + grep '^redis_username' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_redis_username + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_host + ansible.builtin.shell: + cmd: > + grep '^redis_host' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_redis_host + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_sentinel_username + ansible.builtin.shell: + cmd: > + grep '^redis_sentinel_username' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_redis_sentinel_username + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_sentinels + ansible.builtin.shell: + cmd: > + grep '^redis_sentinels' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_redis_sentinels + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_name + ansible.builtin.shell: + cmd: > + grep '^redis_name' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_redis_name + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for redis_tls + ansible.builtin.shell: + cmd: > + grep '^redis_tls' /etc/itential/platform.properties + | awk -F'=' '{print $2}' | + xargs + register: iap_redis_tls + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for task_worker_enabled + ansible.builtin.shell: + cmd: > + grep '^task_worker_enabled' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_task_worker_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for job_worker_enabled + ansible.builtin.shell: + cmd: > + grep '^job_worker_enabled' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_job_worker_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for default_user_username + ansible.builtin.shell: + cmd: > + grep '^default_user_username' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_default_user_username + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_http_enabled + ansible.builtin.shell: + cmd: > + grep '^webserver_http_enabled' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_http_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_https_enabled + ansible.builtin.shell: + cmd: > + grep '^webserver_https_enabled' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_https_enabled + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_http_port + ansible.builtin.shell: + cmd: > + grep '^webserver_http_port' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_http_port + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_https_port + ansible.builtin.shell: + cmd: > + grep '^webserver_https_port' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_https_port + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_https_key + ansible.builtin.shell: + cmd: > + grep '^webserver_https_key' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_https_key + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_https_cert + ansible.builtin.shell: + cmd: > + grep '^webserver_https_cert' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_https_cert + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for log_directory + ansible.builtin.shell: + cmd: > + grep '^log_directory' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_log_directory + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for log_file + ansible.builtin.shell: + cmd: > + grep '^log_file' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_log_file + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for log_level + ansible.builtin.shell: + cmd: > + grep '^log_level' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_log_level + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_log_directory + ansible.builtin.shell: + cmd: > + grep '^webserver_log_directory' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_log_directory + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for webserver_log_filename + ansible.builtin.shell: + cmd: > + grep '^webserver_log_filename' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_webserver_log_filename + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: Parse Itential config for services_directory + ansible.builtin.shell: + cmd: > + grep '^service_directory' /etc/itential/platform.properties | + awk -F'=' '{print $2}' | + xargs + register: iap_service_directory + ignore_errors: true + changed_when: false + when: iap_conf_file.stat.exists + +- name: List all installed custom services + ansible.builtin.command: "ls -la {{ iap_service_directory.stdout }}" + register: iap_service_directory_contents + ignore_errors: true + changed_when: false + +# ============================================================================ +# CONFIRM TLS FILES +# ============================================================================ + +- name: Check TLS certificate file exists + ansible.builtin.stat: + path: "{{ iap_webserver_https_cert.stdout }}" + register: iap_webserver_https_cert_stat + ignore_errors: true + changed_when: false + when: iap_webserver_https_cert.stdout + +- name: Check TLS key file exists + ansible.builtin.stat: + path: "{{ iap_webserver_https_key.stdout }}" + register: iap_webserver_https_key_stat + ignore_errors: true + changed_when: false + when: iap_webserver_https_key.stdout + +# ============================================================================ +# CONFIRM NODEJS +# ============================================================================ + +- name: Get Node.js version + ansible.builtin.command: node --version + register: nodejs_version + ignore_errors: true + changed_when: false + failed_when: false + +- name: Get Node.js executable path + ansible.builtin.command: which node + register: nodejs_path + ignore_errors: true + changed_when: false + failed_when: false + +# ============================================================================ +# CONFIRM PYTHON +# ============================================================================ + +- name: Get Python version + ansible.builtin.command: python3 --version + register: python_version + ignore_errors: true + changed_when: false + failed_when: false + become: true + become_user: "itential" + become_flags: '-i' + # become_method: ansible.builtin.su + +- name: Get Python executable path + ansible.builtin.command: which python3 + register: python_path + ignore_errors: true + changed_when: false + failed_when: false + become: true + become_user: "itential" + become_flags: '-i' + +- name: Get list of installed Python modules + ansible.builtin.command: python3 -m pip list + register: python_modules + ignore_errors: true + changed_when: false + failed_when: false + become: true + become_user: "itential" + become_flags: '-i' + +- name: Get pip version + ansible.builtin.command: python3 -m pip --version + register: pip_version + ignore_errors: true + changed_when: false + failed_when: false + become: true + become_user: "itential" + become_flags: '-i' + +# ============================================================================ +# CONFIRM CONNECTIVITY +# ============================================================================ + +- name: Initialize MongoDB and Redis connectivity flags + ansible.builtin.set_fact: + mongodb_connection: false + redis_connection: false + +- name: Check health status endpoint using HTTP + ansible.builtin.uri: + url: "http://{{ inventory_hostname }}:{{ iap_webserver_http_port.stdout }}/health/status" + method: GET + return_content: true + status_code: 200 + validate_certs: false # Set to 'no' for self-signed certs + force: true + register: http_health_check + ignore_errors: true + changed_when: false + failed_when: false + when: iap_webserver_http_enabled.stdout | bool + +- name: Check MongoDB connection (HTTP) + ansible.builtin.set_fact: + mongodb_connection: "{{ (http_health_check.json.services | selectattr('service', 'equalto', 'mongo') | selectattr('status', 'equalto', 'running') | list | length) > 0 }}" + when: iap_webserver_http_enabled.stdout | bool + +- name: Check Redis connection (HTTP) + ansible.builtin.set_fact: + redis_connection: "{{ (http_health_check.json.services | selectattr('service', 'equalto', 'redis') | selectattr('status', 'equalto', 'running') | list | length) > 0 }}" + when: iap_webserver_http_enabled.stdout | bool + +- name: Check health status endpoint using HTTPS + ansible.builtin.uri: + url: "http://{{ inventory_hostname }}:{{ iap_webserver_https_port.stdout }}/health/status" + method: GET + return_content: true + status_code: 200 + validate_certs: false # Set to 'no' for self-signed certs + force: true + register: https_health_check + ignore_errors: true + changed_when: false + failed_when: false + when: iap_webserver_https_enabled.stdout | bool + +- name: Check MongoDB connection (HTTPS) + ansible.builtin.set_fact: + mongodb_connection: "{{ (https_health_check.json | selectattr('service', 'equalto', 'mongo') | selectattr('status', 'equalto', 'running') | list | length) > 0 }}" + when: iap_webserver_https_enabled.stdout | bool + +- name: Check Redis connection (HTTPS) + ansible.builtin.set_fact: + redis_connection: "{{ (https_health_check.json | selectattr('service', 'equalto', 'redis') | selectattr('status', 'equalto', 'running') | list | length) > 0 }}" + when: iap_webserver_https_enabled.stdout | bool + +# ============================================================================ +# CONFIRM LOG FILES +# ============================================================================ + +- name: Check Itential log file exists + ansible.builtin.stat: + path: "{{ iap_log_directory.stdout }}/{{ iap_log_file.stdout }}" + register: iap_log_file_stat + ignore_errors: true + changed_when: false + when: iap_log_file.stdout + +- name: Check Itential web server log file exists + ansible.builtin.stat: + path: "{{ iap_webserver_log_directory.stdout }}/{{ iap_webserver_log_filename.stdout }}" + register: iap_web_log_file_stat + ignore_errors: true + changed_when: false + when: iap_webserver_log_filename.stdout + +# ============================================================================ +# GATHER HOST INFORMATION +# ============================================================================ + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_details + +# ============================================================================ +# GENERATE REPORT +# ============================================================================ +- name: Generate Itential platform validation report + ansible.builtin.template: + src: platform-validation-report.md.j2 + dest: "{{ certify_report_file }}" + mode: '0644' + owner: "{{ platform_user }}" + group: "{{ platform_group }}" + +- name: Display report location + ansible.builtin.debug: + msg: "Itential platform validation report generated at: {{ certify_report_file }}" diff --git a/roles/platform/templates/platform-validation-report.md.j2 b/roles/platform/templates/platform-validation-report.md.j2 new file mode 100644 index 00000000..371d0bfd --- /dev/null +++ b/roles/platform/templates/platform-validation-report.md.j2 @@ -0,0 +1,187 @@ +# Itential Platform 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('') }} + +--- + +## 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 iap_service_status is defined and iap_service_status.status is defined %} +- **Service Name:** {{ iap_service_status.name | default('Unknown') }} +- **Service State:** {{ iap_service_status.status.ActiveState | default('Unknown') }} +- **Service SubState:** {{ iap_service_status.status.SubState | default('Unknown') }} +- **Service Enabled:** {{ iap_service_status.status.UnitFileState | default('Unknown') }} +{% else %} +- **Service Status:** Could not determine (service may not exist) +{% endif %} + +**Process Running:** {{ 'YES ✓' if (iap_process is defined and iap_process.rc == 0) else 'NO ✗' }} + +{% if iap_process is defined and iap_process.rc == 0 %} +**Process Details:** +``` +{{ iap_process.stdout | default('N/A') }} +``` +{% endif %} + +--- + +## Configuration Files +- **Config File Exists:** {{ 'YES ✓' if (iap_conf_file is defined and iap_conf_file.stat.exists) else 'NO ✗' }} +{% if iap_conf_file is defined and iap_conf_file.stat.exists %} +- **Config File Path:** `/etc/itential/platform.properties` +- **Permissions:** {{ iap_conf_permissions.stdout | default('Unknown') if iap_conf_permissions is defined else 'Unknown' }} +{% endif %} + +- **Systemd Unit File:** {{ 'YES ✓' if (iap_systemd_file is defined and iap_systemd_file.stat.exists) else 'NO ✗' }} +{% if iap_systemd_file is defined and iap_systemd_file.stat.exists %} +- **Unit File Path:** `/etc/systemd/system/itential-platform.service` +{% endif %} + +--- + +## Critical Configuration Properties + +- **server_id:** {{ iap_server_id.stdout | default('Unknown') }} +- **mongo_auth_enabled:** {{ iap_mongo_auth_enabled.stdout | default('Unknown') }} +- **mongo_user:** {{ (iap_mongo_user.stdout | default('')) or 'Default value' }} +- **mongo_auth_db:** {{ (iap_mongo_auth_db.stdout | default('')) or 'Default value' }} +- **mongo_db_name:** {{ iap_mongo_db_name.stdout | default('Unknown') }} +- **mongo_url:** {{ mongo_url_masked | default('Unknown') }} +- **mongo_tls_enabled:** {{ iap_mongo_tls_enabled.stdout | default('Unknown') }} +- **redis_db:** {{ (iap_mongo_auth_db.stdout | default('')) or 'Default value' }} +- **redis_user:** {{ (iap_mongo_auth_db.stdout | default('')) or 'Default value' }} +- **redis_host:** {{ (iap_redis_host.stdout | default('')) or 'Unknown' }} +- **redis_sentinel_username:** {{ iap_redis_sentinel_username.stdout | default('Unknown') }} +- **redis_sentinels:** {{ iap_redis_sentinels.stdout | default('Unknown') }} +- **redis_name:** {{ iap_redis_name.stdout | default('Unknown') }} +- **redis_tls:** {{ (iap_redis_tls.stdout | default('')) or 'Unknown' }} +- **task_worker_enabled:** {{ iap_task_worker_enabled.stdout | default('Unknown') }} +- **job_worker_enabled:** {{ iap_job_worker_enabled.stdout | default('Unknown') }} +- **default_user_username:** {{ iap_default_user_username.stdout | default('Unknown') }} +- **webserver_http_enabled:** {{ iap_webserver_http_enabled.stdout | default('Unknown') }} +- **webserver_https_enabled:** {{ iap_webserver_https_enabled.stdout | default('Unknown') }} +- **webserver_http_port:** {{ iap_webserver_http_port.stdout | default('Unknown') }} +- **webserver_https_port:** {{ iap_webserver_https_port.stdout | default('Unknown') }} +- **webserver_https_key:** {{ iap_webserver_https_key.stdout | default('Unknown') }} +- **webserver_https_cert:** {{ iap_webserver_https_cert.stdout | default('Unknown') }} +- **log_file:** {{ iap_log_directory.stdout + "/" + iap_log_file.stdout | default('Unknown') }} +- **log_level:** {{ iap_log_level.stdout | default('Unknown') }} +- **webserver_log_file:** {{ iap_webserver_log_directory.stdout + "/" + iap_webserver_log_filename.stdout | default('Unknown') }} + +--- + +## Custom Services + +- **Custom Service Directory:** {{ service_directory | default('Unknown') }} + +## TLS Files + +{% if iap_webserver_https_cert is defined and iap_webserver_https_cert_stat.stat.exists %} +- **TLS certificate files exist:** YES ✓ +- **TLS certificate files permissions:** {{ iap_webserver_https_cert_stat.stat.mode }} +{% else %} +- **TLS certificate files exist:** NO ✗ +{% endif %} +{% if iap_webserver_https_key is defined and iap_webserver_https_key_stat.stat.exists %} +- **TLS key files exist:** YES ✓ +- **TLS key files permissions:** {{ iap_webserver_https_key_stat.stat.mode }} +{% else %} +- **TLS key files exist:** NO ✗ +{% endif %} + +## Log Files + +{% if iap_log_file is defined and iap_log_file_stat.stat.exists %} +- **Log files exist:** YES ✓ +- **Log files permissions:** {{ iap_log_file_stat.stat.mode }} +{% else %} +- **Log files exist:** NO ✗ +{% endif %} +{% if iap_web_log_file is defined and iap_web_log_file_stat.stat.exists %} +- **Web log files exist:** YES ✓ +- **Web log files permissions:** {{ iap_web_log_file_stat.stat.mode }} +{% else %} +- **Web log files exist:** NO ✗ +{% endif %} + +--- + +## NodeJs +- **Node Version:** {{ nodejs_version.stdout | default('Unknown') }} +- **Node Executable:** {{ nodejs_path.stdout | default('Unknown') }} + +--- + +## Python +- **Python Version:** {{ python_version.stdout | default('Unknown') }} +- **Python Executable:** {{ python_path.stdout | default('Unknown') }} +- **Pip Version:** {{ pip_version.stdout | default('Unknown') }} +- **Python Modules:** +``` +{{ python_modules.stdout | default('Unknown') }} +``` + +--- + +## Connectivity + +- **HTTP health check:** {{ 'YES ✓' if (http_health_check.status | default(0) == 200) else 'NO ✗' }} +- **HTTPS health check:** {{ 'YES ✓' if (https_health_check.status | default(0) == 200) else 'NO ✗' }} +- **MongoDB connectivity:** {{ 'YES ✓' if mongodb_connection else 'NO ✗' }} +- **Redis connectivity:** {{ 'YES ✓' if redis_connection else 'NO ✗' }} + + diff --git a/roles/platform/vars/platform-release-6.yml b/roles/platform/vars/platform-release-6.yml index 36a9c8d4..b3aa8b1a 100644 --- a/roles/platform/vars/platform-release-6.yml +++ b/roles/platform/vars/platform-release-6.yml @@ -35,3 +35,17 @@ platform_python_app_dependencies_default: - jinja2==3.1.2 - markupsafe==2.1.4 - textfsm==1.1.3 + +platform_hw_specs: + "dev": + "cpu_count": 8 + "ram_size": 32 + "disk_size": 250 + "test": + "cpu_count": 16 + "ram_size": 64 + "disk_size": 250 + "production": + "cpu_count": 16 + "ram_size": 64 + "disk_size": 250 diff --git a/roles/preflight/README.md b/roles/preflight/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/preflight/tasks/main.yml b/roles/preflight/tasks/main.yml deleted file mode 100644 index d06379bf..00000000 --- a/roles/preflight/tasks/main.yml +++ /dev/null @@ -1,114 +0,0 @@ -# # Copyright (c) 2024, Itential, Inc -# # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Include environment specs - ansible.builtin.include_vars: - file: "{{ item }}" - with_first_found: - - "{{ preflight_env }}.specs.yml" - - "undefined.specs.yml" - -- name: Check for valid environment - ansible.builtin.fail: - msg: "Do defined environment. Please add env: dev, staging, or prod to your host file." - when: invalid_env is defined - -- name: Initialize Results - ansible.builtin.set_fact: - results: '{}' - -- name: Set Results var to JSON - ansible.builtin.set_fact: - results: "{{ results | from_json }}" - -- name: Set Inventory Name - ansible.builtin.set_fact: - results: '{{ results | combine({"name": inventory_hostname}) }}' - -- name: Initialize pass to false - ansible.builtin.set_fact: - results: '{{ results | combine({"pass": false}) }}' - -- name: Set OS - ansible.builtin.set_fact: - results: '{{ results | combine({"os": ansible_facts.os_family}) }}' - -- name: Set OS Version - ansible.builtin.set_fact: - results: '{{ results | combine({"osVersion": ansible_facts.distribution_version}) }}' - -- name: Set Mount - ansible.builtin.set_fact: - results: '{{ results | combine({"mount": preflight_mounts}) }}' - -- name: Set SELinux Variable - ansible.builtin.set_fact: - selinux: "{{ 'enabled' if (ansible_facts.selinux.config_mode == 'enforcing') else 'disabled' }}" - -- name: Set SELinux Variable in results - ansible.builtin.set_fact: - results: '{{ results | combine({"SELinux": selinux}) }}' - -- name: Set IPv6 Variable - ansible.builtin.set_fact: - results: '{{ results | combine({"ipv6": ansible_facts.all_ipv6_addresses | length > 0}) }}' - -- name: Set CPU cores - ansible.builtin.set_fact: - results: '{{ results | combine({"cpuCores": ansible_processor_cores}) }}' - -- name: Set RAM - ansible.builtin.set_fact: - results: '{{ results | combine({"memory": (ansible_memory_mb["real"]["total"] / 1000)}) }}' - -- name: Get mount info - ansible.builtin.set_fact: - mount_info: "{{ ansible_facts.mounts | - selectattr('mount', 'defined') | - selectattr('size_available', 'defined') | - selectattr('mount', '==', preflight_mounts) | list | unique }}" - -- name: Get size_available from mount - ansible.builtin.set_fact: - size_available: "{{ mount_info[0]['size_available'] / 1024 / 1024 / 1024 }}" - -- name: Set Size Available - ansible.builtin.set_fact: - results: '{{ results | combine({preflight_mounts + "_sizeAvailable": size_available | int}) }}' - mountvarname: "{{ preflight_mounts }}_sizeAvailable" - -- name: Set http_proxy - ansible.builtin.set_fact: - results: '{{ results | combine({"http_proxy": ansible_env.http_proxy | default(false)}) }}' - -- name: Set https_proxy - ansible.builtin.set_fact: - results: '{{ results | combine({"https_proxy": ansible_env.http_proxy | default(false)}) }}' - -- name: Initialize url status variable - ansible.builtin.set_fact: - url_status: {} - -- name: Check if urls are available - when: - - preflight_url_checks is defined - - preflight_url_checks is iterable - - preflight_url_checks | length > 0 - block: - - name: Test url availability - ansible.builtin.uri: - url: "{{ item }}" - timeout: 3 - return_content: false - register: url_result - ignore_errors: true - with_items: "{{ preflight_url_checks }}" - - - name: Update URL status - ansible.builtin.set_fact: - url_status: "{{ url_status | combine({item.item: item.status}) }}" - with_items: "{{ url_result.results }}" - -- name: Set url_status - ansible.builtin.set_fact: - results: '{{ results | combine({"url_status": url_status}) }}' diff --git a/roles/preflight/vars/dev.specs.yml b/roles/preflight/vars/dev.specs.yml deleted file mode 100644 index 764e9d19..00000000 --- a/roles/preflight/vars/dev.specs.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -# Platform - Dev -platform_cpu_cores: 8 -platform_os: [8, 9] -platform_ram: 32 -platform_free_disk_space: 250 - -# MongoDB - Dev -mongodb_cpu_cores: 8 -mongodb_os: [8, 9] -mongodb_ram: 64 -mongodb_free_disk_space: 1000 - -# Redis - Dev -redis_cpu_cores: 4 -redis_os: [8, 9] -redis_ram: 16 -redis_free_disk_space: 100 - -# Gateway - Dev -gateway_cpu_cores: 4 -gateway_os: [8, 9] -gateway_ram: 16 -gateway_free_disk_space: 10 diff --git a/roles/preflight/vars/prod.specs.yml b/roles/preflight/vars/prod.specs.yml deleted file mode 100644 index bf994c7e..00000000 --- a/roles/preflight/vars/prod.specs.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -# Platform - Prod -platform_cpu_cores: 16 -platform_os: [8, 9] -platform_ram: 64 -platform_free_disk_space: 250 - -# MongoDB - Prod -mongodb_cpu_cores: 16 -mongodb_os: [8, 9] -mongodb_ram: 128 -mongodb_free_disk_space: 1000 - -# Redis - Prod -redis_cpu_cores: 8 -redis_os: [8, 9] -redis_ram: 32 -redis_free_disk_space: 100 - -# Gateway - Prod -gateway_cpu_cores: 16 -gateway_os: [8, 9] -gateway_ram: 32 -gateway_free_disk_space: 50 diff --git a/roles/preflight/vars/staging.specs.yml b/roles/preflight/vars/staging.specs.yml deleted file mode 100644 index d400325a..00000000 --- a/roles/preflight/vars/staging.specs.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -# Platform - Staging -platform_cpu_cores: 16 -platform_os: [8, 9] -platform_ram: 64 -platform_free_disk_space: 250 - -# MongoDB - Staging -mongodb_cpu_cores: 16 -mongodb_os: [8, 9] -mongodb_ram: 128 -mongodb_free_disk_space: 1000 - -# Redis - Staging -redis_cpu_cores: 8 -redis_os: [8, 9] -redis_ram: 32 -redis_free_disk_space: 100 - -# Gateway - Staging -gateway_cpu_cores: 4 -gateway_os: [8, 9] -gateway_ram: 16 -gateway_free_disk_space: 10 diff --git a/roles/preflight/vars/undefined.specs.yml b/roles/preflight/vars/undefined.specs.yml deleted file mode 100644 index 2a04726c..00000000 --- a/roles/preflight/vars/undefined.specs.yml +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -invalid_env: true diff --git a/roles/redis/defaults/main/install.yml b/roles/redis/defaults/main/install.yml index 894a6afd..ac62dd8b 100644 --- a/roles/redis/defaults/main/install.yml +++ b/roles/redis/defaults/main/install.yml @@ -27,7 +27,3 @@ 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" diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 1e1b70bc..cb3e90b4 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -2,14 +2,25 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- +- name: Load variables from the Redis role + ansible.builtin.include_vars: + dir: "{{ item }}" + loop: + - "{{ role_path }}/../redis/defaults" + - "{{ role_path }}/../redis/vars" + - name: Ensure report directory exists ansible.builtin.file: - path: "{{ redis_report_dir }}" + path: "{{ certify_report_dir }}" state: directory owner: "{{ redis_owner }}" group: "{{ redis_group }}" mode: "0755" +- name: Set report filename + ansible.builtin.set_fact: + certify_report_file: "{{ certify_report_dir }}/redis-report-{{ inventory_hostname }}.md" + - name: Gather host information itential.deployer.gather_host_information: register: host_details @@ -299,6 +310,7 @@ - name: Capture itentialmaster ansible.builtin.set_fact: itential_master: "{{ (sentinel_masters.stdout | from_json)[0] }}" + when: sentinel_is_running | bool - name: Get details for each monitored master ansible.builtin.shell: | @@ -312,13 +324,13 @@ --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) }}" + when: sentinel_is_running | bool - name: Get known sentinels for each master ansible.builtin.shell: | @@ -339,6 +351,7 @@ - name: Capture known sentinel details ansible.builtin.set_fact: known_sentinel_details: "{{ (known_sentinels.stdout | from_json) }}" + when: sentinel_is_running | bool - name: Get known replicas for each master ansible.builtin.shell: | @@ -359,6 +372,7 @@ - name: Capture known replica details ansible.builtin.set_fact: known_replica_details: "{{ (known_replicas.stdout | from_json) }}" + when: sentinel_is_running | bool - name: Check master status ansible.builtin.shell: | @@ -379,6 +393,7 @@ - name: Capture known replica details ansible.builtin.set_fact: quorum_check_details: "{{ (quorum_check.stdout | from_json) }}" + when: sentinel_is_running | bool - name: Get Sentinel configuration ansible.builtin.shell: | @@ -522,7 +537,7 @@ - name: Generate validation report ansible.builtin.template: backup: true - dest: "{{ redis_report_file }}" + dest: "{{ certify_report_file }}" group: "{{ redis_group }}" mode: "0665" owner: "{{ redis_owner }}" @@ -533,9 +548,9 @@ 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 }}" + - "Report saved to: {{ certify_report_file }}" - name: Display report location ansible.builtin.debug: - msg: "Full report available at: {{ redis_report_file }}" + msg: "Full report available at: {{ certify_report_file }}" # run_once: no diff --git a/roles/redis/vars/platform-release-6.yml b/roles/redis/vars/platform-release-6.yml index ddfbfb0e..b2057884 100644 --- a/roles/redis/vars/platform-release-6.yml +++ b/roles/redis/vars/platform-release-6.yml @@ -17,3 +17,17 @@ redis_source_url_default: "8": "https://github.com/redis/redis/archive/7.4.6.tar.gz" "9": "https://github.com/redis/redis/archive/7.4.6.tar.gz" "2023": "https://github.com/redis/redis/archive/7.4.6.tar.gz" + +redis_hw_specs: + "dev": + "cpu_count": 8 + "ram_size": 16 + "disk_size": 100 + "test": + "cpu_count": 8 + "ram_size": 32 + "disk_size": 100 + "production": + "cpu_count": 8 + "ram_size": 32 + "disk_size": 100 From 31c24558b3e4c2b1baae99fc309ec7858e33d76d Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 29 Jan 2026 12:22:35 -0500 Subject: [PATCH 07/11] remove os_compatibility.py --- plugins/modules/os_compatibility.py | 101 ---------------------------- 1 file changed, 101 deletions(-) delete mode 100644 plugins/modules/os_compatibility.py diff --git a/plugins/modules/os_compatibility.py b/plugins/modules/os_compatibility.py deleted file mode 100644 index baffbe21..00000000 --- a/plugins/modules/os_compatibility.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/python - -# Copyright (c) 2024, 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: os_compatibility - -short_description: Inspect facts and determine if the host is compatible - -# If this is part of a collection, you need to use semantic versioning, -# i.e. the version is of the form "2.5.0" and not "2.4". -version_added: "3.0.0" - -description: This module will inspect the host facts and determine if the host is compatible for - installation of the Itential stack. The stack requires a dnf package manager and Redhat family - of linux of specific major versions. - -# Specify this value according to your collection -# in format of namespace.collection.doc_fragment_name -# extends_documentation_fragment: -# - my_namespace.my_collection.my_doc_fragment_name - -author: - - Steven Schattenberg (@steven-schattenberg-itential) -''' - -EXAMPLES = r''' -- name: Determine compatibility - itential.deployer.os_compatibility: -''' - -RETURN = r''' -# These are examples of possible return values, and in general should use other names for return values. -compatible: - description: Is this operating system compatible with the Itential platform - type: bool - returned: always - sample: false -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.facts.compat import ansible_facts - -def run_module(): - # define available arguments/parameters a user can pass to the module - module_args = dict() - - # seed the result dict in the object - # we primarily care about changed and state - # changed is if this module effectively modified the target - # state will include any data that you want your module to pass back - # for consumption, for example, in a subsequent task - result = dict( - changed=False, - compatible=False, - ) - - # the AnsibleModule object will be our abstraction working with Ansible - # this includes instantiation, a couple of common attr would be the - # args/params passed to the execution, as well as if the module - # supports check mode - 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) - - # If its a RedHat family of linux then set compatible to True - if facts["os_family"].lower() == "redhat": - if facts["distribution"].lower() == "redhat" or facts["distribution"].lower() == "rocky": - if int(facts["distribution_major_version"]) >= 8: - result["compatible"] = True - if facts["distribution"].lower() == "amazon": - if int(facts["distribution_major_version"]) >= 2023: - result["compatible"] = True - - # Fail the module if this host is not compatible - if result["compatible"] == False: - module.fail_json(msg='This is not a supported OS family!', **result) - - # 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 From 607da6b6459eb8166b8d18d65a55b7e2616f1736 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 2 Feb 2026 12:42:03 -0500 Subject: [PATCH 08/11] modified certify playbook to limit execution to groups --- playbooks/certify.yml | 69 ++----------------- playbooks/certify_mongodb.yml | 17 +++++ playbooks/certify_platform.yml | 17 +++++ playbooks/certify_redis.yml | 13 ++++ playbooks/preflight_gateway.yml | 45 ------------ playbooks/preflight_mongodb.yml | 46 ------------- playbooks/preflight_platform.yml | 46 ------------- playbooks/preflight_redis.yml | 45 ------------ roles/common_vars/defaults/main/preflight.yml | 14 ---- roles/mongodb/defaults/main/mongodb.yml | 3 + roles/mongodb/tasks/certify-mongodb.yml | 25 +++---- roles/platform/defaults/main/platform.yml | 3 + roles/platform/tasks/certify-platform.yml | 25 +++---- roles/redis/defaults/main/redis.yml | 3 + roles/redis/tasks/certify-redis.yml | 27 +++----- 15 files changed, 98 insertions(+), 300 deletions(-) create mode 100644 playbooks/certify_mongodb.yml create mode 100644 playbooks/certify_platform.yml create mode 100644 playbooks/certify_redis.yml delete mode 100644 playbooks/preflight_gateway.yml delete mode 100644 playbooks/preflight_mongodb.yml delete mode 100644 playbooks/preflight_platform.yml delete mode 100644 playbooks/preflight_redis.yml delete mode 100644 roles/common_vars/defaults/main/preflight.yml diff --git a/playbooks/certify.yml b/playbooks/certify.yml index 7688d469..2cb4b00b 100644 --- a/playbooks/certify.yml +++ b/playbooks/certify.yml @@ -1,69 +1,12 @@ # Copyright (c) 2026, Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Run Environment Certification Tasks - hosts: all - gather_facts: true - become: true - vars: - certify_report_dir: "/tmp/itential-reports" - # certify_report_file: "{{ certify_report_dir }}/{{ group_names[0] }}_report_{{ inventory_hostname }}" - roles: - - role: itential.deployer.common_vars - tags: always - tasks: - - name: Certify Redis Installation # noqa run-once - ansible.builtin.import_role: - name: itential.deployer.redis - tasks_from: certify-redis - when: inventory_hostname in groups['redis'] - tags: certify-redis - - name: Certify MongoDB Installation # noqa run-once - ansible.builtin.import_role: - name: itential.deployer.mongodb - tasks_from: certify-mongodb - when: inventory_hostname in groups['mongodb'] - tags: certify-mongodb +- name: Certify Redis Installation + import_playbook: itential.deployer.certify_redis - - name: Certify Itential Platform Installation # noqa run-once - ansible.builtin.import_role: - name: itential.deployer.platform - tasks_from: certify-platform - when: inventory_hostname in groups['platform'] - tags: certify-platform +- name: Certify MongoDB Installation + import_playbook: itential.deployer.certify_mongodb - - name: Remove old reports on the control node - ansible.builtin.file: - path: "{{ certify_report_dir }}" - state: absent - delegate_to: localhost - become: false - run_once: true - tags: always - - - name: Ensure the report directory exists on the control node - ansible.builtin.file: - path: "{{ certify_report_dir }}" - state: directory - mode: '0755' - delegate_to: localhost - become: false - run_once: true - tags: always - - - name: Find report files on remote host - ansible.builtin.find: - paths: "{{ certify_report_dir }}" - patterns: "*.md" - register: report_files_found - tags: always - - - name: Fetch all the report files to the control node - ansible.builtin.fetch: - fail_on_missing: false - src: "{{ item.path }}" - dest: "{{ certify_report_dir }}/{{ item.path | basename }}" - flat: true - loop: "{{ report_files_found.files }}" - tags: always +- name: Certify Platform Installation + import_playbook: itential.deployer.certify_platform diff --git a/playbooks/certify_mongodb.yml b/playbooks/certify_mongodb.yml new file mode 100644 index 00000000..ea5891b4 --- /dev/null +++ b/playbooks/certify_mongodb.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run MongoDB Certification Tasks + hosts: mongodb* + gather_facts: true + become: true + roles: + - role: itential.deployer.common_vars + tags: always + tasks: + - name: Certify MongoDB Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.mongodb + tasks_from: certify-mongodb + tags: certify-mongodb diff --git a/playbooks/certify_platform.yml b/playbooks/certify_platform.yml new file mode 100644 index 00000000..daf57b37 --- /dev/null +++ b/playbooks/certify_platform.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run Platform Certification Tasks + hosts: platform* + gather_facts: true + become: true + roles: + - role: itential.deployer.common_vars + tags: always + tasks: + - name: Certify Platform Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.platform + tasks_from: certify-platform + tags: certify-platform diff --git a/playbooks/certify_redis.yml b/playbooks/certify_redis.yml new file mode 100644 index 00000000..51ed8d8c --- /dev/null +++ b/playbooks/certify_redis.yml @@ -0,0 +1,13 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run Redis Certification Tasks + hosts: redis* + gather_facts: true + become: true + tasks: + - name: Certify Redis Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.redis + tasks_from: certify-redis diff --git a/playbooks/preflight_gateway.yml b/playbooks/preflight_gateway.yml deleted file mode 100644 index bc64abe2..00000000 --- a/playbooks/preflight_gateway.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Run Redis Preflight checks - hosts: gateway - become: true - roles: - # Pull in the common vars - - role: itential.deployer.common_vars - tags: - - always - tasks: - - name: Preflight - ansible.builtin.import_role: - name: itential.deployer.gateway - tasks_from: preflight - -- name: Read all data and combines and displays results - hosts: localhost - become: false - tags: always - roles: - - role: itential.deployer.common_vars - tasks: - - name: Set results_file - delegate_to: localhost - ansible.builtin.set_fact: - results_file: "{{ preflight_directory }}/Results_Gateway.txt" - - - name: Ensure the destination file exists and is empty - ansible.builtin.file: - path: "{{ results_file }}" - state: absent - - - name: Combine files into the output file - ansible.builtin.shell: | - echo "GATEWAY RESULTS" >> {{ results_file }} - for file in {{ preflight_directory }}/*.txt; do - if [[ $(basename "$file") == gateway*.txt ]]; then - echo ---------------- >> {{ results_file }} - cat "$file" >> {{ results_file }} - echo "" >> {{ results_file }} - fi - done - changed_when: true diff --git a/playbooks/preflight_mongodb.yml b/playbooks/preflight_mongodb.yml deleted file mode 100644 index 79f517fc..00000000 --- a/playbooks/preflight_mongodb.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Run Redis Preflight checks - hosts: mongodb, mongodb_secondary - become: true - roles: - # Pull in the common vars - - role: itential.deployer.common_vars - tags: - - always - tasks: - - name: Preflight - ansible.builtin.import_role: - name: itential.deployer.mongodb - tasks_from: preflight - -- name: Read all data and combines and displays results - hosts: localhost - become: false - tags: always - roles: - - role: itential.deployer.common_vars - tasks: - - name: Set results_file - delegate_to: localhost - ansible.builtin.set_fact: - results_file: "{{ preflight_directory }}/Results_MongoDB.txt" - - - name: Ensure the destination file exists and is empty - ansible.builtin.file: - path: "{{ results_file }}" - state: absent - - - name: Combine files into the output file - ansible.builtin.shell: | - echo "Mongodb RESULTS" >> {{ results_file }} - for file in {{ preflight_directory }}/*.txt; do - if [[ $(basename "$file") == mongodb*.txt ]]; then - echo ---------------- >> {{ results_file }} - cat "$file" >> {{ results_file }} - echo "" >> {{ results_file }} - fi - done - echo "" >> {{ results_file }} - changed_when: true diff --git a/playbooks/preflight_platform.yml b/playbooks/preflight_platform.yml deleted file mode 100644 index 070cc1ba..00000000 --- a/playbooks/preflight_platform.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Run Redis Preflight checks - hosts: platform, platform_secondary - become: true - roles: - # Pull in the common vars - - role: itential.deployer.common_vars - tags: - - always - tasks: - - name: Preflight - ansible.builtin.import_role: - name: itential.deployer.platform - tasks_from: preflight - -- name: Read all data and combines and displays results - hosts: localhost - become: false - tags: always - roles: - - role: itential.deployer.common_vars - tasks: - - name: Set results_file - delegate_to: localhost - ansible.builtin.set_fact: - results_file: "{{ preflight_directory }}/Results_Platform.txt" - - - name: Ensure the destination file exists and is empty - ansible.builtin.file: - path: "{{ results_file }}" - state: absent - - - name: Combine files into the output file - ansible.builtin.shell: | - echo "Platform RESULTS" >> {{ results_file }} - for file in {{ preflight_directory }}/*.txt; do - if [[ $(basename "$file") == platform*.txt ]]; then - echo ---------------- >> {{ results_file }} - cat "$file" >> {{ results_file }} - echo "" >> {{ results_file }} - fi - done - echo "" >> {{ results_file }} - changed_when: true diff --git a/playbooks/preflight_redis.yml b/playbooks/preflight_redis.yml deleted file mode 100644 index b60bd207..00000000 --- a/playbooks/preflight_redis.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -- name: Run Redis Preflight checks - hosts: redis, redis_secondary - become: true - roles: - # Pull in the common vars - - role: itential.deployer.common_vars - tags: - - always - tasks: - - name: Preflight - ansible.builtin.import_role: - name: itential.deployer.redis - tasks_from: preflight - -- name: Read all data and combines and displays results - hosts: localhost - become: false - tags: always - roles: - - role: itential.deployer.common_vars - tasks: - - name: Set results_file - delegate_to: localhost - ansible.builtin.set_fact: - results_file: "{{ preflight_directory }}/Results_Redis.txt" - - - name: Ensure the destination file exists and is empty - ansible.builtin.file: - path: "{{ results_file }}" - state: absent - - - name: Combine files into the output file - ansible.builtin.shell: | - echo "Redis RESULTS" >> {{ results_file }} - for file in {{ preflight_directory }}/*.txt; do - if [[ $(basename "$file") == redis*.txt ]]; then - echo ---------------- >> {{ results_file }} - cat "$file" >> {{ results_file }} - echo "" >> {{ results_file }} - fi - done - changed_when: true diff --git a/roles/common_vars/defaults/main/preflight.yml b/roles/common_vars/defaults/main/preflight.yml deleted file mode 100644 index bb5ea7ac..00000000 --- a/roles/common_vars/defaults/main/preflight.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2024, Itential, Inc -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) ---- -preflight_directory: "/tmp/preflight" - -# default mount to check -preflight_mounts: "/" - -# default env specs to check -preflight_env: dev - -# defaults for running and ignoring the preflight checks -preflight_run_checks: false -preflight_enforce_checks: false diff --git a/roles/mongodb/defaults/main/mongodb.yml b/roles/mongodb/defaults/main/mongodb.yml index 44785c8c..fd963163 100644 --- a/roles/mongodb/defaults/main/mongodb.yml +++ b/roles/mongodb/defaults/main/mongodb.yml @@ -71,3 +71,6 @@ mongodb_user_itential_password: itential # The name of the mongo replica set mongodb_replset_name: "{{ mongodb_replset_name_default }}" + +# Default location for the certification report files +mongodb_certify_report_dir: /tmp/itential-reports/mongodb diff --git a/roles/mongodb/tasks/certify-mongodb.yml b/roles/mongodb/tasks/certify-mongodb.yml index 70e0f5be..da44ea4a 100644 --- a/roles/mongodb/tasks/certify-mongodb.yml +++ b/roles/mongodb/tasks/certify-mongodb.yml @@ -2,16 +2,9 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Load variables from the MongoDB role - ansible.builtin.include_vars: - dir: "{{ item }}" - loop: - - "{{ role_path }}/../mongodb/defaults" - - "{{ role_path }}/../mongodb/vars" - - name: Ensure report directory exists ansible.builtin.file: - path: "{{ certify_report_dir }}" + path: "{{ mongodb_certify_report_dir }}" state: directory owner: "{{ mongodb_owner }}" group: "{{ mongodb_group }}" @@ -19,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - certify_report_file: "{{ certify_report_dir }}/mongodb-report-{{ inventory_hostname }}.md" + mongodb_certify_report_file: "{{ mongodb_certify_report_dir }}/mongodb-report-{{ inventory_hostname }}.md" - name: Check if MongoDB service exists ansible.builtin.systemd: @@ -383,11 +376,19 @@ - name: Generate MongoDB validation report ansible.builtin.template: src: mongodb-validation-report.md.j2 - dest: "{{ certify_report_file }}" + dest: "{{ mongodb_certify_report_file }}" mode: '0644' owner: "{{ mongodb_owner }}" group: "{{ mongodb_group }}" -- name: Display report location +- name: Copy validation report to the control node + itential.deployer.fetch_to_control: + src: "{{ mongodb_certify_report_file }}" + dest: "{{ mongodb_certify_report_file }}" + +- name: Display report summary ansible.builtin.debug: - msg: "MongoDB validation report generated at: {{ certify_report_file }}" + msg: + - "MongoDB validation complete for {{ inventory_hostname }}" + - "Overall Status: {{ 'PASSED ✓' if (mongodb_process is defined and mongodb_process.rc == 0) else 'FAILED ✗' }}" + - "Report saved to: {{ mongodb_certify_report_file }} on both the remote and control nodes." diff --git a/roles/platform/defaults/main/platform.yml b/roles/platform/defaults/main/platform.yml index 4ad71e11..3563acca 100644 --- a/roles/platform/defaults/main/platform.yml +++ b/roles/platform/defaults/main/platform.yml @@ -52,3 +52,6 @@ platform_app_artifacts_enabled: false # Flag to determine if the service is started platform_start_service: true + +# Default location for the certification report files +platform_certify_report_dir: /tmp/itential-reports/platform \ No newline at end of file diff --git a/roles/platform/tasks/certify-platform.yml b/roles/platform/tasks/certify-platform.yml index e9ebe43e..4af6237d 100644 --- a/roles/platform/tasks/certify-platform.yml +++ b/roles/platform/tasks/certify-platform.yml @@ -2,16 +2,9 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Load variables from the Platform role - ansible.builtin.include_vars: - dir: "{{ item }}" - loop: - - "{{ role_path }}/../platform/defaults" - - "{{ role_path }}/../platform/vars" - - name: Ensure report directory exists ansible.builtin.file: - path: "{{ certify_report_dir }}" + path: "{{ platform_certify_report_dir }}" state: directory owner: "{{ platform_user }}" group: "{{ platform_group }}" @@ -19,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - certify_report_file: "{{ certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" + platform_certify_report_file: "{{ platform_certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" - name: Check if Itential service exists ansible.builtin.systemd: @@ -555,11 +548,19 @@ - name: Generate Itential platform validation report ansible.builtin.template: src: platform-validation-report.md.j2 - dest: "{{ certify_report_file }}" + dest: "{{ platform_certify_report_file }}" mode: '0644' owner: "{{ platform_user }}" group: "{{ platform_group }}" -- name: Display report location +- name: Copy validation report to the control node + itential.deployer.fetch_to_control: + src: "{{ platform_certify_report_file }}" + dest: "{{ platform_certify_report_file }}" + +- name: Display report summary ansible.builtin.debug: - msg: "Itential platform validation report generated at: {{ certify_report_file }}" + msg: + - "Platform validation complete for {{ inventory_hostname }}" + - "Overall Status: {{ 'PASSED ✓' if (iap_process is defined and iap_process.rc == 0) else 'FAILED ✗' }}" + - "Report saved to: {{ platform_certify_report_file }} on both the remote and control nodes." diff --git a/roles/redis/defaults/main/redis.yml b/roles/redis/defaults/main/redis.yml index afde9d4d..cc9fbfd7 100644 --- a/roles/redis/defaults/main/redis.yml +++ b/roles/redis/defaults/main/redis.yml @@ -61,3 +61,6 @@ redis_user_repluser_password: repluser redis_user_sentineladmin_password: admin redis_user_sentineluser_password: sentineluser redis_user_prometheus_password: prometheus + +# Default location for the certification report files +redis_certify_report_dir: /tmp/itential-reports/redis diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index cb3e90b4..2245ad89 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -2,16 +2,9 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Load variables from the Redis role - ansible.builtin.include_vars: - dir: "{{ item }}" - loop: - - "{{ role_path }}/../redis/defaults" - - "{{ role_path }}/../redis/vars" - - name: Ensure report directory exists ansible.builtin.file: - path: "{{ certify_report_dir }}" + path: "{{ redis_certify_report_dir }}" state: directory owner: "{{ redis_owner }}" group: "{{ redis_group }}" @@ -19,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - certify_report_file: "{{ certify_report_dir }}/redis-report-{{ inventory_hostname }}.md" + redis_certify_report_file: "{{ redis_certify_report_dir }}/redis-report-{{ inventory_hostname }}.md" - name: Gather host information itential.deployer.gather_host_information: @@ -532,25 +525,25 @@ - sentinel_acl_list.rc == 0 # ========================================================================= -# Generate the report +# Generate the report and copy to control node # ========================================================================= - name: Generate validation report ansible.builtin.template: backup: true - dest: "{{ certify_report_file }}" + dest: "{{ redis_certify_report_file }}" group: "{{ redis_group }}" mode: "0665" owner: "{{ redis_owner }}" src: redis-validation-report.md.j2 +- name: Copy validation report to the control node + itential.deployer.fetch_to_control: + src: "{{ redis_certify_report_file }}" + dest: "{{ redis_certify_report_file }}" + - 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: {{ certify_report_file }}" - -- name: Display report location - ansible.builtin.debug: - msg: "Full report available at: {{ certify_report_file }}" - # run_once: no + - "Report saved to: {{ redis_certify_report_file }} on both the remote and control nodes." From 21ff1d8ea222b73be67ef74b4bbdf7de6d4e0a76 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 2 Feb 2026 12:57:02 -0500 Subject: [PATCH 09/11] Fix lint errors --- playbooks/certify_mongodb.yml | 3 --- playbooks/certify_platform.yml | 3 --- roles/platform/defaults/main/platform.yml | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/playbooks/certify_mongodb.yml b/playbooks/certify_mongodb.yml index ea5891b4..04c3fb31 100644 --- a/playbooks/certify_mongodb.yml +++ b/playbooks/certify_mongodb.yml @@ -6,9 +6,6 @@ hosts: mongodb* gather_facts: true become: true - roles: - - role: itential.deployer.common_vars - tags: always tasks: - name: Certify MongoDB Installation # noqa run-once ansible.builtin.import_role: diff --git a/playbooks/certify_platform.yml b/playbooks/certify_platform.yml index daf57b37..e6c30b31 100644 --- a/playbooks/certify_platform.yml +++ b/playbooks/certify_platform.yml @@ -6,9 +6,6 @@ hosts: platform* gather_facts: true become: true - roles: - - role: itential.deployer.common_vars - tags: always tasks: - name: Certify Platform Installation # noqa run-once ansible.builtin.import_role: diff --git a/roles/platform/defaults/main/platform.yml b/roles/platform/defaults/main/platform.yml index 3563acca..447b71fa 100644 --- a/roles/platform/defaults/main/platform.yml +++ b/roles/platform/defaults/main/platform.yml @@ -54,4 +54,4 @@ platform_app_artifacts_enabled: false platform_start_service: true # Default location for the certification report files -platform_certify_report_dir: /tmp/itential-reports/platform \ No newline at end of file +platform_certify_report_dir: /tmp/itential-reports/platform From 55c72082002f1de4916f58c01548e25c04b6eca7 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 3 Feb 2026 14:41:21 -0500 Subject: [PATCH 10/11] Add capability to verify environment readiness --- .gitignore | 6 + playbooks/verify.yml | 146 ++-------------------- playbooks/verify_mongodb.yml | 13 ++ playbooks/verify_platform.yml | 13 ++ playbooks/verify_redis.yml | 13 ++ plugins/action/fetch_to_control.py | 147 +++++++++++++++++++++++ roles/mongodb/tasks/verify-mongodb.yml | 137 +++++++++++++++++++++ roles/platform/tasks/verify-platform.yml | 137 +++++++++++++++++++++ roles/redis/tasks/verify-redis.yml | 137 +++++++++++++++++++++ 9 files changed, 610 insertions(+), 139 deletions(-) create mode 100644 playbooks/verify_mongodb.yml create mode 100644 playbooks/verify_platform.yml create mode 100644 playbooks/verify_redis.yml create mode 100644 plugins/action/fetch_to_control.py create mode 100644 roles/mongodb/tasks/verify-mongodb.yml create mode 100644 roles/platform/tasks/verify-platform.yml create mode 100644 roles/redis/tasks/verify-redis.yml diff --git a/.gitignore b/.gitignore index 6a76e1ac..1a071cec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +cspell.json +.ansible/* +.vscode/* .DS_Store **.DS_Store **/ansible.cfg @@ -8,3 +11,6 @@ **/*.pem **/*.log **/*.keep +inventories/* +inventories +certificates/* diff --git a/playbooks/verify.yml b/playbooks/verify.yml index fea9aa44..ab15cb1f 100644 --- a/playbooks/verify.yml +++ b/playbooks/verify.yml @@ -1,144 +1,12 @@ -# Copyright (c) 2024, Itential, Inc +# Copyright (c) 2026, Itential, Inc # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: Run Environment Verification Tasks - hosts: all - gather_facts: true - become: true - roles: - - role: itential.deployer.common_vars - tags: always - tasks: +- name: Verify Redis Installation + import_playbook: itential.deployer.verify_redis - - name: Import role vars - ansible.builtin.include_vars: - file: "{{ item }}" - loop: - - "../roles/redis/vars/platform-release-{{ platform_release }}.yml" - - "../roles/mongodb/vars/platform-release-{{ platform_release }}.yml" - - "../roles/platform/vars/platform-release-{{ platform_release }}.yml" +- name: Verify MongoDB Installation + import_playbook: itential.deployer.verify_mongodb - - name: Gather hardware specs - ansible.builtin.set_fact: - hardware_specs: - "mongodb": "{{ mongodb_hw_specs[env] }}" - "platform": "{{ platform_hw_specs[env] }}" - "redis": "{{ redis_hw_specs[env] }}" - - - name: Gather host information - itential.deployer.gather_host_information: - register: host_info - - - name: Extract OS information - ansible.builtin.set_fact: - os: "{{ host_info.os }}" - - # OS and Architecture validation - - name: Check OS compatibility - ansible.builtin.set_fact: - os_valid: >- - {{ - (os.distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or - (os.distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or - (os.distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or - (os.distribution == 'Amazon' and ansible_distribution_major_version == '2023') - }} - - - name: Assert that this is a supported OS - ansible.builtin.assert: - that: "{{ os_valid }} == true" - fail_msg: "{{ os.distribution }} {{ os.distribution_version }} is not a supported OS!" - success_msg: "OS validation passed!" - quiet: true - - - name: Check architecture compatibility - ansible.builtin.set_fact: - arch_valid: "{{ os.architecture in ['x86_64', 'aarch64'] }}" - - - name: Assert that this is a supported Architecture - ansible.builtin.assert: - that: "{{ arch_valid }} == true" - fail_msg: "{{ os.architecture }} is not a supported architecture!" - success_msg: "Architecture validation passed!" - quiet: true - - # Hardware spec validation - - name: Determine the applicable specs based on host group - 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: - 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)) }}" - - - name: Validate CPU Count - ansible.builtin.assert: - that: hardware_validation.validation.cpu_valid | bool - fail_msg: "CPU validation failed" - quiet: true - ignore_errors: true - register: cpu_validation - - - name: Validate Memory Amount - ansible.builtin.assert: - that: hardware_validation.validation.memory_valid | bool - fail_msg: "Memory validation failed" - quiet: true - ignore_errors: true - register: memory_validation - - - name: Validate Disk Size - ansible.builtin.assert: - that: hardware_validation.validation.disk_valid | bool - fail_msg: "Disk validation failed" - quiet: true - ignore_errors: true - register: disk_validation - - - name: Print host information - ansible.builtin.debug: - msg: "{{ host_info }}" - - # Fail at the end if any validation failed - - name: Check if any validations failed - ansible.builtin.fail: - msg: | - Hardware validation failures detected: - {% if cpu_validation is failed %} - - CPU: {{ hardware_validation.required.cpu_count }} required, {{ hardware_validation.actual.cpu_count }} found - {% endif %} - {% if memory_validation is failed %} - - Memory: {{ hardware_validation.required.ram_size_gb }}GB required, {{ hardware_validation.actual.ram_size_gb }}GB found - {% endif %} - {% if disk_validation is failed %} - - Disk: {{ hardware_validation.required.disk_size_gb }}GB required, {{ hardware_validation.actual.disk_size_gb }}GB found - {% endif %} - when: cpu_validation is failed or memory_validation is failed or disk_validation is failed +- name: Verify Platform Installation + import_playbook: itential.deployer.verify_platform diff --git a/playbooks/verify_mongodb.yml b/playbooks/verify_mongodb.yml new file mode 100644 index 00000000..8010081a --- /dev/null +++ b/playbooks/verify_mongodb.yml @@ -0,0 +1,13 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run MongoDB Verification Tasks + hosts: mongodb* + gather_facts: true + become: true + tasks: + - name: Verify MongoDB Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.mongodb + tasks_from: verify-mongodb diff --git a/playbooks/verify_platform.yml b/playbooks/verify_platform.yml new file mode 100644 index 00000000..e2c7b74c --- /dev/null +++ b/playbooks/verify_platform.yml @@ -0,0 +1,13 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run Platform Verification Tasks + hosts: platform* + gather_facts: true + become: true + tasks: + - name: Verify Platform Environment # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.platform + tasks_from: verify-platform diff --git a/playbooks/verify_redis.yml b/playbooks/verify_redis.yml new file mode 100644 index 00000000..b2bcb22a --- /dev/null +++ b/playbooks/verify_redis.yml @@ -0,0 +1,13 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Run Redis Verification Tasks + hosts: redis* + gather_facts: true + become: true + tasks: + - name: Verify Redis Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.redis + tasks_from: verify-redis diff --git a/plugins/action/fetch_to_control.py b/plugins/action/fetch_to_control.py new file mode 100644 index 00000000..68b5e5d4 --- /dev/null +++ b/plugins/action/fetch_to_control.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Itential LLC +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: fetch_to_control +short_description: Fetch file from remote node to control node +description: + - Fetches a file from the remote node to the control node + - Creates destination directories if they don't exist + - Replaces existing files on the control node + - This is an action plugin that runs on the control node + - Uses the slurp module internally to read files from remote nodes +version_added: "1.0.0" +options: + src: + description: + - Path to file on remote node + required: true + type: str + dest: + description: + - Destination path on control node (full path including filename) + required: true + type: str +author: + - Steven Schattenberg (steven.schattenberg@itential.com) +notes: + - This is an action plugin, not a regular module + - It must be placed in plugins/action/ directory of your collection + - Internally uses the slurp module to read files from remote nodes +''' + +EXAMPLES = r''' +# Fetch a report file +- name: Fetch remote file + fetch_to_control: + src: /tmp/itential-reports/report.md + dest: /tmp/itential-reports/report.md +''' + +RETURN = r''' +src: + description: Source path on remote node + type: str + returned: always +dest: + description: Destination path on control node + type: str + returned: always +size: + description: Size of the fetched file in bytes + type: int + returned: success +changed: + description: Whether the file was fetched + type: bool + returned: always +''' + +import os +import base64 +from ansible.plugins.action import ActionBase +from ansible.errors import AnsibleActionFail + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp + + # Get parameters + src = self._task.args.get('src') + dest = self._task.args.get('dest') + + # Validate required parameters + if not src: + raise AnsibleActionFail("'src' parameter is required") + if not dest: + raise AnsibleActionFail("'dest' parameter is required") + + # Expand paths + dest = os.path.expanduser(dest) + dest_dir = os.path.dirname(dest) + + # Create destination directory on control node if it doesn't exist + if dest_dir and not os.path.exists(dest_dir): + try: + os.makedirs(dest_dir, mode=0o755) + except Exception as e: + raise AnsibleActionFail(f"Failed to create destination directory {dest_dir}: {str(e)}") + + # Remove existing file on control node if it exists + if os.path.exists(dest): + try: + os.remove(dest) + except Exception as e: + raise AnsibleActionFail(f"Failed to remove existing file {dest}: {str(e)}") + + # Use slurp module to read file from remote node + slurp_result = self._execute_module( + module_name='slurp', + module_args={'src': src}, + task_vars=task_vars + ) + + # Check if slurp failed + if slurp_result.get('failed'): + raise AnsibleActionFail(f"Failed to read remote file {src}: {slurp_result.get('msg', 'Unknown error')}") + + # Get the base64 encoded content + content = slurp_result.get('content', '') + if not content: + raise AnsibleActionFail(f"No content returned from remote file {src}") + + # Decode base64 content + try: + decoded_content = base64.b64decode(content) + except Exception as e: + raise AnsibleActionFail(f"Failed to decode file content: {str(e)}") + + # Write content to control node + try: + with open(dest, 'wb') as f: + f.write(decoded_content) + except Exception as e: + raise AnsibleActionFail(f"Failed to write file to {dest}: {str(e)}") + + # Build result + result.update({ + 'changed': True, + 'src': src, + 'dest': dest, + 'size': len(decoded_content), + 'msg': f"File fetched from {src} to {dest}" + }) + + return result \ No newline at end of file diff --git a/roles/mongodb/tasks/verify-mongodb.yml b/roles/mongodb/tasks/verify-mongodb.yml new file mode 100644 index 00000000..1224a3e0 --- /dev/null +++ b/roles/mongodb/tasks/verify-mongodb.yml @@ -0,0 +1,137 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Announce Intention + ansible.builtin.debug: + msg: "Validating {{ env }} host {{ inventory_hostname }} for MongoDB installation..." + +- name: Load Itential Platform release default variables + ansible.builtin.include_vars: + file: "{{ item }}" + with_first_found: + - "platform-release-{{ platform_release }}.yml" + - "platform-release-{{ platform_release | string | split('.') | first }}.yml" + - "platform-release-undefined.yml" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_info + +- name: Extract OS information + ansible.builtin.set_fact: + os: "{{ host_info.os }}" + +# OS and Architecture validation +- name: Check OS compatibility + ansible.builtin.set_fact: + os_valid: >- + {{ + (os.distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Amazon' and ansible_distribution_major_version == '2023') + }} + +- name: Assert that this is a supported OS + ansible.builtin.assert: + that: "{{ os_valid }} == true" + fail_msg: "{{ os.distribution }} {{ os.distribution_version }} is not a supported OS!" + success_msg: "OS validation passed!" + quiet: true + +- name: Check architecture compatibility + ansible.builtin.set_fact: + arch_valid: "{{ os.architecture in ['x86_64', 'aarch64'] }}" + +- name: Assert that this is a supported Architecture + ansible.builtin.assert: + that: "{{ arch_valid }} == true" + fail_msg: "{{ os.architecture }} is not a supported architecture!" + success_msg: "Architecture validation passed!" + quiet: true + +- name: Initialize validation errors list + ansible.builtin.set_fact: + validation_errors: [] + +- 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: + required: + cpu_count: "{{ mongodb_hw_specs[env].cpu_count if mongodb_hw_specs != 'none' else 'N/A' }}" + ram_size_gb: "{{ mongodb_hw_specs[env].ram_size if mongodb_hw_specs != 'none' else 'N/A' }}" + disk_size_gb: "{{ mongodb_hw_specs[env].disk_size if mongodb_hw_specs != '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: "{{ (env == 'none') or (ansible_processor_vcpus >= mongodb_hw_specs[env].cpu_count) }}" + ram_valid: "{{ (env == 'none') or ((ansible_memtotal_mb / 1024) >= mongodb_hw_specs[env].ram_size) }}" + disk_valid: "{{ (env == 'none') or ((root_disk_size_gb | default(0) | float) >= mongodb_hw_specs[env].disk_size) }}" + all_valid: "{{ (env == 'none') or ((ansible_processor_vcpus >= mongodb_hw_specs[env].cpu_count) and ((ansible_memtotal_mb / 1024) >= mongodb_hw_specs[env].ram_size) and ((root_disk_size_gb | default(0) | float) >= mongodb_hw_specs[env].disk_size)) }}" + +- name: Validate CPU Count + ansible.builtin.assert: + that: hardware_validation.validation.cpu_valid | bool + fail_msg: "CPU validation failed!" + quiet: true + ignore_errors: true + register: cpu_validation + +- name: Add CPU error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['CPU: ' ~ hardware_validation.required.cpu_count ~ ' required, ' ~ hardware_validation.actual.cpu_count ~ ' found'] }}" + when: cpu_validation is failed + +- name: Validate memory amount + ansible.builtin.assert: + that: hardware_validation.validation.ram_valid | bool + fail_msg: "Memory validation failed!" + quiet: true + ignore_errors: true + register: memory_validation + +- name: Add memory error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['RAM: ' ~ hardware_validation.required.ram_size_gb ~ 'GB required, ' ~ hardware_validation.actual.ram_size_gb ~ 'GB found'] }}" + when: memory_validation is failed + +- name: Validate disk size + ansible.builtin.assert: + that: hardware_validation.validation.disk_valid | bool + fail_msg: "Disk validation failed!" + quiet: true + ignore_errors: true + register: disk_validation + +- name: Add disk error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['Disk: ' ~ hardware_validation.required.disk_size_gb ~ 'GB required, ' ~ hardware_validation.actual.disk_size_gb ~ 'GB found'] }}" + when: disk_validation is failed + +- name: Print host information + ansible.builtin.debug: + msg: "{{ host_info }}" + +# Display results +- name: Display failed validation results + ansible.builtin.debug: + msg: "{{ validation_errors }}" + when: cpu_validation is failed or memory_validation is failed or disk_validation is failed + +# Assert that none of the tests failed +- name: Verify that all tests passed + ansible.builtin.assert: + that: + - "cpu_validation is not failed" + - "memory_validation is not failed" + - "disk_validation is no failed" + fail_msg: "See above, assertions not passed! ✗" + success_msg: "All assertions passed! ✓" diff --git a/roles/platform/tasks/verify-platform.yml b/roles/platform/tasks/verify-platform.yml new file mode 100644 index 00000000..5398855d --- /dev/null +++ b/roles/platform/tasks/verify-platform.yml @@ -0,0 +1,137 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Announce Intention + ansible.builtin.debug: + msg: "Validating {{ env }} host {{ inventory_hostname }} for Platform installation..." + +- name: Load Itential Platform release default variables + ansible.builtin.include_vars: + file: "{{ item }}" + with_first_found: + - "platform-release-{{ platform_release }}.yml" + - "platform-release-{{ platform_release | string | split('.') | first }}.yml" + - "platform-release-undefined.yml" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_info + +- name: Extract OS information + ansible.builtin.set_fact: + os: "{{ host_info.os }}" + +# OS and Architecture validation +- name: Check OS compatibility + ansible.builtin.set_fact: + os_valid: >- + {{ + (os.distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Amazon' and ansible_distribution_major_version == '2023') + }} + +- name: Assert that this is a supported OS + ansible.builtin.assert: + that: "{{ os_valid }} == true" + fail_msg: "{{ os.distribution }} {{ os.distribution_version }} is not a supported OS!" + success_msg: "OS validation passed!" + quiet: true + +- name: Check architecture compatibility + ansible.builtin.set_fact: + arch_valid: "{{ os.architecture in ['x86_64', 'aarch64'] }}" + +- name: Assert that this is a supported Architecture + ansible.builtin.assert: + that: "{{ arch_valid }} == true" + fail_msg: "{{ os.architecture }} is not a supported architecture!" + success_msg: "Architecture validation passed!" + quiet: true + +- name: Initialize validation errors list + ansible.builtin.set_fact: + validation_errors: [] + +- 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: + required: + cpu_count: "{{ platform_hw_specs[env].cpu_count if platform_hw_specs != 'none' else 'N/A' }}" + ram_size_gb: "{{ platform_hw_specs[env].ram_size if platform_hw_specs != 'none' else 'N/A' }}" + disk_size_gb: "{{ platform_hw_specs[env].disk_size if platform_hw_specs != '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: "{{ (env == 'none') or (ansible_processor_vcpus >= platform_hw_specs[env].cpu_count) }}" + ram_valid: "{{ (env == 'none') or ((ansible_memtotal_mb / 1024) >= platform_hw_specs[env].ram_size) }}" + disk_valid: "{{ (env == 'none') or ((root_disk_size_gb | default(0) | float) >= platform_hw_specs[env].disk_size) }}" + all_valid: "{{ (env == 'none') or ((ansible_processor_vcpus >= platform_hw_specs[env].cpu_count) and ((ansible_memtotal_mb / 1024) >= platform_hw_specs[env].ram_size) and ((root_disk_size_gb | default(0) | float) >= platform_hw_specs[env].disk_size)) }}" + +- name: Validate CPU Count + ansible.builtin.assert: + that: hardware_validation.validation.cpu_valid | bool + fail_msg: "CPU validation failed!" + quiet: true + ignore_errors: true + register: cpu_validation + +- name: Add CPU error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['CPU: ' ~ hardware_validation.required.cpu_count ~ ' required, ' ~ hardware_validation.actual.cpu_count ~ ' found'] }}" + when: cpu_validation is failed + +- name: Validate memory amount + ansible.builtin.assert: + that: hardware_validation.validation.ram_valid | bool + fail_msg: "Memory validation failed!" + quiet: true + ignore_errors: true + register: memory_validation + +- name: Add memory error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['RAM: ' ~ hardware_validation.required.ram_size_gb ~ 'GB required, ' ~ hardware_validation.actual.ram_size_gb ~ 'GB found'] }}" + when: memory_validation is failed + +- name: Validate disk size + ansible.builtin.assert: + that: hardware_validation.validation.disk_valid | bool + fail_msg: "Disk validation failed!" + quiet: true + ignore_errors: true + register: disk_validation + +- name: Add disk error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['Disk: ' ~ hardware_validation.required.disk_size_gb ~ 'GB required, ' ~ hardware_validation.actual.disk_size_gb ~ 'GB found'] }}" + when: disk_validation is failed + +- name: Print host information + ansible.builtin.debug: + msg: "{{ host_info }}" + +# Display results +- name: Display failed validation results + ansible.builtin.debug: + msg: "{{ validation_errors }}" + when: cpu_validation is failed or memory_validation is failed or disk_validation is failed + +# Assert that none of the tests failed +- name: Verify that all tests passed + ansible.builtin.assert: + that: + - "cpu_validation is not failed" + - "memory_validation is not failed" + - "disk_validation is no failed" + fail_msg: "See above, assertions not passed! ✗" + success_msg: "All assertions passed! ✓" diff --git a/roles/redis/tasks/verify-redis.yml b/roles/redis/tasks/verify-redis.yml new file mode 100644 index 00000000..97ec36fb --- /dev/null +++ b/roles/redis/tasks/verify-redis.yml @@ -0,0 +1,137 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- + +- name: Announce Intention + ansible.builtin.debug: + msg: "Validating {{ env }} host {{ inventory_hostname }} for Redis installation..." + +- name: Load Itential Platform release default variables + ansible.builtin.include_vars: + file: "{{ item }}" + with_first_found: + - "platform-release-{{ platform_release }}.yml" + - "platform-release-{{ platform_release | string | split('.') | first }}.yml" + - "platform-release-undefined.yml" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_info + +- name: Extract OS information + ansible.builtin.set_fact: + os: "{{ host_info.os }}" + +# OS and Architecture validation +- name: Check OS compatibility + ansible.builtin.set_fact: + os_valid: >- + {{ + (os.distribution == 'RedHat' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Rocky' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'OracleLinux' and ansible_distribution_major_version in ['8', '9']) or + (os.distribution == 'Amazon' and ansible_distribution_major_version == '2023') + }} + +- name: Assert that this is a supported OS + ansible.builtin.assert: + that: "{{ os_valid }} == true" + fail_msg: "{{ os.distribution }} {{ os.distribution_version }} is not a supported OS!" + success_msg: "OS validation passed!" + quiet: true + +- name: Check architecture compatibility + ansible.builtin.set_fact: + arch_valid: "{{ os.architecture in ['x86_64', 'aarch64'] }}" + +- name: Assert that this is a supported Architecture + ansible.builtin.assert: + that: "{{ arch_valid }} == true" + fail_msg: "{{ os.architecture }} is not a supported architecture!" + success_msg: "Architecture validation passed!" + quiet: true + +- name: Initialize validation errors list + ansible.builtin.set_fact: + validation_errors: [] + +- 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: + required: + cpu_count: "{{ redis_hw_specs[env].cpu_count if redis_hw_specs != 'none' else 'N/A' }}" + ram_size_gb: "{{ redis_hw_specs[env].ram_size if redis_hw_specs != 'none' else 'N/A' }}" + disk_size_gb: "{{ redis_hw_specs[env].disk_size if redis_hw_specs != '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: "{{ (env == 'none') or (ansible_processor_vcpus >= redis_hw_specs[env].cpu_count) }}" + ram_valid: "{{ (env == 'none') or ((ansible_memtotal_mb / 1024) >= redis_hw_specs[env].ram_size) }}" + disk_valid: "{{ (env == 'none') or ((root_disk_size_gb | default(0) | float) >= redis_hw_specs[env].disk_size) }}" + all_valid: "{{ (env == 'none') or ((ansible_processor_vcpus >= redis_hw_specs[env].cpu_count) and ((ansible_memtotal_mb / 1024) >= redis_hw_specs[env].ram_size) and ((root_disk_size_gb | default(0) | float) >= redis_hw_specs[env].disk_size)) }}" + +- name: Validate CPU Count + ansible.builtin.assert: + that: hardware_validation.validation.cpu_valid | bool + fail_msg: "CPU validation failed!" + quiet: true + ignore_errors: true + register: cpu_validation + +- name: Add CPU error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['CPU: ' ~ hardware_validation.required.cpu_count ~ ' required, ' ~ hardware_validation.actual.cpu_count ~ ' found'] }}" + when: cpu_validation is failed + +- name: Validate memory amount + ansible.builtin.assert: + that: hardware_validation.validation.ram_valid | bool + fail_msg: "Memory validation failed!" + quiet: true + ignore_errors: true + register: memory_validation + +- name: Add memory error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['RAM: ' ~ hardware_validation.required.ram_size_gb ~ 'GB required, ' ~ hardware_validation.actual.ram_size_gb ~ 'GB found'] }}" + when: memory_validation is failed + +- name: Validate disk size + ansible.builtin.assert: + that: hardware_validation.validation.disk_valid | bool + fail_msg: "Disk validation failed!" + quiet: true + ignore_errors: true + register: disk_validation + +- name: Add disk error to list + ansible.builtin.set_fact: + validation_errors: "{{ validation_errors + ['Disk: ' ~ hardware_validation.required.disk_size_gb ~ 'GB required, ' ~ hardware_validation.actual.disk_size_gb ~ 'GB found'] }}" + when: disk_validation is failed + +- name: Print host information + ansible.builtin.debug: + msg: "{{ host_info }}" + +# Display results +- name: Display failed validation results + ansible.builtin.debug: + msg: "{{ validation_errors }}" + when: cpu_validation is failed or memory_validation is failed or disk_validation is failed + +# Assert that none of the tests failed +- name: Verify that all tests passed + ansible.builtin.assert: + that: + - "cpu_validation is not failed" + - "memory_validation is not failed" + - "disk_validation is not failed" + fail_msg: "See above, assertions not passed! ✗" + success_msg: "All assertions passed! ✓" From 9163a91a234831bf7e1dcc0fa7f01e9fee14ff60 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 5 Feb 2026 13:40:53 -0500 Subject: [PATCH 11/11] Code review changes --- plugins/action/fetch_to_control.py | 147 ---------------------- roles/mongodb/defaults/main/mongodb.yml | 3 +- roles/mongodb/tasks/certify-mongodb.yml | 57 ++++++--- roles/platform/defaults/main/platform.yml | 3 +- roles/platform/tasks/certify-platform.yml | 14 ++- roles/redis/defaults/main/redis.yml | 3 +- roles/redis/tasks/certify-redis.yml | 18 ++- 7 files changed, 67 insertions(+), 178 deletions(-) delete mode 100644 plugins/action/fetch_to_control.py diff --git a/plugins/action/fetch_to_control.py b/plugins/action/fetch_to_control.py deleted file mode 100644 index 68b5e5d4..00000000 --- a/plugins/action/fetch_to_control.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Itential LLC -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = r''' ---- -module: fetch_to_control -short_description: Fetch file from remote node to control node -description: - - Fetches a file from the remote node to the control node - - Creates destination directories if they don't exist - - Replaces existing files on the control node - - This is an action plugin that runs on the control node - - Uses the slurp module internally to read files from remote nodes -version_added: "1.0.0" -options: - src: - description: - - Path to file on remote node - required: true - type: str - dest: - description: - - Destination path on control node (full path including filename) - required: true - type: str -author: - - Steven Schattenberg (steven.schattenberg@itential.com) -notes: - - This is an action plugin, not a regular module - - It must be placed in plugins/action/ directory of your collection - - Internally uses the slurp module to read files from remote nodes -''' - -EXAMPLES = r''' -# Fetch a report file -- name: Fetch remote file - fetch_to_control: - src: /tmp/itential-reports/report.md - dest: /tmp/itential-reports/report.md -''' - -RETURN = r''' -src: - description: Source path on remote node - type: str - returned: always -dest: - description: Destination path on control node - type: str - returned: always -size: - description: Size of the fetched file in bytes - type: int - returned: success -changed: - description: Whether the file was fetched - type: bool - returned: always -''' - -import os -import base64 -from ansible.plugins.action import ActionBase -from ansible.errors import AnsibleActionFail - -class ActionModule(ActionBase): - - def run(self, tmp=None, task_vars=None): - if task_vars is None: - task_vars = dict() - - result = super(ActionModule, self).run(tmp, task_vars) - del tmp - - # Get parameters - src = self._task.args.get('src') - dest = self._task.args.get('dest') - - # Validate required parameters - if not src: - raise AnsibleActionFail("'src' parameter is required") - if not dest: - raise AnsibleActionFail("'dest' parameter is required") - - # Expand paths - dest = os.path.expanduser(dest) - dest_dir = os.path.dirname(dest) - - # Create destination directory on control node if it doesn't exist - if dest_dir and not os.path.exists(dest_dir): - try: - os.makedirs(dest_dir, mode=0o755) - except Exception as e: - raise AnsibleActionFail(f"Failed to create destination directory {dest_dir}: {str(e)}") - - # Remove existing file on control node if it exists - if os.path.exists(dest): - try: - os.remove(dest) - except Exception as e: - raise AnsibleActionFail(f"Failed to remove existing file {dest}: {str(e)}") - - # Use slurp module to read file from remote node - slurp_result = self._execute_module( - module_name='slurp', - module_args={'src': src}, - task_vars=task_vars - ) - - # Check if slurp failed - if slurp_result.get('failed'): - raise AnsibleActionFail(f"Failed to read remote file {src}: {slurp_result.get('msg', 'Unknown error')}") - - # Get the base64 encoded content - content = slurp_result.get('content', '') - if not content: - raise AnsibleActionFail(f"No content returned from remote file {src}") - - # Decode base64 content - try: - decoded_content = base64.b64decode(content) - except Exception as e: - raise AnsibleActionFail(f"Failed to decode file content: {str(e)}") - - # Write content to control node - try: - with open(dest, 'wb') as f: - f.write(decoded_content) - except Exception as e: - raise AnsibleActionFail(f"Failed to write file to {dest}: {str(e)}") - - # Build result - result.update({ - 'changed': True, - 'src': src, - 'dest': dest, - 'size': len(decoded_content), - 'msg': f"File fetched from {src} to {dest}" - }) - - return result \ No newline at end of file diff --git a/roles/mongodb/defaults/main/mongodb.yml b/roles/mongodb/defaults/main/mongodb.yml index fd963163..624347ba 100644 --- a/roles/mongodb/defaults/main/mongodb.yml +++ b/roles/mongodb/defaults/main/mongodb.yml @@ -73,4 +73,5 @@ mongodb_user_itential_password: itential mongodb_replset_name: "{{ mongodb_replset_name_default }}" # Default location for the certification report files -mongodb_certify_report_dir: /tmp/itential-reports/mongodb +mongodb_certify_report_dir_remote: /var/tmp/itential-reports/mongodb +mongodb_certify_report_dir_local: /tmp/itential-reports/mongodb diff --git a/roles/mongodb/tasks/certify-mongodb.yml b/roles/mongodb/tasks/certify-mongodb.yml index da44ea4a..38a71378 100644 --- a/roles/mongodb/tasks/certify-mongodb.yml +++ b/roles/mongodb/tasks/certify-mongodb.yml @@ -4,7 +4,7 @@ - name: Ensure report directory exists ansible.builtin.file: - path: "{{ mongodb_certify_report_dir }}" + path: "{{ mongodb_certify_report_dir_remote }}" state: directory owner: "{{ mongodb_owner }}" group: "{{ mongodb_group }}" @@ -12,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - mongodb_certify_report_file: "{{ mongodb_certify_report_dir }}/mongodb-report-{{ inventory_hostname }}.md" + mongodb_certify_report_file: "{{ mongodb_certify_report_dir_remote }}/mongodb-report-{{ inventory_hostname }}.md" - name: Check if MongoDB service exists ansible.builtin.systemd: @@ -80,7 +80,9 @@ ansible.builtin.stat: path: "{{ mongodb_data_dir.stdout }}" register: mongodb_data_dir_stat - when: mongodb_data_dir.stdout is defined and mongodb_data_dir.stdout != "" + when: + - mongodb_data_dir.stdout is defined + - mongodb_data_dir.stdout != "" ignore_errors: true - name: Get data directory size @@ -88,7 +90,9 @@ register: mongodb_data_size ignore_errors: true changed_when: false - when: mongodb_data_dir.stdout is defined and mongodb_data_dir.stdout != "" + when: + - mongodb_data_dir.stdout is defined + - mongodb_data_dir.stdout != "" # ============================================================================ # CONNECTIVITY TESTS @@ -109,7 +113,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined # ============================================================================ # TLS/SSL CHECKS @@ -135,7 +141,9 @@ ansible.builtin.stat: path: "{{ mongodb_tls_cert_path.stdout }}" register: mongodb_tls_cert_file - when: mongodb_tls_cert_path.stdout is defined and mongodb_tls_cert_path.stdout != "" + when: + - mongodb_tls_cert_path.stdout is defined + - mongodb_tls_cert_path.stdout != "" ignore_errors: true - name: Get TLS certificate expiration @@ -174,7 +182,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined - name: Get MongoDB build info ansible.builtin.shell: | @@ -184,7 +194,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined # ============================================================================ # REPLICA SET STATUS @@ -252,7 +264,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined - name: Parse MongoDB users ansible.builtin.set_fact: @@ -298,7 +312,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined # ============================================================================ # LOGS @@ -350,7 +366,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined - name: Get connection count ansible.builtin.shell: | @@ -360,7 +378,9 @@ ignore_errors: true changed_when: false no_log: true - when: mongodb_user_admin is defined and mongodb_user_admin_password is defined + when: + - mongodb_user_admin is defined + - mongodb_user_admin_password is defined # ============================================================================ # GATHER HOST INFORMATION @@ -375,16 +395,19 @@ # ============================================================================ - name: Generate MongoDB validation report ansible.builtin.template: - src: mongodb-validation-report.md.j2 + backup: true dest: "{{ mongodb_certify_report_file }}" - mode: '0644' - owner: "{{ mongodb_owner }}" + src: mongodb-validation-report.md.j2 group: "{{ mongodb_group }}" + owner: "{{ mongodb_owner }}" + mode: '0644' - name: Copy validation report to the control node - itential.deployer.fetch_to_control: + ansible.builtin.fetch: + dest: "{{ mongodb_certify_report_dir_local }}/" + fail_on_missing: false + flat: true src: "{{ mongodb_certify_report_file }}" - dest: "{{ mongodb_certify_report_file }}" - name: Display report summary ansible.builtin.debug: diff --git a/roles/platform/defaults/main/platform.yml b/roles/platform/defaults/main/platform.yml index 447b71fa..7afe5f4e 100644 --- a/roles/platform/defaults/main/platform.yml +++ b/roles/platform/defaults/main/platform.yml @@ -54,4 +54,5 @@ platform_app_artifacts_enabled: false platform_start_service: true # Default location for the certification report files -platform_certify_report_dir: /tmp/itential-reports/platform +platform_certify_report_dir_remote: /var/tmp/itential-reports/platform +platform_certify_report_dir_local: /tmp/itential-reports/platform diff --git a/roles/platform/tasks/certify-platform.yml b/roles/platform/tasks/certify-platform.yml index 4af6237d..7e6749a4 100644 --- a/roles/platform/tasks/certify-platform.yml +++ b/roles/platform/tasks/certify-platform.yml @@ -4,7 +4,7 @@ - name: Ensure report directory exists ansible.builtin.file: - path: "{{ platform_certify_report_dir }}" + path: "{{ platform_certify_report_dir_remote }}" state: directory owner: "{{ platform_user }}" group: "{{ platform_group }}" @@ -12,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - platform_certify_report_file: "{{ platform_certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" + platform_certify_report_file: "{{ platform_certify_report_dir_remote }}/platform-report-{{ inventory_hostname }}.md" - name: Check if Itential service exists ansible.builtin.systemd: @@ -124,7 +124,9 @@ ansible.builtin.set_fact: mongo_url_masked: "{{ iap_mongo_url.stdout | regex_replace('(mongodb[^:]*://[^:]+:)([^@]+)(@.*)', '\\1****\\3') }}" no_log: true - when: iap_mongo_url.stdout is defined and iap_mongo_url.stdout != "" + when: + - iap_mongo_url.stdout is defined + - iap_mongo_url.stdout != "" - name: Parse Itential config for mongo_tls_enabled ansible.builtin.shell: @@ -554,9 +556,11 @@ group: "{{ platform_group }}" - name: Copy validation report to the control node - itential.deployer.fetch_to_control: + ansible.builtin.fetch: + dest: "{{ platform_certify_report_dir_local }}/" + fail_on_missing: false + flat: true src: "{{ platform_certify_report_file }}" - dest: "{{ platform_certify_report_file }}" - name: Display report summary ansible.builtin.debug: diff --git a/roles/redis/defaults/main/redis.yml b/roles/redis/defaults/main/redis.yml index cc9fbfd7..1ebab4e9 100644 --- a/roles/redis/defaults/main/redis.yml +++ b/roles/redis/defaults/main/redis.yml @@ -63,4 +63,5 @@ redis_user_sentineluser_password: sentineluser redis_user_prometheus_password: prometheus # Default location for the certification report files -redis_certify_report_dir: /tmp/itential-reports/redis +redis_certify_report_dir_remote: /var/tmp/itential-reports/redis +redis_certify_report_dir_local: /tmp/itential-reports/redis diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 2245ad89..2bf38af6 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -4,7 +4,7 @@ - name: Ensure report directory exists ansible.builtin.file: - path: "{{ redis_certify_report_dir }}" + path: "{{ redis_certify_report_dir_remote }}" state: directory owner: "{{ redis_owner }}" group: "{{ redis_group }}" @@ -12,7 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: - redis_certify_report_file: "{{ redis_certify_report_dir }}/redis-report-{{ inventory_hostname }}.md" + redis_certify_report_file: "{{ redis_certify_report_dir_remote }}/redis-report-{{ inventory_hostname }}.md" - name: Gather host information itential.deployer.gather_host_information: @@ -134,7 +134,9 @@ 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 + when: + - redis_info.rc is defined + - redis_info.rc == 0 - name: Get list of configured Redis users ansible.builtin.shell: | @@ -146,7 +148,9 @@ -a "{{ redis_user_admin_password }}" \ --no-auth-warning \ ACL LIST - when: redis_info.rc is defined and redis_info.rc == 0 + when: + - redis_info.rc is defined + - redis_info.rc == 0 register: redis_acl_list no_log: false failed_when: false @@ -537,9 +541,11 @@ src: redis-validation-report.md.j2 - name: Copy validation report to the control node - itential.deployer.fetch_to_control: + ansible.builtin.fetch: + dest: "{{ redis_certify_report_dir_local }}/" + fail_on_missing: false + flat: true src: "{{ redis_certify_report_file }}" - dest: "{{ redis_certify_report_file }}" - name: Display report summary ansible.builtin.debug: