mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 07:25:48 -07:00
AGENT: Windows Server Compatibility Fixes
This commit is contained in:
@@ -598,6 +598,15 @@ class Role:
|
|||||||
pass
|
pass
|
||||||
return 'http://localhost:5000'
|
return 'http://localhost:5000'
|
||||||
|
|
||||||
|
def _http_client(self):
|
||||||
|
try:
|
||||||
|
fn = (self.ctx.hooks or {}).get('http_client')
|
||||||
|
if callable(fn):
|
||||||
|
return fn()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def _ansible_log(self, msg: str, error: bool = False, run_id: str = None):
|
def _ansible_log(self, msg: str, error: bool = False, run_id: str = None):
|
||||||
try:
|
try:
|
||||||
d = os.path.join(_project_root(), 'Agent', 'Logs')
|
d = os.path.join(_project_root(), 'Agent', 'Logs')
|
||||||
@@ -629,8 +638,6 @@ class Role:
|
|||||||
async def _fetch_service_creds(self) -> dict:
|
async def _fetch_service_creds(self) -> dict:
|
||||||
if self._svc_creds and isinstance(self._svc_creds, dict):
|
if self._svc_creds and isinstance(self._svc_creds, dict):
|
||||||
return self._svc_creds
|
return self._svc_creds
|
||||||
try:
|
|
||||||
import aiohttp
|
|
||||||
url = self._server_base().rstrip('/') + '/api/agent/checkin'
|
url = self._server_base().rstrip('/') + '/api/agent/checkin'
|
||||||
payload = {
|
payload = {
|
||||||
'agent_id': self.ctx.agent_id,
|
'agent_id': self.ctx.agent_id,
|
||||||
@@ -638,21 +645,24 @@ class Role:
|
|||||||
'username': DEFAULT_SERVICE_ACCOUNT,
|
'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)
|
client = self._http_client()
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
if client is None:
|
||||||
async with sess.post(url, json=payload) as resp:
|
self._ansible_log(f"[checkin] http_client unavailable agent_id={self.ctx.agent_id}", error=True)
|
||||||
js = await resp.json()
|
return {'username': DEFAULT_SERVICE_ACCOUNT, 'password': ''}
|
||||||
u = (js or {}).get('username') or DEFAULT_SERVICE_ACCOUNT
|
try:
|
||||||
p = (js or {}).get('password') or ''
|
js = await client.async_post_json('/api/agent/checkin', payload, require_auth=True)
|
||||||
|
except Exception as exc:
|
||||||
|
self._ansible_log(f"[checkin] failed agent_id={self.ctx.agent_id} err={exc}", error=True)
|
||||||
|
return {'username': DEFAULT_SERVICE_ACCOUNT, 'password': ''}
|
||||||
|
js = js if isinstance(js, dict) else {}
|
||||||
|
u = js.get('username') or DEFAULT_SERVICE_ACCOUNT
|
||||||
|
p = js.get('password') or ''
|
||||||
if u in LEGACY_SERVICE_ACCOUNTS:
|
if u in LEGACY_SERVICE_ACCOUNTS:
|
||||||
self._ansible_log(f"[checkin] legacy service username {u!r}; requesting rotate", error=True)
|
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)
|
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:
|
|
||||||
self._ansible_log(f"[checkin] failed agent_id={self.ctx.agent_id}", error=True)
|
|
||||||
return {'username': DEFAULT_SERVICE_ACCOUNT, 'password': ''}
|
|
||||||
|
|
||||||
def _normalize_playbook_content(self, content: str) -> str:
|
def _normalize_playbook_content(self, content: str) -> str:
|
||||||
try:
|
try:
|
||||||
@@ -675,8 +685,6 @@ class Role:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
async def _rotate_service_creds(self, reason: str = 'bad_credentials', force_username: Optional[str] = None) -> 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'
|
url = self._server_base().rstrip('/') + '/api/agent/service-account/rotate'
|
||||||
payload = {
|
payload = {
|
||||||
'agent_id': self.ctx.agent_id,
|
'agent_id': self.ctx.agent_id,
|
||||||
@@ -685,12 +693,18 @@ class Role:
|
|||||||
if force_username:
|
if force_username:
|
||||||
payload['username'] = 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)
|
client = self._http_client()
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
if client is None:
|
||||||
async with sess.post(url, json=payload) as resp:
|
self._ansible_log(f"[rotate] http_client unavailable agent_id={self.ctx.agent_id}", error=True)
|
||||||
js = await resp.json()
|
return await self._fetch_service_creds()
|
||||||
u = (js or {}).get('username') or force_username or DEFAULT_SERVICE_ACCOUNT
|
try:
|
||||||
p = (js or {}).get('password') or ''
|
js = await client.async_post_json('/api/agent/service-account/rotate', payload, require_auth=True)
|
||||||
|
except Exception as exc:
|
||||||
|
self._ansible_log(f"[rotate] failed agent_id={self.ctx.agent_id} err={exc}", error=True)
|
||||||
|
return await self._fetch_service_creds()
|
||||||
|
js = js if isinstance(js, dict) else {}
|
||||||
|
u = js.get('username') or force_username or DEFAULT_SERVICE_ACCOUNT
|
||||||
|
p = js.get('password') or ''
|
||||||
if u in LEGACY_SERVICE_ACCOUNTS and force_username != DEFAULT_SERVICE_ACCOUNT:
|
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)
|
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)
|
return await self._rotate_service_creds(reason='legacy_username', force_username=DEFAULT_SERVICE_ACCOUNT)
|
||||||
@@ -699,9 +713,6 @@ class Role:
|
|||||||
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
|
||||||
except Exception:
|
|
||||||
self._ansible_log(f"[rotate] failed agent_id={self.ctx.agent_id}", error=True)
|
|
||||||
return await self._fetch_service_creds()
|
|
||||||
|
|
||||||
def _ps_module_path(self) -> str:
|
def _ps_module_path(self) -> str:
|
||||||
# Place PS module under Roles so it's deployed with the agent
|
# Place PS module under Roles so it's deployed with the agent
|
||||||
@@ -827,17 +838,16 @@ try {{
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def _post_recap(self, payload: dict):
|
async def _post_recap(self, payload: dict):
|
||||||
try:
|
|
||||||
import aiohttp
|
|
||||||
url = self._server_base().rstrip('/') + '/api/ansible/recap/report'
|
url = self._server_base().rstrip('/') + '/api/ansible/recap/report'
|
||||||
timeout = aiohttp.ClientTimeout(total=30)
|
client = self._http_client()
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
if client is None:
|
||||||
async with sess.post(url, json=payload) as resp:
|
self._log_local("Failed to post recap: http_client unavailable", error=True)
|
||||||
# best-effort; ignore body
|
return
|
||||||
await resp.read()
|
try:
|
||||||
|
await client.async_post_json('/api/ansible/recap/report', payload, require_auth=True)
|
||||||
self._log_local(f"Posted recap: run_id={payload.get('run_id')} status={payload.get('status')} bytes={len((payload.get('recap_text') or '').encode('utf-8'))}")
|
self._log_local(f"Posted recap: run_id={payload.get('run_id')} status={payload.get('status')} bytes={len((payload.get('recap_text') or '').encode('utf-8'))}")
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
self._log_local(f"Failed to post recap for run_id={payload.get('run_id')}", error=True)
|
self._log_local(f"Failed to post recap for run_id={payload.get('run_id')}: {exc}", error=True)
|
||||||
|
|
||||||
async def _run_playbook_runner(self, run_id: str, playbook_content: str, playbook_name: str = '', activity_job_id=None, connection: str = 'local', exec_ctx: dict = None, files=None):
|
async def _run_playbook_runner(self, run_id: str, playbook_content: str, playbook_name: str = '', activity_job_id=None, connection: str = 'local', exec_ctx: dict = None, files=None):
|
||||||
exec_ctx = exec_ctx or {}
|
exec_ctx = exec_ctx or {}
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import threading
|
|||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
import re
|
import re
|
||||||
|
import ipaddress
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import Any, Dict, Optional, List, Callable, Tuple
|
from typing import Any, Dict, Optional, List, Callable, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
try:
|
try:
|
||||||
import psutil
|
import psutil
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -250,6 +252,19 @@ def _settings_dir():
|
|||||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings'))
|
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings'))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_literal_ip(value: Optional[str]) -> bool:
|
||||||
|
try:
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
candidate = value.strip().strip('[]')
|
||||||
|
if '%' in candidate:
|
||||||
|
candidate = candidate.split('%', 1)[0]
|
||||||
|
ipaddress.ip_address(candidate)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class _CrossProcessFileLock:
|
class _CrossProcessFileLock:
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -863,6 +878,26 @@ CONFIG = ConfigManager(CONFIG_PATH)
|
|||||||
CONFIG.load()
|
CONFIG.load()
|
||||||
|
|
||||||
|
|
||||||
|
class _HostnameFlexibleAdapter(HTTPAdapter):
|
||||||
|
"""HTTPAdapter that can disable urllib3 hostname verification."""
|
||||||
|
|
||||||
|
def __init__(self, *, disable_hostname_check: bool = False, **kwargs):
|
||||||
|
self._disable_hostname_check = disable_hostname_check
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
|
||||||
|
if self._disable_hostname_check:
|
||||||
|
pool_kwargs = dict(pool_kwargs or {})
|
||||||
|
pool_kwargs['assert_hostname'] = False
|
||||||
|
super().init_poolmanager(connections, maxsize, block, **pool_kwargs)
|
||||||
|
|
||||||
|
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
||||||
|
if self._disable_hostname_check:
|
||||||
|
proxy_kwargs = dict(proxy_kwargs or {})
|
||||||
|
proxy_kwargs['assert_hostname'] = False
|
||||||
|
return super().proxy_manager_for(proxy, **proxy_kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AgentHttpClient:
|
class AgentHttpClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.key_store = _key_store()
|
self.key_store = _key_store()
|
||||||
@@ -872,6 +907,12 @@ class AgentHttpClient:
|
|||||||
if context_label:
|
if context_label:
|
||||||
self.session.headers.setdefault(_AGENT_CONTEXT_HEADER, context_label)
|
self.session.headers.setdefault(_AGENT_CONTEXT_HEADER, context_label)
|
||||||
self.base_url: Optional[str] = None
|
self.base_url: Optional[str] = None
|
||||||
|
self._base_scheme: Optional[str] = None
|
||||||
|
self._base_netloc: Optional[str] = None
|
||||||
|
self._base_host: Optional[str] = None
|
||||||
|
self._base_host_is_ip: bool = False
|
||||||
|
self._hostname_adapter_prefix: Optional[str] = None
|
||||||
|
self._hostname_adapter_ip_active: bool = False
|
||||||
self.guid: Optional[str] = None
|
self.guid: Optional[str] = None
|
||||||
self.access_token: Optional[str] = None
|
self.access_token: Optional[str] = None
|
||||||
self.refresh_token: Optional[str] = None
|
self.refresh_token: Optional[str] = None
|
||||||
@@ -899,8 +940,12 @@ class AgentHttpClient:
|
|||||||
url = "https://localhost:5000"
|
url = "https://localhost:5000"
|
||||||
if url.endswith("/"):
|
if url.endswith("/"):
|
||||||
url = url[:-1]
|
url = url[:-1]
|
||||||
if url != self.base_url:
|
|
||||||
self.base_url = url
|
self.base_url = url
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
except Exception:
|
||||||
|
parsed = urlparse("https://localhost:5000")
|
||||||
|
self._update_base_url_state(parsed)
|
||||||
|
|
||||||
def _configure_verify(self) -> None:
|
def _configure_verify(self) -> None:
|
||||||
cert_path = self.key_store.server_certificate_path()
|
cert_path = self.key_store.server_certificate_path()
|
||||||
@@ -915,6 +960,57 @@ class AgentHttpClient:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _update_base_url_state(self, parsed) -> None:
|
||||||
|
try:
|
||||||
|
host = (parsed.hostname or '').strip()
|
||||||
|
netloc = parsed.netloc or host
|
||||||
|
scheme = (parsed.scheme or 'https').lower()
|
||||||
|
except Exception:
|
||||||
|
host = ''
|
||||||
|
netloc = ''
|
||||||
|
scheme = 'https'
|
||||||
|
host_is_ip = _is_literal_ip(host)
|
||||||
|
state_changed = (
|
||||||
|
scheme != self._base_scheme
|
||||||
|
or netloc != self._base_netloc
|
||||||
|
or host != (self._base_host or '')
|
||||||
|
or host_is_ip != self._base_host_is_ip
|
||||||
|
)
|
||||||
|
self._base_scheme = scheme
|
||||||
|
self._base_netloc = netloc
|
||||||
|
self._base_host = host
|
||||||
|
self._base_host_is_ip = host_is_ip
|
||||||
|
if state_changed:
|
||||||
|
self._configure_hostname_adapter()
|
||||||
|
|
||||||
|
def _configure_hostname_adapter(self) -> None:
|
||||||
|
scheme = self._base_scheme
|
||||||
|
netloc = self._base_netloc
|
||||||
|
if not scheme or not netloc:
|
||||||
|
return
|
||||||
|
prefix = f"{scheme}://{netloc}"
|
||||||
|
if self._base_host_is_ip:
|
||||||
|
if (not self._hostname_adapter_ip_active) or (self._hostname_adapter_prefix != prefix):
|
||||||
|
adapter = _HostnameFlexibleAdapter(disable_hostname_check=True)
|
||||||
|
self.session.mount(prefix, adapter)
|
||||||
|
self._hostname_adapter_prefix = prefix
|
||||||
|
self._hostname_adapter_ip_active = True
|
||||||
|
_log_agent(
|
||||||
|
f"Hostname verification disabled for literal IP target {prefix}",
|
||||||
|
fname="agent.log",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self._hostname_adapter_ip_active:
|
||||||
|
restore_prefix = self._hostname_adapter_prefix or prefix
|
||||||
|
if restore_prefix:
|
||||||
|
self.session.mount(restore_prefix, HTTPAdapter())
|
||||||
|
self._hostname_adapter_prefix = None
|
||||||
|
self._hostname_adapter_ip_active = False
|
||||||
|
_log_agent(
|
||||||
|
"Hostname verification restored for primary agent HTTP session",
|
||||||
|
fname="agent.log",
|
||||||
|
)
|
||||||
|
|
||||||
def _reload_tokens_from_disk(self) -> None:
|
def _reload_tokens_from_disk(self) -> None:
|
||||||
raw_guid = self.key_store.load_guid()
|
raw_guid = self.key_store.load_guid()
|
||||||
normalized_guid = _normalize_agent_guid(raw_guid) if raw_guid else ''
|
normalized_guid = _normalize_agent_guid(raw_guid) if raw_guid else ''
|
||||||
@@ -1010,8 +1106,8 @@ class AgentHttpClient:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
context = None
|
|
||||||
bundle_summary = {"count": None, "fingerprint": None, "layered_default": None}
|
bundle_summary = {"count": None, "fingerprint": None, "layered_default": None}
|
||||||
|
bundle_fp = None
|
||||||
context = None
|
context = None
|
||||||
if isinstance(verify, str) and os.path.isfile(verify):
|
if isinstance(verify, str) and os.path.isfile(verify):
|
||||||
bundle_count, bundle_fp, layered_default = self.key_store.summarize_server_certificate()
|
bundle_count, bundle_fp, layered_default = self.key_store.summarize_server_certificate()
|
||||||
@@ -1021,6 +1117,11 @@ class AgentHttpClient:
|
|||||||
"layered_default": layered_default,
|
"layered_default": layered_default,
|
||||||
}
|
}
|
||||||
context = self.key_store.build_ssl_context()
|
context = self.key_store.build_ssl_context()
|
||||||
|
if context is not None and self._base_host_is_ip:
|
||||||
|
try:
|
||||||
|
context.check_hostname = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if context is not None:
|
if context is not None:
|
||||||
self._cached_ssl_context = context
|
self._cached_ssl_context = context
|
||||||
if bundle_summary["layered_default"] is None:
|
if bundle_summary["layered_default"] is None:
|
||||||
@@ -1038,6 +1139,23 @@ class AgentHttpClient:
|
|||||||
"SocketIO TLS alignment failed to build context from pinned bundle", # noqa: E501
|
"SocketIO TLS alignment failed to build context from pinned bundle", # noqa: E501
|
||||||
fname="agent.error.log",
|
fname="agent.error.log",
|
||||||
)
|
)
|
||||||
|
if self._base_host_is_ip:
|
||||||
|
try:
|
||||||
|
fallback_context = ssl.create_default_context()
|
||||||
|
fallback_context.load_verify_locations(cafile=verify)
|
||||||
|
fallback_context.check_hostname = False
|
||||||
|
context = fallback_context
|
||||||
|
self._cached_ssl_context = context
|
||||||
|
_log_agent(
|
||||||
|
"SocketIO TLS alignment generated hostname-relaxed SSLContext for literal IP target",
|
||||||
|
fname="agent.log",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
context = None
|
||||||
|
_log_agent(
|
||||||
|
f"SocketIO TLS fallback context creation failed: {exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
|
||||||
if context is not None:
|
if context is not None:
|
||||||
_set_attr(engine, "ssl_context", context)
|
_set_attr(engine, "ssl_context", context)
|
||||||
|
|||||||
Reference in New Issue
Block a user