Files
vtl-appliance/dist/adastra-vtl-installer/web-ui/api.php
Othman H. Suseno 01080498af feat: Major VTL System Upgrade (Auth, Monitoring, CLI, Installer)
- Web UI:
  - Added secure Authentication system (Login, 2 Roles: Admin/Viewer)
  - Added System Monitoring Dashboard (Health, Services, Power Mgmt)
  - Added User Management Interface (Create, Delete, Enable/Disable)
  - Added Device Mapping view in iSCSI tab (lsscsi output)
- Backend:
  - Implemented secure session management (auth.php)
  - Added power management APIs (restart/shutdown appliance)
  - Added device mapping API
- CLI:
  - Created global 'vtl' management tool
  - Added scripts for reliable startup (vtllibrary fix)
- Installer:
  - Updated install.sh with new dependencies (tgt, sudoers, permissions)
  - Included all new components in build-installer.sh
- Docs:
  - Consolidated documentation into docs/ folder
2025-12-09 18:15:36 +00:00

939 lines
25 KiB
PHP

<?php
header('Content-Type: application/json');
// Include authentication system
require_once 'auth.php';
// Configuration
$CONFIG_DIR = '/etc/mhvtl';
$DEVICE_CONF = $CONFIG_DIR . '/device.conf';
$BACKUP_DIR = $CONFIG_DIR . '/backups';
// Ensure backup directory exists
if (!is_dir($BACKUP_DIR)) {
mkdir($BACKUP_DIR, 0755, true);
}
// Get POST data
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['action'])) {
echo json_encode(['success' => 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 '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;
exec('sudo 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;
$inACLSection = false;
foreach ($output as $line) {
if (preg_match('/^Target (\d+): (.+)$/', $line, $matches)) {
if ($currentTarget) {
$targets[] = $currentTarget;
}
$currentTarget = [
'tid' => intval($matches[1]),
'name' => trim($matches[2]),
'luns' => 0,
'acls' => 0
];
$inACLSection = false;
} elseif ($currentTarget && preg_match('/^\s+LUN: (\d+)/', $line)) {
$currentTarget['luns']++;
$inACLSection = false;
} elseif ($currentTarget && preg_match('/^\s+ACL information:/', $line)) {
$inACLSection = true;
} elseif ($currentTarget && $inACLSection && preg_match('/^\s+(.+)$/', $line, $matches)) {
$acl = trim($matches[1]);
if (!empty($acl) && !preg_match('/^(Account|I_T nexus|LUN|System)/', $acl)) {
$currentTarget['acls']++;
}
}
}
if ($currentTarget) {
$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;
}
$command = sprintf(
'sudo tgtadm --lld iscsi --mode logicalunit --op new --tid %d --lun %d --backing-store %s 2>&1',
$tid,
$lun,
escapeshellarg($device)
);
$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)
]);
}
}
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() {
// Check if user has sudo privileges
$output = [];
$returnCode = 0;
exec('sudo systemctl restart mhvtl 2>&1', $output, $returnCode);
if ($returnCode === 0) {
echo json_encode([
'success' => true,
'message' => 'Service restarted successfully'
]);
} else {
echo json_encode([
'success' => false,
'error' => 'Failed to restart service: ' . implode("\n", $output)
]);
}
}
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
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
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("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
]);
}
?>