1056 lines
30 KiB
PHP
1056 lines
30 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 '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]);
|
|
}
|
|
?>
|