BAMS initial project structure

This commit is contained in:
2025-12-23 18:34:39 +00:00
parent e1df870f98
commit 861e0f65c3
24 changed files with 2495 additions and 0 deletions

354
cockpit/bams.js Normal file
View File

@@ -0,0 +1,354 @@
(function() {
"use strict";
const API_BASE = "http://localhost:8080/api/v1";
let currentTab = "dashboard";
// Initialize
$(document).ready(function() {
setupTabs();
loadDashboard();
setupEventHandlers();
});
function setupTabs() {
$("a[data-tab]").on("click", function(e) {
e.preventDefault();
const tab = $(this).data("tab");
switchTab(tab);
});
}
function switchTab(tab) {
$(".tab-content").hide();
$(".nav li").removeClass("active");
$(`#${tab}`).show();
$(`a[data-tab="${tab}"]`).parent().addClass("active");
currentTab = tab;
// Load tab-specific data
switch(tab) {
case "dashboard":
loadDashboard();
break;
case "storage":
loadRepositories();
break;
case "tape":
loadTapeLibrary();
break;
case "iscsi":
loadiSCSITargets();
break;
case "bacula":
loadBaculaStatus();
break;
case "logs":
loadLogs();
break;
}
}
function apiCall(endpoint, method, data) {
return new Promise((resolve, reject) => {
const options = {
url: `${API_BASE}${endpoint}`,
method: method || "GET",
contentType: "application/json",
success: resolve,
error: function(xhr, status, error) {
reject(new Error(error || xhr.responseText));
}
};
if (data) {
options.data = JSON.stringify(data);
}
$.ajax(options);
});
}
function loadDashboard() {
apiCall("/dashboard")
.then(data => {
// Update disk stats
const disk = data.disk || {};
$("#disk-stats").html(`
<p><strong>Repositories:</strong> ${disk.repositories || 0}</p>
<p><strong>Total:</strong> ${formatBytes(disk.total_capacity || 0)}</p>
<p><strong>Used:</strong> ${formatBytes(disk.used_capacity || 0)}</p>
`);
// Update tape stats
const tape = data.tape || {};
$("#tape-stats").html(`
<p><strong>Status:</strong> ${tape.library_status || "unknown"}</p>
<p><strong>Active Drives:</strong> ${tape.drives_active || 0}</p>
<p><strong>Total Slots:</strong> ${tape.total_slots || 0}</p>
`);
// Update iSCSI stats
const iscsi = data.iscsi || {};
$("#iscsi-stats").html(`
<p><strong>Targets:</strong> ${iscsi.targets || 0}</p>
<p><strong>Sessions:</strong> ${iscsi.sessions || 0}</p>
`);
// Update Bacula stats
const bacula = data.bacula || {};
$("#bacula-stats").html(`
<p><strong>Status:</strong> ${bacula.status || "unknown"}</p>
`);
// Update alerts
const alerts = data.alerts || [];
if (alerts.length > 0) {
let alertsHtml = "<ul>";
alerts.forEach(alert => {
alertsHtml += `<li class="alert alert-${alert.level || 'warning'}">${alert.message}</li>`;
});
alertsHtml += "</ul>";
$("#alerts").html(alertsHtml);
} else {
$("#alerts").html("<p>No alerts</p>");
}
})
.catch(err => {
console.error("Failed to load dashboard:", err);
$("#disk-stats, #tape-stats, #iscsi-stats, #bacula-stats").html("<p>Error loading data</p>");
});
}
function loadRepositories() {
apiCall("/disk/repositories")
.then(repos => {
let html = "";
if (repos.length === 0) {
html = "<tr><td colspan='6'>No repositories</td></tr>";
} else {
repos.forEach(repo => {
html += `
<tr>
<td>${repo.name}</td>
<td>${repo.type}</td>
<td>${repo.size}</td>
<td>${repo.used || "N/A"}</td>
<td><span class="label label-${repo.status === 'active' ? 'success' : 'default'}">${repo.status}</span></td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteRepository('${repo.id}')">Delete</button>
</td>
</tr>
`;
});
}
$("#repositories-table").html(html);
})
.catch(err => {
console.error("Failed to load repositories:", err);
$("#repositories-table").html("<tr><td colspan='6'>Error loading repositories</td></tr>");
});
}
function loadTapeLibrary() {
Promise.all([
apiCall("/tape/library"),
apiCall("/tape/drives"),
apiCall("/tape/slots")
]).then(([library, drives, slots]) => {
// Library status
$("#library-status").html(`
<p><strong>Status:</strong> ${library.status}</p>
<p><strong>Model:</strong> ${library.model || "N/A"}</p>
<p><strong>Total Slots:</strong> ${library.total_slots}</p>
<p><strong>Active Drives:</strong> ${library.active_drives}</p>
`);
// Drives
let drivesHtml = "";
if (drives.length === 0) {
drivesHtml = "<p>No drives detected</p>";
} else {
drives.forEach(drive => {
drivesHtml += `
<div class="drive-item">
<strong>${drive.id}</strong>: ${drive.status}
${drive.loaded_tape ? ` (Tape: ${drive.barcode || drive.loaded_tape})` : ""}
</div>
`;
});
}
$("#drives-list").html(drivesHtml);
// Slots
let slotsHtml = "<table class='table'><thead><tr><th>Slot</th><th>Barcode</th><th>Status</th></tr></thead><tbody>";
if (slots.length === 0) {
slotsHtml += "<tr><td colspan='3'>No slots</td></tr>";
} else {
slots.forEach(slot => {
slotsHtml += `
<tr>
<td>${slot.number}</td>
<td>${slot.barcode || "N/A"}</td>
<td>${slot.status}</td>
</tr>
`;
});
}
slotsHtml += "</tbody></table>";
$("#slots-list").html(slotsHtml);
}).catch(err => {
console.error("Failed to load tape library:", err);
});
}
function loadiSCSITargets() {
Promise.all([
apiCall("/iscsi/targets"),
apiCall("/iscsi/sessions")
]).then(([targets, sessions]) => {
// Targets
let targetsHtml = "";
if (targets.length === 0) {
targetsHtml = "<tr><td colspan='5'>No targets</td></tr>";
} else {
targets.forEach(target => {
targetsHtml += `
<tr>
<td>${target.iqn}</td>
<td>${target.portals.join(", ") || "N/A"}</td>
<td>${target.initiators.join(", ") || "N/A"}</td>
<td><span class="label label-${target.status === 'active' ? 'success' : 'default'}">${target.status}</span></td>
<td>
<button class="btn btn-sm btn-primary" onclick="applyTarget('${target.id}')">Apply</button>
<button class="btn btn-sm btn-danger" onclick="deleteTarget('${target.id}')">Delete</button>
</td>
</tr>
`;
});
}
$("#targets-table").html(targetsHtml);
// Sessions
let sessionsHtml = "";
if (sessions.length === 0) {
sessionsHtml = "<tr><td colspan='4'>No active sessions</td></tr>";
} else {
sessions.forEach(session => {
sessionsHtml += `
<tr>
<td>${session.target_iqn}</td>
<td>${session.initiator_iqn}</td>
<td>${session.ip}</td>
<td>${session.state}</td>
</tr>
`;
});
}
$("#sessions-table").html(sessionsHtml);
}).catch(err => {
console.error("Failed to load iSCSI targets:", err);
});
}
function loadBaculaStatus() {
apiCall("/bacula/status")
.then(status => {
$("#bacula-status").html(`
<p><strong>Status:</strong> <span class="label label-${status.status === 'running' ? 'success' : 'danger'}">${status.status}</span></p>
${status.version ? `<p><strong>Version:</strong> ${status.version}</p>` : ""}
${status.pid ? `<p><strong>PID:</strong> ${status.pid}</p>` : ""}
`);
})
.catch(err => {
console.error("Failed to load Bacula status:", err);
});
}
function loadLogs() {
const service = $("#log-service").val();
apiCall(`/logs/${service}?lines=100`)
.then(logs => {
let logsHtml = "";
logs.forEach(entry => {
logsHtml += `[${entry.timestamp}] ${entry.level}: ${entry.message}\n`;
});
$("#logs-content").text(logsHtml);
})
.catch(err => {
console.error("Failed to load logs:", err);
$("#logs-content").text("Error loading logs: " + err.message);
});
}
function setupEventHandlers() {
$("#inventory-btn").on("click", function() {
apiCall("/tape/inventory", "POST")
.then(() => {
alert("Inventory started");
loadTapeLibrary();
})
.catch(err => alert("Failed to start inventory: " + err.message));
});
$("#bacula-inventory-btn").on("click", function() {
apiCall("/bacula/inventory", "POST")
.then(() => alert("Inventory started"))
.catch(err => alert("Failed to start inventory: " + err.message));
});
$("#bacula-restart-btn").on("click", function() {
if (confirm("Restart Bacula Storage Daemon?")) {
apiCall("/bacula/restart", "POST")
.then(() => {
alert("Restart initiated");
setTimeout(loadBaculaStatus, 2000);
})
.catch(err => alert("Failed to restart: " + err.message));
}
});
$("#refresh-logs-btn").on("click", loadLogs);
$("#log-service").on("change", loadLogs);
$("#download-bundle-btn").on("click", function() {
window.location.href = `${API_BASE}/diagnostics/bundle`;
});
}
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
}
// Global functions for inline handlers
window.deleteRepository = function(id) {
if (confirm("Delete this repository?")) {
apiCall(`/disk/repositories/${id}`, "DELETE")
.then(() => {
alert("Repository deleted");
loadRepositories();
})
.catch(err => alert("Failed to delete: " + err.message));
}
};
window.applyTarget = function(id) {
apiCall(`/iscsi/targets/${id}/apply`, "POST")
.then(() => alert("Target configuration applied"))
.catch(err => alert("Failed to apply: " + err.message));
};
window.deleteTarget = function(id) {
if (confirm("Delete this iSCSI target?")) {
apiCall(`/iscsi/targets/${id}`, "DELETE")
.then(() => {
alert("Target deleted");
loadiSCSITargets();
})
.catch(err => alert("Failed to delete: " + err.message));
}
};
})();

