false, 'error' => 'Invalid request']); exit; } $action = $input['action']; // Public actions (no authentication required) $publicActions = ['login', 'check_session']; // Check authentication for non-public actions if (!in_array($action, $publicActions)) { requireLogin(); } switch ($action) { // Authentication actions case 'login': $username = $input['username'] ?? ''; $password = $input['password'] ?? ''; if (authenticateUser($username, $password)) { echo json_encode([ 'success' => true, 'user' => getCurrentUser() ]); } else { echo json_encode([ 'success' => false, 'error' => 'Invalid username or password' ]); } break; case 'logout': logout(); echo json_encode(['success' => true]); break; case 'check_session': if (isLoggedIn()) { echo json_encode([ 'success' => true, 'logged_in' => true, 'user' => getCurrentUser() ]); } else { echo json_encode([ 'success' => true, 'logged_in' => false ]); } break; case 'get_users': getAllUsers(); break; case 'create_user': createUser($input); break; case 'update_user': updateUser($input); break; case 'delete_user': deleteUser($input['username'] ?? ''); break; case 'change_password': changePassword($input); break; case 'save_config': requireAdmin(); // Only admin can save config saveConfig($input['config']); break; case 'load_config': loadConfig(); break; case 'restart_service': requireAdmin(); // Only admin can restart service break; case 'list_tapes': listTapes(); break; case 'delete_tape': deleteTape($input['tape_name']); break; case 'bulk_delete_tapes': bulkDeleteTapes($input['pattern']); break; case 'create_tapes': createTapes($input); break; case 'list_targets': listTargets(); break; case 'create_target': createTarget($input); break; case 'delete_target': deleteTarget($input['tid']); break; case 'add_lun': addLun($input); break; case 'bind_initiator': bindInitiator($input); break; case 'unbind_initiator': unbindInitiator($input); break; case 'device_mapping': getDeviceMapping(); break; case 'library_status': getLibraryStatus(); break; case 'system_health': getSystemHealth(); break; case 'restart_appliance': restartAppliance(); break; case 'shutdown_appliance': shutdownAppliance(); break; default: echo json_encode(['success' => false, 'error' => 'Unknown action']); } // ============================================ // iSCSI Target Management Functions // ============================================ function listTargets() { $output = []; $returnCode = 0; // Use absolute path exec('sudo /usr/sbin/tgtadm --lld iscsi --mode target --op show 2>&1', $output, $returnCode); if ($returnCode !== 0) { echo json_encode([ 'success' => false, 'error' => 'Failed to list targets: ' . implode(' ', $output) ]); return; } $targets = []; $currentTarget = null; $currentSession = null; $section = ''; // 'acl', 'nexus', 'account', etc. foreach ($output as $line) { // Check for new Target start if (preg_match('/^Target (\d+): (.+)$/', $line, $matches)) { // Save previous target and session if ($currentTarget) { if ($currentSession) { $currentTarget['sessions'][] = $currentSession; $currentSession = null; } $targets[] = $currentTarget; } $currentTarget = [ 'tid' => intval($matches[1]), 'name' => trim($matches[2]), 'luns' => 0, 'acls' => 0, 'sessions' => [] ]; $section = ''; } elseif ($currentTarget) { // Check for LUNs (also simple check for count) if (preg_match('/^\s+LUN: (\d+)/', $line)) { $currentTarget['luns']++; // If we were parsing a session, save it as LUN info usually means we left nexus section or are in LUN section if ($currentSession) { $currentTarget['sessions'][] = $currentSession; $currentSession = null; } } // Section Headers elseif (preg_match('/^\s+ACL information:/', $line)) { $section = 'acl'; if ($currentSession) { $currentTarget['sessions'][] = $currentSession; $currentSession = null; } } elseif (preg_match('/^\s+I_T nexus information:/', $line)) { $section = 'nexus'; } elseif (preg_match('/^\s+Account information:/', $line)) { $section = 'account'; // Ignore or handle if needed if ($currentSession) { $currentTarget['sessions'][] = $currentSession; $currentSession = null; } } // ACL Section Content elseif ($section === 'acl' && preg_match('/^\s+(.+)$/', $line, $matches)) { $content = trim($matches[1]); // Filter out headers if regex matches them by accident (though section logic prevents this usually) if (!empty($content) && !preg_match('/^(Account|I_T nexus|LUN|System)/', $content)) { $currentTarget['acls']++; } } // I_T Nexus Section Content (Sessions) elseif ($section === 'nexus') { if (preg_match('/^\s+I_T nexus: (\d+)/', $line, $matches)) { // New session found if ($currentSession) { $currentTarget['sessions'][] = $currentSession; } $currentSession = [ 'nexus_id' => $matches[1], 'initiator' => 'Unknown', 'ip' => 'Unknown' ]; } elseif ($currentSession && preg_match('/^\s+Initiator: (.+)$/', $line, $matches)) { $currentSession['initiator'] = trim($matches[1]); } elseif ($currentSession && preg_match('/^\s+IP Address: (.+)$/', $line, $matches)) { $currentSession['ip'] = trim($matches[1]); } } } } // Save last items if ($currentTarget) { if ($currentSession) { $currentTarget['sessions'][] = $currentSession; } $targets[] = $currentTarget; } echo json_encode([ 'success' => true, 'targets' => $targets ]); } function createTarget($params) { $tid = isset($params['tid']) ? intval($params['tid']) : 0; $name = isset($params['name']) ? trim($params['name']) : ''; if ($tid <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid TID']); return; } if (empty($name)) { echo json_encode(['success' => false, 'error' => 'Target name is required']); return; } if (!preg_match('/^[a-zA-Z0-9._-]+$/', $name)) { echo json_encode(['success' => false, 'error' => 'Invalid target name format']); return; } $iqn = "iqn.2024-01.com.vtl-linux:$name"; $command = sprintf( 'sudo tgtadm --lld iscsi --mode target --op new --tid %d --targetname %s 2>&1', $tid, escapeshellarg($iqn) ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'Target created successfully', 'iqn' => $iqn ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to create target: ' . implode(' ', $output) ]); } } function deleteTarget($tid) { $tid = intval($tid); if ($tid <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid TID']); return; } $command = sprintf( 'sudo tgtadm --lld iscsi --mode target --op delete --force --tid %d 2>&1', $tid ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'Target deleted successfully' ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to delete target: ' . implode(' ', $output) ]); } } function addLun($params) { $tid = isset($params['tid']) ? intval($params['tid']) : 0; $lun = isset($params['lun']) ? intval($params['lun']) : 0; $device = isset($params['device']) ? trim($params['device']) : ''; if ($tid <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid TID']); return; } if ($lun < 0) { echo json_encode(['success' => false, 'error' => 'Invalid LUN number']); return; } if (empty($device)) { echo json_encode(['success' => false, 'error' => 'Device path is required']); return; } if (!preg_match('#^/dev/(sg\d+|sd[a-z]+)$#', $device)) { echo json_encode(['success' => false, 'error' => 'Invalid device path']); return; } if (!file_exists($device)) { echo json_encode(['success' => false, 'error' => 'Device does not exist']); return; } // For generic SCSI devices (sg), we MUST specify bstype=sg AND device-type=pt (passthrough) $bstypeParam = ''; // Validate device path strictly to avoid needing escapeshellarg (which adds quotes that might confuse tgtadm parsing in some versions) // Regex allows /dev/sg0-999 or /dev/st0-999 or /dev/nst0-999 if (preg_match('#^/dev/(sg|st|nst)[0-9]+$#', $device)) { $bstypeParam = '--bstype sg --device-type pt'; } $command = sprintf( 'sudo /usr/sbin/tgtadm --lld iscsi --mode logicalunit --op new --tid %d --lun %d --backing-store %s %s 2>&1', $tid, $lun, $device, // Safe to use raw because we validated usage above or in previous regex check $bstypeParam ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'LUN added successfully' ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to add LUN: ' . implode(' ', $output), 'command' => $command // Debug info ]); } } function bindInitiator($params) { $tid = isset($params['tid']) ? intval($params['tid']) : 0; $address = isset($params['address']) ? trim($params['address']) : ''; if ($tid <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid TID']); return; } if (empty($address)) { echo json_encode(['success' => false, 'error' => 'Initiator address is required']); return; } if ($address !== 'ALL' && !filter_var($address, FILTER_VALIDATE_IP)) { echo json_encode(['success' => false, 'error' => 'Invalid IP address']); return; } $command = sprintf( 'sudo tgtadm --lld iscsi --mode target --op bind --tid %d --initiator-address %s 2>&1', $tid, escapeshellarg($address) ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'Initiator allowed successfully' ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to bind initiator: ' . implode(' ', $output) ]); } } function unbindInitiator($params) { $tid = isset($params['tid']) ? intval($params['tid']) : 0; $address = isset($params['address']) ? trim($params['address']) : ''; if ($tid <= 0) { echo json_encode(['success' => false, 'error' => 'Invalid TID']); return; } if (empty($address)) { echo json_encode(['success' => false, 'error' => 'Initiator address is required']); return; } if ($address !== 'ALL' && !filter_var($address, FILTER_VALIDATE_IP)) { echo json_encode(['success' => false, 'error' => 'Invalid IP address']); return; } $command = sprintf( 'sudo tgtadm --lld iscsi --mode target --op unbind --tid %d --initiator-address %s 2>&1', $tid, escapeshellarg($address) ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'Initiator blocked successfully' ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to unbind initiator: ' . implode(' ', $output) ]); } } function createTapes($params) { $library = isset($params['library']) ? intval($params['library']) : 10; $barcodePrefix = isset($params['barcode_prefix']) ? trim($params['barcode_prefix']) : ''; $startNum = isset($params['start_num']) ? intval($params['start_num']) : 0; $count = isset($params['count']) ? intval($params['count']) : 1; $size = isset($params['size']) ? intval($params['size']) : 2500000; $mediaType = isset($params['media_type']) ? $params['media_type'] : 'data'; $density = isset($params['density']) ? $params['density'] : 'LTO6'; if (empty($barcodePrefix)) { echo json_encode(['success' => false, 'error' => 'Barcode prefix is required']); return; } if ($count < 1 || $count > 100) { echo json_encode(['success' => false, 'error' => 'Count must be between 1 and 100']); return; } if (strlen($barcodePrefix) > 6) { echo json_encode(['success' => false, 'error' => 'Barcode prefix too long (max 6 chars)']); return; } $validMediaTypes = ['data', 'clean', 'WORM']; if (!in_array($mediaType, $validMediaTypes)) { echo json_encode(['success' => false, 'error' => 'Invalid media type']); return; } $validDensities = ['LTO5', 'LTO6', 'LTO7', 'LTO8', 'LTO9']; if (!in_array($density, $validDensities)) { echo json_encode(['success' => false, 'error' => 'Invalid density']); return; } $createdCount = 0; $errors = []; for ($i = 0; $i < $count; $i++) { $barcodeNum = str_pad($startNum + $i, 6, '0', STR_PAD_LEFT); $barcode = $barcodePrefix . $barcodeNum; $command = sprintf( 'mktape -l %d -m %s -s %d -t %s -d %s 2>&1', $library, escapeshellarg($barcode), $size, escapeshellarg($mediaType), escapeshellarg($density) ); $output = []; $returnCode = 0; exec($command, $output, $returnCode); if ($returnCode === 0) { $createdCount++; } else { $errors[] = $barcode . ': ' . implode(' ', $output); } } if ($createdCount > 0) { $response = [ 'success' => true, 'created_count' => $createdCount, 'message' => "Created $createdCount tape(s)" ]; if (!empty($errors)) { $response['errors'] = $errors; } echo json_encode($response); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to create tapes: ' . implode('; ', $errors) ]); } } function saveConfig($config) { global $DEVICE_CONF, $BACKUP_DIR; if (empty($config)) { echo json_encode(['success' => false, 'error' => 'Empty configuration']); return; } // Create backup of existing config if (file_exists($DEVICE_CONF)) { $backupFile = $BACKUP_DIR . '/device.conf.' . date('Y-m-d_H-i-s'); if (!copy($DEVICE_CONF, $backupFile)) { echo json_encode(['success' => false, 'error' => 'Failed to create backup']); return; } } // Write new config if (file_put_contents($DEVICE_CONF, $config) === false) { echo json_encode(['success' => false, 'error' => 'Failed to write configuration file. Check permissions.']); return; } // Set proper permissions chmod($DEVICE_CONF, 0644); echo json_encode([ 'success' => true, 'file' => $DEVICE_CONF, 'backup' => isset($backupFile) ? $backupFile : null, 'message' => 'Configuration saved successfully' ]); } function loadConfig() { global $DEVICE_CONF; if (!file_exists($DEVICE_CONF)) { echo json_encode(['success' => false, 'error' => 'Configuration file not found']); return; } $config = file_get_contents($DEVICE_CONF); echo json_encode([ 'success' => true, 'config' => $config ]); } function restartService() { // Run systemctl restart in background to prevent PHP timeout/hanging // We strictly redirect output to /dev/null to ensure exec returns immediately exec("sudo /usr/bin/systemctl restart mhvtl > /dev/null 2>&1 &"); echo json_encode([ 'success' => true, 'message' => 'Service restart initiated. Changes will take effect in a few seconds.' ]); } function listTapes() { $tapeDir = '/opt/mhvtl'; if (!is_dir($tapeDir)) { echo json_encode(['success' => false, 'error' => 'Tape directory not found']); return; } $tapes = []; $items = scandir($tapeDir); foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $tapeDir . '/' . $item; if (is_dir($path)) { $stat = stat($path); $size = getDirSize($path); $tapes[] = [ 'name' => $item, 'size' => formatBytes($size), 'modified' => date('Y-m-d H:i:s', $stat['mtime']) ]; } } usort($tapes, function($a, $b) { return strcmp($a['name'], $b['name']); }); echo json_encode([ 'success' => true, 'tapes' => $tapes ]); } function getDirSize($dir) { $size = 0; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) { $size += $file->getSize(); } return $size; } function formatBytes($bytes) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, 2) . ' ' . $units[$pow]; } function deleteTape($tapeName) { $tapeDir = '/opt/mhvtl'; $tapePath = $tapeDir . '/' . basename($tapeName); if (!file_exists($tapePath)) { echo json_encode(['success' => false, 'error' => 'Tape not found']); return; } if (!is_dir($tapePath)) { echo json_encode(['success' => false, 'error' => 'Invalid tape path']); return; } if (strpos(realpath($tapePath), realpath($tapeDir)) !== 0) { echo json_encode(['success' => false, 'error' => 'Security violation: Path traversal detected']); return; } $output = []; $returnCode = 0; exec('sudo rm -rf ' . escapeshellarg($tapePath) . ' 2>&1', $output, $returnCode); if ($returnCode === 0) { echo json_encode([ 'success' => true, 'message' => 'Tape deleted successfully' ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to delete tape: ' . implode("\n", $output) ]); } } function bulkDeleteTapes($pattern) { $tapeDir = '/opt/mhvtl'; if (empty($pattern)) { echo json_encode(['success' => false, 'error' => 'Pattern is required']); return; } if (strpos($pattern, '/') !== false || strpos($pattern, '..') !== false) { echo json_encode(['success' => false, 'error' => 'Invalid pattern']); return; } $items = glob($tapeDir . '/' . $pattern, GLOB_ONLYDIR); if (empty($items)) { echo json_encode([ 'success' => true, 'deleted_count' => 0, 'message' => 'No tapes found matching pattern' ]); return; } $deletedCount = 0; $errors = []; foreach ($items as $item) { if (strpos(realpath($item), realpath($tapeDir)) !== 0) { continue; } $output = []; $returnCode = 0; exec('sudo rm -rf ' . escapeshellarg($item) . ' 2>&1', $output, $returnCode); if ($returnCode === 0) { $deletedCount++; } else { $errors[] = basename($item) . ': ' . implode(' ', $output); } } if ($deletedCount > 0) { $message = "Deleted $deletedCount tape(s)"; if (!empty($errors)) { $message .= '. Errors: ' . implode('; ', $errors); } echo json_encode([ 'success' => true, 'deleted_count' => $deletedCount, 'message' => $message ]); } else { echo json_encode([ 'success' => false, 'error' => 'Failed to delete tapes: ' . implode('; ', $errors) ]); } } // ============================================ // System Health & Management Functions // ============================================ function getSystemHealth() { $health = []; // Check services $services = ['mhvtl', 'apache2', 'tgt']; $health['services'] = []; foreach ($services as $service) { $output = []; $returnCode = 0; exec("systemctl is-active $service 2>&1", $output, $returnCode); $isActive = ($returnCode === 0 && trim($output[0]) === 'active'); // Get enabled status $enabledOutput = []; $enabledCode = 0; exec("systemctl is-enabled $service 2>&1", $enabledOutput, $enabledCode); $isEnabled = ($enabledCode === 0 && trim($enabledOutput[0]) === 'enabled'); $health['services'][$service] = [ 'running' => $isActive, 'enabled' => $isEnabled, 'status' => $isActive ? 'running' : 'stopped' ]; } // Check components $health['components'] = []; // vtltape processes $output = []; exec("pgrep -f 'vtltape' | wc -l", $output); $vtltapeCount = intval($output[0]); $health['components']['vtltape'] = [ 'running' => $vtltapeCount > 0, 'count' => $vtltapeCount ]; // vtllibrary process $output = []; $returnCode = 0; exec("pgrep -f 'vtllibrary' 2>&1", $output, $returnCode); $health['components']['vtllibrary'] = [ 'running' => $returnCode === 0 ]; // Check SCSI devices $health['devices'] = []; // Library $output = []; exec("lsscsi -g 2>/dev/null | grep mediumx", $output); $health['devices']['library'] = [ 'detected' => count($output) > 0, 'info' => count($output) > 0 ? $output[0] : null ]; // Tape drives $output = []; exec("lsscsi -g 2>/dev/null | grep tape", $output); $health['devices']['drives'] = [ 'detected' => count($output) > 0, 'count' => count($output), 'list' => $output ]; // Calculate overall health score $totalChecks = 0; $passedChecks = 0; foreach ($health['services'] as $service) { $totalChecks++; if ($service['running']) $passedChecks++; } if ($health['components']['vtltape']['running']) $passedChecks++; $totalChecks++; if ($health['components']['vtllibrary']['running']) $passedChecks++; $totalChecks++; if ($health['devices']['library']['detected']) $passedChecks++; $totalChecks++; if ($health['devices']['drives']['detected']) $passedChecks++; $totalChecks++; $percentage = $totalChecks > 0 ? round(($passedChecks / $totalChecks) * 100) : 0; if ($percentage == 100) { $status = 'healthy'; $message = 'All systems operational'; } elseif ($percentage >= 66) { $status = 'degraded'; $message = 'Some components need attention'; } else { $status = 'critical'; $message = 'Multiple components offline'; } $health['overall'] = [ 'status' => $status, 'message' => $message, 'score' => $passedChecks, 'total' => $totalChecks, 'percentage' => $percentage ]; // System info $output = []; exec("uptime -p 2>/dev/null || uptime", $output); $health['system'] = [ 'uptime' => isset($output[0]) ? $output[0] : 'Unknown' ]; echo json_encode([ 'success' => true, 'health' => $health ]); } function restartAppliance() { // Create a script to restart after a delay $script = '#!/bin/bash sleep 2 /usr/bin/systemctl reboot '; $scriptPath = '/tmp/restart-appliance.sh'; file_put_contents($scriptPath, $script); chmod($scriptPath, 0755); // Execute in background exec("sudo $scriptPath > /dev/null 2>&1 &"); echo json_encode([ 'success' => true, 'message' => 'System restart initiated. The appliance will reboot in a few seconds.' ]); } function shutdownAppliance() { // Create a script to shutdown after a delay $script = '#!/bin/bash sleep 2 /usr/bin/systemctl poweroff '; $scriptPath = '/tmp/shutdown-appliance.sh'; file_put_contents($scriptPath, $script); chmod($scriptPath, 0755); // Execute in background exec("sudo $scriptPath > /dev/null 2>&1 &"); echo json_encode([ 'success' => true, 'message' => 'System shutdown initiated. The appliance will power off in a few seconds.' ]); } function getDeviceMapping() { $output = []; // Get all SCSI devices with generic device names (sg) exec("sudo /usr/bin/lsscsi -g 2>&1", $output); // Filter for interesting devices (mediumx and tape) $devices = []; foreach ($output as $line) { // Parse the line to make it cleaner if needed, or just return raw lines // Example line: [3:0:0:0] mediumx ADASTRA HEPHAESTUS-V 0107 - /dev/sg6 if (strpos($line, 'mediumx') !== false || strpos($line, 'tape') !== false) { $parts = preg_split('/\s+/', $line); $devices[] = [ 'raw' => $line, 'scsi_id' => $parts[0] ?? '', 'type' => $parts[1] ?? '', 'vendor' => $parts[2] ?? '', 'model' => $parts[3] . (isset($parts[4]) && $parts[4] != '-' && !str_starts_with($parts[4], '/dev') ? ' ' . $parts[4] : '') ?? '', 'rev' => '', // specific parsing depends on varying output 'dev_path' => end($parts) // typically the last one is /dev/sgX ]; } } echo json_encode([ 'success' => true, 'devices' => $devices, 'raw_output' => $output ]); } function getLibraryStatus() { // Find library contents file $files = glob('/etc/mhvtl/library_contents.*'); if (empty($files)) { echo json_encode(['success' => false, 'error' => 'No library config found']); return; } // Use the first one found, typically library_contents.10 $file = $files[0]; $libId = substr(strrchr($file, '.'), 1); $lines = file($file); $status = [ 'library_id' => $libId, 'drives' => [], 'slots' => [], 'maps' => [], 'pickers' => [] ]; foreach ($lines as $line) { $line = trim($line); if (empty($line) || $line[0] == '#') continue; if (preg_match('/^Drive\s+(\d+):\s*(.*)$/i', $line, $matches)) { $status['drives'][] = [ 'id' => intval($matches[1]), 'barcode' => trim($matches[2]), 'full' => !empty(trim($matches[2])) ]; } elseif (preg_match('/^Slot\s+(\d+):\s*(.*)$/i', $line, $matches)) { $status['slots'][] = [ 'id' => intval($matches[1]), 'barcode' => trim($matches[2]), 'full' => !empty(trim($matches[2])) ]; } elseif (preg_match('/^MAP\s+(\d+):\s*(.*)$/i', $line, $matches)) { $status['maps'][] = [ 'id' => intval($matches[1]), 'barcode' => trim($matches[2]), 'full' => !empty(trim($matches[2])) ]; } elseif (preg_match('/^Picker\s+(\d+):\s*(.*)$/i', $line, $matches)) { $status['pickers'][] = [ 'id' => intval($matches[1]), 'barcode' => trim($matches[2]), 'full' => !empty(trim($matches[2])) ]; } } echo json_encode(['success' => true, 'data' => $status]); } ?>