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:
914
web-ui/script.js
Normal file
914
web-ui/script.js
Normal file
@@ -0,0 +1,914 @@
|
||||
let drives = [];
|
||||
let driveCounter = 0;
|
||||
|
||||
const driveTypes = {
|
||||
'IBM ULT3580-TD5': { vendor: 'IBM', product: 'ULT3580-TD5', type: 'LTO-5' },
|
||||
'IBM ULT3580-TD6': { vendor: 'IBM', product: 'ULT3580-TD6', type: 'LTO-6' },
|
||||
'IBM ULT3580-TD7': { vendor: 'IBM', product: 'ULT3580-TD7', type: 'LTO-7' },
|
||||
'IBM ULT3580-TD8': { vendor: 'IBM', product: 'ULT3580-TD8', type: 'LTO-8' },
|
||||
'IBM ULT3580-TD9': { vendor: 'IBM', product: 'ULT3580-TD9', type: 'LTO-9' },
|
||||
'HP Ultrium 5-SCSI': { vendor: 'HP', product: 'Ultrium 5-SCSI', type: 'LTO-5' },
|
||||
'HP Ultrium 6-SCSI': { vendor: 'HP', product: 'Ultrium 6-SCSI', type: 'LTO-6' },
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initNavigation();
|
||||
addDefaultDrives();
|
||||
generateConfig();
|
||||
});
|
||||
|
||||
function initNavigation() {
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href').substring(1);
|
||||
|
||||
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
||||
|
||||
this.classList.add('active');
|
||||
document.getElementById(targetId).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultDrives() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
addDrive(i < 2 ? 'IBM ULT3580-TD5' : 'IBM ULT3580-TD6');
|
||||
}
|
||||
}
|
||||
|
||||
function addDrive(driveType = 'IBM ULT3580-TD5') {
|
||||
const driveId = driveCounter++;
|
||||
const drive = {
|
||||
id: driveId,
|
||||
driveNum: drives.length,
|
||||
channel: 0,
|
||||
target: drives.length + 1,
|
||||
lun: 0,
|
||||
libraryId: 10,
|
||||
slot: drives.length + 1,
|
||||
type: driveType,
|
||||
serial: `XYZZY_A${drives.length + 1}`,
|
||||
naa: `10:22:33:44:ab:cd:ef:0${drives.length + 1}`,
|
||||
compression: 3,
|
||||
compressionEnabled: 1,
|
||||
compressionType: 'lzo',
|
||||
backoff: 400
|
||||
};
|
||||
|
||||
drives.push(drive);
|
||||
renderDrive(drive);
|
||||
}
|
||||
|
||||
function renderDrive(drive) {
|
||||
const container = document.getElementById('drives-container');
|
||||
const driveInfo = driveTypes[drive.type];
|
||||
|
||||
const driveCard = document.createElement('div');
|
||||
driveCard.className = 'drive-card';
|
||||
driveCard.id = `drive-${drive.id}`;
|
||||
|
||||
driveCard.innerHTML = `
|
||||
<div class="drive-card-header">
|
||||
<h4>💾 Drive ${drive.driveNum}</h4>
|
||||
<button class="btn btn-danger" onclick="removeDrive(${drive.id})">
|
||||
<span>🗑️</span> Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Drive Type</label>
|
||||
<select onchange="updateDriveType(${drive.id}, this.value)">
|
||||
${Object.keys(driveTypes).map(type =>
|
||||
`<option value="${type}" ${type === drive.type ? 'selected' : ''}>${type}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Drive Number</label>
|
||||
<input type="number" value="${drive.driveNum}" min="0" max="99"
|
||||
onchange="updateDrive(${drive.id}, 'driveNum', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Channel</label>
|
||||
<input type="number" value="${drive.channel}" min="0" max="15"
|
||||
onchange="updateDrive(${drive.id}, 'channel', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Target</label>
|
||||
<input type="number" value="${drive.target}" min="0" max="15"
|
||||
onchange="updateDrive(${drive.id}, 'target', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>LUN</label>
|
||||
<input type="number" value="${drive.lun}" min="0" max="7"
|
||||
onchange="updateDrive(${drive.id}, 'lun', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Library ID</label>
|
||||
<input type="number" value="${drive.libraryId}" min="0" max="99"
|
||||
onchange="updateDrive(${drive.id}, 'libraryId', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Slot</label>
|
||||
<input type="number" value="${drive.slot}" min="1" max="999"
|
||||
onchange="updateDrive(${drive.id}, 'slot', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Serial Number</label>
|
||||
<input type="text" value="${drive.serial}" maxlength="10"
|
||||
onchange="updateDrive(${drive.id}, 'serial', this.value)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>NAA</label>
|
||||
<input type="text" value="${drive.naa}" pattern="[0-9a-f:]+"
|
||||
onchange="updateDrive(${drive.id}, 'naa', this.value)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Compression Factor</label>
|
||||
<input type="number" value="${drive.compression}" min="1" max="10"
|
||||
onchange="updateDrive(${drive.id}, 'compression', parseInt(this.value))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Compression Type</label>
|
||||
<select onchange="updateDrive(${drive.id}, 'compressionType', this.value)">
|
||||
<option value="lzo" ${drive.compressionType === 'lzo' ? 'selected' : ''}>LZO</option>
|
||||
<option value="zlib" ${drive.compressionType === 'zlib' ? 'selected' : ''}>ZLIB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Backoff (ms)</label>
|
||||
<input type="number" value="${drive.backoff}" min="0" max="10000"
|
||||
onchange="updateDrive(${drive.id}, 'backoff', parseInt(this.value))">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(driveCard);
|
||||
}
|
||||
|
||||
function updateDrive(driveId, field, value) {
|
||||
const drive = drives.find(d => d.id === driveId);
|
||||
if (drive) {
|
||||
drive[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDriveType(driveId, type) {
|
||||
const drive = drives.find(d => d.id === driveId);
|
||||
if (drive) {
|
||||
drive.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
function removeDrive(driveId) {
|
||||
const index = drives.findIndex(d => d.id === driveId);
|
||||
if (index !== -1) {
|
||||
drives.splice(index, 1);
|
||||
document.getElementById(`drive-${driveId}`).remove();
|
||||
|
||||
drives.forEach((drive, idx) => {
|
||||
drive.driveNum = idx;
|
||||
});
|
||||
|
||||
document.getElementById('drives-container').innerHTML = '';
|
||||
drives.forEach(drive => renderDrive(drive));
|
||||
}
|
||||
}
|
||||
|
||||
function generateConfig() {
|
||||
const libId = document.getElementById('lib-id').value;
|
||||
const libChannel = document.getElementById('lib-channel').value;
|
||||
const libTarget = document.getElementById('lib-target').value;
|
||||
const libLun = document.getElementById('lib-lun').value;
|
||||
const libVendor = document.getElementById('lib-vendor').value;
|
||||
const libProduct = document.getElementById('lib-product').value;
|
||||
const libSerial = document.getElementById('lib-serial').value;
|
||||
const libNaa = document.getElementById('lib-naa').value;
|
||||
const libHome = document.getElementById('lib-home').value;
|
||||
const libBackoff = document.getElementById('lib-backoff').value;
|
||||
|
||||
let config = `VERSION: 5\n\n`;
|
||||
config += `Library: ${libId} CHANNEL: ${libChannel.padStart(2, '0')} TARGET: ${libTarget.padStart(2, '0')} LUN: ${libLun.padStart(2, '0')}\n`;
|
||||
config += ` Vendor identification: ${libVendor}\n`;
|
||||
config += ` Product identification: ${libProduct}\n`;
|
||||
config += ` Unit serial number: ${libSerial}\n`;
|
||||
config += ` NAA: ${libNaa}\n`;
|
||||
config += ` Home directory: ${libHome}\n`;
|
||||
config += ` Backoff: ${libBackoff}\n`;
|
||||
|
||||
drives.forEach(drive => {
|
||||
const driveInfo = driveTypes[drive.type];
|
||||
config += `\nDrive: ${drive.driveNum.toString().padStart(2, '0')} CHANNEL: ${drive.channel.toString().padStart(2, '0')} TARGET: ${drive.target.toString().padStart(2, '0')} LUN: ${drive.lun.toString().padStart(2, '0')}\n`;
|
||||
config += ` Library ID: ${drive.libraryId} Slot: ${drive.slot.toString().padStart(2, '0')}\n`;
|
||||
config += ` Vendor identification: ${driveInfo.vendor}\n`;
|
||||
config += ` Product identification: ${driveInfo.product}\n`;
|
||||
config += ` Unit serial number: ${drive.serial}\n`;
|
||||
config += ` NAA: ${drive.naa}\n`;
|
||||
config += ` Compression: factor ${drive.compression} enabled ${drive.compressionEnabled}\n`;
|
||||
config += ` Compression type: ${drive.compressionType}\n`;
|
||||
config += ` Backoff: ${drive.backoff}\n`;
|
||||
});
|
||||
|
||||
document.getElementById('config-preview').textContent = config;
|
||||
|
||||
const tapeLibrary = document.getElementById('tape-library').value;
|
||||
const tapeBarcodePrefix = document.getElementById('tape-barcode-prefix').value;
|
||||
const tapeStartNum = parseInt(document.getElementById('tape-start-num').value);
|
||||
const tapeSize = document.getElementById('tape-size').value;
|
||||
const tapeMediaType = document.getElementById('tape-media-type').value;
|
||||
const tapeDensity = document.getElementById('tape-density').value;
|
||||
const tapeCount = parseInt(document.getElementById('tape-count').value);
|
||||
|
||||
let installCmds = '#!/bin/bash\n';
|
||||
installCmds += '# Generate virtual tapes for mhvtl\n';
|
||||
installCmds += '# Run this script after mhvtl installation\n\n';
|
||||
|
||||
for (let i = 0; i < tapeCount; i++) {
|
||||
const barcodeNum = String(tapeStartNum + i).padStart(6, '0');
|
||||
const barcode = `${tapeBarcodePrefix}${barcodeNum}`;
|
||||
installCmds += `mktape -l ${tapeLibrary} -m ${barcode} -s ${tapeSize} -t ${tapeMediaType} -d ${tapeDensity}\n`;
|
||||
}
|
||||
|
||||
document.getElementById('install-command').textContent = installCmds;
|
||||
}
|
||||
|
||||
function downloadConfig() {
|
||||
generateConfig();
|
||||
const config = document.getElementById('config-preview').textContent;
|
||||
const blob = new Blob([config], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'device.conf';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showNotification('Configuration downloaded successfully!', 'success');
|
||||
}
|
||||
|
||||
function copyConfig() {
|
||||
generateConfig();
|
||||
const config = document.getElementById('config-preview').textContent;
|
||||
navigator.clipboard.writeText(config).then(() => {
|
||||
showNotification('Configuration copied to clipboard!', 'success');
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy configuration', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function copyInstallCommand() {
|
||||
const cmd = document.getElementById('install-command').textContent;
|
||||
navigator.clipboard.writeText(cmd).then(() => {
|
||||
showNotification('Command copied to clipboard!', 'success');
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy command', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function generateConfigText() {
|
||||
const libId = document.getElementById('lib-id').value;
|
||||
const libChannel = document.getElementById('lib-channel').value;
|
||||
const libTarget = document.getElementById('lib-target').value;
|
||||
const libLun = document.getElementById('lib-lun').value;
|
||||
const libVendor = document.getElementById('lib-vendor').value;
|
||||
const libProduct = document.getElementById('lib-product').value;
|
||||
const libSerial = document.getElementById('lib-serial').value;
|
||||
const libNaa = document.getElementById('lib-naa').value;
|
||||
const libHome = document.getElementById('lib-home').value;
|
||||
const libBackoff = document.getElementById('lib-backoff').value;
|
||||
|
||||
let config = `VERSION: 5\n\n`;
|
||||
config += `Library: ${libId} CHANNEL: ${libChannel.padStart(2, '0')} TARGET: ${libTarget.padStart(2, '0')} LUN: ${libLun.padStart(2, '0')}\n`;
|
||||
config += ` Vendor identification: ${libVendor}\n`;
|
||||
config += ` Product identification: ${libProduct}\n`;
|
||||
config += ` Unit serial number: ${libSerial}\n`;
|
||||
config += ` NAA: ${libNaa}\n`;
|
||||
config += ` Home directory: ${libHome}\n`;
|
||||
config += ` Backoff: ${libBackoff}\n`;
|
||||
|
||||
drives.forEach(drive => {
|
||||
const driveInfo = driveTypes[drive.type];
|
||||
config += `\nDrive: ${drive.driveNum.toString().padStart(2, '0')} CHANNEL: ${drive.channel.toString().padStart(2, '0')} TARGET: ${drive.target.toString().padStart(2, '0')} LUN: ${drive.lun.toString().padStart(2, '0')}\n`;
|
||||
config += ` Library ID: ${drive.libraryId} Slot: ${drive.slot.toString().padStart(2, '0')}\n`;
|
||||
config += ` Vendor identification: ${driveInfo.vendor}\n`;
|
||||
config += ` Product identification: ${driveInfo.product}\n`;
|
||||
config += ` Unit serial number: ${drive.serial}\n`;
|
||||
config += ` NAA: ${drive.naa}\n`;
|
||||
config += ` Compression: factor ${drive.compression} enabled ${drive.compressionEnabled}\n`;
|
||||
config += ` Compression type: ${drive.compressionType}\n`;
|
||||
config += ` Backoff: ${drive.backoff}\n`;
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function applyConfig() {
|
||||
const config = generateConfigText();
|
||||
const resultDiv = document.getElementById('apply-result');
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Applying configuration to server...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'save_config',
|
||||
config: config
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = `
|
||||
<strong>✅ Success!</strong> Configuration saved to ${data.file}<br>
|
||||
<small>Restart mhvtl service to apply changes using the button below.</small>
|
||||
`;
|
||||
showNotification('Configuration applied successfully!', 'success');
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
showNotification('Failed to apply configuration', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
showNotification('Failed to apply configuration', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function restartService() {
|
||||
const resultDiv = document.getElementById('restart-result');
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Restarting mhvtl service...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'restart_service'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = '<strong>✅ Success!</strong> Service restarted successfully';
|
||||
showNotification('Service restarted successfully!', 'success');
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
showNotification('Failed to restart service', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
showNotification('Failed to restart service', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type}`;
|
||||
notification.style.position = 'fixed';
|
||||
notification.style.top = '20px';
|
||||
notification.style.right = '20px';
|
||||
notification.style.zIndex = '9999';
|
||||
notification.style.minWidth = '300px';
|
||||
notification.style.animation = 'slideIn 0.3s ease';
|
||||
notification.innerHTML = `<strong>${type === 'success' ? '✅' : '❌'}</strong> ${message}`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
let tapeListCache = [];
|
||||
|
||||
function loadTapeList() {
|
||||
const loadingDiv = document.getElementById('tape-list-loading');
|
||||
const errorDiv = document.getElementById('tape-list-error');
|
||||
const emptyDiv = document.getElementById('tape-list-empty');
|
||||
const containerDiv = document.getElementById('tape-list-container');
|
||||
|
||||
loadingDiv.style.display = 'block';
|
||||
errorDiv.style.display = 'none';
|
||||
emptyDiv.style.display = 'none';
|
||||
containerDiv.style.display = 'none';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'list_tapes'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
tapeListCache = data.tapes;
|
||||
if (data.tapes.length === 0) {
|
||||
emptyDiv.style.display = 'block';
|
||||
} else {
|
||||
containerDiv.style.display = 'block';
|
||||
renderTapeList(data.tapes);
|
||||
}
|
||||
} else {
|
||||
errorDiv.style.display = 'block';
|
||||
errorDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loadingDiv.style.display = 'none';
|
||||
errorDiv.style.display = 'block';
|
||||
errorDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTapeList(tapes) {
|
||||
const tbody = document.getElementById('tape-list-body');
|
||||
const countDisplay = document.getElementById('tape-count-display');
|
||||
|
||||
tbody.innerHTML = '';
|
||||
countDisplay.textContent = tapes.length;
|
||||
|
||||
tapes.forEach(tape => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td class="tape-barcode">${tape.name}</td>
|
||||
<td class="tape-size">${tape.size}</td>
|
||||
<td class="tape-date">${tape.modified}</td>
|
||||
<td class="tape-actions">
|
||||
<button class="btn btn-danger btn-small" onclick="deleteTape('${tape.name}')">
|
||||
<span>🗑️</span> Delete
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function filterTapes() {
|
||||
const searchTerm = document.getElementById('tape-search').value.toLowerCase();
|
||||
const filteredTapes = tapeListCache.filter(tape =>
|
||||
tape.name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
renderTapeList(filteredTapes);
|
||||
}
|
||||
|
||||
function deleteTape(tapeName) {
|
||||
if (!confirm(`Are you sure you want to delete tape "${tapeName}"?\n\nThis action cannot be undone!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete_tape',
|
||||
tape_name: tapeName
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(`Tape "${tapeName}" deleted successfully!`, 'success');
|
||||
loadTapeList();
|
||||
} else {
|
||||
showNotification(`Failed to delete tape: ${data.error}`, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(`Error: ${error.message}`, 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function bulkDeleteTapes() {
|
||||
const pattern = document.getElementById('bulk-delete-pattern').value.trim();
|
||||
const resultDiv = document.getElementById('bulk-delete-result');
|
||||
|
||||
if (!pattern) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Please enter a delete pattern';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to delete all tapes matching "${pattern}"?\n\nThis action cannot be undone!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Deleting tapes...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'bulk_delete_tapes',
|
||||
pattern: pattern
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = `<strong>✅ Success!</strong> Deleted ${data.deleted_count} tape(s)`;
|
||||
showNotification(`Deleted ${data.deleted_count} tape(s)`, 'success');
|
||||
loadTapeList();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
function createTapes() {
|
||||
const library = document.getElementById('create-library').value;
|
||||
const barcodePrefix = document.getElementById('create-barcode-prefix').value.trim();
|
||||
const startNum = parseInt(document.getElementById('create-start-num').value);
|
||||
const count = parseInt(document.getElementById('create-count').value);
|
||||
const size = document.getElementById('create-size').value;
|
||||
const mediaType = document.getElementById('create-media-type').value;
|
||||
const density = document.getElementById('create-density').value;
|
||||
const resultDiv = document.getElementById('create-result');
|
||||
|
||||
if (!barcodePrefix) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Barcode prefix is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (count < 1 || count > 100) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Number of tapes must be between 1 and 100';
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Creating tapes...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'create_tapes',
|
||||
library: library,
|
||||
barcode_prefix: barcodePrefix,
|
||||
start_num: startNum,
|
||||
count: count,
|
||||
size: size,
|
||||
media_type: mediaType,
|
||||
density: density
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = `<strong>✅ Success!</strong> Created ${data.created_count} tape(s)`;
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
resultDiv.innerHTML += `<br><small>Errors: ${data.errors.join(', ')}</small>`;
|
||||
}
|
||||
showNotification(`Created ${data.created_count} tape(s)`, 'success');
|
||||
loadTapeList();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// iSCSI Target Management Functions
|
||||
// ============================================
|
||||
|
||||
function loadTargets() {
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'list_targets'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const tbody = document.getElementById('target-list-body');
|
||||
const emptyDiv = document.getElementById('target-list-empty');
|
||||
const containerDiv = document.getElementById('target-list-container');
|
||||
|
||||
if (data.targets.length === 0) {
|
||||
emptyDiv.style.display = 'block';
|
||||
containerDiv.style.display = 'none';
|
||||
} else {
|
||||
emptyDiv.style.display = 'none';
|
||||
containerDiv.style.display = 'block';
|
||||
|
||||
tbody.innerHTML = data.targets.map(target => `
|
||||
<tr>
|
||||
<td><strong>${target.tid}</strong></td>
|
||||
<td><code>${target.name}</code></td>
|
||||
<td>${target.luns || 0}</td>
|
||||
<td>${target.acls || 0}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteTarget(${target.tid})">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} else {
|
||||
showNotification(data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Failed to load targets: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function createTarget() {
|
||||
const tid = document.getElementById('target-tid').value;
|
||||
const name = document.getElementById('target-name').value.trim();
|
||||
const resultDiv = document.getElementById('create-target-result');
|
||||
|
||||
if (!name) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Target name is required';
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Creating target...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'create_target',
|
||||
tid: tid,
|
||||
name: name
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = `<strong>✅ Success!</strong> Target created: ${data.iqn}`;
|
||||
showNotification('Target created successfully', 'success');
|
||||
loadTargets();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTarget(tid) {
|
||||
if (!confirm(`Delete target ${tid}? This will remove all LUNs and ACLs.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete_target',
|
||||
tid: tid
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('Target deleted successfully', 'success');
|
||||
loadTargets();
|
||||
} else {
|
||||
showNotification(data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Failed to delete target: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function addLun() {
|
||||
const tid = document.getElementById('lun-tid').value;
|
||||
const lun = document.getElementById('lun-number').value;
|
||||
const device = document.getElementById('lun-device').value.trim();
|
||||
const resultDiv = document.getElementById('add-lun-result');
|
||||
|
||||
if (!device) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Device path is required';
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Adding LUN...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'add_lun',
|
||||
tid: tid,
|
||||
lun: lun,
|
||||
device: device
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = '<strong>✅ Success!</strong> LUN added successfully';
|
||||
showNotification('LUN added successfully', 'success');
|
||||
loadTargets();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
function bindInitiator() {
|
||||
const tid = document.getElementById('acl-tid').value;
|
||||
const address = document.getElementById('acl-address').value.trim();
|
||||
const resultDiv = document.getElementById('acl-result');
|
||||
|
||||
if (!address) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Initiator address is required';
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Binding initiator...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'bind_initiator',
|
||||
tid: tid,
|
||||
address: address
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = '<strong>✅ Success!</strong> Initiator allowed';
|
||||
showNotification('Initiator allowed successfully', 'success');
|
||||
loadTargets();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
function unbindInitiator() {
|
||||
const tid = document.getElementById('acl-tid').value;
|
||||
const address = document.getElementById('acl-address').value.trim();
|
||||
const resultDiv = document.getElementById('acl-result');
|
||||
|
||||
if (!address) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = '<strong>❌ Error:</strong> Initiator address is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Block initiator ${address} from target ${tid}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'alert alert-info';
|
||||
resultDiv.innerHTML = '<strong>⏳</strong> Unbinding initiator...';
|
||||
|
||||
fetch('api.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'unbind_initiator',
|
||||
tid: tid,
|
||||
address: address
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultDiv.className = 'alert alert-success';
|
||||
resultDiv.innerHTML = '<strong>✅ Success!</strong> Initiator blocked';
|
||||
showNotification('Initiator blocked successfully', 'success');
|
||||
loadTargets();
|
||||
} else {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.className = 'alert alert-danger';
|
||||
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user