Surface LXC interface IPs via PVE interfaces API (#596)
This commit is contained in:
parent
b95c01066e
commit
a885fb5472
7 changed files with 91 additions and 179 deletions
|
|
@ -414,8 +414,8 @@ func (noopPVEClient) GetContainerStatus(ctx context.Context, node string, vmid i
|
|||
func (noopPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (noopPVEClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
return "", nil
|
||||
func (noopPVEClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (noopPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||
return nil, nil
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ type PVEClientInterface interface {
|
|||
GetVMStatus(ctx context.Context, node string, vmid int) (*proxmox.VMStatus, error)
|
||||
GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error)
|
||||
GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error)
|
||||
ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error)
|
||||
GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error)
|
||||
GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error)
|
||||
IsClusterMember(ctx context.Context) (bool, error)
|
||||
GetVMFSInfo(ctx context.Context, node string, vmid int) ([]proxmox.VMFileSystem, error)
|
||||
|
|
@ -468,8 +468,6 @@ type Monitor struct {
|
|||
dockerCommandIndex map[string]string
|
||||
guestMetadataMu sync.RWMutex
|
||||
guestMetadataCache map[string]guestMetadataCacheEntry
|
||||
containerIPCacheMu sync.RWMutex
|
||||
containerIPCache map[string]containerIPCacheEntry
|
||||
executor PollExecutor
|
||||
breakerBaseRetry time.Duration
|
||||
breakerMaxDelay time.Duration
|
||||
|
|
@ -486,13 +484,6 @@ type rrdMemCacheEntry struct {
|
|||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
const containerIPCacheTTL = 90 * time.Second
|
||||
|
||||
type containerIPCacheEntry struct {
|
||||
addresses []string
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
// safePercentage calculates percentage safely, returning 0 if divisor is 0
|
||||
func safePercentage(used, total float64) float64 {
|
||||
if total == 0 {
|
||||
|
|
@ -1969,13 +1960,59 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
|||
}
|
||||
|
||||
if len(addressOrder) == 0 {
|
||||
dynamicIPs := m.lookupDynamicContainerIPs(ctx, client, instanceName, nodeName, container.VMID)
|
||||
if len(dynamicIPs) > 0 {
|
||||
for _, addr := range dynamicIPs {
|
||||
addAddress(addr)
|
||||
}
|
||||
if len(networkIfaces) == 1 && len(networkIfaces[0].Addresses) == 0 {
|
||||
networkIfaces[0].Addresses = dedupeStringsPreserveOrder(dynamicIPs)
|
||||
interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second)
|
||||
ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID)
|
||||
cancelInterfaces()
|
||||
if ifaceErr != nil {
|
||||
log.Debug().
|
||||
Err(ifaceErr).
|
||||
Str("instance", instanceName).
|
||||
Str("node", nodeName).
|
||||
Str("container", container.Name).
|
||||
Int("vmid", container.VMID).
|
||||
Msg("Container interface metadata unavailable")
|
||||
} else if len(ifaceDetails) > 0 {
|
||||
for _, detail := range ifaceDetails {
|
||||
parsed := containerNetworkDetails{}
|
||||
parsed.Name = strings.TrimSpace(detail.Name)
|
||||
parsed.MAC = strings.ToUpper(strings.TrimSpace(detail.HWAddr))
|
||||
|
||||
for _, addr := range detail.IPAddresses {
|
||||
stripped := strings.TrimSpace(addr.Address)
|
||||
if stripped == "" {
|
||||
continue
|
||||
}
|
||||
if slash := strings.Index(stripped, "/"); slash > 0 {
|
||||
stripped = stripped[:slash]
|
||||
}
|
||||
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
||||
}
|
||||
|
||||
if len(parsed.Addresses) == 0 && strings.TrimSpace(detail.Inet) != "" {
|
||||
parts := strings.Fields(detail.Inet)
|
||||
for _, part := range parts {
|
||||
stripped := strings.TrimSpace(part)
|
||||
if stripped == "" {
|
||||
continue
|
||||
}
|
||||
if slash := strings.Index(stripped, "/"); slash > 0 {
|
||||
stripped = stripped[:slash]
|
||||
}
|
||||
parsed.Addresses = append(parsed.Addresses, sanitizeGuestAddressStrings(stripped)...)
|
||||
}
|
||||
}
|
||||
|
||||
parsed.Addresses = dedupeStringsPreserveOrder(parsed.Addresses)
|
||||
|
||||
if len(parsed.Addresses) > 0 {
|
||||
for _, addr := range parsed.Addresses {
|
||||
addAddress(addr)
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.Name != "" || parsed.MAC != "" || len(parsed.Addresses) > 0 {
|
||||
mergeContainerNetworkInterface(&networkIfaces, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2353,114 +2390,6 @@ func extractContainerRootDeviceFromConfig(config map[string]interface{}) string
|
|||
return device
|
||||
}
|
||||
|
||||
func (m *Monitor) lookupDynamicContainerIPs(ctx context.Context, client PVEClientInterface, instanceName, nodeName string, vmid int) []string {
|
||||
if m == nil || client == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s|%s|%d", instanceName, nodeName, vmid)
|
||||
now := time.Now()
|
||||
|
||||
m.containerIPCacheMu.RLock()
|
||||
if entry, ok := m.containerIPCache[cacheKey]; ok && now.Sub(entry.fetchedAt) < containerIPCacheTTL {
|
||||
addresses := cloneStringSlice(entry.addresses)
|
||||
m.containerIPCacheMu.RUnlock()
|
||||
return addresses
|
||||
}
|
||||
m.containerIPCacheMu.RUnlock()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
output, err := client.ExecContainerCommand(execCtx, nodeName, vmid, []string{"ip", "-j", "addr", "show", "scope", "global"})
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Str("instance", instanceName).
|
||||
Str("node", nodeName).
|
||||
Int("vmid", vmid).
|
||||
Msg("Failed to execute container IP command")
|
||||
}
|
||||
|
||||
addresses := parseContainerIPsFromJSON(output)
|
||||
if len(addresses) == 0 {
|
||||
fallbackCtx, fallbackCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
fallbackOutput, fallbackErr := client.ExecContainerCommand(fallbackCtx, nodeName, vmid, []string{"hostname", "-I"})
|
||||
fallbackCancel()
|
||||
if fallbackErr != nil {
|
||||
log.Debug().
|
||||
Err(fallbackErr).
|
||||
Str("instance", instanceName).
|
||||
Str("node", nodeName).
|
||||
Int("vmid", vmid).
|
||||
Msg("Failed to execute hostname -I for container")
|
||||
} else {
|
||||
addresses = parseContainerIPsFromWhitespaceList(fallbackOutput)
|
||||
}
|
||||
}
|
||||
|
||||
addresses = dedupeStringsPreserveOrder(addresses)
|
||||
|
||||
m.containerIPCacheMu.Lock()
|
||||
m.containerIPCache[cacheKey] = containerIPCacheEntry{
|
||||
addresses: cloneStringSlice(addresses),
|
||||
fetchedAt: time.Now(),
|
||||
}
|
||||
m.containerIPCacheMu.Unlock()
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
func parseContainerIPsFromJSON(output string) []string {
|
||||
output = strings.TrimSpace(output)
|
||||
if output == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
type addrInfo struct {
|
||||
Local string `json:"local"`
|
||||
}
|
||||
type iface struct {
|
||||
Name string `json:"ifname"`
|
||||
AddrInfo []addrInfo `json:"addr_info"`
|
||||
}
|
||||
|
||||
var interfaces []iface
|
||||
if err := json.Unmarshal([]byte(output), &interfaces); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var addresses []string
|
||||
for _, network := range interfaces {
|
||||
for _, info := range network.AddrInfo {
|
||||
sanitized := sanitizeGuestAddressStrings(info.Local)
|
||||
if len(sanitized) > 0 {
|
||||
addresses = append(addresses, sanitized...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
func parseContainerIPsFromWhitespaceList(output string) []string {
|
||||
output = strings.TrimSpace(output)
|
||||
if output == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := strings.Fields(output)
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var addresses []string
|
||||
for _, field := range fields {
|
||||
addresses = append(addresses, sanitizeGuestAddressStrings(field)...)
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
// GetConnectionStatuses returns the current connection status for all nodes
|
||||
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||
if mock.IsMockEnabled() {
|
||||
|
|
@ -2661,7 +2590,6 @@ func New(cfg *config.Config) (*Monitor, error) {
|
|||
dockerCommands: make(map[string]*dockerHostCommand),
|
||||
dockerCommandIndex: make(map[string]string),
|
||||
guestMetadataCache: make(map[string]guestMetadataCacheEntry),
|
||||
containerIPCache: make(map[string]containerIPCacheEntry),
|
||||
instanceInfoCache: make(map[string]*instanceInfo),
|
||||
pollStatusMap: make(map[string]*pollStatus),
|
||||
dlqInsightMap: make(map[string]*dlqInsight),
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ func (s *stubPVEClient) GetContainerStatus(ctx context.Context, node string, vmi
|
|||
func (s *stubPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *stubPVEClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
return "", nil
|
||||
func (s *stubPVEClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *stubPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ func (f fakeSnapshotClient) GetContainerStatus(ctx context.Context, node string,
|
|||
func (f fakeSnapshotClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeSnapshotClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
return "", nil
|
||||
func (f fakeSnapshotClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeSnapshotClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||
return nil, nil
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ func (f *fakeStorageClient) GetContainerStatus(ctx context.Context, node string,
|
|||
func (f *fakeStorageClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeStorageClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
return "", nil
|
||||
func (f *fakeStorageClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeStorageClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||
|
|
|
|||
|
|
@ -785,6 +785,21 @@ type ContainerDiskUsage struct {
|
|||
Used uint64 `json:"used,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerInterfaceAddress describes an IP entry associated with a container interface.
|
||||
type ContainerInterfaceAddress struct {
|
||||
Address string `json:"ip-address"`
|
||||
Type string `json:"ip-address-type"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// ContainerInterface describes a container network interface returned by Proxmox.
|
||||
type ContainerInterface struct {
|
||||
Name string `json:"name"`
|
||||
HWAddr string `json:"hwaddr"`
|
||||
Inet string `json:"inet,omitempty"`
|
||||
IPAddresses []ContainerInterfaceAddress `json:"ip-addresses,omitempty"`
|
||||
}
|
||||
|
||||
// GetContainerConfig returns the configuration of a specific container
|
||||
func (c *Client) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||
resp, err := c.get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/config", node, vmid))
|
||||
|
|
@ -1095,59 +1110,28 @@ func (c *Client) getTaskLog(ctx context.Context, node, upid string) ([]string, e
|
|||
return lines, nil
|
||||
}
|
||||
|
||||
// ExecContainerCommand executes a command within an LXC container and returns the stdout output.
|
||||
func (c *Client) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
if len(command) == 0 {
|
||||
return "", fmt.Errorf("command is required")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("command", command[0])
|
||||
for _, arg := range command[1:] {
|
||||
params.Add("extra-args", arg)
|
||||
}
|
||||
|
||||
resp, err := c.post(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/exec", node, vmid), params)
|
||||
// GetContainerInterfaces returns the network interfaces (with IPs) for a container.
|
||||
func (c *Client) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]ContainerInterface, error) {
|
||||
resp, err := c.get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/interfaces", node, vmid))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("failed to execute container command (status %d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
return nil, fmt.Errorf("failed to get container interfaces (status %d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
UPID string `json:"upid"`
|
||||
} `json:"data"`
|
||||
Data []ContainerInterface `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Data.UPID == "" {
|
||||
return "", fmt.Errorf("exec call did not return a UPID")
|
||||
}
|
||||
|
||||
status, err := c.waitForTaskCompletion(ctx, node, result.Data.UPID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if status != nil && status.ExitStatus != "" && !strings.EqualFold(status.ExitStatus, "ok") {
|
||||
return "", fmt.Errorf("container command failed: %s", status.ExitStatus)
|
||||
}
|
||||
|
||||
lines, err := c.getTaskLog(ctx, node, result.Data.UPID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := strings.Join(lines, "\n")
|
||||
return strings.TrimSpace(output), nil
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetStorageContent returns the content of a specific storage
|
||||
|
|
|
|||
|
|
@ -1017,18 +1017,18 @@ func (cc *ClusterClient) GetContainerConfig(ctx context.Context, node string, vm
|
|||
return result, err
|
||||
}
|
||||
|
||||
// ExecContainerCommand executes a command within an LXC container using cluster failover
|
||||
func (cc *ClusterClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
||||
var output string
|
||||
// GetContainerInterfaces returns interface details for a container
|
||||
func (cc *ClusterClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]ContainerInterface, error) {
|
||||
var result []ContainerInterface
|
||||
err := cc.executeWithFailover(ctx, func(client *Client) error {
|
||||
result, err := client.ExecContainerCommand(ctx, node, vmid, command)
|
||||
interfaces, err := client.GetContainerInterfaces(ctx, node, vmid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output = result
|
||||
result = interfaces
|
||||
return nil
|
||||
})
|
||||
return output, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IsClusterMember checks if this node is part of a cluster
|
||||
|
|
|
|||
Loading…
Reference in a new issue