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 = <<&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 = <<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(); } } ?>