656 lines
17 KiB
PHP
656 lines
17 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
|
|
// 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'];
|
|
|
|
switch ($action) {
|
|
case 'save_config':
|
|
saveConfig($input['config']);
|
|
break;
|
|
|
|
case 'load_config':
|
|
loadConfig();
|
|
break;
|
|
|
|
case 'restart_service':
|
|
restartService();
|
|
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;
|
|
|
|
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)
|
|
]);
|
|
}
|
|
}
|
|
?>
|