mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 09:18:51 +02:00
feat: idle sleep for proxmox LXCs
This commit is contained in:
68
internal/proxmox/client.go
Normal file
68
internal/proxmox/client.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/yusing/go-proxy/internal/utils/pool"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*proxmox.Client
|
||||
proxmox.Cluster
|
||||
Version *proxmox.Version
|
||||
}
|
||||
|
||||
var Clients = pool.New[*Client]("proxmox_clients")
|
||||
|
||||
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
|
||||
return &Client{Client: proxmox.NewClient(baseUrl, opts...)}
|
||||
}
|
||||
|
||||
func (c *Client) UpdateClusterInfo(ctx context.Context) (err error) {
|
||||
c.Version, err = c.Client.Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// requires (/, Sys.Audit)
|
||||
if err := c.Get(ctx, "/cluster/status", &c.Cluster); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range c.Cluster.Nodes {
|
||||
Nodes.Add(&Node{name: node.Name, id: node.ID, client: c.Client})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Key implements pool.Object
|
||||
func (c *Client) Key() string {
|
||||
return c.Cluster.ID
|
||||
}
|
||||
|
||||
// Name implements pool.Object
|
||||
func (c *Client) Name() string {
|
||||
return c.Cluster.Name
|
||||
}
|
||||
|
||||
// MarshalMap implements pool.Object
|
||||
func (c *Client) MarshalMap() map[string]any {
|
||||
return map[string]any{
|
||||
"version": c.Version,
|
||||
"cluster": map[string]any{
|
||||
"name": c.Cluster.Name,
|
||||
"id": c.Cluster.ID,
|
||||
"version": c.Cluster.Version,
|
||||
"nodes": c.Cluster.Nodes,
|
||||
"quorate": c.Cluster.Quorate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) NumNodes() int {
|
||||
return len(c.Cluster.Nodes)
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
return fmt.Sprintf("%s (%s)", c.Cluster.Name, c.Cluster.ID)
|
||||
}
|
||||
69
internal/proxmox/config.go
Normal file
69
internal/proxmox/config.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
URL string `json:"url" yaml:"url" validate:"required,url"`
|
||||
|
||||
TokenID string `json:"token_id" yaml:"token_id" validate:"required"`
|
||||
Secret string `json:"secret" yaml:"token_secret" validate:"required"`
|
||||
|
||||
NoTLSVerify bool `json:"no_tls_verify" yaml:"no_tls_verify,omitempty"`
|
||||
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *Config) Client() *Client {
|
||||
if c.client == nil {
|
||||
panic("proxmox client accessed before init")
|
||||
}
|
||||
return c.client
|
||||
}
|
||||
|
||||
func (c *Config) Init() gperr.Error {
|
||||
var tr *http.Transport
|
||||
if c.NoTLSVerify {
|
||||
tr = gphttp.NewTransportWithTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
} else {
|
||||
tr = gphttp.NewTransport()
|
||||
}
|
||||
|
||||
if strings.HasSuffix(c.URL, "/") {
|
||||
c.URL = c.URL[:len(c.URL)-1]
|
||||
}
|
||||
if !strings.HasSuffix(c.URL, "/api2/json") {
|
||||
c.URL += "/api2/json"
|
||||
}
|
||||
|
||||
opts := []proxmox.Option{
|
||||
proxmox.WithAPIToken(c.TokenID, c.Secret),
|
||||
proxmox.WithHTTPClient(&http.Client{
|
||||
Transport: tr,
|
||||
}),
|
||||
}
|
||||
c.client = NewClient(c.URL, opts...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := c.client.UpdateClusterInfo(ctx); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return gperr.New("timeout fetching proxmox cluster info")
|
||||
}
|
||||
return gperr.New("failed to fetch proxmox cluster info").With(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
239
internal/proxmox/lxc.go
Normal file
239
internal/proxmox/lxc.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
)
|
||||
|
||||
type (
|
||||
LXCAction string
|
||||
LXCStatus string
|
||||
|
||||
statusOnly struct {
|
||||
Status LXCStatus `json:"status"`
|
||||
}
|
||||
nameOnly struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
LXCStart LXCAction = "start"
|
||||
LXCShutdown LXCAction = "shutdown"
|
||||
LXCSuspend LXCAction = "suspend"
|
||||
LXCResume LXCAction = "resume"
|
||||
LXCReboot LXCAction = "reboot"
|
||||
)
|
||||
|
||||
const (
|
||||
LXCStatusRunning LXCStatus = "running"
|
||||
LXCStatusStopped LXCStatus = "stopped"
|
||||
LXCStatusSuspended LXCStatus = "suspended" // placeholder, suspending lxc is experimental and the enum is undocumented
|
||||
)
|
||||
|
||||
const (
|
||||
proxmoxReqTimeout = 3 * time.Second
|
||||
proxmoxTaskCheckInterval = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
func (n *Node) LXCAction(ctx context.Context, vmid int, action LXCAction) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, proxmoxReqTimeout)
|
||||
defer cancel()
|
||||
|
||||
var upid proxmox.UPID
|
||||
if err := n.client.Post(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/status/%s", n.name, vmid, action), nil, &upid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task := proxmox.NewTask(upid, n.client)
|
||||
checkTicker := time.NewTicker(proxmoxTaskCheckInterval)
|
||||
defer checkTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-checkTicker.C:
|
||||
if err := task.Ping(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status != proxmox.TaskRunning {
|
||||
status, err := n.LXCStatus(ctx, vmid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case LXCStatusRunning:
|
||||
if action == LXCStart {
|
||||
return nil
|
||||
}
|
||||
case LXCStatusStopped:
|
||||
if action == LXCShutdown {
|
||||
return nil
|
||||
}
|
||||
case LXCStatusSuspended:
|
||||
if action == LXCSuspend {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) LXCName(ctx context.Context, vmid int) (string, error) {
|
||||
var name nameOnly
|
||||
if err := n.client.Get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/status/current", n.name, vmid), &name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return name.Name, nil
|
||||
}
|
||||
|
||||
func (n *Node) LXCStatus(ctx context.Context, vmid int) (LXCStatus, error) {
|
||||
var status statusOnly
|
||||
if err := n.client.Get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/status/current", n.name, vmid), &status); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return status.Status, nil
|
||||
}
|
||||
|
||||
func (n *Node) LXCIsRunning(ctx context.Context, vmid int) (bool, error) {
|
||||
status, err := n.LXCStatus(ctx, vmid)
|
||||
return status == LXCStatusRunning, err
|
||||
}
|
||||
|
||||
func (n *Node) LXCIsStopped(ctx context.Context, vmid int) (bool, error) {
|
||||
status, err := n.LXCStatus(ctx, vmid)
|
||||
return status == LXCStatusStopped, err
|
||||
}
|
||||
|
||||
func (n *Node) LXCSetShutdownTimeout(ctx context.Context, vmid int, timeout time.Duration) error {
|
||||
return n.client.Put(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/config", n.name, vmid), map[string]interface{}{
|
||||
"startup": fmt.Sprintf("down=%.0f", timeout.Seconds()),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func parseCIDR(s string) net.IP {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
ip, _, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return checkIPPrivate(ip)
|
||||
}
|
||||
|
||||
func checkIPPrivate(ip net.IP) net.IP {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
if ip.IsPrivate() {
|
||||
return ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIPFromNet(s string) (res []net.IP) { // name:...,bridge:...,gw=..,ip=...,ip6=...
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
var i4, i6 net.IP
|
||||
cidrIndex := strings.Index(s, "ip=")
|
||||
if cidrIndex != -1 {
|
||||
cidrIndex += 3
|
||||
slash := strings.Index(s[cidrIndex:], "/")
|
||||
if slash != -1 {
|
||||
i4 = checkIPPrivate(net.ParseIP(s[cidrIndex : cidrIndex+slash]))
|
||||
} else {
|
||||
i4 = checkIPPrivate(net.ParseIP(s[cidrIndex:]))
|
||||
}
|
||||
}
|
||||
cidr6Index := strings.Index(s, "ip6=")
|
||||
if cidr6Index != -1 {
|
||||
cidr6Index += 4
|
||||
slash := strings.Index(s[cidr6Index:], "/")
|
||||
if slash != -1 {
|
||||
i6 = checkIPPrivate(net.ParseIP(s[cidr6Index : cidr6Index+slash]))
|
||||
} else {
|
||||
i6 = checkIPPrivate(net.ParseIP(s[cidr6Index:]))
|
||||
}
|
||||
}
|
||||
if i4 != nil {
|
||||
res = append(res, i4)
|
||||
}
|
||||
if i6 != nil {
|
||||
res = append(res, i6)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 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
|
||||
func (n *Node) LXCGetIPs(ctx context.Context, vmid int) (res []net.IP, err error) {
|
||||
ips, err := n.LXCGetIPsFromConfig(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
ips, err = n.LXCGetIPsFromInterfaces(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// LXCGetIPsFromConfig returns the ip addresses of the container from the config
|
||||
func (n *Node) LXCGetIPsFromConfig(ctx context.Context, vmid int) (res []net.IP, err error) {
|
||||
type Config struct {
|
||||
Net0 string `json:"net0"`
|
||||
Net1 string `json:"net1"`
|
||||
Net2 string `json:"net2"`
|
||||
}
|
||||
var cfg Config
|
||||
if err := n.client.Get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/config", n.name, vmid), &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, getIPFromNet(cfg.Net0)...)
|
||||
res = append(res, getIPFromNet(cfg.Net1)...)
|
||||
res = append(res, getIPFromNet(cfg.Net2)...)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// LXCGetIPsFromInterfaces returns the ip addresses of the container from the interfaces
|
||||
// it will return nothing if the container is stopped
|
||||
func (n *Node) LXCGetIPsFromInterfaces(ctx context.Context, vmid int) ([]net.IP, error) {
|
||||
type Interface struct {
|
||||
IPv4 string `json:"inet"`
|
||||
IPv6 string `json:"inet6"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var res []Interface
|
||||
if err := n.client.Get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/interfaces", n.name, vmid), &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips := make([]net.IP, 0)
|
||||
for _, ip := range res {
|
||||
if ip.Name == "lo" ||
|
||||
strings.HasPrefix(ip.Name, "br-") ||
|
||||
strings.HasPrefix(ip.Name, "veth") ||
|
||||
strings.HasPrefix(ip.Name, "docker") {
|
||||
continue
|
||||
}
|
||||
if ip := parseCIDR(ip.IPv4); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
if ip := parseCIDR(ip.IPv6); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
40
internal/proxmox/lxc_test.go
Normal file
40
internal/proxmox/lxc_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetIPFromNet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want []net.IP
|
||||
}{
|
||||
{
|
||||
name: "ipv4 only",
|
||||
input: "name=eth0,bridge=vmbr0,gw=10.0.0.1,hwaddr=BC:24:11:10:88:97,ip=10.0.6.68/16,type=veth",
|
||||
want: []net.IP{net.ParseIP("10.0.6.68")},
|
||||
},
|
||||
{
|
||||
name: "ipv6 only, at the end",
|
||||
input: "name=eth0,bridge=vmbr0,hwaddr=BC:24:11:10:88:97,gw=::ffff:a00:1,type=veth,ip6=::ffff:a00:644/48",
|
||||
want: []net.IP{net.ParseIP("::ffff:a00:644")},
|
||||
},
|
||||
{
|
||||
name: "both",
|
||||
input: "name=eth0,bridge=vmbr0,hwaddr=BC:24:11:10:88:97,gw=::ffff:a00:1,type=veth,ip6=::ffff:a00:644/48,ip=10.0.6.68/16",
|
||||
want: []net.IP{net.ParseIP("10.0.6.68"), net.ParseIP("::ffff:a00:644")},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := getIPFromNet(tc.input)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("getIPFromNet(%q) = %s, want %s", tc.name, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
internal/proxmox/node.go
Normal file
50
internal/proxmox/node.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/yusing/go-proxy/internal/utils/pool"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
name string
|
||||
id string // likely node/<name>
|
||||
client *proxmox.Client
|
||||
}
|
||||
|
||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
|
||||
func AvailableNodeNames() string {
|
||||
var sb strings.Builder
|
||||
for _, node := range Nodes.Iter {
|
||||
sb.WriteString(node.name)
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
return sb.String()[:sb.Len()-2]
|
||||
}
|
||||
|
||||
func (n *Node) Key() string {
|
||||
return n.name
|
||||
}
|
||||
|
||||
func (n *Node) Name() string {
|
||||
return n.name
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
return fmt.Sprintf("%s (%s)", n.name, n.id)
|
||||
}
|
||||
|
||||
func (n *Node) MarshalMap() map[string]any {
|
||||
return map[string]any{
|
||||
"name": n.name,
|
||||
"id": n.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) Get(ctx context.Context, path string, v any) error {
|
||||
return n.client.Get(ctx, path, v)
|
||||
}
|
||||
Reference in New Issue
Block a user