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/certify.yml b/playbooks/certify.yml new file mode 100644 index 00000000..2cb4b00b --- /dev/null +++ b/playbooks/certify.yml @@ -0,0 +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: Certify Redis Installation + import_playbook: itential.deployer.certify_redis + +- name: Certify MongoDB Installation + import_playbook: itential.deployer.certify_mongodb + +- 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..04c3fb31 --- /dev/null +++ b/playbooks/certify_mongodb.yml @@ -0,0 +1,14 @@ +# 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 + 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..e6c30b31 --- /dev/null +++ b/playbooks/certify_platform.yml @@ -0,0 +1,14 @@ +# 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 + 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.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/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/playbooks/verify.yml b/playbooks/verify.yml new file mode 100644 index 00000000..ab15cb1f --- /dev/null +++ b/playbooks/verify.yml @@ -0,0 +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: Verify Redis Installation + import_playbook: itential.deployer.verify_redis + +- name: Verify MongoDB Installation + import_playbook: itential.deployer.verify_mongodb + +- 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/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/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 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..624347ba 100644 --- a/roles/mongodb/defaults/main/mongodb.yml +++ b/roles/mongodb/defaults/main/mongodb.yml @@ -71,3 +71,7 @@ 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_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 new file mode 100644 index 00000000..38a71378 --- /dev/null +++ b/roles/mongodb/tasks/certify-mongodb.yml @@ -0,0 +1,417 @@ +# 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: "{{ mongodb_certify_report_dir_remote }}" + state: directory + owner: "{{ mongodb_owner }}" + group: "{{ mongodb_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + mongodb_certify_report_file: "{{ mongodb_certify_report_dir_remote }}/mongodb-report-{{ inventory_hostname }}.md" + +- 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 + - 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 + - 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 + - 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 + - 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 + - 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 + - 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 + - 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 + - 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 + - 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 + - mongodb_user_admin_password is defined + +# ============================================================================ +# GATHER HOST INFORMATION +# ============================================================================ + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_details + +# ============================================================================ +# GENERATE REPORT +# ============================================================================ +- name: Generate MongoDB validation report + ansible.builtin.template: + backup: true + dest: "{{ mongodb_certify_report_file }}" + src: mongodb-validation-report.md.j2 + group: "{{ mongodb_group }}" + owner: "{{ mongodb_owner }}" + mode: '0644' + +- name: Copy validation report to the control node + ansible.builtin.fetch: + dest: "{{ mongodb_certify_report_dir_local }}/" + fail_on_missing: false + flat: true + src: "{{ mongodb_certify_report_file }}" + +- name: Display report summary + ansible.builtin.debug: + 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/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/mongodb/templates/mongodb-validation-report.md.j2 b/roles/mongodb/templates/mongodb-validation-report.md.j2 new file mode 100644 index 00000000..e117fa2b --- /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/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/defaults/main/platform.yml b/roles/platform/defaults/main/platform.yml index 4ad71e11..7afe5f4e 100644 --- a/roles/platform/defaults/main/platform.yml +++ b/roles/platform/defaults/main/platform.yml @@ -52,3 +52,7 @@ 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_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 new file mode 100644 index 00000000..7e6749a4 --- /dev/null +++ b/roles/platform/tasks/certify-platform.yml @@ -0,0 +1,570 @@ +# 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: "{{ platform_certify_report_dir_remote }}" + state: directory + owner: "{{ platform_user }}" + group: "{{ platform_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + platform_certify_report_file: "{{ platform_certify_report_dir_remote }}/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 + - 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: "{{ platform_certify_report_file }}" + mode: '0644' + owner: "{{ platform_user }}" + group: "{{ platform_group }}" + +- name: Copy validation report to the control node + ansible.builtin.fetch: + dest: "{{ platform_certify_report_dir_local }}/" + fail_on_missing: false + flat: true + src: "{{ platform_certify_report_file }}" + +- name: Display report summary + ansible.builtin.debug: + 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/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/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/redis.yml b/roles/redis/defaults/main/redis.yml index afde9d4d..1ebab4e9 100644 --- a/roles/redis/defaults/main/redis.yml +++ b/roles/redis/defaults/main/redis.yml @@ -61,3 +61,7 @@ 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_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 new file mode 100644 index 00000000..2bf38af6 --- /dev/null +++ b/roles/redis/tasks/certify-redis.yml @@ -0,0 +1,555 @@ +# 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_certify_report_dir_remote }}" + state: directory + owner: "{{ redis_owner }}" + group: "{{ redis_group }}" + mode: "0755" + +- name: Set report filename + ansible.builtin.set_fact: + redis_certify_report_file: "{{ redis_certify_report_dir_remote }}/redis-report-{{ inventory_hostname }}.md" + +- name: Gather host information + itential.deployer.gather_host_information: + register: host_details + +- 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 + - 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 + - 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] }}" + 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 +# ========================================================================= +- name: Generate validation report + ansible.builtin.template: + backup: true + 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 + ansible.builtin.fetch: + dest: "{{ redis_certify_report_dir_local }}/" + fail_on_missing: false + flat: true + src: "{{ 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: {{ redis_certify_report_file }} on both the remote and control nodes." 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! ✓" diff --git a/roles/redis/templates/redis-validation-report.md.j2 b/roles/redis/templates/redis-validation-report.md.j2 new file mode 100644 index 00000000..665cce92 --- /dev/null +++ b/roles/redis/templates/redis-validation-report.md.j2 @@ -0,0 +1,355 @@ +# 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('') }} + +--- + +## 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 %} +- **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 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