mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Additional Ansible Changes
This commit is contained in:
61
Borealis.ps1
61
Borealis.ps1
@@ -121,6 +121,40 @@ function Write-AgentLog {
|
|||||||
"[$ts] $Message" | Out-File -FilePath $path -Append -Encoding UTF8
|
"[$ts] $Message" | Out-File -FilePath $path -Append -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script:Utf8CodePageChanged = $false
|
||||||
|
|
||||||
|
function Ensure-SystemUtf8CodePage {
|
||||||
|
param([string]$LogName = 'Install.log')
|
||||||
|
|
||||||
|
$codePageKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage'
|
||||||
|
$target = '65001'
|
||||||
|
try {
|
||||||
|
$props = Get-ItemProperty -Path $codePageKey -ErrorAction Stop
|
||||||
|
$currentAcp = ($props.ACP | ForEach-Object { $_.ToString() })
|
||||||
|
$currentOem = ($props.OEMCP | ForEach-Object { $_.ToString() })
|
||||||
|
Write-AgentLog -FileName $LogName -Message ("[UTF8] Detected ACP={0} OEMCP={1}" -f $currentAcp,$currentOem)
|
||||||
|
} catch {
|
||||||
|
Write-AgentLog -FileName $LogName -Message ("[UTF8] Failed to read code page info: {0}" -f $_.Exception.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentAcp -eq $target -and $currentOem -eq $target) {
|
||||||
|
Write-AgentLog -FileName $LogName -Message '[UTF8] System code pages already set to 65001 (UTF-8).'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-AgentLog -FileName $LogName -Message '[UTF8] Updating system code pages to UTF-8 (65001). Requires reboot to finalize.'
|
||||||
|
try {
|
||||||
|
Set-ItemProperty -Path $codePageKey -Name 'ACP' -Value $target -Force
|
||||||
|
Set-ItemProperty -Path $codePageKey -Name 'OEMCP' -Value $target -Force
|
||||||
|
try { Set-ItemProperty -Path $codePageKey -Name 'MACCP' -Value $target -Force } catch {}
|
||||||
|
$script:Utf8CodePageChanged = $true
|
||||||
|
Write-AgentLog -FileName $LogName -Message '[UTF8] Code page registry values updated successfully.'
|
||||||
|
} catch {
|
||||||
|
Write-AgentLog -FileName $LogName -Message ("[UTF8] Failed to update code pages: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Forcefully remove legacy and current Borealis services and tasks
|
# Forcefully remove legacy and current Borealis services and tasks
|
||||||
function Remove-BorealisServicesAndTasks {
|
function Remove-BorealisServicesAndTasks {
|
||||||
param([string]$LogName)
|
param([string]$LogName)
|
||||||
@@ -428,6 +462,7 @@ function InstallOrUpdate-BorealisAgent {
|
|||||||
$env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH
|
$env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH
|
||||||
Write-Host "Cleaning previous agent tasks/processes..." -ForegroundColor Yellow
|
Write-Host "Cleaning previous agent tasks/processes..." -ForegroundColor Yellow
|
||||||
Remove-BorealisServicesAndTasks -LogName 'Install.log'
|
Remove-BorealisServicesAndTasks -LogName 'Install.log'
|
||||||
|
Ensure-SystemUtf8CodePage -LogName 'Install.log'
|
||||||
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
|
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
|
||||||
|
|
||||||
# Resolve all paths relative to the script directory to avoid CWD issues
|
# Resolve all paths relative to the script directory to avoid CWD issues
|
||||||
@@ -479,6 +514,27 @@ function InstallOrUpdate-BorealisAgent {
|
|||||||
if (Test-Path $agentRequirements) {
|
if (Test-Path $agentRequirements) {
|
||||||
& $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null
|
& $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stubSource = Join-Path $agentSourceRoot 'fcntl_stub.py'
|
||||||
|
if (Test-Path $stubSource) {
|
||||||
|
$stubDest = Join-Path $venvFolderPath 'Lib\site-packages\fcntl.py'
|
||||||
|
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring Windows fcntl shim is installed.'
|
||||||
|
Copy-Item $stubSource $stubDest -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$termiosSource = Join-Path $agentSourceRoot 'termios_stub.py'
|
||||||
|
if (Test-Path $termiosSource) {
|
||||||
|
$termiosDest = Join-Path $venvFolderPath 'Lib\site-packages\termios.py'
|
||||||
|
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring Windows termios shim is installed.'
|
||||||
|
Copy-Item $termiosSource $termiosDest -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteCustomSource = Join-Path $agentSourceRoot 'sitecustomize.py'
|
||||||
|
if (Test-Path $siteCustomSource) {
|
||||||
|
$siteCustomDest = Join-Path $venvFolderPath 'Lib\site-packages\sitecustomize.py'
|
||||||
|
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring sitecustomize shim is installed.'
|
||||||
|
Copy-Item $siteCustomSource $siteCustomDest -Force
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Run-Step "Configure Agent Settings" {
|
Run-Step "Configure Agent Settings" {
|
||||||
@@ -512,6 +568,11 @@ function InstallOrUpdate-BorealisAgent {
|
|||||||
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
|
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
|
||||||
Write-Host "===================================================================================="
|
Write-Host "===================================================================================="
|
||||||
Ensure-AgentTasks -ScriptRoot $scriptDir
|
Ensure-AgentTasks -ScriptRoot $scriptDir
|
||||||
|
if ($script:Utf8CodePageChanged) {
|
||||||
|
$msg = 'System code pages set to UTF-8. A reboot is required before Ansible can run.'
|
||||||
|
Write-AgentLog -FileName 'Install.log' -Message ("[UTF8] {0}" -f $msg)
|
||||||
|
Write-Host "`n$msg" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------- Main ----------------------
|
# ---------------------- Main ----------------------
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ function Ensure-LocalhostWinRMHttps {
|
|||||||
# Harden auth and encryption
|
# Harden auth and encryption
|
||||||
try { winrm set winrm/config/service/auth @{Basic="false"; Kerberos="true"; Negotiate="true"; CredSSP="false"} | Out-Null } catch {}
|
try { winrm set winrm/config/service/auth @{Basic="false"; Kerberos="true"; Negotiate="true"; CredSSP="false"} | Out-Null } catch {}
|
||||||
try { winrm set winrm/config/service @{AllowUnencrypted="false"} | Out-Null } catch {}
|
try { winrm set winrm/config/service @{AllowUnencrypted="false"} | Out-Null } catch {}
|
||||||
|
try { winrm set winrm/config/service @{AllowFreshCredentialsWhenNTLMOnly="true"} | Out-Null } catch {}
|
||||||
|
try { winrm set winrm/config/service @{AllowCredSspAuthentication="false"} | Out-Null } catch {}
|
||||||
try { winrm set winrm/config/service @{IPv4Filter="127.0.0.1"} | Out-Null } catch {}
|
try { winrm set winrm/config/service @{IPv4Filter="127.0.0.1"} | Out-Null } catch {}
|
||||||
|
try { New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'LocalAccountTokenFilterPolicy' -PropertyType DWord -Value 1 -Force | Out-Null } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Ensure-BorealisServiceUser {
|
function Ensure-BorealisServiceUser {
|
||||||
@@ -62,6 +65,18 @@ function Ensure-BorealisServiceUser {
|
|||||||
Add-LocalGroupMember -Group "Administrators" -Member $localName -ErrorAction SilentlyContinue
|
Add-LocalGroupMember -Group "Administrators" -Member $localName -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
$legacy = 'svcBorealisAnsibleRunner'
|
||||||
|
if ($localName -ne $legacy) {
|
||||||
|
try {
|
||||||
|
$legacyUser = Get-LocalUser -Name $legacy -ErrorAction SilentlyContinue
|
||||||
|
if ($legacyUser) {
|
||||||
|
try { Remove-LocalGroupMember -Group "Administrators" -Member $legacy -ErrorAction SilentlyContinue } catch {}
|
||||||
|
try { Disable-LocalUser -Name $legacy -ErrorAction SilentlyContinue } catch {}
|
||||||
|
try { Remove-LocalUser -Name $legacy -ErrorAction SilentlyContinue } catch {}
|
||||||
|
try { cmd /c "net user $legacy /DELETE" | Out-Null } catch {}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Write-LocalInventory {
|
function Write-LocalInventory {
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import time
|
|||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import winrm # type: ignore
|
import winrm # type: ignore
|
||||||
except Exception:
|
except Exception:
|
||||||
winrm = None
|
winrm = None
|
||||||
|
|
||||||
|
DEFAULT_SERVICE_ACCOUNT = '.\\svcBorealis'
|
||||||
|
LEGACY_SERVICE_ACCOUNTS = {'.\\svcBorealisAnsibleRunner', 'svcBorealisAnsibleRunner'}
|
||||||
|
|
||||||
ROLE_NAME = 'playbook_exec_system'
|
ROLE_NAME = 'playbook_exec_system'
|
||||||
ROLE_CONTEXTS = ['system']
|
ROLE_CONTEXTS = ['system']
|
||||||
@@ -166,21 +169,24 @@ class Role:
|
|||||||
payload = {
|
payload = {
|
||||||
'agent_id': self.ctx.agent_id,
|
'agent_id': self.ctx.agent_id,
|
||||||
'hostname': socket.gethostname(),
|
'hostname': socket.gethostname(),
|
||||||
'username': '.\\svcBorealisAnsibleRunner',
|
'username': DEFAULT_SERVICE_ACCOUNT,
|
||||||
}
|
}
|
||||||
self._ansible_log(f"[checkin] POST {url} agent_id={self.ctx.agent_id}")
|
self._ansible_log(f"[checkin] POST {url} agent_id={self.ctx.agent_id}")
|
||||||
timeout = aiohttp.ClientTimeout(total=15)
|
timeout = aiohttp.ClientTimeout(total=15)
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
||||||
async with sess.post(url, json=payload) as resp:
|
async with sess.post(url, json=payload) as resp:
|
||||||
js = await resp.json()
|
js = await resp.json()
|
||||||
u = (js or {}).get('username') or '.\\svcBorealisAnsibleRunner'
|
u = (js or {}).get('username') or DEFAULT_SERVICE_ACCOUNT
|
||||||
p = (js or {}).get('password') or ''
|
p = (js or {}).get('password') or ''
|
||||||
|
if u in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
self._ansible_log(f"[checkin] legacy service username {u!r}; requesting rotate", error=True)
|
||||||
|
return await self._rotate_service_creds(reason='legacy_username', force_username=DEFAULT_SERVICE_ACCOUNT)
|
||||||
self._svc_creds = {'username': u, 'password': p}
|
self._svc_creds = {'username': u, 'password': p}
|
||||||
self._ansible_log(f"[checkin] received user={u} pw_len={len(p)}")
|
self._ansible_log(f"[checkin] received user={u} pw_len={len(p)}")
|
||||||
return self._svc_creds
|
return self._svc_creds
|
||||||
except Exception:
|
except Exception:
|
||||||
self._ansible_log(f"[checkin] failed agent_id={self.ctx.agent_id}", error=True)
|
self._ansible_log(f"[checkin] failed agent_id={self.ctx.agent_id}", error=True)
|
||||||
return {'username': '.\\svcBorealisAnsibleRunner', 'password': ''}
|
return {'username': DEFAULT_SERVICE_ACCOUNT, 'password': ''}
|
||||||
|
|
||||||
def _normalize_playbook_content(self, content: str) -> str:
|
def _normalize_playbook_content(self, content: str) -> str:
|
||||||
try:
|
try:
|
||||||
@@ -202,21 +208,28 @@ class Role:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
async def _rotate_service_creds(self) -> dict:
|
async def _rotate_service_creds(self, reason: str = 'bad_credentials', force_username: Optional[str] = None) -> dict:
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
url = self._server_base().rstrip('/') + '/api/agent/service-account/rotate'
|
url = self._server_base().rstrip('/') + '/api/agent/service-account/rotate'
|
||||||
payload = {
|
payload = {
|
||||||
'agent_id': self.ctx.agent_id,
|
'agent_id': self.ctx.agent_id,
|
||||||
'reason': 'bad_credentials',
|
'reason': reason,
|
||||||
}
|
}
|
||||||
|
if force_username:
|
||||||
|
payload['username'] = force_username
|
||||||
self._ansible_log(f"[rotate] POST {url} agent_id={self.ctx.agent_id}")
|
self._ansible_log(f"[rotate] POST {url} agent_id={self.ctx.agent_id}")
|
||||||
timeout = aiohttp.ClientTimeout(total=15)
|
timeout = aiohttp.ClientTimeout(total=15)
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
||||||
async with sess.post(url, json=payload) as resp:
|
async with sess.post(url, json=payload) as resp:
|
||||||
js = await resp.json()
|
js = await resp.json()
|
||||||
u = (js or {}).get('username') or '.\\svcBorealisAnsibleRunner'
|
u = (js or {}).get('username') or force_username or DEFAULT_SERVICE_ACCOUNT
|
||||||
p = (js or {}).get('password') or ''
|
p = (js or {}).get('password') or ''
|
||||||
|
if u in LEGACY_SERVICE_ACCOUNTS and force_username != DEFAULT_SERVICE_ACCOUNT:
|
||||||
|
self._ansible_log(f"[rotate] legacy username {u!r} returned; retrying with default", error=True)
|
||||||
|
return await self._rotate_service_creds(reason='legacy_username', force_username=DEFAULT_SERVICE_ACCOUNT)
|
||||||
|
if u in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
u = DEFAULT_SERVICE_ACCOUNT
|
||||||
self._svc_creds = {'username': u, 'password': p}
|
self._svc_creds = {'username': u, 'password': p}
|
||||||
self._ansible_log(f"[rotate] received user={u} pw_len={len(p)}")
|
self._ansible_log(f"[rotate] received user={u} pw_len={len(p)}")
|
||||||
return self._svc_creds
|
return self._svc_creds
|
||||||
@@ -384,7 +397,7 @@ try {{
|
|||||||
self._ansible_log(f"[runner] prepared playbook={play_abs} bytes={len(_norm.encode('utf-8'))}")
|
self._ansible_log(f"[runner] prepared playbook={play_abs} bytes={len(_norm.encode('utf-8'))}")
|
||||||
# WinRM service account credentials
|
# WinRM service account credentials
|
||||||
creds = await self._fetch_service_creds()
|
creds = await self._fetch_service_creds()
|
||||||
user = creds.get('username') or '.\\svcBorealisAnsibleRunner'
|
user = creds.get('username') or DEFAULT_SERVICE_ACCOUNT
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
# Converge endpoint state (listener + user)
|
# Converge endpoint state (listener + user)
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
@@ -392,7 +405,7 @@ try {{
|
|||||||
pre_ok = self._winrm_preflight(user, pwd)
|
pre_ok = self._winrm_preflight(user, pwd)
|
||||||
if not pre_ok:
|
if not pre_ok:
|
||||||
# rotate and retry once
|
# rotate and retry once
|
||||||
creds = await self._rotate_service_creds()
|
creds = await self._rotate_service_creds(reason='winrm_preflight_failure')
|
||||||
user = creds.get('username') or user
|
user = creds.get('username') or user
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
@@ -508,7 +521,7 @@ try {{
|
|||||||
# If authentication failed on first pass, rotate password and try once more
|
# If authentication failed on first pass, rotate password and try once more
|
||||||
if auth_failed:
|
if auth_failed:
|
||||||
try:
|
try:
|
||||||
newc = await self._rotate_service_creds()
|
newc = await self._rotate_service_creds(reason='auth_failed_retry')
|
||||||
user2 = newc.get('username') or user
|
user2 = newc.get('username') or user
|
||||||
pwd2 = newc.get('password') or ''
|
pwd2 = newc.get('password') or ''
|
||||||
self._ensure_winrm_and_user(user2, pwd2)
|
self._ensure_winrm_and_user(user2, pwd2)
|
||||||
@@ -551,11 +564,11 @@ try {{
|
|||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
try:
|
try:
|
||||||
creds = await self._fetch_service_creds()
|
creds = await self._fetch_service_creds()
|
||||||
user = creds.get('username') or '.\\svcBorealisAnsibleRunner'
|
user = creds.get('username') or DEFAULT_SERVICE_ACCOUNT
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
if not self._winrm_preflight(user, pwd):
|
if not self._winrm_preflight(user, pwd):
|
||||||
creds = await self._rotate_service_creds()
|
creds = await self._rotate_service_creds(reason='winrm_preflight_failure')
|
||||||
user = creds.get('username') or user
|
user = creds.get('username') or user
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
@@ -576,7 +589,7 @@ try {{
|
|||||||
use_module = True
|
use_module = True
|
||||||
if use_module:
|
if use_module:
|
||||||
py = _venv_python() or sys.executable
|
py = _venv_python() or sys.executable
|
||||||
base_cmd = [py, '-m', 'ansible.cli.playbook']
|
base_cmd = [py, '-X', 'utf8', '-m', 'ansible.cli.playbook']
|
||||||
self._ansible_log(f"[cli] ansible-playbook not found; using python -m ansible.cli.playbook via {py}")
|
self._ansible_log(f"[cli] ansible-playbook not found; using python -m ansible.cli.playbook via {py}")
|
||||||
else:
|
else:
|
||||||
base_cmd = [ap]
|
base_cmd = [ap]
|
||||||
@@ -593,21 +606,23 @@ try {{
|
|||||||
self._ansible_log(f"[cli] cmd={' '.join(cmd)}")
|
self._ansible_log(f"[cli] cmd={' '.join(cmd)}")
|
||||||
# Ensure clean, plain output and correct interpreter for localhost
|
# Ensure clean, plain output and correct interpreter for localhost
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.setdefault('ANSIBLE_FORCE_COLOR', '0')
|
env['ANSIBLE_FORCE_COLOR'] = '0'
|
||||||
env.setdefault('ANSIBLE_NOCOLOR', '1')
|
env['ANSIBLE_NOCOLOR'] = '1'
|
||||||
env.setdefault('PYTHONIOENCODING', 'utf-8')
|
env['PYTHONIOENCODING'] = 'utf-8'
|
||||||
env.setdefault('PYTHONUTF8', '1')
|
env['PYTHONUTF8'] = '1'
|
||||||
|
env['ANSIBLE_STDOUT_CALLBACK'] = 'default'
|
||||||
|
env['ANSIBLE_LOCALHOST_WARNING'] = '0'
|
||||||
|
env['ANSIBLE_COLLECTIONS_PATHS'] = _collections_dir()
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
env.setdefault('LANG', 'en_US.UTF-8')
|
env['LANG'] = 'en_US.UTF-8'
|
||||||
env.setdefault('ANSIBLE_STDOUT_CALLBACK', 'default')
|
env['LC_ALL'] = 'en_US.UTF-8'
|
||||||
# Help Ansible pick the correct python for localhost
|
env['LC_CTYPE'] = 'en_US.UTF-8'
|
||||||
env.setdefault('ANSIBLE_LOCALHOST_WARNING', '0')
|
|
||||||
# Ensure collections path is discoverable
|
|
||||||
env.setdefault('ANSIBLE_COLLECTIONS_PATHS', _collections_dir())
|
|
||||||
vp = _venv_python()
|
vp = _venv_python()
|
||||||
if vp:
|
if vp:
|
||||||
env.setdefault('ANSIBLE_PYTHON_INTERPRETER', vp)
|
env.setdefault('ANSIBLE_PYTHON_INTERPRETER', vp)
|
||||||
|
|
||||||
|
self._ansible_log(f"[cli] locale env overrides: PYTHONUTF8={env.get('PYTHONUTF8')} LANG={env.get('LANG')} LC_ALL={env.get('LC_ALL')} LC_CTYPE={env.get('LC_CTYPE')}")
|
||||||
|
|
||||||
creationflags = 0
|
creationflags = 0
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
# CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
# CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
||||||
@@ -751,7 +766,7 @@ try {{
|
|||||||
if os.name != 'nt':
|
if os.name != 'nt':
|
||||||
return
|
return
|
||||||
creds = await self._fetch_service_creds()
|
creds = await self._fetch_service_creds()
|
||||||
user = creds.get('username') or '.\\svcBorealisAnsibleRunner'
|
user = creds.get('username') or DEFAULT_SERVICE_ACCOUNT
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
self._ansible_log(f"[bootstrap] ensure winrm+user user={user} pw_len={len(pwd)}")
|
self._ansible_log(f"[bootstrap] ensure winrm+user user={user} pw_len={len(pwd)}")
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
@@ -759,7 +774,7 @@ try {{
|
|||||||
self._ansible_log(f"[bootstrap] preflight_ok={ok}")
|
self._ansible_log(f"[bootstrap] preflight_ok={ok}")
|
||||||
if not ok:
|
if not ok:
|
||||||
self._ansible_log("[bootstrap] preflight failed; rotating creds", error=True)
|
self._ansible_log("[bootstrap] preflight failed; rotating creds", error=True)
|
||||||
creds = await self._rotate_service_creds()
|
creds = await self._rotate_service_creds(reason='bootstrap_preflight_failed')
|
||||||
user = creds.get('username') or user
|
user = creds.get('username') or user
|
||||||
pwd = creds.get('password') or ''
|
pwd = creds.get('password') or ''
|
||||||
self._ensure_winrm_and_user(user, pwd)
|
self._ensure_winrm_and_user(user, pwd)
|
||||||
|
|||||||
@@ -1178,7 +1178,7 @@ async def connect():
|
|||||||
async def _svc_checkin_once():
|
async def _svc_checkin_once():
|
||||||
try:
|
try:
|
||||||
url = get_server_url().rstrip('/') + "/api/agent/checkin"
|
url = get_server_url().rstrip('/') + "/api/agent/checkin"
|
||||||
payload = {"agent_id": AGENT_ID, "hostname": socket.gethostname(), "username": ".\\svcBorealisAnsibleRunner"}
|
payload = {"agent_id": AGENT_ID, "hostname": socket.gethostname(), "username": ".\\svcBorealis"}
|
||||||
timeout = aiohttp.ClientTimeout(total=10)
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
await session.post(url, json=payload)
|
await session.post(url, json=payload)
|
||||||
|
|||||||
90
Data/Agent/fcntl_stub.py
Normal file
90
Data/Agent/fcntl_stub.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Windows compatibility layer for the POSIX fcntl module."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import msvcrt
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.debug("Borealis fcntl shim active (Windows)")
|
||||||
|
|
||||||
|
F_DUPFD = 0
|
||||||
|
F_GETFD = 1
|
||||||
|
F_SETFD = 2
|
||||||
|
F_GETFL = 3
|
||||||
|
F_SETFL = 4
|
||||||
|
|
||||||
|
LOCK_UN = 8
|
||||||
|
LOCK_SH = 1
|
||||||
|
LOCK_EX = 2
|
||||||
|
LOCK_NB = 4
|
||||||
|
|
||||||
|
__fd_flags: Dict[int, int] = {}
|
||||||
|
__locks: Dict[int, Tuple[int, int]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _winsize() -> bytes:
|
||||||
|
rows, cols = shutil.get_terminal_size(fallback=(24, 80))
|
||||||
|
rows = max(1, int(rows))
|
||||||
|
cols = max(1, int(cols))
|
||||||
|
return struct.pack("HHHH", rows, cols, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def ioctl(fd: int, op: int, arg: Any = 0) -> bytes:
|
||||||
|
return _winsize()
|
||||||
|
|
||||||
|
|
||||||
|
def fcntl(fd: int, op: int, arg: Any = 0) -> Any:
|
||||||
|
if op == F_GETFL:
|
||||||
|
return __fd_flags.get(fd, 0)
|
||||||
|
if op == F_SETFL:
|
||||||
|
try:
|
||||||
|
__fd_flags[fd] = int(arg)
|
||||||
|
except Exception:
|
||||||
|
__fd_flags[fd] = 0
|
||||||
|
return 0
|
||||||
|
return arg
|
||||||
|
|
||||||
|
|
||||||
|
def flock(fd: int, op: int) -> None:
|
||||||
|
if op & LOCK_UN:
|
||||||
|
if fd in __locks:
|
||||||
|
_apply_lock(fd, msvcrt.LK_UNLCK, *__locks.pop(fd))
|
||||||
|
return
|
||||||
|
|
||||||
|
length = 1
|
||||||
|
nb = bool(op & LOCK_NB)
|
||||||
|
mode = msvcrt.LK_LOCK if not nb else msvcrt.LK_NBLCK
|
||||||
|
try:
|
||||||
|
_apply_lock(fd, mode, 0, length)
|
||||||
|
__locks[fd] = (0, length)
|
||||||
|
except OSError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def lockf(fd: int, cmd: int, length: int = 0) -> None:
|
||||||
|
if length <= 0:
|
||||||
|
length = 1
|
||||||
|
if cmd & LOCK_UN:
|
||||||
|
if fd in __locks:
|
||||||
|
_apply_lock(fd, msvcrt.LK_UNLCK, *__locks.pop(fd))
|
||||||
|
return
|
||||||
|
nb = bool(cmd & LOCK_NB)
|
||||||
|
mode = msvcrt.LK_LOCK if not nb else msvcrt.LK_NBLCK
|
||||||
|
_apply_lock(fd, mode, 0, length)
|
||||||
|
__locks[fd] = (0, length)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_lock(fd: int, mode: int, offset: int, length: int) -> None:
|
||||||
|
cur = os.lseek(fd, 0, os.SEEK_CUR)
|
||||||
|
os.lseek(fd, offset, os.SEEK_SET)
|
||||||
|
try:
|
||||||
|
msvcrt.locking(fd, mode, length)
|
||||||
|
finally:
|
||||||
|
os.lseek(fd, cur, os.SEEK_SET)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [name for name in globals() if name.isupper() or name in {'ioctl', 'fcntl', 'flock', 'lockf'}]
|
||||||
25
Data/Agent/sitecustomize.py
Normal file
25
Data/Agent/sitecustomize.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Borealis controller shims executed on interpreter startup."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
multiprocessing.set_start_method('spawn')
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
mp_ctx = multiprocessing.get_context('fork')
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
import ansible.utils.multiprocessing as ansible_mp # type: ignore
|
||||||
|
except Exception:
|
||||||
|
log.debug('Borealis sitecustomize: ansible multiprocessing module missing; skipping fork patch')
|
||||||
|
else:
|
||||||
|
ansible_mp.context = multiprocessing.get_context('spawn')
|
||||||
|
log.debug('Borealis sitecustomize: patched ansible multiprocessing context to spawn')
|
||||||
|
else:
|
||||||
|
log.debug('Borealis sitecustomize: fork context available (%s)', mp_ctx)
|
||||||
121
Data/Agent/termios_stub.py
Normal file
121
Data/Agent/termios_stub.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Windows compatibility shim for termios used by Ansible."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Iterable, List
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.debug("Borealis termios shim active (Windows)")
|
||||||
|
|
||||||
|
NCCS = 32
|
||||||
|
|
||||||
|
VINTR = 0
|
||||||
|
VQUIT = 1
|
||||||
|
VERASE = 2
|
||||||
|
VKILL = 3
|
||||||
|
VEOF = 4
|
||||||
|
VTIME = 5
|
||||||
|
VMIN = 6
|
||||||
|
VSTART = 8
|
||||||
|
VSTOP = 9
|
||||||
|
VSUSP = 10
|
||||||
|
VEOL = 11
|
||||||
|
VREPRINT = 12
|
||||||
|
VDISCARD = 13
|
||||||
|
VWERASE = 14
|
||||||
|
VLNEXT = 15
|
||||||
|
VEOL2 = 16
|
||||||
|
|
||||||
|
IGNBRK = 0x0001
|
||||||
|
BRKINT = 0x0002
|
||||||
|
IGNPAR = 0x0004
|
||||||
|
PARMRK = 0x0008
|
||||||
|
INPCK = 0x0010
|
||||||
|
ISTRIP = 0x0020
|
||||||
|
INLCR = 0x0040
|
||||||
|
IGNCR = 0x0080
|
||||||
|
ICRNL = 0x0100
|
||||||
|
IXON = 0x0400
|
||||||
|
IXOFF = 0x1000
|
||||||
|
|
||||||
|
OPOST = 0x0001
|
||||||
|
ONLCR = 0x0004
|
||||||
|
OCRNL = 0x0008
|
||||||
|
ONOCR = 0x0010
|
||||||
|
ONLRET = 0x0020
|
||||||
|
OFILL = 0x0040
|
||||||
|
|
||||||
|
CSIZE = 0x0030
|
||||||
|
CS8 = 0x0030
|
||||||
|
CSTOPB = 0x0040
|
||||||
|
CREAD = 0x0080
|
||||||
|
PARENB = 0x0100
|
||||||
|
PARODD = 0x0200
|
||||||
|
CLOCAL = 0x0800
|
||||||
|
|
||||||
|
ISIG = 0x0001
|
||||||
|
ICANON = 0x0002
|
||||||
|
ECHO = 0x0008
|
||||||
|
ECHOE = 0x0010
|
||||||
|
ECHOK = 0x0020
|
||||||
|
ECHONL = 0x0040
|
||||||
|
IEXTEN = 0x0200
|
||||||
|
|
||||||
|
TCSANOW = 0
|
||||||
|
TCSADRAIN = 1
|
||||||
|
TCSAFLUSH = 2
|
||||||
|
|
||||||
|
_default_cc: List[int] = [0] * NCCS
|
||||||
|
|
||||||
|
|
||||||
|
def _make_termios() -> list:
|
||||||
|
return [0, 0, 0, 0, 0, 0, _default_cc.copy()]
|
||||||
|
|
||||||
|
|
||||||
|
def tcgetattr(fd: int) -> list:
|
||||||
|
return _make_termios()
|
||||||
|
|
||||||
|
|
||||||
|
def tcsetattr(fd: int, when: int, attributes: Iterable) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cfgetospeed(attrs: Iterable) -> int:
|
||||||
|
return 9600
|
||||||
|
|
||||||
|
|
||||||
|
def cfgetispeed(attrs: Iterable) -> int:
|
||||||
|
return 9600
|
||||||
|
|
||||||
|
|
||||||
|
def cfsetospeed(attrs: Iterable, speed: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cfsetispeed(attrs: Iterable, speed: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tcflush(fd: int, queue: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tcflow(fd: int, action: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tcsendbreak(fd: int, duration: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tcdrain(fd: int) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def tcgetsid(fd: int) -> int:
|
||||||
|
raise OSError("tcgetsid not supported on Windows")
|
||||||
|
|
||||||
|
__all__ = [name for name in globals() if name.isupper() or name in {
|
||||||
|
'tcgetattr', 'tcsetattr', 'cfgetospeed', 'cfgetispeed', 'cfsetospeed',
|
||||||
|
'cfsetispeed', 'tcflush', 'tcflow', 'tcsendbreak', 'tcdrain', 'tcgetsid'
|
||||||
|
}]
|
||||||
@@ -69,6 +69,9 @@ def _write_service_log(service: str, msg: str):
|
|||||||
def _ansible_log_server(msg: str):
|
def _ansible_log_server(msg: str):
|
||||||
_write_service_log('ansible', msg)
|
_write_service_log('ansible', msg)
|
||||||
|
|
||||||
|
DEFAULT_SERVICE_ACCOUNT = '.\\svcBorealis'
|
||||||
|
LEGACY_SERVICE_ACCOUNTS = {'.\\svcBorealisAnsibleRunner', 'svcBorealisAnsibleRunner'}
|
||||||
|
|
||||||
# Borealis Python API Endpoints
|
# Borealis Python API Endpoints
|
||||||
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
||||||
from Python_API_Endpoints.script_engines import run_powershell_script
|
from Python_API_Endpoints.script_engines import run_powershell_script
|
||||||
@@ -2906,6 +2909,9 @@ def _service_acct_get(conn, agent_id: str):
|
|||||||
|
|
||||||
|
|
||||||
def _service_acct_set(conn, agent_id: str, username: str, plaintext_password: str):
|
def _service_acct_set(conn, agent_id: str, username: str, plaintext_password: str):
|
||||||
|
username = (username or '').strip()
|
||||||
|
if not username or username in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
username = DEFAULT_SERVICE_ACCOUNT
|
||||||
enc = _encrypt_secret(plaintext_password)
|
enc = _encrypt_secret(plaintext_password)
|
||||||
now_utc = _now_iso_utc()
|
now_utc = _now_iso_utc()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@@ -2929,15 +2935,17 @@ def _service_acct_set(conn, agent_id: str, username: str, plaintext_password: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/agent/checkin', methods=['POST'])
|
@app.route('/api/agent/checkin', methods=['POST'])
|
||||||
def api_agent_checkin():
|
def api_agent_checkin():
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
agent_id = (payload.get('agent_id') or '').strip()
|
agent_id = (payload.get('agent_id') or '').strip()
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
return jsonify({'error': 'agent_id required'}), 400
|
return jsonify({'error': 'agent_id required'}), 400
|
||||||
username = (payload.get('username') or '.\\svcBorealisAnsibleRunner').strip()
|
raw_username = (payload.get('username') or '').strip()
|
||||||
# Optional hostname here for future auditing/joins
|
username = raw_username or DEFAULT_SERVICE_ACCOUNT
|
||||||
# Upsert service account, creating new creds if missing
|
if username in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
username = DEFAULT_SERVICE_ACCOUNT
|
||||||
try:
|
try:
|
||||||
conn = _db_conn()
|
conn = _db_conn()
|
||||||
row = _service_acct_get(conn, agent_id)
|
row = _service_acct_get(conn, agent_id)
|
||||||
@@ -2946,17 +2954,25 @@ def api_agent_checkin():
|
|||||||
out = _service_acct_set(conn, agent_id, username, pw)
|
out = _service_acct_set(conn, agent_id, username, pw)
|
||||||
_ansible_log_server(f"[checkin] created creds agent_id={agent_id} user={out['username']} rotated={out['last_rotated_utc']}")
|
_ansible_log_server(f"[checkin] created creds agent_id={agent_id} user={out['username']} rotated={out['last_rotated_utc']}")
|
||||||
else:
|
else:
|
||||||
# row: agent_id, username, password_encrypted, last_rotated_utc, version
|
stored_username = (row[1] or '').strip()
|
||||||
try:
|
try:
|
||||||
plain = _decrypt_secret(row[2])
|
plain = _decrypt_secret(row[2])
|
||||||
except Exception:
|
except Exception:
|
||||||
plain = ''
|
plain = ''
|
||||||
if not plain:
|
if stored_username in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
if not plain:
|
||||||
|
plain = _gen_strong_password()
|
||||||
|
out = _service_acct_set(conn, agent_id, DEFAULT_SERVICE_ACCOUNT, plain)
|
||||||
|
_ansible_log_server(f"[checkin] upgraded legacy service user for agent_id={agent_id} -> {out['username']}")
|
||||||
|
elif not plain:
|
||||||
plain = _gen_strong_password()
|
plain = _gen_strong_password()
|
||||||
out = _service_acct_set(conn, agent_id, row[1] or username, plain)
|
out = _service_acct_set(conn, agent_id, stored_username or username, plain)
|
||||||
else:
|
else:
|
||||||
|
eff_user = stored_username or username
|
||||||
|
if eff_user in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
eff_user = DEFAULT_SERVICE_ACCOUNT
|
||||||
out = {
|
out = {
|
||||||
'username': row[1] or username,
|
'username': eff_user,
|
||||||
'password': plain,
|
'password': plain,
|
||||||
'last_rotated_utc': row[3] or _now_iso_utc(),
|
'last_rotated_utc': row[3] or _now_iso_utc(),
|
||||||
}
|
}
|
||||||
@@ -2978,11 +2994,17 @@ def api_agent_service_account_rotate():
|
|||||||
agent_id = (payload.get('agent_id') or '').strip()
|
agent_id = (payload.get('agent_id') or '').strip()
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
return jsonify({'error': 'agent_id required'}), 400
|
return jsonify({'error': 'agent_id required'}), 400
|
||||||
username = (payload.get('username') or '.\\svcBorealisAnsibleRunner').strip()
|
requested_username = (payload.get('username') or '').strip()
|
||||||
try:
|
try:
|
||||||
conn = _db_conn()
|
conn = _db_conn()
|
||||||
row = _service_acct_get(conn, agent_id)
|
row = _service_acct_get(conn, agent_id)
|
||||||
user_eff = row[1] if row else username
|
stored_username = ''
|
||||||
|
if row:
|
||||||
|
stored_username = (row[1] or '').strip()
|
||||||
|
user_eff = requested_username or stored_username or DEFAULT_SERVICE_ACCOUNT
|
||||||
|
if user_eff in LEGACY_SERVICE_ACCOUNTS:
|
||||||
|
user_eff = DEFAULT_SERVICE_ACCOUNT
|
||||||
|
_ansible_log_server(f"[rotate] upgrading legacy service user for agent_id={agent_id}")
|
||||||
pw_new = _gen_strong_password()
|
pw_new = _gen_strong_password()
|
||||||
out = _service_acct_set(conn, agent_id, user_eff, pw_new)
|
out = _service_acct_set(conn, agent_id, user_eff, pw_new)
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -2996,7 +3018,6 @@ def api_agent_service_account_rotate():
|
|||||||
_ansible_log_server(f"[rotate] error agent_id={agent_id} err={e}")
|
_ansible_log_server(f"[rotate] error agent_id={agent_id} err={e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/ansible/recap/report", methods=["POST"])
|
@app.route("/api/ansible/recap/report", methods=["POST"])
|
||||||
def api_ansible_recap_report():
|
def api_ansible_recap_report():
|
||||||
"""Create or update an Ansible recap row for a running/finished playbook.
|
"""Create or update an Ansible recap row for a running/finished playbook.
|
||||||
|
|||||||
Reference in New Issue
Block a user