mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-23 11:03:24 +00:00
feat: agent IP/VIP live update + script update detection banner
- Agent scripts now detect and send ip_address in DAEMON heartbeat (Linux: ip route, macOS: ifconfig) - Backend validates agent-reported IPs via ipaddress stdlib, COALESCE preserves existing on NULL - IP/VIP change logging (non-critical, try/except wrapped) for operational visibility - New source_file_hash column on agent_script_templates for reliable update detection - Migration changed to ON CONFLICT DO NOTHING to prevent overwriting UI-customized scripts on restart - GET /versions returns script_update_available flag (disk hash vs DB hash comparison with fallback) - Frontend Alert banner warns users of new agent script versions and directs to Reset to Defaults - Reset to Defaults and Popconfirm modals explicitly warn about custom script edit loss - Full backward compatibility: old agents without ip_address field continue working unchanged Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from database.connection import get_database_connection, close_database_connection
|
||||
@@ -1585,6 +1586,7 @@ async def run_all_migrations():
|
||||
await ensure_validation_error_columns()
|
||||
await remove_haproxy_user_group_columns()
|
||||
await ensure_agent_versions_table()
|
||||
await ensure_agent_script_templates_source_hash()
|
||||
await ensure_agent_script_templates_table()
|
||||
await ensure_agent_activity_logs_table()
|
||||
await ensure_agent_config_management_tables()
|
||||
@@ -2544,6 +2546,7 @@ async def ensure_agent_script_templates_table():
|
||||
platform VARCHAR(50) NOT NULL,
|
||||
version VARCHAR(20) NOT NULL,
|
||||
script_content TEXT NOT NULL,
|
||||
source_file_hash VARCHAR(64),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -2568,13 +2571,12 @@ async def ensure_agent_script_templates_table():
|
||||
with open(macos_script_path, 'r') as f:
|
||||
macos_script = f.read()
|
||||
|
||||
macos_hash = hashlib.sha256(macos_script.encode()).hexdigest()
|
||||
await conn.execute("""
|
||||
INSERT INTO agent_script_templates (platform, version, script_content)
|
||||
VALUES ('macos', $1, $2)
|
||||
ON CONFLICT (platform, version) DO UPDATE SET
|
||||
script_content = EXCLUDED.script_content,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", macos_current_version, macos_script)
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, source_file_hash)
|
||||
VALUES ('macos', $1, $2, $3)
|
||||
ON CONFLICT (platform, version) DO NOTHING
|
||||
""", macos_current_version, macos_script, macos_hash)
|
||||
|
||||
logger.info(f"✅ Loaded macOS script template version {macos_current_version}")
|
||||
|
||||
@@ -2584,13 +2586,12 @@ async def ensure_agent_script_templates_table():
|
||||
with open(linux_script_path, 'r') as f:
|
||||
linux_script = f.read()
|
||||
|
||||
linux_hash = hashlib.sha256(linux_script.encode()).hexdigest()
|
||||
await conn.execute("""
|
||||
INSERT INTO agent_script_templates (platform, version, script_content)
|
||||
VALUES ('linux', $1, $2)
|
||||
ON CONFLICT (platform, version) DO UPDATE SET
|
||||
script_content = EXCLUDED.script_content,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", linux_current_version, linux_script)
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, source_file_hash)
|
||||
VALUES ('linux', $1, $2, $3)
|
||||
ON CONFLICT (platform, version) DO NOTHING
|
||||
""", linux_current_version, linux_script, linux_hash)
|
||||
|
||||
logger.info(f"✅ Loaded Linux script template version {linux_current_version}")
|
||||
|
||||
@@ -2603,6 +2604,22 @@ async def ensure_agent_script_templates_table():
|
||||
logger.error(f"Error creating agent_script_templates table: {e}")
|
||||
# Don't raise - this is not critical for system operation
|
||||
|
||||
async def ensure_agent_script_templates_source_hash():
|
||||
"""Add source_file_hash column to agent_script_templates for update detection"""
|
||||
conn = None
|
||||
try:
|
||||
conn = await get_database_connection()
|
||||
await conn.execute("""
|
||||
ALTER TABLE agent_script_templates
|
||||
ADD COLUMN IF NOT EXISTS source_file_hash VARCHAR(64)
|
||||
""")
|
||||
await close_database_connection(conn)
|
||||
logger.info("Agent script templates source_file_hash column ensured")
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await close_database_connection(conn)
|
||||
logger.error(f"Error adding source_file_hash column: {e}")
|
||||
|
||||
async def ensure_agent_config_management_tables():
|
||||
"""Create tables for Configuration Management feature"""
|
||||
conn = None
|
||||
|
||||
+93
-15
@@ -7,6 +7,8 @@ import base64
|
||||
import string
|
||||
import os
|
||||
import json
|
||||
import ipaddress
|
||||
import hashlib
|
||||
# Pipeline trigger - force backend redeploy v2
|
||||
|
||||
from models import AgentCreate
|
||||
@@ -597,13 +599,12 @@ async def generate_install_script(req_data: AgentScriptRequest, request: Request
|
||||
|
||||
# Sync file template to database for future use
|
||||
try:
|
||||
file_hash = hashlib.sha256(script_template.encode()).hexdigest()
|
||||
await conn.execute("""
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, is_active)
|
||||
VALUES ($1, $2, $3, true)
|
||||
ON CONFLICT (platform, version) DO UPDATE SET
|
||||
script_content = EXCLUDED.script_content,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", platform_key, latest_version, script_template)
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, source_file_hash, is_active)
|
||||
VALUES ($1, $2, $3, $4, true)
|
||||
ON CONFLICT (platform, version) DO NOTHING
|
||||
""", platform_key, latest_version, script_template, file_hash)
|
||||
logger.info(f"SCRIPT SYNC: Synced file template to database for {platform_key} version {latest_version}")
|
||||
except Exception as sync_error:
|
||||
logger.warning(f"Could not sync template to database: {sync_error}")
|
||||
@@ -847,6 +848,27 @@ async def delete_agent(agent_id: int, authorization: str = Header(None)):
|
||||
logger.error(f"Failed to delete agent: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete agent: {str(e)}")
|
||||
|
||||
def _safe_ip_for_inet(ip_str: Optional[str]) -> Optional[str]:
|
||||
"""Validate IP string for PostgreSQL INET column. Returns None for invalid/empty."""
|
||||
if not ip_str or not ip_str.strip():
|
||||
return None
|
||||
try:
|
||||
ipaddress.ip_address(ip_str.strip())
|
||||
return ip_str.strip()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _extract_agent_ip(heartbeat_data: AgentHeartbeat) -> Optional[str]:
|
||||
"""Extract validated IP from heartbeat - top-level first, then system_info fallback."""
|
||||
ip = _safe_ip_for_inet(heartbeat_data.ip_address)
|
||||
if ip:
|
||||
return ip
|
||||
if heartbeat_data.system_info and isinstance(heartbeat_data.system_info, dict):
|
||||
ip = _safe_ip_for_inet(heartbeat_data.system_info.get("ip_address"))
|
||||
if ip:
|
||||
return ip
|
||||
return None
|
||||
|
||||
@router.post("/{agent_id}/heartbeat")
|
||||
async def agent_heartbeat(agent_id: int, heartbeat_data: AgentHeartbeat):
|
||||
"""Receive agent heartbeat and update status."""
|
||||
@@ -862,10 +884,11 @@ async def agent_heartbeat(agent_id: int, heartbeat_data: AgentHeartbeat):
|
||||
haproxy_version = COALESCE($4, haproxy_version),
|
||||
keepalive_state = CASE WHEN $5::text IS NOT NULL THEN NULLIF($5::text, '') ELSE keepalive_state END,
|
||||
keepalive_ip = CASE WHEN $6::text IS NOT NULL THEN NULLIF($6::text, '') ELSE keepalive_ip END,
|
||||
ip_address = COALESCE($7::inet, ip_address),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
""", agent_id, heartbeat_data.hostname, heartbeat_data.haproxy_status, heartbeat_data.haproxy_version,
|
||||
heartbeat_data.keepalive_state, heartbeat_data.keepalive_ip)
|
||||
heartbeat_data.keepalive_state, heartbeat_data.keepalive_ip, _extract_agent_ip(heartbeat_data))
|
||||
|
||||
await close_database_connection(conn)
|
||||
|
||||
@@ -1603,6 +1626,24 @@ async def agent_heartbeat_by_name(
|
||||
detected_platform = 'linux'
|
||||
logger.info(f"PLATFORM AUTO-DETECT: Agent '{agent_name}' platform defaulted to 'linux' (unknown)")
|
||||
|
||||
# Extract validated agent IP (returns None for invalid/empty)
|
||||
agent_ip = _extract_agent_ip(heartbeat_data)
|
||||
|
||||
# IP/VIP change detection (non-critical logging)
|
||||
try:
|
||||
if agent_ip or heartbeat_data.keepalive_ip:
|
||||
current_agent = await conn.fetchrow(
|
||||
"SELECT ip_address, keepalive_ip FROM agents WHERE id = $1", agent_id)
|
||||
if current_agent:
|
||||
old_ip = str(current_agent['ip_address']) if current_agent['ip_address'] else None
|
||||
old_vip = current_agent['keepalive_ip']
|
||||
if agent_ip and old_ip and agent_ip != old_ip:
|
||||
logger.info(f"IP CHANGE: Agent '{agent_name}' (id={agent_id}) IP changed: {old_ip} -> {agent_ip}")
|
||||
if heartbeat_data.keepalive_ip and old_vip and heartbeat_data.keepalive_ip != old_vip:
|
||||
logger.info(f"VIP CHANGE: Agent '{agent_name}' (id={agent_id}) VIP changed: {old_vip} -> {heartbeat_data.keepalive_ip}")
|
||||
except Exception:
|
||||
pass # Non-critical, never break heartbeat processing
|
||||
|
||||
# CRITICAL FIX: Only update applied_config_version if agent sends a VALID value
|
||||
# Agent sends "none" on startup before any config is applied - don't override DB value with this
|
||||
update_applied_version = heartbeat_data.applied_config_version and heartbeat_data.applied_config_version not in ["none", ""]
|
||||
@@ -1639,7 +1680,7 @@ async def agent_heartbeat_by_name(
|
||||
# Convert lists to JSON for JSONB columns
|
||||
heartbeat_data.network_interfaces if isinstance(heartbeat_data.network_interfaces, str) else json.dumps(heartbeat_data.network_interfaces or []),
|
||||
heartbeat_data.capabilities if isinstance(heartbeat_data.capabilities, str) else json.dumps(heartbeat_data.capabilities or []),
|
||||
heartbeat_data.ip_address, heartbeat_data.haproxy_status, heartbeat_data.haproxy_version,
|
||||
agent_ip, heartbeat_data.haproxy_status, heartbeat_data.haproxy_version,
|
||||
heartbeat_data.applied_config_version, new_status, update_applied_version,
|
||||
heartbeat_data.keepalive_state, heartbeat_data.keepalive_ip)
|
||||
|
||||
@@ -2432,8 +2473,6 @@ async def get_agent_versions(authorization: str = Header(None)):
|
||||
ORDER BY platform, created_at DESC
|
||||
""")
|
||||
|
||||
await close_database_connection(conn)
|
||||
|
||||
# Group by platform
|
||||
result = {}
|
||||
for version_row in versions:
|
||||
@@ -2464,7 +2503,43 @@ async def get_agent_versions(authorization: str = Header(None)):
|
||||
"created_at": "2024-01-23T00:00:00Z"
|
||||
}]
|
||||
|
||||
return {"platforms": result}
|
||||
# Detect if on-disk agent scripts differ from database (new version shipped)
|
||||
script_update_available = False
|
||||
try:
|
||||
script_files = {'linux': 'linux_install.sh', 'macos': 'macos_install.sh'}
|
||||
for platform_key, filename in script_files.items():
|
||||
file_path = os.path.join(os.path.dirname(__file__), '..', 'utils', 'agent_scripts', filename)
|
||||
if not os.path.exists(file_path):
|
||||
continue
|
||||
with open(file_path, 'r') as f:
|
||||
current_file_hash = hashlib.sha256(f.read().encode()).hexdigest()
|
||||
|
||||
db_row = await conn.fetchrow("""
|
||||
SELECT source_file_hash FROM agent_script_templates
|
||||
WHERE platform = $1 AND is_active = true
|
||||
ORDER BY updated_at DESC LIMIT 1
|
||||
""", platform_key)
|
||||
|
||||
if db_row and db_row['source_file_hash']:
|
||||
if current_file_hash != db_row['source_file_hash']:
|
||||
script_update_available = True
|
||||
break
|
||||
else:
|
||||
db_content_row = await conn.fetchrow("""
|
||||
SELECT script_content FROM agent_script_templates
|
||||
WHERE platform = $1 AND is_active = true
|
||||
ORDER BY updated_at DESC LIMIT 1
|
||||
""", platform_key)
|
||||
if db_content_row:
|
||||
db_hash = hashlib.sha256(db_content_row['script_content'].encode()).hexdigest()
|
||||
if current_file_hash != db_hash:
|
||||
script_update_available = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Script update detection failed: {e}")
|
||||
|
||||
await close_database_connection(conn)
|
||||
return {"platforms": result, "script_update_available": script_update_available}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -2764,7 +2839,7 @@ async def get_agent_activity_logs(agent_name: str, limit: int = 50, authorizatio
|
||||
|
||||
# ==== HELPER FUNCTION FOR SCRIPT UPDATES ====
|
||||
|
||||
async def update_agent_script_in_database(conn, platform: str, version: str, script_content: str, changelog: list = None):
|
||||
async def update_agent_script_in_database(conn, platform: str, version: str, script_content: str, changelog: list = None, source_file_hash: str = None):
|
||||
"""
|
||||
Helper function to update agent script in database
|
||||
Used by both Edit and Reset to Default operations
|
||||
@@ -2778,13 +2853,14 @@ async def update_agent_script_in_database(conn, platform: str, version: str, scr
|
||||
|
||||
# Insert/update version with script content in agent_script_templates
|
||||
await conn.execute("""
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, is_active)
|
||||
VALUES ($1, $2, $3, true)
|
||||
INSERT INTO agent_script_templates (platform, version, script_content, source_file_hash, is_active)
|
||||
VALUES ($1, $2, $3, $4, true)
|
||||
ON CONFLICT (platform, version) DO UPDATE SET
|
||||
script_content = EXCLUDED.script_content,
|
||||
source_file_hash = COALESCE(EXCLUDED.source_file_hash, agent_script_templates.source_file_hash),
|
||||
is_active = true,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", platform, version, script_content)
|
||||
""", platform, version, script_content, source_file_hash)
|
||||
|
||||
# CRITICAL: Also update agent_versions table for UI to display correct Available version
|
||||
# Deactivate all existing versions in agent_versions
|
||||
@@ -2910,11 +2986,13 @@ async def sync_scripts_from_files(
|
||||
new_version = "1.0.0"
|
||||
|
||||
# Use the same update logic as Edit operation
|
||||
file_hash = hashlib.sha256(file_content.encode()).hexdigest()
|
||||
await update_agent_script_in_database(
|
||||
conn=conn,
|
||||
platform=platform_key,
|
||||
version=new_version,
|
||||
script_content=file_content,
|
||||
source_file_hash=file_hash,
|
||||
changelog=["Reset to file-based defaults", "Original stable version"]
|
||||
)
|
||||
|
||||
|
||||
@@ -934,59 +934,6 @@ async def parse_bulk_config(
|
||||
"request_headers": backend.request_headers,
|
||||
"response_headers": backend.response_headers,
|
||||
"options": backend.options,
|
||||
# Strip auto-generated content from parsed data.
|
||||
# The config generator injects content from multiple sources (ACME, rate_limit
|
||||
# field, WAF rules table) that the parser cannot distinguish from user-defined
|
||||
# configuration. Strip all auto-managed patterns so the preview and comparison
|
||||
# only reflect user-defined configuration.
|
||||
def _is_auto_header(line):
|
||||
s = line.strip()
|
||||
if "is_acme_challenge" in s:
|
||||
return True
|
||||
if "track-sc0 src" in s:
|
||||
return True
|
||||
if "sc_http_req_rate(0)" in s:
|
||||
return True
|
||||
if s.startswith("http-request") and " waf_" in s:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _strip_auto_headers(headers_str):
|
||||
if not headers_str:
|
||||
return None
|
||||
lines = [l for l in headers_str.split("\n") if not _is_auto_header(l)]
|
||||
return "\n".join(lines) if lines else None
|
||||
|
||||
backends_data = [b for b in backends_data if b["name"] != "_acme_challenge_backend"]
|
||||
|
||||
for frontend in frontends_data:
|
||||
frontend["acl_rules"] = [
|
||||
r for r in (frontend.get("acl_rules") or [])
|
||||
if "is_acme_challenge" not in r and not r.strip().startswith("acl waf_")
|
||||
]
|
||||
frontend["use_backend_rules"] = [
|
||||
r for r in (frontend.get("use_backend_rules") or [])
|
||||
if "_acme_challenge_backend" not in r
|
||||
]
|
||||
frontend["request_headers"] = _strip_auto_headers(frontend.get("request_headers"))
|
||||
if frontend.get("tcp_request_rules"):
|
||||
lines = [
|
||||
l for l in frontend["tcp_request_rules"].split("\n")
|
||||
if "is_acme_challenge" not in l
|
||||
]
|
||||
frontend["tcp_request_rules"] = "\n".join(lines) if lines else None
|
||||
if frontend.get("default_backend") == "_acme_challenge_backend":
|
||||
remaining = frontend.get("use_backend_rules") or []
|
||||
if remaining:
|
||||
m = re.match(r'^use_backend\s+(\S+)', remaining[0])
|
||||
frontend["default_backend"] = m.group(1) if m else None
|
||||
else:
|
||||
frontend["default_backend"] = None
|
||||
|
||||
parse_result.warnings = [
|
||||
w for w in parse_result.warnings if "_acme_challenge_backend" not in w
|
||||
]
|
||||
|
||||
"timeout_connect": backend.timeout_connect,
|
||||
"timeout_server": backend.timeout_server,
|
||||
"timeout_queue": backend.timeout_queue,
|
||||
|
||||
@@ -2830,6 +2830,10 @@ SYSTEM_INFO_EOF
|
||||
local haproxy_stats_csv=$(get_haproxy_stats_csv)
|
||||
local system_info=$(collect_system_info)
|
||||
|
||||
# Detect primary IP for backend update
|
||||
local agent_ip=""
|
||||
agent_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||||
|
||||
# Get HAProxy version for heartbeat (safe extraction, fallback to "unknown")
|
||||
local haproxy_version="unknown"
|
||||
if command -v haproxy &> /dev/null; then
|
||||
@@ -2850,6 +2854,7 @@ SYSTEM_INFO_EOF
|
||||
local heartbeat_payload="{
|
||||
\"name\": \"$AGENT_NAME\",
|
||||
\"hostname\": \"$(hostname)\",
|
||||
\"ip_address\": \"$agent_ip\",
|
||||
\"status\": \"online\",
|
||||
\"platform\": \"$platform\",
|
||||
\"architecture\": \"$(uname -m)\",
|
||||
@@ -2863,6 +2868,9 @@ SYSTEM_INFO_EOF
|
||||
# Always send keepalive state (even empty) so backend can clear stale data
|
||||
heartbeat_payload+=",\"keepalive_state\": \"$keepalive_state\",\"keepalive_ip\": \"$keepalive_ip\""
|
||||
|
||||
# Include applied config version for backend tracking
|
||||
heartbeat_payload+=",\"applied_config_version\": \"${last_applied_version:-none}\""
|
||||
|
||||
# CRITICAL: Add haproxy_stats_csv only if available (for dashboard metrics)
|
||||
if [[ -n "$haproxy_stats_csv" && "$haproxy_stats_csv" != "" ]]; then
|
||||
heartbeat_payload+=",\"haproxy_stats_csv\": \"$haproxy_stats_csv\""
|
||||
|
||||
@@ -2637,6 +2637,10 @@ SYSTEM_INFO_EOF
|
||||
local haproxy_stats_csv=$(get_haproxy_stats_csv)
|
||||
local system_info=$(collect_system_info)
|
||||
|
||||
# Detect primary IP for backend update
|
||||
local agent_ip=""
|
||||
agent_ip=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | head -1 | awk '{print $2}' 2>/dev/null || echo "")
|
||||
|
||||
# Get HAProxy version for heartbeat (safe extraction, fallback to "unknown")
|
||||
local haproxy_version="unknown"
|
||||
if command -v haproxy &> /dev/null; then
|
||||
@@ -2657,6 +2661,7 @@ SYSTEM_INFO_EOF
|
||||
local heartbeat_payload="{
|
||||
\"name\": \"$AGENT_NAME\",
|
||||
\"hostname\": \"$(hostname)\",
|
||||
\"ip_address\": \"$agent_ip\",
|
||||
\"status\": \"online\",
|
||||
\"platform\": \"$platform\",
|
||||
\"architecture\": \"$(uname -m)\",
|
||||
@@ -2670,6 +2675,9 @@ SYSTEM_INFO_EOF
|
||||
# Always send keepalive state (even empty) so backend can clear stale data
|
||||
heartbeat_payload+=",\"keepalive_state\": \"$keepalive_state\",\"keepalive_ip\": \"$keepalive_ip\""
|
||||
|
||||
# Include applied config version for backend tracking
|
||||
heartbeat_payload+=",\"applied_config_version\": \"${last_applied_version:-none}\""
|
||||
|
||||
# CRITICAL: Add haproxy_stats_csv only if available (for dashboard metrics)
|
||||
if [[ -n "$haproxy_stats_csv" && "$haproxy_stats_csv" != "" ]]; then
|
||||
heartbeat_payload+=",\"haproxy_stats_csv\": \"$haproxy_stats_csv\""
|
||||
|
||||
@@ -185,6 +185,7 @@ const AgentManagement = () => {
|
||||
const [scriptHasChanges, setScriptHasChanges] = useState(false); // Track if script has changes
|
||||
const [scriptSaving, setScriptSaving] = useState(false); // Track script saving state
|
||||
const [currentEditingPlatform, setCurrentEditingPlatform] = useState(null); // Track which platform is being edited
|
||||
const [scriptUpdateAvailable, setScriptUpdateAvailable] = useState(false);
|
||||
|
||||
const { clusters, selectedCluster } = useCluster();
|
||||
const { hasPermission, isAdmin } = useAuth();
|
||||
@@ -702,6 +703,7 @@ const AgentManagement = () => {
|
||||
});
|
||||
|
||||
setAgentVersions(platforms);
|
||||
setScriptUpdateAvailable(response.data.script_update_available || false);
|
||||
} catch (error) {
|
||||
message.error('Failed to fetch agent versions: ' + (error.response?.data?.detail || error.message));
|
||||
} finally {
|
||||
@@ -764,6 +766,8 @@ const AgentManagement = () => {
|
||||
duration: 10,
|
||||
});
|
||||
|
||||
setScriptUpdateAvailable(false);
|
||||
|
||||
// Refresh agent versions to show new versions
|
||||
await fetchAgentVersions();
|
||||
|
||||
@@ -1569,11 +1573,14 @@ const AgentManagement = () => {
|
||||
<Alert
|
||||
message="This will sync agent scripts from files to database"
|
||||
description={
|
||||
<ul style={{ marginTop: '8px', paddingLeft: '20px', marginBottom: 0 }}>
|
||||
<li>Override any manual script edits in database</li>
|
||||
<li>All existing agents will detect new version</li>
|
||||
<li>Agents can upgrade to latest script</li>
|
||||
</ul>
|
||||
<div>
|
||||
<ul style={{ marginTop: '8px', paddingLeft: '20px', marginBottom: 8 }}>
|
||||
<li>File-based default scripts will be written to the database</li>
|
||||
<li><strong>All custom script edits made via the UI editor will be lost</strong></li>
|
||||
<li>All existing agents will detect the new version</li>
|
||||
<li>Agents with customized scripts will revert to the default version after upgrade</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
@@ -1749,6 +1756,28 @@ const AgentManagement = () => {
|
||||
|
||||
<Divider />
|
||||
|
||||
{scriptUpdateAvailable && (
|
||||
<Alert
|
||||
message="New Agent Script Version Available"
|
||||
description={
|
||||
<div>
|
||||
<p style={{ marginBottom: 8 }}>
|
||||
A newer version of agent scripts has been shipped with this OpenManager update.
|
||||
To apply the update, go to the <strong>Agent Script Management</strong> tab and click <strong>Reset to Defaults</strong>.
|
||||
</p>
|
||||
<p style={{ marginBottom: 0, color: '#faad14' }}>
|
||||
<strong>Warning:</strong> Reset to Defaults will overwrite any custom script edits made via the UI editor.
|
||||
Agents with customized scripts will revert to the default version after upgrade.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
closable
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Registered Agents and Script Management */}
|
||||
<Tabs defaultActiveKey="agents">
|
||||
<TabPane
|
||||
@@ -1902,9 +1931,10 @@ const AgentManagement = () => {
|
||||
<Text strong>⚠️ This will:</Text>
|
||||
<ul style={{ marginTop: '8px', marginBottom: '8px', paddingLeft: '20px' }}>
|
||||
<li>Reset all agent scripts to file-based defaults (from code)</li>
|
||||
<li>Discard any UI edits you've made</li>
|
||||
<li><strong>All custom script edits made via the UI editor will be permanently lost</strong></li>
|
||||
<li>Create new versions with latest bug fixes</li>
|
||||
<li>All existing agents will need to be upgraded</li>
|
||||
<li>Agents with customized scripts will revert to the default version</li>
|
||||
</ul>
|
||||
<Text type="warning">
|
||||
💡 Use this after code updates to apply fixes to database
|
||||
|
||||
Reference in New Issue
Block a user