Files
api/services/nginx/app/classes/edge_gateway_schema_bootstrap.php
T
Jeppe Bundgaard 7d450e285e Remove outdated edge gateway object classes, add new agent implementation
Transitioned from obsolete gateway object classes (`edge_gateway_shell_action_jobs_o`, `edge_gateway_shell_events_o`, `edge_gateway_shell_sessions_o`, `edge_gateway_update_jobs_o`) to the new agent implementation (`edge-gateway-agent/agent.php`).
2026-04-21 14:13:17 +02:00

292 lines
14 KiB
PHP

<?php
namespace classes;
/**
* Ensures additive department edge gateway tables exist.
*
* The legacy PHP stack has no centralized migration runner, so this bootstrap
* must be safe to call from runtime flows and tests.
*/
class edge_gateway_schema_bootstrap
{
private static bool $initialized = false;
public static function ensureTables(): void
{
if (self::$initialized) {
return;
}
global $db;
$queries = [
"CREATE TABLE IF NOT EXISTS edge_gateways (
id INT AUTO_INCREMENT PRIMARY KEY,
department_id INT NOT NULL,
label VARCHAR(255) NOT NULL,
hostname VARCHAR(255) NULL,
agent_token_hash VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
transport_mode VARCHAR(32) NOT NULL DEFAULT 'gateway',
release_channel VARCHAR(32) NOT NULL DEFAULT 'stable',
installed_version VARCHAR(64) NULL,
target_version VARCHAR(64) NULL,
last_heartbeat_at DATETIME NULL,
last_seen_ip VARCHAR(64) NULL,
discovery_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
is_primary TINYINT(1) NOT NULL DEFAULT 1,
metadata_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
INDEX idx_edge_gateways_department_status (department_id, status),
INDEX idx_edge_gateways_department_primary (department_id, is_primary),
INDEX idx_edge_gateways_last_heartbeat (last_heartbeat_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_claim_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
department_id INT NOT NULL,
label VARCHAR(255) NULL,
token_hash VARCHAR(255) NOT NULL,
created_by INT NULL,
expires_at DATETIME NOT NULL,
used_at DATETIME NULL,
metadata_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
INDEX idx_edge_gateway_claim_tokens_department (department_id),
INDEX idx_edge_gateway_claim_tokens_expires (expires_at),
INDEX idx_edge_gateway_claim_tokens_used (used_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_device_inventory (
id INT AUTO_INCREMENT PRIMARY KEY,
gateway_id INT NOT NULL,
device_id VARCHAR(255) NOT NULL,
local_ip VARCHAR(64) NULL,
model VARCHAR(255) NULL,
channel_count INT NOT NULL DEFAULT 1,
capabilities_json JSON NULL,
online TINYINT(1) NOT NULL DEFAULT 0,
last_seen_at DATETIME NULL,
metadata_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
UNIQUE KEY uniq_edge_gateway_inventory_device (gateway_id, device_id),
INDEX idx_edge_gateway_inventory_gateway (gateway_id),
INDEX idx_edge_gateway_inventory_ip (local_ip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_relay_bindings (
id INT AUTO_INCREMENT PRIMARY KEY,
gateway_id INT NOT NULL,
department_id INT NOT NULL,
relay_id VARCHAR(255) NOT NULL,
device_id VARCHAR(255) NOT NULL,
local_ip VARCHAR(64) NULL,
channel INT NOT NULL DEFAULT 0,
fallback_mode VARCHAR(32) NOT NULL DEFAULT 'PREFER_LOCAL',
binding_source VARCHAR(64) NOT NULL DEFAULT 'MANUAL',
approved_by INT NULL,
approved_at DATETIME NULL,
metadata_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
UNIQUE KEY uniq_edge_gateway_binding (gateway_id, relay_id),
INDEX idx_edge_gateway_binding_department (department_id),
INDEX idx_edge_gateway_binding_device (gateway_id, device_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_command_jobs (
id INT AUTO_INCREMENT PRIMARY KEY,
gateway_id INT NOT NULL,
command_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
request_json JSON NULL,
response_json JSON NULL,
delivery_json JSON NULL,
correlation_id VARCHAR(128) NOT NULL,
requested_by INT NULL,
requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME NULL,
error_message TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
UNIQUE KEY uniq_edge_gateway_command_correlation (correlation_id),
INDEX idx_edge_gateway_command_gateway (gateway_id),
INDEX idx_edge_gateway_command_status (status),
INDEX idx_edge_gateway_command_type (command_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_operations (
id INT AUTO_INCREMENT PRIMARY KEY,
gateway_id INT NOT NULL,
type VARCHAR(32) NOT NULL,
operation_type VARCHAR(32) NOT NULL DEFAULT 'DISCOVERY',
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
request_json JSON NULL,
summary_json JSON NULL,
result_json JSON NULL,
error_code VARCHAR(128) NULL,
error_message TEXT NULL,
correlation_id VARCHAR(128) NOT NULL,
agent_instance_id VARCHAR(128) NULL,
lease_expires_at DATETIME NULL,
last_progress_at DATETIME NULL,
attempt_count INT NOT NULL DEFAULT 0,
requested_by INT NULL,
requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME NULL,
completed_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
UNIQUE KEY uniq_edge_gateway_operation_correlation (correlation_id),
INDEX idx_edge_gateway_operations_gateway (gateway_id),
INDEX idx_edge_gateway_operations_status (status),
INDEX idx_edge_gateway_operations_type (type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_operation_events (
id INT AUTO_INCREMENT PRIMARY KEY,
operation_id INT NOT NULL,
gateway_id INT NOT NULL,
level VARCHAR(16) NOT NULL DEFAULT 'INFO',
code VARCHAR(128) NULL,
message TEXT NOT NULL,
context_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_edge_gateway_operation_events_operation (operation_id),
INDEX idx_edge_gateway_operation_events_gateway (gateway_id),
INDEX idx_edge_gateway_operation_events_level (level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS edge_gateway_audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
gateway_id INT NULL,
department_id INT NULL,
action VARCHAR(128) NOT NULL,
actor_user_id INT NULL,
actor_type VARCHAR(32) NOT NULL DEFAULT 'USER',
severity VARCHAR(16) NOT NULL DEFAULT 'INFO',
context_json JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_edge_gateway_audit_gateway (gateway_id),
INDEX idx_edge_gateway_audit_department (department_id),
INDEX idx_edge_gateway_audit_action (action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($queries as $sql) {
$db->query($sql);
}
self::ensureColumn('edge_gateway_command_jobs', 'delivery_json', 'JSON NULL AFTER response_json');
self::ensureColumn('edge_gateway_relay_bindings', 'fallback_mode', "VARCHAR(32) NOT NULL DEFAULT 'PREFER_LOCAL' AFTER channel");
self::ensureColumn('edge_gateway_operations', 'type', "VARCHAR(32) NOT NULL DEFAULT 'DISCOVERY' AFTER gateway_id");
self::ensureColumn('edge_gateway_operations', 'operation_type', "VARCHAR(32) NOT NULL DEFAULT 'DISCOVERY' AFTER type");
self::ensureColumn('edge_gateway_operations', 'summary_json', 'JSON NULL AFTER request_json');
self::ensureColumn('edge_gateway_operations', 'result_json', 'JSON NULL AFTER summary_json');
self::ensureColumn('edge_gateway_operations', 'error_code', 'VARCHAR(128) NULL AFTER result_json');
self::ensureColumn('edge_gateway_operations', 'error_message', 'TEXT NULL AFTER error_code');
self::ensureColumn('edge_gateway_operations', 'correlation_id', 'VARCHAR(128) NULL AFTER error_message');
self::ensureColumn('edge_gateway_operations', 'agent_instance_id', 'VARCHAR(128) NULL AFTER correlation_id');
self::ensureColumn('edge_gateway_operations', 'lease_expires_at', 'DATETIME NULL AFTER agent_instance_id');
self::ensureColumn('edge_gateway_operations', 'last_progress_at', 'DATETIME NULL AFTER lease_expires_at');
self::ensureColumn('edge_gateway_operations', 'attempt_count', 'INT NOT NULL DEFAULT 0 AFTER last_progress_at');
self::ensureColumn('edge_gateway_operations', 'requested_by', 'INT NULL AFTER attempt_count');
self::ensureColumn('edge_gateway_operations', 'requested_at', 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER requested_by');
self::ensureColumn('edge_gateway_operations', 'started_at', 'DATETIME NULL AFTER requested_at');
self::ensureColumn('edge_gateway_operations', 'completed_at', 'DATETIME NULL AFTER started_at');
self::ensureColumn('edge_gateway_operations', 'updated_at', 'TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP AFTER created_at');
self::ensureColumn('edge_gateway_operations', 'deleted_at', 'TIMESTAMP NULL DEFAULT NULL AFTER updated_at');
self::ensureColumn('edge_gateway_operation_events', 'code', 'VARCHAR(128) NULL AFTER level');
self::ensureColumn('edge_gateway_operation_events', 'context_json', 'JSON NULL AFTER message');
self::ensureColumn('edge_gateway_audit_logs', 'actor_type', "VARCHAR(32) NOT NULL DEFAULT 'USER' AFTER actor_user_id");
self::ensureColumn('edge_gateway_audit_logs', 'severity', "VARCHAR(16) NOT NULL DEFAULT 'INFO' AFTER actor_type");
self::ensureColumn('edge_gateway_audit_logs', 'context_json', 'JSON NULL AFTER severity');
self::syncOperationTypeColumns();
self::$initialized = true;
}
private static function ensureColumn(string $table, string $column, string $definition): void
{
global $db;
if (self::tableHasColumn($table, $column)) {
return;
}
if (!preg_match('/^[A-Za-z0-9_]+$/', $table) || !preg_match('/^[A-Za-z0-9_]+$/', $column)) {
throw new \RuntimeException('Invalid schema bootstrap identifier');
}
$db->query(
"ALTER TABLE `$table`
ADD COLUMN `$column` $definition"
);
}
private static function tableHasColumn(string $table, string $column): bool
{
global $db;
$table = $db->escape_string($table);
$column = $db->escape_string($column);
$database = $db->escape_string($db->getDatabase());
$result = $db->query(
"SELECT COUNT(*) AS c
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = '$database'
AND TABLE_NAME = '$table'
AND COLUMN_NAME = '$column'"
);
if (!$result) {
return false;
}
$row = $result->fetch_assoc();
return ((int)($row['c'] ?? 0)) > 0;
}
private static function syncOperationTypeColumns(): void
{
global $db;
if (!self::tableHasColumn('edge_gateway_operations', 'type')
|| !self::tableHasColumn('edge_gateway_operations', 'operation_type')) {
return;
}
$db->query(
"UPDATE edge_gateway_operations
SET type = operation_type
WHERE operation_type IS NOT NULL
AND operation_type <> ''
AND (type IS NULL OR type = '' OR type <> operation_type)"
);
$db->query(
"UPDATE edge_gateway_operations
SET operation_type = type
WHERE type IS NOT NULL
AND type <> ''
AND (operation_type IS NULL OR operation_type = '')"
);
}
}