Files
vtl-appliance/web-ui/api.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)
]);
}
}
?>