Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions plugins/module_utils/dependency_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
# Copyright (c) IBM Corporation 2025
# Licensed under the Apache License, Version 2.0

from __future__ import absolute_import, division, print_function
import re
import subprocess
import sys
import json

__metaclass__ = type

# ------------------------------
# Compatibility Matrix
# ------------------------------
COMPATIBILITY_MATRIX = [
{"zoau_version": "1.3.5", "python_version": "3.12", "galaxy_core_version": "1.12.0", "zos_range": "2.5-3.1"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be easier to use min_zos_version and max_zos_version instead of zos_range and then having to do a string manipulation, same for things that allow ranges like python.

Instead of galaxy_core_version I would use collection_version since we know this is our collection.

Suggested change
{"zoau_version": "1.3.5", "python_version": "3.12", "galaxy_core_version": "1.12.0", "zos_range": "2.5-3.1"},
{"zoau_version": "1.3.5", "python_version": "3.12", "collection_version": "1.12.0", "min_zos_version": "2.5", "max_zos_version": "3.1"},

{"zoau_version": "1.3.5.1", "python_version": "3.10", "galaxy_core_version": "1.12.0", "zos_range": "2.5-3.1"},
{"zoau_version": "1.3.6.0", "python_version": "3.10", "galaxy_core_version": "1.10.1", "zos_range": "2.5-3.1"},
{"zoau_version": "1.4.0", "python_version": "3.11", "galaxy_core_version": "1.11.0", "zos_range": "2.5-3.1"},
{"zoau_version": "1.4.0", "python_version": "3.11", "galaxy_core_version": "1.12.0", "zos_range": "2.5-3.1"},
]

# ------------------------------
# Helpers
# ------------------------------
def run_command(module, cmd):
rc, out, err = module.run_command(cmd)
if rc != 0:
module.fail_json(msg=f"Command failed: {cmd}", stdout=out, stderr=err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be problematic, rather than catching the error here I think we should do a try in the main dependency validation, and throw an Exception from there saying something like "The dependencies could not be fetched correctly. " and maybe add some details about the dependencias that were fetched successfully

return out.strip()

# ------------------------------
# Version Fetchers
# ------------------------------
def get_zoau_version(module):
output = run_command(module, "zoaversion")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to run zoaversion command, as we spoke before better to use the zoau_dependency_checker functionality.

try:
    from zoautil_py import ZOAU_API_VERSION
except Exception:
    ZOAU_API_VERSION = "1.4.0"

match = re.search(r'v(\d+\.\d+\.\d+(?:\.\d+)?)', output)
return match.group(1) if match else None

def get_python_version_info():
return sys.version_info.major, sys.version_info.minor

def get_python_version():
return f"{sys.version_info.major}.{sys.version_info.minor}.0"

def get_zos_version(module):
output = run_command(module, "zinfo -t sys -j")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, better to use ZOAU's zsystem python API https://www.ibm.com/docs/en/zoau/1.3.x?topic=apis-zsystem we try to avoid using commands when an API is available unless there is a bug in the python API

try:
data = json.loads(output)
sys_info = data.get("data", {}).get("sys_info", {})
version = sys_info.get("product_version")
release = sys_info.get("product_release")
if version and release:
return f"{int(version)}.{int(release)}"
except json.JSONDecodeError:
match_ver = re.search(r'"product_version":"(\d+)"', output)
match_rel = re.search(r'"product_release":"(\d+)"', output)
if match_ver and match_rel:
return f"{int(match_ver.group(1))}.{int(match_rel.group(1))}"
return None

def get_galaxy_core_version():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are in the managed node we cannot access the collection version using ansible-galaxy command, this is what we were talking about the other day, is it possible to get through an import by fetching the valye from meta/ibm_zos_core?

try:
result = subprocess.run(
["ansible-galaxy", "collection", "list", "ibm.ibm_zos_core"],
capture_output=True,
text=True,
check=True
)
match = re.search(r'ibm\.ibm_zos_core\s+([0-9.]+)', result.stdout)
if match:
return match.group(1)
except subprocess.CalledProcessError:
return None
return None

# ------------------------------
# Validation
# ------------------------------
def validate_dependencies(module):
zoau_version = get_zoau_version(module)
python_major, python_minor = get_python_version_info()
python_version_str = get_python_version()
zos_version = get_zos_version(module)
galaxy_core_version = get_galaxy_core_version()

if not all([zoau_version, zos_version, galaxy_core_version]):
module.fail_json(msg=" Missing one or more required versions (ZOAU, Python, z/OS, Galaxy Core).")

try:
current_zos = float(zos_version)
except (ValueError, TypeError):
module.fail_json(msg=f"Unable to parse z/OS version: {zos_version}")

for row in COMPATIBILITY_MATRIX:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of iterating over the compatibility matrix we can use the core collection version as the key to find the correct row and only check the compatibility for the current version

if "zos_range" in row:
try:
zos_min, zos_max = map(float, row["zos_range"].split("-"))
except ValueError:
continue
if not (zos_min <= current_zos <= zos_max):
continue

row_py_major, row_py_minor = map(int, row["python_version"].split("."))
if (row["zoau_version"].strip() == zoau_version.strip() and
row_py_major == python_major and
row_py_minor == python_minor and
str(row["galaxy_core_version"]).strip() == str(galaxy_core_version).strip()):
module.exit_json(
msg=f" Dependency compatibility check passed: "
f"ZOAU {zoau_version}, Python {python_version_str}, z/OS {zos_version}, Galaxy Core {galaxy_core_version}"
)


module.fail_json(
msg=(
f" Incompatible configuration detected:\n"
f" ZOAU: {zoau_version}\n"
f" Python: {python_version_str}\n"
f" z/OS: {zos_version}\n"
f" Galaxy Core: {galaxy_core_version}"
)
)
43 changes: 43 additions & 0 deletions tests/unit/test_dependency_checker_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import dependency_checker as dc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability, would recommend this to be kept as dependency_checker


class FakeModule:
def run_command(self, cmd):
return 0, "", ""

def fail_json(self, **kwargs):
raise Exception(kwargs.get("msg", "fail_json called"))

def exit_json(self, **kwargs):
# Instead of StopIteration, store the result so we can inspect it
self.result = kwargs
raise SystemExit # This simulates Ansible stopping execution

def test_validate_dependencies_success(monkeypatch):
# Monkeypatch fetchers to match the compatibility matrix exactly
monkeypatch.setattr(dc, "get_zoau_version", lambda mod: "1.3.5")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know about this fixture, please add a comment explaining what this does so that next person who sees this doesn't have to spend much time researching why it was implemented this way and not with simple dictionaries as inputs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

monkeypatch.setattr(dc, "get_zos_version", lambda mod: "2.5")
monkeypatch.setattr(dc, "get_python_version_info", lambda: (3, 12))
monkeypatch.setattr(dc, "get_python_version", lambda: "3.12.0")
monkeypatch.setattr(dc, "get_galaxy_core_version", lambda: "1.12.0")

mod = FakeModule()
try:
dc.validate_dependencies(mod)
except SystemExit:
# Capture the message from exit_json
print("Message:", mod.result["msg"])
assert "Dependency compatibility check passed" in mod.result["msg"]

def test_validate_dependencies_failure(monkeypatch):
monkeypatch.setattr(dc, "get_zoau_version", lambda mod: "9.9.9")
monkeypatch.setattr(dc, "get_zos_version", lambda mod: "9.9")
monkeypatch.setattr(dc, "get_python_version_info", lambda: (9, 9))
monkeypatch.setattr(dc, "get_python_version", lambda: "9.9.9")
monkeypatch.setattr(dc, "get_galaxy_core_version", lambda: "9.9.9")

mod = FakeModule()
with pytest.raises(Exception) as exc:
dc.validate_dependencies(mod)
print("Failure message:", str(exc.value))
assert "Incompatible configuration detected" in str(exc.value)