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"); if ($this->progressCallback && is_callable($this->progressCallback)) { call_user_func($this->progressCallback, $step, $message, $percentage); } } /** * Main enrollment process */ public function enrollRouter($hashKey, $apiEndpoint = null) { try { 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', 'Setting up OpenZiti package repository...'); // Add GPG key using enhanced method to handle PHP execution environment if (!$this->addOpenZitiGpgKey()) { throw new Exception('Failed to add OpenZiti GPG key'); } // Set proper permissions if (!executeCommand('chmod a+r /usr/share/keyrings/openziti.gpg')) { throw new Exception('Failed to set GPG key permissions'); } // Add repository to sources list $repoContent = 'deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable debian main'; $tempFile = tempnam(sys_get_temp_dir(), 'openziti-repo'); file_put_contents($tempFile, $repoContent); if (!executeCommand("cp '$tempFile' /etc/apt/sources.list.d/openziti-release.list")) { unlink($tempFile); throw new Exception('Failed to add OpenZiti repository'); } unlink($tempFile); // Update package list $this->reportProgress('INSTALL', 'Updating package list...'); if (!executeCommand('apt-get update')) { throw new Exception('Failed to update package list'); } // Install openziti-router package $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'); } } // 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 ); } /** * Add OpenZiti GPG key with enhanced error handling for CloudStack environments */ private function addOpenZitiGpgKey() { $gpgKeyUrl = 'https://get.openziti.io/tun/package-repos.gpg'; $gpgKeyPath = '/usr/share/keyrings/openziti.gpg'; // First, check if GPG key already exists and is valid if (file_exists($gpgKeyPath) && filesize($gpgKeyPath) > 0) { $this->reportProgress('INSTALL', 'OpenZiti GPG key already exists (' . filesize($gpgKeyPath) . ' bytes), skipping installation'); logMessage('INFO', 'GPG key already exists at: ' . $gpgKeyPath); return true; } $this->reportProgress('INSTALL', 'Installing OpenZiti GPG key...'); // Ensure keyrings directory exists with proper permissions if (!$this->ensureKeyringsDirectory()) { logMessage('ERROR', 'Failed to create keyrings directory'); return false; } // Method 1: CloudStack-optimized approach - download and process separately $this->reportProgress('INSTALL', 'Downloading GPG key...'); $tempGpgFile = tempnam(sys_get_temp_dir(), 'openziti-gpg'); // Step 1: Download GPG key with detailed error reporting $downloadCommand = 'curl -sSLf --connect-timeout 30 --max-time 60 ' . $gpgKeyUrl . ' -o ' . $tempGpgFile; $output = ''; if (!executeCommand($downloadCommand, $output)) { @unlink($tempGpgFile); $errorMsg = 'Failed to download GPG key from ' . $gpgKeyUrl . '. Error: ' . $output; logMessage('ERROR', $errorMsg); $this->reportProgress('INSTALL', 'Download failed: ' . $output); return false; } // Step 2: Verify downloaded file if (!file_exists($tempGpgFile) || filesize($tempGpgFile) == 0) { @unlink($tempGpgFile); $errorMsg = 'Downloaded GPG key file is empty or missing'; logMessage('ERROR', $errorMsg); $this->reportProgress('INSTALL', $errorMsg); return false; } $fileSize = filesize($tempGpgFile); $this->reportProgress('INSTALL', "GPG key downloaded successfully ($fileSize bytes), processing..."); logMessage('INFO', "Downloaded GPG key: $fileSize bytes to $tempGpgFile"); // Step 3: Try multiple processing methods with detailed error reporting $methods = [ 'sudo_gpg_dearmor' => [ 'name' => 'Sudo GPG dearmor', 'command' => "sudo /usr/bin/gpg --dearmor --output '$gpgKeyPath' '$tempGpgFile'" ], 'direct_gpg_dearmor' => [ 'name' => 'Direct GPG dearmor', 'command' => "/usr/bin/gpg --dearmor --output '$gpgKeyPath' '$tempGpgFile'" ], 'sudo_cat_pipe' => [ 'name' => 'Sudo cat pipe', 'command' => "sudo bash -c 'cat $tempGpgFile | /usr/bin/gpg --dearmor > $gpgKeyPath'" ], 'sudo_redirect' => [ 'name' => 'Sudo redirect', 'command' => "sudo bash -c '/usr/bin/gpg --dearmor < $tempGpgFile > $gpgKeyPath'" ], 'python_base64' => [ 'name' => 'Python base64 decode', 'command' => "sudo python3 -c \"import base64; open('$gpgKeyPath', 'wb').write(base64.b64decode(open('$tempGpgFile', 'rb').read()))\"" ] ]; foreach ($methods as $methodKey => $method) { $this->reportProgress('INSTALL', "Trying method: {$method['name']}..."); logMessage('INFO', "Attempting GPG processing method: {$method['name']}"); $output = ''; if (executeCommand($method['command'], $output)) { // Verify the processed file exists and has content if (file_exists($gpgKeyPath) && filesize($gpgKeyPath) > 0) { @unlink($tempGpgFile); $processedSize = filesize($gpgKeyPath); $this->reportProgress('INSTALL', "GPG key processed successfully using {$method['name']} ($processedSize bytes)"); logMessage('INFO', "GPG key successfully processed: $processedSize bytes at $gpgKeyPath"); return true; } else { logMessage('WARNING', "Method {$method['name']} completed but output file is missing or empty"); } } else { logMessage('WARNING', "Method {$method['name']} failed: $output"); } } // Method 4: Last resort - try to use the raw file directly $this->reportProgress('INSTALL', 'Trying raw file copy as last resort...'); logMessage('INFO', 'Attempting raw file copy as final fallback'); if (executeCommand("sudo cp '$tempGpgFile' '$gpgKeyPath'", $output)) { if (file_exists($gpgKeyPath) && filesize($gpgKeyPath) > 0) { @unlink($tempGpgFile); $this->reportProgress('INSTALL', 'GPG key copied as raw file - apt will handle format conversion'); logMessage('INFO', 'GPG key copied as raw file for apt to process'); return true; } } // Clean up and report final failure @unlink($tempGpgFile); $errorMsg = 'All GPG key installation methods failed. Check system permissions and network connectivity.'; logMessage('ERROR', $errorMsg); $this->reportProgress('INSTALL', $errorMsg); // Additional diagnostic information $this->logDiagnosticInfo(); return false; } /** * Ensure keyrings directory exists with proper permissions */ private function ensureKeyringsDirectory() { $keyringsDir = '/usr/share/keyrings'; // Check if directory exists if (is_dir($keyringsDir)) { logMessage('INFO', 'Keyrings directory already exists'); return true; } // Try to create directory $output = ''; if (executeCommand("sudo mkdir -p '$keyringsDir'", $output)) { if (executeCommand("sudo chmod 755 '$keyringsDir'", $output)) { logMessage('INFO', 'Keyrings directory created successfully'); return true; } else { logMessage('ERROR', 'Failed to set keyrings directory permissions: ' . $output); return false; } } else { logMessage('ERROR', 'Failed to create keyrings directory: ' . $output); return false; } } /** * Log diagnostic information for troubleshooting */ private function logDiagnosticInfo() { $diagnostics = []; // Check basic commands $commands = ['curl', 'gpg', 'python3', 'sudo']; foreach ($commands as $cmd) { $output = ''; $available = executeCommand("which $cmd", $output) ? 'available' : 'not found'; $diagnostics[] = "$cmd: $available"; } // Check permissions $output = ''; executeCommand('id', $output); $diagnostics[] = "Current user: " . trim($output); // Check sudo permissions $output = ''; executeCommand('sudo -l | grep gpg', $output); $diagnostics[] = "GPG sudo permissions: " . (empty(trim($output)) ? 'none found' : 'available'); // Check network connectivity $output = ''; $networkOk = executeCommand('curl -I --connect-timeout 5 https://get.openziti.io', $output) ? 'working' : 'failed'; $diagnostics[] = "Network connectivity: $networkOk"; // Check disk space $output = ''; executeCommand('df -h /usr/share', $output); $diagnostics[] = "Disk space: " . trim($output); logMessage('INFO', 'GPG Installation Diagnostics: ' . implode('; ', $diagnostics)); } /** * 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 */ public function getSystemStatus() { $status = [ 'hostname' => '', 'ziti_status' => 'unknown', 'ziti_version' => '', 'service_active' => false, 'config_exists' => false, 'certificates_exist' => false ]; // Get hostname executeCommand('hostname', $status['hostname']); $status['hostname'] = trim($status['hostname']); // Check if ziti command exists and get version if ($this->checkCommand('ziti')) { executeCommand('ziti version 2>/dev/null | head -n1', $status['ziti_version']); $status['ziti_version'] = trim($status['ziti_version']); $status['ziti_status'] = 'installed'; } else { $status['ziti_status'] = 'not_installed'; } // Check service status $output = ''; if (executeCommand('systemctl is-active ziti-router.service 2>/dev/null', $output)) { $status['service_active'] = trim($output) === 'active'; } // Check configuration files $status['config_exists'] = file_exists(ROUTER_CONFIG); // Check certificates if (is_dir(CERTS_DIR)) { $certFiles = glob(CERTS_DIR . '/*.cert'); $status['certificates_exist'] = !empty($certFiles); } return $status; } } ?>