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) {
|
func (noopPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (noopPVEClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (noopPVEClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (noopPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
func (noopPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ type PVEClientInterface interface {
|
||||||
GetVMStatus(ctx context.Context, node string, vmid int) (*proxmox.VMStatus, error)
|
GetVMStatus(ctx context.Context, node string, vmid int) (*proxmox.VMStatus, error)
|
||||||
GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error)
|
GetContainerStatus(ctx context.Context, node string, vmid int) (*proxmox.Container, error)
|
||||||
GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, 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)
|
GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error)
|
||||||
IsClusterMember(ctx context.Context) (bool, error)
|
IsClusterMember(ctx context.Context) (bool, error)
|
||||||
GetVMFSInfo(ctx context.Context, node string, vmid int) ([]proxmox.VMFileSystem, error)
|
GetVMFSInfo(ctx context.Context, node string, vmid int) ([]proxmox.VMFileSystem, error)
|
||||||
|
|
@ -468,8 +468,6 @@ type Monitor struct {
|
||||||
dockerCommandIndex map[string]string
|
dockerCommandIndex map[string]string
|
||||||
guestMetadataMu sync.RWMutex
|
guestMetadataMu sync.RWMutex
|
||||||
guestMetadataCache map[string]guestMetadataCacheEntry
|
guestMetadataCache map[string]guestMetadataCacheEntry
|
||||||
containerIPCacheMu sync.RWMutex
|
|
||||||
containerIPCache map[string]containerIPCacheEntry
|
|
||||||
executor PollExecutor
|
executor PollExecutor
|
||||||
breakerBaseRetry time.Duration
|
breakerBaseRetry time.Duration
|
||||||
breakerMaxDelay time.Duration
|
breakerMaxDelay time.Duration
|
||||||
|
|
@ -486,13 +484,6 @@ type rrdMemCacheEntry struct {
|
||||||
fetchedAt time.Time
|
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
|
// safePercentage calculates percentage safely, returning 0 if divisor is 0
|
||||||
func safePercentage(used, total float64) float64 {
|
func safePercentage(used, total float64) float64 {
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
|
|
@ -1969,13 +1960,59 @@ func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientI
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(addressOrder) == 0 {
|
if len(addressOrder) == 0 {
|
||||||
dynamicIPs := m.lookupDynamicContainerIPs(ctx, client, instanceName, nodeName, container.VMID)
|
interfacesCtx, cancelInterfaces := context.WithTimeout(ctx, 5*time.Second)
|
||||||
if len(dynamicIPs) > 0 {
|
ifaceDetails, ifaceErr := client.GetContainerInterfaces(interfacesCtx, nodeName, container.VMID)
|
||||||
for _, addr := range dynamicIPs {
|
cancelInterfaces()
|
||||||
addAddress(addr)
|
if ifaceErr != nil {
|
||||||
}
|
log.Debug().
|
||||||
if len(networkIfaces) == 1 && len(networkIfaces[0].Addresses) == 0 {
|
Err(ifaceErr).
|
||||||
networkIfaces[0].Addresses = dedupeStringsPreserveOrder(dynamicIPs)
|
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
|
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
|
// GetConnectionStatuses returns the current connection status for all nodes
|
||||||
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||||
if mock.IsMockEnabled() {
|
if mock.IsMockEnabled() {
|
||||||
|
|
@ -2661,7 +2590,6 @@ func New(cfg *config.Config) (*Monitor, error) {
|
||||||
dockerCommands: make(map[string]*dockerHostCommand),
|
dockerCommands: make(map[string]*dockerHostCommand),
|
||||||
dockerCommandIndex: make(map[string]string),
|
dockerCommandIndex: make(map[string]string),
|
||||||
guestMetadataCache: make(map[string]guestMetadataCacheEntry),
|
guestMetadataCache: make(map[string]guestMetadataCacheEntry),
|
||||||
containerIPCache: make(map[string]containerIPCacheEntry),
|
|
||||||
instanceInfoCache: make(map[string]*instanceInfo),
|
instanceInfoCache: make(map[string]*instanceInfo),
|
||||||
pollStatusMap: make(map[string]*pollStatus),
|
pollStatusMap: make(map[string]*pollStatus),
|
||||||
dlqInsightMap: make(map[string]*dlqInsight),
|
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) {
|
func (s *stubPVEClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (s *stubPVEClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (s *stubPVEClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubPVEClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
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) {
|
func (f fakeSnapshotClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (f fakeSnapshotClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (f fakeSnapshotClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (f fakeSnapshotClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
func (f fakeSnapshotClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||||
return nil, nil
|
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) {
|
func (f *fakeStorageClient) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (f *fakeStorageClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (f *fakeStorageClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]proxmox.ContainerInterface, error) {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeStorageClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
func (f *fakeStorageClient) GetClusterResources(ctx context.Context, resourceType string) ([]proxmox.ClusterResource, error) {
|
||||||
|
|
|
||||||
|
|
@ -785,6 +785,21 @@ type ContainerDiskUsage struct {
|
||||||
Used uint64 `json:"used,omitempty"`
|
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
|
// GetContainerConfig returns the configuration of a specific container
|
||||||
func (c *Client) GetContainerConfig(ctx context.Context, node string, vmid int) (map[string]interface{}, error) {
|
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))
|
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
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecContainerCommand executes a command within an LXC container and returns the stdout output.
|
// GetContainerInterfaces returns the network interfaces (with IPs) for a container.
|
||||||
func (c *Client) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (c *Client) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]ContainerInterface, error) {
|
||||||
if len(command) == 0 {
|
resp, err := c.get(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/interfaces", node, vmid))
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
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 {
|
var result struct {
|
||||||
Data struct {
|
Data []ContainerInterface `json:"data"`
|
||||||
UPID string `json:"upid"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Data.UPID == "" {
|
return result.Data, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStorageContent returns the content of a specific storage
|
// 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
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecContainerCommand executes a command within an LXC container using cluster failover
|
// GetContainerInterfaces returns interface details for a container
|
||||||
func (cc *ClusterClient) ExecContainerCommand(ctx context.Context, node string, vmid int, command []string) (string, error) {
|
func (cc *ClusterClient) GetContainerInterfaces(ctx context.Context, node string, vmid int) ([]ContainerInterface, error) {
|
||||||
var output string
|
var result []ContainerInterface
|
||||||
err := cc.executeWithFailover(ctx, func(client *Client) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
output = result
|
result = interfaces
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return output, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsClusterMember checks if this node is part of a cluster
|
// IsClusterMember checks if this node is part of a cluster
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue