Update modal lxc

This commit is contained in:
MacRimi
2026-02-03 22:10:53 +01:00
parent 458f2cdf16
commit f13427ca27
2 changed files with 227 additions and 11 deletions

View File

@@ -7,9 +7,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Progress } from "./ui/progress" import { Progress } from "./ui/progress"
import { Button } from "./ui/button" import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "./ui/dialog"
import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, Terminal, Archive, Plus, Loader2, Clock, Database } from 'lucide-react' import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, Terminal, Archive, Plus, Loader2, Clock, Database, Shield, Bell, FileText, Settings2 } from 'lucide-react'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Checkbox } from "./ui/checkbox"
import { Textarea } from "./ui/textarea"
import { Label } from "./ui/label"
import useSWR from "swr" import useSWR from "swr"
import { MetricsView } from "./metrics-dialog" import { MetricsView } from "./metrics-dialog"
import { LxcTerminalModal } from "./lxc-terminal-modal" import { LxcTerminalModal } from "./lxc-terminal-modal"
@@ -302,6 +305,14 @@ export function VirtualMachines() {
const [selectedBackupStorage, setSelectedBackupStorage] = useState<string>("") const [selectedBackupStorage, setSelectedBackupStorage] = useState<string>("")
const [loadingBackups, setLoadingBackups] = useState(false) const [loadingBackups, setLoadingBackups] = useState(false)
const [creatingBackup, setCreatingBackup] = useState(false) const [creatingBackup, setCreatingBackup] = useState(false)
// Backup modal states
const [showBackupModal, setShowBackupModal] = useState(false)
const [backupMode, setBackupMode] = useState<string>("snapshot")
const [backupProtected, setBackupProtected] = useState(false)
const [backupNotification, setBackupNotification] = useState<string>("auto")
const [backupNotes, setBackupNotes] = useState<string>("{{guestname}}")
const [backupPbsChangeMode, setBackupPbsChangeMode] = useState<string>("default")
useEffect(() => { useEffect(() => {
const fetchLXCIPs = async () => { const fetchLXCIPs = async () => {
@@ -432,14 +443,34 @@ export function VirtualMachines() {
} }
} }
const openBackupModal = () => {
// Reset modal to defaults
setBackupMode("snapshot")
setBackupProtected(false)
setBackupNotification("auto")
setBackupNotes("{{guestname}}")
setBackupPbsChangeMode("default")
setShowBackupModal(true)
}
const handleCreateBackup = async () => { const handleCreateBackup = async () => {
if (!selectedVM || !selectedBackupStorage) return if (!selectedVM || !selectedBackupStorage) return
setCreatingBackup(true) setCreatingBackup(true)
setShowBackupModal(false)
try { try {
await fetchApi(`/api/vms/${selectedVM.vmid}/backup`, { await fetchApi(`/api/vms/${selectedVM.vmid}/backup`, {
method: "POST", method: "POST",
body: JSON.stringify({ storage: selectedBackupStorage }), body: JSON.stringify({
storage: selectedBackupStorage,
mode: backupMode,
compress: "zstd",
protected: backupProtected,
notification: backupNotification,
notes: backupNotes,
pbs_change_detection: backupPbsChangeMode
}),
}) })
setTimeout(() => fetchVmBackups(selectedVM.vmid), 2000) setTimeout(() => fetchVmBackups(selectedVM.vmid), 2000)
} catch (error) { } catch (error) {
@@ -1335,7 +1366,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
<Button <Button
size="sm" size="sm"
className="h-9 bg-amber-600 hover:bg-amber-700 text-white gap-1.5" className="h-9 bg-amber-600 hover:bg-amber-700 text-white gap-1.5"
onClick={handleCreateBackup} onClick={openBackupModal}
disabled={creatingBackup || !selectedBackupStorage} disabled={creatingBackup || !selectedBackupStorage}
> >
{creatingBackup ? ( {creatingBackup ? (
@@ -1388,12 +1419,15 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
key={`backup-${backup.volid}-${index}`} key={`backup-${backup.volid}-${index}`}
className="flex items-center justify-between p-2.5 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors" className="flex items-center justify-between p-2.5 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<div className="w-1.5 h-1.5 rounded-full bg-green-500" /> <div className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
<Clock className="h-3.5 w-3.5 text-muted-foreground" /> <Clock className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<span className="text-sm text-foreground">{backup.date}</span> <span className="text-sm text-foreground">{backup.date}</span>
<Badge variant="outline" className="text-xs bg-muted/50 ml-auto flex-shrink-0">
{backup.storage}
</Badge>
</div> </div>
<Badge variant="outline" className="text-xs font-mono"> <Badge variant="outline" className="text-xs font-mono ml-2 flex-shrink-0">
{backup.size_human} {backup.size_human}
</Badge> </Badge>
</div> </div>
@@ -2005,6 +2039,155 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Backup Configuration Modal */}
<Dialog open={showBackupModal} onOpenChange={setShowBackupModal}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-amber-500">
<Archive className="h-5 w-5" />
Backup {selectedVM?.type?.toUpperCase()} {selectedVM?.vmid} ({selectedVM?.name})
</DialogTitle>
<DialogDescription>
Configure backup options for this {selectedVM?.type === 'lxc' ? 'container' : 'virtual machine'}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{/* Storage & Mode Row */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm flex items-center gap-1.5">
<Database className="h-3.5 w-3.5" />
Storage
</Label>
<Select value={selectedBackupStorage} onValueChange={setSelectedBackupStorage}>
<SelectTrigger>
<SelectValue placeholder="Select storage" />
</SelectTrigger>
<SelectContent>
{backupStorages.map((storage) => (
<SelectItem key={`modal-storage-${storage.storage}`} value={storage.storage}>
{storage.storage} ({storage.avail_human} free)
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-sm flex items-center gap-1.5">
<Settings2 className="h-3.5 w-3.5" />
Mode
</Label>
<Select value={backupMode} onValueChange={setBackupMode}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="snapshot">Snapshot</SelectItem>
<SelectItem value="suspend">Suspend</SelectItem>
<SelectItem value="stop">Stop</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Notification Row */}
<div className="space-y-2">
<Label className="text-sm flex items-center gap-1.5">
<Bell className="h-3.5 w-3.5" />
Notification
</Label>
<Select value={backupNotification} onValueChange={setBackupNotification}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Use global settings</SelectItem>
<SelectItem value="always">Always notify</SelectItem>
<SelectItem value="failure">Notify on failure</SelectItem>
<SelectItem value="never">Never notify</SelectItem>
</SelectContent>
</Select>
</div>
{/* Protected Checkbox */}
<div className="flex items-center space-x-2">
<Checkbox
id="backup-protected"
checked={backupProtected}
onCheckedChange={(checked) => setBackupProtected(checked === true)}
/>
<Label htmlFor="backup-protected" className="text-sm flex items-center gap-1.5 cursor-pointer">
<Shield className="h-3.5 w-3.5" />
Protected (prevent accidental deletion)
</Label>
</div>
{/* PBS Change Detection Mode (only for LXC) */}
{selectedVM?.type === 'lxc' && (
<div className="space-y-2">
<Label className="text-sm flex items-center gap-1.5">
<Settings2 className="h-3.5 w-3.5" />
PBS change detection mode
<span className="text-xs text-muted-foreground ml-1">(for PBS storage)</span>
</Label>
<Select value={backupPbsChangeMode} onValueChange={setBackupPbsChangeMode}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="legacy">Legacy</SelectItem>
<SelectItem value="data">Data</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Notes */}
<div className="space-y-2">
<Label className="text-sm flex items-center gap-1.5">
<FileText className="h-3.5 w-3.5" />
Notes
</Label>
<Textarea
value={backupNotes}
onChange={(e) => setBackupNotes(e.target.value)}
placeholder="{{guestname}}"
className="min-h-[80px] resize-none"
/>
<p className="text-xs text-muted-foreground">
{'Variables: {{cluster}}, {{guestname}}, {{node}}, {{vmid}}'}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowBackupModal(false)}>
Cancel
</Button>
<Button
onClick={handleCreateBackup}
disabled={creatingBackup || !selectedBackupStorage}
className="bg-amber-600 hover:bg-amber-700 text-white"
>
{creatingBackup ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Creating...
</>
) : (
<>
<Archive className="h-4 w-4 mr-2" />
Backup
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* LXC Terminal Modal */} {/* LXC Terminal Modal */}
{terminalVmid !== null && ( {terminalVmid !== null && (
<LxcTerminalModal <LxcTerminalModal

View File

@@ -5616,11 +5616,17 @@ def api_create_backup(vmid):
storage = data.get('storage', 'local') storage = data.get('storage', 'local')
mode = data.get('mode', 'snapshot') # snapshot, suspend, stop mode = data.get('mode', 'snapshot') # snapshot, suspend, stop
compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd
protected = data.get('protected', False) # True/False
notification = data.get('notification', 'auto') # always, failure, never, auto
notes = data.get('notes', '') # Backup notes/description
pbs_change_detection = data.get('pbs_change_detection', None) # default, legacy, data (for PBS + LXC)
# Get node for this VM # Get node and VM type for this VM
node = None node = None
vm_type = None
vm_name = None
# Try to find VM in qemu # Try to find VM in cluster resources
try: try:
result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'], result = subprocess.run(['pvesh', 'get', '/cluster/resources', '--type', 'vm', '--output-format', 'json'],
capture_output=True, text=True, timeout=10) capture_output=True, text=True, timeout=10)
@@ -5629,6 +5635,8 @@ def api_create_backup(vmid):
for vm in vms: for vm in vms:
if vm.get('vmid') == vmid: if vm.get('vmid') == vmid:
node = vm.get('node') node = vm.get('node')
vm_type = vm.get('type') # 'qemu' or 'lxc'
vm_name = vm.get('name', '')
break break
except: except:
pass pass
@@ -5636,6 +5644,12 @@ def api_create_backup(vmid):
if not node: if not node:
return jsonify({'error': 'VM not found'}), 404 return jsonify({'error': 'VM not found'}), 404
# Process notes template variables
if notes:
notes = notes.replace('{{guestname}}', vm_name or '')
notes = notes.replace('{{vmid}}', str(vmid))
notes = notes.replace('{{node}}', node or '')
# Create backup using vzdump # Create backup using vzdump
cmd = [ cmd = [
'vzdump', str(vmid), 'vzdump', str(vmid),
@@ -5645,6 +5659,22 @@ def api_create_backup(vmid):
'--node', node '--node', node
] ]
# Add protected flag if enabled
if protected:
cmd.extend(['--protected', '1'])
# Add notification mode
if notification and notification != 'auto':
cmd.extend(['--notification-mode', notification])
# Add notes if provided
if notes:
cmd.extend(['--notes-template', notes])
# Add PBS change detection mode (only for LXC with PBS storage)
if pbs_change_detection and vm_type == 'lxc':
cmd.extend(['--pbs-change-detection-mode', pbs_change_detection])
# Run vzdump in background # Run vzdump in background
result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -5653,7 +5683,10 @@ def api_create_backup(vmid):
'message': f'Backup started for VM {vmid}', 'message': f'Backup started for VM {vmid}',
'storage': storage, 'storage': storage,
'mode': mode, 'mode': mode,
'compress': compress 'compress': compress,
'protected': protected,
'notification': notification,
'notes': notes
}) })
except Exception as e: except Exception as e: