Skip to content

Commit c307c36

Browse files
committed
feat(kubevirt): add vm_guest_info tool for QEMU guest agent access
Add new vm_guest_info tool that enables querying information from inside VirtualMachines using the QEMU guest agent, without requiring SSH access or credentials. The tool supports querying: - os: Operating system information (name, version, kernel, hostname) - filesystem: Mounted filesystems and disk usage - network: Network interfaces and IP addresses - users: Currently logged-in users and sessions This provides a secure way to gather runtime information from VMs for monitoring, troubleshooting, and compliance purposes. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Ben Oukhanov <boukhanov@redhat.com>
1 parent 9ab0efd commit c307c36

10 files changed

Lines changed: 1181 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,11 @@ In case multi-cluster support is enabled (default) and you have access to multip
514514
- `storage` (`string`) - Optional storage size for the VM's root disk when using DataSources (e.g., '30Gi', '50Gi', '100Gi'). Defaults to 30Gi. Ignored when using container disks.
515515
- `workload` (`string`) - The workload for the VM. Accepts OS names (e.g., 'fedora' (default), 'ubuntu', 'centos', 'centos-stream', 'debian', 'rhel', 'opensuse', 'opensuse-tumbleweed', 'opensuse-leap') or full container disk image URLs
516516

517+
- **vm_guest_info** - Get guest operating system information from a VirtualMachine's QEMU guest agent. Requires the guest agent to be installed and running inside the VM. Provides detailed information about the OS, filesystems, network interfaces, and logged-in users.
518+
- `info_type` (`string`) - Type of information to retrieve: 'all' (default - all available info), 'os' (operating system details), 'filesystem' (disk and filesystem info), 'users' (logged-in users), 'network' (network interfaces and IPs)
519+
- `name` (`string`) **(required)** - The name of the virtual machine
520+
- `namespace` (`string`) **(required)** - The namespace of the virtual machine
521+
517522
- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, or restart a VM
518523
- `action` (`string`) **(required)** - The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)
519524
- `name` (`string`) **(required)** - The name of the virtual machine

