feat: Add complete iSCSI target management to Web UI- Add iSCSI tab with full target management- Implement create/delete targets with auto-generated IQN- Add LUN (backing store) management- Implement initiator ACL management (bind/unbind)- Add real-time target listing with LUN/ACL counts- Add comprehensive iSCSI management guide- Update sudoers to allow tgtadm commands- Add tape management features (create/list/delete/bulk delete)- Add service status monitoring- Security: Input validation, path security, sudo restrictions- Tested: Full CRUD operations working- Package size: 29KB, production ready
This commit is contained in:
655
dist/adastra-vtl-installer/web-ui/api.php
vendored
Normal file
655
dist/adastra-vtl-installer/web-ui/api.php
vendored
Normal file
@@ -0,0 +1,655 @@
|
||||
<?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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user