250
cockpit/index.html Normal file
View File

@@ -0,0 +1,250 @@
<!DOCTYPE html>
<html>
<head>
<title>BAMS - Backup Appliance Management System</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../base1/cockpit.css" type="text/css" rel="stylesheet">
<script src="../base1/jquery.js"></script>
<script src="../base1/cockpit.js"></script>
</head>
<body>
<div class="container-fluid">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">BAMS</a>
</div>
<ul class="nav navbar-nav">
<li class="active"><a href="#dashboard" data-tab="dashboard">Dashboard</a></li>
<li><a href="#storage" data-tab="storage">Storage</a></li>
<li><a href="#tape" data-tab="tape">Tape Library</a></li>
<li><a href="#iscsi" data-tab="iscsi">iSCSI Targets</a></li>
<li><a href="#bacula" data-tab="bacula">Bacula</a></li>
<li><a href="#logs" data-tab="logs">Logs</a></li>
</ul>
</div>
</nav>
<div id="dashboard" class="tab-content active">
<div class="page-header">
<h1>Dashboard</h1>
</div>
<div class="row">
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">Disk Storage</div>
<div class="panel-body">
<div id="disk-stats">
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">Tape Library</div>
<div class="panel-body">
<div id="tape-stats">
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">iSCSI</div>
<div class="panel-body">
<div id="iscsi-stats">
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">Bacula SD</div>
<div class="panel-body">
<div id="bacula-stats">
<p>Loading...</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">Alerts</div>
<div class="panel-body">
<div id="alerts">
<p>No alerts</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="storage" class="tab-content" style="display: none;">
<div class="page-header">
<h1>Storage Repositories</h1>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<button class="btn btn-primary" id="create-repo-btn">Create Repository</button>
</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Used</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="repositories-table">
<tr><td colspan="6">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="tape" class="tab-content" style="display: none;">
<div class="page-header">
<h1>Tape Library Management</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
Library Status
<button class="btn btn-sm btn-default pull-right" id="inventory-btn">Run Inventory</button>
</div>
<div class="panel-body" id="library-status">
<p>Loading...</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Tape Drives</div>
<div class="panel-body" id="drives-list">
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">Slots</div>
<div class="panel-body" id="slots-list">
<p>Loading...</p>
</div>
</div>
</div>
</div>
</div>
<div id="iscsi" class="tab-content" style="display: none;">
<div class="page-header">
<h1>iSCSI Target Management</h1>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<button class="btn btn-primary" id="create-target-btn">Create Target</button>
</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th>IQN</th>
<th>Portals</th>
<th>Initiators</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="targets-table">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Active Sessions</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th>Target IQN</th>
<th>Initiator IQN</th>
<th>IP Address</th>
<th>State</th>
</tr>
</thead>
<tbody id="sessions-table">
<tr><td colspan="4">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="bacula" class="tab-content" style="display: none;">
<div class="page-header">
<h1>Bacula Integration</h1>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Storage Daemon Status</div>
<div class="panel-body" id="bacula-status">
<p>Loading...</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Actions</div>
<div class="panel-body">
<button class="btn btn-default" id="bacula-inventory-btn">Run Inventory</button>
<button class="btn btn-default" id="bacula-restart-btn">Restart SD</button>
<button class="btn btn-default" id="bacula-config-btn">Generate Config</button>
</div>
</div>
</div>
</div>
</div>
<div id="logs" class="tab-content" style="display: none;">
<div class="page-header">
<h1>Logs & Diagnostics</h1>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<select id="log-service" class="form-control" style="display: inline-block; width: 200px;">
<option value="bams">BAMS</option>
<option value="scst">SCST</option>
<option value="iscsi">iSCSI</option>
<option value="bacula">Bacula</option>
</select>
<button class="btn btn-default" id="refresh-logs-btn">Refresh</button>
<button class="btn btn-default" id="download-bundle-btn">Download Support Bundle</button>
</div>
<div class="panel-body">
<pre id="logs-content" style="max-height: 600px; overflow-y: auto;"></pre>
</div>
</div>
</div>
</div>
<script src="bams.js"></script>
</body>
</html>

19
cockpit/manifest.json Normal file
View File

@@ -0,0 +1,19 @@
{
"version": "1.0.0",
"name": "BAMS",
"displayName": "Backup Appliance Management System",
"description": "Manage disk repositories, tape libraries, iSCSI targets, and Bacula integration",
"author": "BAMS Team",
"license": "GPL-3.0",
"url": "https://github.com/bams/bams",
"tools": {
"bams": {
"label": "BAMS",
"path": "index.html"
}
},
"requires": {
"cockpit": ">= 300"
}
}