pkg/kubevirt/guestagent.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package kubevirt
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8+
"k8s.io/apimachinery/pkg/runtime/schema"
9+
"k8s.io/apimachinery/pkg/runtime/serializer"
10+
"k8s.io/client-go/rest"
11+
)
12+
13+
const (
14+
// VMI subresource names for guest agent queries
15+
subresourceGuestOSInfo = "guestosinfo"
16+
subresourceFilesystemList = "filesystemlist"
17+
subresourceUserList = "userlist"
18+
subresourceInterfaceList = "interfacelist"
19+
)
20+
21+
var (
22+
// subresourcesScheme is the scheme for subresources API
23+
subresourcesScheme = Scheme
24+
// subresourcesCodec is the codec for encoding/decoding subresources
25+
subresourcesCodec = serializer.NewCodecFactory(subresourcesScheme)
26+
)
27+
28+
// AllGuestInfo holds all guest agent information with error tracking
29+
type AllGuestInfo struct {
30+
GuestOSInfo any `json:"guestOSInfo,omitempty" yaml:"guestOSInfo,omitempty"`
31+
GuestOSInfoError string `json:"guestOSInfoError,omitempty" yaml:"guestOSInfoError,omitempty"`
32+
Filesystems any `json:"filesystems,omitempty" yaml:"filesystems,omitempty"`
33+
FilesystemsError string `json:"filesystemsError,omitempty" yaml:"filesystemsError,omitempty"`
34+
Users any `json:"users,omitempty" yaml:"users,omitempty"`
35+
UsersError string `json:"usersError,omitempty" yaml:"usersError,omitempty"`
36+
NetworkInterfaces any `json:"networkInterfaces,omitempty" yaml:"networkInterfaces,omitempty"`
37+
NetworkInterfacesError string `json:"networkInterfacesError,omitempty" yaml:"networkInterfacesError,omitempty"`
38+
}
39+
40+
// getVMISubresource retrieves a VMI subresource using the REST client
41+
func getVMISubresource(ctx context.Context, restConfig *rest.Config, namespace, vmiName, subresource string) (map[string]any, error) {
42+
// Create a copy to avoid mutating the original config
43+
config := rest.CopyConfig(restConfig)
44+
45+
// Create a REST client configured for the subresources.kubevirt.io API group
46+
gv := schema.GroupVersion{Group: "subresources.kubevirt.io", Version: "v1"}
47+
config.GroupVersion = &gv
48+
config.APIPath = "/apis"
49+
config.NegotiatedSerializer = subresourcesCodec.WithoutConversion()
50+
51+
restClient, err := rest.RESTClientFor(config)
52+
if err != nil {
53+
return nil, fmt.Errorf("failed to create REST client for subresources: %w", err)
54+
}
55+
56+
// Make the request using SubResource() to properly construct the URL
57+
result := &unstructured.Unstructured{}
58+
err = restClient.Get().
59+
Namespace(namespace).
60+
Resource("virtualmachineinstances").
61+
Name(vmiName).
62+
SubResource(subresource).
63+
Do(ctx).
64+
Into(result)
65+
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
return result.Object, nil
71+
}
72+
73+
// GetGuestOSInfo retrieves operating system information from the guest agent
74+
func GetGuestOSInfo(ctx context.Context, restConfig *rest.Config, namespace, name string) (map[string]any, error) {
75+
result, err := getVMISubresource(ctx, restConfig, namespace, name, subresourceGuestOSInfo)
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to get guest OS info - guest agent may not be installed or running: %w", err)
78+
}
79+
80+
return map[string]any{
81+
"guestOSInfo": result,
82+
}, nil
83+
}
84+
85+
// GetFilesystemInfo retrieves filesystem and disk information from the guest agent
86+
func GetFilesystemInfo(ctx context.Context, restConfig *rest.Config, namespace, name string) (map[string]any, error) {
87+
result, err := getVMISubresource(ctx, restConfig, namespace, name, subresourceFilesystemList)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to get filesystem info - guest agent may not be installed or running: %w", err)
90+
}
91+
92+
return map[string]any{
93+
"filesystems": result,
94+
}, nil
95+
}
96+
97+
// GetUserInfo retrieves logged-in user information from the guest agent
98+
func GetUserInfo(ctx context.Context, restConfig *rest.Config, namespace, name string) (map[string]any, error) {
99+
result, err := getVMISubresource(ctx, restConfig, namespace, name, subresourceUserList)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to get user info - guest agent may not be installed or running: %w", err)
102+
}
103+
104+
return map[string]any{
105+
"users": result,
106+
}, nil
107+
}
108+
109+
// GetNetworkInfo retrieves network interface information from the guest agent
110+
func GetNetworkInfo(ctx context.Context, restConfig *rest.Config, namespace, name string) (map[string]any, error) {
111+
result, err := getVMISubresource(ctx, restConfig, namespace, name, subresourceInterfaceList)
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to get network interface info - guest agent may not be installed or running: %w", err)
114+
}
115+
116+
return map[string]any{
117+
"networkInterfaces": result,
118+
}, nil
119+
}
120+
121+
// collectGuestOSInfo collects OS information and updates the result struct
122+
func collectGuestOSInfo(ctx context.Context, restConfig *rest.Config, namespace, name string, result *AllGuestInfo) (bool, error) {
123+
osInfo, err := GetGuestOSInfo(ctx, restConfig, namespace, name)
124+
if err != nil {
125+
result.GuestOSInfoError = err.Error()
126+
return false, fmt.Errorf("guestOSInfo: %v", err)
127+
}
128+
result.GuestOSInfo = osInfo["guestOSInfo"]
129+
return true, nil
130+
}
131+
132+
// collectFilesystemInfo collects filesystem information and updates the result struct
133+
func collectFilesystemInfo(ctx context.Context, restConfig *rest.Config, namespace, name string, result *AllGuestInfo) (bool, error) {
134+
fsInfo, err := GetFilesystemInfo(ctx, restConfig, namespace, name)
135+
if err != nil {
136+
result.FilesystemsError = err.Error()
137+
return false, fmt.Errorf("filesystems: %v", err)
138+
}
139+
result.Filesystems = fsInfo["filesystems"]
140+
return true, nil
141+
}
142+
143+
// collectUserInfo collects user information and updates the result struct
144+
func collectUserInfo(ctx context.Context, restConfig *rest.Config, namespace, name string, result *AllGuestInfo) (bool, error) {
145+
userInfo, err := GetUserInfo(ctx, restConfig, namespace, name)
146+
if err != nil {
147+
result.UsersError = err.Error()
148+
return false, fmt.Errorf("users: %v", err)
149+
}
150+
result.Users = userInfo["users"]
151+
return true, nil
152+
}
153+
154+
// collectNetworkInfo collects network information and updates the result struct
155+
func collectNetworkInfo(ctx context.Context, restConfig *rest.Config, namespace, name string, result *AllGuestInfo) (bool, error) {
156+
netInfo, err := GetNetworkInfo(ctx, restConfig, namespace, name)
157+
if err != nil {
158+
result.NetworkInterfacesError = err.Error()
159+
return false, fmt.Errorf("networkInterfaces: %v", err)
160+
}
161+
result.NetworkInterfaces = netInfo["networkInterfaces"]
162+
return true, nil
163+
}
164+
165+
// convertToMap converts AllGuestInfo struct to a map for consistent return type
166+
func convertToMap(result *AllGuestInfo) map[string]any {
167+
resultMap := make(map[string]any)
168+
169+
if result.GuestOSInfo != nil {
170+
resultMap["guestOSInfo"] = result.GuestOSInfo
171+
} else if result.GuestOSInfoError != "" {
172+
resultMap["guestOSInfoError"] = result.GuestOSInfoError
173+
}
174+
175+
if result.Filesystems != nil {
176+
resultMap["filesystems"] = result.Filesystems
177+
} else if result.FilesystemsError != "" {
178+
resultMap["filesystemsError"] = result.FilesystemsError
179+
}
180+
181+
if result.Users != nil {
182+
resultMap["users"] = result.Users
183+
} else if result.UsersError != "" {
184+
resultMap["usersError"] = result.UsersError
185+
}
186+
187+
if result.NetworkInterfaces != nil {
188+
resultMap["networkInterfaces"] = result.NetworkInterfaces
189+
} else if result.NetworkInterfacesError != "" {
190+
resultMap["networkInterfacesError"] = result.NetworkInterfacesError
191+
}
192+
193+
return resultMap
194+
}
195+
196+
// GetAllGuestInfo retrieves all available guest agent information
197+
func GetAllGuestInfo(ctx context.Context, restConfig *rest.Config, namespace, name string) (map[string]any, error) {
198+
result := &AllGuestInfo{}
199+
var errors []error
200+
successCount := 0
201+
202+
// Collect all info types, but don't fail if one is unavailable
203+
if success, err := collectGuestOSInfo(ctx, restConfig, namespace, name, result); success {
204+
successCount++
205+
} else if err != nil {
206+
errors = append(errors, err)
207+
}
208+
209+
if success, err := collectFilesystemInfo(ctx, restConfig, namespace, name, result); success {
210+
successCount++
211+
} else if err != nil {
212+
errors = append(errors, err)
213+
}
214+
215+
if success, err := collectUserInfo(ctx, restConfig, namespace, name, result); success {
216+
successCount++
217+
} else if err != nil {
218+
errors = append(errors, err)
219+
}
220+
221+
if success, err := collectNetworkInfo(ctx, restConfig, namespace, name, result); success {
222+
successCount++
223+
} else if err != nil {
224+
errors = append(errors, err)
225+
}
226+
227+
// If all failed, return an aggregated error
228+
if successCount == 0 {
229+
return nil, fmt.Errorf("guest agent is not responding - all queries failed: %v - ensure QEMU guest agent is installed and running in the VM", errors)
230+
}
231+
232+
return convertToMap(result), nil
233+
}

0 commit comments

Comments
 (0)