This is an automated email from the ASF dual-hosted git repository.

ssulav pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone-installer.git


The following commit(s) were added to refs/heads/master by this push:
     new d8aa6ee  HDDS-14521: Read hosts from file, add verbose flag and option 
to override python interpreter
d8aa6ee is described below

commit d8aa6ee5857f08a97f4e496dff7313550c927e45
Author: Soumitra Sulav <[email protected]>
AuthorDate: Thu Jan 29 06:14:36 2026 +0530

    HDDS-14521: Read hosts from file, add verbose flag and option to override 
python interpreter
---
 .gitignore                         |   3 +-
 README.md                          | 119 +++++++++++++++++++++++++++++++++++--
 ansible.cfg                        |   3 +-
 ansible.cfg => hosts.txt.example   |  44 +++++++-------
 inventories/dev/group_vars/all.yml |   2 -
 ozone_installer.py                 |  97 ++++++++++++++++++++++++------
 playbooks/cluster.yml              |  61 +++++++++++++++----
 roles/cleanup/tasks/main.yml       |   4 +-
 roles/java/tasks/main.yml          |   1 +
 roles/ozone_fetch/tasks/main.yml   |   2 +
 roles/ozone_ui/tasks/main.yml      |  26 ++++----
 11 files changed, 284 insertions(+), 78 deletions(-)

diff --git a/.gitignore b/.gitignore
index 7859f36..37ccc66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 logs/**
-*.pyc
\ No newline at end of file
+*.pyc
+hosts.txt
\ No newline at end of file
diff --git a/README.md b/README.md
index ea393b8..6a3e710 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,8 @@ Ports and service behavior follow Ozone defaults; consult the 
official documenta
 
 ## Software Requirements
 
-- Controller: Python 3.10–3.12 (prefer 3.11) and pip
+### Controller node requirements
+- Python 3.10–3.12 (prefer 3.11) and pip
 - Ansible Community 10.x (ansible-core 2.17.x)
 - Python packages (installed via `requirements.txt`):
   - `ansible-core==2.17.*`
@@ -43,10 +44,55 @@ Ports and service behavior follow Ozone defaults; consult 
the official documenta
     - RHEL/CentOS/Rocky: `sudo yum install -y sshpass` or `sudo dnf install -y 
sshpass`
     - SUSE: `sudo zypper in -y sshpass`
 
-### Controller node requirements
-- Can be local or remote.
-- Must be on the same network as the target hosts.
-- Requires SSH access (key or password).
+### Managed node requirements (target hosts)
+- **Python 3.7 or higher** (required by Ansible 2.17)
+- SSH server enabled
+- Sudo access (if using `--use-sudo`)
+
+**⚠️ Known Issue: CentOS 8 / RHEL 8 with Python 3.6**
+
+On CentOS 8/RHEL 8, the system's `dnf` package manager may use Python 3.6 
(`/usr/libexec/platform-python`), and the DNF Python module (`python3-dnf`) is 
only available for Python 3.6, not for Python 3.9+. 
+
+The installer works around this by using direct shell commands (e.g., 
`/usr/libexec/platform-python /usr/bin/dnf install`) for package installation 
rather than Ansible's package module.
+
+#### Python Version Requirements by OS
+
+| Operating System | Default Python | Available Versions | Installation 
Command |
+|-----------------|----------------|-------------------|---------------------|
+| RHEL 9+ / Rocky 9+ | Python 3.9+ ✅ | 3.11, 3.9 | `sudo yum install -y 
python3.11` |
+| RHEL 8 / Rocky 8 / CentOS 8 | Python 3.6 ❌ | 3.9, 3.8 (python39, python38) | 
`sudo yum install -y python39` |
+| CentOS 7 | Python 3.6 ❌ | 3.6 only | Must use EPEL or SCL for newer versions 
|
+| Ubuntu 20.04+ | Python 3.8+ ✅ | 3.11, 3.10, 3.9, 3.8 | `sudo apt-get install 
-y python3.11` |
+| Debian 11+ | Python 3.9+ ✅ | 3.11, 3.9 | `sudo apt-get install -y 
python3.11` |
+
+**Important**: If your managed nodes have Python 3.6 or older, you must 
upgrade:
+
+```bash
+# CentOS 8 / RHEL 8 / Rocky 8 (most common)
+sudo yum install -y python39
+# Verify: /usr/bin/python3.9 --version
+
+# RHEL 9+ / Rocky 9+
+sudo yum install -y python3.11
+# Verify: /usr/bin/python3.11 --version
+
+# Ubuntu / Debian
+sudo apt-get update && sudo apt-get install -y python3.9
+# Verify: /usr/bin/python3.9 --version
+```
+
+**Then specify the Python interpreter when running the installer:**
+```bash
+# For CentOS 8 / RHEL 8
+python3 ozone_installer.py -H hosts -v 2.0.0 --python-interpreter 
/usr/bin/python3.9
+
+# For RHEL 9+
+python3 ozone_installer.py -H hosts -v 2.0.0 --python-interpreter 
/usr/bin/python3.11
+```
+
+### Network and access requirements
+- Controller must be on the same network as the target hosts
+- Controller requires SSH access (key or password) to all target hosts
 
 ### Run on the controller node
 ```bash
@@ -70,17 +116,53 @@ python3 ozone_installer.py -H host1.domain -v 2.0.0
 # HA upstream (3+ hosts) - mode auto-detected
 python3 ozone_installer.py -H "host{1..3}.domain" -v 2.0.0
 
+# Using host file instead of CLI (one host per line, supports user@host:port 
format)
+python3 ozone_installer.py -F hosts.txt -v 2.0.0
+
 # Local snapshot build
 python3 ozone_installer.py -H host1 -v local --local-path 
/path/to/share/ozone-2.1.0-SNAPSHOT
 
 # Cleanup and reinstall
 python3 ozone_installer.py --clean -H "host{1..3}.domain" -v 2.0.0
 
+# Specify Python interpreter (if managed nodes have Python 3.6 or need 
specific version)
+python3 ozone_installer.py -H "host{1..3}.domain" -v 2.0.0 
--python-interpreter /usr/bin/python3.9
+
+# Verbose mode for debugging (passes -vvv to Ansible)
+python3 ozone_installer.py -H "host{1..3}.domain" -v 2.0.0 --verbose
+# OR with short flag
+python3 ozone_installer.py -H "host{1..3}.domain" -v 2.0.0 -V
+
 # Notes on cleanup
 # - During a normal install, you'll be asked whether to cleanup an existing 
install (if present). Default is No.
 # - Use --clean to cleanup without prompting before reinstall.
 ```
 
+### Python interpreter configuration
+
+The installer requires Python 3.7+ on managed nodes (Ansible 2.17 requirement).
+
+**You must configure the Python interpreter if your managed nodes don't have 
Python 3.7+ as the default `/usr/bin/python3`.**
+
+**Via CLI (recommended):**
+```bash
+python3 ozone_installer.py -H hosts -v 2.0.0 --python-interpreter 
/usr/bin/python3.9
+```
+
+**Via group_vars (for static inventory):**
+```yaml
+# inventories/dev/group_vars/all.yml
+ansible_python_interpreter: /usr/bin/python3.9  # or /usr/bin/python39 for 
CentOS 8
+```
+
+**Via dynamic inventory:**
+Add `ansible_python_interpreter=/usr/bin/python3.9` to each host line in your 
inventory.
+
+### Host file format
+
+When using `-F/--host-file`, create a text file with one host per line. See 
`hosts.txt.example` for a complete example.
+
+
 ### Interactive prompts and version selection
 - The installer uses `click` for interactive prompts when available (TTY).
 - Version selection shows a numbered list; you can select by number, type a 
specific version, or `local`.
@@ -100,6 +182,33 @@ ANSIBLE_CONFIG=ansible.cfg ansible-playbook -i 
inventories/dev/hosts.ini playboo
   --start-at-task "$(head -n1 logs/last_failed_task.txt)"
 ```
 
+### Debugging and Troubleshooting
+
+**Verbose Mode:** For detailed Ansible output (useful for debugging failures):
+
+```bash
+# Python wrapper - passes -vvv to Ansible
+python3 ozone_installer.py -H hosts -v 2.0.0 --verbose
+# OR with short flag
+python3 ozone_installer.py -H hosts -v 2.0.0 -V
+```
+
+The verbose flag provides:
+- Detailed task execution information
+- Variable values at each step
+- Full error tracebacks
+- SSH connection details
+- Module arguments and return values
+
+**Check Logs:**
+```bash
+# View the latest installer log
+tail -f logs/ansible-*.log
+
+# Check for task failures
+grep -i "fatal\|error" logs/ansible-*.log
+```
+
 2) Direct Ansible (run playbooks yourself)
 
 ```bash
diff --git a/ansible.cfg b/ansible.cfg
index 44beba1..4378dc5 100644
--- a/ansible.cfg
+++ b/ansible.cfg
@@ -19,7 +19,8 @@ stdout_callback = default
 retry_files_enabled = False
 gathering = smart
 forks = 20
-strategy = free
+strategy = linear
+; strategy = free for concurrent tasks
 timeout = 30
 roles_path = roles
 log_path = logs/ansible.log
diff --git a/ansible.cfg b/hosts.txt.example
similarity index 53%
copy from ansible.cfg
copy to hosts.txt.example
index 44beba1..dcd13a8 100644
--- a/ansible.cfg
+++ b/hosts.txt.example
@@ -13,29 +13,29 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-[defaults]
-inventory = inventories/dev/hosts.ini
-stdout_callback = default
-retry_files_enabled = False
-gathering = smart
-forks = 20
-strategy = free
-timeout = 30
-roles_path = roles
-log_path = logs/ansible.log
-bin_ansible_callbacks = True
-callback_plugins = callback_plugins
-callbacks_enabled = timer, profile_tasks, last_failed ; for execution time 
profiling and resume hints
-deprecation_warnings = False
-host_key_checking = False
-remote_tmp = /tmp/.ansible-${USER}
+# Example host file for ozone_installer.py
+# Usage: python3 ozone_installer.py -F hosts.txt.example -v 2.0.0
+#
+# Format: One host per line
+# Supports: user@host:port
+# Comments and empty lines are ignored
+
+# Simple hostname
+# host1.example.com
+
+# With SSH user
+# [email protected]
 
-[privilege_escalation]
-become = True
-become_method = sudo
+# With custom SSH port
+# host3.example.com:2222
 
-[ssh_connection]
-pipelining = True
-ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o 
StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
+# Combined user and port
+# [email protected]:2222
 
+# For HA deployment (3+ hosts recommended)
+# host1.example.com
+# host2.example.com
+# host3.example.com
 
+# For Non-HA deployment (single host)
+# host1.example.com
diff --git a/inventories/dev/group_vars/all.yml 
b/inventories/dev/group_vars/all.yml
index aae7150..31f7b9e 100644
--- a/inventories/dev/group_vars/all.yml
+++ b/inventories/dev/group_vars/all.yml
@@ -52,5 +52,3 @@ ssh_private_key_path: ""      # optional path to private key 
to copy for cluster
 # Markers for profile management
 JAVA_MARKER: "Apache Ozone Installer Java Home"
 ENV_MARKER: "Apache Ozone Installer Env"
-
-
diff --git a/ozone_installer.py b/ozone_installer.py
index da0bd33..93c0084 100755
--- a/ozone_installer.py
+++ b/ozone_installer.py
@@ -79,6 +79,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
         description="Ozone Ansible Installer (Python trigger) - mirrors bash 
installer flags"
     )
     p.add_argument("-H", "--host", help="Target host(s). Non-HA: host. HA: 
comma-separated or brace expansion host{1..n}")
+    p.add_argument("-F", "--host-file", help="File containing target hosts 
(one per line, supports @, : for user/port)")
     p.add_argument("-m", "--auth-method", choices=["password", "key"], 
default=None)
     p.add_argument("-p", "--password", help="SSH password (for 
--auth-method=password)")
     p.add_argument("-k", "--keyfile", help="SSH private key file (for 
--auth-method=key)")
@@ -98,6 +99,8 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
     # Local extras
     p.add_argument("--local-path", help="Path to local Ozone build (contains 
bin/ozone)")
     p.add_argument("--dl-url", help="Upstream download base URL")
+    p.add_argument("--python-interpreter", help="Python interpreter for 
managed nodes (e.g., /usr/bin/python3.9). If not specified, Ansible will 
auto-detect.")
+    p.add_argument("--verbose", "-V", action="store_true", help="Verbose 
output (passes -vvv to Ansible for detailed debugging)")
     p.add_argument("--yes", action="store_true", help="Non-interactive; accept 
defaults for missing values")
     p.add_argument("-R", "--resume", action="store_true", help="Resume play at 
last failed task (if available)")
     return p.parse_args(argv)
@@ -293,12 +296,44 @@ def parse_hosts(hosts_raw: Optional[str]) -> List[dict]:
             out.append({"host": host, "user": user, "port": port})
     return out
 
+def read_hosts_from_file(filepath: str) -> Optional[str]:
+    """
+    Reads hosts from a file (one host per line).
+    Lines starting with # are treated as comments and ignored.
+    Empty lines are ignored.
+    Supports same format as CLI: user@host:port
+    Returns comma-separated host string suitable for parse_hosts().
+    """
+    logger = get_logger()
+    try:
+        path = Path(filepath)
+        if not path.exists():
+            logger.error(f"Host file not found: {filepath}")
+            return None
+        hosts = []
+        with path.open('r') as f:
+            for line in f:
+                line = line.strip()
+                # Skip empty lines and comments
+                if not line or line.startswith('#'):
+                    continue
+                hosts.append(line)
+        if hosts:
+            logger.info(f"Read {len(hosts)} host(s) from {filepath}")
+            return ','.join(hosts)
+        else:
+            logger.error(f"No valid hosts found in {filepath}")
+            return None
+    except Exception as e:
+        logger.error(f"Error reading host file {filepath}: {e}")
+        return None
+
 def auto_cluster_mode(hosts: List[dict], forced: Optional[str] = None) -> str:
     if forced in ("non-ha", "ha"):
         return forced
     return "ha" if len(hosts) >= 3 else "non-ha"
 
-def build_inventory(hosts: List[dict], ssh_user: Optional[str] = None, 
keyfile: Optional[str] = None, password: Optional[str] = None, cluster_mode: 
str = "non-ha") -> str:
+def build_inventory(hosts: List[dict], ssh_user: Optional[str] = None, 
keyfile: Optional[str] = None, password: Optional[str] = None, cluster_mode: 
str = "non-ha", python_interpreter: Optional[str] = None) -> str:
     """
     Returns INI inventory text for our groups: [om], [scm], [datanodes], 
[recon], [s3g]
     """
@@ -309,7 +344,7 @@ def build_inventory(hosts: List[dict], ssh_user: 
Optional[str] = None, keyfile:
         h = hosts[0]
         return _render_inv_groups(
             om=[h], scm=[h], dn=hosts, recon=[h], s3g=[h],
-            ssh_user=ssh_user, keyfile=keyfile, password=password
+            ssh_user=ssh_user, keyfile=keyfile, password=password, 
python_interpreter=python_interpreter
         )
     # HA: first 3 go to OM and SCM; all to datanodes; recon is first if present
     om = hosts[:3] if len(hosts) >= 3 else hosts
@@ -318,9 +353,9 @@ def build_inventory(hosts: List[dict], ssh_user: 
Optional[str] = None, keyfile:
     recon = [hosts[0]]
     s3g = [hosts[0]]
     return _render_inv_groups(om=om, scm=scm, dn=dn, recon=recon, s3g=s3g,
-                              ssh_user=ssh_user, keyfile=keyfile, 
password=password)
+                              ssh_user=ssh_user, keyfile=keyfile, 
password=password, python_interpreter=python_interpreter)
 
-def _render_inv_groups(om: List[dict], scm: List[dict], dn: List[dict], recon: 
List[dict], s3g: List[dict], ssh_user: Optional[str] = None, keyfile: 
Optional[str] = None, password: Optional[str] = None) -> str:
+def _render_inv_groups(om: List[dict], scm: List[dict], dn: List[dict], recon: 
List[dict], s3g: List[dict], ssh_user: Optional[str] = None, keyfile: 
Optional[str] = None, password: Optional[str] = None, python_interpreter: 
Optional[str] = None) -> str:
     def hostline(hd):
         parts = [hd["host"]]
         if ssh_user or hd.get("user"):
@@ -331,6 +366,8 @@ def _render_inv_groups(om: List[dict], scm: List[dict], dn: 
List[dict], recon: L
             
parts.append(f"ansible_ssh_private_key_file={shlex.quote(str(keyfile))}")
         if password:
             parts.append(f"ansible_password={shlex.quote(password)}")
+        if python_interpreter:
+            parts.append(f"ansible_python_interpreter={python_interpreter}")
         return " ".join(parts)
 
     sections = []
@@ -347,13 +384,15 @@ def _render_inv_groups(om: List[dict], scm: List[dict], 
dn: List[dict], recon: L
     sections.append("\n")
     return "\n".join(sections)
 
-def run_playbook(playbook: Path, inventory_path: Path, extra_vars_path: Path, 
ask_pass: bool = False, become: bool = True, start_at_task: Optional[str] = 
None, tags: Optional[List[str]] = None) -> int:
+def run_playbook(playbook: Path, inventory_path: Path, extra_vars_path: Path, 
ask_pass: bool = False, become: bool = True, start_at_task: Optional[str] = 
None, tags: Optional[List[str]] = None, verbose: bool = False) -> int:
     cmd = [
         "ansible-playbook",
         "-i", str(inventory_path),
         str(playbook),
         "-e", f"@{extra_vars_path}",
     ]
+    if verbose:
+        cmd.append("-vvv")
     if ask_pass:
         cmd.append("-k")
     if become:
@@ -398,7 +437,15 @@ def main(argv: List[str]) -> int:
 
     # Gather inputs interactively where missing
     hosts_raw_default = (last_cfg.get("hosts_raw") if last_cfg else None)
-    hosts_raw = args.host or hosts_raw_default or prompt("Target host(s) 
[non-ha: host | HA: h1,h2,h3 or brace expansion]", default="", yes_mode=yes)
+    # Check if hosts are provided via file first, then CLI, then default/prompt
+    if args.host_file:
+        hosts_raw = read_hosts_from_file(args.host_file)
+        if not hosts_raw:
+            logger = get_logger()
+            logger.error(f"Error: Could not read hosts from file: 
{args.host_file}")
+            return 2
+    else:
+        hosts_raw = args.host or hosts_raw_default or prompt("Target host(s) 
[non-ha: host | HA: h1,h2,h3 or brace expansion]", default="", yes_mode=yes)
     hosts = parse_hosts(hosts_raw) if hosts_raw else []
     # Initialize per-run logger as soon as we have hosts_raw
     try:
@@ -414,7 +461,7 @@ def main(argv: List[str]) -> int:
         logger.info(f"Logging to: {run_log_path} (fallback)")
 
     if not hosts:
-        logger.error("Error: No hosts provided (-H/--host).")
+        logger.error("Error: No hosts provided (-H/--host or -F/--host-file).")
         return 2
     # Decide HA vs Non-HA with user input; default depends on host count
     resume_cluster_mode = (last_cfg.get("cluster_mode") if last_cfg else None)
@@ -424,7 +471,7 @@ def main(argv: List[str]) -> int:
         cluster_mode = resume_cluster_mode
     else:
         default_mode = "ha" if len(hosts) >= 3 else "non-ha"
-        selected = prompt("Deployment type (ha|non-ha)", default=default_mode, 
yes_mode=yes)
+        selected = prompt("Deployment type (option: ha or non-ha)", 
default=default_mode, yes_mode=yes)
         cluster_mode = (selected or default_mode).strip().lower()
         if cluster_mode not in ("ha", "non-ha"):
             cluster_mode = default_mode
@@ -446,19 +493,19 @@ def main(argv: List[str]) -> int:
             ozone_version = prompt("Ozone version (e.g., 2.1.0 | local)", 
default=DEFAULTS["ozone_version"], yes_mode=yes)
     jdk_major = args.jdk_version if args.jdk_version is not None else 
((last_cfg.get("jdk_major") if last_cfg else None))
     if jdk_major is None:
-        _jdk_val = prompt("JDK major (17|21)", 
default=str(DEFAULTS["jdk_major"]), yes_mode=yes)
+        _jdk_val = prompt("JDK major (option: 17 or 21)", 
default=str(DEFAULTS["jdk_major"]), yes_mode=yes)
         try:
             jdk_major = int(str(_jdk_val)) if _jdk_val is not None else 
DEFAULTS["jdk_major"]
         except Exception:
             jdk_major = DEFAULTS["jdk_major"]
     install_base = args.install_dir or (last_cfg.get("install_base") if 
last_cfg else None) \
-        or prompt("Install base directory (binaries and configs; e.g., 
/opt/ozone)", default=DEFAULTS["install_base"], yes_mode=yes)
+        or prompt("Install directory (base directory path to store ozone 
binaries, configs and logs)", default=DEFAULTS["install_base"], yes_mode=yes)
     data_base = args.data_dir or (last_cfg.get("data_base") if last_cfg else 
None) \
-        or prompt("Data base directory (metadata and DN data; e.g., 
/data/ozone)", default=DEFAULTS["data_base"], yes_mode=yes)
+        or prompt("Data directory (base directory path to store ozone metadata 
and data)", default=DEFAULTS["data_base"], yes_mode=yes)
 
     # Auth (before service user/group)
     auth_method = args.auth_method or (last_cfg.get("auth_method") if last_cfg 
else None) \
-        or prompt("Auth method (key|password)", default="password", 
yes_mode=yes)
+        or prompt("SSH authentication method (option: key or password)", 
default="password", yes_mode=yes)
     if auth_method not in ("key", "password"):
         auth_method = "password"
     ssh_user = args.ssh_user or (last_cfg.get("ssh_user") if last_cfg else 
None) \
@@ -475,9 +522,9 @@ def main(argv: List[str]) -> int:
     elif auth_method == "key":
         password = None
     service_user = args.service_user or (last_cfg.get("service_user") if 
last_cfg else None) \
-        or prompt("Service user", default=DEFAULTS["service_user"], 
yes_mode=yes)
+        or prompt("Service user name ", default=DEFAULTS["service_user"], 
yes_mode=yes)
     service_group = args.service_group or (last_cfg.get("service_group") if 
last_cfg else None) \
-        or prompt("Service group", default=DEFAULTS["service_group"], 
yes_mode=yes)
+        or prompt("Service group name", default=DEFAULTS["service_group"], 
yes_mode=yes)
     dl_url = args.dl_url or (last_cfg.get("dl_url") if last_cfg else None) or 
DEFAULTS["dl_url"]
     start_after_install = (args.start or (last_cfg.get("start_after_install") 
if last_cfg else None)
                            or DEFAULTS["start_after_install"])
@@ -527,10 +574,10 @@ def main(argv: List[str]) -> int:
         ("Cluster mode", cluster_mode),
         ("Ozone version", str(ozone_version)),
         ("JDK major", str(jdk_major)),
-        ("Install base", str(install_base)),
-        ("Data base", str(data_base)),
+        ("Install directory", str(install_base)),
+        ("Data directory", str(data_base)),
         ("SSH user", str(ssh_user)),
-        ("Auth method", str(auth_method))
+        ("SSH auth method", str(auth_method))
     ]
     if keyfile:
         summary_rows.append(("Key file", str(keyfile)))
@@ -544,9 +591,16 @@ def main(argv: List[str]) -> int:
         logger.info("Aborted by user.")
         return 1
 
+    # Python interpreter (optional, auto-detected if not provided)
+    python_interpreter = args.python_interpreter or 
(last_cfg.get("python_interpreter") if last_cfg else None)
+    if python_interpreter:
+        logger.info(f"Using Python interpreter: {python_interpreter}")
+    else:
+        logger.info("Python interpreter will be auto-detected by playbook")
+    
     # Prepare dynamic inventory and extra-vars
     inventory_text = build_inventory(hosts, ssh_user=ssh_user, 
keyfile=keyfile, password=password,
-                                     cluster_mode=cluster_mode)
+                                     cluster_mode=cluster_mode, 
python_interpreter=python_interpreter)
     # Decide cleanup behavior up-front (so we can pass it into the unified 
play)
     do_cleanup = False
     if args.clean:
@@ -572,6 +626,10 @@ def main(argv: List[str]) -> int:
         "ENV_MARKER": DEFAULTS["ENV_MARKER"],
         "controller_logs_dir": str(LOGS_DIR),
     }
+    # Add Python interpreter if explicitly specified by user
+    if python_interpreter:
+        extra_vars["ansible_python_interpreter"] = python_interpreter
+        extra_vars["ansible_python_interpreter_discovery"] = "explicit"
     if ozone_version and ozone_version.lower() == "local":
         extra_vars.update({
             "local_shared_path": local_shared_path or "",
@@ -615,6 +673,7 @@ def main(argv: List[str]) -> int:
                 "use_sudo": bool(use_sudo),
                 "local_shared_path": local_shared_path or "",
                 "local_ozone_dirname": local_oz_dir or "",
+                "python_interpreter": python_interpreter or "",
             }, indent=2), encoding="utf-8")
         except Exception:
             # Fall back to temp files if persisting fails
@@ -639,7 +698,7 @@ def main(argv: List[str]) -> int:
                             use_tags = [role_name]
                 except Exception:
                     start_at = None
-        rc = run_playbook(playbook, inv_path, ev_path, ask_pass=ask_pass, 
become=True, start_at_task=start_at, tags=use_tags)
+        rc = run_playbook(playbook, inv_path, ev_path, ask_pass=ask_pass, 
become=True, start_at_task=start_at, tags=use_tags, verbose=args.verbose)
         if rc != 0:
             return rc
 
diff --git a/playbooks/cluster.yml b/playbooks/cluster.yml
index 9e6da01..bef59f4 100644
--- a/playbooks/cluster.yml
+++ b/playbooks/cluster.yml
@@ -22,17 +22,6 @@
     cluster_mode: "{{ cluster_mode | default('non-ha') }}"
     ha_enabled: "{{ cluster_mode == 'ha' }}"
   pre_tasks:
-    - name: "Pre-install: Ensure python3 present"
-      raw: |
-        if command -v apt-get >/dev/null 2>&1; then sudo -n apt-get update -y 
&& sudo -n apt-get install -y python3 || true;
-        elif command -v dnf >/dev/null 2>&1; then sudo -n dnf install -y 
python3 || true;
-        elif command -v yum >/dev/null 2>&1; then sudo -n yum install -y 
python3 || true;
-        elif command -v zypper >/dev/null 2>&1; then sudo -n zypper 
--non-interactive in -y python3 || true;
-        fi
-      args:
-        executable: /bin/bash
-      changed_when: false
-      failed_when: false
 
     - name: "Pre-install: Gather facts"
       setup:
@@ -72,3 +61,53 @@
       tags: ["ozone_ui"]
     - role: ozone_smoke
       tags: ["ozone_smoke"]
+
+  post_tasks:
+    - name: "Build UI endpoints display lines"
+      set_fact:
+        ui_display_lines: >-
+          {{
+            [
+              '',
+              '==========================================',
+              '   Ozone Cluster UI Endpoints',
+              '==========================================',
+              '',
+              'OM UI:'
+            ] +
+            (endpoint_urls.om | map('regex_replace', '^', '  - ') | list) +
+            ['', 'SCM UI:'] +
+            (endpoint_urls.scm | map('regex_replace', '^', '  - ') | list) +
+            ['', 'Recon UI:'] +
+            ((endpoint_urls.recon | length > 0) | ternary(
+              endpoint_urls.recon | map('regex_replace', '^', '  - ') | list,
+              ['  - Not configured']
+            )) +
+            ['', 'S3 Gateway (HTTP):'] +
+            ((endpoint_urls.s3g_http | length > 0) | ternary(
+              endpoint_urls.s3g_http | map('regex_replace', '^', '  - ') | 
list,
+              ['  - Not configured']
+            )) +
+            ['', 'S3 Gateway (Admin):'] +
+            ((endpoint_urls.s3g_admin | length > 0) | ternary(
+              endpoint_urls.s3g_admin | map('regex_replace', '^', '  - ') | 
list,
+              ['  - Not configured']
+            )) +
+            [
+              '',
+              '==========================================',
+              'UI endpoints also saved to: ' ~ (controller_logs_dir | 
default('logs')) ~ '/endpoint_urls.json',
+              '==========================================',
+              ''
+            ]
+          }}
+      run_once: true
+      when: endpoint_urls is defined
+      tags: ["always"]
+
+    - name: "Display UI Endpoints Summary"
+      debug:
+        msg: "{{ ui_display_lines }}"
+      run_once: true
+      when: endpoint_urls is defined
+      tags: ["always"]
diff --git a/roles/cleanup/tasks/main.yml b/roles/cleanup/tasks/main.yml
index 7180288..5d41c8f 100644
--- a/roles/cleanup/tasks/main.yml
+++ b/roles/cleanup/tasks/main.yml
@@ -52,13 +52,13 @@
       loop_control:
         label: "{{ item }}"
 
-    - name: "Remove install base"
+    - name: "Remove install directory"
       file:
         path: "{{ install_base }}"
         state: absent
       become: true
 
-    - name: "Remove data base"
+    - name: "Remove data directory"
       file:
         path: "{{ data_base }}"
         state: absent
diff --git a/roles/java/tasks/main.yml b/roles/java/tasks/main.yml
index d5f07b2..8d95654 100644
--- a/roles/java/tasks/main.yml
+++ b/roles/java/tasks/main.yml
@@ -59,6 +59,7 @@
   become: false
   vars:
     last_vars_path: "{{ playbook_dir }}/../logs/last_vars.json"
+    ansible_python_interpreter: "{{ ansible_playbook_python }}"
   block:
     - name: "last_vars.json | Read"
       slurp:
diff --git a/roles/ozone_fetch/tasks/main.yml b/roles/ozone_fetch/tasks/main.yml
index 40588c1..b4f2b24 100644
--- a/roles/ozone_fetch/tasks/main.yml
+++ b/roles/ozone_fetch/tasks/main.yml
@@ -84,6 +84,7 @@
   become: false
   vars:
     ansible_become: false
+    ansible_python_interpreter: "{{ ansible_playbook_python }}"
   command:
     argv:
       - tar
@@ -122,6 +123,7 @@
   become: false
   vars:
     ansible_become: false
+    ansible_python_interpreter: "{{ ansible_playbook_python }}"
   file:
     path: "/tmp/{{ local_ozone_dirname }}.tar.gz"
     state: absent
diff --git a/roles/ozone_ui/tasks/main.yml b/roles/ozone_ui/tasks/main.yml
index f17b2af..692550b 100644
--- a/roles/ozone_ui/tasks/main.yml
+++ b/roles/ozone_ui/tasks/main.yml
@@ -14,34 +14,30 @@
 # limitations under the License.
 
 ## Print and export service UI endpoints
-- name: "Compute service UI URLs"
+- name: "Get host lists for UI endpoints"
   set_fact:
     _om_hosts_ui: "{{ groups.get('om', []) | list }}"
     _scm_hosts_ui: "{{ groups.get('scm', []) | list }}"
     _recon_hosts_ui: "{{ groups.get('recon', []) | list }}"
     _s3g_hosts_ui: "{{ groups.get('s3g', []) | list }}"
-    ui_urls:
+
+- name: "Compute service UI URLs"
+  set_fact:
+    endpoint_urls:
       om: "{{ _om_hosts_ui | map('regex_replace','^(.*)$','http://\\1:9874') | 
list }}"
       scm: "{{ _scm_hosts_ui | map('regex_replace','^(.*)$','http://\\1:9876') 
| list }}"
       recon: "{{ (_recon_hosts_ui | length > 0) | ternary(['http://' + 
_recon_hosts_ui[0] + ':9888'], []) }}"
       s3g_http: "{{ _s3g_hosts_ui | 
map('regex_replace','^(.*)$','http://\\1:9878') | list }}"
       s3g_admin: "{{ _s3g_hosts_ui | 
map('regex_replace','^(.*)$','http://\\1:19878') | list }}"
 
-- name: "Service UI Endpoints"
-  debug:
-    msg:
-      - "OM UI: {{ ui_urls.om }}"
-      - "SCM UI: {{ ui_urls.scm }}"
-      - "Recon UI: {{ ui_urls.recon }}"
-      - "S3G HTTP: {{ ui_urls.s3g_http }}"
-      - "S3G Admin: {{ ui_urls.s3g_admin }}"
-  run_once: true
-
 - name: "Export UI endpoints to controller logs directory"
   copy:
-    content: "{{ ui_urls | to_nice_json }}"
-    dest: "{{ controller_logs_dir }}/ui_urls.json"
+    content: "{{ endpoint_urls | to_nice_json }}"
+    dest: "{{ controller_logs_dir }}/endpoint_urls.json"
     mode: "0644"
   delegate_to: localhost
+  become: false
   run_once: true
-  when: controller_logs_dir is defined
\ No newline at end of file
+  when: controller_logs_dir is defined
+  vars:
+    ansible_python_interpreter: "{{ ansible_playbook_python }}"
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to