Files
GT610 09431a0b08 fix(pve): Fix connection issues and add more error handlings (#1081)
* feat(PVE): Added display of PVE connection loading steps

Added a detailed display of loading steps during the PVE connection process, including stages such as establishing an SSH tunnel, authentication, and data retrieval

Also optimized the sorting of PVE storage content and the logic for handling connection errors

* feat(pve): Added error handling and prompts for PVE two-factor authentication

Added error handling for PVE servers when two-factor authentication is enabled, along with relevant error types and localized prompts

* feat(PVE): Added support for PVE passwords during key-based authentication

- Added the `pvePwd` field to the `ServerCustom` model
- Added a PVE password input field to the edit page (displayed only during key-based authentication)
- Updated multilingual files to support PVE-related loading states and password prompts
- Optimized PVE connection logic to support password verification during key-based authentication
2026-03-22 16:25:48 +08:00

441 lines
10 KiB
Dart

import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum PveResType {
lxc,
qemu,
node,
storage,
sdn;
static PveResType? fromString(String type) => switch (type.toLowerCase()) {
'lxc' => PveResType.lxc,
'qemu' => PveResType.qemu,
'node' => PveResType.node,
'storage' => PveResType.storage,
'sdn' => PveResType.sdn,
_ => null,
};
String get toStr => switch (this) {
PveResType.node => libL10n.node,
PveResType.qemu => 'QEMU',
PveResType.lxc => 'LXC',
PveResType.storage => libL10n.storage,
PveResType.sdn => 'SDN',
};
}
sealed class PveResIface {
String get id;
String get status;
PveResType get type;
static PveResIface? fromJson(Map<String, dynamic> json) {
final type = PveResType.fromString(json['type']);
if (type == null) return null;
switch (type) {
case PveResType.lxc:
return PveLxc.fromJson(json);
case PveResType.qemu:
return PveQemu.fromJson(json);
case PveResType.node:
return PveNode.fromJson(json);
case PveResType.storage:
return PveStorage.fromJson(json);
case PveResType.sdn:
return PveSdn.fromJson(json);
}
}
}
abstract interface class PveCtrlIface {
String get node;
String get id;
bool get available;
String get summary;
String get name;
}
final class PveLxc extends PveResIface implements PveCtrlIface {
@override
final String id;
@override
final PveResType type;
final int vmid;
@override
final String node;
@override
final String name;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
final int disk;
final int maxdisk;
final int diskread;
final int diskwrite;
final int netin;
final int netout;
PveLxc({
required this.id,
required this.type,
required this.vmid,
required this.node,
required this.name,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
required this.disk,
required this.maxdisk,
required this.diskread,
required this.diskwrite,
required this.netin,
required this.netout,
});
static PveLxc fromJson(Map<String, dynamic> json) {
return PveLxc(
id: json['id'],
type: PveResType.lxc,
vmid: json['vmid'],
node: json['node'],
name: json['name'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
disk: json['disk'],
maxdisk: json['maxdisk'],
diskread: json['diskread'],
diskwrite: json['diskwrite'],
netin: json['netin'],
netout: json['netout'],
);
}
@override
bool get available => status == 'running';
@override
String get summary {
if (available) {
return uptime.secondsToDuration().toAgoStr;
}
return libL10n.stopped;
}
}
final class PveQemu extends PveResIface implements PveCtrlIface {
@override
final String id;
@override
final PveResType type;
final int vmid;
@override
final String node;
@override
final String name;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
final int disk;
final int maxdisk;
final int diskread;
final int diskwrite;
final int netin;
final int netout;
PveQemu({
required this.id,
required this.type,
required this.vmid,
required this.node,
required this.name,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
required this.disk,
required this.maxdisk,
required this.diskread,
required this.diskwrite,
required this.netin,
required this.netout,
});
static PveQemu fromJson(Map<String, dynamic> json) {
return PveQemu(
id: json['id'],
type: PveResType.qemu,
vmid: json['vmid'],
node: json['node'],
name: json['name'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
disk: json['disk'],
maxdisk: json['maxdisk'],
diskread: json['diskread'],
diskwrite: json['diskwrite'],
netin: json['netin'],
netout: json['netout'],
);
}
@override
bool get available => status == 'running';
@override
String get summary {
if (available) {
return uptime.secondsToDuration().toAgoStr;
}
return libL10n.stopped;
}
}
final class PveNode extends PveResIface {
@override
final String id;
@override
final PveResType type;
final String node;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
PveNode({
required this.id,
required this.type,
required this.node,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
});
static PveNode fromJson(Map<String, dynamic> json) {
return PveNode(
id: json['id'],
type: PveResType.node,
node: json['node'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
);
}
bool get isRunning => status == 'online';
String get topRight {
if (isRunning) {
return uptime.secondsToDuration().toAgoStr;
}
return libL10n.stopped;
}
}
final class PveStorage extends PveResIface implements PveCtrlIface {
@override
final String id;
@override
final PveResType type;
final String storage;
@override
final String node;
@override
final String status;
final String plugintype;
final String content;
final int shared;
final int disk;
final int maxdisk;
PveStorage({
required this.id,
required this.type,
required this.storage,
required this.node,
required this.status,
required this.plugintype,
required this.content,
required this.shared,
required this.disk,
required this.maxdisk,
});
static PveStorage fromJson(Map<String, dynamic> json) {
final rawContent = json['content'] as String?;
final contentParts = rawContent?.split(',');
contentParts?.sort();
final content = contentParts?.join(',') ?? rawContent ?? '';
return PveStorage(
id: json['id'],
type: PveResType.storage,
storage: json['storage'],
node: json['node'],
status: json['status'],
plugintype: json['plugintype'],
content: content,
shared: json['shared'],
disk: json['disk'],
maxdisk: json['maxdisk'],
);
}
@override
bool get available => status == 'available';
@override
String get name => storage;
@override
String get summary {
if (available) {
return '${l10n.used}: ${disk.bytes2Str} / ${libL10n.total}: ${maxdisk.bytes2Str}';
}
return libL10n.notAvailable;
}
}
final class PveSdn extends PveResIface implements PveCtrlIface {
@override
final String id;
@override
final PveResType type;
final String sdn;
@override
final String node;
@override
final String status;
PveSdn({required this.id, required this.type, required this.sdn, required this.node, required this.status});
static PveSdn fromJson(Map<String, dynamic> json) {
return PveSdn(
id: json['id'],
type: PveResType.sdn,
sdn: json['sdn'],
node: json['node'],
status: json['status'],
);
}
@override
bool get available => status == 'ok';
@override
String get name => sdn;
@override
String get summary => available ? status : libL10n.notAvailable;
}
final class PveRes {
final List<PveNode> nodes;
final List<PveQemu> qemus;
final List<PveLxc> lxcs;
final List<PveStorage> storages;
final List<PveSdn> sdns;
const PveRes({
required this.nodes,
required this.qemus,
required this.lxcs,
required this.storages,
required this.sdns,
});
bool get onlyOneNode => nodes.length == 1;
int get length => qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
PveResIface operator [](int index) {
if (index < nodes.length) {
return nodes[index];
}
index -= nodes.length;
if (index < qemus.length) {
return qemus[index];
}
index -= qemus.length;
if (index < lxcs.length) {
return lxcs[index];
}
index -= lxcs.length;
if (index < storages.length) {
return storages[index];
}
index -= storages.length;
return sdns[index];
}
static Future<PveRes> parse((List list, PveRes? old) val) async {
final (list, old) = val;
final items = list.map((e) => PveResIface.fromJson(e)).toList();
final List<PveQemu> qemus = [];
final List<PveLxc> lxcs = [];
final List<PveNode> nodes = [];
final List<PveStorage> storages = [];
final List<PveSdn> sdns = [];
for (final item in items) {
if (item == null) continue;
switch (item.type) {
case PveResType.lxc:
lxcs.add(item as PveLxc);
break;
case PveResType.qemu:
qemus.add(item as PveQemu);
break;
case PveResType.node:
nodes.add(item as PveNode);
break;
case PveResType.storage:
storages.add(item as PveStorage);
break;
case PveResType.sdn:
sdns.add(item as PveSdn);
break;
}
}
if (old != null) {
qemus.reorder(order: old.qemus.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
lxcs.reorder(order: old.lxcs.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
nodes.reorder(order: old.nodes.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
storages.reorder(order: old.storages.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
sdns.reorder(order: old.sdns.map((e) => e.id).toList(), finder: (e, s) => e.id == s);
}
return PveRes(qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns);
}
}