381 lines
14 KiB
HTML
381 lines
14 KiB
HTML
{{define "iscsi-content"}}
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">iSCSI Targets</h1>
|
|
<p class="text-slate-400">Manage iSCSI targets and LUNs</p>
|
|
</div>
|
|
<button onclick="showCreateISCSIModal()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
|
Create Target
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">iSCSI Targets</h2>
|
|
<button onclick="loadISCSITargets()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
<div id="iscsi-targets-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create iSCSI Target Modal -->
|
|
<div id="create-iscsi-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Create iSCSI Target</h3>
|
|
<form id="create-iscsi-form" onsubmit="createISCSITarget(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">IQN</label>
|
|
<input type="text" name="iqn" placeholder="iqn.2024-12.com.atlas:target1" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<p class="text-xs text-slate-400 mt-1">iSCSI Qualified Name</p>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-iscsi-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add LUN Modal -->
|
|
<div id="add-lun-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Add LUN to Target</h3>
|
|
<form id="add-lun-form" onsubmit="addLUN(event)" class="space-y-4">
|
|
<input type="hidden" name="target_id" id="lun-target-id">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Select Storage Volume</label>
|
|
<select name="zvol" id="lun-zvol-select" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="">Loading volumes...</option>
|
|
</select>
|
|
<p class="text-xs text-slate-400 mt-1">Or enter manually below</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Or Enter Volume Name Manually</label>
|
|
<input type="text" name="zvol-manual" id="lun-zvol-manual" placeholder="pool/zvol" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('add-lun-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Add LUN
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
};
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
async function loadISCSITargets() {
|
|
try {
|
|
const res = await fetch('/api/v1/iscsi/targets', { headers: getAuthHeaders() });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
document.getElementById('iscsi-targets-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load iSCSI targets'}</p>`;
|
|
return;
|
|
}
|
|
const targets = await res.json();
|
|
const listEl = document.getElementById('iscsi-targets-list');
|
|
|
|
if (!Array.isArray(targets)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
if (targets.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No iSCSI targets found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = targets.map(target => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white">${target.iqn}</h3>
|
|
${target.enabled ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Enabled</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Disabled</span>'}
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1 mb-3">
|
|
<p>LUNs: ${target.luns ? target.luns.length : 0}</p>
|
|
${target.initiators && target.initiators.length > 0 ? `<p>Initiators: ${target.initiators.join(', ')}</p>` : ''}
|
|
</div>
|
|
${target.luns && target.luns.length > 0 ? `
|
|
<div class="bg-slate-900 rounded p-3 mt-2">
|
|
<h4 class="text-sm font-medium text-slate-300 mb-2">LUNs (${target.luns.length}):</h4>
|
|
<div class="space-y-2">
|
|
${target.luns.map(lun => `
|
|
<div class="flex items-center justify-between text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-slate-400">LUN ${lun.id}:</span>
|
|
<span class="text-slate-300 font-mono">${lun.zvol}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-slate-300">${formatBytes(lun.size)}</span>
|
|
<button onclick="removeLUN('${target.id}', ${lun.id})" class="ml-2 px-2 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs" title="Remove LUN">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
` : '<p class="text-sm text-slate-500 mt-2">No LUNs attached. Click "Add LUN" to bind a volume.</p>'}
|
|
</div>
|
|
<div class="flex gap-2 ml-4">
|
|
<button onclick="showAddLUNModal('${target.id}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Add LUN
|
|
</button>
|
|
<button onclick="showConnectionInstructions('${target.id}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Connection Info
|
|
</button>
|
|
<button onclick="deleteISCSITarget('${target.id}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('iscsi-targets-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function showCreateISCSIModal() {
|
|
document.getElementById('create-iscsi-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function showAddLUNModal(targetId) {
|
|
document.getElementById('lun-target-id').value = targetId;
|
|
document.getElementById('add-lun-modal').classList.remove('hidden');
|
|
|
|
// Load available storage volumes
|
|
await loadZVOLsForLUN();
|
|
}
|
|
|
|
async function loadZVOLsForLUN() {
|
|
try {
|
|
const res = await fetch('/api/v1/zvols', { headers: getAuthHeaders() });
|
|
const selectEl = document.getElementById('lun-zvol-select');
|
|
|
|
if (!res.ok) {
|
|
selectEl.innerHTML = '<option value="">Error loading volumes</option>';
|
|
return;
|
|
}
|
|
|
|
const zvols = await res.json();
|
|
|
|
if (!Array.isArray(zvols)) {
|
|
selectEl.innerHTML = '<option value="">No volumes available</option>';
|
|
return;
|
|
}
|
|
|
|
if (zvols.length === 0) {
|
|
selectEl.innerHTML = '<option value="">No volumes found. Create a storage volume first.</option>';
|
|
return;
|
|
}
|
|
|
|
// Clear and populate dropdown
|
|
selectEl.innerHTML = '<option value="">Select a volume...</option>';
|
|
zvols.forEach(zvol => {
|
|
const option = document.createElement('option');
|
|
option.value = zvol.name;
|
|
option.textContent = `${zvol.name} (${formatBytes(zvol.size)})`;
|
|
selectEl.appendChild(option);
|
|
});
|
|
|
|
// Update manual input when dropdown changes
|
|
selectEl.addEventListener('change', function() {
|
|
if (this.value) {
|
|
document.getElementById('lun-zvol-manual').value = this.value;
|
|
}
|
|
});
|
|
|
|
// Update dropdown when manual input changes
|
|
document.getElementById('lun-zvol-manual').addEventListener('input', function() {
|
|
if (this.value && !selectEl.value) {
|
|
// Allow manual entry even if not in dropdown
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error('Error loading volumes:', err);
|
|
document.getElementById('lun-zvol-select').innerHTML = '<option value="">Error loading volumes</option>';
|
|
}
|
|
}
|
|
|
|
async function showConnectionInstructions(targetId) {
|
|
try {
|
|
const res = await fetch(`/api/v1/iscsi/targets/${targetId}/connection`, { headers: getAuthHeaders() });
|
|
const data = await res.json();
|
|
|
|
const instructions = `
|
|
Connection Instructions for ${data.target?.iqn || 'target'}:
|
|
|
|
Portal: ${data.portal || 'N/A'}
|
|
|
|
Linux:
|
|
iscsiadm -m discovery -t st -p ${data.portal || '127.0.0.1'}
|
|
iscsiadm -m node -T ${data.target?.iqn || ''} -p ${data.portal || '127.0.0.1'} --login
|
|
|
|
Windows:
|
|
Use iSCSI Initiator from Control Panel
|
|
Add target: ${data.portal || '127.0.0.1'}:${data.port || 3260}
|
|
Target: ${data.target?.iqn || ''}
|
|
|
|
macOS:
|
|
Use System Preferences > Network > iSCSI
|
|
`;
|
|
|
|
alert(instructions);
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
}
|
|
|
|
async function createISCSITarget(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/iscsi/targets', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
iqn: formData.get('iqn')
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-iscsi-modal');
|
|
e.target.reset();
|
|
loadISCSITargets();
|
|
alert('iSCSI target created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create iSCSI target'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function addLUN(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const targetId = formData.get('target_id');
|
|
|
|
// Get volume from either dropdown or manual input
|
|
const zvolSelect = document.getElementById('lun-zvol-select').value;
|
|
const zvolManual = document.getElementById('lun-zvol-manual').value.trim();
|
|
const zvol = zvolSelect || zvolManual;
|
|
|
|
if (!zvol) {
|
|
alert('Please select or enter a volume name');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/iscsi/targets/${targetId}/luns`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
zvol: zvol
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('add-lun-modal');
|
|
e.target.reset();
|
|
document.getElementById('lun-zvol-select').innerHTML = '<option value="">Loading volumes...</option>';
|
|
document.getElementById('lun-zvol-manual').value = '';
|
|
loadISCSITargets();
|
|
alert('LUN added successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to add LUN'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function removeLUN(targetId, lunId) {
|
|
if (!confirm(`Are you sure you want to remove LUN ${lunId} from this target?`)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/iscsi/targets/${targetId}/luns/remove`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
lun_id: lunId
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadISCSITargets();
|
|
alert('LUN removed successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to remove LUN'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function deleteISCSITarget(id) {
|
|
if (!confirm('Are you sure you want to delete this iSCSI target? All LUNs will be removed.')) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/iscsi/targets/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadISCSITargets();
|
|
alert('iSCSI target deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete iSCSI target'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Load initial data
|
|
loadISCSITargets();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{define "iscsi.html"}}
|
|
{{template "base" .}}
|
|
{{end}}
|