mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-18 07:15:48 -07: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"
|
$sevenZipExe = Join-Path $depsRoot "7zip\7z.exe"
|
||||||
$npmCmd = Join-Path (Split-Path $nodeExe) 'npm.cmd'
|
$npmCmd = Join-Path (Split-Path $nodeExe) 'npm.cmd'
|
||||||
$npxCmd = Join-Path (Split-Path $nodeExe) 'npx.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"
|
$node7zUrl = "https://nodejs.org/dist/v23.11.0/node-v23.11.0-win-x64.7z"
|
||||||
$nodeInstallDir = Join-Path $depsRoot "NodeJS"
|
$nodeInstallDir = Join-Path $depsRoot "NodeJS"
|
||||||
$node7zPath = Join-Path $depsRoot "node-v23.11.0-win-x64.7z"
|
$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 {
|
function Ensure-AgentTasks {
|
||||||
param([string]$ScriptRoot)
|
param([string]$ScriptRoot)
|
||||||
$pyw = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
|
$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" {
|
Run-Step "Configure Agent Settings" {
|
||||||
$settingsDir = Join-Path $scriptDir 'Agent\Borealis\Settings'
|
$settingsDir = Join-Path $scriptDir 'Agent\Borealis\Settings'
|
||||||
$oldSettingsDir = Join-Path $scriptDir 'Agent\Settings'
|
$oldSettingsDir = Join-Path $scriptDir 'Agent\Settings'
|
||||||
|
|||||||
@@ -229,18 +229,55 @@ def detect_agent_os():
|
|||||||
return "Unknown"
|
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):
|
def collect_summary(CONFIG):
|
||||||
try:
|
try:
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
return {
|
summary = {
|
||||||
'hostname': hostname,
|
'hostname': hostname,
|
||||||
'os': detect_agent_os(),
|
'os': detect_agent_os(),
|
||||||
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
|
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
|
||||||
'domain': os.environ.get('USERDOMAIN') or '',
|
'domain': os.environ.get('USERDOMAIN') or '',
|
||||||
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
|
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
|
||||||
}
|
}
|
||||||
|
summary['ansible_ee_ver'] = _ansible_ee_version()
|
||||||
|
return summary
|
||||||
except Exception:
|
except Exception:
|
||||||
return {'hostname': socket.gethostname()}
|
return {
|
||||||
|
'hostname': socket.gethostname(),
|
||||||
|
'ansible_ee_ver': _ansible_ee_version(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _project_root():
|
def _project_root():
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import base64
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -53,6 +54,62 @@ def _project_root():
|
|||||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
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):
|
def _decode_base64_text(value):
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return None
|
return None
|
||||||
@@ -99,17 +156,38 @@ def _agent_root():
|
|||||||
|
|
||||||
def _scripts_bin():
|
def _scripts_bin():
|
||||||
# Return the venv Scripts (Windows) or bin (POSIX) path adjacent to Borealis
|
# 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()
|
agent_root = _agent_root()
|
||||||
candidates = [
|
candidates.extend(
|
||||||
os.path.join(agent_root, 'Scripts'), # Windows venv
|
[
|
||||||
os.path.join(agent_root, 'bin'), # POSIX venv
|
os.path.join(agent_root, 'Scripts'), # Windows venv
|
||||||
]
|
os.path.join(agent_root, 'bin'), # POSIX venv
|
||||||
|
]
|
||||||
|
)
|
||||||
for base in candidates:
|
for base in candidates:
|
||||||
if os.path.isdir(base):
|
if os.path.isdir(base):
|
||||||
return base
|
return base
|
||||||
return None
|
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():
|
def _ansible_playbook_cmd():
|
||||||
exe = 'ansible-playbook.exe' if os.name == 'nt' else 'ansible-playbook'
|
exe = 'ansible-playbook.exe' if os.name == 'nt' else 'ansible-playbook'
|
||||||
sdir = _scripts_bin()
|
sdir = _scripts_bin()
|
||||||
@@ -137,10 +215,19 @@ def _collections_dir():
|
|||||||
return base
|
return base
|
||||||
|
|
||||||
def _venv_python():
|
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:
|
try:
|
||||||
sdir = _scripts_bin()
|
sdir = os.path.join(_agent_root(), 'Scripts' if os.name == 'nt' else 'bin')
|
||||||
if not sdir:
|
|
||||||
return None
|
|
||||||
cand = os.path.join(sdir, 'python.exe' if os.name == 'nt' else 'python3')
|
cand = os.path.join(sdir, 'python.exe' if os.name == 'nt' else 'python3')
|
||||||
return cand if os.path.isfile(cand) else None
|
return cand if os.path.isfile(cand) else None
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -175,50 +262,108 @@ class Role:
|
|||||||
return os.path.join(tmp_dir, 'ansible_bootstrap.json')
|
return os.path.join(tmp_dir, 'ansible_bootstrap.json')
|
||||||
|
|
||||||
def _detect_missing_modules(self) -> dict:
|
def _detect_missing_modules(self) -> dict:
|
||||||
|
"""Return any required modules that the execution environment lacks."""
|
||||||
|
|
||||||
missing = {}
|
missing = {}
|
||||||
for module, spec in REQUIRED_MODULES.items():
|
|
||||||
try:
|
python_exe = _venv_python()
|
||||||
__import__(module)
|
if not python_exe or not os.path.isfile(python_exe):
|
||||||
except Exception:
|
missing['python'] = 'execution-environment python missing'
|
||||||
missing[module] = spec
|
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
|
return missing
|
||||||
|
|
||||||
def _bootstrap_ansible_sync(self) -> bool:
|
def _bootstrap_ansible_sync(self) -> bool:
|
||||||
missing = self._detect_missing_modules()
|
missing = self._detect_missing_modules()
|
||||||
if not missing:
|
if missing:
|
||||||
return True
|
self._ansible_log(
|
||||||
specs = sorted({spec for spec in missing.values() if spec})
|
f"[bootstrap] required agent modules missing: {', '.join(sorted(missing.keys()))}",
|
||||||
python_exe = _venv_python() or sys.executable
|
error=True,
|
||||||
if not python_exe:
|
)
|
||||||
self._ansible_log('[bootstrap] python executable not found for pip install', error=True)
|
|
||||||
return False
|
return False
|
||||||
cmd = [python_exe, '-m', 'pip', 'install', '--disable-pip-version-check'] + specs
|
ee_root = _ansible_ee_root()
|
||||||
self._ansible_log(f"[bootstrap] ensuring modules via pip: {', '.join(specs)}")
|
if not ee_root or not os.path.isdir(ee_root):
|
||||||
try:
|
self._ansible_log('[bootstrap] execution environment folder Agent/Ansible_EE not found', error=True)
|
||||||
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)
|
|
||||||
return False
|
return False
|
||||||
if result.returncode != 0:
|
|
||||||
err_tail = (result.stderr or '').strip()
|
scripts_dir = _scripts_bin()
|
||||||
if len(err_tail) > 500:
|
exe_name = 'ansible-playbook.exe' if os.name == 'nt' else 'ansible-playbook'
|
||||||
err_tail = err_tail[-500:]
|
playbook_path = None
|
||||||
self._ansible_log(f"[bootstrap] pip install failed rc={result.returncode} err={err_tail}", error=True)
|
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
|
return False
|
||||||
remaining = self._detect_missing_modules()
|
|
||||||
if remaining:
|
python_exe = _venv_python()
|
||||||
self._ansible_log(f"[bootstrap] modules still missing after install: {', '.join(sorted(remaining.keys()))}", error=True)
|
if not python_exe or not os.path.isfile(python_exe):
|
||||||
|
self._ansible_log('[bootstrap] execution environment python not found', error=True)
|
||||||
return False
|
return False
|
||||||
try:
|
|
||||||
marker = self._bootstrap_marker_path()
|
env_path = os.environ.get('PATH') or ''
|
||||||
payload = {
|
bin_dir = os.path.dirname(playbook_path)
|
||||||
'timestamp': int(time.time()),
|
if bin_dir:
|
||||||
'modules': specs,
|
segments = [seg for seg in env_path.split(os.pathsep) if seg]
|
||||||
}
|
if bin_dir not in segments:
|
||||||
with open(marker, 'w', encoding='utf-8') as fh:
|
os.environ['PATH'] = bin_dir + (os.pathsep + env_path if env_path else '')
|
||||||
json.dump(payload, fh)
|
|
||||||
except Exception:
|
collections_dir = os.path.join(ee_root, 'collections')
|
||||||
pass
|
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
|
return True
|
||||||
|
|
||||||
async def _ensure_ansible_ready(self) -> bool:
|
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 [credentialLoading, setCredentialLoading] = useState(false);
|
||||||
const [credentialError, setCredentialError] = useState("");
|
const [credentialError, setCredentialError] = useState("");
|
||||||
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
||||||
|
const [useSvcAccount, setUseSvcAccount] = useState(true);
|
||||||
|
|
||||||
const loadCredentials = useCallback(async () => {
|
const loadCredentials = useCallback(async () => {
|
||||||
setCredentialLoading(true);
|
setCredentialLoading(true);
|
||||||
@@ -453,6 +454,16 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
}, [loadCredentials]);
|
}, [loadCredentials]);
|
||||||
|
|
||||||
const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]);
|
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(() => {
|
const filteredCredentials = useMemo(() => {
|
||||||
if (!remoteExec) return credentials;
|
if (!remoteExec) return credentials;
|
||||||
const target = execContext === "winrm" ? "winrm" : "ssh";
|
const target = execContext === "winrm" ? "winrm" : "ssh";
|
||||||
@@ -463,6 +474,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
if (!remoteExec) {
|
if (!remoteExec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (execContext === "winrm" && useSvcAccount) {
|
||||||
|
setSelectedCredentialId("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!filteredCredentials.length) {
|
if (!filteredCredentials.length) {
|
||||||
setSelectedCredentialId("");
|
setSelectedCredentialId("");
|
||||||
return;
|
return;
|
||||||
@@ -470,7 +485,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
if (!selectedCredentialId || !filteredCredentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
if (!selectedCredentialId || !filteredCredentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
||||||
setSelectedCredentialId(String(filteredCredentials[0].id));
|
setSelectedCredentialId(String(filteredCredentials[0].id));
|
||||||
}
|
}
|
||||||
}, [remoteExec, filteredCredentials, selectedCredentialId]);
|
}, [remoteExec, filteredCredentials, selectedCredentialId, execContext, useSvcAccount]);
|
||||||
|
|
||||||
// dialogs state
|
// dialogs state
|
||||||
const [addCompOpen, setAddCompOpen] = useState(false);
|
const [addCompOpen, setAddCompOpen] = useState(false);
|
||||||
@@ -877,12 +892,13 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
const isValid = useMemo(() => {
|
const isValid = useMemo(() => {
|
||||||
const base = jobName.trim().length > 0 && components.length > 0 && targets.length > 0;
|
const base = jobName.trim().length > 0 && components.length > 0 && targets.length > 0;
|
||||||
if (!base) return false;
|
if (!base) return false;
|
||||||
if (remoteExec && !selectedCredentialId) return false;
|
const needsCredential = remoteExec && !(execContext === "winrm" && useSvcAccount);
|
||||||
|
if (needsCredential && !selectedCredentialId) return false;
|
||||||
if (scheduleType !== "immediately") {
|
if (scheduleType !== "immediately") {
|
||||||
return !!startDateTime;
|
return !!startDateTime;
|
||||||
}
|
}
|
||||||
return true;
|
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 [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const editing = !!(initialJob && initialJob.id);
|
const editing = !!(initialJob && initialJob.id);
|
||||||
@@ -1358,6 +1374,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
setExpiration(initialJob.expiration || "no_expire");
|
setExpiration(initialJob.expiration || "no_expire");
|
||||||
setExecContext(initialJob.execution_context || "system");
|
setExecContext(initialJob.execution_context || "system");
|
||||||
setSelectedCredentialId(initialJob.credential_id ? String(initialJob.credential_id) : "");
|
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 comps = Array.isArray(initialJob.components) ? initialJob.components : [];
|
||||||
const hydrated = await hydrateExistingComponents(comps);
|
const hydrated = await hydrateExistingComponents(comps);
|
||||||
if (!canceled) {
|
if (!canceled) {
|
||||||
@@ -1369,6 +1390,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
setComponents([]);
|
setComponents([]);
|
||||||
setComponentVarErrors({});
|
setComponentVarErrors({});
|
||||||
setSelectedCredentialId("");
|
setSelectedCredentialId("");
|
||||||
|
setUseSvcAccount(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
hydrate();
|
hydrate();
|
||||||
@@ -1464,7 +1486,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (remoteExec && !selectedCredentialId) {
|
if (remoteExec && !(execContext === "winrm" && useSvcAccount) && !selectedCredentialId) {
|
||||||
alert("Please select a credential for this execution context.");
|
alert("Please select a credential for this execution context.");
|
||||||
return;
|
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 },
|
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 },
|
duration: { stopAfterEnabled, expiration },
|
||||||
execution_context: execContext,
|
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 {
|
try {
|
||||||
const resp = await fetch(initialJob && initialJob.id ? `/api/scheduled_jobs/${initialJob.id}` : "/api/scheduled_jobs", {
|
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
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
value={execContext}
|
value={execContext}
|
||||||
onChange={(e) => setExecContext(e.target.value)}
|
onChange={(e) => handleExecContextChange(e.target.value)}
|
||||||
sx={{ minWidth: 320 }}
|
sx={{ minWidth: 320 }}
|
||||||
>
|
>
|
||||||
<MenuItem value="system">Run on agent as SYSTEM (device-local)</MenuItem>
|
<MenuItem value="system">Run on agent as SYSTEM (device-local)</MenuItem>
|
||||||
@@ -1736,10 +1759,29 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
</Select>
|
</Select>
|
||||||
{remoteExec && (
|
{remoteExec && (
|
||||||
<Box sx={{ mt: 2, display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}>
|
<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
|
<FormControl
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth: 320 }}
|
sx={{ minWidth: 320 }}
|
||||||
disabled={credentialLoading || !filteredCredentials.length}
|
disabled={credentialLoading || !filteredCredentials.length || (execContext === "winrm" && useSvcAccount)}
|
||||||
>
|
>
|
||||||
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -1771,7 +1813,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
|||||||
{credentialError}
|
{credentialError}
|
||||||
</Typography>
|
</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" }}>
|
<Typography variant="body2" sx={{ color: "#ff8080" }}>
|
||||||
No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
|
No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
const [credentialsLoading, setCredentialsLoading] = useState(false);
|
const [credentialsLoading, setCredentialsLoading] = useState(false);
|
||||||
const [credentialsError, setCredentialsError] = useState("");
|
const [credentialsError, setCredentialsError] = useState("");
|
||||||
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
||||||
|
const [useSvcAccount, setUseSvcAccount] = useState(true);
|
||||||
const [variables, setVariables] = useState([]);
|
const [variables, setVariables] = useState([]);
|
||||||
const [variableValues, setVariableValues] = useState({});
|
const [variableValues, setVariableValues] = useState({});
|
||||||
const [variableErrors, setVariableErrors] = useState({});
|
const [variableErrors, setVariableErrors] = useState({});
|
||||||
@@ -120,6 +121,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
setVariableValues({});
|
setVariableValues({});
|
||||||
setVariableErrors({});
|
setVariableErrors({});
|
||||||
setVariableStatus({ loading: false, error: "" });
|
setVariableStatus({ loading: false, error: "" });
|
||||||
|
setUseSvcAccount(true);
|
||||||
|
setSelectedCredentialId("");
|
||||||
loadTree();
|
loadTree();
|
||||||
}
|
}
|
||||||
}, [open, loadTree]);
|
}, [open, loadTree]);
|
||||||
@@ -136,7 +139,10 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
const list = Array.isArray(data?.credentials)
|
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 || "")));
|
list.sort((a, b) => String(a?.name || "").localeCompare(String(b?.name || "")));
|
||||||
setCredentials(list);
|
setCredentials(list);
|
||||||
@@ -161,7 +167,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== "ansible") return;
|
if (mode !== "ansible" || useSvcAccount) return;
|
||||||
if (!credentials.length) {
|
if (!credentials.length) {
|
||||||
setSelectedCredentialId("");
|
setSelectedCredentialId("");
|
||||||
return;
|
return;
|
||||||
@@ -169,7 +175,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
if (!selectedCredentialId || !credentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
if (!selectedCredentialId || !credentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
|
||||||
setSelectedCredentialId(String(credentials[0].id));
|
setSelectedCredentialId(String(credentials[0].id));
|
||||||
}
|
}
|
||||||
}, [mode, credentials, selectedCredentialId]);
|
}, [mode, credentials, selectedCredentialId, useSvcAccount]);
|
||||||
|
|
||||||
const renderNodes = (nodes = []) =>
|
const renderNodes = (nodes = []) =>
|
||||||
nodes.map((n) => (
|
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.");
|
setError(mode === 'ansible' ? "Please choose a playbook to run." : "Please choose a script to run.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mode === 'ansible' && !selectedCredentialId) {
|
if (mode === 'ansible' && !useSvcAccount && !selectedCredentialId) {
|
||||||
setError("Select a credential to run this playbook.");
|
setError("Select a credential to run this playbook.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -378,7 +384,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
playbook_path,
|
playbook_path,
|
||||||
hostnames,
|
hostnames,
|
||||||
variable_values: variableOverrides,
|
variable_values: variableOverrides,
|
||||||
credential_id: selectedCredentialId ? Number(selectedCredentialId) : null
|
credential_id: !useSvcAccount && selectedCredentialId ? Number(selectedCredentialId) : null,
|
||||||
|
use_service_account: Boolean(useSvcAccount)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -405,8 +412,11 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const credentialRequired = mode === "ansible";
|
const credentialRequired = mode === "ansible" && !useSvcAccount;
|
||||||
const disableRun = running || !selectedPath || (credentialRequired && (!selectedCredentialId || !credentials.length));
|
const disableRun =
|
||||||
|
running ||
|
||||||
|
!selectedPath ||
|
||||||
|
(credentialRequired && (!selectedCredentialId || !credentials.length));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={running ? undefined : onClose} fullWidth maxWidth="md"
|
<Dialog open={open} onClose={running ? undefined : onClose} fullWidth maxWidth="md"
|
||||||
@@ -423,10 +433,29 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
{mode === 'ansible' && (
|
{mode === 'ansible' && (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap", mb: 2 }}>
|
<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
|
<FormControl
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ minWidth: 260 }}
|
sx={{ minWidth: 260 }}
|
||||||
disabled={credentialsLoading || !credentials.length}
|
disabled={useSvcAccount || credentialsLoading || !credentials.length}
|
||||||
>
|
>
|
||||||
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -435,20 +464,29 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
|
|||||||
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||||
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
|
sx={{ bgcolor: "#1f1f1f", color: "#fff" }}
|
||||||
>
|
>
|
||||||
{credentials.map((cred) => (
|
{credentials.map((cred) => {
|
||||||
<MenuItem key={cred.id} value={String(cred.id)}>
|
const conn = String(cred.connection_type || "").toUpperCase();
|
||||||
{cred.name}
|
return (
|
||||||
</MenuItem>
|
<MenuItem key={cred.id} value={String(cred.id)}>
|
||||||
))}
|
{cred.name}
|
||||||
|
{conn ? ` (${conn})` : ""}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{useSvcAccount && (
|
||||||
|
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||||
|
Runs with the agent's svcBorealis account.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
{credentialsLoading && <CircularProgress size={18} sx={{ color: "#58a6ff" }} />}
|
{credentialsLoading && <CircularProgress size={18} sx={{ color: "#58a6ff" }} />}
|
||||||
{!credentialsLoading && credentialsError && (
|
{!credentialsLoading && credentialsError && (
|
||||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>{credentialsError}</Typography>
|
<Typography variant="body2" sx={{ color: "#ff8080" }}>{credentialsError}</Typography>
|
||||||
)}
|
)}
|
||||||
{!credentialsLoading && !credentialsError && !credentials.length && (
|
{!useSvcAccount && !credentialsLoading && !credentialsError && !credentials.length && (
|
||||||
<Typography variant="body2" sx={{ color: "#ff8080" }}>
|
<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>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import re
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Callable
|
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
|
Job Scheduler module for Borealis
|
||||||
|
|
||||||
@@ -54,6 +58,26 @@ def _decode_base64_text(value: Any) -> Optional[str]:
|
|||||||
return decoded.decode("utf-8", errors="replace")
|
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:
|
def _decode_script_content(value: Any, encoding_hint: str = "") -> str:
|
||||||
encoding = (encoding_hint or "").strip().lower()
|
encoding = (encoding_hint or "").strip().lower()
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
@@ -311,6 +335,8 @@ class JobScheduler:
|
|||||||
self._online_lookup: Optional[Callable[[], List[str]]] = None
|
self._online_lookup: Optional[Callable[[], List[str]]] = None
|
||||||
# Optional callback to execute Ansible directly from the server
|
# Optional callback to execute Ansible directly from the server
|
||||||
self._server_ansible_runner: Optional[Callable[..., str]] = None
|
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
|
# Ensure run-history table exists
|
||||||
self._init_tables()
|
self._init_tables()
|
||||||
@@ -485,6 +511,7 @@ class JobScheduler:
|
|||||||
scheduled_run_row_id: int,
|
scheduled_run_row_id: int,
|
||||||
run_mode: str,
|
run_mode: str,
|
||||||
credential_id: Optional[int] = None,
|
credential_id: Optional[int] = None,
|
||||||
|
use_service_account: bool = False,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
import os, uuid
|
import os, uuid
|
||||||
@@ -522,7 +549,24 @@ class JobScheduler:
|
|||||||
variables = doc.get("variables") or []
|
variables = doc.get("variables") or []
|
||||||
files = doc.get("files") or []
|
files = doc.get("files") or []
|
||||||
run_mode_norm = (run_mode or "system").strip().lower()
|
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
|
# Record in activity_history for UI parity
|
||||||
now = _now_ts()
|
now = _now_ts()
|
||||||
@@ -743,6 +787,9 @@ class JobScheduler:
|
|||||||
def _conn(self):
|
def _conn(self):
|
||||||
return sqlite3.connect(self.db_path)
|
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):
|
def _init_tables(self):
|
||||||
conn = self._conn()
|
conn = self._conn()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@@ -954,7 +1001,7 @@ class JobScheduler:
|
|||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
cur.execute(
|
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()
|
jobs = cur.fetchall()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -972,7 +1019,18 @@ class JobScheduler:
|
|||||||
five_min = 300
|
five_min = 300
|
||||||
now_min = _now_minute()
|
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:
|
try:
|
||||||
# Targets list for this job
|
# Targets list for this job
|
||||||
try:
|
try:
|
||||||
@@ -1008,6 +1066,9 @@ class JobScheduler:
|
|||||||
continue
|
continue
|
||||||
run_mode = (execution_context or "system").strip().lower()
|
run_mode = (execution_context or "system").strip().lower()
|
||||||
job_credential_id = None
|
job_credential_id = None
|
||||||
|
job_use_service_account = bool(use_service_account_flag)
|
||||||
|
if run_mode != "winrm":
|
||||||
|
job_use_service_account = False
|
||||||
try:
|
try:
|
||||||
job_credential_id = int(credential_id) if credential_id is not None else None
|
job_credential_id = int(credential_id) if credential_id is not None else None
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1098,7 +1159,7 @@ class JobScheduler:
|
|||||||
run_row_id = c2.lastrowid or 0
|
run_row_id = c2.lastrowid or 0
|
||||||
conn2.commit()
|
conn2.commit()
|
||||||
activity_links: List[Dict[str, Any]] = []
|
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:
|
if remote_requires_cred and not job_credential_id:
|
||||||
err_msg = "Credential required for remote execution"
|
err_msg = "Credential required for remote execution"
|
||||||
c2.execute(
|
c2.execute(
|
||||||
@@ -1132,6 +1193,7 @@ class JobScheduler:
|
|||||||
run_row_id,
|
run_row_id,
|
||||||
run_mode,
|
run_mode,
|
||||||
job_credential_id,
|
job_credential_id,
|
||||||
|
job_use_service_account,
|
||||||
)
|
)
|
||||||
if link and link.get("activity_id"):
|
if link and link.get("activity_id"):
|
||||||
activity_links.append({
|
activity_links.append({
|
||||||
@@ -1243,9 +1305,10 @@ class JobScheduler:
|
|||||||
"expiration": r[7] or "no_expire",
|
"expiration": r[7] or "no_expire",
|
||||||
"execution_context": r[8] or "system",
|
"execution_context": r[8] or "system",
|
||||||
"credential_id": r[9],
|
"credential_id": r[9],
|
||||||
"enabled": bool(r[10] or 0),
|
"use_service_account": bool(r[10] or 0),
|
||||||
"created_at": r[11] or 0,
|
"enabled": bool(r[11] or 0),
|
||||||
"updated_at": r[12] or 0,
|
"created_at": r[12] or 0,
|
||||||
|
"updated_at": r[13] or 0,
|
||||||
}
|
}
|
||||||
# Attach computed status summary for latest occurrence
|
# Attach computed status summary for latest occurrence
|
||||||
try:
|
try:
|
||||||
@@ -1322,7 +1385,8 @@ class JobScheduler:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
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
|
FROM scheduled_jobs
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
"""
|
"""
|
||||||
@@ -1350,6 +1414,8 @@ class JobScheduler:
|
|||||||
credential_id = int(credential_id) if credential_id is not None else None
|
credential_id = int(credential_id) if credential_id is not None else None
|
||||||
except Exception:
|
except Exception:
|
||||||
credential_id = None
|
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)))
|
enabled = int(bool(data.get("enabled", True)))
|
||||||
if not name or not components or not targets:
|
if not name or not components or not targets:
|
||||||
return json.dumps({"error": "name, components, targets required"}), 400, {"Content-Type": "application/json"}
|
return json.dumps({"error": "name, components, targets required"}), 400, {"Content-Type": "application/json"}
|
||||||
@@ -1360,8 +1426,8 @@ class JobScheduler:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO scheduled_jobs
|
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)
|
(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 (?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
@@ -1373,6 +1439,7 @@ class JobScheduler:
|
|||||||
expiration,
|
expiration,
|
||||||
execution_context,
|
execution_context,
|
||||||
credential_id,
|
credential_id,
|
||||||
|
use_service_account,
|
||||||
enabled,
|
enabled,
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
@@ -1383,7 +1450,7 @@ class JobScheduler:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
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=?
|
FROM scheduled_jobs WHERE id=?
|
||||||
""",
|
""",
|
||||||
(job_id,),
|
(job_id,),
|
||||||
@@ -1402,7 +1469,7 @@ class JobScheduler:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
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=?
|
FROM scheduled_jobs WHERE id=?
|
||||||
""",
|
""",
|
||||||
(job_id,),
|
(job_id,),
|
||||||
@@ -1435,7 +1502,10 @@ class JobScheduler:
|
|||||||
if "expiration" in data or (data.get("duration") and "expiration" in data.get("duration")):
|
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"
|
fields["expiration"] = (data.get("duration") or {}).get("expiration") or data.get("expiration") or "no_expire"
|
||||||
if "execution_context" in data:
|
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:
|
if "credential_id" in data:
|
||||||
cred_val = data.get("credential_id")
|
cred_val = data.get("credential_id")
|
||||||
if cred_val in (None, "", "null"):
|
if cred_val in (None, "", "null"):
|
||||||
@@ -1445,6 +1515,8 @@ class JobScheduler:
|
|||||||
fields["credential_id"] = int(cred_val)
|
fields["credential_id"] = int(cred_val)
|
||||||
except Exception:
|
except Exception:
|
||||||
fields["credential_id"] = None
|
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:
|
if "enabled" in data:
|
||||||
fields["enabled"] = int(bool(data.get("enabled")))
|
fields["enabled"] = int(bool(data.get("enabled")))
|
||||||
if not fields:
|
if not fields:
|
||||||
@@ -1462,7 +1534,7 @@ class JobScheduler:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, components_json, targets_json, schedule_type, start_ts,
|
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=?
|
FROM scheduled_jobs WHERE id=?
|
||||||
""",
|
""",
|
||||||
(job_id,),
|
(job_id,),
|
||||||
@@ -1486,7 +1558,7 @@ class JobScheduler:
|
|||||||
return json.dumps({"error": "not found"}), 404, {"Content-Type": "application/json"}
|
return json.dumps({"error": "not found"}), 404, {"Content-Type": "application/json"}
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.execute(
|
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,),
|
(job_id,),
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
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]):
|
def set_server_ansible_runner(scheduler: JobScheduler, fn: Callable[..., str]):
|
||||||
scheduler._server_ansible_runner = fn
|
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 register as register_job_scheduler
|
||||||
from job_scheduler import set_online_lookup as scheduler_set_online_lookup
|
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_server_ansible_runner as scheduler_set_server_runner
|
||||||
|
from job_scheduler import set_credential_fetcher as scheduler_set_credential_fetcher
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Section: Runtime Stack Configuration
|
# Section: Runtime Stack Configuration
|
||||||
@@ -1859,6 +1860,11 @@ def _ensure_ansible_workspace() -> str:
|
|||||||
return _ANSIBLE_WORKSPACE_DIR
|
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]]:
|
def _fetch_credential_with_secrets(credential_id: int) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
conn = _db_conn()
|
conn = _db_conn()
|
||||||
@@ -1876,7 +1882,8 @@ def _fetch_credential_with_secrets(credential_id: int) -> Optional[Dict[str, Any
|
|||||||
private_key_passphrase_encrypted,
|
private_key_passphrase_encrypted,
|
||||||
become_method,
|
become_method,
|
||||||
become_username,
|
become_username,
|
||||||
become_password_encrypted
|
become_password_encrypted,
|
||||||
|
metadata_json
|
||||||
FROM credentials
|
FROM credentials
|
||||||
WHERE id=?
|
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_method": _normalize_become_method(row[8]),
|
||||||
"become_username": row[9] or "",
|
"become_username": row[9] or "",
|
||||||
"become_password": _decrypt_secret(row[10]) if row[10] else "",
|
"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):
|
def _emit_ansible_recap_from_row(row):
|
||||||
if not row:
|
if not row:
|
||||||
@@ -3514,6 +3553,7 @@ _DEVICE_TABLE_COLUMNS = [
|
|||||||
"operating_system",
|
"operating_system",
|
||||||
"uptime",
|
"uptime",
|
||||||
"agent_id",
|
"agent_id",
|
||||||
|
"ansible_ee_ver",
|
||||||
"connection_type",
|
"connection_type",
|
||||||
"connection_endpoint",
|
"connection_endpoint",
|
||||||
]
|
]
|
||||||
@@ -3603,6 +3643,7 @@ def _assemble_device_snapshot(record: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"created": _ts_to_human(created_ts),
|
"created": _ts_to_human(created_ts),
|
||||||
"connection_type": _clean_device_str(record.get("connection_type")) or "",
|
"connection_type": _clean_device_str(record.get("connection_type")) or "",
|
||||||
"connection_endpoint": _clean_device_str(record.get("connection_endpoint")) or "",
|
"connection_endpoint": _clean_device_str(record.get("connection_endpoint")) or "",
|
||||||
|
"ansible_ee_ver": _clean_device_str(record.get("ansible_ee_ver")) or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
details = {
|
details = {
|
||||||
@@ -3747,6 +3788,7 @@ def _extract_device_columns(details: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
payload["uptime"] = _coerce_int(uptime_value)
|
payload["uptime"] = _coerce_int(uptime_value)
|
||||||
payload["agent_id"] = _clean_device_str(summary.get("agent_id"))
|
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(
|
payload["connection_type"] = _clean_device_str(
|
||||||
summary.get("connection_type")
|
summary.get("connection_type")
|
||||||
or summary.get("remote_type")
|
or summary.get("remote_type")
|
||||||
@@ -3815,9 +3857,10 @@ def _device_upsert(
|
|||||||
operating_system,
|
operating_system,
|
||||||
uptime,
|
uptime,
|
||||||
agent_id,
|
agent_id,
|
||||||
|
ansible_ee_ver,
|
||||||
connection_type,
|
connection_type,
|
||||||
connection_endpoint
|
connection_endpoint
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
ON CONFLICT(hostname) DO UPDATE SET
|
ON CONFLICT(hostname) DO UPDATE SET
|
||||||
description=excluded.description,
|
description=excluded.description,
|
||||||
created_at=COALESCE({DEVICE_TABLE}.created_at, excluded.created_at),
|
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),
|
operating_system=COALESCE(NULLIF(excluded.operating_system, ''), {DEVICE_TABLE}.operating_system),
|
||||||
uptime=COALESCE(NULLIF(excluded.uptime, 0), {DEVICE_TABLE}.uptime),
|
uptime=COALESCE(NULLIF(excluded.uptime, 0), {DEVICE_TABLE}.uptime),
|
||||||
agent_id=COALESCE(NULLIF(excluded.agent_id, ''), {DEVICE_TABLE}.agent_id),
|
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_type=COALESCE(NULLIF(excluded.connection_type, ''), {DEVICE_TABLE}.connection_type),
|
||||||
connection_endpoint=COALESCE(NULLIF(excluded.connection_endpoint, ''), {DEVICE_TABLE}.connection_endpoint)
|
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("operating_system"),
|
||||||
column_values.get("uptime"),
|
column_values.get("uptime"),
|
||||||
column_values.get("agent_id"),
|
column_values.get("agent_id"),
|
||||||
|
column_values.get("ansible_ee_ver"),
|
||||||
column_values.get("connection_type"),
|
column_values.get("connection_type"),
|
||||||
column_values.get("connection_endpoint"),
|
column_values.get("connection_endpoint"),
|
||||||
]
|
]
|
||||||
@@ -4160,7 +4205,10 @@ def init_db():
|
|||||||
last_user TEXT,
|
last_user TEXT,
|
||||||
operating_system TEXT,
|
operating_system TEXT,
|
||||||
uptime INTEGER,
|
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("operating_system", "TEXT")
|
||||||
_ensure_column("uptime", "INTEGER")
|
_ensure_column("uptime", "INTEGER")
|
||||||
_ensure_column("agent_id", "TEXT")
|
_ensure_column("agent_id", "TEXT")
|
||||||
|
_ensure_column("ansible_ee_ver", "TEXT")
|
||||||
_ensure_column("connection_type", "TEXT")
|
_ensure_column("connection_type", "TEXT")
|
||||||
_ensure_column("connection_endpoint", "TEXT")
|
_ensure_column("connection_endpoint", "TEXT")
|
||||||
|
|
||||||
@@ -4274,6 +4323,7 @@ def init_db():
|
|||||||
operating_system TEXT,
|
operating_system TEXT,
|
||||||
uptime INTEGER,
|
uptime INTEGER,
|
||||||
agent_id TEXT,
|
agent_id TEXT,
|
||||||
|
ansible_ee_ver TEXT,
|
||||||
connection_type TEXT,
|
connection_type TEXT,
|
||||||
connection_endpoint TEXT
|
connection_endpoint TEXT
|
||||||
)
|
)
|
||||||
@@ -4481,6 +4531,7 @@ def init_db():
|
|||||||
expiration TEXT,
|
expiration TEXT,
|
||||||
execution_context TEXT NOT NULL,
|
execution_context TEXT NOT NULL,
|
||||||
credential_id INTEGER,
|
credential_id INTEGER,
|
||||||
|
use_service_account INTEGER NOT NULL DEFAULT 1,
|
||||||
enabled INTEGER DEFAULT 1,
|
enabled INTEGER DEFAULT 1,
|
||||||
created_at INTEGER,
|
created_at INTEGER,
|
||||||
updated_at INTEGER
|
updated_at INTEGER
|
||||||
@@ -4492,6 +4543,8 @@ def init_db():
|
|||||||
sj_cols = [row[1] for row in cur.fetchall()]
|
sj_cols = [row[1] for row in cur.fetchall()]
|
||||||
if "credential_id" not in sj_cols:
|
if "credential_id" not in sj_cols:
|
||||||
cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN credential_id INTEGER")
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -4553,6 +4606,7 @@ ensure_default_admin()
|
|||||||
|
|
||||||
job_scheduler = register_job_scheduler(app, socketio, DB_PATH)
|
job_scheduler = register_job_scheduler(app, socketio, DB_PATH)
|
||||||
scheduler_set_server_runner(job_scheduler, _queue_server_ansible_run)
|
scheduler_set_server_runner(job_scheduler, _queue_server_ansible_run)
|
||||||
|
scheduler_set_credential_fetcher(job_scheduler, _fetch_credential_with_secrets)
|
||||||
job_scheduler.start()
|
job_scheduler.start()
|
||||||
|
|
||||||
# Provide scheduler with online device lookup based on registered agents
|
# 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()
|
rel_path = (data.get("playbook_path") or "").strip()
|
||||||
hostnames = data.get("hostnames") or []
|
hostnames = data.get("hostnames") or []
|
||||||
credential_id = data.get("credential_id")
|
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:
|
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}")
|
_ansible_log_server(f"[quick_run] invalid payload rel_path='{rel_path}' hostnames={hostnames}")
|
||||||
return jsonify({"error": "Missing playbook_path or hostnames[]"}), 400
|
return jsonify({"error": "Missing playbook_path or hostnames[]"}), 400
|
||||||
server_mode = False
|
server_mode = False
|
||||||
cred_id_int = None
|
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"):
|
if credential_id not in (None, "", "null"):
|
||||||
try:
|
try:
|
||||||
cred_id_int = int(credential_id)
|
cred_id_int = int(credential_id)
|
||||||
if cred_id_int <= 0:
|
if cred_id_int <= 0:
|
||||||
cred_id_int = None
|
cred_id_int = None
|
||||||
else:
|
|
||||||
server_mode = True
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"error": "Invalid credential_id"}), 400
|
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:
|
try:
|
||||||
root, abs_path, _ = _resolve_assembly_path('ansible', rel_path)
|
root, abs_path, _ = _resolve_assembly_path('ansible', rel_path)
|
||||||
if not os.path.isfile(abs_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 []
|
variables = doc.get('variables') if isinstance(doc.get('variables'), list) else []
|
||||||
files = doc.get('files') if isinstance(doc.get('files'), 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)
|
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:
|
if server_mode and not cred_id_int:
|
||||||
return jsonify({"error": "credential_id is required for server-side execution"}), 400
|
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 = []
|
results = []
|
||||||
for host in hostnames:
|
for host in hostnames:
|
||||||
# Create activity_history row so UI shows running state and can receive recap mirror
|
# Create activity_history row so UI shows running state and can receive recap mirror
|
||||||
@@ -6499,6 +6560,9 @@ def ansible_quick_run():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
results.append({"hostname": host, "run_id": run_id, "status": "Failed", "activity_job_id": job_id, "error": str(ex)})
|
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})
|
return jsonify({"results": results})
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
return jsonify({"error": str(ve)}), 400
|
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"
|
$preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
|
||||||
$preserveBackupPath = Join-Path $scriptDir "Update_Staging\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" {
|
Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" {
|
||||||
if (Test-Path $preservePath) {
|
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" {
|
Run-Step "Updating: Clean Up Folders to Prepare for Update" {
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
||||||
(Join-Path $scriptDir "Data"), `
|
(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" {
|
Run-Step "Updating: Clean Up Update Staging Folder" {
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user