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
|
||||
}
|
||||
|
||||
$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
|
||||
function Remove-BorealisServicesAndTasks {
|
||||
param([string]$LogName)
|
||||
@@ -428,6 +462,7 @@ function InstallOrUpdate-BorealisAgent {
|
||||
$env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH
|
||||
Write-Host "Cleaning previous agent tasks/processes..." -ForegroundColor Yellow
|
||||
Remove-BorealisServicesAndTasks -LogName 'Install.log'
|
||||
Ensure-SystemUtf8CodePage -LogName 'Install.log'
|
||||
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
|
||||
|
||||
# Resolve all paths relative to the script directory to avoid CWD issues
|
||||
@@ -479,6 +514,27 @@ function InstallOrUpdate-BorealisAgent {
|
||||
if (Test-Path $agentRequirements) {
|
||||
& $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" {
|
||||
@@ -512,6 +568,11 @@ function InstallOrUpdate-BorealisAgent {
|
||||
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
|
||||
Write-Host "===================================================================================="
|
||||
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 ----------------------
|
||||
|
||||
@@ -35,7 +35,10 @@ function Ensure-LocalhostWinRMHttps {
|
||||
# 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 @{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 { New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'LocalAccountTokenFilterPolicy' -PropertyType DWord -Value 1 -Force | Out-Null } catch {}
|
||||
}
|
||||
|
||||
function Ensure-BorealisServiceUser {
|
||||
@@ -62,6 +65,18 @@ function Ensure-BorealisServiceUser {
|
||||
Add-LocalGroupMember -Group "Administrators" -Member $localName -ErrorAction SilentlyContinue
|
||||
}
|
||||
} 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 {
|
||||
|
||||
@@ -7,12 +7,15 @@ import time
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import winrm # type: ignore
|
||||
except Exception:
|
||||
winrm = None
|
||||
|
||||
DEFAULT_SERVICE_ACCOUNT = '.\\svcBorealis'
|
||||
LEGACY_SERVICE_ACCOUNTS = {'.\\svcBorealisAnsibleRunner', 'svcBorealisAnsibleRunner'}
|
||||
|
||||
ROLE_NAME = 'playbook_exec_system'
|
||||
ROLE_CONTEXTS = ['system']
|
||||
@@ -166,21 +169,24 @@ class Role:
|
||||
payload = {
|
||||
'agent_id': self.ctx.agent_id,
|
||||
'hostname': socket.gethostname(),
|
||||
'username': '.\\svcBorealisAnsibleRunner',
|
||||
'username': DEFAULT_SERVICE_ACCOUNT,
|
||||
}
|
||||
self._ansible_log(f"[checkin] POST {url} agent_id={self.ctx.agent_id}")
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
||||
async with sess.post(url, json=payload) as resp:
|
||||
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 ''
|
||||
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._ansible_log(f"[checkin] received user={u} pw_len={len(p)}")
|
||||
return self._svc_creds
|
||||
except Exception:
|
||||
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:
|
||||
try:
|
||||
@@ -202,21 +208,28 @@ class Role:
|
||||
except Exception:
|
||||
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:
|
||||
import aiohttp
|
||||
url = self._server_base().rstrip('/') + '/api/agent/service-account/rotate'
|
||||
payload = {
|
||||
'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}")
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
||||
async with sess.post(url, json=payload) as resp:
|
||||
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 ''
|
||||
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._ansible_log(f"[rotate] received user={u} pw_len={len(p)}")
|
||||
return self._svc_creds
|
||||
@@ -384,7 +397,7 @@ try {{
|
||||
self._ansible_log(f"[runner] prepared playbook={play_abs} bytes={len(_norm.encode('utf-8'))}")
|
||||
# WinRM service account credentials
|
||||
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 ''
|
||||
# Converge endpoint state (listener + user)
|
||||
self._ensure_winrm_and_user(user, pwd)
|
||||
@@ -392,7 +405,7 @@ try {{
|
||||
pre_ok = self._winrm_preflight(user, pwd)
|
||||
if not pre_ok:
|
||||
# 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
|
||||
pwd = creds.get('password') or ''
|
||||
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 auth_failed:
|
||||
try:
|
||||
newc = await self._rotate_service_creds()
|
||||
newc = await self._rotate_service_creds(reason='auth_failed_retry')
|
||||
user2 = newc.get('username') or user
|
||||
pwd2 = newc.get('password') or ''
|
||||
self._ensure_winrm_and_user(user2, pwd2)
|
||||
@@ -551,11 +564,11 @@ try {{
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
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 ''
|
||||
self._ensure_winrm_and_user(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
|
||||
pwd = creds.get('password') or ''
|
||||
self._ensure_winrm_and_user(user, pwd)
|
||||
@@ -576,7 +589,7 @@ try {{
|
||||
use_module = True
|
||||
if use_module:
|
||||
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}")
|
||||
else:
|
||||
base_cmd = [ap]
|
||||
@@ -593,21 +606,23 @@ try {{
|
||||
self._ansible_log(f"[cli] cmd={' '.join(cmd)}")
|
||||
# Ensure clean, plain output and correct interpreter for localhost
|
||||
env = os.environ.copy()
|
||||
env.setdefault('ANSIBLE_FORCE_COLOR', '0')
|
||||
env.setdefault('ANSIBLE_NOCOLOR', '1')
|
||||
env.setdefault('PYTHONIOENCODING', 'utf-8')
|
||||
env.setdefault('PYTHONUTF8', '1')
|
||||
env['ANSIBLE_FORCE_COLOR'] = '0'
|
||||
env['ANSIBLE_NOCOLOR'] = '1'
|
||||
env['PYTHONIOENCODING'] = 'utf-8'
|
||||
env['PYTHONUTF8'] = '1'
|
||||
env['ANSIBLE_STDOUT_CALLBACK'] = 'default'
|
||||
env['ANSIBLE_LOCALHOST_WARNING'] = '0'
|
||||
env['ANSIBLE_COLLECTIONS_PATHS'] = _collections_dir()
|
||||
if os.name == 'nt':
|
||||
env.setdefault('LANG', 'en_US.UTF-8')
|
||||
env.setdefault('ANSIBLE_STDOUT_CALLBACK', 'default')
|
||||
# Help Ansible pick the correct python for localhost
|
||||
env.setdefault('ANSIBLE_LOCALHOST_WARNING', '0')
|
||||
# Ensure collections path is discoverable
|
||||
env.setdefault('ANSIBLE_COLLECTIONS_PATHS', _collections_dir())
|
||||
env['LANG'] = 'en_US.UTF-8'
|
||||
env['LC_ALL'] = 'en_US.UTF-8'
|
||||
env['LC_CTYPE'] = 'en_US.UTF-8'
|
||||
vp = _venv_python()
|
||||
if 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
|
||||
if os.name == 'nt':
|
||||
# CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
||||
@@ -751,7 +766,7 @@ try {{
|
||||
if os.name != 'nt':
|
||||
return
|
||||
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 ''
|
||||
self._ansible_log(f"[bootstrap] ensure winrm+user user={user} pw_len={len(pwd)}")
|
||||
self._ensure_winrm_and_user(user, pwd)
|
||||
@@ -759,7 +774,7 @@ try {{
|
||||
self._ansible_log(f"[bootstrap] preflight_ok={ok}")
|
||||
if not ok:
|
||||
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
|
||||
pwd = creds.get('password') or ''
|
||||
self._ensure_winrm_and_user(user, pwd)
|
||||
|
||||
@@ -1178,7 +1178,7 @@ async def connect():
|
||||
async def _svc_checkin_once():
|
||||
try:
|
||||
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)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
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):
|
||||
_write_service_log('ansible', msg)
|
||||
|
||||
DEFAULT_SERVICE_ACCOUNT = '.\\svcBorealis'
|
||||
LEGACY_SERVICE_ACCOUNTS = {'.\\svcBorealisAnsibleRunner', 'svcBorealisAnsibleRunner'}
|
||||
|
||||
# Borealis Python API Endpoints
|
||||
from Python_API_Endpoints.ocr_engines import run_ocr_on_base64
|
||||
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):
|
||||
username = (username or '').strip()
|
||||
if not username or username in LEGACY_SERVICE_ACCOUNTS:
|
||||
username = DEFAULT_SERVICE_ACCOUNT
|
||||
enc = _encrypt_secret(plaintext_password)
|
||||
now_utc = _now_iso_utc()
|
||||
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'])
|
||||
def api_agent_checkin():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
agent_id = (payload.get('agent_id') or '').strip()
|
||||
if not agent_id:
|
||||
return jsonify({'error': 'agent_id required'}), 400
|
||||
username = (payload.get('username') or '.\\svcBorealisAnsibleRunner').strip()
|
||||
# Optional hostname here for future auditing/joins
|
||||
# Upsert service account, creating new creds if missing
|
||||
raw_username = (payload.get('username') or '').strip()
|
||||
username = raw_username or DEFAULT_SERVICE_ACCOUNT
|
||||
if username in LEGACY_SERVICE_ACCOUNTS:
|
||||
username = DEFAULT_SERVICE_ACCOUNT
|
||||
try:
|
||||
conn = _db_conn()
|
||||
row = _service_acct_get(conn, agent_id)
|
||||
@@ -2946,17 +2954,25 @@ def api_agent_checkin():
|
||||
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']}")
|
||||
else:
|
||||
# row: agent_id, username, password_encrypted, last_rotated_utc, version
|
||||
stored_username = (row[1] or '').strip()
|
||||
try:
|
||||
plain = _decrypt_secret(row[2])
|
||||
except Exception:
|
||||
plain = ''
|
||||
if stored_username in LEGACY_SERVICE_ACCOUNTS:
|
||||
if not plain:
|
||||
plain = _gen_strong_password()
|
||||
out = _service_acct_set(conn, agent_id, row[1] or username, plain)
|
||||
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()
|
||||
out = _service_acct_set(conn, agent_id, stored_username or username, plain)
|
||||
else:
|
||||
eff_user = stored_username or username
|
||||
if eff_user in LEGACY_SERVICE_ACCOUNTS:
|
||||
eff_user = DEFAULT_SERVICE_ACCOUNT
|
||||
out = {
|
||||
'username': row[1] or username,
|
||||
'username': eff_user,
|
||||
'password': plain,
|
||||
'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()
|
||||
if not agent_id:
|
||||
return jsonify({'error': 'agent_id required'}), 400
|
||||
username = (payload.get('username') or '.\\svcBorealisAnsibleRunner').strip()
|
||||
requested_username = (payload.get('username') or '').strip()
|
||||
try:
|
||||
conn = _db_conn()
|
||||
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()
|
||||
out = _service_acct_set(conn, agent_id, user_eff, pw_new)
|
||||
conn.close()
|
||||
@@ -2996,7 +3018,6 @@ def api_agent_service_account_rotate():
|
||||
_ansible_log_server(f"[rotate] error agent_id={agent_id} err={e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/ansible/recap/report", methods=["POST"])
|
||||
def api_ansible_recap_report():
|
||||
"""Create or update an Ansible recap row for a running/finished playbook.
|
||||
|
||||
Reference in New Issue
Block a user