From 8109bed9799f40348386c7e5904f172a264bb9dd Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Thu, 27 Nov 2025 10:57:19 +0100 Subject: [PATCH 1/4] refactor: Add Molecule testing for Stream deployment - Introduced Molecule scenarios for preparing and verifying Stream nodes and MongoDB. - Created `prepare.yml` to ensure system readiness before deployment. - Added `verify.yml` to validate installation and configuration of Stream and MongoDB. - Updated `requirements.yml` to include necessary Ansible collections. - Enhanced `config.yml` with checks for Stream installation and license file handling. - Implemented firewall configuration in `firewall.yml` to manage service access. - Refactored tasks to improve clarity and maintainability, including dynamic inventory script for cluster deployment. - Added uninstall playbook to cleanly remove Stream and its dependencies. --- .gitignore | 4 + README.md | 354 +++++++++++++++++++++++++- defaults/main/default_values.yml | 60 +++-- defaults/main/mandatory_vars.yml | 80 ++++-- handlers/main.yml | 19 +- meta/main.yml | 40 +-- molecule/default/converge.yml | 8 + molecule/default/create.yml | 26 ++ molecule/default/destroy.yml | 31 +++ molecule/default/molecule.yml | 128 ++++++++++ molecule/default/prepare.yml | 17 ++ molecule/default/requirements.yml | 5 + molecule/default/verify.yml | 176 +++++++++++++ tasks/config.yml | 145 +++++++++-- tasks/enterprise_linux_OS_install.yml | 69 ++++- tasks/firewall.yml | 54 ++++ tasks/firewall_config.yml | 47 ---- tasks/launching.yml | 2 +- tasks/main.yml | 32 ++- tasks/nginx_certs.yml | 43 ++-- templates/etc_default_stream.j2 | 20 +- templates/etc_hosts.j2 | 6 - templates/hosts_allowed.j2 | 23 +- tests/deploy.yml | 16 ++ tests/inventory | 2 - tests/inventory.py | 113 ++++++++ tests/test.yml | 5 - tests/uninstall.yml | 69 +++++ vars/main.yml | 15 +- 29 files changed, 1377 insertions(+), 232 deletions(-) create mode 100644 .gitignore create mode 100644 molecule/default/converge.yml create mode 100644 molecule/default/create.yml create mode 100644 molecule/default/destroy.yml create mode 100644 molecule/default/molecule.yml create mode 100644 molecule/default/prepare.yml create mode 100644 molecule/default/requirements.yml create mode 100644 molecule/default/verify.yml create mode 100644 tasks/firewall.yml delete mode 100644 tasks/firewall_config.yml delete mode 100644 templates/etc_hosts.j2 create mode 100644 tests/deploy.yml delete mode 100644 tests/inventory create mode 100755 tests/inventory.py delete mode 100644 tests/test.yml create mode 100644 tests/uninstall.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9af97db --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Licence files - never commit these! +*.lic +!horizon-test.lic +.DS_Store diff --git a/README.md b/README.md index dc957e9..426d2fe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,352 @@ -------- -This role deploys Stream in High availabilty mode. +Role Name +========= + +Requirements +------------ +To be able to use this role you have to previously provision VMs running on **CentOS/RHEL** and meeting the requirements from the official EVERTRUST documentation. +A root access to these VMs is mandatory, as per a normal RPM install of Stream. You will have to configure your Ansible playbook to use these accounts while playing the role. + +**IMPORTANT:** A running instance of MongoDB is also necessary. MongoDB should be accessible from all Stream nodes. + +### Ansible Prerequisites + +It is necessary to: + +1. **Install the ansible.posix collection:** + ```bash + ansible-galaxy collection install ansible.posix + ``` + +2. **Configure SSH access:** Set up SSH key-based authentication from your control machine to all target nodes: + ```bash + # Generate SSH key if needed + ssh-keygen -t rsa -b 4096 + + # Copy to each target node + ssh-copy-id root@stream-node1-ip + ssh-copy-id root@stream-node2-ip + ... + ``` + +3. **Dynamic Inventory:** The role includes a dynamic inventory script at `tests/inventory.py` that automatically generates the Ansible inventory from your `mandatory_vars.yml` configuration. The script sets appropriate connection parameters and maps hostnames to IP addresses, eliminating manual inventory management. + +4. **Licence File Setup** +Before deployment, you must provide a valid Stream licence file. Create the required directories and place your licence file: +```bash +# For Molecule testing +mkdir -p molecule/default/files +cp /path/to/your/stream.lic molecule/default/files/stream.lic + +# For VM deployment +mkdir -p tests/files +cp /path/to/your/stream.lic tests/files/stream.lic +``` + +Note: The files/ directories are not included in the repository and must be created before running the role. + +### Firewall Configuration + +**IMPORTANT:** This role automatically configures firewalld with the necessary ports. The following ports are opened: + +| Port/Service | Purpose | When Opened | +|--------------|---------|-------------| +| **22 (SSH)** | Remote administration | Always | +| **80** | HTTP web access | Always | +| **443** | HTTPS web access | Always | +| **7626** | Akka Management (cluster discovery) | Only for HA (2+ nodes) | +| **17355** | Akka Artery (cluster communication) | Only for HA (2+ nodes) | + +**Note:** Port 9000 (Stream application) is bound to `127.0.0.1` only and is NOT exposed externally. Nginx acts as a reverse proxy on ports 80/443. + +**Note on Akka/Pekko:** Stream internally uses Pekko (the successor to Akka), but configuration variables maintain the `AKKA_*` naming convention for backwards compatibility with older Stream versions. The functionality and port numbers remain the same. + +**For MongoDB:** If managing the MongoDB VM separately, ensure port 27017 is open only to Stream node IPs for security. + +## Role Variables + +The following table regroups the data that you have to provide the Ansible role with for the deployment and configuration to work properly. + +**IMPORTANT:** All these variables must be configured in `defaults/main/mandatory_vars.yml` with real values before deployment. + +| Key | Value Type | +|-----|------------| +| `stream_play_http_secret_key` | String (32+ characters), alphanumeric only | +| `stream_default_ssv_key` | String (32+ characters), alphanumeric only | +| `stream_event_seal_secret` | String (32+ characters), alphanumeric only | +| `stream_version` | Stream version (e.g., `2.1.5-1`) | +| `stream_pkg_uri` | URL where you store the Stream RPM | +| `stream_tinkey_version` | Tinkey version (e.g., `1.12.0.1-1`) | +| `stream_tinkey_pkg_uri` | URL where you store the Tinkey RPM | +| `stream_repository_username` | Username to authenticate to this URL | +| `stream_repository_password` | Password to authenticate to this URL | +| `stream_licence_src_path` | Path to your Stream license on your control machine | +| `stream_mongodb_uri` | MongoDB connection string with authentication | +| `stream_mongodb_shell_package_uri` | URL where you store the Mongosh RPM | +| `stream_mongodb_hostname` | Hostname of your MongoDB instance | +| `stream_mongodb_ip` | IP address of your MongoDB instance | +| `stream_nodes` | List of Stream nodes with hostname and IP (see below) | +| `stream_akka_discovery_port` | Port for Akka cluster discovery (default: 7626) | +| `stream_akka_artery_port` | Port for Akka cluster communication (default: 17355) | +| `stream_internal_api_port` | Port for internal API (default: 9000) | + +**Stream Nodes Configuration:** + +For **standalone deployment** (single node): +```yaml +stream_nodes: + - hostname: stream-node1-vm + ip: 10.10.22.1 +``` + +For **High Availability** (2+ nodes): +```yaml +stream_nodes: + - hostname: stream-node1-vm + ip: 10.10.22.1 + - hostname: stream-node2-vm + ip: 10.10.22.2 + - hostname: stream-node3-vm + ip: 10.10.22.3 +``` + +**IMPORTANT - Architecture Note:** Ensure the `stream_mongodb_shell_package_uri` matches your system architecture: +- For **x86_64** (most common): Use the x86_64 RPM URL +- For **aarch64** (ARM): Use the aarch64 RPM URL + +The default configuration uses aarch64. Change this if your systems are x86_64. + +## Overview + +The previous table is not exhaustive but the excluded variables can be left as they are for a deployment with default values (valid in most cases). Note that the more variables you customize for your environment and needs, the less configuration will be necessary afterwards. + +| Initial variables localisation | +|--------------------------------| +| `defaults/main/default_values.yml` : variables that can optionally be changed | +| `defaults/main/mandatory_vars.yml` : list of variables that must be set before using the role | + +This role is composed of the following steps played in order: + +### 1. Package Installation + +The download and install of the necessary packages to run Stream, including: +- System dependencies (wget, nginx, postfix, firewalld, epel-release) +- Stream RPM package +- Tinkey (Google Tink key management tool) +- MongoDB shell (mongosh) +- SELinux configuration if enforcing mode is detected + +### 2. Certificate Generation + +The generation of certificates for Nginx if you didn't configure the variables `stream_csr`, `stream_pem`, `stream_key`, and `stream_chain_pem`. If you did, this step copies the content of these variables to provide your web server. + +By default (`stream_generate_default_cert: true`), the role automatically generates self-signed SSL/TLS certificates for Nginx with the following specifications: + +- **Key type**: RSA 4096-bit +- **Hashing algorithm**: SHA-256 +- **Certificate location**: `/etc/nginx/ssl/` +- **Validity**: 365 days +- **Subject fields**: Configurable via variables in `default_values.yml` (country, organization, OU, CN) + +The generated files include: +- `stream.key` - Private key +- `stream.csr` - Certificate Signing Request +- `stream.pem` - Self-signed certificate +- `stream-chain.pem` - Certificate chain + +All certificate files are automatically set with proper permissions (root:nginx, 0640). + +**If you want to add your own certificate for Stream after deployment:** + +1. Copy your certificates to the Stream nodes: + - Certificate: `/etc/nginx/ssl/stream.pem` + - Private key: `/etc/nginx/ssl/stream.key` + - Certificate chain: `/etc/nginx/ssl/stream-chain.pem` + - CSR (optional): `/etc/nginx/ssl/stream.csr` + +2. Set proper permissions: +```bash + sudo chown root:nginx /etc/nginx/ssl/stream.* + sudo chmod 640 /etc/nginx/ssl/stream.* +``` +3. Test and Reload Nginx: +```bash + sudo nginx -t + sudo systemctl reload nginx +``` + +### 3. Configuration Provisioning + +The provisioning of your Stream licence and the different configuration files needed for Stream to run properly, based on their respective templates: +- Deployment of license file to `/opt/stream/etc/stream.lic` +- Updates to `/etc/hosts` with cluster node entries +- Generation of `/etc/default/stream` with JVM, Play, MongoDB, and Akka cluster settings +- Configuration of hosts.allowed whitelist +- Nginx symlink creation +- HTTP/HTTPS configuration files + +### 4. Tinkey Keyset Management + +**IMPORTANT:** For Stream deployments, the role automatically manages Tinkey keysets for encryption: + +For **HA deployments** (2+ nodes): +- Generates the keyset on the first node +- Distributes the same keyset to all other nodes +- Ensures all nodes share the same encryption keys + +For **standalone deployments** (single node): +- Generates a keyset locally on that node + +The keyset file is located at `/opt/stream/etc/stream.keyset` with proper permissions (stream:stream, 0640). + +### 5. Firewall Configuration + +Automatic configuration of firewalld to open necessary ports: +- SSH (22) to prevent lockout +- HTTP (80) and HTTPS (443) for web access +- Akka cluster ports (7626, 17355) for HA deployments only + +### 6. Service Management + +The start of adequate services: +- Postfix service +- Stream service +- Nginx service (with configuration test) + +### Akka Cluster Split-Brain Resolver + +**IMPORTANT:** For High Availability deployments (2+ nodes), the role automatically configures Akka's split-brain resolver using a MongoDB-based lease-majority strategy: + +```hocon +akka.cluster.split-brain-resolver { + active-strategy = "lease-majority" + lease-majority { + lease-implementation = "lease.mongo" + } +} +``` + +This configuration ensures proper cluster behavior during network partitions and prevents data inconsistency. It is automatically included in the `hosts_allowed.j2` template. + +### Hosts Allowed Configuration + +Additionally, a touchy and key element of the Stream configuration is the Play variable "hosts allowed" in the stream-extra.conf file. It configures the whitelist allowed to access Stream. + +The role automatically configures this whitelist to include: +- localhost +- All Stream cluster node hostnames +- Load balancer hostname (if `stream_use_load_balancer` is set to true) + +This prevents "Host not allowed" errors when accessing Stream through different hostnames. + +## Deployment Types + +### Standalone Deployment + +For a single Stream instance (no High Availability), configure only one node in `stream_nodes`. The role will: +- Only open HTTP, HTTPS, SSH, and Internal API ports +- Not open Akka cluster ports +- Configure Stream without cluster formation +- Generate keyset locally + +### High Availability Deployment + +For a clustered Stream deployment (2+ nodes), configure multiple nodes in `stream_nodes`. The role will: +- Open all required ports including Akka cluster ports +- Configure nodes to automatically discover each other +- Enable split-brain resolver for cluster stability +- Generate and distribute shared keyset from first node +- Provide redundancy and load distribution + +## Example Playbook + +Here is a basic way of using this role: + +```yaml +- name: Deploy Stream in HA + hosts: stream_cluster + become: true + gather_facts: true + roles: + - role: stream +``` + +### Using the Provided Test Playbook + +```bash +# 1. Configure your variables in defaults/main/mandatory_vars.yml + +# 2. Syntax check +ansible-playbook -i tests/inventory.py tests/deploy.yml --syntax-check + +# 3. Test connectivity +ansible -i tests/inventory.py all -m ping + +# 4. Dry run (check mode) +ansible-playbook -i tests/inventory.py tests/deploy.yml --check + +# 5. Deploy +ansible-playbook -i tests/inventory.py tests/deploy.yml + +# 6. With verbose output +ansible-playbook -i tests/inventory.py tests/deploy.yml -vvv +``` + +## Check the Installation + +If your Ansible role didn't fail during the play, your Stream HA should be deployed. + +To test it, run: + +```bash +# Check Stream service status +sudo systemctl status stream + +# Verify Tinkey installation +ls -la /opt/evertrust/tinkey/bin/tinkey + +# Check keyset exists +ls -la /opt/stream/etc/stream.keyset + +# Verify firewall configuration +sudo firewall-cmd --list-all + +# For HA: Check Akka cluster configuration +grep "AKKA_DISCOVERY_ENDPOINTS" /etc/default/stream + +# Check Stream logs +sudo journalctl -u stream -f +``` + +### Additional Verification Steps + +```bash +# Verify nginx configuration +sudo nginx -t + +# Check nginx service status +sudo systemctl status nginx + +# Check postfix service status +sudo systemctl status postfix + +# For HA: Verify MongoDB connectivity +mongosh --host --port 27017 +``` + +## Pre-Deployment Checklist + +Before running the playbook, verify: + +- [ ] All VMs are provisioned and accessible via SSH +- [ ] SSH keys are copied to all target nodes +- [ ] MongoDB is running and accessible +- [ ] `mandatory_vars.yml` is configured with real values (no placeholders) +- [ ] MongoDB shell package URI matches your architecture (x86_64 vs aarch64) +- [ ] Stream license file path is correct +- [ ] Ansible and ansible.posix collection are installed +- [ ] For RHEL 9: System is registered with subscription manager +- [ ] Repository credentials are valid for both Stream and Tinkey RPMs + +## Author Information + +If you have difficulties to use this role or modifications recommendations, please contact EVERTRUST. diff --git a/defaults/main/default_values.yml b/defaults/main/default_values.yml index 1262b90..019ac99 100644 --- a/defaults/main/default_values.yml +++ b/defaults/main/default_values.yml @@ -1,40 +1,50 @@ --- -# defaults file for stream -stream_version: 2.5.7-1 +# Stream default version +stream_version: 2.1.5-1 +# Tinkey default version +stream_tinkey_version: 1.12.0.1-1 + +# Licence file path stream_licence_dest_path: /opt/stream/etc/stream.lic -nginx_certs_directory_path: /etc/nginx/ssl + +# SSL/TLS certificate configuration +stream_cert_directory_path: /etc/nginx/ssl stream_cert_key_size: 4096 stream_cert_key_type: RSA -stream_cert_C: FR -stream_cert_O: org -stream_cert_OU: ou -stream_cert_CN: cn -stream_hashing_algo: "sha256" -path_to_template_default_stream: etc_default_stream.j2 -path_to_template_etc_hosts: etc_hosts.j2 -path_to_template_hosts_allowed: hosts_allowed.j2 -path_to_template_http_conf: http_conf.j2 -path_to_template_https_conf: https_conf.j2 -path_to_default_stream: /etc/default/stream -path_to_etc_hosts: /etc/hosts -path_to_hosts_allowed: /opt/stream/etc/conf.d/stream-extra.conf -path_to_stream_http_conf: /opt/stream/etc/conf.d/stream-http.conf -path_to_stream_https_conf: /opt/stream/etc/conf.d/stream-https.conf - +stream_cert_country: FR +stream_cert_organization: org +stream_cert_organizational_unit: ou +stream_cert_common_name: cn +stream_cert_hashing_algo: "sha256" + +# Template names +stream_template_default_stream: etc_default_stream.j2 +stream_template_hosts_allowed: hosts_allowed.j2 +stream_template_http_conf: http_conf.j2 +stream_template_https_conf: https_conf.j2 + +# Paths to configuration files inside the VM +stream_path_to_default_stream: /etc/default/stream +stream_path_to_hosts_allowed: /opt/stream/etc/conf.d/stream-extra.conf +stream_path_to_stream_http_conf: /opt/stream/etc/conf.d/stream-http.conf +stream_path_to_stream_https_conf: /opt/stream/etc/conf.d/stream-https.conf + +# Certificate generation and custom certs stream_generate_default_cert: true stream_csr: "" #path to the web server certificate csr stream_pem: "" #path to the web server certificate pem stream_key: "" #path to the key stream_chain_pem: "" #path to the certificate's trust chain -web_server: nginx +# Web server +stream_web_server: nginx +# System dependencies stream_dependencies_list: - - "{{ web_server }}" + - wget + - epel-release + - "{{ stream_web_server }}" - postfix - - firewalld - - - + - firewalld \ No newline at end of file diff --git a/defaults/main/mandatory_vars.yml b/defaults/main/mandatory_vars.yml index 90b91b8..f9f498e 100644 --- a/defaults/main/mandatory_vars.yml +++ b/defaults/main/mandatory_vars.yml @@ -1,26 +1,58 @@ --- -# vars file for stream - -play_http_secret_key: http_secret_key -stream_default_ssv_key: test_ssv_key -stream_event_seal_secret: event_seal_secret - -stream_version: 2.0.6-1 -stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}.x86_64.rpm" -sonatype_username: login -sonatype_pwd: pwd - -stream_licence_src_path: ../../../licences/stream.lic - -mongodb_uri: "mongodb://stream_db_admin:AComplexPassword@vdo-mongodb-database:27017/stream?authSource=admin" -mongodb_org_repo_uri: https://repo.mongodb.org/yum/redhat/9Server/mongodb-org/6.0/aarch64/RPMS/mongodb-mongosh-2.2.9.aarch64.rpm - -stream_hostname_node1: stream-n1 -stream_hostname_node2: stream-n2 -stream_hostname_node3: stream-n3 -mongodb_hostname: mongodb-database -load_balancer_hostname: load_balancer - - - +# Stream mandatory variables - change these to your own secure values + +# Firewall configuration, set to true to configure firewall rules, false to skip firewall configuration +stream_configure_firewall: true + +# Stream secret keys - generate secure random keys for your deployment +stream_play_http_secret_key: "CHANGE_THIS_play_http_secret_key" +stream_default_ssv_key: "CHANGE_THIS_default_ssv_key" +stream_event_seal_secret: "CHANGE_THIS_event_seal_secret" + +# Stream Package installation +stream_version: 2.1.5-1 +stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 +stream_repository_username: "CHANGE_THIS_repository_username" +stream_repository_password: "CHANGE_THIS_repository_password" + +# Tinkey package installation +stream_tinkey_version: 1.12.0.1-1 +stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 + +# Licence file source path +stream_licence_src_path: "{{ playbook_dir }}/files/stream.lic" + +# Cluster nodes configuration - change these to match your environment +# Set the hostname and IP address for each Stream node in your cluster +# For testing with Molecule, these values will be overridden in molecule.yml +stream_nodes: + - hostname: stream1 # These values are filled for example, you can change these hostnames and IPs as needed based on your inventory + ip: 10.10.21.1 + - hostname: stream2 + ip: 10.10.21.2 +# - hostname: stream3 +# ip: 10.10.21.3 +# - hostname: stream-node4 +# ip: 10.10.21.4 +# - hostname: stream-node5 +# ip: 10.10.21.5 + +# MongoDB configuration +# MongoDB Shell package URI, as for now, we only support MongoDB 7.0 and 8.0. +# Example format: mongodb://username:password@mongodb-host:27017/stream?authSource=admin +stream_mongodb_uri: "mongodb://stream:evertrust123@10.10.21.6:27017/stream?authSource=admin" # Change to your MongoDB connection string +stream_mongodb_shell_package_uri: https://repo.mongodb.org/yum/redhat/9Server/mongodb-org/8.0/aarch64/RPMS/mongodb-mongosh-2.5.8.aarch64.rpm +stream_mongodb_hostname: mongo +stream_mongodb_ip: 10.10.21.6 # Change to your MongoDB server IP + +# Akka cluster communication ports +# These ports must be open between all nodes in the cluster +stream_akka_discovery_port: 7626 # Akka Management HTTP port (for discovery and health checks) +stream_akka_artery_port: 17355 # Akka Artery port (for cluster communication) +stream_internal_api_port: 9000 # Internal API port (bound to 127.0.0.1) + +# Load balancer configuration - change these to match your environment +stream_use_load_balancer: false # Set to true if using a load balancer in front of Stream nodes +stream_load_balancer_hostname: load_balancer +stream_load_balancer_ip: 10.10.21.7 # Change to your load balancer IP \ No newline at end of file diff --git a/handlers/main.yml b/handlers/main.yml index b3c7adb..8e5f298 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,21 +1,28 @@ --- -- name: Stream - Handlers - Restart Stream service +- name: Stream - Handlers - Restart postfix service + ansible.builtin.systemd: + name: postfix + state: restarted + enabled: yes + listen: "Stream - Handlers - Restart postfix services" + +- name: Stream - Handlers - Restart stream service ansible.builtin.systemd: name: stream state: restarted enabled: yes - listen: "Stream - Handlers - Restart Stream services" + listen: "Stream - Handlers - Restart stream services" - name: Stream - nginx - Test Config ansible.builtin.command: nginx -t changed_when: false - listen: "Stream - Handlers - Restart Nginx services" + listen: Stream - Handlers - Restart nginx services become: true -- name: Stream - Nginx - Reload +- name: Stream - nginx - Reload ansible.builtin.service: name: nginx state: restarted - listen: "Stream - Handlers - Restart Nginx services" - become: true + listen: Stream - Handlers - Restart nginx services + become: true \ No newline at end of file diff --git a/meta/main.yml b/meta/main.yml index ea68190..78234bf 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,34 +1,8 @@ galaxy_info: - author: your name - description: your role description - company: your company (optional) - - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value - # issue_tracker_url: http://example.com/issue/tracker - - # Choose a valid license ID from https://spdx.org - some suggested licenses: - # - BSD-3-Clause (default) - # - MIT - # - GPL-2.0-or-later - # - GPL-3.0-only - # - Apache-2.0 - # - CC-BY-4.0 - license: license (GPL-2.0-or-later, MIT, etc) - - min_ansible_version: 2.1 - - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - galaxy_tags: [] - # List tags for your role here, one per line. A tag is a keyword that describes - # and categorizes the role. Users find roles by searching for tags. Be sure to - # remove the '[]' above, if you add tags to this list. - # - # NOTE: A tag is limited to a single word comprised of alphanumeric characters. - # Maximum 20 tags per role. - -dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. + role_name: ansible_role_stream + namespace: evertrust + author: evertrust + description: Deployment of Stream in HA using RPMs v2 using Dynamic inventory + company: EVERTRUST + min_ansible_version: "2.1" + license: proprietary \ No newline at end of file diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..bc6c9b4 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,8 @@ +--- + +- name: Converge + hosts: stream_nodes + become: true + gather_facts: true + roles: + - role: "{{ playbook_dir }}/../.." \ No newline at end of file diff --git a/molecule/default/create.yml b/molecule/default/create.yml new file mode 100644 index 0000000..644bd1b --- /dev/null +++ b/molecule/default/create.yml @@ -0,0 +1,26 @@ +--- + +- name: Create + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Create docker network + community.docker.docker_network: + name: stream-network + state: present + + - name: Create molecule instance(s) + community.docker.docker_container: + name: "{{ item.name }}" + image: "{{ item.image }}" + state: started + command: "{{ item.command | default('') }}" + privileged: "{{ item.privileged | default(omit) }}" + volumes: "{{ item.volumes | default(omit) }}" + cgroupns_mode: "{{ item.cgroupns_mode | default(omit) }}" + networks: "{{ item.networks | default(omit) }}" + published_ports: "{{ item.published_ports | default(omit) }}" + env: "{{ item.environment | default(omit) }}" + register: server + loop: "{{ molecule_yml.platforms }}" diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml new file mode 100644 index 0000000..8c0df9d --- /dev/null +++ b/molecule/default/destroy.yml @@ -0,0 +1,31 @@ +--- + +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + - name: Destroy molecule instance(s) + community.docker.docker_container: + name: "{{ item.name }}" + state: absent + force_kill: true + register: server + loop: "{{ molecule_yml.platforms }}" + async: 7200 + poll: 0 + + - name: Wait for instance(s) deletion to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + register: docker_jobs + until: docker_jobs.finished + retries: 300 + delay: 1 + loop: "{{ server.results }}" + + - name: Remove docker network + community.docker.docker_network: + name: stream-network + state: absent diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..47b1e2d --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,128 @@ +--- + +driver: + name: docker + +platforms: + # Stream application nodes, make as many as needed, this is only the default configuration for testing with Molecule + - name: stream-node1 + image: geerlingguy/docker-rockylinux9-ansible:latest + command: "" + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true + networks: + - name: stream-network + published_ports: + - "9001:9000" + groups: + - stream_nodes + + - name: stream-node2 + image: geerlingguy/docker-rockylinux9-ansible:latest + command: "" + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + pre_build_image: true + networks: + - name: stream-network + published_ports: + - "9002:9000" + groups: + - stream_nodes + + # MongoDB database server + - name: mongodb-database + image: mongo:8.0 + command: mongod --bind_ip_all + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: evertrust123 + networks: + - name: stream-network + published_ports: + - "27017:27017" + groups: + - mongodb + +provisioner: + name: ansible + connection_options: + ansible_connection: docker + config_options: + defaults: + host_key_checking: false + callbacks_enabled: profile_tasks, timer + stdout_callback: default + result_format: yaml + inventory: + host_vars: + stream-node1: + ansible_connection: docker + stream-node2: + ansible_connection: docker + mongodb-database: + ansible_connection: docker + group_vars: + all: + # Stream secret keys + stream_play_http_secret_key: "CHANGE_THIS_play_http_secret_key" + stream_default_ssv_key: "CHANGE_THIS_default_ssv_key" + stream_event_seal_secret: "CHANGE_THIS_event_seal_secret" + + # Stream package installation + stream_version: 2.1.5-1 + stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}.noarch.rpm" + stream_repository_username: "CHANGE_THIS_repository_username" + stream_repository_password: "CHANGE_THIS_repository_password" + + # Tinkey installation + stream_tinkey_version: 1.12.0.1-1 + stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}.noarch.rpm" #please check on the repo for the version, as it could be x86_64 or aarch64 + + # Licence configuration + stream_licence_src_path: "{{ playbook_dir }}/files/stream.lic" + + # MongoDB configuration + stream_mongodb_uri: "mongodb://admin:evertrust123@mongodb-database:27017/stream?authSource=admin" + stream_mongodb_hostname: mongodb-database + stream_mongodb_ip: mongodb-database + + # Cluster nodes configuration + stream_nodes: + - hostname: stream-node1 + ip: stream-node1 + #- hostname: stream-node2 + # ip: stream-node2 + + # Pekko/Akka cluster communication ports + stream_pekko_discovery_port: 7626 + stream_pekko_artery_port: 17355 + stream_internal_api_port: 9000 + + # Load balancer configuration + stream_use_load_balancer: false + stream_load_balancer_hostname: load_balancer + stream_load_balancer_ip: load_balancer + +verifier: + name: ansible + +scenario: + name: default + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - verify + - cleanup + - destroy diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml new file mode 100644 index 0000000..fece1f4 --- /dev/null +++ b/molecule/default/prepare.yml @@ -0,0 +1,17 @@ +--- + +- name: Prepare stream nodes + hosts: stream_nodes + gather_facts: false + tasks: + - name: Wait for systemd to be ready + ansible.builtin.wait_for_connection: + timeout: 30 + +- name: Prepare MongoDB + hosts: mongodb + gather_facts: false + tasks: + - name: Wait for MongoDB to start + ansible.builtin.pause: + seconds: 10 diff --git a/molecule/default/requirements.yml b/molecule/default/requirements.yml new file mode 100644 index 0000000..d4e1f03 --- /dev/null +++ b/molecule/default/requirements.yml @@ -0,0 +1,5 @@ +collections: + - name: community.docker + version: "4.7.0" + - name: ansible.posix + version: "1.5.4" diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml new file mode 100644 index 0000000..43b4aeb --- /dev/null +++ b/molecule/default/verify.yml @@ -0,0 +1,176 @@ +--- + +- name: Verify + hosts: stream_nodes + gather_facts: true + tasks: + - name: Check if Stream is installed + ansible.builtin.stat: + path: /opt/stream + register: stream_dir + + - name: Assert Stream directory exists + ansible.builtin.assert: + that: + - stream_dir.stat.exists + - stream_dir.stat.isdir + fail_msg: "Stream installation directory does not exist" + success_msg: "Stream is installed" + + - name: Check Stream configuration file + ansible.builtin.stat: + path: /opt/stream/etc/stream.conf + register: stream_conf + + - name: Assert Stream config exists + ansible.builtin.assert: + that: + - stream_conf.stat.exists + fail_msg: "Stream configuration file does not exist" + success_msg: "Stream configuration file exists" + + - name: Check Stream environment file + ansible.builtin.stat: + path: /etc/default/stream + register: stream_env + + - name: Assert Stream environment file exists + ansible.builtin.assert: + that: + - stream_env.stat.exists + fail_msg: "Stream environment file does not exist" + success_msg: "Stream environment file exists" + + - name: Check Tinkey installation + ansible.builtin.stat: + path: /opt/evertrust/tinkey/bin/tinkey + register: tinkey_binary + + - name: Assert Tinkey is installed + ansible.builtin.assert: + that: + - tinkey_binary.stat.exists + fail_msg: "Tinkey binary does not exist" + success_msg: "Tinkey is installed" + + - name: Check Stream keyset file + ansible.builtin.stat: + path: /opt/stream/etc/stream.keyset + register: stream_keyset + + - name: Assert Stream keyset exists + ansible.builtin.assert: + that: + - stream_keyset.stat.exists + fail_msg: "Stream keyset file does not exist" + success_msg: "Stream keyset file exists" + + - name: Verify AKKA_DISCOVERY_ENDPOINTS contains all nodes + ansible.builtin.shell: | + grep "AKKA_DISCOVERY_ENDPOINTS" /etc/default/stream + register: AKKA_endpoints + changed_when: false + +# Make sure to adjust the number of hostnames based on the nodes configured + - name: Assert all nodes are in AKKA discovery + ansible.builtin.assert: + that: + - "'stream-node1:7626' in AKKA_endpoints.stdout" + - "'stream-node2:7626' in AKKA_endpoints.stdout" + # - "'stream-node3:7626' in AKKA_endpoints.stdout" + # - "'stream-node4:7626' in AKKA_endpoints.stdout" + # - "'stream-node5:7626' in AKKA_endpoints.stdout" + fail_msg: "Not all nodes are configured in AKKA_DISCOVERY_ENDPOINTS" + success_msg: "All nodes are properly configured in AKKA discovery" + + - name: Check Stream service status + ansible.builtin.systemd: + name: stream + register: stream_service + failed_when: false + + - name: Assert Stream service is running + ansible.builtin.assert: + that: + - stream_service.status.ActiveState == "active" + - stream_service.status.SubState == "running" + fail_msg: "Stream service is not running (State: {{ stream_service.status.ActiveState }}, SubState: {{ stream_service.status.SubState }})" + success_msg: "Stream service is running" + + - name: Check Nginx service status + ansible.builtin.systemd: + name: nginx + register: nginx_service + failed_when: false + + - name: Assert Nginx service is running + ansible.builtin.assert: + that: + - nginx_service.status.ActiveState == "active" + - nginx_service.status.SubState == "running" + fail_msg: "Nginx service is not running (State: {{ nginx_service.status.ActiveState }}, SubState: {{ nginx_service.status.SubState }})" + success_msg: "Nginx service is running" + + - name: Check Postfix service status + ansible.builtin.systemd: + name: postfix + register: postfix_service + failed_when: false + + - name: Assert Postfix service is running + ansible.builtin.assert: + that: + - postfix_service.status.ActiveState == "active" + - postfix_service.status.SubState == "running" + fail_msg: "Postfix service is not running (State: {{ postfix_service.status.ActiveState }}, SubState: {{ postfix_service.status.SubState }})" + success_msg: "Postfix service is running" + + - name: Check Nginx configuration + ansible.builtin.command: nginx -t + register: nginx_config_test + changed_when: false + failed_when: false + + - name: Assert Nginx configuration is valid + ansible.builtin.assert: + that: + - nginx_config_test.rc == 0 + fail_msg: "Nginx configuration test failed: {{ nginx_config_test.stderr }}" + success_msg: "Nginx configuration is valid" + + - name: Check Stream service is enabled + ansible.builtin.systemd: + name: stream + register: stream_enabled + failed_when: false + + - name: Assert Stream service is enabled + ansible.builtin.assert: + that: + - stream_enabled.status.UnitFileState == "enabled" + fail_msg: "Stream service is not enabled to start on boot" + success_msg: "Stream service is enabled" + +- name: Verify MongoDB + hosts: stream_nodes + gather_facts: false + run_once: true + tasks: + - name: Wait for MongoDB to be ready + ansible.builtin.wait_for: + host: mongodb-database + port: 27017 + timeout: 30 + + - name: Test MongoDB connection with mongosh + ansible.builtin.command: mongosh --host mongodb-database --port 27017 --eval "db.adminCommand('ping')" + register: mongodb_ping + changed_when: false + failed_when: false + + - name: Assert MongoDB is responding + ansible.builtin.assert: + that: + - mongodb_ping.rc == 0 + fail_msg: "MongoDB is not responding to ping command" + success_msg: "MongoDB is running and responding" \ No newline at end of file diff --git a/tasks/config.yml b/tasks/config.yml index 786640e..527a0c7 100644 --- a/tasks/config.yml +++ b/tasks/config.yml @@ -1,4 +1,32 @@ --- + +- name: Config - Check if Stream is installed + ansible.builtin.stat: + path: /opt/stream + register: stream_check + +- name: Config - Fail if Stream not installed + ansible.builtin.fail: + msg: | + Stream is not installed! The /opt/stream directory does not exist. + This usually means the Stream RPM installation failed. + Please check: + 1. Repository credentials are correct (stream_repository_username/password) + 2. The RPM download succeeded + 3. The RPM installation completed + Check the previous task output for errors. + when: not stream_check.stat.exists + +- name: Config - Ensure Stream directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/stream/etc + - /opt/stream/var/log + - /opt/stream/var/run + - name: Config - Symlink file: src: /opt/stream/etc/stream-httpd.conf @@ -6,58 +34,135 @@ state: link force: yes -- name: Config - Update licence +- name: Config - Check if licence file source exists + ansible.builtin.stat: + path: "{{ stream_licence_src_path }}" + register: licence_file_check + delegate_to: localhost + become: false + when: + - stream_licence_src_path is defined + - stream_licence_src_path != "" + - stream_licence_src_path != "path_to_your_licence_file" + +- name: Config - Fail if licence file not found + ansible.builtin.fail: + msg: | + Licence file not found at: {{ stream_licence_src_path }} + Please provide a valid Stream licence file by either: + 1. Setting STREAM_LICENCE_PATH environment variable + 2. Setting stream_licence_src_path in your configuration + 3. Placing the licence file at the specified path + when: + - stream_licence_src_path is defined + - stream_licence_src_path != "" + - stream_licence_src_path != "path_to_your_licence_file" + - licence_file_check.stat is defined + - not (licence_file_check.stat.exists | default(false)) + +- name: Config - Copy licence from source file ansible.builtin.copy: src: "{{ stream_licence_src_path }}" dest: "{{ stream_licence_dest_path }}" + mode: '0644' + when: + - stream_licence_src_path is defined + - stream_licence_src_path != "" + - stream_licence_src_path != "path_to_your_licence_file" + - licence_file_check.stat is defined + - licence_file_check.stat.exists | default(false) + +- name: Config - Fail if no licence path configured + ansible.builtin.fail: + msg: | + No licence file path configured! + Please provide a Stream licence file by setting: + - STREAM_LICENCE_PATH environment variable, or + - stream_licence_src_path in mandatory_vars.yml + when: + - stream_licence_src_path is not defined or stream_licence_src_path == "" or stream_licence_src_path == "path_to_your_licence_file" + +- name: Config - Add stream nodes to /etc/hosts (non-Docker) + ansible.builtin.blockinfile: + path: /etc/hosts + block: | + {% for node in stream_nodes %} + {{ node.ip }} {{ node.hostname }} + {% endfor %} + {{ stream_mongodb_ip }} {{ stream_mongodb_hostname }} + marker: "# {mark} ANSIBLE MANAGED BLOCK - Stream Cluster" + create: no + unsafe_writes: yes + when: ansible_virtualization_type != "docker" + +- name: Config - Add hosts for Docker environment + ansible.builtin.shell: | + {% for node in stream_nodes %} + grep -q "{{ node.hostname }}" /etc/hosts || echo "{{ node.ip }} {{ node.hostname }}" >> /etc/hosts + {% endfor %} + grep -q "{{ stream_mongodb_hostname }}" /etc/hosts || echo "{{ stream_mongodb_ip }} {{ stream_mongodb_hostname }}" >> /etc/hosts + args: + executable: /bin/bash + when: ansible_virtualization_type == "docker" + changed_when: false - name: Config - File creation /etc/default/stream ansible.builtin.template: - src: "{{ path_to_template_default_stream }}" - dest: "{{ path_to_default_stream }}" + src: "../templates/{{ stream_template_default_stream }}" + dest: "{{ stream_path_to_default_stream }}" - name: Config - Update hosts allowed ansible.builtin.template: - src: "{{ path_to_template_hosts_allowed }}" - dest: "{{ path_to_hosts_allowed }}" - + src: "../templates/{{ stream_template_hosts_allowed }}" + dest: "{{ stream_path_to_hosts_allowed }}" + - name: Config - Update http.conf ansible.builtin.template: - src: "{{ path_to_template_http_conf }}" - dest: "{{ path_to_stream_http_conf }}" + src: "../templates/{{ stream_template_http_conf }}" + dest: "{{ stream_path_to_stream_http_conf }}" - name: Config - Update https.conf ansible.builtin.template: - src: "{{ path_to_template_https_conf }}" - dest: "{{ path_to_stream_https_conf }}" + src: "../templates/{{ stream_template_https_conf }}" + dest: "{{ stream_path_to_stream_https_conf }}" -- name: Nginx_certs - Flush handlers +- name: Config - Flush handlers ansible.builtin.meta: flush_handlers - name: Config - Generate the tink key on node 1 - shell: /opt/stream/sbin/tinkey generate-keyset --out=/opt/stream/etc/stream.keyset > /opt/stream/etc/stream.keyset + shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset args: creates: /opt/stream/etc/stream.keyset - when: inventory_hostname == "{{ stream_hostname_node1 }}" + when: + - inventory_hostname == stream_nodes[0].hostname + - stream_nodes | length > 1 - name: Config - Copy keyset file content on control node ansible.builtin.slurp: src: /opt/stream/etc/stream.keyset register: stream_keyset - when: inventory_hostname == "{{ stream_hostname_node1 }}" + when: + - inventory_hostname == stream_nodes[0].hostname + - stream_nodes | length > 1 run_once: true - name: Config - Distribute the file to other nodes ansible.builtin.copy: - content: "{{ (stream_keyset['content'] | b64decode) }}" + content: "{{ hostvars[stream_nodes[0].hostname]['stream_keyset']['content'] | b64decode }}" dest: "/opt/stream/etc/stream.keyset" - when: inventory_hostname != "{{ stream_hostname_node1 }}" + when: + - inventory_hostname != stream_nodes[0].hostname + - stream_nodes | length > 1 + +- name: Config - Generate keyset for standalone node + shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset + args: + creates: /opt/stream/etc/stream.keyset + when: stream_nodes | length == 1 -- name: Config - Set permissions and ownership of these files +- name: Config - Set permissions and ownership of keyset file ansible.builtin.file: path: "/opt/stream/etc/stream.keyset" owner: stream group: stream - mode: '0640' - - + mode: '0640' \ No newline at end of file diff --git a/tasks/enterprise_linux_OS_install.yml b/tasks/enterprise_linux_OS_install.yml index 856c0d7..0ad612a 100644 --- a/tasks/enterprise_linux_OS_install.yml +++ b/tasks/enterprise_linux_OS_install.yml @@ -9,41 +9,90 @@ - name: Enterprise_linux_OS_install - Check SELinux status command: getenforce register: selinux_status + changed_when: false - name: Enterprise_linux_OS_install - Set SELinux to permissive mode if it is enforcing - ansible.posix.selinux: - policy: targeted - state: permissive + command: setsebool -P httpd_can_network_connect 1 when: selinux_status['stdout'] == "Enforcing" +- name: Enterprise_linux_OS_install - Check if Stream RPM is already installed + ansible.builtin.stat: + path: /opt/stream + register: stream_installed + - name: Enterprise_linux_OS_install - Temporary directory creation ansible.builtin.tempfile: state: directory prefix: stream_ register: stream_temp_install_directory + when: not stream_installed.stat.exists - name: Enterprise_linux_OS_install - Download Stream's rpm ansible.builtin.get_url: url: "{{ stream_pkg_uri }}" dest: "{{ stream_temp_install_directory['path'] }}" - username: "{{ sonatype_username }}" - password: '{{ sonatype_pwd }}' + username: "{{ stream_repository_username }}" + password: '{{ stream_repository_password }}' + when: not stream_installed.stat.exists - name: Enterprise_linux_OS_install - Install Stream's rpm ansible.builtin.yum: name: "{{ [ stream_temp_install_directory['path'], (stream_pkg_uri | basename) ] | path_join }}" state: present disable_gpg_check: yes - become: yes + become: true + when: not stream_installed.stat.exists + +- name: Enterprise_linux_OS_install - Check if Tinkey is already installed + ansible.builtin.stat: + path: /opt/stream/sbin/tinkey + register: tinkey_installed + +- name: Enterprise_linux_OS_install - Create temp directory for Tinkey + ansible.builtin.tempfile: + state: directory + prefix: stream_tinkey_ + register: stream_tinkey_temp_directory + when: not tinkey_installed.stat.exists + +- name: Enterprise_linux_OS_install - Download Tinkey RPM + ansible.builtin.get_url: + url: "{{ stream_tinkey_pkg_uri }}" + dest: "{{ [ stream_tinkey_temp_directory['path'], (stream_tinkey_pkg_uri | basename) ] | path_join }}" + username: "{{ stream_repository_username }}" + password: '{{ stream_repository_password }}' + when: not tinkey_installed.stat.exists + +- name: Enterprise_linux_OS_install - Install Tinkey RPM + ansible.builtin.yum: + name: "{{ [ stream_tinkey_temp_directory['path'], (stream_tinkey_pkg_uri | basename) ] | path_join }}" + state: present + disable_gpg_check: yes + skip_broken: yes + when: not tinkey_installed.stat.exists + +- name: Enterprise_linux_OS_install - Check if mongosh is already installed + ansible.builtin.stat: + path: /usr/bin/mongosh + register: mongosh_installed + +- name: Enterprise_linux_OS_install - Create temp directory for mongosh + ansible.builtin.tempfile: + state: directory + prefix: stream_mongosh_ + register: stream_mongosh_temp_directory + when: not mongosh_installed.stat.exists - name: Enterprise_linux_OS_install - Download mongodb-mongosh RPM ansible.builtin.get_url: - url: "{{ mongodb_org_repo_uri }}" - dest: "{{ [ stream_temp_install_directory['path'], (mongodb_org_repo_uri | basename) ] | path_join }}" + url: "{{ stream_mongodb_shell_package_uri }}" + dest: "{{ [ stream_mongosh_temp_directory['path'], (stream_mongodb_shell_package_uri | basename) ] | path_join }}" + when: not mongosh_installed.stat.exists -- name: Enterprise_linux_OS_install - Install MongoDB RPMs +- name: Enterprise_linux_OS_install - Install Mongosh RPMs ansible.builtin.yum: - name: "{{ [ stream_temp_install_directory['path'], (mongodb_org_repo_uri | basename) ] | path_join }}" + name: "{{ [ stream_mongosh_temp_directory['path'], (stream_mongodb_shell_package_uri | basename) ] | path_join }}" state: present disable_gpg_check: yes skip_broken: yes + when: not mongosh_installed.stat.exists diff --git a/tasks/firewall.yml b/tasks/firewall.yml new file mode 100644 index 0000000..61c5bf2 --- /dev/null +++ b/tasks/firewall.yml @@ -0,0 +1,54 @@ +--- + +- name: Firewall - Check if firewalld is installed + ansible.builtin.package_facts: + manager: auto + +- name: Firewall - Install firewalld if not present + ansible.builtin.yum: + name: firewalld + state: present + when: "'firewalld' not in ansible_facts.packages" + +- name: Firewall - Ensure firewalld is running + ansible.builtin.systemd: + name: firewalld + state: started + enabled: yes + +- name: Firewall - Ensure SSH is allowed + ansible.posix.firewalld: + service: ssh + permanent: yes + state: enabled + immediate: yes + +- name: Firewall - Open HTTP port (80) + ansible.posix.firewalld: + service: http + permanent: yes + state: enabled + immediate: yes + +- name: Firewall - Open HTTPS port (443) + ansible.posix.firewalld: + service: https + permanent: yes + state: enabled + immediate: yes + +- name: Firewall - Open Akka Management HTTP port (7626) + ansible.posix.firewalld: + port: "{{ stream_akka_discovery_port }}/tcp" + permanent: yes + state: enabled + immediate: yes + when: stream_nodes | length > 1 + +- name: Firewall - Open Akka Artery port for cluster communication (17355) + ansible.posix.firewalld: + port: "{{ stream_akka_artery_port }}/tcp" + permanent: yes + state: enabled + immediate: yes + when: stream_nodes | length > 1 \ No newline at end of file diff --git a/tasks/firewall_config.yml b/tasks/firewall_config.yml deleted file mode 100644 index 85bd2ad..0000000 --- a/tasks/firewall_config.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- - -- name: Firewall Config - Start firewalld service - ansible.builtin.systemd: - name: firewalld - state: started - enabled: yes - -- name: Firewall Config - Open firewall's https service - ansible.posix.firewalld: - permanent: true - service: https - state: enabled - immediate: true - notify: "Stream - Handlers - Restart Nginx services" - -- name: Firewall Config - Open firewall's http service - ansible.posix.firewalld: - permanent: true - service: http - state: enabled - immediate: true - notify: "Stream - Handlers - Restart Nginx services" - -- name: Firewall Config - Open Akka_HA - ansible.posix.firewalld: - permanent: true - port: 25520/tcp - state: enabled - immediate: true - notify: "Stream - Handlers - Restart Nginx services" - -- name: Firewall Config - Open Akka_MGMT - ansible.posix.firewalld: - permanent: true - port: 8558/tcp - state: enabled - immediate: true - notify: "Stream - Handlers - Restart Nginx services" - -- name: Firewall Config - Internal API - ansible.posix.firewalld: - permanent: true - port: 9000/tcp - state: enabled - immediate: true - notify: "Stream - Handlers - Restart Nginx services" \ No newline at end of file diff --git a/tasks/launching.yml b/tasks/launching.yml index 24806b7..5996a6a 100644 --- a/tasks/launching.yml +++ b/tasks/launching.yml @@ -6,7 +6,7 @@ state: started enabled: yes -- name: Launching - Start Horizon service +- name: Launching - Start Stream service ansible.builtin.systemd: name: stream state: started diff --git a/tasks/main.yml b/tasks/main.yml index cbc7f3f..a2cb59f 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,16 +1,40 @@ --- +- name: Main - Set stream_hostname_node to current inventory hostname + ansible.builtin.set_fact: + stream_hostname_node: "{{ inventory_hostname }}" + +- name: Main - Find node number in stream_nodes list + ansible.builtin.set_fact: + node_number: "{{ idx }}" + loop: "{{ stream_nodes }}" + loop_control: + index_var: idx + when: + - item.hostname == inventory_hostname + - node_number is not defined + +- name: Main - Fail if node not found + ansible.builtin.fail: + msg: "Could not find {{ inventory_hostname }} in stream_nodes list" + when: node_number is not defined + +- name: Main - Show configuration + ansible.builtin.debug: + msg: "Node configured as {{ stream_hostname_node }} with index {{ node_number }}" + - name: Installation for Enterprise Linux OS include_tasks: enterprise_linux_OS_install.yml - name: Generate and/or config the Nginx certs include_tasks: nginx_certs.yml -#- name: Configure the firewalld's configuration, mainly for testing purposes -# include_tasks: firewall_config.yml - - name: Edit config files include_tasks: config.yml +- name: Configure firewall + include_tasks: firewall.yml + when: stream_configure_firewall | default(true) + - name: Launch Stream and its necessary services - include_tasks: launching.yml + include_tasks: launching.yml \ No newline at end of file diff --git a/tasks/nginx_certs.yml b/tasks/nginx_certs.yml index 482d990..6480bd6 100644 --- a/tasks/nginx_certs.yml +++ b/tasks/nginx_certs.yml @@ -1,56 +1,54 @@ --- -##If no certificate has been set up in variables - name: Nginx_certs - Ensure SSL directory exists ansible.builtin.file: - path: "{{ nginx_certs_directory_path }}" + path: "{{ stream_cert_directory_path }}" state: directory mode: '0755' - fail: - msg: "You should set all of the stream_csr etc... " + msg: "You should set all of the stream_csr, stream_pem, stream_key, and stream_chain_pem variables" when: stream_generate_default_cert == false and (stream_csr == "" or stream_pem == "" or stream_key == "") - name: Nginx_certs - Generate OpenSSL key - default ansible.builtin.command: - cmd: "openssl genpkey -algorithm {{ stream_cert_key_type }} -out {{ [nginx_certs_directory_path, 'stream.key'] | path_join }} -pkeyopt rsa_keygen_bits:{{ stream_cert_key_size }}" - creates: "{{ [nginx_certs_directory_path, 'stream.key'] | path_join }}" + cmd: "openssl genpkey -algorithm {{ stream_cert_key_type }} -out {{ [stream_cert_directory_path, 'stream.key'] | path_join }} -pkeyopt rsa_keygen_bits:{{ stream_cert_key_size }}" + creates: "{{ [stream_cert_directory_path, 'stream.key'] | path_join }}" when: stream_generate_default_cert == true - notify: "Stream - Handlers - Restart Nginx services" + notify: "Stream - Handlers - Restart nginx services" register: generate_key - name: Nginx_certs - Generate OpenSSL CSR - default ansible.builtin.command: cmd: > - openssl req -new -key {{ [nginx_certs_directory_path, 'stream.key'] | path_join }} -out {{ [nginx_certs_directory_path, 'stream.csr'] | path_join }} - -subj "/C={{ stream_cert_C }}/O={{ stream_cert_O }}/OU={{ stream_cert_OU }}/CN={{ stream_cert_CN }}" - creates: "{{ [nginx_certs_directory_path, 'stream.csr'] | path_join }}" + openssl req -new -key {{ [stream_cert_directory_path, 'stream.key'] | path_join }} -out {{ [stream_cert_directory_path, 'stream.csr'] | path_join }} + -subj "/C={{ stream_cert_country }}/O={{ stream_cert_organization }}/OU={{ stream_cert_organizational_unit }}/CN={{ stream_cert_common_name }}" + creates: "{{ [stream_cert_directory_path, 'stream.csr'] | path_join }}" when: stream_generate_default_cert == true - notify: "Stream - Handlers - Restart Nginx services" + notify: "Stream - Handlers - Restart nginx services" register: generate_csr - name: Nginx_certs - Self-sign the certificate - default ansible.builtin.command: cmd: > - openssl x509 -req -days 365 -in {{ [nginx_certs_directory_path, 'stream.csr'] | path_join }} - -signkey {{ [nginx_certs_directory_path, 'stream.key'] | path_join }} -out {{ [nginx_certs_directory_path, 'stream.pem'] | path_join }} - -set_serial 01 -days 365 -sha256 - creates: "{{ [nginx_certs_directory_path, 'stream.pem'] | path_join }}" + openssl x509 -req -days 365 -in {{ [stream_cert_directory_path, 'stream.csr'] | path_join }} + -signkey {{ [stream_cert_directory_path, 'stream.key'] | path_join }} -out {{ [stream_cert_directory_path, 'stream.pem'] | path_join }} + -set_serial 01 -days 365 -{{ stream_cert_hashing_algo }} + creates: "{{ [stream_cert_directory_path, 'stream.pem'] | path_join }}" when: stream_generate_default_cert == true - notify: "Stream - Handlers - Restart Nginx services" + notify: "Stream - Handlers - Restart nginx services" register: sign_certificate - name: Nginx_certs - Create the certificate chain file - default ansible.builtin.copy: - src: "{{ [nginx_certs_directory_path, 'stream.pem'] | path_join }}" - dest: "{{ [nginx_certs_directory_path, 'stream-chain.pem'] | path_join }}" + src: "{{ [stream_cert_directory_path, 'stream.pem'] | path_join }}" + dest: "{{ [stream_cert_directory_path, 'stream-chain.pem'] | path_join }}" force: yes remote_src: yes register: chain_pem_created - notify: "Stream - Handlers - Restart Nginx services" + notify: "Stream - Handlers - Restart nginx services" when: stream_generate_default_cert == true -##If a certificate, its key and its trustchain have been setup - name: Nginx_certs - Create stream.key with provided content ansible.builtin.copy: content: "{{ stream_current_file['value']['src'] }}" @@ -58,11 +56,9 @@ loop: "{{ stream_custom_certs | dict2items }}" loop_control: loop_var: stream_current_file - notify: "Stream - Handlers - Restart Nginx services" + notify: "Stream - Handlers - Restart nginx services" when: stream_generate_default_cert == false -##Permissions changes : not incorporated to the cert generation to handle the cases where the certs -##already exist or have been created using builtin.command - name: Nginx_certs - Set permissions and ownership of these files ansible.builtin.file: path: "{{ stream_current_file['value']['dest'] }}" @@ -72,5 +68,4 @@ loop: "{{ stream_custom_certs | dict2items }}" loop_control: loop_var: stream_current_file - notify: "Stream - Handlers - Restart Nginx services" - + notify: "Stream - Handlers - Restart nginx services" \ No newline at end of file diff --git a/templates/etc_default_stream.j2 b/templates/etc_default_stream.j2 index 9a7e180..a361de4 100644 --- a/templates/etc_default_stream.j2 +++ b/templates/etc_default_stream.j2 @@ -4,28 +4,32 @@ JVM_MAX_MEMORY=4096m JVM_TYPE=server # Play variables -PLAY_HTTP_PORT=9000 +PLAY_HTTP_PORT={{ stream_internal_api_port }} PLAY_HTTP_ADDRESS=127.0.0.1 PLAY_CONFIG_FILE=/opt/stream/etc/stream.conf PLAY_LOGGER_FILE=/opt/stream/etc/stream-logback.xml PLAY_PID_FILE=/opt/stream/var/run/stream.pid -PLAY_HTTP_SECRET_KEY={{ play_http_secret_key }} +PLAY_HTTP_SECRET_KEY={{ stream_play_http_secret_key }} # Mongo variables -MONGODB_URI="{{ mongodb_uri }}" +MONGODB_URI="{{ stream_mongodb_uri }}" -# stream variables +# Stream variables +STREAM_NOTIFICATION_SMTP_HOST=127.0.0.1 STREAM_HOSTNAME={{ stream_hostname_node }} -STREAM_EVENT_SEAL_SECRET={{ stream_event_seal_secret }} +STREAM_DEFAULT_SSV_KEY={{ stream_default_ssv_key }} +STREAM_EVENT_SEAL_SECRET={{ stream_event_seal_secret }} STREAM_ENFORCE_X509_AUTH=false # Akka nodes for HA # All HA nodes in hostname:port format, comma separated -AKKA_DISCOVERY_ENDPOINTS={{ stream_hostname_node1 }}:8558,{{ stream_hostname_node2 }}:8558,{{ stream_hostname_node3 }}:8558 +AKKA_DISCOVERY_ENDPOINTS={% if stream_nodes | length > 1 %}{% for node in stream_nodes %}{{ node.hostname }}:{{ stream_akka_discovery_port }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + # The index of this machine's node in the above list. Index starts at 0 -AKKA_MANAGEMENT_LOCAL={{ node_number }} +AKKA_MANAGEMENT_LOCAL={{ node_number if stream_nodes | length > 1 else '' }} + # The port used for communication between nodes, in hostname:port format -AKKA_ARTERY={{ stream_hostname_node }}:25520 +AKKA_ARTERY={{ (stream_hostname_node ~ ':' ~ stream_akka_artery_port) if stream_nodes | length > 1 else '' }} # x509 authentication header CERTIFICATE_HEADER=SSL_CLIENT_CERT \ No newline at end of file diff --git a/templates/etc_hosts.j2 b/templates/etc_hosts.j2 deleted file mode 100644 index 6469fa8..0000000 --- a/templates/etc_hosts.j2 +++ /dev/null @@ -1,6 +0,0 @@ -127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 -::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 -{{ stream_ip_node1 }} {{ stream_hostname_node1 }} -{{ stream_ip_node2 }} {{ stream_hostname_node2 }} -{{ stream_ip_node3 }} {{ stream_hostname_node3 }} -{{ mongodb_ip }} {{ mongodb_hostname }} \ No newline at end of file diff --git a/templates/hosts_allowed.j2 b/templates/hosts_allowed.j2 index a825a10..51e7c85 100644 --- a/templates/hosts_allowed.j2 +++ b/templates/hosts_allowed.j2 @@ -1,5 +1,18 @@ -# Define custom configuration for play/akka/mongo/stream here -play.filters.hosts.allowed.4="{{ stream_hostname_node1 }}" -play.filters.hosts.allowed.5="{{ stream_hostname_node2 }}" -play.filters.hosts.allowed.6="{{ stream_hostname_node3 }}" -play.filters.hosts.allowed.7="{{ load_balancer_hostname }}" \ No newline at end of file +{% set start_index = 4 %} +{% for node in stream_nodes %} +play.filters.hosts.allowed.{{ start_index + loop.index0 }}="{{ node.hostname }}" +{% endfor %} +{% if stream_use_load_balancer | default(false) %} +play.filters.hosts.allowed.{{ start_index + stream_nodes | length }}="{{ stream_load_balancer_hostname }}" + +{% endif %} + +# Akka cluster split-brain resolver configuration +{% if stream_nodes | length > 1 %} +akka.cluster.split-brain-resolver { + active-strategy = "lease-majority" + lease-majority { + lease-implementation = "lease.mongo" + } +} +{% endif %} \ No newline at end of file diff --git a/tests/deploy.yml b/tests/deploy.yml new file mode 100644 index 0000000..8e5da03 --- /dev/null +++ b/tests/deploy.yml @@ -0,0 +1,16 @@ +--- + +- name: Deploy Stream in HA + hosts: stream_cluster + become: true + gather_facts: true + vars_files: + - ../defaults/main/default_values.yml + - ../defaults/main/mandatory_vars.yml + - ../vars/main.yml + tasks: + - name: Execute Stream role tasks + ansible.builtin.import_tasks: ../tasks/main.yml + + handlers: + - ansible.builtin.import_tasks: ../handlers/main.yml diff --git a/tests/inventory b/tests/inventory deleted file mode 100644 index 878877b..0000000 --- a/tests/inventory +++ /dev/null @@ -1,2 +0,0 @@ -localhost - diff --git a/tests/inventory.py b/tests/inventory.py new file mode 100755 index 0000000..f033212 --- /dev/null +++ b/tests/inventory.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Dynamic inventory script for Stream cluster deployment. +Reads node configuration from mandatory_vars.yml and generates Ansible inventory. +""" +import yaml +import json +import sys +import os + +def load_vars_file(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + possible_paths = [ + os.path.join(script_dir, '..', '..', 'defaults', 'main', 'mandatory_vars.yml'), + os.path.join(script_dir, '..', 'defaults', 'main', 'mandatory_vars.yml'), + os.path.join(script_dir, 'defaults', 'main', 'mandatory_vars.yml'), + ] + + vars_file = None + for path in possible_paths: + normalized_path = os.path.normpath(path) + if os.path.exists(normalized_path): + vars_file = normalized_path + break + + if vars_file is None: + print(f"Warning: mandatory_vars.yml not found in any expected location", file=sys.stderr) + print(f"Searched paths:", file=sys.stderr) + for path in possible_paths: + print(f" - {os.path.normpath(path)}", file=sys.stderr) + return None + + try: + with open(vars_file, 'r') as f: + vars_data = yaml.safe_load(f) + + if vars_data is None: + print(f"Warning: {vars_file} is empty or invalid", file=sys.stderr) + return None + + return vars_data + + except FileNotFoundError: + print(f"Warning: {vars_file} not found", file=sys.stderr) + return None + except yaml.YAMLError as e: + print(f"Error parsing YAML file: {e}", file=sys.stderr) + return None + except Exception as e: + print(f"Unexpected error loading vars file: {e}", file=sys.stderr) + return None + +def build_inventory(vars_data): + inventory = { + 'all': { + 'children': ['stream_cluster'] + }, + 'stream_cluster': { + 'hosts': [], + 'vars': { + 'ansible_user': 'root', + 'ansible_python_interpreter': '/usr/bin/python3', + 'ansible_ssh_common_args': '-o StrictHostKeyChecking=no' + } + }, + '_meta': { + 'hostvars': {} + } + } + + if vars_data is None: + return inventory + + stream_nodes = vars_data.get('stream_nodes', []) + + if not stream_nodes: + print("Warning: No stream_nodes found in configuration", file=sys.stderr) + return inventory + + if not isinstance(stream_nodes, list): + print(f"Warning: stream_nodes should be a list, got {type(stream_nodes)}", file=sys.stderr) + return inventory + + for node in stream_nodes: + if not isinstance(node, dict): + print(f"Warning: Invalid node configuration: {node}", file=sys.stderr) + continue + + hostname = node.get('hostname') + ip = node.get('ip') + + if not hostname: + print(f"Warning: Node missing hostname: {node}", file=sys.stderr) + continue + + if not ip: + print(f"Warning: Node {hostname} missing IP address", file=sys.stderr) + continue + + inventory['stream_cluster']['hosts'].append(hostname) + inventory['_meta']['hostvars'][hostname] = { + 'ansible_host': ip + } + + return inventory + +def main(): + vars_data = load_vars_file() + inventory = build_inventory(vars_data) + print(json.dumps(inventory, indent=2)) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/test.yml b/tests/test.yml deleted file mode 100644 index 924bd71..0000000 --- a/tests/test.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- hosts: localhost - remote_user: root - roles: - - Stream diff --git a/tests/uninstall.yml b/tests/uninstall.yml new file mode 100644 index 0000000..4e65ebc --- /dev/null +++ b/tests/uninstall.yml @@ -0,0 +1,69 @@ +--- + +- name: Uninstall Stream + hosts: stream_cluster + become: true + gather_facts: true + + tasks: + - name: Stop Stream service + ansible.builtin.systemd: + name: stream + state: stopped + ignore_errors: yes + + - name: Stop nginx service + ansible.builtin.systemd: + name: nginx + state: stopped + ignore_errors: yes + + - name: Stop postfix service + ansible.builtin.systemd: + name: postfix + state: stopped + ignore_errors: yes + + - name: Remove Stream RPM + ansible.builtin.yum: + name: stream + state: absent + + - name: Remove Tinkey RPM + ansible.builtin.yum: + name: tinkey + state: absent + + - name: Remove Stream directories + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /opt/stream + - /etc/default/stream + - /etc/nginx/conf.d/stream.conf + - /etc/nginx/ssl/stream.key + - /etc/nginx/ssl/stream.pem + - /etc/nginx/ssl/stream.csr + - /etc/nginx/ssl/stream-chain.pem + + - name: Remove Stream entries from /etc/hosts + ansible.builtin.lineinfile: + path: /etc/hosts + regexp: "{{ item }}" + state: absent + loop: + - "stream-node" + - "mongodb" + + - name: Remove firewall rules + ansible.posix.firewalld: + port: "{{ item }}" + permanent: yes + state: disabled + immediate: yes + loop: + - 443/tcp + - 7626/tcp + - 17355/tcp + ignore_errors: yes \ No newline at end of file diff --git a/vars/main.yml b/vars/main.yml index ad0d3a9..886d856 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -1,20 +1,15 @@ --- + stream_custom_certs: key: src: "{{ stream_key }}" - dest: "{{ [ nginx_certs_directory_path, 'stream.key' ] | path_join }}" + dest: "{{ [ stream_cert_directory_path, 'stream.key' ] | path_join }}" pem: src: "{{ stream_pem }}" - dest: "{{ [ nginx_certs_directory_path, 'stream.pem' ] | path_join }}" + dest: "{{ [ stream_cert_directory_path, 'stream.pem' ] | path_join }}" csr: src: "{{ stream_csr }}" - dest: "{{ [ nginx_certs_directory_path, 'stream.csr' ] | path_join }}" + dest: "{{ [ stream_cert_directory_path, 'stream.csr' ] | path_join }}" chain_pem: src: "{{ stream_chain_pem }}" - dest: "{{ [ nginx_certs_directory_path, 'stream-chain.pem' ] | path_join }}" - - - - - - + dest: "{{ [ stream_cert_directory_path, 'stream-chain.pem' ] | path_join }}" From 1bbe3a774931c4018268624d45359f3bcbb78f43 Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Fri, 20 Mar 2026 13:52:57 +0100 Subject: [PATCH 2/4] refactor: add Molecule testing, handlers fixes, Tinkey version guard, and README update - Add Molecule test suite: prepare.yml with MongoDB user creation via docker exec, verify.yml with AKKA discovery checks (loop-based for Ansible 2.17+ compat), collections.yml, updated molecule.yml with KMS env var lookups - Add become: true to all systemd handlers (postfix, stream) - Add notify to config template tasks (etc_default_stream, hosts_allowed, http.conf, https.conf) - Add Tinkey/Tink env vars version guard (only rendered for Stream >= 2.1.7) - Add stream_extra_allowed_hosts support in hosts_allowed.j2 - Update README: Evertrust branding, KMS section, correct variable tables - Add stream.env.example with all required environment variables - Fix tests/uninstall.yml: hosts stream_nodes, boolean values - Update .gitignore: track *.env.example, ignore *.env --- .gitignore | 3 +- README.md | 95 +++++++++++----------- defaults/main/default_values.yml | 17 +++- defaults/main/mandatory_vars.yml | 56 ++++++------- handlers/main.yml | 8 +- meta/main.yml | 4 +- molecule/default/collections.yml | 5 ++ molecule/default/molecule.yml | 109 ++++++++++++++++++++------ molecule/default/prepare.yml | 25 +++++- molecule/default/verify.yml | 37 ++++++--- stream.env.example | 44 +++++++++++ tasks/config.yml | 86 +++++++++++++++++--- tasks/enterprise_linux_OS_install.yml | 42 +++++----- tasks/firewall.yml | 22 +++--- tasks/launching.yml | 11 ++- tasks/main.yml | 31 ++++++-- tasks/nginx_certs.yml | 7 +- templates/etc_default_stream.j2 | 4 + templates/hosts_allowed.j2 | 7 +- tests/uninstall.yml | 12 +-- 20 files changed, 438 insertions(+), 187 deletions(-) create mode 100644 molecule/default/collections.yml create mode 100644 stream.env.example diff --git a/.gitignore b/.gitignore index 9af97db..958a275 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Licence files - never commit these! *.lic -!horizon-test.lic .DS_Store +# Environment files with credentials - never commit these! +*.env diff --git a/README.md b/README.md index 426d2fe..26fc050 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -Role Name -========= +Evertrust Stream +================ Requirements ------------ -To be able to use this role you have to previously provision VMs running on **CentOS/RHEL** and meeting the requirements from the official EVERTRUST documentation. +To be able to use this role you have to previously provision VMs running on **CentOS/RHEL** and meeting the requirements from the official Evertrust documentation. A root access to these VMs is mandatory, as per a normal RPM install of Stream. You will have to configure your Ansible playbook to use these accounts while playing the role. **IMPORTANT:** A running instance of MongoDB is also necessary. MongoDB should be accessible from all Stream nodes. @@ -21,11 +21,10 @@ It is necessary to: ```bash # Generate SSH key if needed ssh-keygen -t rsa -b 4096 - + # Copy to each target node ssh-copy-id root@stream-node1-ip ssh-copy-id root@stream-node2-ip - ... ``` 3. **Dynamic Inventory:** The role includes a dynamic inventory script at `tests/inventory.py` that automatically generates the Ansible inventory from your `mandatory_vars.yml` configuration. The script sets appropriate connection parameters and maps hostnames to IP addresses, eliminating manual inventory management. @@ -51,12 +50,11 @@ Note: The files/ directories are not included in the repository and must be crea | Port/Service | Purpose | When Opened | |--------------|---------|-------------| | **22 (SSH)** | Remote administration | Always | -| **80** | HTTP web access | Always | | **443** | HTTPS web access | Always | | **7626** | Akka Management (cluster discovery) | Only for HA (2+ nodes) | | **17355** | Akka Artery (cluster communication) | Only for HA (2+ nodes) | -**Note:** Port 9000 (Stream application) is bound to `127.0.0.1` only and is NOT exposed externally. Nginx acts as a reverse proxy on ports 80/443. +**Note:** Port 9000 (Stream application) is bound to `127.0.0.1` only and is NOT exposed externally. Nginx acts as a reverse proxy on port 443. **Note on Akka/Pekko:** Stream internally uses Pekko (the successor to Akka), but configuration variables maintain the `AKKA_*` naming convention for backwards compatibility with older Stream versions. The functionality and port numbers remain the same. @@ -70,24 +68,36 @@ The following table regroups the data that you have to provide the Ansible role | Key | Value Type | |-----|------------| -| `stream_play_http_secret_key` | String (32+ characters), alphanumeric only | -| `stream_default_ssv_key` | String (32+ characters), alphanumeric only | -| `stream_event_seal_secret` | String (32+ characters), alphanumeric only | -| `stream_version` | Stream version (e.g., `2.1.5-1`) | -| `stream_pkg_uri` | URL where you store the Stream RPM | -| `stream_tinkey_version` | Tinkey version (e.g., `1.12.0.1-1`) | -| `stream_tinkey_pkg_uri` | URL where you store the Tinkey RPM | -| `stream_repository_username` | Username to authenticate to this URL | -| `stream_repository_password` | Password to authenticate to this URL | +| `stream_play_http_secret_key` | Random string, 128 characters, may include `@`, `!`, `#` | +| `stream_default_ssv_key` | Random string, 128 characters, may include `@`, `!`, `#` | +| `stream_event_seal_secret` | Random string, 128 characters, may include `@`, `!`, `#` | +| `stream_version` | Stream version (e.g., `2.1.7`) | +| `stream_pkg_uri` | URL where you store the Stream RPM (check architecture: x86_64 or aarch64) | +| `stream_repository_username` | Username to authenticate to the Evertrust repository | +| `stream_repository_password` | Password to authenticate to the Evertrust repository | +| `stream_tinkey_version` | Tinkey version (e.g., `1.12.0.1`) | +| `stream_tinkey_pkg_uri` | URL where you store the Tinkey RPM (check architecture: x86_64 or aarch64) | | `stream_licence_src_path` | Path to your Stream license on your control machine | -| `stream_mongodb_uri` | MongoDB connection string with authentication | -| `stream_mongodb_shell_package_uri` | URL where you store the Mongosh RPM | +| `stream_mongodb_username` | MongoDB user pre-created on your MongoDB instance | +| `stream_mongodb_password` | Password of the MongoDB user | +| `stream_mongodb_uri` | MongoDB connection string (auto-built from username/password/hostname) | +| `stream_mongodb_shell_package_uri` | URL where you store the Mongosh RPM (check architecture: x86_64 or aarch64) | | `stream_mongodb_hostname` | Hostname of your MongoDB instance | | `stream_mongodb_ip` | IP address of your MongoDB instance | | `stream_nodes` | List of Stream nodes with hostname and IP (see below) | | `stream_akka_discovery_port` | Port for Akka cluster discovery (default: 7626) | | `stream_akka_artery_port` | Port for Akka cluster communication (default: 17355) | | `stream_internal_api_port` | Port for internal API (default: 9000) | +| `stream_configure_firewall` | Set to `false` to skip firewall configuration (default: `true`) | +| `stream_extra_allowed_hosts` | Additional hosts allowed in Play filter, e.g. load balancer hostname (default: `[]`) | + +### Tinkey / KMS Variables + +| Key | Value Type | +|-----|------------| +| `stream_tink_master_key_uri` | KMS key URI (e.g. `gcp-kms://...` or `aws-kms://...`). Leave empty for soft mode (plaintext keyset). | +| `stream_tink_credentials_src_path` | Path to the KMS credentials file on the Ansible controller. Leave empty for soft mode or HSM. | +| `stream_tink_credentials_path` | Destination path of the KMS credentials file on the VM (default: `/opt/stream/etc/tink-credentials.json`) | **Stream Nodes Configuration:** @@ -113,7 +123,7 @@ stream_nodes: - For **x86_64** (most common): Use the x86_64 RPM URL - For **aarch64** (ARM): Use the aarch64 RPM URL -The default configuration uses aarch64. Change this if your systems are x86_64. +The default configuration uses the architecture detected at runtime (`ansible_facts['architecture']`). The Stream and Tinkey RPMs default to `noarch` and work on both architectures. ## Overview @@ -131,7 +141,7 @@ This role is composed of the following steps played in order: The download and install of the necessary packages to run Stream, including: - System dependencies (wget, nginx, postfix, firewalld, epel-release) - Stream RPM package -- Tinkey (Google Tink key management tool) +- Tinkey RPM package (used for AES256 keyset encryption) - MongoDB shell (mongosh) - SELinux configuration if enforcing mode is detected @@ -174,7 +184,14 @@ All certificate files are automatically set with proper permissions (root:nginx, sudo systemctl reload nginx ``` -### 3. Configuration Provisioning +### 3. Tinkey Keyset Management + +Generation and distribution of the AES256-GCM encryption keyset used by Stream: +- On HA deployments: generated on node 1 and distributed to all other nodes +- On standalone deployments: generated directly on the single node +- Keyset stored at `/opt/stream/etc/stream.keyset` (permissions: `stream:stream 0640`) + +### 4. Configuration Provisioning The provisioning of your Stream licence and the different configuration files needed for Stream to run properly, based on their respective templates: - Deployment of license file to `/opt/stream/etc/stream.lic` @@ -182,28 +199,14 @@ The provisioning of your Stream licence and the different configuration files ne - Generation of `/etc/default/stream` with JVM, Play, MongoDB, and Akka cluster settings - Configuration of hosts.allowed whitelist - Nginx symlink creation -- HTTP/HTTPS configuration files - -### 4. Tinkey Keyset Management - -**IMPORTANT:** For Stream deployments, the role automatically manages Tinkey keysets for encryption: - -For **HA deployments** (2+ nodes): -- Generates the keyset on the first node -- Distributes the same keyset to all other nodes -- Ensures all nodes share the same encryption keys - -For **standalone deployments** (single node): -- Generates a keyset locally on that node - -The keyset file is located at `/opt/stream/etc/stream.keyset` with proper permissions (stream:stream, 0640). ### 5. Firewall Configuration Automatic configuration of firewalld to open necessary ports: - SSH (22) to prevent lockout -- HTTP (80) and HTTPS (443) for web access +- HTTPS (443) for web access - Akka cluster ports (7626, 17355) for HA deployments only +- Can be disabled by setting `stream_configure_firewall: false` ### 6. Service Management @@ -229,12 +232,12 @@ This configuration ensures proper cluster behavior during network partitions and ### Hosts Allowed Configuration -Additionally, a touchy and key element of the Stream configuration is the Play variable "hosts allowed" in the stream-extra.conf file. It configures the whitelist allowed to access Stream. +Additionally, a touchy and key element of the Stream configuration is the Play variable "hosts allowed" in the stream-extra.conf file. It configures the whitelist allowed to access Stream. The role automatically configures this whitelist to include: - localhost - All Stream cluster node hostnames -- Load balancer hostname (if `stream_use_load_balancer` is set to true) +- Any additional hosts defined in `stream_extra_allowed_hosts` (e.g. load balancer hostname, `127.0.0.1` for local testing) This prevents "Host not allowed" errors when accessing Stream through different hostnames. @@ -243,7 +246,7 @@ This prevents "Host not allowed" errors when accessing Stream through different ### Standalone Deployment For a single Stream instance (no High Availability), configure only one node in `stream_nodes`. The role will: -- Only open HTTP, HTTPS, SSH, and Internal API ports +- Only open HTTPS and SSH ports - Not open Akka cluster ports - Configure Stream without cluster formation - Generate keyset locally @@ -262,8 +265,8 @@ For a clustered Stream deployment (2+ nodes), configure multiple nodes in `strea Here is a basic way of using this role: ```yaml -- name: Deploy Stream in HA - hosts: stream_cluster +- name: Deploy Stream + hosts: stream_nodes become: true gather_facts: true roles: @@ -293,7 +296,7 @@ ansible-playbook -i tests/inventory.py tests/deploy.yml -vvv ## Check the Installation -If your Ansible role didn't fail during the play, your Stream HA should be deployed. +If your Ansible role didn't fail during the play, your Stream instance should be deployed. To test it, run: @@ -328,9 +331,6 @@ sudo systemctl status nginx # Check postfix service status sudo systemctl status postfix - -# For HA: Verify MongoDB connectivity -mongosh --host --port 27017 ``` ## Pre-Deployment Checklist @@ -340,6 +340,7 @@ Before running the playbook, verify: - [ ] All VMs are provisioned and accessible via SSH - [ ] SSH keys are copied to all target nodes - [ ] MongoDB is running and accessible +- [ ] MongoDB user is created with appropriate roles on the `stream` database - [ ] `mandatory_vars.yml` is configured with real values (no placeholders) - [ ] MongoDB shell package URI matches your architecture (x86_64 vs aarch64) - [ ] Stream license file path is correct @@ -349,4 +350,4 @@ Before running the playbook, verify: ## Author Information -If you have difficulties to use this role or modifications recommendations, please contact EVERTRUST. +If you have difficulties to use this role or modifications recommendations, please contact Evertrust. \ No newline at end of file diff --git a/defaults/main/default_values.yml b/defaults/main/default_values.yml index 019ac99..253f298 100644 --- a/defaults/main/default_values.yml +++ b/defaults/main/default_values.yml @@ -1,10 +1,10 @@ --- # Stream default version -stream_version: 2.1.5-1 +stream_version: 2.1.7 # Tinkey default version -stream_tinkey_version: 1.12.0.1-1 +stream_tinkey_version: 1.12.0.1 # Licence file path stream_licence_dest_path: /opt/stream/etc/stream.lic @@ -41,6 +41,19 @@ stream_chain_pem: "" #path to the certificate's trust chain # Web server stream_web_server: nginx +# Tinkey configuration +# Soft mode (default): leave stream_tink_master_key_uri empty — keyset generated as plaintext locally +# KMS mode (GCP/AWS): set stream_tink_master_key_uri to the KMS key URI +# stream_tink_credentials_path = dest path of KMS credentials file on the VM +# stream_tink_credentials_src_path = source path of KMS credentials file on the Ansible controller +# HSM mode (PKCS#11): set stream_tink_master_key_uri to pkcs11:// URI, leave credentials empty +stream_tink_master_key_uri: "{{ lookup('env', 'STREAM_TINK_MASTER_KEY_URI') }}" +stream_tink_credentials_path: "{{ lookup('env', 'STREAM_TINK_CREDENTIALS_PATH') | default('/opt/stream/etc/tink-credentials.json', true) }}" +stream_tink_credentials_src_path: "{{ lookup('env', 'STREAM_TINK_CREDENTIALS_SRC_PATH') | default('', true) }}" + +# Extra allowed hosts for Play filter (e.g. load balancer hostname, "127.0.0.1" for local testing) +stream_extra_allowed_hosts: [] + # System dependencies stream_dependencies_list: - wget diff --git a/defaults/main/mandatory_vars.yml b/defaults/main/mandatory_vars.yml index f9f498e..3998a5a 100644 --- a/defaults/main/mandatory_vars.yml +++ b/defaults/main/mandatory_vars.yml @@ -5,46 +5,50 @@ # Firewall configuration, set to true to configure firewall rules, false to skip firewall configuration stream_configure_firewall: true -# Stream secret keys - generate secure random keys for your deployment -stream_play_http_secret_key: "CHANGE_THIS_play_http_secret_key" -stream_default_ssv_key: "CHANGE_THIS_default_ssv_key" -stream_event_seal_secret: "CHANGE_THIS_event_seal_secret" +# Stream secret keys - random string with special chars (@!#), 128 characters +# Set via environment variables for idempotent deployments +# If not set, a random value is auto-generated ONCE per play on the first node and distributed +stream_play_http_secret_key: "{{ lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') }}" +stream_default_ssv_key: "{{ lookup('env', 'STREAM_DEFAULT_SSV_KEY') }}" +stream_event_seal_secret: "{{ lookup('env', 'STREAM_EVENT_SEAL_SECRET') }}" # Stream Package installation -stream_version: 2.1.5-1 -stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 -stream_repository_username: "CHANGE_THIS_repository_username" -stream_repository_password: "CHANGE_THIS_repository_password" +stream_version: 2.1.7 +stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}-1.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 +stream_repository_username: "{{ lookup('env', 'EVERTRUST_REPO_USERNAME') | default('CHANGE_THIS_repository_username', true) }}" +stream_repository_password: "{{ lookup('env', 'EVERTRUST_REPO_PASSWORD') | default('CHANGE_THIS_repository_password', true) }}" # Tinkey package installation -stream_tinkey_version: 1.12.0.1-1 -stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 +stream_tinkey_version: 1.12.0.1 +stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}-1.noarch.rpm" # please check on the repo for the version, as it could be x86_64 or aarch64 # Licence file source path -stream_licence_src_path: "{{ playbook_dir }}/files/stream.lic" +stream_licence_src_path: "{{ lookup('env', 'STREAM_LICENCE_PATH') | default(playbook_dir ~ '/files/stream.lic', true) }}" # Cluster nodes configuration - change these to match your environment # Set the hostname and IP address for each Stream node in your cluster # For testing with Molecule, these values will be overridden in molecule.yml stream_nodes: - - hostname: stream1 # These values are filled for example, you can change these hostnames and IPs as needed based on your inventory - ip: 10.10.21.1 - - hostname: stream2 - ip: 10.10.21.2 -# - hostname: stream3 -# ip: 10.10.21.3 + - hostname: stream-node1 + ip: 10.10.0.1 # Change to your actual IP + - hostname: stream-node2 + ip: 10.10.0.2 # Change to your actual IP +# - hostname: stream-node3 +# ip: 10.10.0.3 # - hostname: stream-node4 -# ip: 10.10.21.4 +# ip: 10.10.0.4 # - hostname: stream-node5 -# ip: 10.10.21.5 +# ip: 10.10.0.5 # MongoDB configuration # MongoDB Shell package URI, as for now, we only support MongoDB 7.0 and 8.0. -# Example format: mongodb://username:password@mongodb-host:27017/stream?authSource=admin -stream_mongodb_uri: "mongodb://stream:evertrust123@10.10.21.6:27017/stream?authSource=admin" # Change to your MongoDB connection string -stream_mongodb_shell_package_uri: https://repo.mongodb.org/yum/redhat/9Server/mongodb-org/8.0/aarch64/RPMS/mongodb-mongosh-2.5.8.aarch64.rpm -stream_mongodb_hostname: mongo -stream_mongodb_ip: 10.10.21.6 # Change to your MongoDB server IP +# MongoDB credentials - must be set explicitly (user must be created in MongoDB beforehand) +stream_mongodb_username: "{{ lookup('env', 'STREAM_MONGODB_USERNAME') | default('CHANGE_THIS_mongodb_username', true) }}" +stream_mongodb_password: "{{ lookup('env', 'STREAM_MONGODB_PASSWORD') | default('CHANGE_THIS_mongodb_password', true) }}" +stream_mongodb_uri: "mongodb://{{ stream_mongodb_username }}:{{ stream_mongodb_password }}@{{ stream_mongodb_hostname }}:27017/stream?authSource=admin" +stream_mongodb_shell_package_uri: "https://repo.mongodb.org/yum/redhat/9Server/mongodb-org/8.0/{{ ansible_facts['architecture'] }}/RPMS/mongodb-mongosh-2.5.8.{{ ansible_facts['architecture'] }}.rpm" +stream_mongodb_hostname: mongodb-machine +stream_mongodb_ip: 10.10.0.10 # Change to your MongoDB server IP # Akka cluster communication ports # These ports must be open between all nodes in the cluster @@ -52,7 +56,3 @@ stream_akka_discovery_port: 7626 # Akka Management HTTP port (for discovery a stream_akka_artery_port: 17355 # Akka Artery port (for cluster communication) stream_internal_api_port: 9000 # Internal API port (bound to 127.0.0.1) -# Load balancer configuration - change these to match your environment -stream_use_load_balancer: false # Set to true if using a load balancer in front of Stream nodes -stream_load_balancer_hostname: load_balancer -stream_load_balancer_ip: 10.10.21.7 # Change to your load balancer IP \ No newline at end of file diff --git a/handlers/main.yml b/handlers/main.yml index 8e5f298..b0f1217 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -4,15 +4,17 @@ ansible.builtin.systemd: name: postfix state: restarted - enabled: yes + enabled: true listen: "Stream - Handlers - Restart postfix services" + become: true - name: Stream - Handlers - Restart stream service ansible.builtin.systemd: name: stream state: restarted - enabled: yes + enabled: true listen: "Stream - Handlers - Restart stream services" + become: true - name: Stream - nginx - Test Config ansible.builtin.command: nginx -t @@ -21,7 +23,7 @@ become: true - name: Stream - nginx - Reload - ansible.builtin.service: + ansible.builtin.systemd: name: nginx state: restarted listen: Stream - Handlers - Restart nginx services diff --git a/meta/main.yml b/meta/main.yml index 78234bf..563b89b 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -2,7 +2,7 @@ galaxy_info: role_name: ansible_role_stream namespace: evertrust author: evertrust - description: Deployment of Stream in HA using RPMs v2 using Dynamic inventory + description: Deployment of Stream in Standalone or HA using RPMs company: EVERTRUST - min_ansible_version: "2.1" + min_ansible_version: "2.17" license: proprietary \ No newline at end of file diff --git a/molecule/default/collections.yml b/molecule/default/collections.yml new file mode 100644 index 0000000..74a2a13 --- /dev/null +++ b/molecule/default/collections.yml @@ -0,0 +1,5 @@ +collections: + - name: community.docker + version: "5.0.6" + - name: ansible.posix + version: "2.1.0" diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index 47b1e2d..1e225e6 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -17,9 +17,11 @@ platforms: - name: stream-network published_ports: - "9001:9000" + - "4431:443" groups: - stream_nodes +# Uncomment nodes below and add them to stream_nodes group_vars to test with more nodes - name: stream-node2 image: geerlingguy/docker-rockylinux9-ansible:latest command: "" @@ -32,8 +34,56 @@ platforms: - name: stream-network published_ports: - "9002:9000" + - "4432:443" groups: - stream_nodes +# - name: stream-node3 +# image: geerlingguy/docker-rockylinux9-ansible:latest +# command: "" +# volumes: +# - /sys/fs/cgroup:/sys/fs/cgroup:rw +# cgroupns_mode: host +# privileged: true +# pre_build_image: true +# networks: +# - name: stream-network +# published_ports: +# - "9003:9000" +# - "4433:443" +# groups: +# - stream_nodes + +# - name: stream-node4 +# image: geerlingguy/docker-rockylinux9-ansible:latest +# command: "" +# volumes: +# - /sys/fs/cgroup:/sys/fs/cgroup:rw +# cgroupns_mode: host +# privileged: true +# pre_build_image: true +# networks: +# - name: stream-network +# published_ports: +# - "9004:9000" +# - "4434:443" +# groups: +# - stream_nodes + +# - name: stream-node5 +# image: geerlingguy/docker-rockylinux9-ansible:latest +# command: "" +# volumes: +# - /sys/fs/cgroup:/sys/fs/cgroup:rw +# cgroupns_mode: host +# privileged: true +# pre_build_image: true +# networks: +# - name: stream-network +# published_ports: +# - "9005:9000" +# - "4435:443" +# groups: +# - stream_nodes # MongoDB database server - name: mongodb-database @@ -56,7 +106,7 @@ provisioner: config_options: defaults: host_key_checking: false - callbacks_enabled: profile_tasks, timer + callbacks_enabled: timer stdout_callback: default result_format: yaml inventory: @@ -69,26 +119,34 @@ provisioner: ansible_connection: docker group_vars: all: - # Stream secret keys - stream_play_http_secret_key: "CHANGE_THIS_play_http_secret_key" - stream_default_ssv_key: "CHANGE_THIS_default_ssv_key" - stream_event_seal_secret: "CHANGE_THIS_event_seal_secret" + # Stream secret keys - fixed values for molecule testing (override via env vars in production) + stream_play_http_secret_key: "{{ lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') | default('MoleculeTestPlayHttpSecretKeyForStreamDoNotUseInProduction12345', true) }}" + stream_default_ssv_key: "{{ lookup('env', 'STREAM_DEFAULT_SSV_KEY') | default('MoleculeTestDefaultSSVKeyForStreamDoNotUseInProduction1234567890', true) }}" + stream_event_seal_secret: "{{ lookup('env', 'STREAM_EVENT_SEAL_SECRET') | default('MoleculeTestEventSealSecretForStreamDoNotUseInProduction123456', true) }}" # Stream package installation - stream_version: 2.1.5-1 - stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}.noarch.rpm" - stream_repository_username: "CHANGE_THIS_repository_username" - stream_repository_password: "CHANGE_THIS_repository_password" + stream_version: 2.1.7 + stream_pkg_uri: "https://repo.evertrust.io/repository/stream-rpm/stream-{{ stream_version }}-1.noarch.rpm" + stream_repository_username: "{{ lookup('env', 'EVERTRUST_REPO_USERNAME') | default('CHANGE_THIS_repository_username', true) }}" + stream_repository_password: "{{ lookup('env', 'EVERTRUST_REPO_PASSWORD') | default('CHANGE_THIS_repository_password', true) }}" # Tinkey installation - stream_tinkey_version: 1.12.0.1-1 - stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}.noarch.rpm" #please check on the repo for the version, as it could be x86_64 or aarch64 + stream_tinkey_version: 1.12.0.1 + stream_tinkey_pkg_uri: "https://repo.evertrust.io/repository/tinkey-rpm/tinkey-{{ stream_tinkey_version }}-1.noarch.rpm" #please check on the repo for the version, as it could be x86_64 or aarch64 + + # Tinkey KMS - requires stream_version >= 2.1.7 + # Soft mode by default (stream_version < 2.1.7 always uses soft mode regardless of URI) + # Set STREAM_TINK_MASTER_KEY_URI and STREAM_TINK_CREDENTIALS_SRC_PATH env vars to enable KMS/HSM mode + stream_tink_master_key_uri: "{{ lookup('env', 'STREAM_TINK_MASTER_KEY_URI') | default('', true) }}" + stream_tink_credentials_src_path: "{{ lookup('env', 'STREAM_TINK_CREDENTIALS_SRC_PATH') | default('', true) }}" + stream_tink_credentials_path: "{{ lookup('env', 'STREAM_TINK_CREDENTIALS_PATH') | default('/opt/stream/etc/tink-credentials.json', true) }}" # Licence configuration - stream_licence_src_path: "{{ playbook_dir }}/files/stream.lic" + stream_licence_src_path: "{{ lookup('env', 'STREAM_LICENCE_PATH') | default(playbook_dir ~ '/files/stream.lic', true) }}" - # MongoDB configuration - stream_mongodb_uri: "mongodb://admin:evertrust123@mongodb-database:27017/stream?authSource=admin" + # MongoDB configuration - credentials match the Docker container initialization + stream_mongodb_username: "{{ lookup('env', 'STREAM_MONGODB_USERNAME') | default('admin', true) }}" + stream_mongodb_password: "{{ lookup('env', 'STREAM_MONGODB_PASSWORD') | default('evertrust123', true) }}" stream_mongodb_hostname: mongodb-database stream_mongodb_ip: mongodb-database @@ -96,18 +154,18 @@ provisioner: stream_nodes: - hostname: stream-node1 ip: stream-node1 - #- hostname: stream-node2 - # ip: stream-node2 + - hostname: stream-node2 + ip: stream-node2 - # Pekko/Akka cluster communication ports - stream_pekko_discovery_port: 7626 - stream_pekko_artery_port: 17355 + # Extra allowed hosts - 127.0.0.1 needed for browser access via port-mapped localhost + stream_extra_allowed_hosts: + - "127.0.0.1" + + # Akka cluster communication ports + stream_akka_discovery_port: 7626 + stream_akka_artery_port: 17355 stream_internal_api_port: 9000 - # Load balancer configuration - stream_use_load_balancer: false - stream_load_balancer_hostname: load_balancer - stream_load_balancer_ip: load_balancer verifier: name: ansible @@ -116,7 +174,6 @@ scenario: name: default test_sequence: - dependency - - cleanup - destroy - syntax - create @@ -124,5 +181,7 @@ scenario: - converge - idempotence - verify - - cleanup + - destroy + destroy_sequence: + - dependency - destroy diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml index fece1f4..0bf71cd 100644 --- a/molecule/default/prepare.yml +++ b/molecule/default/prepare.yml @@ -12,6 +12,25 @@ hosts: mongodb gather_facts: false tasks: - - name: Wait for MongoDB to start - ansible.builtin.pause: - seconds: 10 + - name: Wait for MongoDB to be ready + delegate_to: localhost + ansible.builtin.command: > + docker exec mongodb-database mongosh + "mongodb://admin:evertrust123@localhost:27017/admin" + --eval "db.adminCommand('ping')" + --quiet + register: mongo_ready + until: mongo_ready.rc == 0 + retries: 30 + delay: 2 + changed_when: false + + - name: Create application MongoDB user + delegate_to: localhost + ansible.builtin.command: > + docker exec mongodb-database mongosh + "mongodb://admin:evertrust123@localhost:27017/admin" + --eval "try { db.createUser({ user: '{{ stream_mongodb_username }}', pwd: '{{ stream_mongodb_password }}', roles: [{ role: 'readWriteAnyDatabase', db: 'admin' }] }) } catch(e) { if (e.code !== 51003) throw e }" + --quiet + when: stream_mongodb_username != 'admin' + ignore_errors: true diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 43b4aeb..6151be9 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -71,17 +71,31 @@ register: AKKA_endpoints changed_when: false -# Make sure to adjust the number of hostnames based on the nodes configured - - name: Assert all nodes are in AKKA discovery + - name: Assert all nodes are in AKKA discovery (HA mode) ansible.builtin.assert: that: - - "'stream-node1:7626' in AKKA_endpoints.stdout" - - "'stream-node2:7626' in AKKA_endpoints.stdout" - # - "'stream-node3:7626' in AKKA_endpoints.stdout" - # - "'stream-node4:7626' in AKKA_endpoints.stdout" - # - "'stream-node5:7626' in AKKA_endpoints.stdout" - fail_msg: "Not all nodes are configured in AKKA_DISCOVERY_ENDPOINTS" - success_msg: "All nodes are properly configured in AKKA discovery" + - "item.hostname ~ ':' ~ stream_akka_discovery_port in AKKA_endpoints.stdout" + fail_msg: "Node {{ item.hostname }}:{{ stream_akka_discovery_port }} not found in AKKA_DISCOVERY_ENDPOINTS" + success_msg: "Node {{ item.hostname }} is in AKKA discovery" + loop: "{{ stream_nodes }}" + when: stream_nodes | length > 1 + + - name: Assert AKKA_DISCOVERY_ENDPOINTS is empty (standalone mode) + ansible.builtin.assert: + that: + - "'AKKA_DISCOVERY_ENDPOINTS=' in AKKA_endpoints.stdout" + fail_msg: "AKKA_DISCOVERY_ENDPOINTS should be empty in standalone mode" + success_msg: "Standalone mode: AKKA_DISCOVERY_ENDPOINTS is empty as expected" + when: stream_nodes | length == 1 + + - name: Wait for Stream service to be active + ansible.builtin.shell: systemctl is-active stream + register: stream_active + until: stream_active.stdout == "active" + retries: 12 + delay: 10 + changed_when: false + failed_when: false - name: Check Stream service status ansible.builtin.systemd: @@ -163,7 +177,8 @@ timeout: 30 - name: Test MongoDB connection with mongosh - ansible.builtin.command: mongosh --host mongodb-database --port 27017 --eval "db.adminCommand('ping')" + ansible.builtin.shell: | + mongosh "mongodb://admin:evertrust123@mongodb-database:27017/admin" --eval "db.adminCommand('ping')" --quiet register: mongodb_ping changed_when: false failed_when: false @@ -172,5 +187,5 @@ ansible.builtin.assert: that: - mongodb_ping.rc == 0 - fail_msg: "MongoDB is not responding to ping command" + fail_msg: "MongoDB is not responding to ping command (rc={{ mongodb_ping.rc }}, stderr={{ mongodb_ping.stderr | default('') }})" success_msg: "MongoDB is running and responding" \ No newline at end of file diff --git a/stream.env.example b/stream.env.example new file mode 100644 index 0000000..6625c24 --- /dev/null +++ b/stream.env.example @@ -0,0 +1,44 @@ +# ============================================================ +# Stream deployment environment variables +# Copy this file to stream.env and fill in your values. +# stream.env is gitignored and will never be committed. +# ============================================================ + +# Evertrust repository credentials +export EVERTRUST_REPO_USERNAME='your_repo_username' +export EVERTRUST_REPO_PASSWORD='your_repo_password' + +# Licence +export STREAM_LICENCE_PATH=/path/to/your/stream.lic + +# MongoDB credentials (user must be pre-created in MongoDB) +export STREAM_MONGODB_USERNAME='stream_db_admin' +export STREAM_MONGODB_PASSWORD='your_mongodb_password' + +# Secret keys (leave empty to auto-generate on first run) +# Once generated, save the values and set them here for idempotent re-runs +export STREAM_PLAY_HTTP_SECRET_KEY='' +export STREAM_DEFAULT_SSV_KEY='' +export STREAM_EVENT_SEAL_SECRET='' + +# ------------------------------------------------------------ +# Tinkey - Soft mode (default, no KMS) +# Leave the vars below empty or commented out for soft mode. +# ------------------------------------------------------------ +#export STREAM_TINK_MASTER_KEY_URI= +#export STREAM_TINK_CREDENTIALS_PATH= +#export STREAM_TINK_CREDENTIALS_SRC_PATH= + +# ------------------------------------------------------------ +# Tinkey - GCP KMS (uncomment to use) +# ------------------------------------------------------------ +#export STREAM_TINK_MASTER_KEY_URI='gcp-kms://projects/YOUR_PROJECT/locations/YOUR_REGION/keyRings/YOUR_KEYRING/cryptoKeys/YOUR_KEY' +#export STREAM_TINK_CREDENTIALS_PATH=/opt/stream/etc/tink-credentials.json +#export STREAM_TINK_CREDENTIALS_SRC_PATH=/path/to/your/gcp-credentials.json + +# ------------------------------------------------------------ +# Tinkey - AWS KMS (uncomment to use) +# ------------------------------------------------------------ +#export STREAM_TINK_MASTER_KEY_URI='aws-kms://arn:aws:kms:YOUR_REGION:YOUR_ACCOUNT_ID:key/YOUR_KEY_ID' +#export STREAM_TINK_CREDENTIALS_PATH=/opt/stream/etc/aws-credentials +#export STREAM_TINK_CREDENTIALS_SRC_PATH=/path/to/your/aws-credentials \ No newline at end of file diff --git a/tasks/config.yml b/tasks/config.yml index 527a0c7..b43a19f 100644 --- a/tasks/config.yml +++ b/tasks/config.yml @@ -28,11 +28,12 @@ - /opt/stream/var/run - name: Config - Symlink - file: + ansible.builtin.file: src: /opt/stream/etc/stream-httpd.conf dest: /etc/nginx/conf.d/stream.conf state: link - force: yes + force: true + notify: "Stream - Handlers - Restart nginx services" - name: Config - Check if licence file source exists ansible.builtin.stat: @@ -91,9 +92,9 @@ {% endfor %} {{ stream_mongodb_ip }} {{ stream_mongodb_hostname }} marker: "# {mark} ANSIBLE MANAGED BLOCK - Stream Cluster" - create: no - unsafe_writes: yes - when: ansible_virtualization_type != "docker" + create: false + unsafe_writes: true + when: ansible_facts['virtualization_type'] != "docker" - name: Config - Add hosts for Docker environment ansible.builtin.shell: | @@ -103,39 +104,70 @@ grep -q "{{ stream_mongodb_hostname }}" /etc/hosts || echo "{{ stream_mongodb_ip }} {{ stream_mongodb_hostname }}" >> /etc/hosts args: executable: /bin/bash - when: ansible_virtualization_type == "docker" + when: ansible_facts['virtualization_type'] == "docker" and (stream_nodes[0].ip | regex_search('^\d+\.\d+\.\d+\.\d+$') is not none) changed_when: false - name: Config - File creation /etc/default/stream ansible.builtin.template: src: "../templates/{{ stream_template_default_stream }}" dest: "{{ stream_path_to_default_stream }}" + notify: "Stream - Handlers - Restart stream services" - name: Config - Update hosts allowed ansible.builtin.template: src: "../templates/{{ stream_template_hosts_allowed }}" dest: "{{ stream_path_to_hosts_allowed }}" + notify: "Stream - Handlers - Restart stream services" - name: Config - Update http.conf ansible.builtin.template: src: "../templates/{{ stream_template_http_conf }}" dest: "{{ stream_path_to_stream_http_conf }}" + notify: "Stream - Handlers - Restart nginx services" - name: Config - Update https.conf ansible.builtin.template: src: "../templates/{{ stream_template_https_conf }}" dest: "{{ stream_path_to_stream_https_conf }}" + notify: "Stream - Handlers - Restart nginx services" - name: Config - Flush handlers ansible.builtin.meta: flush_handlers -- name: Config - Generate the tink key on node 1 - shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset +- name: Config - Copy Tinkey KMS/HSM credentials file to VM + ansible.builtin.copy: + src: "{{ stream_tink_credentials_src_path }}" + dest: "{{ stream_tink_credentials_path }}" + owner: stream + group: stream + mode: '0640' + when: + - stream_version is version('2.1.7', '>=') + - stream_tink_master_key_uri != "" + - stream_tink_credentials_src_path != "" + +- name: Config - Generate the tink key on node 1 (soft mode) + ansible.builtin.shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset args: creates: /opt/stream/etc/stream.keyset - when: + when: + - inventory_hostname == stream_nodes[0].hostname + - stream_nodes | length > 1 + - stream_tink_master_key_uri == "" or stream_version is version('2.1.7', '<') + +- name: Config - Generate the tink key on node 1 (KMS/HSM mode) + ansible.builtin.shell: >- + /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM + --out=/opt/stream/etc/stream.keyset + --master-key-uri "{{ stream_tink_master_key_uri }}" + {{ '--credential "' + stream_tink_credentials_path + '"' if stream_tink_credentials_src_path != '' else '' }} + args: + creates: /opt/stream/etc/stream.keyset + when: - inventory_hostname == stream_nodes[0].hostname - stream_nodes | length > 1 + - stream_version is version('2.1.7', '>=') + - stream_tink_master_key_uri != "" - name: Config - Copy keyset file content on control node ansible.builtin.slurp: @@ -154,15 +186,43 @@ - inventory_hostname != stream_nodes[0].hostname - stream_nodes | length > 1 -- name: Config - Generate keyset for standalone node - shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset +- name: Config - Generate keyset for standalone node (soft mode) + ansible.builtin.shell: /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM --out=/opt/stream/etc/stream.keyset args: creates: /opt/stream/etc/stream.keyset - when: stream_nodes | length == 1 + when: + - stream_nodes | length == 1 + - stream_tink_master_key_uri == "" or stream_version is version('2.1.7', '<') + +- name: Config - Generate keyset for standalone node (KMS/HSM mode) + ansible.builtin.shell: >- + /opt/evertrust/tinkey/bin/tinkey create-keyset --key-template AES256_GCM + --out=/opt/stream/etc/stream.keyset + --master-key-uri "{{ stream_tink_master_key_uri }}" + {{ '--credential "' + stream_tink_credentials_path + '"' if stream_tink_credentials_src_path != '' else '' }} + args: + creates: /opt/stream/etc/stream.keyset + when: + - stream_nodes | length == 1 + - stream_version is version('2.1.7', '>=') + - stream_tink_master_key_uri != "" - name: Config - Set permissions and ownership of keyset file ansible.builtin.file: path: "/opt/stream/etc/stream.keyset" owner: stream group: stream - mode: '0640' \ No newline at end of file + mode: '0640' + +- name: Config - Display generated credentials (save these if auto-generated) + ansible.builtin.debug: + msg: + - "========== STREAM GENERATED CREDENTIALS ==========" + - "These values were auto-generated. Save them and re-run with env vars set for idempotency." + - "" + - "STREAM_PLAY_HTTP_SECRET_KEY : {{ stream_play_http_secret_key }}" + - "STREAM_DEFAULT_SSV_KEY : {{ stream_default_ssv_key }}" + - "STREAM_EVENT_SEAL_SECRET : {{ stream_event_seal_secret }}" + - "===================================================" + run_once: true + when: lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' \ No newline at end of file diff --git a/tasks/enterprise_linux_OS_install.yml b/tasks/enterprise_linux_OS_install.yml index 0ad612a..86ec2a6 100644 --- a/tasks/enterprise_linux_OS_install.yml +++ b/tasks/enterprise_linux_OS_install.yml @@ -7,13 +7,14 @@ update_cache: true - name: Enterprise_linux_OS_install - Check SELinux status - command: getenforce + ansible.builtin.command: getenforce register: selinux_status changed_when: false - name: Enterprise_linux_OS_install - Set SELinux to permissive mode if it is enforcing - command: setsebool -P httpd_can_network_connect 1 + ansible.builtin.command: setsebool -P httpd_can_network_connect 1 when: selinux_status['stdout'] == "Enforcing" + changed_when: false - name: Enterprise_linux_OS_install - Check if Stream RPM is already installed ansible.builtin.stat: @@ -39,13 +40,13 @@ ansible.builtin.yum: name: "{{ [ stream_temp_install_directory['path'], (stream_pkg_uri | basename) ] | path_join }}" state: present - disable_gpg_check: yes + disable_gpg_check: true become: true when: not stream_installed.stat.exists - name: Enterprise_linux_OS_install - Check if Tinkey is already installed ansible.builtin.stat: - path: /opt/stream/sbin/tinkey + path: /opt/evertrust/tinkey/bin/tinkey register: tinkey_installed - name: Enterprise_linux_OS_install - Create temp directory for Tinkey @@ -67,32 +68,35 @@ ansible.builtin.yum: name: "{{ [ stream_tinkey_temp_directory['path'], (stream_tinkey_pkg_uri | basename) ] | path_join }}" state: present - disable_gpg_check: yes - skip_broken: yes + disable_gpg_check: true + skip_broken: true when: not tinkey_installed.stat.exists - name: Enterprise_linux_OS_install - Check if mongosh is already installed - ansible.builtin.stat: - path: /usr/bin/mongosh + ansible.builtin.shell: rpm -qa | grep -qi mongosh register: mongosh_installed + changed_when: false + failed_when: false + args: + executable: /bin/bash -- name: Enterprise_linux_OS_install - Create temp directory for mongosh - ansible.builtin.tempfile: +- name: Enterprise_linux_OS_install - Ensure mongosh install directory exists + ansible.builtin.file: + path: /tmp/stream-mongosh-install state: directory - prefix: stream_mongosh_ - register: stream_mongosh_temp_directory - when: not mongosh_installed.stat.exists + mode: '0755' + when: mongosh_installed.rc != 0 - name: Enterprise_linux_OS_install - Download mongodb-mongosh RPM ansible.builtin.get_url: url: "{{ stream_mongodb_shell_package_uri }}" - dest: "{{ [ stream_mongosh_temp_directory['path'], (stream_mongodb_shell_package_uri | basename) ] | path_join }}" - when: not mongosh_installed.stat.exists + dest: "/tmp/stream-mongosh-install/{{ stream_mongodb_shell_package_uri | basename }}" + when: mongosh_installed.rc != 0 - name: Enterprise_linux_OS_install - Install Mongosh RPMs ansible.builtin.yum: - name: "{{ [ stream_mongosh_temp_directory['path'], (stream_mongodb_shell_package_uri | basename) ] | path_join }}" + name: "/tmp/stream-mongosh-install/{{ stream_mongodb_shell_package_uri | basename }}" state: present - disable_gpg_check: yes - skip_broken: yes - when: not mongosh_installed.stat.exists + disable_gpg_check: true + skip_broken: true + when: mongosh_installed.rc != 0 diff --git a/tasks/firewall.yml b/tasks/firewall.yml index 61c5bf2..f87fd30 100644 --- a/tasks/firewall.yml +++ b/tasks/firewall.yml @@ -14,41 +14,41 @@ ansible.builtin.systemd: name: firewalld state: started - enabled: yes + enabled: true - name: Firewall - Ensure SSH is allowed ansible.posix.firewalld: service: ssh - permanent: yes + permanent: true state: enabled - immediate: yes + immediate: true - name: Firewall - Open HTTP port (80) ansible.posix.firewalld: service: http - permanent: yes + permanent: true state: enabled - immediate: yes + immediate: true - name: Firewall - Open HTTPS port (443) ansible.posix.firewalld: service: https - permanent: yes + permanent: true state: enabled - immediate: yes + immediate: true - name: Firewall - Open Akka Management HTTP port (7626) ansible.posix.firewalld: port: "{{ stream_akka_discovery_port }}/tcp" - permanent: yes + permanent: true state: enabled - immediate: yes + immediate: true when: stream_nodes | length > 1 - name: Firewall - Open Akka Artery port for cluster communication (17355) ansible.posix.firewalld: port: "{{ stream_akka_artery_port }}/tcp" - permanent: yes + permanent: true state: enabled - immediate: yes + immediate: true when: stream_nodes | length > 1 \ No newline at end of file diff --git a/tasks/launching.yml b/tasks/launching.yml index 5996a6a..fd50b93 100644 --- a/tasks/launching.yml +++ b/tasks/launching.yml @@ -1,13 +1,20 @@ --- +- name: Launching - Wait for MongoDB to be reachable + ansible.builtin.wait_for: + host: "{{ stream_mongodb_hostname }}" + port: 27017 + timeout: 30 + msg: "MongoDB at {{ stream_mongodb_hostname }}:27017 is not reachable" + - name: Launching - Start postfix service ansible.builtin.systemd: name: postfix state: started - enabled: yes + enabled: true - name: Launching - Start Stream service ansible.builtin.systemd: name: stream state: started - enabled: yes + enabled: true diff --git a/tasks/main.yml b/tasks/main.yml index a2cb59f..ccd2458 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,5 +1,22 @@ --- +- name: Main - Generate secret keys once on first node (if not set via env vars) + ansible.builtin.set_fact: + stream_play_http_secret_key: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" + stream_default_ssv_key: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" + stream_event_seal_secret: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" + run_once: true + when: lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' + +- name: Main - Distribute generated secret keys to all nodes + ansible.builtin.set_fact: + stream_play_http_secret_key: "{{ hostvars[stream_nodes[0].hostname]['stream_play_http_secret_key'] }}" + stream_default_ssv_key: "{{ hostvars[stream_nodes[0].hostname]['stream_default_ssv_key'] }}" + stream_event_seal_secret: "{{ hostvars[stream_nodes[0].hostname]['stream_event_seal_secret'] }}" + when: + - lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' + - inventory_hostname != stream_nodes[0].hostname + - name: Main - Set stream_hostname_node to current inventory hostname ansible.builtin.set_fact: stream_hostname_node: "{{ inventory_hostname }}" @@ -10,7 +27,7 @@ loop: "{{ stream_nodes }}" loop_control: index_var: idx - when: + when: - item.hostname == inventory_hostname - node_number is not defined @@ -24,17 +41,17 @@ msg: "Node configured as {{ stream_hostname_node }} with index {{ node_number }}" - name: Installation for Enterprise Linux OS - include_tasks: enterprise_linux_OS_install.yml + ansible.builtin.include_tasks: enterprise_linux_OS_install.yml - name: Generate and/or config the Nginx certs - include_tasks: nginx_certs.yml + ansible.builtin.include_tasks: nginx_certs.yml - name: Edit config files - include_tasks: config.yml + ansible.builtin.include_tasks: config.yml - name: Configure firewall - include_tasks: firewall.yml + ansible.builtin.include_tasks: firewall.yml when: stream_configure_firewall | default(true) - + - name: Launch Stream and its necessary services - include_tasks: launching.yml \ No newline at end of file + ansible.builtin.include_tasks: launching.yml diff --git a/tasks/nginx_certs.yml b/tasks/nginx_certs.yml index 6480bd6..3bca30a 100644 --- a/tasks/nginx_certs.yml +++ b/tasks/nginx_certs.yml @@ -6,7 +6,8 @@ state: directory mode: '0755' -- fail: +- name: Nginx_certs - Fail if custom certs not provided + ansible.builtin.fail: msg: "You should set all of the stream_csr, stream_pem, stream_key, and stream_chain_pem variables" when: stream_generate_default_cert == false and (stream_csr == "" or stream_pem == "" or stream_key == "") @@ -43,8 +44,8 @@ ansible.builtin.copy: src: "{{ [stream_cert_directory_path, 'stream.pem'] | path_join }}" dest: "{{ [stream_cert_directory_path, 'stream-chain.pem'] | path_join }}" - force: yes - remote_src: yes + force: true + remote_src: true register: chain_pem_created notify: "Stream - Handlers - Restart nginx services" when: stream_generate_default_cert == true diff --git a/templates/etc_default_stream.j2 b/templates/etc_default_stream.j2 index a361de4..30fe759 100644 --- a/templates/etc_default_stream.j2 +++ b/templates/etc_default_stream.j2 @@ -20,6 +20,10 @@ STREAM_HOSTNAME={{ stream_hostname_node }} STREAM_DEFAULT_SSV_KEY={{ stream_default_ssv_key }} STREAM_EVENT_SEAL_SECRET={{ stream_event_seal_secret }} STREAM_ENFORCE_X509_AUTH=false +{% if stream_version is version('2.1.7', '>=') %} +STREAM_TINK_MASTER_KEY_URI={{ stream_tink_master_key_uri }} +STREAM_TINK_CREDENTIALS_PATH={{ stream_tink_credentials_path }} +{% endif %} # Akka nodes for HA # All HA nodes in hostname:port format, comma separated diff --git a/templates/hosts_allowed.j2 b/templates/hosts_allowed.j2 index 51e7c85..3f6df55 100644 --- a/templates/hosts_allowed.j2 +++ b/templates/hosts_allowed.j2 @@ -2,10 +2,9 @@ {% for node in stream_nodes %} play.filters.hosts.allowed.{{ start_index + loop.index0 }}="{{ node.hostname }}" {% endfor %} -{% if stream_use_load_balancer | default(false) %} -play.filters.hosts.allowed.{{ start_index + stream_nodes | length }}="{{ stream_load_balancer_hostname }}" - -{% endif %} +{% for host in stream_extra_allowed_hosts | default([]) %} +play.filters.hosts.allowed.{{ start_index + stream_nodes | length + loop.index0 }}="{{ host }}" +{% endfor %} # Akka cluster split-brain resolver configuration {% if stream_nodes | length > 1 %} diff --git a/tests/uninstall.yml b/tests/uninstall.yml index 4e65ebc..d4a3330 100644 --- a/tests/uninstall.yml +++ b/tests/uninstall.yml @@ -1,7 +1,7 @@ --- - name: Uninstall Stream - hosts: stream_cluster + hosts: stream_nodes become: true gather_facts: true @@ -10,19 +10,19 @@ ansible.builtin.systemd: name: stream state: stopped - ignore_errors: yes + ignore_errors: true - name: Stop nginx service ansible.builtin.systemd: name: nginx state: stopped - ignore_errors: yes + ignore_errors: true - name: Stop postfix service ansible.builtin.systemd: name: postfix state: stopped - ignore_errors: yes + ignore_errors: true - name: Remove Stream RPM ansible.builtin.yum: @@ -59,11 +59,11 @@ - name: Remove firewall rules ansible.posix.firewalld: port: "{{ item }}" - permanent: yes + permanent: true state: disabled immediate: yes loop: - 443/tcp - 7626/tcp - 17355/tcp - ignore_errors: yes \ No newline at end of file + ignore_errors: true \ No newline at end of file From 31bdf243612fa750a19dac74a27479f895041f4d Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Wed, 1 Apr 2026 09:48:40 +0200 Subject: [PATCH 3/4] fix: apply PR review feedback on secret keys, SELinux and uninstall script - Use the proper Ansible module for SELinux instead of running a command - Secret keys are now required, the role fails if they are missing - Fixed the uninstall script to properly clean up all files and services - Updated inventory group name from stream_cluster to stream_nodes --- defaults/main/default_values.yml | 3 ++- defaults/main/mandatory_vars.yml | 3 +-- tasks/config.yml | 13 ---------- tasks/enterprise_linux_OS_install.yml | 6 +++-- tasks/main.yml | 25 ++++++++----------- tests/inventory.py | 6 ++--- tests/uninstall.yml | 35 +++++++++++++++++++-------- 7 files changed, 45 insertions(+), 46 deletions(-) diff --git a/defaults/main/default_values.yml b/defaults/main/default_values.yml index 253f298..12a7e48 100644 --- a/defaults/main/default_values.yml +++ b/defaults/main/default_values.yml @@ -60,4 +60,5 @@ stream_dependencies_list: - epel-release - "{{ stream_web_server }}" - postfix - - firewalld \ No newline at end of file + - firewalld + - python3-libsemanage \ No newline at end of file diff --git a/defaults/main/mandatory_vars.yml b/defaults/main/mandatory_vars.yml index 3998a5a..f3d5c6d 100644 --- a/defaults/main/mandatory_vars.yml +++ b/defaults/main/mandatory_vars.yml @@ -6,8 +6,7 @@ stream_configure_firewall: true # Stream secret keys - random string with special chars (@!#), 128 characters -# Set via environment variables for idempotent deployments -# If not set, a random value is auto-generated ONCE per play on the first node and distributed +# These MUST be set via environment variables - the role will fail if any of them are missing stream_play_http_secret_key: "{{ lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') }}" stream_default_ssv_key: "{{ lookup('env', 'STREAM_DEFAULT_SSV_KEY') }}" stream_event_seal_secret: "{{ lookup('env', 'STREAM_EVENT_SEAL_SECRET') }}" diff --git a/tasks/config.yml b/tasks/config.yml index b43a19f..58299f6 100644 --- a/tasks/config.yml +++ b/tasks/config.yml @@ -176,7 +176,6 @@ when: - inventory_hostname == stream_nodes[0].hostname - stream_nodes | length > 1 - run_once: true - name: Config - Distribute the file to other nodes ansible.builtin.copy: @@ -214,15 +213,3 @@ group: stream mode: '0640' -- name: Config - Display generated credentials (save these if auto-generated) - ansible.builtin.debug: - msg: - - "========== STREAM GENERATED CREDENTIALS ==========" - - "These values were auto-generated. Save them and re-run with env vars set for idempotency." - - "" - - "STREAM_PLAY_HTTP_SECRET_KEY : {{ stream_play_http_secret_key }}" - - "STREAM_DEFAULT_SSV_KEY : {{ stream_default_ssv_key }}" - - "STREAM_EVENT_SEAL_SECRET : {{ stream_event_seal_secret }}" - - "===================================================" - run_once: true - when: lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' \ No newline at end of file diff --git a/tasks/enterprise_linux_OS_install.yml b/tasks/enterprise_linux_OS_install.yml index 86ec2a6..37ce9ec 100644 --- a/tasks/enterprise_linux_OS_install.yml +++ b/tasks/enterprise_linux_OS_install.yml @@ -12,9 +12,11 @@ changed_when: false - name: Enterprise_linux_OS_install - Set SELinux to permissive mode if it is enforcing - ansible.builtin.command: setsebool -P httpd_can_network_connect 1 + ansible.posix.seboolean: + name: httpd_can_network_connect + state: true + persistent: true when: selinux_status['stdout'] == "Enforcing" - changed_when: false - name: Enterprise_linux_OS_install - Check if Stream RPM is already installed ansible.builtin.stat: diff --git a/tasks/main.yml b/tasks/main.yml index ccd2458..460d041 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,21 +1,16 @@ --- -- name: Main - Generate secret keys once on first node (if not set via env vars) - ansible.builtin.set_fact: - stream_play_http_secret_key: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" - stream_default_ssv_key: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" - stream_event_seal_secret: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,@,!,# length=128') }}" +- name: Main - Fail if mandatory secret keys are not set + ansible.builtin.fail: + msg: | + Mandatory variable '{{ item.name }}' is not set. + Please set the '{{ item.env }}' environment variable before running this role. + when: item.value == '' + loop: + - { name: 'stream_play_http_secret_key', env: 'STREAM_PLAY_HTTP_SECRET_KEY', value: "{{ stream_play_http_secret_key }}" } + - { name: 'stream_default_ssv_key', env: 'STREAM_DEFAULT_SSV_KEY', value: "{{ stream_default_ssv_key }}" } + - { name: 'stream_event_seal_secret', env: 'STREAM_EVENT_SEAL_SECRET', value: "{{ stream_event_seal_secret }}" } run_once: true - when: lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' - -- name: Main - Distribute generated secret keys to all nodes - ansible.builtin.set_fact: - stream_play_http_secret_key: "{{ hostvars[stream_nodes[0].hostname]['stream_play_http_secret_key'] }}" - stream_default_ssv_key: "{{ hostvars[stream_nodes[0].hostname]['stream_default_ssv_key'] }}" - stream_event_seal_secret: "{{ hostvars[stream_nodes[0].hostname]['stream_event_seal_secret'] }}" - when: - - lookup('env', 'STREAM_PLAY_HTTP_SECRET_KEY') == '' - - inventory_hostname != stream_nodes[0].hostname - name: Main - Set stream_hostname_node to current inventory hostname ansible.builtin.set_fact: diff --git a/tests/inventory.py b/tests/inventory.py index f033212..d2d6a6d 100755 --- a/tests/inventory.py +++ b/tests/inventory.py @@ -53,9 +53,9 @@ def load_vars_file(): def build_inventory(vars_data): inventory = { 'all': { - 'children': ['stream_cluster'] + 'children': ['stream_nodes'] }, - 'stream_cluster': { + 'stream_nodes': { 'hosts': [], 'vars': { 'ansible_user': 'root', @@ -97,7 +97,7 @@ def build_inventory(vars_data): print(f"Warning: Node {hostname} missing IP address", file=sys.stderr) continue - inventory['stream_cluster']['hosts'].append(hostname) + inventory['stream_nodes']['hosts'].append(hostname) inventory['_meta']['hostvars'][hostname] = { 'ansible_host': ip } diff --git a/tests/uninstall.yml b/tests/uninstall.yml index d4a3330..862058d 100644 --- a/tests/uninstall.yml +++ b/tests/uninstall.yml @@ -28,40 +28,55 @@ ansible.builtin.yum: name: stream state: absent - + + - name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + + - name: Reset failed state of stream service + ansible.builtin.command: systemctl reset-failed stream + ignore_errors: true + changed_when: false + - name: Remove Tinkey RPM ansible.builtin.yum: name: tinkey state: absent - + ignore_errors: true + + - name: Remove Mongosh RPM + ansible.builtin.yum: + name: mongodb-mongosh + state: absent + ignore_errors: true + - name: Remove Stream directories ansible.builtin.file: path: "{{ item }}" state: absent loop: - /opt/stream + - /opt/evertrust - /etc/default/stream + - /etc/default/stream.rpmsave - /etc/nginx/conf.d/stream.conf - /etc/nginx/ssl/stream.key - /etc/nginx/ssl/stream.pem - /etc/nginx/ssl/stream.csr - /etc/nginx/ssl/stream-chain.pem - + - name: Remove Stream entries from /etc/hosts - ansible.builtin.lineinfile: + ansible.builtin.blockinfile: path: /etc/hosts - regexp: "{{ item }}" + marker: "# {mark} ANSIBLE MANAGED BLOCK - Stream Cluster" state: absent - loop: - - "stream-node" - - "mongodb" - + - name: Remove firewall rules ansible.posix.firewalld: port: "{{ item }}" permanent: true state: disabled - immediate: yes + immediate: true loop: - 443/tcp - 7626/tcp From 381c21ef25512f2d81c6a7052d2d281d80a04a05 Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Wed, 22 Apr 2026 09:49:47 +0200 Subject: [PATCH 4/4] change to stream node instead of stream cluster --- tests/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/deploy.yml b/tests/deploy.yml index 8e5da03..d7e7ec2 100644 --- a/tests/deploy.yml +++ b/tests/deploy.yml @@ -1,7 +1,7 @@ --- - name: Deploy Stream in HA - hosts: stream_cluster + hosts: stream_nodes become: true gather_facts: true vars_files: