Basic functionality

Lists all vms (of first node only)
shows VM status
has vm toggle button
android_attempt
Felix Bruns 10 months ago
parent 01e9b309b4
commit 6d798ef82b

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:pi_dashboard/src/proxmox_lister/proxmox_lister_list_view.dart'; import 'package:pi_dashboard/src/proxmox_lister/proxmox_lister_list_view.dart';
import 'settings/settings_controller.dart'; import 'settings/settings_controller.dart';
@ -51,9 +50,9 @@ class MyApp extends StatelessWidget {
case SettingsView.routeName: case SettingsView.routeName:
return SettingsView(controller: settingsController); return SettingsView(controller: settingsController);
case ProxmoxListerView.routeName: case ProxmoxListerView.routeName:
return ProxmoxListerView(); return ProxmoxListerView(settings: settingsController);
default: default:
return ProxmoxListerView(); return ProxmoxListerView(settings: settingsController);
} }
}, },
); );

@ -1,17 +1,70 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pi_dashboard/src/proxmox_webservice/model.dart';
import 'package:pi_dashboard/src/proxmox_webservice/service.dart';
import 'package:pi_dashboard/src/settings/settings_controller.dart';
import 'package:pi_dashboard/src/settings/settings_service.dart';
import '../settings/settings_view.dart'; import '../settings/settings_view.dart';
import 'vm_card.dart'; import 'vm_card.dart';
import 'proxomx_vm.dart';
class ProxmoxListerView extends StatelessWidget { class ProxmoxListerView extends StatefulWidget {
ProxmoxListerView({ const ProxmoxListerView({
super.key, super.key,
this.vms = const [ProxmoxVM(id: 1, name: "template")], required this.settings,
}); });
static const routeName = '/proxmox_lister'; static const routeName = '/proxmox_lister';
List<ProxmoxVM> vms; final SettingsController settings;
@override
State<ProxmoxListerView> createState() => _ProxmoxListerState();
}
class _ProxmoxListerState extends State<ProxmoxListerView> {
late Future<ProxmoxNodeMap> nodes;
late SettingsController settings;
late ProxmoxWebService _service;
@override
void initState() {
super.initState();
settings = super.widget.settings;
nodes = Future<ProxmoxNodeMap>.delayed(Duration.zero, () => getVms());
Timer.periodic(const Duration(seconds: 3), (_) {
syncVMs();
});
}
Future<ProxmoxNodeMap> getVms() async {
await settings.loadSettings();
_service = ProxmoxWebService(
hostname: settings.hostname,
username: settings.username,
password: settings.password,
);
final success = await _service.authenticate();
if (!success) {
return Future.error(Exception("couldn't authenticate against Proxmox"));
}
ProxmoxNodeMap map = {};
final nodes = await _service.listNodes();
for (final node in nodes) {
map[node] = await _service.listVms(node.node);
}
return map;
}
void syncVMs() async {
nodes = getVms();
await nodes;
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,23 +75,47 @@ class ProxmoxListerView extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
onPressed: () { onPressed: () {
Navigator.restorablePushNamed(context, SettingsView.routeName); syncVMs();
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
onPressed: () {}, onPressed: () {
Navigator.restorablePushNamed(context, SettingsView.routeName);
},
), ),
], ],
), ),
body: ListView.builder( body: FutureBuilder<ProxmoxNodeMap>(
future: nodes,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
if (snapshot.requireData.isEmpty) {
return const Center(child: Icon(Icons.block));
}
final nodeEntry = snapshot.requireData.entries.first;
return ListView.builder(
restorationId: "proxmoxVMLister", restorationId: "proxmoxVMLister",
itemCount: vms.length, itemCount: nodeEntry.value.length,
itemBuilder: (BuildContext ctx, int index) { itemBuilder: (BuildContext ctx, int index) {
final vm = vms[index]; return ProxmoxVmCard(
node: nodeEntry.key,
return ProxmoxVmCard(vm: vm); vm: nodeEntry.value[index],
pm_service: _service,
);
},
);
} else if (snapshot.hasError) {
return AlertDialog(
title: const Text("error"),
content: Text(snapshot.error.toString()),
);
} else {
return const Center(child: CircularProgressIndicator());
}
}, },
)); ));
} }
} }

