zitinexus-router-script/UI/includes/enrollment.php

768 lines
25 KiB
PHP

<?php
/**
* Enrollment Manager for Ziti Router
* Replicates the functionality of the bash script
*/
require_once 'config.php';
require_once 'api_client.php';
class EnrollmentManager {
private $apiClient;
private $routerData;
private $progressCallback;
public function __construct($apiEndpoint = DEFAULT_API_ENDPOINT) {
$this->apiClient = new ApiClient($apiEndpoint);
$this->routerData = [];
}
/**
* Set progress callback function
*/
public function setProgressCallback($callback) {
$this->progressCallback = $callback;
}
/**
* Report progress
*/
private function reportProgress($step, $message, $percentage = null) {
logMessage('INFO', "[$step] $message");
// Store progress in session for AJAX polling
$this->updateSessionProgress($step, $message, $percentage);
if ($this->progressCallback && is_callable($this->progressCallback)) {
call_user_func($this->progressCallback, $step, $message, $percentage);
}
}
/**
* Update session progress for AJAX polling
*/
private function updateSessionProgress($step, $message, $percentage = null) {
$steps = [
'INIT' => 0, 'REQUIREMENTS' => 1, 'INSTALL' => 2, 'DIRECTORIES' => 3,
'REGISTER' => 4, 'CONFIG' => 5, 'ENROLL' => 6, 'SERVICE' => 7,
'START' => 8, 'REPORT' => 9, 'COMPLETE' => 10, 'ERROR' => -1
];
$currentStepIndex = $steps[$step] ?? 0;
// Initialize progress if not exists
if (!isset($_SESSION['enrollment_progress'])) {
$_SESSION['enrollment_progress'] = [
'step' => 'INIT',
'message' => 'Ready to start enrollment',
'percentage' => 0,
'completed_steps' => [],
'current_step_index' => 0,
'status' => 'ready',
'error' => null,
'logs' => []
];
}
$progress = &$_SESSION['enrollment_progress'];
// Update progress data
$progress['step'] = $step;
$progress['message'] = $message;
$progress['current_step_index'] = $currentStepIndex;
if ($percentage !== null) {
$progress['percentage'] = $percentage;
}
// Update status
if ($step === 'ERROR') {
$progress['status'] = 'error';
$progress['error'] = $message;
} elseif ($step === 'COMPLETE') {
$progress['status'] = 'completed';
$progress['percentage'] = 100;
} else {
$progress['status'] = 'running';
}
// Add completed steps
if ($currentStepIndex > 0 && $step !== 'ERROR') {
$stepNames = array_keys($steps);
for ($i = 0; $i < $currentStepIndex; $i++) {
if (!in_array($stepNames[$i], $progress['completed_steps'])) {
$progress['completed_steps'][] = $stepNames[$i];
}
}
}
// Add log entry
$timestamp = date('H:i:s');
$progress['logs'][] = [
'timestamp' => $timestamp,
'step' => $step,
'message' => $message,
'type' => $step === 'ERROR' ? 'error' : 'info'
];
// Keep only last 50 log entries
if (count($progress['logs']) > 50) {
$progress['logs'] = array_slice($progress['logs'], -50);
}
}
/**
* Initialize progress tracking
*/
private function initializeProgress() {
$_SESSION['enrollment_progress'] = [
'step' => 'INIT',
'message' => 'Starting router enrollment process...',
'percentage' => 0,
'completed_steps' => [],
'current_step_index' => 0,
'status' => 'running',
'error' => null,
'logs' => []
];
}
/**
* Clear progress tracking
*/
public function clearProgress() {
unset($_SESSION['enrollment_progress']);
}
/**
* Main enrollment process
*/
public function enrollRouter($hashKey, $apiEndpoint = null) {
try {
// Initialize progress tracking
$this->initializeProgress();
if ($apiEndpoint) {
$this->apiClient = new ApiClient($apiEndpoint);
}
$this->reportProgress('INIT', 'Starting router enrollment process...', 0);
// Step 1: Check system requirements
$this->reportProgress('REQUIREMENTS', 'Checking system requirements...', 10);
if (!$this->checkSystemRequirements()) {
throw new Exception('System requirements check failed');
}
// Step 2: Install OpenZiti if needed
$this->reportProgress('INSTALL', 'Installing OpenZiti CLI...', 20);
if (!$this->installZiti()) {
throw new Exception('OpenZiti installation failed');
}
// Step 3: Create directories
$this->reportProgress('DIRECTORIES', 'Creating necessary directories...', 30);
if (!$this->createDirectories()) {
throw new Exception('Failed to create directories');
}
// Step 4: Register router with API
$this->reportProgress('REGISTER', 'Registering router with ZitiNexus Portal...', 40);
$result = $this->apiClient->registerRouter($hashKey);
if (!$result['success']) {
throw new Exception('Router registration failed: ' . $result['error']);
}
$this->routerData = $result['data'];
$this->reportProgress('REGISTER', 'Router registered successfully: ' . $this->routerData['routerInfo']['name'], 50);
// Step 5: Save configuration files
$this->reportProgress('CONFIG', 'Saving configuration files...', 60);
if (!$this->saveConfiguration()) {
throw new Exception('Failed to save configuration files');
}
// Step 6: Enroll router with OpenZiti
$this->reportProgress('ENROLL', 'Enrolling router with OpenZiti controller...', 70);
if (!$this->enrollWithZiti()) {
throw new Exception('Router enrollment with OpenZiti failed');
}
// Step 7: Create systemd service
$this->reportProgress('SERVICE', 'Creating systemd service...', 80);
if (!$this->createSystemdService()) {
throw new Exception('Failed to create systemd service');
}
// Step 8: Start router service
$this->reportProgress('START', 'Starting router service...', 90);
if (!$this->startRouter()) {
throw new Exception('Failed to start router service');
}
// Step 9: Report success status
$this->reportProgress('REPORT', 'Reporting enrollment status...', 95);
$this->reportSuccessStatus($hashKey);
$this->reportProgress('COMPLETE', 'Router enrollment completed successfully!', 100);
return [
'success' => true,
'routerName' => $this->routerData['routerInfo']['name'],
'routerId' => $this->routerData['routerInfo']['id'],
'message' => 'Router enrollment completed successfully'
];
} catch (Exception $e) {
$errorMsg = $e->getMessage();
logMessage('ERROR', $errorMsg);
$this->reportProgress('ERROR', $errorMsg, null);
// Report failure status
if (!empty($hashKey) && !empty($this->routerData['callbackUrl'])) {
$this->apiClient->reportStatus(
$this->routerData['callbackUrl'],
$hashKey,
'failed',
null,
$errorMsg
);
}
return [
'success' => false,
'error' => $errorMsg
];
}
}
/**
* Check system requirements
*/
private function checkSystemRequirements() {
// Check if running as root
if (!isRunningAsRoot()) {
throw new Exception('This script must be run as root (use sudo)');
}
// Check if curl is available
if (!$this->checkCommand('curl')) {
$this->reportProgress('REQUIREMENTS', 'Installing curl...');
if (!$this->installPackage('curl')) {
return false;
}
}
// Check if jq is available
if (!$this->checkCommand('jq')) {
$this->reportProgress('REQUIREMENTS', 'Installing jq...');
if (!$this->installPackage('jq')) {
return false;
}
}
// Check if systemctl is available
if (!$this->checkCommand('systemctl')) {
throw new Exception('systemctl is required but not available');
}
return true;
}
/**
* Install OpenZiti CLI
*/
private function installZiti() {
// Check if ziti is already installed
if ($this->checkCommand('ziti')) {
$output = '';
executeCommand('ziti version 2>/dev/null | head -n1', $output);
$this->reportProgress('INSTALL', 'OpenZiti CLI already installed: ' . trim($output));
return true;
}
$this->reportProgress('INSTALL', 'Installing OpenZiti CLI from pre-configured repository...');
// Verify repository is configured
if (!file_exists('/etc/apt/sources.list.d/openziti-release.list')) {
throw new Exception('OpenZiti repository not configured. Please run install.sh first to set up the system.');
}
if (!file_exists('/usr/share/keyrings/openziti.gpg')) {
throw new Exception('OpenZiti GPG key not found. Please run install.sh first to set up the system.');
}
// Install openziti-router package from pre-configured repository
$this->reportProgress('INSTALL', 'Installing openziti-router package...');
if (!executeCommand('apt-get install -y openziti-router')) {
$this->reportProgress('INSTALL', 'Trying to install ziti CLI only...');
if (!executeCommand('apt-get install -y ziti')) {
throw new Exception('Failed to install OpenZiti CLI. Repository may not be properly configured. Please run install.sh first.');
}
}
// Verify installation
if (!$this->checkCommand('ziti')) {
throw new Exception('OpenZiti CLI installation failed - command not found after installation');
}
$output = '';
executeCommand('ziti version 2>/dev/null | head -n1', $output);
$this->reportProgress('INSTALL', 'OpenZiti CLI installed successfully: ' . trim($output));
return true;
}
/**
* Create necessary directories
*/
private function createDirectories() {
$directories = [
CONFIG_DIR => 0755,
CERTS_DIR => 0700,
dirname(LOG_FILE) => 0755
];
foreach ($directories as $dir => $permissions) {
if (!is_dir($dir)) {
// Use sudo to create system directories
if (!executeCommand("mkdir -p '$dir'")) {
throw new Exception("Failed to create directory: $dir");
}
if (!executeCommand("chmod " . decoct($permissions) . " '$dir'")) {
throw new Exception("Failed to set permissions for directory: $dir");
}
} else {
// Ensure permissions are correct even if directory exists
executeCommand("chmod " . decoct($permissions) . " '$dir'");
}
}
return true;
}
/**
* Save configuration files
*/
private function saveConfiguration() {
// Save JWT using temp file and sudo
$tempJwtFile = tempnam(sys_get_temp_dir(), 'ziti-jwt');
file_put_contents($tempJwtFile, $this->routerData['jwt']);
if (!executeCommand("cp '$tempJwtFile' " . JWT_FILE)) {
unlink($tempJwtFile);
throw new Exception('Failed to save JWT file');
}
unlink($tempJwtFile);
if (!executeCommand("chmod 600 " . JWT_FILE)) {
throw new Exception('Failed to set JWT file permissions');
}
// Save router configuration using temp file and sudo
$tempConfigFile = tempnam(sys_get_temp_dir(), 'ziti-config');
file_put_contents($tempConfigFile, $this->routerData['routerConfig']['yaml']);
if (!executeCommand("cp '$tempConfigFile' " . ROUTER_CONFIG)) {
unlink($tempConfigFile);
throw new Exception('Failed to save router configuration');
}
unlink($tempConfigFile);
if (!executeCommand("chmod 644 " . ROUTER_CONFIG)) {
throw new Exception('Failed to set router config permissions');
}
// Fix router configuration for proper enrollment
$this->fixRouterConfiguration();
return true;
}
/**
* Fix router configuration (replicate bash script logic)
*/
private function fixRouterConfiguration() {
// Create backup using sudo
executeCommand("cp " . ROUTER_CONFIG . " " . ROUTER_CONFIG . ".backup");
$routerName = $this->routerData['routerInfo']['name'];
$routerId = $this->routerData['routerInfo']['id'];
$tenantId = $this->routerData['metadata']['tenantId'];
$controllerEndpoint = $this->routerData['metadata']['controllerEndpoint'] ?? 'enroll.zitinexus.com:443';
// Add tls: prefix if not present
if (strpos($controllerEndpoint, 'tls:') !== 0) {
$controllerEndpoint = 'tls:' . $controllerEndpoint;
}
// Build role attributes
$roleAttributesSection = '# No role attributes specified';
if (!empty($this->routerData['routerInfo']['roleAttributes'])) {
$roleAttributesSection = "roleAttributes:";
foreach ($this->routerData['routerInfo']['roleAttributes'] as $attr) {
$roleAttributesSection .= "\n - \"$attr\"";
}
}
$generatedAt = date('c');
$configContent = <<<EOF
v: 3
identity:
cert: /etc/zitirouter/certs/$routerName.cert
server_cert: /etc/zitirouter/certs/$routerName.server.chain.cert
key: /etc/zitirouter/certs/$routerName.key
ca: /etc/zitirouter/certs/$routerName.cas
ctrl:
endpoint: $controllerEndpoint
link:
dialers:
- binding: transport
listeners:
# bindings of edge and tunnel requires an "edge" section below
- binding: edge
address: tls:0.0.0.0:443
options:
advertise: 127.0.0.1:443
connectTimeoutMs: 5000
getSessionTimeout: 60
- binding: tunnel
options:
mode: host
edge:
csr:
country: SG
province: SG
locality: Singapore
organization: Genworx
organizationalUnit: ZitiNexus
sans:
dns:
- localhost
- $routerName
ip:
- "127.0.0.1"
- "::1"
# Tenant-specific role attributes
$roleAttributesSection
# Router metadata
metadata:
tenantId: "$tenantId"
zitiRouterId: "$routerId"
routerType: "private-edge"
generatedAt: "$generatedAt"
generatedBy: "ZitiNexus"
EOF;
// Write updated config using temp file and sudo
$tempConfigFile = tempnam(sys_get_temp_dir(), 'ziti-fixed-config');
file_put_contents($tempConfigFile, $configContent);
if (!executeCommand("cp '$tempConfigFile' " . ROUTER_CONFIG)) {
unlink($tempConfigFile);
throw new Exception('Failed to save updated router configuration');
}
unlink($tempConfigFile);
if (!executeCommand("chmod 644 " . ROUTER_CONFIG)) {
throw new Exception('Failed to set updated router config permissions');
}
}
/**
* Enroll router with OpenZiti
*/
private function enrollWithZiti() {
$command = 'ziti router enroll --jwt ' . JWT_FILE . ' ' . ROUTER_CONFIG . ' 2>&1';
$output = '';
if (!executeCommand($command, $output)) {
throw new Exception('Router enrollment failed: ' . $output);
}
// Verify certificates were created using sudo (since certs are root-owned with 600 permissions)
$routerName = $this->routerData['routerInfo']['name'];
$certFile = CERTS_DIR . '/' . $routerName . '.cert';
// Use sudo to check if certificate file exists (www-data can't read root-owned 600 files)
$checkOutput = '';
if (!executeCommand("test -f '$certFile'", $checkOutput)) {
// List what files actually exist for debugging
$listOutput = '';
executeCommand("ls -la " . CERTS_DIR . "/", $listOutput);
throw new Exception("Router certificate not found after enrollment. Expected: $certFile. Files in certs directory: " . $listOutput);
}
return true;
}
/**
* Create systemd service
*/
private function createSystemdService() {
$finalConfig = '/etc/zitirouter/zitirouter.yaml';
// Copy router config to final location using sudo
if (!executeCommand("cp " . ROUTER_CONFIG . " '$finalConfig'")) {
throw new Exception('Failed to copy router config to final location');
}
if (!executeCommand("chmod 644 '$finalConfig'")) {
throw new Exception('Failed to set final config permissions');
}
$serviceContent = <<<EOF
[Unit]
Description=OpenZiti Router
After=network.target
[Service]
ExecStart=/usr/bin/ziti router run /etc/zitirouter/zitirouter.yaml
Restart=on-failure
User=root
WorkingDirectory=/etc/zitirouter
LimitNOFILE=65536
StandardOutput=append:/var/log/ziti-router.log
StandardError=append:/var/log/ziti-router.log
[Install]
WantedBy=multi-user.target
EOF;
// Write service file using sudo
$tempFile = tempnam(sys_get_temp_dir(), 'ziti-service');
file_put_contents($tempFile, $serviceContent);
if (!executeCommand("cp '$tempFile' " . SYSTEMD_SERVICE_FILE)) {
unlink($tempFile);
throw new Exception('Failed to create systemd service file');
}
unlink($tempFile);
// Reload systemd and enable service
if (!executeCommand('systemctl daemon-reload')) {
throw new Exception('Failed to reload systemd');
}
if (!executeCommand('systemctl enable ziti-router.service')) {
throw new Exception('Failed to enable ziti-router service');
}
return true;
}
/**
* Start router service
*/
private function startRouter() {
if (!executeCommand('systemctl start ziti-router.service')) {
throw new Exception('Failed to start router service');
}
// Wait and check status
sleep(3);
$output = '';
executeCommand('systemctl is-active ziti-router.service', $output);
if (trim($output) !== 'active') {
logMessage('WARNING', 'Router service may not be running properly');
}
return true;
}
/**
* Report success status
*/
private function reportSuccessStatus($hashKey) {
if (empty($this->routerData['callbackUrl'])) {
return;
}
$hostname = '';
$arch = '';
$os = '';
$zitiVersion = '';
executeCommand('hostname', $hostname);
executeCommand('uname -m', $arch);
executeCommand('lsb_release -d 2>/dev/null | cut -f2', $os);
executeCommand('ziti version 2>/dev/null | head -n1', $zitiVersion);
$routerInfo = [
'version' => trim($zitiVersion) ?: 'unknown',
'hostname' => trim($hostname),
'arch' => trim($arch),
'os' => trim($os) ?: 'Linux'
];
$this->apiClient->reportStatus(
$this->routerData['callbackUrl'],
$hashKey,
'success',
$routerInfo
);
}
/**
* Check if command exists
*/
private function checkCommand($command) {
$output = '';
return executeCommand("which $command", $output);
}
/**
* Install package using apt
*/
private function installPackage($package) {
return executeCommand("apt-get update && apt-get install -y $package");
}
/**
* Get system status information with enhanced package detection
*/
public function getSystemStatus() {
$status = [
'hostname' => '',
'ziti_status' => 'unknown',
'ziti_version' => '',
'service_active' => false,
'config_exists' => false,
'certificates_exist' => false,
'package_installed' => false,
'last_checked' => time()
];
// Get hostname
executeCommand('hostname', $status['hostname']);
$status['hostname'] = trim($status['hostname']);
// Enhanced Ziti installation check
$zitiInstallationStatus = $this->checkZitiInstallation();
$status['ziti_status'] = $zitiInstallationStatus['status'];
$status['ziti_version'] = $zitiInstallationStatus['version'];
$status['package_installed'] = $zitiInstallationStatus['package_installed'];
// Check service status with more thorough checking
$serviceStatus = $this->checkServiceStatus();
$status['service_active'] = $serviceStatus['active'];
// Check configuration files
$status['config_exists'] = file_exists(ROUTER_CONFIG);
// Check certificates with sudo (since they're root-owned)
$status['certificates_exist'] = $this->checkCertificatesExist();
return $status;
}
/**
* Enhanced Ziti installation checking
*/
private function checkZitiInstallation() {
$result = [
'status' => 'not_installed',
'version' => '',
'package_installed' => false
];
// Check if package is installed via dpkg
$dpkgOutput = '';
$packageInstalled = executeCommand('dpkg -l | grep -E "(openziti-router|ziti)" | grep -v "^rc"', $dpkgOutput);
if ($packageInstalled && !empty(trim($dpkgOutput))) {
$result['package_installed'] = true;
// Extract version from dpkg output if available
if (preg_match('/(\d+\.\d+\.\d+)/', $dpkgOutput, $matches)) {
$result['version'] = $matches[1];
}
}
// Check if command works
$versionOutput = '';
$commandWorks = executeCommand('ziti version 2>/dev/null | head -n1', $versionOutput);
if ($commandWorks && !empty(trim($versionOutput))) {
$result['status'] = 'installed';
// Use command output version if we don't have one from dpkg
if (empty($result['version'])) {
$result['version'] = trim($versionOutput);
}
} else if ($result['package_installed']) {
// Package is installed but command doesn't work - might be broken
$result['status'] = 'broken';
}
return $result;
}
/**
* Enhanced service status checking
*/
private function checkServiceStatus() {
$result = [
'active' => false,
'enabled' => false,
'status' => 'unknown'
];
// Check if service is active
$activeOutput = '';
if (executeCommand('systemctl is-active ziti-router.service 2>/dev/null', $activeOutput)) {
$result['active'] = trim($activeOutput) === 'active';
$result['status'] = trim($activeOutput);
}
// Check if service is enabled
$enabledOutput = '';
if (executeCommand('systemctl is-enabled ziti-router.service 2>/dev/null', $enabledOutput)) {
$result['enabled'] = trim($enabledOutput) === 'enabled';
}
return $result;
}
/**
* Check if certificates exist (using sudo since they're root-owned)
*/
private function checkCertificatesExist() {
if (!is_dir(CERTS_DIR)) {
return false;
}
// Use sudo to list certificate files since they're root-owned with 600 permissions
$output = '';
if (executeCommand("find " . CERTS_DIR . " -name '*.cert' -type f 2>/dev/null", $output)) {
return !empty(trim($output));
}
return false;
}
/**
* Force refresh system status (clear any caches)
*/
public function refreshSystemStatus() {
// Clear any potential caches
clearstatcache();
// Update package database
executeCommand('apt-get update -qq 2>/dev/null');
return $this->getSystemStatus();
}
}
?>