mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
3 Commits
bd74ab8d7b
...
temp-pve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eca353429 | ||
|
|
d9e3c4678a | ||
|
|
1243a7bd8d |
203
agent/pve.go
203
agent/pve.go
@@ -18,91 +18,11 @@ type pveManager struct {
|
||||
nodeName string // Cluster node name
|
||||
cpuCount int // CPU count on node
|
||||
nodeStatsMap map[string]*container.PveNodeStats // Keeps track of pve node stats
|
||||
lastInitTry time.Time // Last time node initialization was attempted
|
||||
}
|
||||
|
||||
// Returns stats for all running VMs/LXCs
|
||||
func (pm *pveManager) getPVEStats() ([]*container.PveNodeStats, error) {
|
||||
if pm.client == nil {
|
||||
return nil, errors.New("PVE client not configured")
|
||||
}
|
||||
cluster, err := pm.client.Cluster(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Error getting cluster", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
resources, err := cluster.Resources(context.Background(), "vm")
|
||||
if err != nil {
|
||||
slog.Error("Error getting resources", "err", err, "resources", resources)
|
||||
return nil, err
|
||||
}
|
||||
containersLength := len(resources)
|
||||
containerIds := make(map[string]struct{}, containersLength)
|
||||
|
||||
// only include running vms and lxcs on selected node
|
||||
for _, resource := range resources {
|
||||
if resource.Node == pm.nodeName && resource.Status == "running" {
|
||||
containerIds[resource.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
// remove invalid container stats
|
||||
for id := range pm.nodeStatsMap {
|
||||
if _, exists := containerIds[id]; !exists {
|
||||
delete(pm.nodeStatsMap, id)
|
||||
}
|
||||
}
|
||||
|
||||
// populate stats
|
||||
stats := make([]*container.PveNodeStats, 0, len(containerIds))
|
||||
for _, resource := range resources {
|
||||
if _, exists := containerIds[resource.ID]; !exists {
|
||||
continue
|
||||
}
|
||||
resourceStats, initialized := pm.nodeStatsMap[resource.ID]
|
||||
if !initialized {
|
||||
resourceStats = &container.PveNodeStats{}
|
||||
pm.nodeStatsMap[resource.ID] = resourceStats
|
||||
}
|
||||
// reset current stats
|
||||
resourceStats.Cpu = 0
|
||||
resourceStats.Mem = 0
|
||||
resourceStats.Bandwidth = [2]uint64{0, 0}
|
||||
// Store clean name (no type suffix)
|
||||
resourceStats.Name = resource.Name
|
||||
// Store resource ID (e.g. "qemu/100") in .Id (cbor key 7, json:"-")
|
||||
resourceStats.Id = resource.ID
|
||||
// Store type (e.g. "qemu" or "lxc") in .Image (cbor key 8, json:"-")
|
||||
resourceStats.Type = resource.Type
|
||||
// PVE limits (cbor-only, for pve_vms table)
|
||||
resourceStats.MaxCPU = resource.MaxCPU
|
||||
resourceStats.MaxMem = resource.MaxMem
|
||||
resourceStats.Uptime = resource.Uptime
|
||||
// prevent first run from sending all prev sent/recv bytes
|
||||
total_sent := uint64(resource.NetOut)
|
||||
total_recv := uint64(resource.NetIn)
|
||||
var sent_delta, recv_delta float64
|
||||
if initialized {
|
||||
secondsElapsed := time.Since(resourceStats.PrevReadTime).Seconds()
|
||||
if secondsElapsed > 0 {
|
||||
sent_delta = float64(total_sent-resourceStats.PrevNet.Sent) / secondsElapsed
|
||||
recv_delta = float64(total_recv-resourceStats.PrevNet.Recv) / secondsElapsed
|
||||
}
|
||||
}
|
||||
resourceStats.PrevNet.Sent = total_sent
|
||||
resourceStats.PrevNet.Recv = total_recv
|
||||
resourceStats.PrevReadTime = time.Now()
|
||||
|
||||
// Update final stats values
|
||||
resourceStats.Cpu = twoDecimals(100.0 * resource.CPU * float64(resource.MaxCPU) / float64(pm.cpuCount))
|
||||
resourceStats.Mem = bytesToMegabytes(float64(resource.Mem))
|
||||
resourceStats.Bandwidth = [2]uint64{uint64(sent_delta), uint64(recv_delta)}
|
||||
|
||||
stats = append(stats, resourceStats)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// Creates a new PVE manager - may return nil if required environment variables are not set or if there is an error connecting to the API
|
||||
// newPVEManager creates a new PVE manager - may return nil if required environment variables
|
||||
// are not set or if there is an error connecting to the API
|
||||
func newPVEManager() *pveManager {
|
||||
url, exists := GetEnv("PROXMOX_URL")
|
||||
if !exists {
|
||||
@@ -145,14 +65,113 @@ func newPVEManager() *pveManager {
|
||||
nodeStatsMap: make(map[string]*container.PveNodeStats),
|
||||
}
|
||||
|
||||
// Retrieve node cpu count
|
||||
node, err := client.Node(context.Background(), nodeName)
|
||||
if err != nil {
|
||||
slog.Error("Error connecting to Proxmox", "err", err)
|
||||
return nil
|
||||
} else {
|
||||
pveManager.cpuCount = node.CPUInfo.CPUs
|
||||
}
|
||||
|
||||
return &pveManager
|
||||
}
|
||||
|
||||
// ensureInitialized checks if the PVE manager is initialized and attempts to initialize it if not.
|
||||
// It returns an error if initialization fails or if a retry is pending.
|
||||
func (pm *pveManager) ensureInitialized(ctx context.Context) error {
|
||||
if pm.client == nil {
|
||||
return errors.New("PVE client not configured")
|
||||
}
|
||||
if pm.cpuCount > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Since(pm.lastInitTry) < 30*time.Second {
|
||||
return errors.New("PVE initialization retry pending")
|
||||
}
|
||||
pm.lastInitTry = time.Now()
|
||||
|
||||
node, err := pm.client.Node(ctx, pm.nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.CPUInfo.CPUs <= 0 {
|
||||
return errors.New("node returned zero CPUs")
|
||||
}
|
||||
|
||||
pm.cpuCount = node.CPUInfo.CPUs
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPVEStats returns stats for all running VMs/LXCs
|
||||
func (pm *pveManager) getPVEStats() ([]*container.PveNodeStats, error) {
|
||||
if err := pm.ensureInitialized(context.Background()); err != nil {
|
||||
slog.Warn("Proxmox API unavailable", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
cluster, err := pm.client.Cluster(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Error getting cluster", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
resources, err := cluster.Resources(context.Background(), "vm")
|
||||
if err != nil {
|
||||
slog.Error("Error getting resources", "err", err, "resources", resources)
|
||||
return nil, err
|
||||
}
|
||||
containersLength := len(resources)
|
||||
resourceIds := make(map[string]struct{}, containersLength)
|
||||
|
||||
// only include running vms and lxcs on selected node
|
||||
for _, resource := range resources {
|
||||
if resource.Node == pm.nodeName && resource.Status == "running" {
|
||||
resourceIds[resource.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
// remove invalid container stats
|
||||
for id := range pm.nodeStatsMap {
|
||||
if _, exists := resourceIds[id]; !exists {
|
||||
delete(pm.nodeStatsMap, id)
|
||||
}
|
||||
}
|
||||
|
||||
// populate stats
|
||||
stats := make([]*container.PveNodeStats, 0, len(resourceIds))
|
||||
for _, resource := range resources {
|
||||
if _, exists := resourceIds[resource.ID]; !exists {
|
||||
continue
|
||||
}
|
||||
resourceStats, initialized := pm.nodeStatsMap[resource.ID]
|
||||
if !initialized {
|
||||
resourceStats = &container.PveNodeStats{}
|
||||
pm.nodeStatsMap[resource.ID] = resourceStats
|
||||
}
|
||||
resourceStats.Name = resource.Name
|
||||
resourceStats.Id = resource.ID
|
||||
resourceStats.Type = resource.Type
|
||||
resourceStats.MaxCPU = resource.MaxCPU
|
||||
resourceStats.MaxMem = resource.MaxMem
|
||||
resourceStats.Uptime = resource.Uptime
|
||||
resourceStats.DiskRead = resource.DiskRead
|
||||
resourceStats.DiskWrite = resource.DiskWrite
|
||||
resourceStats.Disk = resource.MaxDisk
|
||||
|
||||
// prevent first run from sending all prev sent/recv bytes
|
||||
total_sent := resource.NetOut
|
||||
total_recv := resource.NetIn
|
||||
var sent_delta, recv_delta float64
|
||||
if initialized {
|
||||
secondsElapsed := time.Since(resourceStats.PrevReadTime).Seconds()
|
||||
if secondsElapsed > 0 {
|
||||
sent_delta = float64(total_sent-resourceStats.PrevNet.Sent) / secondsElapsed
|
||||
recv_delta = float64(total_recv-resourceStats.PrevNet.Recv) / secondsElapsed
|
||||
}
|
||||
}
|
||||
resourceStats.PrevNet.Sent = total_sent
|
||||
resourceStats.PrevNet.Recv = total_recv
|
||||
resourceStats.PrevReadTime = time.Now()
|
||||
|
||||
// Update final stats values
|
||||
resourceStats.Cpu = twoDecimals(100.0 * resource.CPU * float64(resource.MaxCPU) / float64(pm.cpuCount))
|
||||
resourceStats.Mem = bytesToMegabytes(float64(resource.Mem))
|
||||
resourceStats.Bandwidth = [2]uint64{uint64(sent_delta), uint64(recv_delta)}
|
||||
resourceStats.NetOut = total_sent
|
||||
resourceStats.NetIn = total_recv
|
||||
|
||||
stats = append(stats, resourceStats)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
92
agent/pve_test.go
Normal file
92
agent/pve_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewPVEManagerDoesNotConnectAtStartup(t *testing.T) {
|
||||
t.Setenv("BESZEL_AGENT_PROXMOX_URL", "https://127.0.0.1:1/api2/json")
|
||||
t.Setenv("BESZEL_AGENT_PROXMOX_NODE", "pve")
|
||||
t.Setenv("BESZEL_AGENT_PROXMOX_TOKENID", "root@pam!test")
|
||||
t.Setenv("BESZEL_AGENT_PROXMOX_SECRET", "secret")
|
||||
|
||||
pm := newPVEManager()
|
||||
require.NotNil(t, pm)
|
||||
assert.Zero(t, pm.cpuCount)
|
||||
}
|
||||
|
||||
func TestPVEManagerRetriesInitialization(t *testing.T) {
|
||||
var nodeRequests atomic.Int32
|
||||
var clusterRequests atomic.Int32
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api2/json/nodes/pve/status":
|
||||
nodeRequests.Add(1)
|
||||
fmt.Fprint(w, `{"data":{"cpuinfo":{"cpus":8}}}`)
|
||||
case "/api2/json/cluster/status":
|
||||
fmt.Fprint(w, `{"data":[{"type":"cluster","name":"test-cluster","id":"test-cluster","version":1,"quorate":1}]}`)
|
||||
case "/api2/json/cluster/resources":
|
||||
clusterRequests.Add(1)
|
||||
fmt.Fprint(w, `{"data":[{"id":"qemu/101","type":"qemu","node":"pve","status":"running","name":"vm-101","cpu":0.5,"maxcpu":4,"maxmem":4096,"mem":2048,"netin":1024,"netout":2048,"diskread":10,"diskwrite":20,"maxdisk":8192,"uptime":60}]}`)
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pm := &pveManager{
|
||||
client: proxmox.NewClient(server.URL+"/api2/json",
|
||||
proxmox.WithHTTPClient(&http.Client{
|
||||
Transport: &failOnceRoundTripper{
|
||||
base: server.Client().Transport,
|
||||
},
|
||||
}),
|
||||
proxmox.WithAPIToken("root@pam!test", "secret"),
|
||||
),
|
||||
nodeName: "pve",
|
||||
nodeStatsMap: make(map[string]*container.PveNodeStats),
|
||||
}
|
||||
|
||||
stats, err := pm.getPVEStats()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, stats)
|
||||
assert.Zero(t, pm.cpuCount)
|
||||
|
||||
pm.lastInitTry = time.Now().Add(-31 * time.Second)
|
||||
stats, err = pm.getPVEStats()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 1)
|
||||
assert.Equal(t, int32(1), nodeRequests.Load())
|
||||
assert.Equal(t, int32(1), clusterRequests.Load())
|
||||
assert.Equal(t, 8, pm.cpuCount)
|
||||
assert.Equal(t, "qemu/101", stats[0].Id)
|
||||
assert.Equal(t, 25.0, stats[0].Cpu)
|
||||
assert.Equal(t, uint64(1024), stats[0].NetIn)
|
||||
assert.Equal(t, uint64(2048), stats[0].NetOut)
|
||||
}
|
||||
|
||||
type failOnceRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
failed atomic.Bool
|
||||
}
|
||||
|
||||
func (rt *failOnceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Path == "/api2/json/nodes/pve/status" && !rt.failed.Swap(true) {
|
||||
return nil, errors.New("dial tcp 127.0.0.1:8006: connect: connection refused")
|
||||
}
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*failOnceRoundTripper)(nil)
|
||||
@@ -157,8 +157,13 @@ type PveNodeStats struct {
|
||||
|
||||
// fields used for pve_vms table
|
||||
|
||||
MaxCPU uint64 `json:"-" cbor:"10,keyasint,omitzero"` // PVE: max vCPU count
|
||||
MaxMem uint64 `json:"-" cbor:"11,keyasint,omitzero"` // PVE: max memory bytes
|
||||
Uptime uint64 `json:"-" cbor:"12,keyasint,omitzero"` // PVE: uptime in seconds
|
||||
Type string `json:"-" cbor:"13,keyasint,omitzero"` // PVE: resource type (e.g. "qemu" or "lxc")
|
||||
MaxCPU uint64 `json:"-" cbor:"10,keyasint,omitzero"` // PVE: max vCPU count
|
||||
MaxMem uint64 `json:"-" cbor:"11,keyasint,omitzero"` // PVE: max memory bytes
|
||||
Uptime uint64 `json:"-" cbor:"12,keyasint,omitzero"` // PVE: uptime in seconds
|
||||
Type string `json:"-" cbor:"13,keyasint,omitzero"` // PVE: resource type (e.g. "qemu" or "lxc")
|
||||
DiskRead uint64 `json:"-" cbor:"14,keyasint,omitzero"` // PVE: cumulative disk read bytes
|
||||
DiskWrite uint64 `json:"-" cbor:"15,keyasint,omitzero"` // PVE: cumulative disk write bytes
|
||||
Disk uint64 `json:"-" cbor:"16,keyasint,omitzero"` // PVE: allocated disk size in bytes
|
||||
NetOut uint64 `json:"-" cbor:"17,keyasint,omitzero"` // PVE: cumulative bytes sent by VM
|
||||
NetIn uint64 `json:"-" cbor:"18,keyasint,omitzero"` // PVE: cumulative bytes received by VM
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func createPVEVMRecords(app core.App, data []*container.PveNodeStats, systemId s
|
||||
valueStrings := make([]string, 0, len(data))
|
||||
for i, vm := range data {
|
||||
suffix := fmt.Sprintf("%d", i)
|
||||
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:type%[1]s}, {:cpu%[1]s}, {:mem%[1]s}, {:net%[1]s}, {:maxcpu%[1]s}, {:maxmem%[1]s}, {:uptime%[1]s}, {:updated})", suffix))
|
||||
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:type%[1]s}, {:cpu%[1]s}, {:mem%[1]s}, {:netout%[1]s}, {:netin%[1]s}, {:maxcpu%[1]s}, {:maxmem%[1]s}, {:uptime%[1]s}, {:diskread%[1]s}, {:diskwrite%[1]s}, {:disk%[1]s}, {:updated})", suffix))
|
||||
params["id"+suffix] = makeStableHashId(systemId, vm.Id)
|
||||
params["name"+suffix] = vm.Name
|
||||
params["type"+suffix] = vm.Type // "qemu" or "lxc"
|
||||
@@ -376,11 +376,14 @@ func createPVEVMRecords(app core.App, data []*container.PveNodeStats, systemId s
|
||||
params["maxcpu"+suffix] = vm.MaxCPU
|
||||
params["maxmem"+suffix] = vm.MaxMem
|
||||
params["uptime"+suffix] = vm.Uptime
|
||||
netBytes := vm.Bandwidth[0] + vm.Bandwidth[1]
|
||||
params["net"+suffix] = netBytes
|
||||
params["diskread"+suffix] = vm.DiskRead
|
||||
params["diskwrite"+suffix] = vm.DiskWrite
|
||||
params["disk"+suffix] = vm.Disk
|
||||
params["netout"+suffix] = vm.NetOut // cumulative bytes sent by VM
|
||||
params["netin"+suffix] = vm.NetIn // cumulative bytes received by VM
|
||||
}
|
||||
queryString := fmt.Sprintf(
|
||||
"INSERT INTO pve_vms (id, system, name, type, cpu, mem, net, maxcpu, maxmem, uptime, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system=excluded.system, name=excluded.name, type=excluded.type, cpu=excluded.cpu, mem=excluded.mem, net=excluded.net, maxcpu=excluded.maxcpu, maxmem=excluded.maxmem, uptime=excluded.uptime, updated=excluded.updated",
|
||||
"INSERT INTO pve_vms (id, system, name, type, cpu, mem, netout, netin, maxcpu, maxmem, uptime, diskread, diskwrite, disk, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system=excluded.system, name=excluded.name, type=excluded.type, cpu=excluded.cpu, mem=excluded.mem, netout=excluded.netout, netin=excluded.netin, maxcpu=excluded.maxcpu, maxmem=excluded.maxmem, uptime=excluded.uptime, diskread=excluded.diskread, diskwrite=excluded.diskwrite, disk=excluded.disk, updated=excluded.updated",
|
||||
strings.Join(valueStrings, ","),
|
||||
)
|
||||
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
|
||||
|
||||
@@ -1847,30 +1847,6 @@ func init() {
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "pve_vms_mem001",
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"name": "mem",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "pve_vms_net001",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "net",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number1253106325",
|
||||
@@ -1883,6 +1859,18 @@ func init() {
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "pve_vms_mem001",
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"name": "mem",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number1693954525",
|
||||
@@ -1895,6 +1883,66 @@ func init() {
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number208985346",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "disk",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number4125810518",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "diskread",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number752404475",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "diskwrite",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number1880667380",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "netout",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number2702533949",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "netin",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number1563400775",
|
||||
@@ -1932,7 +1980,6 @@ func init() {
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}
|
||||
|
||||
]`
|
||||
|
||||
err := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
|
||||
@@ -3,9 +3,9 @@ import { Button } from "@/components/ui/button"
|
||||
import { cn, decimalString, formatBytes, hourWithSeconds, toFixedFloat } from "@/lib/utils"
|
||||
import type { PveVmRecord } from "@/types"
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
ClockIcon,
|
||||
CpuIcon,
|
||||
HardDriveIcon,
|
||||
MemoryStickIcon,
|
||||
MonitorIcon,
|
||||
ServerIcon,
|
||||
@@ -42,7 +42,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
accessorFn: (record) => record.name,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={MonitorIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 xl:w-48 block truncate">{getValue() as string}</span>
|
||||
return <span className="ms-1 max-w-48 block truncate">{getValue() as string}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -57,7 +57,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const allSystems = useStore($allSystemsById)
|
||||
return <span className="ms-1.5 xl:w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
||||
return <span className="ms-1 max-w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -68,7 +68,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
cell: ({ getValue }) => {
|
||||
const type = getValue() as string
|
||||
return (
|
||||
<Badge variant="outline" className="dark:border-white/12 ms-1.5">
|
||||
<Badge variant="outline" className="dark:border-white/12 ms-1">
|
||||
{type}
|
||||
</Badge>
|
||||
)
|
||||
@@ -81,7 +81,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
return <span className="ms-1.5 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
|
||||
return <span className="ms-1 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -93,41 +93,86 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, false, undefined, true)
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "net",
|
||||
accessorFn: (record) => record.net,
|
||||
id: "maxmem",
|
||||
accessorFn: (record) => record.maxmem,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Max`} Icon={MemoryStickIcon} />,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Net`} Icon={EthernetIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
// maxmem is stored in bytes; convert to MB for formatBytes
|
||||
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
||||
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
accessorFn: (record) => record.disk,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Disk`} Icon={HardDriveIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
||||
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "diskread",
|
||||
accessorFn: (record) => record.diskread,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Read`} Icon={HardDriveIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, true, undefined, false)
|
||||
const formatted = formatBytes(val, false, undefined, false)
|
||||
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "diskwrite",
|
||||
accessorFn: (record) => record.diskwrite,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Write`} Icon={HardDriveIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, false, undefined, false)
|
||||
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "netin",
|
||||
accessorFn: (record) => record.netin,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Download`} Icon={EthernetIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, false, undefined, false)
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "netout",
|
||||
accessorFn: (record) => record.netout,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Upload`} Icon={EthernetIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, false, undefined, false)
|
||||
return (
|
||||
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "maxcpu",
|
||||
accessorFn: (record) => record.maxcpu,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`vCPUs`} Icon={CpuIcon} />,
|
||||
header: ({ column }) => <HeaderButton column={column} name="vCPUs" Icon={CpuIcon} />,
|
||||
invertSorting: true,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 tabular-nums">{getValue() as number}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "maxmem",
|
||||
accessorFn: (record) => record.maxmem,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Max Mem`} Icon={MemoryStickIcon} />,
|
||||
invertSorting: true,
|
||||
cell: ({ getValue }) => {
|
||||
// maxmem is stored in bytes; convert to MB for formatBytes
|
||||
const formatted = formatBytes(getValue() as number, false, undefined, false)
|
||||
return <span className="ms-1.5 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
|
||||
return <span className="ms-1 tabular-nums">{getValue() as number}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -136,7 +181,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Uptime`} Icon={TimerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 w-25 block truncate">{formatUptime(getValue() as number)}</span>
|
||||
return <span className="ms-1">{formatUptime(getValue() as number)}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,7 +191,7 @@ export const pveVmCols: ColumnDef<PveVmRecord>[] = [
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const timestamp = getValue() as number
|
||||
return <span className="ms-1.5 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
|
||||
return <span className="ms-1 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -164,7 +209,7 @@ function HeaderButton({ column, name, Icon }: { column: Column<PveVmRecord>; nam
|
||||
>
|
||||
{Icon && <Icon className="size-4" />}
|
||||
{name}
|
||||
<ArrowUpDownIcon className="size-4" />
|
||||
{/* <ArrowUpDownIcon className="size-4" /> */}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ export default function PveTable({ systemId }: { systemId?: string }) {
|
||||
function fetchData(systemId?: string) {
|
||||
pb.collection<PveVmRecord>("pve_vms")
|
||||
.getList(0, 2000, {
|
||||
fields: "id,name,type,cpu,mem,net,maxcpu,maxmem,uptime,system,updated",
|
||||
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
|
||||
})
|
||||
.then(({ items }) => {
|
||||
@@ -145,7 +144,7 @@ export default function PveTable({ systemId }: { systemId?: string }) {
|
||||
<div className="grid md:flex gap-5 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2">
|
||||
<Trans>All Proxmox VMs</Trans>
|
||||
<Trans>Proxmox Resources</Trans>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex">
|
||||
<Trans>CPU is percent of overall host CPU usage.</Trans>
|
||||
@@ -259,7 +258,11 @@ function PveVmSheet({
|
||||
|
||||
const memFormatted = formatBytes(vm.mem, false, undefined, true)
|
||||
const maxMemFormatted = formatBytes(vm.maxmem, false, undefined, false)
|
||||
const netFormatted = formatBytes(vm.net, true, undefined, false)
|
||||
const netoutFormatted = formatBytes(vm.netout, false, undefined, false)
|
||||
const netinFormatted = formatBytes(vm.netin, false, undefined, false)
|
||||
const diskReadFormatted = formatBytes(vm.diskread, false, undefined, false)
|
||||
const diskWriteFormatted = formatBytes(vm.diskwrite, false, undefined, false)
|
||||
const diskFormatted = formatBytes(vm.disk, false, undefined, false)
|
||||
|
||||
return (
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
@@ -294,9 +297,14 @@ function PveVmSheet({
|
||||
<dd className="tabular-nums">{`${decimalString(memFormatted.value, memFormatted.value >= 10 ? 1 : 2)} ${memFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Network</Trans>
|
||||
<Trans>Upload</Trans>
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(netFormatted.value, netFormatted.value >= 10 ? 1 : 2)} ${netFormatted.unit}`}</dd>
|
||||
<dd className="tabular-nums">{`${decimalString(netoutFormatted.value, netoutFormatted.value >= 10 ? 1 : 2)} ${netoutFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Download</Trans>
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(netinFormatted.value, netinFormatted.value >= 10 ? 1 : 2)} ${netinFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>vCPUs</Trans>
|
||||
@@ -308,6 +316,21 @@ function PveVmSheet({
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(maxMemFormatted.value, maxMemFormatted.value >= 10 ? 1 : 2)} ${maxMemFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Disk Read</Trans>
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(diskReadFormatted.value, diskReadFormatted.value >= 10 ? 1 : 2)} ${diskReadFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Disk Write</Trans>
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(diskWriteFormatted.value, diskWriteFormatted.value >= 10 ? 1 : 2)} ${diskWriteFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Disk Size</Trans>
|
||||
</dt>
|
||||
<dd className="tabular-nums">{`${decimalString(diskFormatted.value, diskFormatted.value >= 10 ? 1 : 2)} ${diskFormatted.unit}`}</dd>
|
||||
|
||||
<dt className="text-muted-foreground">
|
||||
<Trans>Uptime</Trans>
|
||||
</dt>
|
||||
|
||||
@@ -952,6 +952,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
<LazyContainersTable systemId={system.id} />
|
||||
)}
|
||||
|
||||
{pveData.length > 0 && <LazyPveTable systemId={system.id} />}
|
||||
|
||||
{isLinux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
|
||||
<LazySystemdTable systemId={system.id} />
|
||||
)}
|
||||
@@ -1126,6 +1128,17 @@ function LazyContainersTable({ systemId }: { systemId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const PveTable = lazy(() => import("../pve-table/pve-table"))
|
||||
|
||||
function LazyPveTable({ systemId }: { systemId: string }) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
|
||||
return (
|
||||
<div ref={ref} className={cn(isIntersecting && "contents")}>
|
||||
{isIntersecting && <PveTable systemId={systemId} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SmartTable = lazy(() => import("./system/smart-table"))
|
||||
|
||||
function LazySmartTable({ systemId }: { systemId: string }) {
|
||||
|
||||
238
internal/site/src/types.d.ts
vendored
238
internal/site/src/types.d.ts
vendored
@@ -285,14 +285,22 @@ export interface PveVmRecord extends RecordModel {
|
||||
cpu: number
|
||||
/** Memory used (MB) */
|
||||
mem: number
|
||||
/** Network bandwidth (bytes/s, combined send+recv) */
|
||||
net: number
|
||||
/** Total upload (bytes, sent by VM) */
|
||||
netout: number
|
||||
/** Total download (bytes, received by VM) */
|
||||
netin: number
|
||||
/** Max vCPU count */
|
||||
maxcpu: number
|
||||
/** Max memory (bytes) */
|
||||
maxmem: number
|
||||
/** Uptime (seconds) */
|
||||
uptime: number
|
||||
/** Cumulative disk read (bytes) */
|
||||
diskread: number
|
||||
/** Cumulative disk write (bytes) */
|
||||
diskwrite: number
|
||||
/** Allocated disk size (bytes) */
|
||||
disk: number
|
||||
/** Unix timestamp (ms) */
|
||||
updated: number
|
||||
}
|
||||
@@ -447,116 +455,116 @@ export interface SystemdRecord extends RecordModel {
|
||||
}
|
||||
|
||||
export interface SystemdServiceDetails {
|
||||
AccessSELinuxContext: string;
|
||||
ActivationDetails: any[];
|
||||
ActiveEnterTimestamp: number;
|
||||
ActiveEnterTimestampMonotonic: number;
|
||||
ActiveExitTimestamp: number;
|
||||
ActiveExitTimestampMonotonic: number;
|
||||
ActiveState: string;
|
||||
After: string[];
|
||||
AllowIsolate: boolean;
|
||||
AssertResult: boolean;
|
||||
AssertTimestamp: number;
|
||||
AssertTimestampMonotonic: number;
|
||||
Asserts: any[];
|
||||
Before: string[];
|
||||
BindsTo: any[];
|
||||
BoundBy: any[];
|
||||
CPUUsageNSec: number;
|
||||
CanClean: any[];
|
||||
CanFreeze: boolean;
|
||||
CanIsolate: boolean;
|
||||
CanLiveMount: boolean;
|
||||
CanReload: boolean;
|
||||
CanStart: boolean;
|
||||
CanStop: boolean;
|
||||
CollectMode: string;
|
||||
ConditionResult: boolean;
|
||||
ConditionTimestamp: number;
|
||||
ConditionTimestampMonotonic: number;
|
||||
Conditions: any[];
|
||||
ConflictedBy: any[];
|
||||
Conflicts: string[];
|
||||
ConsistsOf: any[];
|
||||
DebugInvocation: boolean;
|
||||
DefaultDependencies: boolean;
|
||||
Description: string;
|
||||
Documentation: string[];
|
||||
DropInPaths: any[];
|
||||
ExecMainPID: number;
|
||||
FailureAction: string;
|
||||
FailureActionExitStatus: number;
|
||||
Following: string;
|
||||
FragmentPath: string;
|
||||
FreezerState: string;
|
||||
Id: string;
|
||||
IgnoreOnIsolate: boolean;
|
||||
InactiveEnterTimestamp: number;
|
||||
InactiveEnterTimestampMonotonic: number;
|
||||
InactiveExitTimestamp: number;
|
||||
InactiveExitTimestampMonotonic: number;
|
||||
InvocationID: string;
|
||||
Job: Array<number | string>;
|
||||
JobRunningTimeoutUSec: number;
|
||||
JobTimeoutAction: string;
|
||||
JobTimeoutRebootArgument: string;
|
||||
JobTimeoutUSec: number;
|
||||
JoinsNamespaceOf: any[];
|
||||
LoadError: string[];
|
||||
LoadState: string;
|
||||
MainPID: number;
|
||||
Markers: any[];
|
||||
MemoryCurrent: number;
|
||||
MemoryLimit: number;
|
||||
MemoryPeak: number;
|
||||
NRestarts: number;
|
||||
Names: string[];
|
||||
NeedDaemonReload: boolean;
|
||||
OnFailure: any[];
|
||||
OnFailureJobMode: string;
|
||||
OnFailureOf: any[];
|
||||
OnSuccess: any[];
|
||||
OnSuccessJobMode: string;
|
||||
OnSuccessOf: any[];
|
||||
PartOf: any[];
|
||||
Perpetual: boolean;
|
||||
PropagatesReloadTo: any[];
|
||||
PropagatesStopTo: any[];
|
||||
RebootArgument: string;
|
||||
Refs: any[];
|
||||
RefuseManualStart: boolean;
|
||||
RefuseManualStop: boolean;
|
||||
ReloadPropagatedFrom: any[];
|
||||
RequiredBy: any[];
|
||||
Requires: string[];
|
||||
RequiresMountsFor: any[];
|
||||
Requisite: any[];
|
||||
RequisiteOf: any[];
|
||||
Result: string;
|
||||
SliceOf: any[];
|
||||
SourcePath: string;
|
||||
StartLimitAction: string;
|
||||
StartLimitBurst: number;
|
||||
StartLimitIntervalUSec: number;
|
||||
StateChangeTimestamp: number;
|
||||
StateChangeTimestampMonotonic: number;
|
||||
StopPropagatedFrom: any[];
|
||||
StopWhenUnneeded: boolean;
|
||||
SubState: string;
|
||||
SuccessAction: string;
|
||||
SuccessActionExitStatus: number;
|
||||
SurviveFinalKillSignal: boolean;
|
||||
TasksCurrent: number;
|
||||
TasksMax: number;
|
||||
Transient: boolean;
|
||||
TriggeredBy: string[];
|
||||
Triggers: any[];
|
||||
UnitFilePreset: string;
|
||||
UnitFileState: string;
|
||||
UpheldBy: any[];
|
||||
Upholds: any[];
|
||||
WantedBy: any[];
|
||||
Wants: string[];
|
||||
WantsMountsFor: any[];
|
||||
}
|
||||
AccessSELinuxContext: string
|
||||
ActivationDetails: any[]
|
||||
ActiveEnterTimestamp: number
|
||||
ActiveEnterTimestampMonotonic: number
|
||||
ActiveExitTimestamp: number
|
||||
ActiveExitTimestampMonotonic: number
|
||||
ActiveState: string
|
||||
After: string[]
|
||||
AllowIsolate: boolean
|
||||
AssertResult: boolean
|
||||
AssertTimestamp: number
|
||||
AssertTimestampMonotonic: number
|
||||
Asserts: any[]
|
||||
Before: string[]
|
||||
BindsTo: any[]
|
||||
BoundBy: any[]
|
||||
CPUUsageNSec: number
|
||||
CanClean: any[]
|
||||
CanFreeze: boolean
|
||||
CanIsolate: boolean
|
||||
CanLiveMount: boolean
|
||||
CanReload: boolean
|
||||
CanStart: boolean
|
||||
CanStop: boolean
|
||||
CollectMode: string
|
||||
ConditionResult: boolean
|
||||
ConditionTimestamp: number
|
||||
ConditionTimestampMonotonic: number
|
||||
Conditions: any[]
|
||||
ConflictedBy: any[]
|
||||
Conflicts: string[]
|
||||
ConsistsOf: any[]
|
||||
DebugInvocation: boolean
|
||||
DefaultDependencies: boolean
|
||||
Description: string
|
||||
Documentation: string[]
|
||||
DropInPaths: any[]
|
||||
ExecMainPID: number
|
||||
FailureAction: string
|
||||
FailureActionExitStatus: number
|
||||
Following: string
|
||||
FragmentPath: string
|
||||
FreezerState: string
|
||||
Id: string
|
||||
IgnoreOnIsolate: boolean
|
||||
InactiveEnterTimestamp: number
|
||||
InactiveEnterTimestampMonotonic: number
|
||||
InactiveExitTimestamp: number
|
||||
InactiveExitTimestampMonotonic: number
|
||||
InvocationID: string
|
||||
Job: Array<number | string>
|
||||
JobRunningTimeoutUSec: number
|
||||
JobTimeoutAction: string
|
||||
JobTimeoutRebootArgument: string
|
||||
JobTimeoutUSec: number
|
||||
JoinsNamespaceOf: any[]
|
||||
LoadError: string[]
|
||||
LoadState: string
|
||||
MainPID: number
|
||||
Markers: any[]
|
||||
MemoryCurrent: number
|
||||
MemoryLimit: number
|
||||
MemoryPeak: number
|
||||
NRestarts: number
|
||||
Names: string[]
|
||||
NeedDaemonReload: boolean
|
||||
OnFailure: any[]
|
||||
OnFailureJobMode: string
|
||||
OnFailureOf: any[]
|
||||
OnSuccess: any[]
|
||||
OnSuccessJobMode: string
|
||||
OnSuccessOf: any[]
|
||||
PartOf: any[]
|
||||
Perpetual: boolean
|
||||
PropagatesReloadTo: any[]
|
||||
PropagatesStopTo: any[]
|
||||
RebootArgument: string
|
||||
Refs: any[]
|
||||
RefuseManualStart: boolean
|
||||
RefuseManualStop: boolean
|
||||
ReloadPropagatedFrom: any[]
|
||||
RequiredBy: any[]
|
||||
Requires: string[]
|
||||
RequiresMountsFor: any[]
|
||||
Requisite: any[]
|
||||
RequisiteOf: any[]
|
||||
Result: string
|
||||
SliceOf: any[]
|
||||
SourcePath: string
|
||||
StartLimitAction: string
|
||||
StartLimitBurst: number
|
||||
StartLimitIntervalUSec: number
|
||||
StateChangeTimestamp: number
|
||||
StateChangeTimestampMonotonic: number
|
||||
StopPropagatedFrom: any[]
|
||||
StopWhenUnneeded: boolean
|
||||
SubState: string
|
||||
SuccessAction: string
|
||||
SuccessActionExitStatus: number
|
||||
SurviveFinalKillSignal: boolean
|
||||
TasksCurrent: number
|
||||
TasksMax: number
|
||||
Transient: boolean
|
||||
TriggeredBy: string[]
|
||||
Triggers: any[]
|
||||
UnitFilePreset: string
|
||||
UnitFileState: string
|
||||
UpheldBy: any[]
|
||||
Upholds: any[]
|
||||
WantedBy: any[]
|
||||
Wants: string[]
|
||||
WantsMountsFor: any[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user