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 = `
`;
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 = '⏳ 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 = `
✅ Success! Configuration saved to ${data.file}
Restart mhvtl service to apply changes using the button below.
`;
showNotification('Configuration applied successfully!', 'success');
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
showNotification('Failed to apply configuration', 'danger');
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = '⏳ 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 = '✅ Success! Service restarted successfully';
showNotification('Service restarted successfully!', 'success');
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
showNotification('Failed to restart service', 'danger');
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = `${type === 'success' ? '✅' : '❌'} ${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 = `❌ Error: ${data.error}`;
}
})
.catch(error => {
loadingDiv.style.display = 'none';
errorDiv.style.display = 'block';
errorDiv.innerHTML = `❌ Error: ${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 = `
${tape.name} |
${tape.size} |
${tape.modified} |
|
`;
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 = '❌ Error: 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 = '⏳ 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 = `✅ Success! Deleted ${data.deleted_count} tape(s)`;
showNotification(`Deleted ${data.deleted_count} tape(s)`, 'success');
loadTapeList();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = '❌ Error: Barcode prefix is required';
return;
}
if (count < 1 || count > 100) {
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = '❌ Error: Number of tapes must be between 1 and 100';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '⏳ 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 = `✅ Success! Created ${data.created_count} tape(s)`;
if (data.errors && data.errors.length > 0) {
resultDiv.innerHTML += `
Errors: ${data.errors.join(', ')}`;
}
showNotification(`Created ${data.created_count} tape(s)`, 'success');
loadTapeList();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 => `
| ${target.tid} |
${target.name} |
${target.luns || 0} |
${target.acls || 0} |
|
`).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 = '❌ Error: Target name is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '⏳ 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 = `✅ Success! Target created: ${data.iqn}`;
showNotification('Target created successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = '❌ Error: Device path is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '⏳ 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 = '✅ Success! LUN added successfully';
showNotification('LUN added successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = '❌ Error: Initiator address is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '⏳ 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 = '✅ Success! Initiator allowed';
showNotification('Initiator allowed successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${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 = '❌ Error: 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 = '⏳ 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 = '✅ Success! Initiator blocked';
showNotification('Initiator blocked successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `❌ Error: ${error.message}`;
});
}