@ -1,12 +0,0 @@
class ProxmoxVM {
const ProxmoxVM({
this.id = 0,
this.name = "",
this.isRunning = true,
});
final int id;
final String name;
final bool isRunning;
}

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pi_dashboard/src/proxmox_lister/proxomx_vm.dart'; import 'package:pi_dashboard/src/proxmox_webservice/model.dart';
import 'package:pi_dashboard/src/proxmox_webservice/service.dart';
class RunningIndicator extends StatelessWidget { class RunningIndicator extends StatelessWidget {
const RunningIndicator({ const RunningIndicator({
@ -23,10 +24,14 @@ class RunningIndicator extends StatelessWidget {
class ProxmoxVmCard extends StatelessWidget { class ProxmoxVmCard extends StatelessWidget {
ProxmoxVmCard({ ProxmoxVmCard({
super.key, super.key,
this.vm = const ProxmoxVM(), required this.node,
required this.vm,
required this.pm_service,
}); });
final ProxmoxVM vm; final ProxmoxNode node;
final ProxmoxVm vm;
final ProxmoxWebService pm_service;
@override @override
Widget build(_) { Widget build(_) {
@ -37,16 +42,19 @@ class ProxmoxVmCard extends StatelessWidget {
leading: const Icon(Icons.dns), leading: const Icon(Icons.dns),
title: Row( title: Row(
children: [ children: [
Text(vm.name), Text(vm.name ?? ""),
const Spacer(), const Spacer(),
RunningIndicator(isRunning: vm.isRunning), RunningIndicator(isRunning: vm.status == "running"),
] ]
), ),
subtitle: Row( subtitle: Row(
children: [ children: [
Text("ID: ${vm.id.toString()}"), Text("ID: ${vm.vmid.toString()}"),
const Spacer(), const Spacer(),
TextButton(onPressed: () => {}, child: const Icon(Icons.power_settings_new)), IconButton(
icon: const Icon(Icons.power_settings_new),
onPressed: () => pm_service.toggleVm(node, vm),
),
], ],
), ),
), ),

@ -0,0 +1,157 @@
import 'dart:ffi';
typedef ProxmoxNodeMap = Map<ProxmoxNode, List<ProxmoxVm>>;
class ProxmoxNode {
final String node;
final String status;
final double? cpu;
final String? level;
final int? maxcpu;
final int? maxmem;
final int? mem;
final String? sslFingerprint;
final int? uptime;
final String? id;
final String? type;
final int? disk;
ProxmoxNode({
required this.node,
required this.status,
this.cpu,
this.level,
this.maxcpu,
this.maxmem,
this.mem,
this.sslFingerprint,
this.uptime,
this.id,
this.type,
this.disk,
});
factory ProxmoxNode.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'node': String node,
'status': String status,
'cpu': double? cpu,
'level': String? level,
'maxcpu': int? maxcpu,
'maxmem': int? maxmem,
'mem': int? mem,
'ssl_fingerprint': String? sslFingerprint,
'uptime': int? uptime,
'id': String? id,
'type': String? type,
'disk': int? disk,
} =>
ProxmoxNode(
node: node,
status: status,
cpu: cpu,
level: level,
maxcpu: maxcpu,
maxmem: maxmem,
mem: mem,
sslFingerprint: sslFingerprint,
uptime: uptime,
id: id,
type: type,
disk: disk,
),
_ => throw FormatException(
"failed to parse proxmox node into object: $json"),
};
}
}
class ProxmoxVm {
final String status;
final num vmid;
final num? cpus;
final num? mem;
final String? name;
final num? diskwrite;
final num? netout;
final num? uptime;
final num? cpu;
final num? maxdisk;
final num? netin;
final num? diskread;
final num? disk;
final num? maxmem;
ProxmoxVm({
required this.status,
required this.vmid,
this.cpus,
this.mem,
this.name,
this.diskwrite,
this.netout,
this.uptime,
this.cpu,
this.maxdisk,
this.netin,
this.diskread,
this.disk,
this.maxmem,
});
factory ProxmoxVm.fromJson(Map<String, dynamic> json) {
return ProxmoxVm(
status: json['status'],
vmid: json['vmid'],
cpus: json['cpus'],
mem: json['mem'],
name: json['name'],
diskwrite: json['diskwrite'],
netout: json['netout'],
uptime: json['uptime'],
cpu: json['cpu'],
maxdisk: json['maxdisk'],
netin: json['netin'],
diskread: json['diskread'],
disk: json['disk'],
maxmem: json['maxmem'],
);
return switch (json) {
{
'status': String status,
'vmid': num vmid,
'cpus': num? cpus,
'mem': num? mem,
'name': String? name,
'diskwrite': num? diskwrite,
'netout': num? netout,
'uptime': num? uptime,
'cpu': num? cpu,
'maxdisk': num? maxdisk,
'netin': num? netin,
'diskread': num? diskread,
'disk': num? disk,
'maxmem': num? maxmem,
} =>
ProxmoxVm(
status: status,
vmid: vmid,
cpus: cpus,
mem: mem,
name: name,
diskwrite: diskwrite,
netout: netout,
uptime: uptime,
cpu: cpu,
maxdisk: maxdisk,
netin: netin,
diskread: diskread,
disk: disk,
maxmem: maxmem,
),
_ => throw FormatException(
"failed to parse proxmox node into object: $json"),
};
}
}

@ -1,3 +1,8 @@
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;
import 'model.dart';
class ProxmoxWebService { class ProxmoxWebService {
ProxmoxWebService({ ProxmoxWebService({
this.hostname = "", this.hostname = "",
@ -9,8 +14,114 @@ class ProxmoxWebService {
final String username; final String username;
final String password; final String password;
Future<void> authenticate() async { String? _csrfToken;
String? _ticket;
bool _authenticated = false;
Future<bool> authenticate() async {
final resp = await http.post(
Uri.https(
hostname,
"/api2/json/access/ticket",
{
'username': username,
'password': password,
},
),
);
if (resp.statusCode == 200) {
final body = convert.jsonDecode(resp.body) as Map<String, dynamic>;
final data = body["data"] as Map<String, dynamic>;
_csrfToken = data["CSRFPreventionToken"];
_ticket = data["ticket"];
_authenticated = true;
} else {
_authenticated = false;
}
return _authenticated;
}
Future<Map<String, dynamic>?> _doGet(String endpoint, {debug = false}) async {
if (!_authenticated) return null;
final resp = await http.get(
Uri.https(
hostname,
endpoint,
),
headers: {
"CSRFPreventionToken": _csrfToken as String,
"Cookie": "PVEAuthCookie=$_ticket"
});
if (debug) print(resp.body);
if (resp.statusCode != 200) return null;
return convert.jsonDecode(resp.body) as Map<String, dynamic>;
}
Future<Map<String, dynamic>?> _doPost(
String endpoint, Map<String, dynamic> payload,
{debug = false}) async {
if (!_authenticated) return null;
final resp = await http.post(
Uri.https(
hostname,
endpoint,
),
headers: {
"CSRFPreventionToken": _csrfToken as String,
"Cookie": "PVEAuthCookie=$_ticket"
},
body: payload,
);
if (debug) print(resp.body);
if (resp.statusCode != 200) return null;
return convert.jsonDecode(resp.body) as Map<String, dynamic>;
}
Future<List<ProxmoxNode>> listNodes() async {
List<ProxmoxNode> nodes = [];
final resp = await _doGet("/api2/json/nodes");
if (resp == null) return [];
for (final nodeJson in resp["data"]) {
nodes.add(ProxmoxNode.fromJson(nodeJson as Map<String, dynamic>));
}
return nodes;
}
Future<List<ProxmoxVm>> listVms(String node) async {
List<ProxmoxVm> vms = [];
final resp = await _doGet("/api2/json/nodes/$node/qemu");
if (resp == null) return [];
for (final vmJson in resp["data"]) {
vms.add(ProxmoxVm.fromJson(vmJson as Map<String, dynamic>));
}
vms.sort((a, b) => a.vmid.compareTo(b.vmid));
return vms;
}
Future<bool> toggleVm(ProxmoxNode node, ProxmoxVm vm) async {
if (!_authenticated) return false;
final endpoint =
"/api2/json/nodes/${node.node}/qemu/${vm.vmid}/status/${vm.status == "running" ? "stop" : "start"}";
final resp = await _doPost(endpoint, {}, debug: true);
if (resp == null) return false;
return true;
} }
} }

@ -31,11 +31,6 @@ class SettingsService {
} }
Future<void> setKeyStr(String key, String value) async { Future<void> setKeyStr(String key, String value) async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
final res = await prefs.setString(key, value); await prefs.setString(key, value);
if(res) {
print("successfully saved");
print(key);
print(value);
}
} }
} }

@ -1,6 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pi_dashboard/src/settings/settings_service.dart';
import 'settings_controller.dart'; import 'settings_controller.dart';
@ -23,15 +21,11 @@ class SettingsView extends StatelessWidget {
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
// Glue the SettingsController to the theme selection DropdownButton. child: Column(
// children: [
// When a user selects a theme from the dropdown list, the const Text("Config file is stored under \$HOME/.local/share"),
// SettingsController is updated, which rebuilds the MaterialApp.
child: Column(children: [
DropdownButton<ThemeMode>( DropdownButton<ThemeMode>(
// Read the selected themeMode from the controller
value: controller.themeMode, value: controller.themeMode,
// Call the updateThemeMode method any time the user selects a theme.
onChanged: controller.updateThemeMode, onChanged: controller.updateThemeMode,
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
@ -75,7 +69,8 @@ class SettingsView extends StatelessWidget {
initialValue: controller.password, initialValue: controller.password,
onChanged: controller.setPassword, onChanged: controller.setPassword,
), ),
],) ],
)
), ),
); );

Loading…
Cancel
Save