mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-18 15:54:35 +01:00
new: pve (#307)
This commit is contained in:
@@ -20,6 +20,8 @@ enum ServerFuncBtn {
|
||||
snippet,
|
||||
@HiveField(6)
|
||||
iperf,
|
||||
@HiveField(7)
|
||||
pve,
|
||||
;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
@@ -30,6 +32,7 @@ enum ServerFuncBtn {
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
pve => Icons.computer,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
@@ -40,6 +43,7 @@ enum ServerFuncBtn {
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
pve => 'PVE',
|
||||
};
|
||||
|
||||
int toJson() => index;
|
||||
|
||||
@@ -27,6 +27,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
return ServerFuncBtn.snippet;
|
||||
case 6:
|
||||
return ServerFuncBtn.iperf;
|
||||
case 7:
|
||||
return ServerFuncBtn.pve;
|
||||
default:
|
||||
return ServerFuncBtn.terminal;
|
||||
}
|
||||
@@ -56,6 +58,9 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
case ServerFuncBtn.iperf:
|
||||
writer.writeByte(6);
|
||||
break;
|
||||
case ServerFuncBtn.pve:
|
||||
writer.writeByte(7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
lib/data/model/server/custom.dart
Normal file
36
lib/data/model/server/custom.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
|
||||
part 'custom.g.dart';
|
||||
|
||||
@HiveType(typeId: 7)
|
||||
final class ServerCustom {
|
||||
@HiveField(0)
|
||||
final String? temperature;
|
||||
@HiveField(1)
|
||||
final String? pveAddr;
|
||||
|
||||
const ServerCustom({
|
||||
this.temperature,
|
||||
this.pveAddr,
|
||||
});
|
||||
|
||||
static ServerCustom fromJson(Map<String, dynamic> json) {
|
||||
final temperature = json["temperature"] as String?;
|
||||
final pveAddr = json["pveAddr"] as String?;
|
||||
return ServerCustom(
|
||||
temperature: temperature,
|
||||
pveAddr: pveAddr,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (temperature != null) {
|
||||
json["temperature"] = temperature;
|
||||
}
|
||||
if (pveAddr != null) {
|
||||
json["pveAddr"] = pveAddr;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
44
lib/data/model/server/custom.g.dart
Normal file
44
lib/data/model/server/custom.g.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'custom.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
|
||||
@override
|
||||
final int typeId = 7;
|
||||
|
||||
@override
|
||||
ServerCustom read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ServerCustom(
|
||||
temperature: fields[0] as String?,
|
||||
pveAddr: fields[1] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerCustom obj) {
|
||||
writer
|
||||
..writeByte(2)
|
||||
..writeByte(0)
|
||||
..write(obj.temperature)
|
||||
..writeByte(1)
|
||||
..write(obj.pveAddr);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServerCustomAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
351
lib/data/model/server/pve.dart
Normal file
351
lib/data/model/server/pve.dart
Normal file
@@ -0,0 +1,351 @@
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/duration.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
|
||||
enum PveResType {
|
||||
lxc,
|
||||
qemu,
|
||||
node,
|
||||
storage,
|
||||
sdn,
|
||||
;
|
||||
|
||||
static PveResType fromString(String type) {
|
||||
switch (type) {
|
||||
case 'lxc':
|
||||
return PveResType.lxc;
|
||||
case 'qemu':
|
||||
return PveResType.qemu;
|
||||
case 'node':
|
||||
return PveResType.node;
|
||||
case 'storage':
|
||||
return PveResType.storage;
|
||||
case 'sdn':
|
||||
return PveResType.sdn;
|
||||
default:
|
||||
throw Exception('Unknown PveResType: $type');
|
||||
}
|
||||
}
|
||||
|
||||
String get toStr => switch (this) {
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.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']);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class PveLxc extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final int vmid;
|
||||
final String node;
|
||||
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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveQemu extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final int vmid;
|
||||
final String node;
|
||||
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'],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isRunning => status == 'running';
|
||||
|
||||
String get topRight {
|
||||
if (!isRunning) {
|
||||
return uptime.secondsToDuration().toStr;
|
||||
}
|
||||
return l10n.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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveStorage extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final String storage;
|
||||
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) {
|
||||
return PveStorage(
|
||||
id: json['id'],
|
||||
type: PveResType.storage,
|
||||
storage: json['storage'],
|
||||
node: json['node'],
|
||||
status: json['status'],
|
||||
plugintype: json['plugintype'],
|
||||
content: json['content'],
|
||||
shared: json['shared'],
|
||||
disk: json['disk'],
|
||||
maxdisk: json['maxdisk'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveSdn extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final String sdn;
|
||||
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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:toolbox/data/model/server/custom.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
@@ -6,6 +7,7 @@ import '../app/error.dart';
|
||||
|
||||
part 'server_private_info.g.dart';
|
||||
|
||||
/// In former version, it's called `ServerPrivateInfo`.
|
||||
@HiveType(typeId: 3)
|
||||
class ServerPrivateInfo {
|
||||
@HiveField(0)
|
||||
@@ -33,6 +35,9 @@ class ServerPrivateInfo {
|
||||
@HiveField(9)
|
||||
final String? jumpId;
|
||||
|
||||
@HiveField(10)
|
||||
final ServerCustom? custom;
|
||||
|
||||
final String id;
|
||||
|
||||
const ServerPrivateInfo({
|
||||
@@ -46,6 +51,7 @@ class ServerPrivateInfo {
|
||||
this.alterUrl,
|
||||
this.autoConnect,
|
||||
this.jumpId,
|
||||
this.custom,
|
||||
}) : id = '$user@$ip:$port';
|
||||
|
||||
static ServerPrivateInfo fromJson(Map<String, dynamic> json) {
|
||||
@@ -59,6 +65,9 @@ class ServerPrivateInfo {
|
||||
final alterUrl = json["alterUrl"] as String?;
|
||||
final autoConnect = json["autoConnect"] as bool?;
|
||||
final jumpId = json["jumpId"] as String?;
|
||||
final custom = json["customCmd"] == null
|
||||
? null
|
||||
: ServerCustom.fromJson(json["custom"].cast<String, dynamic>());
|
||||
|
||||
return ServerPrivateInfo(
|
||||
name: name,
|
||||
@@ -71,6 +80,7 @@ class ServerPrivateInfo {
|
||||
alterUrl: alterUrl,
|
||||
autoConnect: autoConnect,
|
||||
jumpId: jumpId,
|
||||
custom: custom,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,12 +90,27 @@ class ServerPrivateInfo {
|
||||
data["ip"] = ip;
|
||||
data["port"] = port;
|
||||
data["user"] = user;
|
||||
data["authorization"] = pwd;
|
||||
data["pubKeyId"] = keyId;
|
||||
data["tags"] = tags;
|
||||
data["alterUrl"] = alterUrl;
|
||||
data["autoConnect"] = autoConnect;
|
||||
data["jumpId"] = jumpId;
|
||||
if (pwd != null) {
|
||||
data["authorization"] = pwd;
|
||||
}
|
||||
if (keyId != null) {
|
||||
data["pubKeyId"] = keyId;
|
||||
}
|
||||
if (tags != null) {
|
||||
data["tags"] = tags;
|
||||
}
|
||||
if (alterUrl != null) {
|
||||
data["alterUrl"] = alterUrl;
|
||||
}
|
||||
if (autoConnect != null) {
|
||||
data["autoConnect"] = autoConnect;
|
||||
}
|
||||
if (jumpId != null) {
|
||||
data["jumpId"] = jumpId;
|
||||
}
|
||||
if (custom != null) {
|
||||
data["custom"] = custom?.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
|
||||
alterUrl: fields[7] as String?,
|
||||
autoConnect: fields[8] as bool?,
|
||||
jumpId: fields[9] as String?,
|
||||
custom: fields[10] as ServerCustom?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerPrivateInfo obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(11)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -53,7 +54,9 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
|
||||
..writeByte(8)
|
||||
..write(obj.autoConnect)
|
||||
..writeByte(9)
|
||||
..write(obj.jumpId);
|
||||
..write(obj.jumpId)
|
||||
..writeByte(10)
|
||||
..write(obj.custom);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
112
lib/data/provider/pve.dart
Normal file
112
lib/data/provider/pve.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/model/server/pve.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
|
||||
final class PveProvider extends ChangeNotifier {
|
||||
final ServerPrivateInfo spi;
|
||||
late final String addr;
|
||||
//late final SSHClient _client;
|
||||
|
||||
PveProvider({
|
||||
required this.spi,
|
||||
}) {
|
||||
// final client = _spi.server?.client;
|
||||
// if (client == null) {
|
||||
// throw Exception('Server client is null');
|
||||
// }
|
||||
// _client = client;
|
||||
final addr = spi.custom?.pveAddr;
|
||||
if (addr == null) {
|
||||
err.value = 'PVE address is null';
|
||||
return;
|
||||
}
|
||||
this.addr = addr;
|
||||
_init().then((_) => connected.complete());
|
||||
}
|
||||
|
||||
final err = ValueNotifier<String?>(null);
|
||||
final connected = Completer<void>();
|
||||
final session = Dio();
|
||||
|
||||
// int _localPort = 0;
|
||||
// String get addr => 'http://127.0.0.1:$_localPort';
|
||||
|
||||
Future<void> _init() async {
|
||||
//await _forward();
|
||||
await _login();
|
||||
}
|
||||
|
||||
// Future<void> _forward() async {
|
||||
// var retries = 0;
|
||||
// while (retries < 3) {
|
||||
// try {
|
||||
// _localPort = Random().nextInt(1000) + 37000;
|
||||
// print('Forwarding local port $_localPort');
|
||||
// final serverSocket = await ServerSocket.bind('localhost', _localPort);
|
||||
// final forward = await _client.forwardLocal('127.0.0.1', 8006);
|
||||
// serverSocket.listen((socket) {
|
||||
// forward.stream.cast<List<int>>().pipe(socket);
|
||||
// socket.pipe(forward.sink);
|
||||
// });
|
||||
// return;
|
||||
// } on SocketException {
|
||||
// retries++;
|
||||
// }
|
||||
// }
|
||||
// throw Exception('Failed to bind local port');
|
||||
// }
|
||||
|
||||
Future<void> _login() async {
|
||||
final resp = await session.post('$addr/api2/extjs/access/ticket', data: {
|
||||
'username': spi.user,
|
||||
'password': spi.pwd,
|
||||
'realm': 'pam',
|
||||
'new-format': '1'
|
||||
});
|
||||
final ticket = resp.data['data']['ticket'];
|
||||
session.options.headers['CSRFPreventionToken'] =
|
||||
resp.data['data']['CSRFPreventionToken'];
|
||||
session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket';
|
||||
}
|
||||
|
||||
Future<PveRes> list() async {
|
||||
await connected.future;
|
||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||
final list = resp.data['data'] as List;
|
||||
final items = list.map((e) => PveResIface.fromJson(e)).toList();
|
||||
final qemus = <PveQemu>[];
|
||||
final lxcs = <PveLxc>[];
|
||||
final nodes = <PveNode>[];
|
||||
final storages = <PveStorage>[];
|
||||
final sdns = <PveSdn>[];
|
||||
for (final item in items) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return PveRes(
|
||||
qemus: qemus,
|
||||
lxcs: lxcs,
|
||||
nodes: nodes,
|
||||
storages: storages,
|
||||
sdns: sdns,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user