Handle inactive VM and Docker services
This commit is contained in:
@@ -1 +0,0 @@
|
||||
9b133d15689d1d9a7b3532e2fc3ec1a3fd96124ec8b9466451b6a097d911820c backupper-2026.06.14.5-x86_64-1.txz
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
72cbb82fd82f26c3099130bb618ac29077e315693b12a9e77585d05ed355c851 backupper-2026.06.14.6-x86_64-1.txz
|
||||
Vendored
+5
-2
@@ -2,13 +2,16 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "backupper">
|
||||
<!ENTITY author "Backupper Project">
|
||||
<!ENTITY version "2026.06.14.5">
|
||||
<!ENTITY version "2026.06.14.6">
|
||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
||||
<!ENTITY packageSHA256 "9b133d15689d1d9a7b3532e2fc3ec1a3fd96124ec8b9466451b6a097d911820c">
|
||||
<!ENTITY packageSHA256 "72cbb82fd82f26c3099130bb618ac29077e315693b12a9e77585d05ed355c851">
|
||||
]>
|
||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues">
|
||||
<CHANGES>
|
||||
### 2026.06.14.6
|
||||
- Show clear inactive-state messages when Unraid VM Manager or Docker is disabled.
|
||||
|
||||
### 2026.06.14.5
|
||||
- Make backup job forms type-aware with Docker container and VM discovery lists.
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ type Workload struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type WorkloadDiscovery struct {
|
||||
Available bool `json:"available"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Items []Workload `json:"items"`
|
||||
}
|
||||
|
||||
type Prepared struct {
|
||||
Sources []string
|
||||
Stopped []model.Source
|
||||
@@ -29,12 +35,15 @@ type Prepared struct {
|
||||
Kind model.JobType
|
||||
}
|
||||
|
||||
func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) ([]Workload, error) {
|
||||
func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) (WorkloadDiscovery, error) {
|
||||
switch kind {
|
||||
case model.JobDocker:
|
||||
if _, err := os.Stat("/var/run/docker.sock"); err != nil {
|
||||
return WorkloadDiscovery{Available: false, Message: "Docker is not enabled or is not running on this Unraid server.", Items: []Workload{}}, nil
|
||||
}
|
||||
output, err := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{json .}}").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment: list Docker containers: %w", err)
|
||||
return WorkloadDiscovery{}, fmt.Errorf("environment: list Docker containers: %w", err)
|
||||
}
|
||||
var result []Workload
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
||||
@@ -47,15 +56,22 @@ func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) ([]W
|
||||
Status string `json:"Status"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &item); err != nil {
|
||||
return nil, fmt.Errorf("environment: decode Docker container list: %w", err)
|
||||
return WorkloadDiscovery{}, fmt.Errorf("environment: decode Docker container list: %w", err)
|
||||
}
|
||||
result = append(result, Workload{ID: item.Names, Name: item.Names, Status: item.Status})
|
||||
}
|
||||
return result, nil
|
||||
message := ""
|
||||
if len(result) == 0 {
|
||||
message = "Docker is active, but no containers are configured."
|
||||
}
|
||||
return WorkloadDiscovery{Available: true, Message: message, Items: result}, nil
|
||||
case model.JobVM:
|
||||
if !unraidServiceEnabled("/boot/config/domain.cfg") {
|
||||
return WorkloadDiscovery{Available: false, Message: "The Unraid VM Manager is not enabled. Enable VMs under Settings > VM Manager to select virtual machines.", Items: []Workload{}}, nil
|
||||
}
|
||||
output, err := exec.CommandContext(ctx, "virsh", "list", "--all", "--name").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment: list virtual machines: %w", err)
|
||||
return WorkloadDiscovery{}, fmt.Errorf("environment: VM Manager is enabled but virtual machines could not be listed: %w", err)
|
||||
}
|
||||
var result []Workload
|
||||
for _, name := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
||||
@@ -66,12 +82,30 @@ func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) ([]W
|
||||
state, _ := exec.CommandContext(ctx, "virsh", "domstate", name).Output()
|
||||
result = append(result, Workload{ID: name, Name: name, Status: strings.TrimSpace(string(state))})
|
||||
}
|
||||
return result, nil
|
||||
message := ""
|
||||
if len(result) == 0 {
|
||||
message = "The VM Manager is active, but no virtual machines are configured."
|
||||
}
|
||||
return WorkloadDiscovery{Available: true, Message: message, Items: result}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("validation: unsupported workload type %q", kind)
|
||||
return WorkloadDiscovery{}, fmt.Errorf("validation: unsupported workload type %q", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func unraidServiceEnabled(path string) bool {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
parts := strings.SplitN(strings.TrimSpace(line), "=", 2)
|
||||
if len(parts) == 2 && parts[0] == "SERVICE" {
|
||||
return strings.Trim(strings.TrimSpace(parts[1]), "\"'") == "enable"
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *WorkloadManager) Prepare(ctx context.Context, job model.Job) (Prepared, error) {
|
||||
prepared := Prepared{Kind: job.Type}
|
||||
for _, source := range job.Sources {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnraidServiceEnabled(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "domain.cfg")
|
||||
for _, test := range []struct {
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{content: "SERVICE=\"enable\"\n", want: true},
|
||||
{content: "SERVICE=disable\n", want: false},
|
||||
{content: "OTHER=value\n", want: false},
|
||||
} {
|
||||
if err := os.WriteFile(path, []byte(test.content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := unraidServiceEnabled(path); got != test.want {
|
||||
t.Fatalf("content %q: got %v, want %v", test.content, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func (s *Service) BrowseDirectories(path string) ([]platform.DirectoryEntry, err
|
||||
return platform.BrowseDirectories(path)
|
||||
}
|
||||
|
||||
func (s *Service) DiscoverWorkloads(ctx context.Context, kind model.JobType) ([]platform.Workload, error) {
|
||||
func (s *Service) DiscoverWorkloads(ctx context.Context, kind model.JobType) (platform.WorkloadDiscovery, error) {
|
||||
return s.workloads.Discover(ctx, kind)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "backupper">
|
||||
<!ENTITY author "Backupper Project">
|
||||
<!ENTITY version "2026.06.14.5">
|
||||
<!ENTITY version "2026.06.14.6">
|
||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
||||
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
|
||||
]>
|
||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues">
|
||||
<CHANGES>
|
||||
### 2026.06.14.6
|
||||
- Show clear inactive-state messages when Unraid VM Manager or Docker is disabled.
|
||||
|
||||
### 2026.06.14.5
|
||||
- Make backup job forms type-aware with Docker container and VM discovery lists.
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ Tag="backup restic snapshots restore"
|
||||
<?php
|
||||
$pluginRoot = '/plugins/backupper';
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=202606145">
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=202606146">
|
||||
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||
<header class="bu-header">
|
||||
<div><h1>Backupper <span class="bu-version">2026.06.14.5</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
||||
<div><h1>Backupper <span class="bu-version">2026.06.14.6</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die Backupper-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div>
|
||||
</header>
|
||||
<nav class="bu-tabs" aria-label="Backupper sections">
|
||||
@@ -29,4 +29,4 @@ $pluginRoot = '/plugins/backupper';
|
||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||
</div>
|
||||
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.5.js"></script>
|
||||
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.6.js"></script>
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
.bu-browser-card input[type="checkbox"], .bu-table input[type="checkbox"] { accent-color: var(--bu-accent); height: 20px; width: 20px; }
|
||||
.bu-count { align-items: center; border: 1px solid var(--bu-border); border-radius: 999px; display: inline-flex; font-size: 13px; font-weight: 700; padding: 7px 11px; }
|
||||
.bu-inline-error { background: rgba(180,35,47,.08); border: 1px solid var(--bu-bad); border-radius: 5px; color: var(--bu-bad); font-weight: 600; margin: 10px 0; padding: 10px 12px; }
|
||||
.bu-inline-info { background: rgba(19,138,98,.08); border: 1px solid var(--bu-good); border-radius: 5px; color: var(--bu-text); font-weight: 600; margin: 10px 0; padding: 10px 12px; }
|
||||
.bu-workload-options { display: grid; gap: 15px; grid-template-columns: repeat(2, minmax(220px, 1fr)); margin-top: 15px; }
|
||||
.bu-manual-source { display: grid; gap: 7px; margin-top: 14px; }
|
||||
.bu-info-card { background: var(--bu-panel-alt); border: 1px solid var(--bu-border); border-radius: 6px; padding: 16px; }
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
const toast = document.getElementById('bu-toast');
|
||||
const tooltip = document.getElementById('bu-tooltip');
|
||||
const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path}));
|
||||
const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '' };
|
||||
const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '' };
|
||||
|
||||
const fieldHelp = {
|
||||
name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.',
|
||||
@@ -244,7 +244,7 @@
|
||||
function jobForm(job = {}) {
|
||||
state.backupSelections = (job.sources || []).filter(s=>s.path).map(s=>s.path);
|
||||
state.workloadSelections = (job.sources || []).filter(s=>s.workloadId).map(s=>s.workloadId);
|
||||
state.workloads = []; state.workloadError = '';
|
||||
state.workloads = []; state.workloadAvailable = true; state.workloadError = '';
|
||||
state.sourceBrowserPath = '/'; state.sourceDirectories = sourceRoots; state.sourceBrowserError = '';
|
||||
const options = state.config.repositories.map(r => `<option value="${esc(r.id)}" ${r.id===job.repositoryId?'selected':''}>${esc(r.name)}</option>`).join('');
|
||||
content.innerHTML = `<section class="bu-card"><h2>${job.id ? 'Edit' : 'New'} backup job</h2><form id="bu-job-form" class="bu-form">
|
||||
@@ -283,15 +283,15 @@
|
||||
if (!host) return;
|
||||
const selected = new Set(state.workloadSelections);
|
||||
const rows = state.workloads.map(item=>`<tr><td><input type="checkbox" class="bu-workload-select" value="${esc(item.id)}" ${selected.has(item.id)?'checked':''}></td><td><strong>${esc(item.name)}</strong><br><span class="bu-muted">${esc(item.id)}</span></td><td>${esc(item.status||'unknown')}</td></tr>`).join('');
|
||||
const error = state.workloadError ? `<div class="bu-inline-error">${esc(state.workloadError)}</div>` : '';
|
||||
host.innerHTML = `<div class="bu-toolbar"><h3>${type==='docker'?'Available containers':'Available virtual machines'}</h3><span class="bu-count">Selected: ${state.workloadSelections.length}</span></div>${error}<div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Select</th><th>Name</th><th>Status</th></tr></thead><tbody>${rows||'<tr><td colspan="3" class="bu-empty">No workloads found.</td></tr>'}</tbody></table></div>`;
|
||||
const notice = state.workloadError ? `<div class="${state.workloadAvailable===false?'bu-inline-info':'bu-inline-error'}">${esc(state.workloadError)}</div>` : '';
|
||||
host.innerHTML = `<div class="bu-toolbar"><h3>${type==='docker'?'Available containers':'Available virtual machines'}</h3><span class="bu-count">Selected: ${state.workloadSelections.length}</span></div>${notice}<div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Select</th><th>Name</th><th>Status</th></tr></thead><tbody>${rows||(!state.workloadError?'<tr><td colspan="3" class="bu-empty">No workloads found.</td></tr>':'')}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
async function loadWorkloads(type) {
|
||||
try {
|
||||
state.workloads = await api(`/v1/workloads/${type}`); state.workloadError = '';
|
||||
const result = await api(`/v1/workloads/${type}`); state.workloads = result.items || []; state.workloadAvailable = result.available; state.workloadError = result.message || '';
|
||||
} catch (error) {
|
||||
state.workloads = []; state.workloadError = `Workloads could not be loaded: ${error.message}`;
|
||||
state.workloads = []; state.workloadAvailable = true; state.workloadError = `Workloads could not be loaded: ${error.message}`;
|
||||
}
|
||||
renderWorkloadBrowser(type);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user