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 = `

💾 Drive ${drive.driveNum}

`; 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}`; }); }