Provision reusable Ansible execution environment

This commit is contained in:
2025-10-12 14:13:06 -06:00
parent 8cae44539c
commit 1e9912efd2
7 changed files with 367 additions and 41 deletions

View File

@@ -229,18 +229,55 @@ def detect_agent_os():
return "Unknown"
def _ansible_ee_version():
try:
root = _project_root()
meta_path = os.path.join(root, 'Ansible_EE', 'metadata.json')
if os.path.isfile(meta_path):
try:
with open(meta_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
if isinstance(data, dict):
for key in ('version', 'ansible_ee_ver', 'ansible_ee_version'):
value = data.get(key)
if isinstance(value, (str, int, float)):
text = str(value).strip()
if text:
return text
except Exception:
pass
version_txt = os.path.join(root, 'Ansible_EE', 'version.txt')
if os.path.isfile(version_txt):
try:
raw = Path(version_txt).read_text(encoding='utf-8')
if raw:
text = raw.splitlines()[0].strip()
if text:
return text
except Exception:
pass
except Exception:
pass
return ''
def collect_summary(CONFIG):
try:
hostname = socket.gethostname()
return {
summary = {
'hostname': hostname,
'os': detect_agent_os(),
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
'domain': os.environ.get('USERDOMAIN') or '',
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
}
summary['ansible_ee_ver'] = _ansible_ee_version()
return summary
except Exception:
return {'hostname': socket.gethostname()}
return {
'hostname': socket.gethostname(),
'ansible_ee_ver': _ansible_ee_version(),
}
def _project_root():

View File

@@ -8,6 +8,7 @@ import json
import socket
import subprocess
import base64
from pathlib import Path
from typing import Optional
try:
@@ -53,6 +54,62 @@ def _project_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
def _ansible_ee_root():
candidates = []
try:
candidates.append(os.path.join(_project_root(), 'Agent', 'Ansible_EE'))
except Exception:
pass
try:
candidates.append(os.path.join(_agent_root(), 'Ansible_EE'))
except Exception:
pass
for path in candidates:
if path and os.path.isdir(path):
return path
return None
def _ansible_ee_metadata():
root = _ansible_ee_root()
if not root:
return {}
meta_path = os.path.join(root, 'metadata.json')
if not os.path.isfile(meta_path):
return {}
try:
with open(meta_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
if isinstance(data, dict):
return data
except Exception:
return {}
return {}
def _ansible_ee_version():
meta = _ansible_ee_metadata()
for key in ('version', 'ansible_ee_ver', 'ansible_ee_version'):
value = meta.get(key) if isinstance(meta, dict) else None
if isinstance(value, (str, int, float)):
text = str(value).strip()
if text:
return text
root = _ansible_ee_root()
if root:
txt_path = os.path.join(root, 'version.txt')
if os.path.isfile(txt_path):
try:
raw = Path(txt_path).read_text(encoding='utf-8')
if raw:
text = raw.splitlines()[0].strip()
if text:
return text
except Exception:
pass
return ''
def _decode_base64_text(value):
if not isinstance(value, str):
return None
@@ -99,11 +156,22 @@ def _agent_root():
def _scripts_bin():
# Return the venv Scripts (Windows) or bin (POSIX) path adjacent to Borealis
candidates = []
ee_root = _ansible_ee_root()
if ee_root:
candidates.extend(
[
os.path.join(ee_root, 'Scripts'),
os.path.join(ee_root, 'bin'),
]
)
agent_root = _agent_root()
candidates = [
os.path.join(agent_root, 'Scripts'), # Windows venv
os.path.join(agent_root, 'bin'), # POSIX venv
]
candidates.extend(
[
os.path.join(agent_root, 'Scripts'), # Windows venv
os.path.join(agent_root, 'bin'), # POSIX venv
]
)
for base in candidates:
if os.path.isdir(base):
return base
@@ -137,10 +205,19 @@ def _collections_dir():
return base
def _venv_python():
ee_root = _ansible_ee_root()
if ee_root:
ee_candidates = [
os.path.join(ee_root, 'Scripts', 'python.exe'),
os.path.join(ee_root, 'Scripts', 'python3.exe'),
os.path.join(ee_root, 'bin', 'python3'),
os.path.join(ee_root, 'bin', 'python'),
]
for cand in ee_candidates:
if os.path.isfile(cand):
return cand
try:
sdir = _scripts_bin()
if not sdir:
return None
sdir = os.path.join(_agent_root(), 'Scripts' if os.name == 'nt' else 'bin')
cand = os.path.join(sdir, 'python.exe' if os.name == 'nt' else 'python3')
return cand if os.path.isfile(cand) else None
except Exception:
@@ -185,40 +262,55 @@ class Role:
def _bootstrap_ansible_sync(self) -> bool:
missing = self._detect_missing_modules()
if not missing:
return True
specs = sorted({spec for spec in missing.values() if spec})
python_exe = _venv_python() or sys.executable
if not python_exe:
self._ansible_log('[bootstrap] python executable not found for pip install', error=True)
if missing:
self._ansible_log(
f"[bootstrap] required agent modules missing: {', '.join(sorted(missing.keys()))}",
error=True,
)
return False
cmd = [python_exe, '-m', 'pip', 'install', '--disable-pip-version-check'] + specs
self._ansible_log(f"[bootstrap] ensuring modules via pip: {', '.join(specs)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=900)
except Exception as exc:
self._ansible_log(f"[bootstrap] pip install exception: {exc}", error=True)
ee_root = _ansible_ee_root()
if not ee_root or not os.path.isdir(ee_root):
self._ansible_log('[bootstrap] execution environment folder Agent/Ansible_EE not found', error=True)
return False
if result.returncode != 0:
err_tail = (result.stderr or '').strip()
if len(err_tail) > 500:
err_tail = err_tail[-500:]
self._ansible_log(f"[bootstrap] pip install failed rc={result.returncode} err={err_tail}", error=True)
scripts_dir = _scripts_bin()
exe_name = 'ansible-playbook.exe' if os.name == 'nt' else 'ansible-playbook'
playbook_path = None
if scripts_dir:
candidate = os.path.join(scripts_dir, exe_name)
if os.path.isfile(candidate):
playbook_path = candidate
if not playbook_path:
self._ansible_log('[bootstrap] ansible-playbook executable missing in execution environment', error=True)
return False
remaining = self._detect_missing_modules()
if remaining:
self._ansible_log(f"[bootstrap] modules still missing after install: {', '.join(sorted(remaining.keys()))}", error=True)
python_exe = _venv_python()
if not python_exe or not os.path.isfile(python_exe):
self._ansible_log('[bootstrap] execution environment python not found', error=True)
return False
try:
marker = self._bootstrap_marker_path()
payload = {
'timestamp': int(time.time()),
'modules': specs,
}
with open(marker, 'w', encoding='utf-8') as fh:
json.dump(payload, fh)
except Exception:
pass
env_path = os.environ.get('PATH') or ''
bin_dir = os.path.dirname(playbook_path)
if bin_dir:
segments = [seg for seg in env_path.split(os.pathsep) if seg]
if bin_dir not in segments:
os.environ['PATH'] = bin_dir + (os.pathsep + env_path if env_path else '')
collections_dir = os.path.join(ee_root, 'collections')
if os.path.isdir(collections_dir):
existing = os.environ.get('ANSIBLE_COLLECTIONS_PATHS') or ''
paths = [seg for seg in existing.split(os.pathsep) if seg]
if collections_dir not in paths:
os.environ['ANSIBLE_COLLECTIONS_PATHS'] = (
collections_dir if not existing else collections_dir + os.pathsep + existing
)
os.environ['BOREALIS_ANSIBLE_EE_ROOT'] = ee_root
os.environ['BOREALIS_ANSIBLE_EE_PYTHON'] = python_exe
version = _ansible_ee_version()
if version:
self._ansible_log(f"[bootstrap] using execution environment version {version}")
return True
async def _ensure_ansible_ready(self) -> bool:

View File

@@ -0,0 +1,8 @@
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/Agent/ansible-ee-requirements.txt
# Ansible execution environment dependencies
ansible-core==2.16.7
ansible-runner==2.3.5
pywinrm==0.4.3
requests-credssp==2.0.0
requests-ntlm==1.2.0
pypsrp==0.8.1

View File

@@ -0,0 +1,2 @@
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/Agent/ansible-ee-version.txt
1.0.0

View File

@@ -3514,6 +3514,7 @@ _DEVICE_TABLE_COLUMNS = [
"operating_system",
"uptime",
"agent_id",
"ansible_ee_ver",
"connection_type",
"connection_endpoint",
]
@@ -3603,6 +3604,7 @@ def _assemble_device_snapshot(record: Dict[str, Any]) -> Dict[str, Any]:
"created": _ts_to_human(created_ts),
"connection_type": _clean_device_str(record.get("connection_type")) or "",
"connection_endpoint": _clean_device_str(record.get("connection_endpoint")) or "",
"ansible_ee_ver": _clean_device_str(record.get("ansible_ee_ver")) or "",
}
details = {
@@ -3747,6 +3749,7 @@ def _extract_device_columns(details: Dict[str, Any]) -> Dict[str, Any]:
)
payload["uptime"] = _coerce_int(uptime_value)
payload["agent_id"] = _clean_device_str(summary.get("agent_id"))
payload["ansible_ee_ver"] = _clean_device_str(summary.get("ansible_ee_ver"))
payload["connection_type"] = _clean_device_str(
summary.get("connection_type")
or summary.get("remote_type")
@@ -3815,9 +3818,10 @@ def _device_upsert(
operating_system,
uptime,
agent_id,
ansible_ee_ver,
connection_type,
connection_endpoint
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(hostname) DO UPDATE SET
description=excluded.description,
created_at=COALESCE({DEVICE_TABLE}.created_at, excluded.created_at),
@@ -3838,6 +3842,7 @@ def _device_upsert(
operating_system=COALESCE(NULLIF(excluded.operating_system, ''), {DEVICE_TABLE}.operating_system),
uptime=COALESCE(NULLIF(excluded.uptime, 0), {DEVICE_TABLE}.uptime),
agent_id=COALESCE(NULLIF(excluded.agent_id, ''), {DEVICE_TABLE}.agent_id),
ansible_ee_ver=COALESCE(NULLIF(excluded.ansible_ee_ver, ''), {DEVICE_TABLE}.ansible_ee_ver),
connection_type=COALESCE(NULLIF(excluded.connection_type, ''), {DEVICE_TABLE}.connection_type),
connection_endpoint=COALESCE(NULLIF(excluded.connection_endpoint, ''), {DEVICE_TABLE}.connection_endpoint)
"""
@@ -3863,6 +3868,7 @@ def _device_upsert(
column_values.get("operating_system"),
column_values.get("uptime"),
column_values.get("agent_id"),
column_values.get("ansible_ee_ver"),
column_values.get("connection_type"),
column_values.get("connection_endpoint"),
]
@@ -4160,7 +4166,10 @@ def init_db():
last_user TEXT,
operating_system TEXT,
uptime INTEGER,
agent_id TEXT
agent_id TEXT,
ansible_ee_ver TEXT,
connection_type TEXT,
connection_endpoint TEXT
)
"""
)
@@ -4193,6 +4202,7 @@ def init_db():
_ensure_column("operating_system", "TEXT")
_ensure_column("uptime", "INTEGER")
_ensure_column("agent_id", "TEXT")
_ensure_column("ansible_ee_ver", "TEXT")
_ensure_column("connection_type", "TEXT")
_ensure_column("connection_endpoint", "TEXT")
@@ -4274,6 +4284,7 @@ def init_db():
operating_system TEXT,
uptime INTEGER,
agent_id TEXT,
ansible_ee_ver TEXT,
connection_type TEXT,
connection_endpoint TEXT
)