feat(proxmox): enhance VM resource tracking with auto-discovery and cached IPs

- Add VMResource wrapper type with cached IP addresses for efficient lookups
- Implement concurrent IP fetching during resource updates (limited concurrency)
- Add ReverseLookupResource for discovering VMs by IP, hostname, or alias
- Prioritize interfaces API over config for IP retrieval (offline container fallback)
- Enable routes to auto-discover Proxmox resources when no explicit config provided
- Fix configuration type from value to pointer slice for correct proxmox client retrievel
- Ensure Proxmox providers are initialized before route validation
This commit is contained in:
yusing
2026-01-25 02:25:07 +08:00
parent b4646b665f
commit cdd1353102
9 changed files with 145 additions and 21 deletions

View File

@@ -4,12 +4,17 @@ import (
"context"
"errors"
"fmt"
"net"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"github.com/bytedance/sonic"
"github.com/luthermonson/go-proxmox"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
)
type Client struct {
@@ -17,10 +22,15 @@ type Client struct {
*proxmox.Cluster
Version *proxmox.Version
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
resources map[string]*proxmox.ClusterResource
resources map[string]*VMResource
resourcesMu sync.RWMutex
}
type VMResource struct {
*proxmox.ClusterResource
IPs []net.IP
}
var (
ErrResourceNotFound = errors.New("resource not found")
ErrNoResources = errors.New("no resources")
@@ -29,7 +39,7 @@ var (
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
return &Client{
Client: proxmox.NewClient(baseUrl, opts...),
resources: make(map[string]*proxmox.ClusterResource),
resources: make(map[string]*VMResource),
}
}
@@ -62,8 +72,36 @@ func (c *Client) UpdateResources(ctx context.Context) error {
return err
}
clear(c.resources)
var errs errgroup.Group
errs.SetLimit(runtime.GOMAXPROCS(0) * 2)
for _, resource := range resourcesSlice {
c.resources[resource.ID] = resource
c.resources[resource.ID] = &VMResource{
ClusterResource: resource,
IPs: nil,
}
errs.Go(func() error {
node, ok := Nodes.Get(resource.Node)
if !ok {
return fmt.Errorf("node %s not found", resource.Node)
}
vmid, ok := strings.CutPrefix(resource.ID, "lxc/")
if !ok {
return nil // not a lxc resource
}
vmidInt, err := strconv.Atoi(vmid)
if err != nil {
return fmt.Errorf("invalid resource id %s: %w", resource.ID, err)
}
ips, err := node.LXCGetIPs(ctx, vmidInt)
if err != nil {
return fmt.Errorf("failed to get ips for resource %s: %w", resource.ID, err)
}
c.resources[resource.ID].IPs = ips
return nil
})
}
if err := errs.Wait(); err != nil {
return err
}
log.Debug().Str("cluster", c.Cluster.Name).Msgf("[proxmox] updated %d resources", len(c.resources))
return nil
@@ -72,7 +110,7 @@ func (c *Client) UpdateResources(ctx context.Context) error {
// GetResource gets a resource by kind and id.
// kind: lxc or qemu
// id: <vmid>
func (c *Client) GetResource(kind string, id int) (*proxmox.ClusterResource, error) {
func (c *Client) GetResource(kind string, id int) (*VMResource, error) {
c.resourcesMu.RLock()
defer c.resourcesMu.RUnlock()
resource, ok := c.resources[kind+"/"+strconv.Itoa(id)]
@@ -82,6 +120,33 @@ func (c *Client) GetResource(kind string, id int) (*proxmox.ClusterResource, err
return resource, nil
}
// ReverseLookupResource looks up a resource by ip address, hostname, alias or all of them
func (c *Client) ReverseLookupResource(ip net.IP, hostname string, alias string) (*VMResource, error) {
c.resourcesMu.RLock()
defer c.resourcesMu.RUnlock()
shouldCheckIP := ip != nil && !ip.IsLoopback() && !ip.IsUnspecified()
shouldCheckHostname := hostname != ""
shouldCheckAlias := alias != ""
if shouldCheckHostname {
hostname, _, _ = strings.Cut(hostname, ".")
}
for _, resource := range c.resources {
if shouldCheckIP && slices.ContainsFunc(resource.IPs, func(a net.IP) bool { return a.Equal(ip) }) {
return resource, nil
}
if shouldCheckHostname && resource.Name == hostname {
return resource, nil
}
if shouldCheckAlias && resource.Name == alias {
return resource, nil
}
}
return nil, ErrResourceNotFound
}
// Key implements pool.Object
func (c *Client) Key() string {
return c.Cluster.ID

View File

@@ -170,17 +170,17 @@ func getIPFromNet(s string) (res []net.IP) { // name:...,bridge:...,gw=..,ip=...
}
// LXCGetIPs returns the ip addresses of the container
// it first tries to get the ip addresses from the config
// if that fails, it gets the ip addresses from the interfaces
// it first tries to get the ip addresses from the interfaces
// if that fails, it gets the ip addresses from the config (offline containers)
func (n *Node) LXCGetIPs(ctx context.Context, vmid int) (res []net.IP, err error) {
ips, err := n.LXCGetIPsFromConfig(ctx, vmid)
ips, err := n.LXCGetIPsFromInterfaces(ctx, vmid)
if err != nil {
return nil, err
}
if len(ips) > 0 {
return ips, nil
}
ips, err = n.LXCGetIPsFromInterfaces(ctx, vmid)
ips, err = n.LXCGetIPsFromConfig(ctx, vmid)
if err != nil {
return nil, err
}

View File

@@ -7,8 +7,6 @@ import (
"io"
"strings"
"time"
"github.com/luthermonson/go-proxmox"
)
// const statsScriptLocation = "/tmp/godoxy-stats.sh"
@@ -105,7 +103,7 @@ func (n *Node) LXCStats(ctx context.Context, vmid int, stream bool) (io.ReadClos
return pr, nil
}
func writeLXCStatsLine(resource *proxmox.ClusterResource, w io.Writer) error {
func writeLXCStatsLine(resource *VMResource, w io.Writer) error {
cpu := fmt.Sprintf("%.1f%%", resource.CPU*100)
memUsage := formatIECBytes(resource.Mem)

View File

@@ -12,6 +12,7 @@ import (
type NodeConfig struct {
Node string `json:"node" validate:"required"`
VMID int `json:"vmid" validate:"required"`
VMName string `json:"vmname,omitempty"`
Service string `json:"service,omitempty"`
} // @name ProxmoxNodeConfig
@@ -54,6 +55,10 @@ func (n *Node) Name() string {
return n.name
}
func (n *Node) Client() *Client {
return n.client
}
func (n *Node) String() string {
return fmt.Sprintf("%s (%s)", n.name, n.id)
}