From 45ef20301ccd898cf8a6cb86103e7b01db550071 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 20 Jan 2026 20:15:28 +0000 Subject: [PATCH 01/20] Update galaxy version and changelog for release 3.7.2 [skip ci] --- CHANGELOG.md | 11 +++++++++++ galaxy.yml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56fd4f12..c8d71efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,21 @@ # Changelog +## v3.7.2 (January 09, 2026) + +* Added retries to epel repo install task https://github.com/itential/itential.deployer/pull/285 +* Change Gateway offline install method to use pip wheel/download https://github.com/itential/itential.deployer/pull/287 +* Update repository descriptions in README https://github.com/itential/itential.deployer/pull/283 +* added support for TLS 1.3 https://github.com/itential/itential.deployer/pull/284 + +Full Changelog: https://github.com/itential/itential.deployer/compare/v3.7.1...v3.7.2 + + ## v3.7.1 (December 19, 2025) * Redis config updates https://github.com/itential/itential.deployer/pull/279 * Remove old variables that are no longer used https://github.com/itential/itential.deployer/pull/280 * Remove platform password encryption https://github.com/itential/itential.deployer/pull/281 +* Update galaxy version and changelog for release 3.7.1 [skip ci] * setting vm swappiness to recommended value 1 https://github.com/itential/itential.deployer/pull/282 Full Changelog: https://github.com/itential/itential.deployer/compare/v3.7.0...v3.7.1 diff --git a/galaxy.yml b/galaxy.yml index 46b68490..49b68ddb 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,7 +8,7 @@ namespace: itential name: deployer # The version of the collection. Must be compatible with semantic versioning -version: 3.7.1 +version: 3.7.2 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md From a7e78d8260cd976da79fe4c76515981ea3608644 Mon Sep 17 00:00:00 2001 From: kvelarde-itential <87794456+kvelarde-itential@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:54:25 -0700 Subject: [PATCH 02/20] Set vm.max_map_count kernel parameter on MongoDB servers (#288) * Configure vm.max_map_count * Remove mongodb tasks that set pam limits * Add new MongoDB kernel parameter variables to docs --- docs/mongodb_guide.md | 6 +++ roles/mongodb/defaults/main/kernel_params.yml | 10 ++++ ...el-params.yml => adjust-kernel-params.yml} | 49 ++++++++++--------- roles/mongodb/tasks/install-mongodb.yml | 2 +- 4 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 roles/mongodb/defaults/main/kernel_params.yml rename roles/mongodb/tasks/{install-adjust-kernel-params.yml => adjust-kernel-params.yml} (62%) diff --git a/docs/mongodb_guide.md b/docs/mongodb_guide.md index 276fd193..a6a7d3cf 100644 --- a/docs/mongodb_guide.md +++ b/docs/mongodb_guide.md @@ -138,6 +138,12 @@ These variables apply to advanced situations. | `mongodb_mongod_service_delay` | Integer | The time in seconds between retries when starting the mongod service. | 10 | | `mongodb_status_poll` | Integer | The maximum number of times to query for the replicaset status before the set converges or we fail. | 3 | | `mongodb_status_interval` | Integer | The number of seconds to wait between polling executions. | 10 | +| `mongodb_sysctl_file` | String | The name of the MongoDB sysctl file | /etc/sysctl.d/98-mongodb.conf | +| `mongodb_net_ipv4_tcp_keepalive_time` | Integer | Time (in seconds) that a TCP connection remains idle before the kernel starts sending keepalive probes to verify the connection is still alive. | 300 | +| `mongodb_net_core_somaxconn` | Integer | Controls the backlog queue size for incoming connections. When the queue is full, new connection attempts are rejected. | 65535 | +| `mongodb_vm_zone_reclaim_mode` | Integer | Controls whether the kernel reclaims memory from local zones before allocating from remote NUMA nodes. | 0 | +| `mongodb_vm_swappiness` | Integer | Balances between swapping out anonymous pages (process memory) versus dropping page cache (file system buffers). | 1 | +| `mongodb_vm_max_map_count` | Integer | Maximum number of memory map areas (virtual memory areas/VMAs) a process can create. | 262144 | ## Configuring TLS diff --git a/roles/mongodb/defaults/main/kernel_params.yml b/roles/mongodb/defaults/main/kernel_params.yml new file mode 100644 index 00000000..4f533571 --- /dev/null +++ b/roles/mongodb/defaults/main/kernel_params.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2024, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +# Kernel parameters +mongodb_sysctl_file: /etc/sysctl.d/98-mongodb.conf +mongodb_net_ipv4_tcp_keepalive_time: 300 +mongodb_net_core_somaxconn: 65535 +mongodb_vm_zone_reclaim_mode: 0 +mongodb_vm_swappiness: 1 +mongodb_vm_max_map_count: 262144 diff --git a/roles/mongodb/tasks/install-adjust-kernel-params.yml b/roles/mongodb/tasks/adjust-kernel-params.yml similarity index 62% rename from roles/mongodb/tasks/install-adjust-kernel-params.yml rename to roles/mongodb/tasks/adjust-kernel-params.yml index 87f7dc99..7db5bc70 100644 --- a/roles/mongodb/tasks/install-adjust-kernel-params.yml +++ b/roles/mongodb/tasks/adjust-kernel-params.yml @@ -17,34 +17,39 @@ - name: Adjust keepalive ansible.posix.sysctl: name: net.ipv4.tcp_keepalive_time - value: 300 - -- name: Disable zone reclaim mode - ansible.posix.sysctl: - name: vm.zone_reclaim_mode - value: 0 + value: "{{ mongodb_net_ipv4_tcp_keepalive_time }}" + state: present + sysctl_file: "{{ mongodb_sysctl_file }}" + reload: true - name: Increase throughput settings ansible.posix.sysctl: name: net.core.somaxconn - value: 65535 + value: "{{ mongodb_net_core_somaxconn }}" + state: present + sysctl_file: "{{ mongodb_sysctl_file }}" + reload: true + +- name: Disable zone reclaim mode + ansible.posix.sysctl: + name: vm.zone_reclaim_mode + value: "{{ mongodb_vm_zone_reclaim_mode }}" + state: present + sysctl_file: "{{ mongodb_sysctl_file }}" + reload: true - name: Set vm swappiness ansible.posix.sysctl: name: vm.swappiness - value: 1 + value: "{{ mongodb_vm_swappiness }}" + state: present + sysctl_file: "{{ mongodb_sysctl_file }}" + reload: true -# Set Soft User Limits -- name: Set number of procs - community.general.pam_limits: - domain: mongod - limit_type: soft - limit_item: nproc - value: 32000 - -- name: Set number of files - community.general.pam_limits: - domain: mongod - limit_type: soft - limit_item: nofile - value: 64000 +- name: Set vm max_map_count + ansible.posix.sysctl: + name: vm.max_map_count + value: "{{ mongodb_vm_max_map_count }}" + state: present + sysctl_file: "{{ mongodb_sysctl_file }}" + reload: true diff --git a/roles/mongodb/tasks/install-mongodb.yml b/roles/mongodb/tasks/install-mongodb.yml index 7833c7c5..d2d1784b 100644 --- a/roles/mongodb/tasks/install-mongodb.yml +++ b/roles/mongodb/tasks/install-mongodb.yml @@ -53,7 +53,7 @@ - name: Adjust Kernel parameters ansible.builtin.import_tasks: - file: install-adjust-kernel-params.yml + file: adjust-kernel-params.yml - name: Configure SELinux ansible.builtin.include_tasks: From c3aaf2a753040aa0a18391fe1f44313a59a25573 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 29 Jan 2026 12:22:35 -0500 Subject: [PATCH 03/20] 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 9f56c1ccee664ed3578752025ec6b9ae2abbf9ba Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 15 Jan 2026 21:20:19 -0500 Subject: [PATCH 04/20] 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 c6c4a4b89c852deb95b4add53829801b48aa554a Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Fri, 16 Jan 2026 07:37:15 -0500 Subject: [PATCH 05/20] 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 ddb1a896fa32e64f17414699109533b2e57b7424 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Fri, 16 Jan 2026 07:39:21 -0500 Subject: [PATCH 06/20] 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 dd2670aa11331753b3ec953e857847364e148a40 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 20 Jan 2026 12:14:26 -0500 Subject: [PATCH 07/20] 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 81941e7782f3d28580503c93f8de8b8d90922f79 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 20 Jan 2026 15:16:24 -0500 Subject: [PATCH 08/20] 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 128e74519ec58fa807de3b364fb0e9b63ddd43ba Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 29 Jan 2026 12:00:22 -0500 Subject: [PATCH 09/20] 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 fa698b55bbc3299e163da746a487ddfa0ad49092 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 2 Feb 2026 12:42:03 -0500 Subject: [PATCH 10/20] 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 778a7b45977eee0c4b5c18649d846ba8475c8e68 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 2 Feb 2026 12:57:02 -0500 Subject: [PATCH 11/20] 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 45402a844ad7b62c9c5f645a43a446e5db667e6c Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Tue, 3 Feb 2026 14:41:21 -0500 Subject: [PATCH 12/20] 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 dd2ebf19eb00abcee4c53ed41b049e7825f71780 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 5 Feb 2026 13:40:53 -0500 Subject: [PATCH 13/20] 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: From 6fea1a5c9aebe54ea93c227f481d6c7a9b98a20b Mon Sep 17 00:00:00 2001 From: kvelarde-itential <87794456+kvelarde-itential@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:26:29 -0700 Subject: [PATCH 14/20] Update redis playbook and role to support new ASA architecture (#291) * Initial version of new redis groups * Split the redis group into 3 new groups - redis_master, redis_replica and redis_sentinel --- README.md | 55 +++++++++------- docs/redis_guide.md | 71 +++++++++++++------- playbooks/redis.yml | 14 +++- roles/redis/defaults/main/redis.yml | 30 +++------ roles/redis/defaults/main/sentinel.yml | 13 +++- roles/redis/handlers/main.yml | 3 +- roles/redis/tasks/configure-redis.yml | 15 +++++ roles/redis/tasks/configure-selinux.yml | 6 +- roles/redis/tasks/configure-sentinel.yml | 84 +++++++++++++++++++----- roles/redis/tasks/install-common.yml | 29 -------- roles/redis/tasks/main.yml | 23 +++++-- roles/redis/templates/redis.conf.j2 | 37 +++++------ roles/redis/templates/sentinel.conf.j2 | 17 +++-- 13 files changed, 242 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index 68a17a9a..ba9b809b 100644 --- a/README.md +++ b/README.md @@ -546,7 +546,7 @@ all: platform_release: 6 children: - redis: + redis_master: hosts: example1.host.com: @@ -715,7 +715,7 @@ all: platform_release: 6 children: - redis: + redis_master: hosts: example1.host.com: @@ -751,7 +751,7 @@ all: platform_release: 6 children: - redis: + redis_master: hosts: redis.host.com: @@ -791,13 +791,22 @@ all: platform_release: 6 children: - redis: + redis_master: hosts: redis1.host.com: + + redis_replica: + hosts: redis2.host.com: redis3.host.com: vars: - redis_replication_enabled: true + redis_replicaof: # defaults to "{{ groups['redis_master'][0] }} {{ redis_port}}" + + redis_sentinel: + hosts: + sentinel1.host.com: + sentinel2.host.com: + sentinel3.host.com: mongodb: hosts: @@ -818,11 +827,11 @@ all: platform_mongo_url: mongodb://itential:itential@mongodb1.host.com:27017,mongodb2.host.com:27017,mongodb3.host.com:27017/itential?replicaSet=rs0 # Redis config platform_redis_sentinels: - - host: redis1.host.com + - host: sentinel1.host.com port: 26379 - - host: redis2.host.com + - host: sentinel2.host.com port: 26379 - - host: redis3.host.com + - host: rsentinel3.host.com port: 26379 gateway: @@ -885,21 +894,23 @@ all: platform_release: 6 children: - redis: + redis_master: hosts: datacenter1.redis1.host.com: - datacenter1.redis2.host.com: - datacenter1.redis3.host.com: - vars: - redis_replication_enabled: true - redis_secondary: + redis_replica: hosts: + datacenter1.redis2.host.com: datacenter2.redis1.host.com: datacenter2.redis2.host.com: - datacenter2.redis3.host.com: vars: - redis_replication_enabled: true + redis_replicaof: # defaults to "{{ groups['redis_master'][0] }} {{ redis_port}}" + + redis_sentinel: + hosts: + datacenter1.sentinel1.host.com: + datacenter2.sentinel1.host.com: + datacenter3.sentinel1.host.com: mongodb: hosts: @@ -930,11 +941,11 @@ all: platform_redis_sentinel_username: itential platform_redis_sentinel_password: platform_redis_sentinels: - - host: datacenter1.redis1.host.com + - host: datacenter1.sentinel1.host.com port: 26379 - - host: datacenter1.redis2.host.com + - host: datacenter2.sentinel1.host.com port: 26379 - - host: datacenter1.redis3.host.com + - host: datacenter3.sentinel1.host.com port: 26379 platform_secondary: @@ -953,11 +964,11 @@ all: platform_redis_sentinel_username: itential platform_redis_sentinel_password: platform_redis_sentinels: - - host: datacenter2.redis1.host.com + - host: datacenter1.sentinel1.host.com port: 26379 - - host: datacenter2.redis2.host.com + - host: datacenter2.sentinel1.host.com port: 26379 - - host: datacenter2.redis3.host.com + - host: datacenter3.sentinel1.host.com port: 26379 gateway: diff --git a/docs/redis_guide.md b/docs/redis_guide.md index b14b739d..5a5ebdf6 100644 --- a/docs/redis_guide.md +++ b/docs/redis_guide.md @@ -103,9 +103,7 @@ The following tables lists the default variables located in `roles/redis/default | `redis_port` | Integer | The Redis listen port. | `6379` | | `redis_owner` | String | The Redis Linux user. | `redis` | | `redis_group` | String | The Redis Linux group. | `redis` | -| `redis_bind_ipv6` | Boolean | Flag to enable IPv6. | `true` | -| `redis_bind_addr_source` | String | The bind address source. Will default to the Ansible `inventory_hostname` unless explicitly set to `default_ipv4_address`. | `inventory_hostname` | -| `redis_bind_addrs` | String | A space-separated list of hostnames/IP addresses on which Redis listeners will be created. If `redis_bind_ipv6` is set to `true`, `::1` will be added to the addresses. The `redis_bind_addr_source` will also be added to the addresses. | `127.0.0.1` | +| `redis_bind` | String | A space-separated list of hostnames/IP addresses on which Redis listeners will be created. | `bind 127.0.0.1 {{ ansible_default_ipv4.address }}` | | `redis_tls_enabled` | Boolean | Flag to enable TLS connections. | `false` | ### Auth Variables @@ -124,11 +122,18 @@ The following tables lists the default variables located in `roles/redis/default | Variable | Type | Description | Default Value | | :------- | :--- | :---------- | :------------ | -| `redis_replication_enabled` | Boolean | Flag to enable Redis replication. When set to `true`, Redis replication will be configured and the Redis Sentinel service started. | `false` | -| `redis_sentinel_port` | Integer | The Redis Sentinel listen port | `26379` | +| `redis_replicaof` | String | The Redis replicaof setting.
Use replicaof to make a Redis instance a copy of another Redis server. | "{{ groups['redis_master'][0] }} {{ redis_port}}" | + +### Sentinel Variables + +| Variable | Type | Description | Default Value | +| :------- | :--- | :---------- | :------------ | | `redis_sentinel_conf_file` | String | The location of the Redis Sentinel configuration file. | `/etc/redis/sentinel.conf` | | `redis_sentinel_log` | String | The location of the Redis Sentinel log file. | `/var/log/redis/sentinel.log` | -| `redis_master_name` | String | The Redis master name | `itentialmaster` | +| `redis_sentinel_port` | Integer | The Redis Sentinel listen port | `26379` | +| `redis_sentinel_bind` | String | A space-separated list of hostnames/IP addresses on which Redis listeners will be created. | `bind 127.0.0.1 {{ ansible_default_ipv4.address }}` | +| `redis_sentinel_master_name` | String | The Redis master name | `itentialmaster` | +| `redis_sentinel_quorum` | String | The Sentinel quorum setting.
Auto-calculate quorum based on sentinel count (recommended).
Set to explicit number to override (must be <= number of sentinels). | `auto` | ### Offline Variables @@ -168,15 +173,15 @@ be found in `roles/redis/vars/platform-release-.yml`. ## Building Your Inventory -To install and configure Redis, add a `redis` group and host(s) to your inventory. The following -inventory shows a basic Redis configuration with a single Redis node with no authentication. +To install and configure Redis, add a `redis_master` group and host(s) to your inventory. The following +inventory shows a basic Redis configuration with a single Redis node with authentication. ### Example Inventory - Single Redis Node ```yaml all: children: - redis: + redis_master: hosts: : ansible_host: @@ -189,11 +194,12 @@ all: ```yaml all: children: - redis: + redis_master: hosts: : ansible_host: vars: + platform_release: 6 redis_source_url: https://github.com/redis/redis/archive/7.2.7.tar.gz ``` @@ -202,7 +208,7 @@ all: ```yaml all: children: - redis: + redis_master: hosts: : ansible_host: @@ -211,41 +217,60 @@ all: redis_install_from_source: false ``` -To enable authentication, add the `redis_auth_enabled` flag to the `redis` group and set it to `true`. +To configure a Redis replica set, add the replica hosts to the `redis_replica` group and configure the `redis_replicaof` variable. -### Example Inventory - Configure Redis Authentication +### Example Inventory - Configure Redis Replication ```yaml all: + vars: + platform_release: 6 children: - redis: + redis_master: hosts: : ansible_host: + + redis_replica: + hosts: + : + ansible_host: + : + ansible_host: vars: - platform_release: 6 - redis_auth_enabled: true + redis_replicaof: # defaults to "{{ groups['redis_master'][0] }} {{ redis_port}}" ``` -To configure a Redis replica set, add the `redis_replication_enabled` flag to the `redis` group and set it to `true` and add the additional hosts. - -### Example Inventory - Configure Redis Replication +To configure Sentinels, add the sentinel hosts to the `redis_sentinel` group. ```yaml all: + vars: + platform_release: 6 children: - redis: + redis_master: hosts: : ansible_host: + + redis_replica: + hosts: : ansible_host: : ansible_host: vars: - platform_release: 6 - redis_auth_enabled: true - redis_replication_enabled: true + redis_replicaof: # defaults to "{{ groups['redis_master'][0] }} {{ redis_port}}" + + redis_sentinel: + hosts: + hosts: + : + ansible_host: + : + ansible_host: + : + ansible_host: ``` ## Running the Playbook diff --git a/playbooks/redis.yml b/playbooks/redis.yml index 520855fe..f8ca1d8f 100644 --- a/playbooks/redis.yml +++ b/playbooks/redis.yml @@ -2,8 +2,18 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- ### Redis -- name: Install Redis - hosts: redis, redis_secondary +- name: Configure Redis data nodes + hosts: redis_master, redis_replica + become: true + roles: + # Pull in the common vars + - role: itential.deployer.common + + # Perform a base installation of Redis and optionally configure authorization and replication + - role: itential.deployer.redis + +- name: Configure Redis sentinel nodes + hosts: redis_sentinel become: true roles: # Pull in the common vars diff --git a/roles/redis/defaults/main/redis.yml b/roles/redis/defaults/main/redis.yml index 1ebab4e9..0920490b 100644 --- a/roles/redis/defaults/main/redis.yml +++ b/roles/redis/defaults/main/redis.yml @@ -26,32 +26,17 @@ redis_group: redis # Default redis port redis_port: "{{ redis_port_default }}" -# The default redis sentinel listen port -redis_sentinel_port: "{{ redis_sentinel_port_default }}" - -# Flag to enable IPv6 -redis_bind_ipv6: true - -# The bind address source -# Will default to inventory_hostname unless set to default_ipv4_address -redis_bind_addr_source: inventory_hostname - -# The hostnames and/or IP addresses on which redis should listen for -# client connections, separated by spaces. -# -# If redis_bind_ipv6 is set to true, '::1' will be added to the -# redis_bind_addrs. -# -# The inventory_hostname or default_ipv4_address will be automatically added -# to the redis_bind_addrs depending on the redis_bind_addr_source. -redis_bind_addrs: 127.0.0.1 +# Bind to localhost + private network interface +redis_bind: 127.0.0.1 {{ ansible_default_ipv4.address }} # Feature flags that can be overridden in the hosts file -# Essentially, a default installation will have auth, but not replication or tls +# Essentially, a default installation will have auth, but not tls redis_auth_enabled: true -redis_replication_enabled: false redis_tls_enabled: false +# Replication settings +redis_replicaof: "{{ groups['redis_master'][0] }} {{ redis_port }}" + # Default redis user passwords # These are meant to be changed by the customer. By default they are meant to # be predictable and simple. @@ -65,3 +50,6 @@ redis_user_prometheus_password: prometheus # Default location for the certification report files redis_certify_report_dir_remote: /var/tmp/itential-reports/redis redis_certify_report_dir_local: /tmp/itential-reports/redis +# By default the prometheus user will be enabled. If this is set to false, the prometheus user +# will not be created. +redis_prometheus_user_enabled: true diff --git a/roles/redis/defaults/main/sentinel.yml b/roles/redis/defaults/main/sentinel.yml index 65232865..aaf87a05 100644 --- a/roles/redis/defaults/main/sentinel.yml +++ b/roles/redis/defaults/main/sentinel.yml @@ -8,7 +8,14 @@ redis_sentinel_conf_file: "{{ redis_conf_dir }}/sentinel.conf" redis_sentinel_log: "{{ redis_log_dir }}/sentinel.log" # The Redis Sentinel master name -redis_master_name: itentialmaster +redis_sentinel_master_name: itentialmaster -# The Redis Sentinel master host. Defaults to the first server in the redis group. -redis_master_host: "{{ hostvars[groups['redis'][0]].inventory_hostname }}" +# The default redis sentinel listen port +redis_sentinel_port: "{{ redis_sentinel_port_default }}" + +# Bind to localhost + private network interface +redis_sentinel_bind: 127.0.0.1 {{ ansible_default_ipv4.address }} + +# Auto-calculate quorum based on sentinel count (recommended) +# Set to explicit number to override (must be <= number of sentinels) +redis_sentinel_quorum: auto diff --git a/roles/redis/handlers/main.yml b/roles/redis/handlers/main.yml index ec355364..f9bbfcca 100644 --- a/roles/redis/handlers/main.yml +++ b/roles/redis/handlers/main.yml @@ -8,6 +8,7 @@ enabled: true state: restarted daemon_reload: true + when: redis_is_data_node - name: Enable and Start Redis Sentinel throttle: 1 @@ -16,7 +17,7 @@ enabled: true state: restarted daemon_reload: true - when: redis_replication_enabled + when: redis_is_sentinel_node - name: Update Itential release file ansible.builtin.include_tasks: diff --git a/roles/redis/tasks/configure-redis.yml b/roles/redis/tasks/configure-redis.yml index f0a872cf..2175534d 100644 --- a/roles/redis/tasks/configure-redis.yml +++ b/roles/redis/tasks/configure-redis.yml @@ -1,6 +1,21 @@ # 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 service facts + ansible.builtin.service_facts: + +- name: Open Redis port on FirewallD Public Zone + ansible.posix.firewalld: + port: "{{ redis_port }}/tcp" + permanent: true + state: enabled + zone: public + immediate: true + when: + - ansible_facts.services["firewalld.service"] is defined + - ansible_facts.services["firewalld.service"].state == "running" + - ansible_facts.services["firewalld.service"].status == "enabled" + - name: Create Redis systemd file ansible.builtin.template: src: redis.service.j2 diff --git a/roles/redis/tasks/configure-selinux.yml b/roles/redis/tasks/configure-selinux.yml index b58a6406..9868d3f2 100644 --- a/roles/redis/tasks/configure-selinux.yml +++ b/roles/redis/tasks/configure-selinux.yml @@ -33,7 +33,9 @@ proto: tcp setype: redis_port_t state: present - when: redis_port != redis_port_default + when: + - redis_is_data_node + - redis_port != redis_port_default - name: SELinux - Configure port when using non-standard Sentinel port community.general.seport: @@ -42,7 +44,7 @@ setype: redis_port_t state: present when: - - redis_replication_enabled | bool + - redis_is_sentinel_node - redis_sentinel_port != redis_sentinel_port_default - name: SELinux - Configure file context when using non-standard bin directory diff --git a/roles/redis/tasks/configure-sentinel.yml b/roles/redis/tasks/configure-sentinel.yml index acc4395e..37be5f22 100644 --- a/roles/redis/tasks/configure-sentinel.yml +++ b/roles/redis/tasks/configure-sentinel.yml @@ -1,6 +1,32 @@ # 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 service facts + ansible.builtin.service_facts: + +- name: Open Sentinel port on FirewallD Public Zone + ansible.posix.firewalld: + port: "{{ redis_sentinel_port }}/tcp" + permanent: true + state: enabled + zone: public + immediate: true + when: + - ansible_facts.services["firewalld.service"] is defined + - ansible_facts.services["firewalld.service"].state == "running" + - ansible_facts.services["firewalld.service"].status == "enabled" + +- name: Create Redis Sentinel data directory + ansible.builtin.file: + state: directory + path: "{{ redis_data_dir }}/sentinel" + owner: "{{ redis_owner }}" + group: "{{ redis_group }}" + mode: "0755" + seuser: system_u + serole: object_r + setype: redis_var_lib_t + - name: Create Redis Sentinel systemd file ansible.builtin.template: src: redis-sentinel.service.j2 @@ -9,20 +35,48 @@ group: root mode: "0644" -- name: Use template to generate sentinel.conf - ansible.builtin.template: - src: sentinel.conf.j2 - dest: "{{ redis_sentinel_conf_file }}" - owner: "{{ redis_owner }}" - group: "{{ redis_group }}" - mode: "0640" - setype: redis_conf_t - backup: true - when: - - groups['redis'] is defined - - inventory_hostname in groups['redis'] +# Sentinel quorum configuration +# - If redis_sentinel_quorum is not 'auto', use the explicit value +# - Otherwise, calculate majority quorum: ceil(sentinel_count / 2) +# This ensures quorum is "more than half" of sentinels +# Examples: 1→1, 2→1, 3→2, 4→2, 5→3, 6→3, 7→4 +# - Defaults to 1 if redis_sentinel group is undefined or empty +- name: Set sentinel quorum + ansible.builtin.set_fact: + redis_sentinel_quorum_final: >- + {{ + redis_sentinel_quorum if redis_sentinel_quorum != 'auto' + else ((groups['redis_sentinel'] | default([]) | length / 2) | round(0, 'ceil') | int) | default(1) + }} -- name: Use template to generate sentinel.conf for secondary DR +- name: Validate sentinel quorum + ansible.builtin.assert: + that: + - redis_sentinel_quorum_final | int >= 1 + - redis_sentinel_quorum_final | int <= (groups['redis_sentinel'] | default([]) | length) + - groups['redis_sentinel'] | default([]) | length >= 3 or redis_sentinel_quorum != 'auto' + fail_msg: "Invalid quorum {{ redis_sentinel_quorum_final }} for {{ groups['redis_sentinel'] | default([]) | length }} sentinels (min 3 recommended)" + success_msg: "Sentinel quorum: {{ redis_sentinel_quorum_final }}/{{ groups['redis_sentinel'] | default([]) | length }}" + run_once: true + +- name: Display sentinel quorum configuration + ansible.builtin.debug: + msg: "Using sentinel quorum: {{ redis_sentinel_quorum_final }} out of {{ groups['redis_sentinel'] | default([]) | length }} sentinels" + run_once: true + +- name: Check if sentinel is running + ansible.builtin.systemd: + name: redis-sentinel + register: redis_sentinel_service + failed_when: false + +- name: Stop sentinel for config update + ansible.builtin.systemd: + name: redis-sentinel + state: stopped + when: redis_sentinel_service.status.ActiveState == 'active' + +- name: Deploy sentinel.conf ansible.builtin.template: src: sentinel.conf.j2 dest: "{{ redis_sentinel_conf_file }}" @@ -31,9 +85,7 @@ mode: "0640" setype: redis_conf_t backup: true - when: - - groups['redis_secondary'] is defined - - inventory_hostname in groups['redis_secondary'] + notify: Enable and Start Redis Sentinel - name: Deploy logrotate config for Redis Sentinel ansible.builtin.template: diff --git a/roles/redis/tasks/install-common.yml b/roles/redis/tasks/install-common.yml index 38ef3b64..61ab99d6 100644 --- a/roles/redis/tasks/install-common.yml +++ b/roles/redis/tasks/install-common.yml @@ -11,35 +11,6 @@ name: vm.overcommit_memory value: 1 -# Check if firewalld is running, if it is then open the appropriate ports -- name: Gather service facts - ansible.builtin.service_facts: - -- name: Open Redis port on FirewallD Public Zone - ansible.posix.firewalld: - port: "{{ redis_port }}/tcp" - permanent: true - state: enabled - zone: public - immediate: true - when: - - ansible_facts.services["firewalld.service"] is defined - - ansible_facts.services["firewalld.service"].state == "running" - - ansible_facts.services["firewalld.service"].status == "enabled" - -- name: Open Sentinel port on FirewallD Public Zone - ansible.posix.firewalld: - port: "{{ redis_sentinel_port }}/tcp" - permanent: true - state: enabled - zone: public - immediate: true - when: - - ansible_facts.services["firewalld.service"] is defined - - ansible_facts.services["firewalld.service"].state == "running" - - ansible_facts.services["firewalld.service"].status == "enabled" - - redis_replication_enabled | bool - - name: Create Redis group ansible.builtin.group: name: "{{ redis_group }}" diff --git a/roles/redis/tasks/main.yml b/roles/redis/tasks/main.yml index f3e1d5af..83c1b5d5 100644 --- a/roles/redis/tasks/main.yml +++ b/roles/redis/tasks/main.yml @@ -1,10 +1,21 @@ # 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 tasks to validate variables - ansible.builtin.include_tasks: - file: validate-vars.yml +- name: Validate variables and set node type facts tags: always + block: + - name: Include tasks to validate variables + ansible.builtin.include_tasks: + file: validate-vars.yml + + - name: Set Redis node type facts + ansible.builtin.set_fact: + redis_is_master_node: "{{ 'redis_master' in group_names }}" + redis_is_replica_node: "{{ 'redis_replica' in group_names }}" + redis_is_sentinel_node: "{{ 'redis_sentinel' in group_names }}" + redis_is_data_node: "{{ 'redis_master' in group_names or 'redis_replica' in group_names }}" + redis_has_replicas: "{{ groups['redis_replica'] is defined and groups['redis_replica'] | length > 0 }}" + redis_has_sentinels: "{{ groups['redis_sentinel'] is defined and groups['redis_sentinel'] | length > 0 }}" - name: Install and configure Redis notify: @@ -45,14 +56,16 @@ - name: Include tasks to configure Redis ansible.builtin.include_tasks: file: configure-redis.yml + when: redis_is_data_node - name: Include tasks to configure Sentinel ansible.builtin.include_tasks: file: configure-sentinel.yml - when: redis_replication_enabled | bool + when: redis_is_sentinel_node - name: Ensure Redis is running tags: always + when: redis_is_data_node block: - name: Flush all handlers ansible.builtin.meta: flush_handlers @@ -70,7 +83,7 @@ - name: Ensure Redis Sentinel is running tags: always - when: redis_replication_enabled | bool + when: redis_is_sentinel_node block: - name: Flush all handlers ansible.builtin.meta: flush_handlers diff --git a/roles/redis/templates/redis.conf.j2 b/roles/redis/templates/redis.conf.j2 index 6d807084..b6bbe958 100644 --- a/roles/redis/templates/redis.conf.j2 +++ b/roles/redis/templates/redis.conf.j2 @@ -72,16 +72,7 @@ # IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES # JUST COMMENT OUT THE FOLLOWING LINE. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{% set bind_addrs = redis_bind_addrs.split(' ') %} -{% if redis_bind_ipv6 | bool %} - {{ bind_addrs.insert(0, '::1') }} -{% endif %} -{% if redis_bind_addr is defined and redis_bind_addr == "default_ipv4_address" %} - {{ bind_addrs.insert(0, hostvars[inventory_hostname]['ansible_default_ipv4']['address']) }} -{% else %} - {{ bind_addrs.insert(0, inventory_hostname) }} -{% endif %} -bind {{ bind_addrs | join(' ') }} +bind {{ redis_bind }} # Protected mode is a layer of security protection, in order to avoid that # Redis instances left open on the internet are accessed and exploited. @@ -489,12 +480,8 @@ dir {{ redis_data_dir }} # and resynchronize with them. # # replicaof -{% if redis_replication_enabled %} -{% if groups['redis'] is defined and inventory_hostname in groups['redis'] and inventory_hostname not in groups['redis'][0] %} -replicaof {{ hostvars[groups['redis'][0]].inventory_hostname }} {{ redis_port }} -{% elif groups['redis_secondary'] is defined and inventory_hostname in groups['redis_secondary'] and inventory_hostname not in groups['redis_secondary'][0] %} -replicaof {{ hostvars[groups['redis_secondary'][0]].inventory_hostname }} {{ redis_port }} -{% endif %} +{% if redis_is_replica_node %} +replicaof {{ redis_replicaof }} {% endif %} # If the master is password protected (using the "requirepass" configuration @@ -514,7 +501,7 @@ replicaof {{ hostvars[groups['redis_secondary'][0]].inventory_hostname }} {{ red # # When masteruser is specified, the replica will authenticate against its # master using the new AUTH form: AUTH . -{% if redis_auth_enabled and redis_replication_enabled %} +{% if redis_is_data_node and redis_auth_enabled %} masterauth {{ redis_user_repluser_password }} masteruser repluser {% endif %} @@ -728,7 +715,7 @@ replica-priority 100 # # By default min-replicas-to-write is set to 0 (feature disabled) and # min-replicas-max-lag is set to 10. -{% if redis_replication_enabled | bool %} +{% if redis_is_master_node %} min-replicas-to-write 1 min-replicas-max-lag 10 {% endif %} @@ -759,12 +746,12 @@ min-replicas-max-lag 10 # There is no need to use both the options if you need to override just # the port or the IP address. # -{% if redis_replication_enabled | bool %} +{% if redis_is_data_node %} replica-announce-ip {{ inventory_hostname }} replica-announce-port {{ redis_port }} {% endif %} # replica-announce-ip 5.5.5.5 -#replica-announce-port 1234 +# replica-announce-port 1234 ############################### KEYS TRACKING ################################# @@ -908,11 +895,17 @@ replica-announce-port {{ redis_port }} user default off user admin on allkeys allchannels allcommands >{{ redis_user_admin_password }} user itential on ~* &* -@all +@read +@write +@stream +@transaction +@sortedset +@list +@hash +@string +@fast +@scripting +@connection +@pubsub +script|load +script|exists -script|flush -flushall -flushdb -save -bgsave -bgrewriteaof -replicaof -psync -replconf -shutdown -failover -cluster -asking -sync -readonly -readwrite +info +role >{{ redis_user_itential_password }} +{% if redis_prometheus_user_enabled %} user prometheus on -@all +@connection +memory -readonly +strlen +config|get +xinfo +pfcount +zcard +type +xlen -readwrite -command +client -wait +scard +llen +hlen +get +eval +slowlog +cluster|info -hello -echo +info +latency +scan -reset -auth -asking >{{ redis_user_prometheus_password }} -{% if redis_replication_enabled %} +{% endif %} +{% if redis_is_data_node %} +{% if redis_has_replicas %} user repluser on allchannels +psync +replconf +ping >{{ redis_user_repluser_password }} +{% endif %} +{% if redis_has_sentinels %} user sentineluser on &* -@all +slaveof +ping +info +role +publish +subscribe +psubscribe +punsubscribe +client|setname +client|kill +multi +exec +replicaof +script|kill +config|rewrite >{{ redis_user_sentineluser_password }} -{% endif %} +{% endif %} +{% endif %} {% endif %} # ACL LOG diff --git a/roles/redis/templates/sentinel.conf.j2 b/roles/redis/templates/sentinel.conf.j2 index a40fd02d..22d97cb6 100644 --- a/roles/redis/templates/sentinel.conf.j2 +++ b/roles/redis/templates/sentinel.conf.j2 @@ -12,7 +12,7 @@ # # For example you may use one of the following: # -bind {{ inventory_hostname }} +bind {{ redis_sentinel_bind }} # # protected-mode no @@ -62,7 +62,7 @@ sentinel announce-port {{ redis_sentinel_port }} # For Redis Sentinel to chdir to /tmp at startup is the simplest thing # for the process to don't interfere with administrative tasks such as # unmounting filesystems. -dir /tmp +dir {{ redis_data_dir }}/sentinel # sentinel monitor # @@ -81,8 +81,7 @@ dir /tmp # # Note: master name should not include special characters or spaces. # The valid charset is A-z 0-9 and the three characters ".-_". -sentinel monitor {{ redis_master_name }} {{ redis_master_host }} {{ redis_port }} 2 - +sentinel monitor {{ redis_sentinel_master_name }} {{ groups['redis_master'][0] }} {{ redis_port }} {{ redis_sentinel_quorum_final }} # sentinel auth-pass # @@ -103,7 +102,7 @@ sentinel monitor {{ redis_master_name }} {{ redis_master_host }} {{ redis_port } # # sentinel auth-pass mymaster MySUPER--secret-0123passw0rd {% if redis_auth_enabled %} -sentinel auth-pass {{ redis_master_name }} {{ redis_user_sentineluser_password }} +sentinel auth-pass {{ redis_sentinel_master_name }} {{ redis_user_sentineluser_password }} {% endif %} # sentinel auth-user @@ -118,7 +117,7 @@ sentinel auth-pass {{ redis_master_name }} {{ redis_user_sentineluser_password } # user sentinel-user >somepassword +client +subscribe +publish \ # +ping +info +multi +slaveof +config +client +exec on {% if redis_auth_enabled %} -sentinel auth-user {{ redis_master_name }} sentineluser +sentinel auth-user {{ redis_sentinel_master_name }} sentineluser user default off user admin on >{{ redis_user_sentineladmin_password }} ~* &* +@all @@ -133,7 +132,7 @@ user sentineluser on >{{ redis_user_sentineluser_password }} &* -@all +auth +cli # Down). # # Default is 30 seconds. -sentinel down-after-milliseconds {{ redis_master_name }} 5000 +sentinel down-after-milliseconds {{ redis_sentinel_master_name }} 5000 # IMPORTANT NOTE: starting with Redis 6.2 ACL capability is supported for # Sentinel mode, please refer to the Redis website https://redis.io/topics/acl @@ -212,7 +211,7 @@ sentinel sentinel-pass {{ redis_user_sentineluser_password }} # during the failover. Use a low number if you use the replicas to serve query # to avoid that all the replicas will be unreachable at about the same # time while performing the synchronization with the master. -sentinel parallel-syncs {{ redis_master_name }} 1 +sentinel parallel-syncs {{ redis_sentinel_master_name }} 1 # sentinel failover-timeout # @@ -237,7 +236,7 @@ sentinel parallel-syncs {{ redis_master_name }} 1 # the exact parallel-syncs progression as specified. # # Default is 3 minutes. -sentinel failover-timeout {{ redis_master_name }} 60000 +sentinel failover-timeout {{ redis_sentinel_master_name }} 60000 # SCRIPTS EXECUTION # From cd3f8e531dda4883984c32e5424dd99bc7f02332 Mon Sep 17 00:00:00 2001 From: kvelarde-itential <87794456+kvelarde-itential@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:03:43 -0700 Subject: [PATCH 15/20] Fix issue with redis min replicas (#295) --- roles/redis/templates/redis.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/redis/templates/redis.conf.j2 b/roles/redis/templates/redis.conf.j2 index b6bbe958..fd6163c9 100644 --- a/roles/redis/templates/redis.conf.j2 +++ b/roles/redis/templates/redis.conf.j2 @@ -715,7 +715,7 @@ replica-priority 100 # # By default min-replicas-to-write is set to 0 (feature disabled) and # min-replicas-max-lag is set to 10. -{% if redis_is_master_node %} +{% if redis_is_master_node and redis_has_replicas %} min-replicas-to-write 1 min-replicas-max-lag 10 {% endif %} From 7d367f8cfac2d3118865b94215efd4b82981ced6 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Fri, 6 Feb 2026 11:03:44 -0500 Subject: [PATCH 16/20] separate redis and sentinel capabilities --- playbooks/certify_redis.yml | 16 +- playbooks/verify_redis.yml | 16 +- roles/redis/defaults/main/sentinel.yml | 4 + roles/redis/tasks/certify-redis.yml | 326 +---------------------- roles/redis/tasks/certify-sentinel.yml | 349 +++++++++++++++++++++++++ roles/redis/tasks/verify-sentinel.yml | 137 ++++++++++ 6 files changed, 531 insertions(+), 317 deletions(-) create mode 100644 roles/redis/tasks/certify-sentinel.yml create mode 100644 roles/redis/tasks/verify-sentinel.yml diff --git a/playbooks/certify_redis.yml b/playbooks/certify_redis.yml index 51ed8d8c..65f1d770 100644 --- a/playbooks/certify_redis.yml +++ b/playbooks/certify_redis.yml @@ -2,12 +2,26 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- +# run this only for redis_master and redis_replica device group - name: Run Redis Certification Tasks - hosts: redis* + hosts: redis_master:redis_replica gather_facts: true become: true + # environment: + # PATH: "/usr/local/bin:{{ ansible_env.PATH }}" tasks: - name: Certify Redis Installation # noqa run-once ansible.builtin.import_role: name: itential.deployer.redis tasks_from: certify-redis + +# run this only for redis_sentinel device group +- name: Run Sentinel Certification Tasks + hosts: redis_sentinel + gather_facts: true + become: true + tasks: + - name: Certify Sentinel Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.redis + tasks_from: certify-sentinel diff --git a/playbooks/verify_redis.yml b/playbooks/verify_redis.yml index b2bcb22a..a5ba187c 100644 --- a/playbooks/verify_redis.yml +++ b/playbooks/verify_redis.yml @@ -2,8 +2,9 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) --- +# run this only for redis_master and redis_replica device group - name: Run Redis Verification Tasks - hosts: redis* + hosts: redis_master:redis_replica gather_facts: true become: true tasks: @@ -11,3 +12,16 @@ ansible.builtin.import_role: name: itential.deployer.redis tasks_from: verify-redis + tags: verify_redis + +# run this only for redis_sentinel device group +- name: Run Sentinel Verification Tasks + hosts: redis_sentinel + gather_facts: true + become: true + tasks: + - name: Verify Sentinel Installation # noqa run-once + ansible.builtin.import_role: + name: itential.deployer.redis + tasks_from: verify-sentinel + tags: verify_sentinel diff --git a/roles/redis/defaults/main/sentinel.yml b/roles/redis/defaults/main/sentinel.yml index aaf87a05..eca64d30 100644 --- a/roles/redis/defaults/main/sentinel.yml +++ b/roles/redis/defaults/main/sentinel.yml @@ -19,3 +19,7 @@ redis_sentinel_bind: 127.0.0.1 {{ ansible_default_ipv4.address }} # Auto-calculate quorum based on sentinel count (recommended) # Set to explicit number to override (must be <= number of sentinels) redis_sentinel_quorum: auto + +# Default location for the certification report files +redis_sentinel_certify_report_dir_remote: /var/tmp/itential-reports/sentinel +redis_sentinel_certify_report_dir_local: /tmp/itential-reports/sentinel diff --git a/roles/redis/tasks/certify-redis.yml b/roles/redis/tasks/certify-redis.yml index 2bf38af6..85734e1f 100644 --- a/roles/redis/tasks/certify-redis.yml +++ b/roles/redis/tasks/certify-redis.yml @@ -40,7 +40,7 @@ - name: Test Redis connectivity ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user admin \ -p {{ redis_port }} \ -a "{{ redis_user_admin_password }}" \ @@ -51,14 +51,16 @@ changed_when: false - name: Get Redis version - ansible.builtin.shell: set -o pipefail && redis-server --version + ansible.builtin.shell: | + set -o pipefail && \ + {{ redis_bin_dir }}/redis-server --version register: redis_version changed_when: false - name: Get Redis INFO ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user admin \ -p {{ redis_port }} \ -a "{{ redis_user_admin_password }}" \ @@ -72,7 +74,7 @@ - name: Get Redis configuration ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --json \ --user admin \ -p {{ redis_port }} \ @@ -141,7 +143,7 @@ - name: Get list of configured Redis users ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --json \ --user admin \ -p {{ redis_port }} \ @@ -173,7 +175,7 @@ - name: Confirm "itential" user login ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user itential \ -p {{ redis_port }} \ -a "{{ redis_user_itential_password }}" \ @@ -186,7 +188,7 @@ - name: Confirm "repluser" user login ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user repluser \ -p {{ redis_port }} \ -a "{{ redis_user_repluser_password }}" \ @@ -199,7 +201,7 @@ - name: Confirm "sentineluser" user login ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user sentineluser \ -p {{ redis_port }} \ -a "{{ redis_user_sentineluser_password }}" \ @@ -212,7 +214,7 @@ - name: Confirm "prometheus" user login ansible.builtin.shell: | set -o pipefail && - redis-cli \ + {{ redis_bin_dir }}/redis-cli \ --user prometheus \ -p {{ redis_port }} \ -a "{{ redis_user_prometheus_password }}" \ @@ -222,312 +224,6 @@ 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] }}" - when: sentinel_is_running | bool - -- 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 - 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: | - 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) }}" - when: sentinel_is_running | bool - -- 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) }}" - when: sentinel_is_running | bool - -- 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) }}" - when: sentinel_is_running | bool - -- 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 and copy to control node # ========================================================================= diff --git a/roles/redis/tasks/certify-sentinel.yml b/roles/redis/tasks/certify-sentinel.yml new file mode 100644 index 00000000..9fed55a7 --- /dev/null +++ b/roles/redis/tasks/certify-sentinel.yml @@ -0,0 +1,349 @@ +# 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_sentinel_certify_report_dir_remote }}" + state: directory + owner: "{{ redis_owner }}" + group: "{{ redis_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + redis_sentinel_certify_report_file: "{{ redis_sentinel_certify_report_dir_remote }}/sentinel-report-{{ inventory_hostname }}.md" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_details + +- 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_bin_dir }}/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 }}" + +- 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_bin_dir }}/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_bin_dir }}/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] }}" + when: sentinel_is_running | bool + +- name: Get details for each monitored master + ansible.builtin.shell: | + set -o pipefail && + {{ redis_bin_dir }}/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 + 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: | + set -o pipefail && + {{ redis_bin_dir }}/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) }}" + when: sentinel_is_running | bool + +- name: Get known replicas for each master + ansible.builtin.shell: | + set -o pipefail && + {{ redis_bin_dir }}/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) }}" + when: sentinel_is_running | bool + +- name: Check master status + ansible.builtin.shell: | + set -o pipefail && + {{ redis_bin_dir }}/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) }}" + when: sentinel_is_running | bool + +- name: Get Sentinel configuration + ansible.builtin.shell: | + set -o pipefail && + {{ redis_bin_dir }}/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_bin_dir }}/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_bin_dir }}/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 and copy to control node +# ========================================================================= +- name: Generate validation report + ansible.builtin.template: + backup: true + dest: "{{ redis_sentinel_certify_report_file }}" + group: "{{ redis_group }}" + mode: "0665" + owner: "{{ redis_owner }}" + src: redis-validation-report.md.j2 + when: + - sentinel_is_running | bool + +- name: Copy validation report to the control node + ansible.builtin.fetch: + dest: "{{ redis_certify_report_dir_local }}/" + fail_on_missing: false + flat: true + src: "{{ redis_sentinel_certify_report_file }}" + when: + - sentinel_is_running | bool + +- 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_sentinel_certify_report_file }} on both the remote and control nodes." + when: + - sentinel_is_running | bool diff --git a/roles/redis/tasks/verify-sentinel.yml b/roles/redis/tasks/verify-sentinel.yml new file mode 100644 index 00000000..41ef7d95 --- /dev/null +++ b/roles/redis/tasks/verify-sentinel.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 Sentinel 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 f67dc91d2c5d991f10ed8e526fb1fb19d24582fe Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 9 Feb 2026 17:34:25 -0500 Subject: [PATCH 17/20] remove unnecessary tags --- playbooks/certify_mongodb.yml | 1 - playbooks/certify_platform.yml | 1 - playbooks/certify_redis.yml | 2 -- 3 files changed, 4 deletions(-) diff --git a/playbooks/certify_mongodb.yml b/playbooks/certify_mongodb.yml index 04c3fb31..c167cf8d 100644 --- a/playbooks/certify_mongodb.yml +++ b/playbooks/certify_mongodb.yml @@ -11,4 +11,3 @@ 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 index e6c30b31..378fb3a6 100644 --- a/playbooks/certify_platform.yml +++ b/playbooks/certify_platform.yml @@ -11,4 +11,3 @@ 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 index 65f1d770..176844b1 100644 --- a/playbooks/certify_redis.yml +++ b/playbooks/certify_redis.yml @@ -7,8 +7,6 @@ hosts: redis_master:redis_replica gather_facts: true become: true - # environment: - # PATH: "/usr/local/bin:{{ ansible_env.PATH }}" tasks: - name: Certify Redis Installation # noqa run-once ansible.builtin.import_role: From c2fab93a7d8232a24ed44f6a01f942aaa9da09c7 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 12 Feb 2026 20:43:20 -0500 Subject: [PATCH 18/20] Consolidate common code --- .../tasks/verify-component-hardware.yml | 153 ++++++++++++++++++ roles/mongodb/tasks/verify-mongodb.yml | 141 +--------------- roles/platform/tasks/verify-platform.yml | 141 +--------------- roles/redis/tasks/verify-redis.yml | 141 +--------------- 4 files changed, 177 insertions(+), 399 deletions(-) create mode 100644 roles/common/tasks/verify-component-hardware.yml diff --git a/roles/common/tasks/verify-component-hardware.yml b/roles/common/tasks/verify-component-hardware.yml new file mode 100644 index 00000000..a7692c97 --- /dev/null +++ b/roles/common/tasks/verify-component-hardware.yml @@ -0,0 +1,153 @@ +# Copyright (c) 2026, Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +# Shared hardware verification template for Redis, MongoDB, and Platform components +# +# Required variables: +# - component_name: Display name (e.g., "Redis", "MongoDB", "Platform") +# - hw_specs_var_name: Variable name for hardware specs (e.g., "redis_hw_specs") +# - invalid_platform_release_var: Flag variable name for undefined platform (e.g., "redis_invalid_platform_release") + +- name: Validate that the platform_release variable is set + ansible.builtin.assert: + that: platform_release is defined + fail_msg: "platform_release must be defined" + +- name: Validate that the env variable is set + ansible.builtin.assert: + that: env is defined + fail_msg: "env must be defined" + +- name: Announce Intention + ansible.builtin.debug: + msg: "Validating {{ env }} host {{ inventory_hostname }} for {{ component_name }} 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: "{{ lookup('vars', hw_specs_var_name)[env].cpu_count if lookup('vars', hw_specs_var_name) != 'none' else 'N/A' }}" + ram_size_gb: "{{ lookup('vars', hw_specs_var_name)[env].ram_size if lookup('vars', hw_specs_var_name) != 'none' else 'N/A' }}" + disk_size_gb: "{{ lookup('vars', hw_specs_var_name)[env].disk_size if lookup('vars', hw_specs_var_name) != '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 >= lookup('vars', hw_specs_var_name)[env].cpu_count) }}" + ram_valid: "{{ (env == 'none') or ((ansible_memtotal_mb / 1024) >= lookup('vars', hw_specs_var_name)[env].ram_size) }}" + disk_valid: "{{ (env == 'none') or ((root_disk_size_gb | default(0) | float) >= lookup('vars', hw_specs_var_name)[env].disk_size) }}" + all_valid: "{{ (env == 'none') or ((ansible_processor_vcpus >= lookup('vars', hw_specs_var_name)[env].cpu_count) and ((ansible_memtotal_mb / 1024) >= lookup('vars', hw_specs_var_name)[env].ram_size) and ((root_disk_size_gb | default(0) | float) >= lookup('vars', hw_specs_var_name)[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! ✓" diff --git a/roles/mongodb/tasks/verify-mongodb.yml b/roles/mongodb/tasks/verify-mongodb.yml index 1224a3e0..2464deb0 100644 --- a/roles/mongodb/tasks/verify-mongodb.yml +++ b/roles/mongodb/tasks/verify-mongodb.yml @@ -2,136 +2,11 @@ # 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! ✓" +- name: Verify MongoDB Hardware Requirements + ansible.builtin.import_tasks: ../../common/tasks/verify-component-hardware.yml + vars: + component_name: "MongoDB" + hw_specs_var_name: "mongodb_hw_specs" + invalid_platform_release_var: "mongodb_invalid_platform_release" + +# Any mongo specific verifications go here... diff --git a/roles/platform/tasks/verify-platform.yml b/roles/platform/tasks/verify-platform.yml index 5398855d..b862757b 100644 --- a/roles/platform/tasks/verify-platform.yml +++ b/roles/platform/tasks/verify-platform.yml @@ -2,136 +2,11 @@ # 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! ✓" +- name: Verify Platform Hardware Requirements + ansible.builtin.import_tasks: ../../common/tasks/verify-component-hardware.yml + vars: + component_name: "Platform" + hw_specs_var_name: "platform_hw_specs" + invalid_platform_release_var: "platform_invalid_release" + +# Any platform specific verifications go here... diff --git a/roles/redis/tasks/verify-redis.yml b/roles/redis/tasks/verify-redis.yml index 97ec36fb..f66d79d9 100644 --- a/roles/redis/tasks/verify-redis.yml +++ b/roles/redis/tasks/verify-redis.yml @@ -2,136 +2,11 @@ # 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! ✓" +- name: Verify Redis Hardware Requirements + ansible.builtin.import_tasks: ../../common/tasks/verify-component-hardware.yml + vars: + component_name: "Redis" + hw_specs_var_name: "redis_hw_specs" + invalid_platform_release_var: "redis_invalid_platform_release" + +# Any mongo specific verifications go here... From 139ae0638f2a575e3e968d22a569b2ff41197506 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Mon, 2 Feb 2026 12:42:03 -0500 Subject: [PATCH 19/20] modified certify playbook to limit execution to groups --- roles/platform/tasks/certify-platform.yml | 14 ++++++++++++++ roles/redis/defaults/main/redis.yml | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/roles/platform/tasks/certify-platform.yml b/roles/platform/tasks/certify-platform.yml index 7e6749a4..40c1c635 100644 --- a/roles/platform/tasks/certify-platform.yml +++ b/roles/platform/tasks/certify-platform.yml @@ -4,7 +4,11 @@ - name: Ensure report directory exists ansible.builtin.file: +<<<<<<< HEAD path: "{{ platform_certify_report_dir_remote }}" +======= + path: "{{ platform_certify_report_dir }}" +>>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) state: directory owner: "{{ platform_user }}" group: "{{ platform_group }}" @@ -12,7 +16,11 @@ - name: Set report filename ansible.builtin.set_fact: +<<<<<<< HEAD platform_certify_report_file: "{{ platform_certify_report_dir_remote }}/platform-report-{{ inventory_hostname }}.md" +======= + platform_certify_report_file: "{{ platform_certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" +>>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) - name: Check if Itential service exists ansible.builtin.systemd: @@ -556,11 +564,17 @@ group: "{{ platform_group }}" - name: Copy validation report to the control node +<<<<<<< HEAD ansible.builtin.fetch: dest: "{{ platform_certify_report_dir_local }}/" fail_on_missing: false flat: true src: "{{ platform_certify_report_file }}" +======= + itential.deployer.fetch_to_control: + src: "{{ platform_certify_report_file }}" + dest: "{{ platform_certify_report_file }}" +>>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) - name: Display report summary ansible.builtin.debug: diff --git a/roles/redis/defaults/main/redis.yml b/roles/redis/defaults/main/redis.yml index 0920490b..6bfd1454 100644 --- a/roles/redis/defaults/main/redis.yml +++ b/roles/redis/defaults/main/redis.yml @@ -47,9 +47,10 @@ 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_remote: /var/tmp/itential-reports/redis -redis_certify_report_dir_local: /tmp/itential-reports/redis # By default the prometheus user will be enabled. If this is set to false, the prometheus user # will not be created. redis_prometheus_user_enabled: true + +# Default location for the certification report files +redis_certify_report_dir_remote: /var/tmp/itential-reports/redis +redis_certify_report_dir_local: /tmp/itential-reports/redis From ffe8f3993700b406fa2fbc33d6a9951446f58436 Mon Sep 17 00:00:00 2001 From: Steven Schattenberg Date: Thu, 5 Feb 2026 13:40:53 -0500 Subject: [PATCH 20/20] Code review changes --- roles/platform/tasks/certify-platform.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/roles/platform/tasks/certify-platform.yml b/roles/platform/tasks/certify-platform.yml index 40c1c635..7e6749a4 100644 --- a/roles/platform/tasks/certify-platform.yml +++ b/roles/platform/tasks/certify-platform.yml @@ -4,11 +4,7 @@ - name: Ensure report directory exists ansible.builtin.file: -<<<<<<< HEAD path: "{{ platform_certify_report_dir_remote }}" -======= - path: "{{ platform_certify_report_dir }}" ->>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) state: directory owner: "{{ platform_user }}" group: "{{ platform_group }}" @@ -16,11 +12,7 @@ - name: Set report filename ansible.builtin.set_fact: -<<<<<<< HEAD platform_certify_report_file: "{{ platform_certify_report_dir_remote }}/platform-report-{{ inventory_hostname }}.md" -======= - platform_certify_report_file: "{{ platform_certify_report_dir }}/platform-report-{{ inventory_hostname }}.md" ->>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) - name: Check if Itential service exists ansible.builtin.systemd: @@ -564,17 +556,11 @@ group: "{{ platform_group }}" - name: Copy validation report to the control node -<<<<<<< HEAD ansible.builtin.fetch: dest: "{{ platform_certify_report_dir_local }}/" fail_on_missing: false flat: true src: "{{ platform_certify_report_file }}" -======= - itential.deployer.fetch_to_control: - src: "{{ platform_certify_report_file }}" - dest: "{{ platform_certify_report_file }}" ->>>>>>> 9d2c108 (modified certify playbook to limit execution to groups) - name: Display report summary ansible.builtin.debug: