mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:21:57 -06:00
Merge pull request #91 from bunny-lab-io:codex/evaluate-alternative-solutions-for-ansible-playbooks
Provision reusable Ansible execution environment and track EE version
This commit is contained in:
227
Borealis.ps1
227
Borealis.ps1
@@ -256,6 +256,19 @@ $nodeExe = Join-Path $depsRoot 'NodeJS\node.exe'
|
||||
$sevenZipExe = Join-Path $depsRoot "7zip\7z.exe"
|
||||
$npmCmd = Join-Path (Split-Path $nodeExe) 'npm.cmd'
|
||||
$npxCmd = Join-Path (Split-Path $nodeExe) 'npx.cmd'
|
||||
$ansibleEeRequirementsPath = Join-Path $scriptDir 'Data\Agent\ansible-ee-requirements.txt'
|
||||
$ansibleEeVersionFile = Join-Path $scriptDir 'Data\Agent\ansible-ee-version.txt'
|
||||
$script:AnsibleExecutionEnvironmentVersion = '1.0.0'
|
||||
if (Test-Path $ansibleEeVersionFile -PathType Leaf) {
|
||||
try {
|
||||
$rawVersion = (Get-Content -Path $ansibleEeVersionFile -Raw -ErrorAction Stop)
|
||||
if ($rawVersion) {
|
||||
$script:AnsibleExecutionEnvironmentVersion = ($rawVersion.Split("`n")[0]).Trim()
|
||||
}
|
||||
} catch {
|
||||
# Leave default version value
|
||||
}
|
||||
}
|
||||
$node7zUrl = "https://nodejs.org/dist/v23.11.0/node-v23.11.0-win-x64.7z"
|
||||
$nodeInstallDir = Join-Path $depsRoot "NodeJS"
|
||||
$node7zPath = Join-Path $depsRoot "node-v23.11.0-win-x64.7z"
|
||||
@@ -449,6 +462,211 @@ function Install_Agent_Dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-AnsibleExecutionEnvironment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectRoot,
|
||||
|
||||
[string]$PythonBootstrapExe,
|
||||
|
||||
[string]$RequirementsPath,
|
||||
[string]$ExpectedVersion = '1.0.0',
|
||||
[string]$LogName = 'Install.log'
|
||||
)
|
||||
|
||||
$pythonBootstrap = $PythonBootstrapExe
|
||||
$bundleCandidate = Join-Path $ProjectRoot 'Dependencies\Python\python.exe'
|
||||
if ([string]::IsNullOrWhiteSpace($pythonBootstrap)) {
|
||||
$pythonBootstrap = $bundleCandidate
|
||||
}
|
||||
|
||||
if (-not (Test-Path $pythonBootstrap -PathType Leaf)) {
|
||||
if ((-not [string]::IsNullOrWhiteSpace($PythonBootstrapExe)) -and ($PythonBootstrapExe -ne $pythonBootstrap)) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Provided Python bootstrap path '$PythonBootstrapExe' was not found."
|
||||
}
|
||||
|
||||
if (Test-Path $bundleCandidate -PathType Leaf) {
|
||||
$pythonBootstrap = $bundleCandidate
|
||||
} else {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Unable to locate bundled Python bootstrap executable at $bundleCandidate."
|
||||
throw "Bundled Python executable not found for Ansible execution environment provisioning."
|
||||
}
|
||||
}
|
||||
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Using Python bootstrap at $pythonBootstrap"
|
||||
|
||||
$eeRoot = Join-Path $ProjectRoot 'Agent\Ansible_EE'
|
||||
$metadataPath = Join-Path $eeRoot 'metadata.json'
|
||||
$versionTxtPath = Join-Path $eeRoot 'version.txt'
|
||||
|
||||
$requirementsHash = ''
|
||||
if ($RequirementsPath -and (Test-Path $RequirementsPath -PathType Leaf)) {
|
||||
try {
|
||||
$requirementsHash = (Get-FileHash -Path $RequirementsPath -Algorithm SHA256).Hash
|
||||
} catch {
|
||||
$requirementsHash = ''
|
||||
}
|
||||
}
|
||||
|
||||
$currentVersion = ''
|
||||
$currentHash = ''
|
||||
if (Test-Path $metadataPath -PathType Leaf) {
|
||||
try {
|
||||
$metaRaw = Get-Content -Path $metadataPath -Raw -ErrorAction Stop
|
||||
if ($metaRaw) {
|
||||
$meta = $metaRaw | ConvertFrom-Json -ErrorAction Stop
|
||||
if ($meta.version) {
|
||||
$currentVersion = ($meta.version).ToString().Trim()
|
||||
}
|
||||
if ($meta.requirements_hash) {
|
||||
$currentHash = ($meta.requirements_hash).ToString().Trim()
|
||||
} elseif ($meta.requirements_sha256) {
|
||||
$currentHash = ($meta.requirements_sha256).ToString().Trim()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$currentVersion = ''
|
||||
$currentHash = ''
|
||||
}
|
||||
}
|
||||
|
||||
$pythonCandidates = @(
|
||||
(Join-Path $eeRoot 'Scripts\python.exe')
|
||||
(Join-Path $eeRoot 'Scripts\python3.exe')
|
||||
(Join-Path $eeRoot 'bin\python3')
|
||||
(Join-Path $eeRoot 'bin\python')
|
||||
)
|
||||
|
||||
$existingPython = $pythonCandidates | Where-Object { Test-Path $_ -PathType Leaf } | Select-Object -First 1
|
||||
|
||||
$expectedVersionNorm = $ExpectedVersion
|
||||
if ([string]::IsNullOrWhiteSpace($expectedVersionNorm)) {
|
||||
$expectedVersionNorm = '1.0.0'
|
||||
}
|
||||
$expectedVersionNorm = $expectedVersionNorm.Trim()
|
||||
$isUpToDate = $false
|
||||
if ($existingPython -and $currentVersion -and ($currentVersion -eq $expectedVersionNorm)) {
|
||||
if (-not $requirementsHash -or ($currentHash -and $currentHash -eq $requirementsHash)) {
|
||||
$isUpToDate = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($isUpToDate) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Existing execution environment is up-to-date (version $currentVersion)."
|
||||
return
|
||||
}
|
||||
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Provisioning execution environment version $expectedVersionNorm."
|
||||
|
||||
if (Test-Path $eeRoot) {
|
||||
try { Remove-Item -Path $eeRoot -Recurse -Force -ErrorAction Stop } catch {}
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $eeRoot | Out-Null
|
||||
|
||||
& $pythonBootstrap -m venv $eeRoot | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] python -m venv failed with exit code $LASTEXITCODE"
|
||||
throw "Failed to create Ansible execution environment virtual environment."
|
||||
}
|
||||
|
||||
$pythonExe = $pythonCandidates | Where-Object { Test-Path $_ -PathType Leaf } | Select-Object -First 1
|
||||
if (-not $pythonExe) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Unable to locate python executable inside execution environment."
|
||||
throw "Ansible execution environment python executable missing after provisioning."
|
||||
}
|
||||
|
||||
& $pythonExe -m pip install --upgrade pip setuptools wheel --disable-pip-version-check | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] pip bootstrap failed with exit code $LASTEXITCODE"
|
||||
throw "Failed to bootstrap pip inside the Ansible execution environment."
|
||||
}
|
||||
|
||||
if ($RequirementsPath -and (Test-Path $RequirementsPath -PathType Leaf)) {
|
||||
& $pythonExe -m pip install --disable-pip-version-check -r $RequirementsPath | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] pip install -r requirements failed with exit code $LASTEXITCODE"
|
||||
throw "Failed to install Ansible execution environment requirements."
|
||||
}
|
||||
} else {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Requirements file not found; skipping dependency installation."
|
||||
}
|
||||
|
||||
$metadata = [ordered]@{
|
||||
version = $expectedVersionNorm
|
||||
created_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
python = $pythonExe
|
||||
bootstrap_python = $pythonBootstrap
|
||||
}
|
||||
if ($requirementsHash) {
|
||||
$metadata['requirements_hash'] = $requirementsHash
|
||||
}
|
||||
|
||||
$supportDir = Join-Path $eeRoot 'support'
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $supportDir | Out-Null
|
||||
} catch {}
|
||||
|
||||
$fcntlStubPath = Join-Path $supportDir 'fcntl.py'
|
||||
$fcntlStub = @'
|
||||
"""Compat shim for POSIX-only fcntl module.
|
||||
|
||||
Generated by Borealis to allow Ansible tooling to run on Windows hosts
|
||||
where the standard library fcntl module is unavailable. The stub provides
|
||||
symbol constants and no-op function implementations so imports succeed.
|
||||
"""
|
||||
|
||||
LOCK_SH = 1
|
||||
LOCK_EX = 2
|
||||
LOCK_UN = 8
|
||||
LOCK_NB = 4
|
||||
|
||||
F_DUPFD = 0
|
||||
F_GETFD = 1
|
||||
F_SETFD = 2
|
||||
F_GETFL = 3
|
||||
F_SETFL = 4
|
||||
|
||||
FD_CLOEXEC = 1
|
||||
|
||||
def ioctl(*_args, **_kwargs):
|
||||
return 0
|
||||
|
||||
|
||||
def fcntl(*_args, **_kwargs):
|
||||
return 0
|
||||
|
||||
|
||||
def flock(*_args, **_kwargs):
|
||||
return 0
|
||||
|
||||
|
||||
def lockf(*_args, **_kwargs):
|
||||
return 0
|
||||
'@
|
||||
|
||||
try {
|
||||
if (-not (Test-Path (Join-Path $supportDir '__init__.py') -PathType Leaf)) {
|
||||
Set-Content -Path (Join-Path $supportDir '__init__.py') -Value '' -Encoding UTF8NoBOM
|
||||
}
|
||||
Set-Content -Path $fcntlStubPath -Value $fcntlStub -Encoding UTF8NoBOM
|
||||
} catch {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Failed to seed Windows fcntl compatibility shim: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
try {
|
||||
$metadata | ConvertTo-Json -Depth 5 | Set-Content -Path $metadataPath -Encoding UTF8NoBOM
|
||||
} catch {
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Failed to persist metadata.json: $($_.Exception.Message)"
|
||||
throw "Unable to persist Ansible execution environment metadata."
|
||||
}
|
||||
|
||||
try {
|
||||
Set-Content -Path $versionTxtPath -Value $expectedVersionNorm -Encoding UTF8NoBOM
|
||||
} catch {}
|
||||
|
||||
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Execution environment ready at $eeRoot"
|
||||
}
|
||||
|
||||
function Ensure-AgentTasks {
|
||||
param([string]$ScriptRoot)
|
||||
$pyw = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
|
||||
@@ -569,6 +787,15 @@ function InstallOrUpdate-BorealisAgent {
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Provision Ansible Execution Environment" {
|
||||
Ensure-AnsibleExecutionEnvironment `
|
||||
-ProjectRoot $scriptDir `
|
||||
-PythonBootstrapExe $pythonExe `
|
||||
-RequirementsPath $ansibleEeRequirementsPath `
|
||||
-ExpectedVersion $script:AnsibleExecutionEnvironmentVersion `
|
||||
-LogName 'Install.log'
|
||||
}
|
||||
|
||||
Run-Step "Configure Agent Settings" {
|
||||
$settingsDir = Join-Path $scriptDir 'Agent\Borealis\Settings'
|
||||
$oldSettingsDir = Join-Path $scriptDir 'Agent\Settings'
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,17 +156,38 @@ 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
|
||||
return None
|
||||
|
||||
|
||||
def _ee_support_path():
|
||||
root = _ansible_ee_root()
|
||||
if not root:
|
||||
return None
|
||||
support = os.path.join(root, 'support')
|
||||
if os.path.isdir(support):
|
||||
return support
|
||||
return None
|
||||
|
||||
|
||||
def _ansible_playbook_cmd():
|
||||
exe = 'ansible-playbook.exe' if os.name == 'nt' else 'ansible-playbook'
|
||||
sdir = _scripts_bin()
|
||||
@@ -137,10 +215,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:
|
||||
@@ -175,50 +262,108 @@ class Role:
|
||||
return os.path.join(tmp_dir, 'ansible_bootstrap.json')
|
||||
|
||||
def _detect_missing_modules(self) -> dict:
|
||||
"""Return any required modules that the execution environment lacks."""
|
||||
|
||||
missing = {}
|
||||
for module, spec in REQUIRED_MODULES.items():
|
||||
try:
|
||||
__import__(module)
|
||||
except Exception:
|
||||
missing[module] = spec
|
||||
|
||||
python_exe = _venv_python()
|
||||
if not python_exe or not os.path.isfile(python_exe):
|
||||
missing['python'] = 'execution-environment python missing'
|
||||
return missing
|
||||
|
||||
module_names = sorted(REQUIRED_MODULES.keys())
|
||||
probe = (
|
||||
"import importlib.util, sys;"
|
||||
f"mods={module_names!r};"
|
||||
"missing=[m for m in mods if importlib.util.find_spec(m) is None];"
|
||||
"sys.stdout.write('\\n'.join(missing))"
|
||||
)
|
||||
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[python_exe, '-c', probe],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except Exception:
|
||||
for name in module_names:
|
||||
missing[name] = REQUIRED_MODULES[name]
|
||||
return missing
|
||||
|
||||
stdout = (completed.stdout or '').strip()
|
||||
if stdout:
|
||||
for name in stdout.splitlines():
|
||||
mod = name.strip()
|
||||
if mod and mod in REQUIRED_MODULES:
|
||||
missing[mod] = REQUIRED_MODULES[mod]
|
||||
|
||||
return missing
|
||||
|
||||
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}")
|
||||
|
||||
support_dir = _ee_support_path()
|
||||
if support_dir:
|
||||
existing_pp = os.environ.get('PYTHONPATH') or ''
|
||||
paths = [seg for seg in existing_pp.split(os.pathsep) if seg]
|
||||
if support_dir not in paths:
|
||||
os.environ['PYTHONPATH'] = (
|
||||
support_dir
|
||||
if not existing_pp
|
||||
else support_dir + os.pathsep + existing_pp
|
||||
)
|
||||
os.environ['BOREALIS_ANSIBLE_EE_SUPPORT'] = support_dir
|
||||
|
||||
return True
|
||||
|
||||
async def _ensure_ansible_ready(self) -> bool:
|
||||
|
||||
8
Data/Agent/ansible-ee-requirements.txt
Normal file
8
Data/Agent/ansible-ee-requirements.txt
Normal 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
|
||||
2
Data/Agent/ansible-ee-version.txt
Normal file
2
Data/Agent/ansible-ee-version.txt
Normal 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
|
||||
@@ -429,6 +429,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
const [credentialLoading, setCredentialLoading] = useState(false);
|
||||
const [credentialError, setCredentialError] = useState("");
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
||||
const [useSvcAccount, setUseSvcAccount] = useState(true);
|
||||
|
||||
const loadCredentials = useCallback(async () => {
|
||||
setCredentialLoading(true);
|
||||
@@ -453,6 +454,16 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
}, [loadCredentials]);
|
||||
|
||||
const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]);
|
||||
const handleExecContextChange = useCallback((value) => {
|
||||
const normalized = String(value || "system").toLowerCase();
|
||||
setExecContext(normalized);
|
||||
if (normalized === "winrm") {
|
||||
setUseSvcAccount(true);
|
||||
setSelectedCredentialId("");
|
||||
} else {
|
||||
setUseSvcAccount(false);
|
||||
}
|
||||
}, []);
|
||||
const filteredCredentials = useMemo(() => {
|
||||
if (!remoteExec) return credentials;
|
||||
const target = execContext === "winrm" ? "winrm" : "ssh";
|
||||
@@ -463,6 +474,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
if (!remoteExec) {
|
||||
return;
|
||||
}
|
||||
if (execContext === "winrm" && useSvcAccount) {
|
||||
setSelectedCredentialId("");
|
||||
return;
|
||||
}
|
||||
if (!filteredCredentials.length) {
|
||||
setSelectedCredentialId("");
|
||||
return;
|
||||
@@ -470,7 +485,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
if (!selectedCredentialId || !filteredCredentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
||||
setSelectedCredentialId(String(filteredCredentials[0].id));
|
||||
}
|
||||
}, [remoteExec, filteredCredentials, selectedCredentialId]);
|
||||
}, [remoteExec, filteredCredentials, selectedCredentialId, execContext, useSvcAccount]);
|
||||
|
||||
// dialogs state
|
||||
const [addCompOpen, setAddCompOpen] = useState(false);
|
||||
@@ -877,12 +892,13 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
const isValid = useMemo(() => {
|
||||
const base = jobName.trim().length > 0 && components.length > 0 && targets.length > 0;
|
||||
if (!base) return false;
|
||||
if (remoteExec && !selectedCredentialId) return false;
|
||||
const needsCredential = remoteExec && !(execContext === "winrm" && useSvcAccount);
|
||||
if (needsCredential && !selectedCredentialId) return false;
|
||||
if (scheduleType !== "immediately") {
|
||||
return !!startDateTime;
|
||||
}
|
||||
return true;
|
||||
}, [jobName, components.length, targets.length, scheduleType, startDateTime, remoteExec, selectedCredentialId]);
|
||||
}, [jobName, components.length, targets.length, scheduleType, startDateTime, remoteExec, selectedCredentialId, execContext, useSvcAccount]);
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const editing = !!(initialJob && initialJob.id);
|
||||
@@ -1358,6 +1374,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
setExpiration(initialJob.expiration || "no_expire");
|
||||
setExecContext(initialJob.execution_context || "system");
|
||||
setSelectedCredentialId(initialJob.credential_id ? String(initialJob.credential_id) : "");
|
||||
if ((initialJob.execution_context || "").toLowerCase() === "winrm") {
|
||||
setUseSvcAccount(initialJob.use_service_account !== false);
|
||||
} else {
|
||||
setUseSvcAccount(false);
|
||||
}
|
||||
const comps = Array.isArray(initialJob.components) ? initialJob.components : [];
|
||||
const hydrated = await hydrateExistingComponents(comps);
|
||||
if (!canceled) {
|
||||
@@ -1369,6 +1390,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
setComponents([]);
|
||||
setComponentVarErrors({});
|
||||
setSelectedCredentialId("");
|
||||
setUseSvcAccount(true);
|
||||
}
|
||||
};
|
||||
hydrate();
|
||||
@@ -1464,7 +1486,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (remoteExec && !selectedCredentialId) {
|
||||
if (remoteExec && !(execContext === "winrm" && useSvcAccount) && !selectedCredentialId) {
|
||||
alert("Please select a credential for this execution context.");
|
||||
return;
|
||||
}
|
||||
@@ -1496,7 +1518,8 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
schedule: { type: scheduleType, start: scheduleType !== "immediately" ? (() => { try { const d = startDateTime?.toDate?.() || new Date(startDateTime); d.setSeconds(0,0); return d.toISOString(); } catch { return startDateTime; } })() : null },
|
||||
duration: { stopAfterEnabled, expiration },
|
||||
execution_context: execContext,
|
||||
credential_id: remoteExec && selectedCredentialId ? Number(selectedCredentialId) : null
|
||||
credential_id: remoteExec && !useSvcAccount && selectedCredentialId ? Number(selectedCredentialId) : null,
|
||||
use_service_account: execContext === "winrm" ? Boolean(useSvcAccount) : false
|
||||
};
|
||||
try {
|
||||
const resp = await fetch(initialJob && initialJob.id ? `/api/scheduled_jobs/${initialJob.id}` : "/api/scheduled_jobs", {
|
||||
@@ -1726,7 +1749,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
<Select
|
||||
size="small"
|
||||
value={execContext}
|
||||
onChange={(e) => setExecContext(e.target.value)}
|
||||
onChange={(e) => handleExecContextChange(e.target.value)}
|
||||
sx={{ minWidth: 320 }}
|
||||
>
|
||||
<MenuItem value="system">Run on agent as SYSTEM (device-local)</MenuItem>
|
||||
@@ -1736,10 +1759,29 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
</Select>
|
||||
{remoteExec && (
|
||||
<Box sx={{ mt: 2, display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}>
|
||||
{execContext === "winrm" && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={useSvcAccount}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setUseSvcAccount(checked);
|
||||
if (checked) {
|
||||
setSelectedCredentialId("");
|
||||
} else if (!selectedCredentialId && filteredCredentials.length) {
|
||||
setSelectedCredentialId(String(filteredCredentials[0].id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Use Configured svcBorealis Account"
|
||||
/>
|
||||
)}
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{ minWidth: 320 }}
|
||||
disabled={credentialLoading || !filteredCredentials.length}
|
||||
disabled={credentialLoading || !filteredCredentials.length || (execContext === "winrm" && useSvcAccount)}
|
||||
>
|
||||
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
||||
<Select
|
||||
@@ -1771,7 +1813,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
{credentialError}
|
||||
</Typography>
|
||||
)}
|
||||
{!credentialLoading && !credentialError && !filteredCredentials.length && (
|
||||
{execContext === "winrm" && useSvcAccount && (
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
Runs with the agent's svcBorealis account.
|
||||
</Typography>
|
||||
)}
|
||||
{!credentialLoading && !credentialError && !filteredCredentials.length && (!(execContext === "winrm" && useSvcAccount)) && (
|
||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>
|
||||
No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
|
||||
</Typography>
|
||||
|
||||
@@ -91,6 +91,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
const [credentialsLoading, setCredentialsLoading] = useState(false);
|
||||
const [credentialsError, setCredentialsError] = useState("");
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
||||
const [useSvcAccount, setUseSvcAccount] = useState(true);
|
||||
const [variables, setVariables] = useState([]);
|
||||
const [variableValues, setVariableValues] = useState({});
|
||||
const [variableErrors, setVariableErrors] = useState({});
|
||||
@@ -120,6 +121,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
setVariableValues({});
|
||||
setVariableErrors({});
|
||||
setVariableStatus({ loading: false, error: "" });
|
||||
setUseSvcAccount(true);
|
||||
setSelectedCredentialId("");
|
||||
loadTree();
|
||||
}
|
||||
}, [open, loadTree]);
|
||||
@@ -136,7 +139,10 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
const data = await resp.json();
|
||||
if (canceled) return;
|
||||
const list = Array.isArray(data?.credentials)
|
||||
? data.credentials.filter((cred) => String(cred.connection_type || "").toLowerCase() === "ssh")
|
||||
? data.credentials.filter((cred) => {
|
||||
const conn = String(cred.connection_type || "").toLowerCase();
|
||||
return conn === "ssh" || conn === "winrm";
|
||||
})
|
||||
: [];
|
||||
list.sort((a, b) => String(a?.name || "").localeCompare(String(b?.name || "")));
|
||||
setCredentials(list);
|
||||
@@ -161,7 +167,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "ansible") return;
|
||||
if (mode !== "ansible" || useSvcAccount) return;
|
||||
if (!credentials.length) {
|
||||
setSelectedCredentialId("");
|
||||
return;
|
||||
@@ -169,7 +175,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
if (!selectedCredentialId || !credentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
||||
setSelectedCredentialId(String(credentials[0].id));
|
||||
}
|
||||
}, [mode, credentials, selectedCredentialId]);
|
||||
}, [mode, credentials, selectedCredentialId, useSvcAccount]);
|
||||
|
||||
const renderNodes = (nodes = []) =>
|
||||
nodes.map((n) => (
|
||||
@@ -342,7 +348,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
setError(mode === 'ansible' ? "Please choose a playbook to run." : "Please choose a script to run.");
|
||||
return;
|
||||
}
|
||||
if (mode === 'ansible' && !selectedCredentialId) {
|
||||
if (mode === 'ansible' && !useSvcAccount && !selectedCredentialId) {
|
||||
setError("Select a credential to run this playbook.");
|
||||
return;
|
||||
}
|
||||
@@ -378,7 +384,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
playbook_path,
|
||||
hostnames,
|
||||
variable_values: variableOverrides,
|
||||
credential_id: selectedCredentialId ? Number(selectedCredentialId) : null
|
||||
credential_id: !useSvcAccount && selectedCredentialId ? Number(selectedCredentialId) : null,
|
||||
use_service_account: Boolean(useSvcAccount)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
@@ -405,8 +412,11 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
}
|
||||
};
|
||||
|
||||
const credentialRequired = mode === "ansible";
|
||||
const disableRun = running || !selectedPath || (credentialRequired && (!selectedCredentialId || !credentials.length));
|
||||
const credentialRequired = mode === "ansible" && !useSvcAccount;
|
||||
const disableRun =
|
||||
running ||
|
||||
!selectedPath ||
|
||||
(credentialRequired && (!selectedCredentialId || !credentials.length));
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={running ? undefined : onClose} fullWidth maxWidth="md"
|
||||
@@ -423,10 +433,29 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
</Typography>
|
||||
{mode === 'ansible' && (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap", mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={useSvcAccount}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setUseSvcAccount(checked);
|
||||
if (checked) {
|
||||
setSelectedCredentialId("");
|
||||
} else if (!selectedCredentialId && credentials.length) {
|
||||
setSelectedCredentialId(String(credentials[0].id));
|
||||
}
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Use Configured svcBorealis Account"
|
||||
sx={{ mr: 2 }}
|
||||
/>
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{ minWidth: 260 }}
|
||||
disabled={credentialsLoading || !credentials.length}
|
||||
disabled={useSvcAccount || credentialsLoading || !credentials.length}
|
||||
>
|
||||
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
||||
<Select
|
||||
@@ -435,20 +464,29 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
||||
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
|
||||
>
|
||||
{credentials.map((cred) => (
|
||||
<MenuItem key={cred.id} value={String(cred.id)}>
|
||||
{cred.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{credentials.map((cred) => {
|
||||
const conn = String(cred.connection_type || "").toUpperCase();
|
||||
return (
|
||||
<MenuItem key={cred.id} value={String(cred.id)}>
|
||||
{cred.name}
|
||||
{conn ? ` (${conn})` : ""}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{useSvcAccount && (
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
Runs with the agent's svcBorealis account.
|
||||
</Typography>
|
||||
)}
|
||||
{credentialsLoading && <CircularProgress size={18} sx={{ color: "#58a6ff" }} />}
|
||||
{!credentialsLoading && credentialsError && (
|
||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>{credentialsError}</Typography>
|
||||
)}
|
||||
{!credentialsLoading && !credentialsError && !credentials.length && (
|
||||
{!useSvcAccount && !credentialsLoading && !credentialsError && !credentials.length && (
|
||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>
|
||||
No SSH credentials available. Create one under Access Management.
|
||||
No SSH or WinRM credentials available. Create one under Access Management.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -7,6 +7,10 @@ import re
|
||||
import sqlite3
|
||||
from typing import Any, Dict, List, Optional, Tuple, Callable
|
||||
|
||||
_WINRM_USERNAME_VAR = "__borealis_winrm_username"
|
||||
_WINRM_PASSWORD_VAR = "__borealis_winrm_password"
|
||||
_WINRM_TRANSPORT_VAR = "__borealis_winrm_transport"
|
||||
|
||||
"""
|
||||
Job Scheduler module for Borealis
|
||||
|
||||
@@ -54,6 +58,26 @@ def _decode_base64_text(value: Any) -> Optional[str]:
|
||||
return decoded.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def _inject_winrm_credential(
|
||||
base_values: Optional[Dict[str, Any]],
|
||||
credential: Optional[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
values: Dict[str, Any] = dict(base_values or {})
|
||||
if not credential:
|
||||
return values
|
||||
|
||||
username = str(credential.get("username") or "")
|
||||
password = str(credential.get("password") or "")
|
||||
metadata = credential.get("metadata") if isinstance(credential.get("metadata"), dict) else {}
|
||||
transport = metadata.get("winrm_transport") if isinstance(metadata, dict) else None
|
||||
transport_str = str(transport or "ntlm").strip().lower() or "ntlm"
|
||||
|
||||
values[_WINRM_USERNAME_VAR] = username
|
||||
values[_WINRM_PASSWORD_VAR] = password
|
||||
values[_WINRM_TRANSPORT_VAR] = transport_str
|
||||
return values
|
||||
|
||||
|
||||
def _decode_script_content(value: Any, encoding_hint: str = "") -> str:
|
||||
encoding = (encoding_hint or "").strip().lower()
|
||||
if isinstance(value, str):
|
||||
@@ -311,6 +335,8 @@ class JobScheduler:
|
||||
self._online_lookup: Optional[Callable[[], List[str]]] = None
|
||||
# Optional callback to execute Ansible directly from the server
|
||||
self._server_ansible_runner: Optional[Callable[..., str]] = None
|
||||
# Optional callback to fetch stored credentials (with decrypted secrets)
|
||||
self._credential_fetcher: Optional[Callable[[int], Optional[Dict[str, Any]]]] = None
|
||||
|
||||
# Ensure run-history table exists
|
||||
self._init_tables()
|
||||
@@ -485,6 +511,7 @@ class JobScheduler:
|
||||
scheduled_run_row_id: int,
|
||||
run_mode: str,
|
||||
credential_id: Optional[int] = None,
|
||||
use_service_account: bool = False,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
import os, uuid
|
||||
@@ -522,7 +549,24 @@ class JobScheduler:
|
||||
variables = doc.get("variables") or []
|
||||
files = doc.get("files") or []
|
||||
run_mode_norm = (run_mode or "system").strip().lower()
|
||||
server_run = run_mode_norm in ("ssh", "winrm")
|
||||
server_run = run_mode_norm == "ssh"
|
||||
agent_winrm = run_mode_norm == "winrm"
|
||||
|
||||
if agent_winrm and not use_service_account:
|
||||
if not credential_id:
|
||||
raise RuntimeError("WinRM execution requires a credential_id")
|
||||
if not callable(self._credential_fetcher):
|
||||
raise RuntimeError("Credential fetcher is not configured")
|
||||
cred_detail = self._credential_fetcher(int(credential_id))
|
||||
if not cred_detail:
|
||||
raise RuntimeError("Credential not found")
|
||||
try:
|
||||
overrides_map = _inject_winrm_credential(overrides_map, cred_detail)
|
||||
finally:
|
||||
try:
|
||||
cred_detail.clear() # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Record in activity_history for UI parity
|
||||
now = _now_ts()
|
||||
@@ -743,6 +787,9 @@ class JobScheduler:
|
||||
def _conn(self):
|
||||
return sqlite3.connect(self.db_path)
|
||||
|
||||
def set_credential_fetcher(self, fn: Optional[Callable[[int], Optional[Dict[str, Any]]]]):
|
||||
self._credential_fetcher = fn
|
||||
|
||||
def _init_tables(self):
|
||||
conn = self._conn()
|
||||
cur = conn.cursor()
|
||||
@@ -954,7 +1001,7 @@ class JobScheduler:
|
||||
pass
|
||||
try:
|
||||
cur.execute(
|
||||
"SELECT id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, created_at FROM scheduled_jobs WHERE enabled=1 ORDER BY id ASC"
|
||||
"SELECT id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, use_service_account, created_at FROM scheduled_jobs WHERE enabled=1 ORDER BY id ASC"
|
||||
)
|
||||
jobs = cur.fetchall()
|
||||
except Exception:
|
||||
@@ -972,7 +1019,18 @@ class JobScheduler:
|
||||
five_min = 300
|
||||
now_min = _now_minute()
|
||||
|
||||
for (job_id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, created_at) in jobs:
|
||||
for (
|
||||
job_id,
|
||||
components_json,
|
||||
targets_json,
|
||||
schedule_type,
|
||||
start_ts,
|
||||
expiration,
|
||||
execution_context,
|
||||
credential_id,
|
||||
use_service_account_flag,
|
||||
created_at,
|
||||
) in jobs:
|
||||
try:
|
||||
# Targets list for this job
|
||||
try:
|
||||
@@ -1008,6 +1066,9 @@ class JobScheduler:
|
||||
continue
|
||||
run_mode = (execution_context or "system").strip().lower()
|
||||
job_credential_id = None
|
||||
job_use_service_account = bool(use_service_account_flag)
|
||||
if run_mode != "winrm":
|
||||
job_use_service_account = False
|
||||
try:
|
||||
job_credential_id = int(credential_id) if credential_id is not None else None
|
||||
except Exception:
|
||||
@@ -1098,7 +1159,7 @@ class JobScheduler:
|
||||
run_row_id = c2.lastrowid or 0
|
||||
conn2.commit()
|
||||
activity_links: List[Dict[str, Any]] = []
|
||||
remote_requires_cred = run_mode in ("ssh", "winrm")
|
||||
remote_requires_cred = (run_mode == "ssh") or (run_mode == "winrm" and not job_use_service_account)
|
||||
if remote_requires_cred and not job_credential_id:
|
||||
err_msg = "Credential required for remote execution"
|
||||
c2.execute(
|
||||
@@ -1132,6 +1193,7 @@ class JobScheduler:
|
||||
run_row_id,
|
||||
run_mode,
|
||||
job_credential_id,
|
||||
job_use_service_account,
|
||||
)
|
||||
if link and link.get("activity_id"):
|
||||
activity_links.append({
|
||||
@@ -1243,9 +1305,10 @@ class JobScheduler:
|
||||
"expiration": r[7] or "no_expire",
|
||||
"execution_context": r[8] or "system",
|
||||
"credential_id": r[9],
|
||||
"enabled": bool(r[10] or 0),
|
||||
"created_at": r[11] or 0,
|
||||
"updated_at": r[12] or 0,
|
||||
"use_service_account": bool(r[10] or 0),
|
||||
"enabled": bool(r[11] or 0),
|
||||
"created_at": r[12] or 0,
|
||||
"updated_at": r[13] or 0,
|
||||
}
|
||||
# Attach computed status summary for latest occurrence
|
||||
try:
|
||||
@@ -1322,7 +1385,8 @@ class JobScheduler:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at
|
||||
duration_stop_enabled, expiration, execution_context, credential_id,
|
||||
use_service_account, enabled, created_at, updated_at
|
||||
FROM scheduled_jobs
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
@@ -1350,6 +1414,8 @@ class JobScheduler:
|
||||
credential_id = int(credential_id) if credential_id is not None else None
|
||||
except Exception:
|
||||
credential_id = None
|
||||
use_service_account_raw = data.get("use_service_account")
|
||||
use_service_account = 1 if (execution_context == "winrm" and (use_service_account_raw is None or bool(use_service_account_raw))) else 0
|
||||
enabled = int(bool(data.get("enabled", True)))
|
||||
if not name or not components or not targets:
|
||||
return json.dumps({"error": "name, components, targets required"}), 400, {"Content-Type": "application/json"}
|
||||
@@ -1360,8 +1426,8 @@ class JobScheduler:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO scheduled_jobs
|
||||
(name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
(name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
@@ -1373,6 +1439,7 @@ class JobScheduler:
|
||||
expiration,
|
||||
execution_context,
|
||||
credential_id,
|
||||
use_service_account,
|
||||
enabled,
|
||||
now,
|
||||
now,
|
||||
@@ -1383,7 +1450,7 @@ class JobScheduler:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
|
||||
FROM scheduled_jobs WHERE id=?
|
||||
""",
|
||||
(job_id,),
|
||||
@@ -1402,7 +1469,7 @@ class JobScheduler:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
|
||||
FROM scheduled_jobs WHERE id=?
|
||||
""",
|
||||
(job_id,),
|
||||
@@ -1435,7 +1502,10 @@ class JobScheduler:
|
||||
if "expiration" in data or (data.get("duration") and "expiration" in data.get("duration")):
|
||||
fields["expiration"] = (data.get("duration") or {}).get("expiration") or data.get("expiration") or "no_expire"
|
||||
if "execution_context" in data:
|
||||
fields["execution_context"] = (data.get("execution_context") or "system").strip().lower()
|
||||
exec_ctx_val = (data.get("execution_context") or "system").strip().lower()
|
||||
fields["execution_context"] = exec_ctx_val
|
||||
if exec_ctx_val != "winrm":
|
||||
fields["use_service_account"] = 0
|
||||
if "credential_id" in data:
|
||||
cred_val = data.get("credential_id")
|
||||
if cred_val in (None, "", "null"):
|
||||
@@ -1445,6 +1515,8 @@ class JobScheduler:
|
||||
fields["credential_id"] = int(cred_val)
|
||||
except Exception:
|
||||
fields["credential_id"] = None
|
||||
if "use_service_account" in data:
|
||||
fields["use_service_account"] = 1 if bool(data.get("use_service_account")) else 0
|
||||
if "enabled" in data:
|
||||
fields["enabled"] = int(bool(data.get("enabled")))
|
||||
if not fields:
|
||||
@@ -1462,7 +1534,7 @@ class JobScheduler:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at
|
||||
duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
|
||||
FROM scheduled_jobs WHERE id=?
|
||||
""",
|
||||
(job_id,),
|
||||
@@ -1486,7 +1558,7 @@ class JobScheduler:
|
||||
return json.dumps({"error": "not found"}), 404, {"Content-Type": "application/json"}
|
||||
conn.commit()
|
||||
cur.execute(
|
||||
"SELECT id, name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at FROM scheduled_jobs WHERE id=?",
|
||||
"SELECT id, name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at FROM scheduled_jobs WHERE id=?",
|
||||
(job_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -1738,3 +1810,7 @@ def set_online_lookup(scheduler: JobScheduler, fn: Callable[[], List[str]]):
|
||||
|
||||
def set_server_ansible_runner(scheduler: JobScheduler, fn: Callable[..., str]):
|
||||
scheduler._server_ansible_runner = fn
|
||||
|
||||
|
||||
def set_credential_fetcher(scheduler: JobScheduler, fn: Callable[[int], Optional[Dict[str, Any]]]):
|
||||
scheduler._credential_fetcher = fn
|
||||
|
||||
@@ -329,6 +329,7 @@ from Python_API_Endpoints.script_engines import run_powershell_script
|
||||
from job_scheduler import register as register_job_scheduler
|
||||
from job_scheduler import set_online_lookup as scheduler_set_online_lookup
|
||||
from job_scheduler import set_server_ansible_runner as scheduler_set_server_runner
|
||||
from job_scheduler import set_credential_fetcher as scheduler_set_credential_fetcher
|
||||
|
||||
# =============================================================================
|
||||
# Section: Runtime Stack Configuration
|
||||
@@ -1859,6 +1860,11 @@ def _ensure_ansible_workspace() -> str:
|
||||
return _ANSIBLE_WORKSPACE_DIR
|
||||
|
||||
|
||||
_WINRM_USERNAME_VAR = "__borealis_winrm_username"
|
||||
_WINRM_PASSWORD_VAR = "__borealis_winrm_password"
|
||||
_WINRM_TRANSPORT_VAR = "__borealis_winrm_transport"
|
||||
|
||||
|
||||
def _fetch_credential_with_secrets(credential_id: int) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
conn = _db_conn()
|
||||
@@ -1876,7 +1882,8 @@ def _fetch_credential_with_secrets(credential_id: int) -> Optional[Dict[str, Any
|
||||
private_key_passphrase_encrypted,
|
||||
become_method,
|
||||
become_username,
|
||||
become_password_encrypted
|
||||
become_password_encrypted,
|
||||
metadata_json
|
||||
FROM credentials
|
||||
WHERE id=?
|
||||
""",
|
||||
@@ -1900,8 +1907,40 @@ def _fetch_credential_with_secrets(credential_id: int) -> Optional[Dict[str, Any
|
||||
"become_method": _normalize_become_method(row[8]),
|
||||
"become_username": row[9] or "",
|
||||
"become_password": _decrypt_secret(row[10]) if row[10] else "",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
try:
|
||||
meta_json = row[11] if len(row) > 11 else None
|
||||
if meta_json:
|
||||
meta = json.loads(meta_json)
|
||||
if isinstance(meta, dict):
|
||||
cred["metadata"] = meta
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return cred
|
||||
|
||||
|
||||
def _inject_winrm_credential(
|
||||
base_values: Optional[Dict[str, Any]],
|
||||
credential: Optional[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
values: Dict[str, Any] = dict(base_values or {})
|
||||
if not credential:
|
||||
return values
|
||||
|
||||
username = str(credential.get("username") or "")
|
||||
password = str(credential.get("password") or "")
|
||||
metadata = credential.get("metadata") if isinstance(credential.get("metadata"), dict) else {}
|
||||
transport = metadata.get("winrm_transport") if isinstance(metadata, dict) else None
|
||||
transport_str = str(transport or "ntlm").strip().lower() or "ntlm"
|
||||
|
||||
values[_WINRM_USERNAME_VAR] = username
|
||||
values[_WINRM_PASSWORD_VAR] = password
|
||||
values[_WINRM_TRANSPORT_VAR] = transport_str
|
||||
return values
|
||||
|
||||
|
||||
def _emit_ansible_recap_from_row(row):
|
||||
if not row:
|
||||
@@ -3514,6 +3553,7 @@ _DEVICE_TABLE_COLUMNS = [
|
||||
"operating_system",
|
||||
"uptime",
|
||||
"agent_id",
|
||||
"ansible_ee_ver",
|
||||
"connection_type",
|
||||
"connection_endpoint",
|
||||
]
|
||||
@@ -3603,6 +3643,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 +3788,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 +3857,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 +3881,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 +3907,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 +4205,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 +4241,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 +4323,7 @@ def init_db():
|
||||
operating_system TEXT,
|
||||
uptime INTEGER,
|
||||
agent_id TEXT,
|
||||
ansible_ee_ver TEXT,
|
||||
connection_type TEXT,
|
||||
connection_endpoint TEXT
|
||||
)
|
||||
@@ -4481,6 +4531,7 @@ def init_db():
|
||||
expiration TEXT,
|
||||
execution_context TEXT NOT NULL,
|
||||
credential_id INTEGER,
|
||||
use_service_account INTEGER NOT NULL DEFAULT 1,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
@@ -4492,6 +4543,8 @@ def init_db():
|
||||
sj_cols = [row[1] for row in cur.fetchall()]
|
||||
if "credential_id" not in sj_cols:
|
||||
cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN credential_id INTEGER")
|
||||
if "use_service_account" not in sj_cols:
|
||||
cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN use_service_account INTEGER NOT NULL DEFAULT 1")
|
||||
except Exception:
|
||||
pass
|
||||
conn.commit()
|
||||
@@ -4553,6 +4606,7 @@ ensure_default_admin()
|
||||
|
||||
job_scheduler = register_job_scheduler(app, socketio, DB_PATH)
|
||||
scheduler_set_server_runner(job_scheduler, _queue_server_ansible_run)
|
||||
scheduler_set_credential_fetcher(job_scheduler, _fetch_credential_with_secrets)
|
||||
job_scheduler.start()
|
||||
|
||||
# Provide scheduler with online device lookup based on registered agents
|
||||
@@ -6359,20 +6413,46 @@ def ansible_quick_run():
|
||||
rel_path = (data.get("playbook_path") or "").strip()
|
||||
hostnames = data.get("hostnames") or []
|
||||
credential_id = data.get("credential_id")
|
||||
use_service_account_raw = data.get("use_service_account")
|
||||
if not rel_path or not isinstance(hostnames, list) or not hostnames:
|
||||
_ansible_log_server(f"[quick_run] invalid payload rel_path='{rel_path}' hostnames={hostnames}")
|
||||
return jsonify({"error": "Missing playbook_path or hostnames[]"}), 400
|
||||
server_mode = False
|
||||
cred_id_int = None
|
||||
credential_detail: Optional[Dict[str, Any]] = None
|
||||
overrides_raw = data.get("variable_values")
|
||||
variable_values: Dict[str, Any] = {}
|
||||
if isinstance(overrides_raw, dict):
|
||||
for key, val in overrides_raw.items():
|
||||
name = str(key or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
variable_values[name] = val
|
||||
if credential_id not in (None, "", "null"):
|
||||
try:
|
||||
cred_id_int = int(credential_id)
|
||||
if cred_id_int <= 0:
|
||||
cred_id_int = None
|
||||
else:
|
||||
server_mode = True
|
||||
except Exception:
|
||||
return jsonify({"error": "Invalid credential_id"}), 400
|
||||
if use_service_account_raw is None:
|
||||
use_service_account = cred_id_int is None
|
||||
else:
|
||||
use_service_account = bool(use_service_account_raw)
|
||||
if use_service_account:
|
||||
cred_id_int = None
|
||||
credential_detail = None
|
||||
if cred_id_int:
|
||||
credential_detail = _fetch_credential_with_secrets(cred_id_int)
|
||||
if not credential_detail:
|
||||
return jsonify({"error": "Credential not found"}), 404
|
||||
conn_type = (credential_detail.get("connection_type") or "ssh").lower()
|
||||
if conn_type in ("ssh", "linux", "unix"):
|
||||
server_mode = True
|
||||
elif conn_type in ("winrm", "psrp"):
|
||||
variable_values = _inject_winrm_credential(variable_values, credential_detail)
|
||||
else:
|
||||
return jsonify({"error": f"Credential connection '{conn_type}' not supported"}), 400
|
||||
try:
|
||||
root, abs_path, _ = _resolve_assembly_path('ansible', rel_path)
|
||||
if not os.path.isfile(abs_path):
|
||||
@@ -6384,28 +6464,9 @@ def ansible_quick_run():
|
||||
variables = doc.get('variables') if isinstance(doc.get('variables'), list) else []
|
||||
files = doc.get('files') if isinstance(doc.get('files'), list) else []
|
||||
friendly_name = (doc.get("name") or "").strip() or os.path.basename(abs_path)
|
||||
overrides_raw = data.get("variable_values")
|
||||
variable_values = {}
|
||||
if isinstance(overrides_raw, dict):
|
||||
for key, val in overrides_raw.items():
|
||||
name = str(key or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
variable_values[name] = val
|
||||
|
||||
if server_mode and not cred_id_int:
|
||||
return jsonify({"error": "credential_id is required for server-side execution"}), 400
|
||||
|
||||
if server_mode:
|
||||
cred = _fetch_credential_with_secrets(cred_id_int)
|
||||
if not cred:
|
||||
return jsonify({"error": "Credential not found"}), 404
|
||||
conn_type = (cred.get("connection_type") or "ssh").lower()
|
||||
if conn_type not in ("ssh",):
|
||||
return jsonify({"error": f"Credential connection '{conn_type}' not supported for server execution"}), 400
|
||||
# Avoid keeping decrypted secrets in memory longer than necessary
|
||||
del cred
|
||||
|
||||
results = []
|
||||
for host in hostnames:
|
||||
# Create activity_history row so UI shows running state and can receive recap mirror
|
||||
@@ -6499,6 +6560,9 @@ def ansible_quick_run():
|
||||
except Exception:
|
||||
pass
|
||||
results.append({"hostname": host, "run_id": run_id, "status": "Failed", "activity_job_id": job_id, "error": str(ex)})
|
||||
if credential_detail is not None:
|
||||
# Remove decrypted secrets from scope as soon as possible
|
||||
credential_detail.clear()
|
||||
return jsonify({"results": results})
|
||||
except ValueError as ve:
|
||||
return jsonify({"error": str(ve)}), 400
|
||||
|
||||
21
Update.ps1
21
Update.ps1
@@ -497,6 +497,8 @@ function Invoke-BorealisUpdate {
|
||||
|
||||
$preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
|
||||
$preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR"
|
||||
$ansibleEePath = Join-Path $scriptDir "Agent\Ansible_EE"
|
||||
$ansibleEeBackupPath = Join-Path $scriptDir "Update_Staging\Ansible_EE"
|
||||
|
||||
Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" {
|
||||
if (Test-Path $preservePath) {
|
||||
@@ -506,6 +508,17 @@ function Invoke-BorealisUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Updating: Preserve Ansible Execution Environment" {
|
||||
if (Test-Path $ansibleEePath) {
|
||||
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
||||
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
||||
if (Test-Path $ansibleEeBackupPath) {
|
||||
Remove-Item -Path $ansibleEeBackupPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Move-Item -Path $ansibleEePath -Destination $ansibleEeBackupPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Updating: Clean Up Folders to Prepare for Update" {
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
||||
(Join-Path $scriptDir "Data"), `
|
||||
@@ -575,6 +588,14 @@ function Invoke-BorealisUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Updating: Restore Ansible Execution Environment" {
|
||||
$restorePath = Join-Path $scriptDir "Agent"
|
||||
if (Test-Path $ansibleEeBackupPath) {
|
||||
if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null }
|
||||
Move-Item -Path $ansibleEeBackupPath -Destination $restorePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Updating: Clean Up Update Staging Folder" {
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user