Additional Ansible Changes

This commit is contained in:
2025-10-02 20:46:11 -06:00
parent 1ade450d27
commit 211b4262aa
8 changed files with 383 additions and 35 deletions

View File

@@ -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 ----------------------

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
View 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'}]

View 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
View 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'
}]

View File

@@ -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.