Initial commit

This commit is contained in:
Łukasz Moskała 2024-07-07 18:51:49 +02:00
commit 799ea9a90a
12 changed files with 1332 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright 2024 Łukasz Moskała <lm@lukaszmoskala.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

8
Makefile Normal file
View file

@ -0,0 +1,8 @@
BINS := cp2guest guestrun libguestd-cli
SRC_DIR := cmd
.PHONY: all $(BINS)
all: $(BINS)
$(BINS):
go build -C $(SRC_DIR)/$@ -o ../../bin/$@

77
README.md Normal file
View file

@ -0,0 +1,77 @@
# Libguestd
This is supposed to be a library that helps with operations on qemu-guest-agent.
You need to connect to libvirt daemon on your own.
This library implements some(most?) functions described in [QEMU Guest Agent Protocol Reference](https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html).
Not implemented:
- guest-sync-delimited (my understanding is that libvirt does this for me)
- guest-sync (same as above)
- guest-set-time
- guest-shutdown (there is a libvirt command for that already)
- guest-shutdown (there is a libvirt command for that already)
- guest-file-seek (this library only reads/writes whole file)
- guest-file-flush (this library only writes whole file, then closes it immediately)
- guest-fsfreeze-* (there are libvirt commands for that)
- guest-fstrim (I don't need this)
- guest-suspend-*
- guest-get-vcpus (I don't need this)
- guest-set-vcpus (I don't need this)
- guest-get-memory-blocks
- guest-get-memory-block-info
- guest-set-memory-blocks
- guest-get-users (I don't think this is usefull)
- guest-get-timezone
- guest-get-devices (windows only)
Not implemented but I want to implement it:
- guest-get-cpustats
# CLI tools
Alongside this library, a few tools are provided:
## cp2guest
Allows copying files from/to guest using qemu guest agent.
Example usage:
```shell
./bin/cp2guest -domain guesttools -src README.md -dst /tmp/README.md
```
Or, to copy from VM to host:
```shell
./bin/cp2guest -domain guesttools -src /root/anaconda-ks.cfg -dst /tmp/anaconda-ks.cfg -reverse
```
## guestrun
Execute command in guest
Example usage:
```shell
./bin/guestrun -domain guesttools -cmd 'ls -ltrh /bin/'
```
Command will be executed as `/bin/sh -c 'ls -ltrh /bin'`.
## Libguestd-cli
This tool implements most functions of this library.
Since usage should be self-explainatory, I will just leave a few example commands here:
```shell
./bin/libguestd-cli -domain guesttools -username root -password 12345
./bin/libguestd-cli -domain guesttools -username root -sshkey-add 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID8qp7UQUINxLXog/sFgRKDtddiJHzkypyB7/OlmUbK2 lmoskala'\
./bin/libguestd-cli -domain guesttools -username root -sshkey-remove 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID8qp7UQUINxLXog/sFgRKDtddiJHzkypyB7/OlmUbK2 lmoskala'
./bin/libguestd-cli -domain guesttools -username root -listkeys
./bin/libguestd-cli -domain guesttools -username root -fsinfo
```
# Security
Some distributions (rocky linux for example) disables some functionalities of qemu-guest-agent by default.
Most notably, all operations involving files and commands.
Also, selinux is known to cause problems when manipulating SSH keys: [bug report](https://bugzilla.redhat.com/show_bug.cgi?id=1917024)

3
bin/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
cp2guest
guestrun
libguestd-cli

0
bin/.gitkeep Normal file
View file

87
cmd/cp2guest/cp2guest.go Normal file
View file

@ -0,0 +1,87 @@
package main
import (
"flag"
"git.mlody.eu/lmoskala/libguestd/pkg/libguestd"
"github.com/digitalocean/go-libvirt"
"log"
"net/url"
"os"
"time"
)
func main() {
libvirtUri := flag.String("libvirt", "qemu:///system", "libvirt connection URI")
domainName := flag.String("domain", "", "domain (instance) name")
Src := flag.String("src", "", "source file")
Dst := flag.String("dst", "", "destination file")
Reverse := flag.Bool("reverse", false, "reverse mode (guest to host)")
flag.Parse()
if *domainName == "" {
log.Fatalf("Domain name must be specified")
}
if *Src == "" {
log.Fatalf("Source file must be specified")
}
if *Dst == "" {
log.Fatalf("Destination file must be specified")
}
var data []byte
var err error
if !*Reverse {
data, err = os.ReadFile(*Src)
if err != nil {
log.Fatalf("error reading src file: %v\n", err)
}
}
uri, _ := url.Parse(*libvirtUri)
l, err := libvirt.ConnectToURI(uri)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive)
if err != nil {
log.Fatalf("failed to list all domains: %v", err)
}
for _, domain := range domains {
if domain.Name != *domainName {
continue
}
err = libguestd.GuestPing(l, domain)
if err != nil {
log.Printf("Error(GuestPing): %s", err)
continue
}
agentInfo, err := libguestd.GuestInfo(l, domain)
if err != nil {
log.Printf("Error(GuestInfo): %s", err)
continue
}
Start := time.Now()
if *Reverse {
data, err = libguestd.GuestFileRead(l, domain, agentInfo, *Src)
} else {
err = libguestd.GuestFileWrite(l, domain, agentInfo, *Dst, data)
}
if err != nil {
log.Printf("Error(GuestFileWrite): %s", err)
} else {
duration := time.Since(Start)
log.Printf("Copied %d bytes in %.1fs, (%.1f MiB/s)", len(data), duration.Seconds(), float64(len(data))/1024/1024/duration.Seconds())
}
if *Reverse {
err = os.WriteFile(*Dst, data, 0600)
if err != nil {
log.Printf("error writing file: %v", err)
}
}
}
if err = l.Disconnect(); err != nil {
log.Fatalf("failed to disconnect: %v", err)
}
}

77
cmd/guestrun/guestrun.go Normal file
View file

@ -0,0 +1,77 @@
package main
import (
"encoding/base64"
"flag"
"fmt"
"git.mlody.eu/lmoskala/libguestd/pkg/libguestd"
"github.com/digitalocean/go-libvirt"
"log"
"net/url"
)
func main() {
libvirtUri := flag.String("libvirt", "qemu:///system", "libvirt connection URI")
domainName := flag.String("domain", "", "domain (instance) name")
Cmd := flag.String("cmd", "", "Command (will be run as /bin/sh -c ...)")
flag.Parse()
if *domainName == "" {
log.Fatalf("Domain name must be specified")
}
uri, _ := url.Parse(*libvirtUri)
l, err := libvirt.ConnectToURI(uri)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive)
if err != nil {
log.Fatalf("failed to list all domains: %v", err)
}
for _, domain := range domains {
if domain.Name != *domainName {
continue
}
err = libguestd.GuestPing(l, domain)
if err != nil {
log.Printf("Error(GuestPing): %s", err)
continue
}
agentInfo, err := libguestd.GuestInfo(l, domain)
if err != nil {
log.Printf("Error(GuestInfo): %s", err)
continue
}
pid, err := libguestd.GuestExecStart(l, domain, agentInfo, "/bin/sh", []string{"-c", *Cmd}, []string{}, []byte{})
if err != nil {
log.Printf("Error(GuestExecStart): %s", err)
continue
}
log.Printf("PID: %d", pid)
res, err := libguestd.GuestWaitForCommand(l, domain, agentInfo, pid)
if err != nil {
log.Printf("Error(GuestWaitForCommand): %s", err)
}
log.Printf("Exit status: %d", res.ExitCode)
log.Printf("Stdout follows:")
dec, _ := base64.StdEncoding.DecodeString(res.OutDataB64)
fmt.Println(string(dec))
if res.OutTruncated {
log.Printf("[Output was truncated]")
}
log.Printf("Stderr follows:")
dec, _ = base64.StdEncoding.DecodeString(res.ErrDataB64)
fmt.Println(string(dec))
if res.ErrTruncated {
log.Printf("[Output was truncated]")
}
}
if err = l.Disconnect(); err != nil {
log.Fatalf("failed to disconnect: %v", err)
}
}

View file

@ -0,0 +1,172 @@
package main
import (
"flag"
"fmt"
"git.mlody.eu/lmoskala/libguestd/pkg/libguestd"
"github.com/digitalocean/go-libvirt"
"log"
"net/url"
"time"
)
func main() {
libvirtUri := flag.String("libvirt", "qemu:///system", "libvirt connection URI")
domainName := flag.String("domain", "", "domain name")
GetOSInfo := flag.Bool("osinfo", false, "Gather OS info")
GetNICInfo := flag.Bool("nicinfo", false, "Gather NIC info")
GetFSInfo := flag.Bool("fsinfo", false, "Gather Filesystem info")
ListKeys := flag.Bool("listkeys", false, "List ssh keys (specify username)")
GetTimeDelta := flag.Bool("timedelta", false, "Display time delta between this host and guest")
GetHostName := flag.Bool("hostname", false, "Display host name")
GetAgentInfo := flag.Bool("agentinfo", false, "Display agent info")
GetDiskInfo := flag.Bool("diskinfo", false, "Display disk info")
GetDiskStats := flag.Bool("diskstats", false, "Display disk I/O stats")
Username := flag.String("username", "", "username to edit (use with password or ssh keys)")
Password := flag.String("password", "", "password to set (specify username)")
AddKey := flag.String("sshkey-add", "", "SSH key to add (in authorized_keys format), specify username")
RemoveKey := flag.String("sshkey-remove", "", "SSH key to remove (in authorized_keys format), specify username")
flag.Parse()
uri, _ := url.Parse(*libvirtUri)
l, err := libvirt.ConnectToURI(uri)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive)
if err != nil {
log.Fatalf("failed to list all domains: %v", err)
}
for _, domain := range domains {
if domain.Name != *domainName {
continue
}
err = libguestd.GuestPing(l, domain)
if err != nil {
log.Printf("Error(GuestPing): %s", err)
continue
}
agentInfo, err := libguestd.GuestInfo(l, domain)
if err != nil {
log.Printf("Error(GuestInfo): %s", err)
continue
}
if *GetAgentInfo {
agentInfo.Dump()
}
if *GetOSInfo {
osInfo, err := libguestd.GuestGetOsInfo(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetOsInfo): %s", err)
} else {
osInfo.Dump()
}
}
if *GetNICInfo {
interfaces, err := libguestd.GuestGetNetworkInterfaces(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetNetworkInterfaces): %s", err)
} else {
for _, iface := range interfaces {
iface.Dump()
}
}
}
if *GetFSInfo {
fs, err := libguestd.GuestGetFilesystemInfo(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetFilesystemInfo): %s", err)
} else {
for _, fs := range fs {
fs.Dump()
}
}
}
if *GetDiskInfo {
disk, err := libguestd.GuestGetDisks(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetDisks): %s", err)
} else {
for _, disk := range disk {
disk.Dump()
}
}
}
if *GetDiskStats {
diskstats, err := libguestd.GuestGetDiskstats(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetDiskstats): %s", err)
} else {
for _, disk := range diskstats {
disk.Dump()
}
}
}
if *Username != "" && *Password != "" {
err = libguestd.GuestChangeUserPassword(l, domain, agentInfo, *Username, *Password)
if err != nil {
log.Printf("Error(GuestChangeUserPassword): %s", err)
} else {
log.Printf("Password set")
}
}
if *Username != "" && *AddKey != "" {
err = libguestd.GuestAddSshKeys(l, domain, agentInfo, *Username, []string{*AddKey}, false)
if err != nil {
log.Printf("Error(GuestAddSshKeys): %s", err)
} else {
log.Printf("SSH key added")
}
}
if *Username != "" && *RemoveKey != "" {
err = libguestd.GuestRemoveSshKeys(l, domain, agentInfo, *Username, []string{*RemoveKey})
if err != nil {
log.Printf("Error(GuestRemoveSshKeys): %s", err)
} else {
log.Printf("SSH key removed")
}
}
if *Username != "" && *ListKeys {
keys, err := libguestd.GuestGetSshKeys(l, domain, agentInfo, *Username)
if err != nil {
log.Printf("Error(GuestGetSshKeys): %s", err)
}
fmt.Printf("Keys for %s:\n", *Username)
for _, key := range keys {
fmt.Printf("\t%s\n", key)
}
}
if *GetHostName {
hostname, err := libguestd.GuestGetHostName(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetHostName): %s", err)
} else {
fmt.Printf("Hostname: %s\n", hostname)
}
}
if *GetTimeDelta {
guestTime, err := libguestd.GuestGetTime(l, domain, agentInfo)
if err != nil {
log.Printf("Error(GuestGetTime): %s", err)
} else {
d := time.Since(time.Unix(0, guestTime))
fmt.Printf("Time diff: %f seconds\n", d.Seconds())
}
}
}
if err = l.Disconnect(); err != nil {
log.Fatalf("failed to disconnect: %v", err)
}
}

9
go.mod Normal file
View file

@ -0,0 +1,9 @@
module git.mlody.eu/lmoskala/libguestd
go 1.22.5
require (
github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/sys v0.21.0 // indirect
)

6
go.sum Normal file
View file

@ -0,0 +1,6 @@
github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7 h1:KMOLn19gbh7KbPEgu76ZIf/b2CnnYhC2GFLgLiN/YkA=
github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7/go.mod h1:DMUPOdO9OHCbF88MGmFNUnCBH9GLjeHl2RaA49Vy3vo=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

873
pkg/libguestd/libguestd.go Normal file
View file

@ -0,0 +1,873 @@
package libguestd
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/digitalocean/go-libvirt"
"time"
)
// GuestPing returns error when agent didn't respond or something bad happened
// Returns nil on success.
func GuestPing(l *libvirt.Libvirt, domain libvirt.Domain) error {
_, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-ping\"}", 100, 0)
if err != nil {
return err
}
return nil
}
type GuestAgentCommandInfo struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
SuccessResponse bool `json:"success-response"`
}
type GuestAgentInfo struct {
Version string `json:"version"`
SupportedCommands []GuestAgentCommandInfo `json:"supported_commands"`
}
func (ga *GuestAgentInfo) Dump() {
fmt.Printf("Guest Agent Info:\n")
fmt.Printf("\tVersion: %s\n", ga.Version)
fmt.Printf("\tSupportedCommands:\n")
for _, command := range ga.SupportedCommands {
if command.Enabled {
fmt.Printf("\t\t%s\n", command.Name)
}
}
}
type GuestAgentInfoWrapper struct {
Result GuestAgentInfo `json:"return"`
}
func GuestInfo(l *libvirt.Libvirt, domain libvirt.Domain) (GuestAgentInfo, error) {
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-info\"}", 100, 0)
if err != nil {
return GuestAgentInfo{}, err
}
if len(r) > 0 {
var ret GuestAgentInfoWrapper
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return GuestAgentInfo{}, err
}
return ret.Result, nil
}
return GuestAgentInfo{}, nil
}
type GuestOsInfo struct {
KernelRelease string `json:"kernel-release"`
KernelVersion string `json:"kernel-version"`
Machine string `json:"machine"`
ID string `json:"id"`
Name string `json:"name"`
PrettyName string `json:"pretty-name"`
Version string `json:"version"`
VersionId string `json:"version-id"`
Variant string `json:"variant"`
VariantId string `json:"variant-id"`
}
func (goi *GuestOsInfo) Dump() {
fmt.Printf("Guest OS Info:\n")
fmt.Printf("\tKernelRelease: %s\n", goi.KernelRelease)
fmt.Printf("\tKernelVersion: %s\n", goi.KernelVersion)
fmt.Printf("\tMachine: %s\n", goi.Machine)
fmt.Printf("\tID: %s\n", goi.ID)
fmt.Printf("\tName: %s\n", goi.Name)
fmt.Printf("\tPrettyName: %s\n", goi.PrettyName)
fmt.Printf("\tVersion: %s\n", goi.Version)
fmt.Printf("\tVersionId: %s\n", goi.VersionId)
fmt.Printf("\tVariant: %s\n", goi.Variant)
fmt.Printf("\tVariantId: %s\n", goi.VariantId)
}
type GuestOsInfoWrapper struct {
Result GuestOsInfo `json:"return"`
}
func checkSupportedFeature(AgentInfo GuestAgentInfo, feature string) bool {
for _, v := range AgentInfo.SupportedCommands {
if v.Name == feature {
return v.Enabled
}
}
return false
}
func GuestGetOsInfo(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) (GuestOsInfo, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-osinfo") {
return GuestOsInfo{}, fmt.Errorf("guest-get-osinfo not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-osinfo\"}", 100, 0)
if err != nil {
return GuestOsInfo{}, err
}
if len(r) > 0 {
var ret GuestOsInfoWrapper
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return GuestOsInfo{}, err
}
return ret.Result, nil
}
return GuestOsInfo{}, fmt.Errorf("empty response from guest-get-osinfo")
}
type GuestNetworkInterfaceStat struct {
RxBytes uint64 `json:"rx-bytes"`
TxBytes uint64 `json:"tx-bytes"`
RxErrors uint64 `json:"rx-errs"`
TxErrors uint64 `json:"tx-errs"`
RxDropped uint64 `json:"rx-dropped"`
TxDropped uint64 `json:"tx-dropped"`
RxPackets uint64 `json:"rx-packets"`
TxPackets uint64 `json:"tx-packets"`
}
func (gnis *GuestNetworkInterfaceStat) String() string {
return fmt.Sprintf("RX[Bytes: %d, Packets: %d, Dropped: %d, Errors: %d] TX[Bytes: %d, Packets: %d, Dropped: %d, Errors: %d]", gnis.RxBytes, gnis.RxPackets, gnis.RxDropped, gnis.RxErrors, gnis.TxBytes, gnis.TxPackets, gnis.TxDropped, gnis.TxErrors)
}
type GuestIpAddress struct {
IPAddress string `json:"ip-address"`
IPAddressType string `json:"ip-address-type"`
Prefix uint32 `json:"prefix"`
}
type GuestNetworkInterface struct {
Name string `json:"name"`
HardwareAddress string `json:"hardware-address"`
IpAddresses []GuestIpAddress `json:"ip-addresses"`
Statistics GuestNetworkInterfaceStat `json:"statistics"`
}
func (gni *GuestNetworkInterface) Dump() {
fmt.Printf("Guest Network Interface:\n")
fmt.Printf("\tName: %s\n", gni.Name)
fmt.Printf("\tHardware Address: %s\n", gni.HardwareAddress)
fmt.Printf("\tStatistics: %s\n", gni.Statistics.String())
for _, ip := range gni.IpAddresses {
fmt.Printf("\t\t%s/%d (%s)\n", ip.IPAddress, ip.Prefix, ip.IPAddressType)
}
}
type GuestNetworkInterfaceWrapper struct {
Result []GuestNetworkInterface `json:"return"`
}
func GuestGetNetworkInterfaces(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) ([]GuestNetworkInterface, error) {
if !checkSupportedFeature(AgentInfo, "guest-network-get-interfaces") {
return nil, fmt.Errorf("guest-network-get-interfaces not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-network-get-interfaces\"}", 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
var ret GuestNetworkInterfaceWrapper
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return nil, err
}
return ret.Result, nil
}
return nil, fmt.Errorf("empty response from guest-network-get-interfaces")
}
type GuestPCIAddress struct {
Domain int `json:"domain"`
Bus int `json:"bus"`
Slot int `json:"slot"`
Function int `json:"function"`
}
func (gpi *GuestPCIAddress) String() string {
return fmt.Sprintf("pci%d:%d:%d:%d", gpi.Domain, gpi.Bus, gpi.Slot, gpi.Function)
}
type GuestDiskAddress struct {
PciController GuestPCIAddress `json:"pci-controller"`
BusType string `json:"bus-type"`
Bus int `json:"bus"`
Target int `json:"target"`
Unit int `json:"unit"`
Serial string `json:"serial"`
Dev string `json:"dev"`
}
func (gdi *GuestDiskAddress) String() string {
return fmt.Sprintf("%s (%s%d/%d/%d at %s)", gdi.Dev, gdi.BusType, gdi.Bus, gdi.Target, gdi.Unit, gdi.PciController.String())
}
type GuestFilesystemInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Mountpoint string `json:"mountpoint"`
UsedBytes uint64 `json:"used-bytes"`
TotalBytes uint64 `json:"total-bytes"`
TotalBytesPrivileged uint64 `json:"total-bytes-privileged"`
Disk []GuestDiskAddress `json:"disk"`
}
func (gfi *GuestFilesystemInfo) Dump() {
fmt.Printf("Guest Filesystem Info: %s\n", gfi.Mountpoint)
fmt.Printf("\tName: %s\n", gfi.Name)
fmt.Printf("\tType: %s\n", gfi.Type)
fmt.Printf("\tMountpoint: %s\n", gfi.Mountpoint)
fmt.Printf("\tUsedBytes: %d MiB\n", gfi.UsedBytes/1024/1024)
fmt.Printf("\tTotalBytes: %d MiB\n", gfi.TotalBytes/1024/1024)
fmt.Printf("\tTotalBytesPrivileged: %d MiB\n", gfi.TotalBytesPrivileged/1024/1024)
fmt.Printf("\t[")
Space := gfi.UsedBytes * 40 / gfi.TotalBytes
var i uint64
for i = 0; i < Space; i++ {
fmt.Printf("=")
}
for i = Space; i < 40; i++ {
fmt.Printf(" ")
}
fmt.Printf("] %d%%\n", gfi.UsedBytes*100/gfi.TotalBytes)
fmt.Printf("\tDisk: \n")
for _, d := range gfi.Disk {
fmt.Printf("\t\t%s\n", d.String())
}
}
type GuestFilesystemInfoWrapper struct {
Result []GuestFilesystemInfo `json:"return"`
}
func GuestGetFilesystemInfo(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) ([]GuestFilesystemInfo, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-fsinfo") {
return nil, fmt.Errorf("guest-get-fsinfo not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-fsinfo\"}", 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
var ret GuestFilesystemInfoWrapper
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return nil, err
}
return ret.Result, nil
}
return nil, fmt.Errorf("empty response from guest-get-fsinfo")
}
func GuestChangeUserPassword(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, Username string, Password string) error {
if !checkSupportedFeature(AgentInfo, "guest-set-user-password") {
return fmt.Errorf("guest-set-user-password not supported by agent")
}
type Args struct {
Username string `json:"username"`
Password string `json:"password"`
Crypted bool `json:"crypted"`
}
type req struct {
Execute string `json:"execute"`
Args Args `json:"arguments"`
}
re := req{
Execute: "guest-set-user-password",
Args: Args{
Username: Username,
Password: base64.StdEncoding.EncodeToString([]byte(Password)),
Crypted: false,
},
}
reS, err := json.Marshal(&re)
if err != nil {
return err
}
_, err = l.QEMUDomainAgentCommand(domain, string(reS), 100, 0)
if err != nil {
return err
}
return nil
}
func GuestAddSshKeys(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, Username string, Keys []string, reset bool) error {
if !checkSupportedFeature(AgentInfo, "guest-ssh-add-authorized-keys") {
return fmt.Errorf("guest-ssh-add-authorized-keys not supported by agent")
}
type Args struct {
Username string `json:"username"`
Keys []string `json:"keys"`
Reset bool `json:"reset"`
}
type req struct {
Execute string `json:"execute"`
Args Args `json:"arguments"`
}
re := req{
Execute: "guest-ssh-add-authorized-keys",
Args: Args{
Username: Username,
Keys: Keys,
Reset: reset,
},
}
reS, err := json.Marshal(&re)
if err != nil {
return err
}
_, err = l.QEMUDomainAgentCommand(domain, string(reS), 100, 0)
if err != nil {
return err
}
return nil
}
func GuestRemoveSshKeys(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, Username string, Keys []string) error {
if !checkSupportedFeature(AgentInfo, "guest-ssh-remove-authorized-keys") {
return fmt.Errorf("guest-ssh-remove-authorized-keys not supported by agent")
}
type Args struct {
Username string `json:"username"`
Keys []string `json:"keys"`
}
type req struct {
Execute string `json:"execute"`
Args Args `json:"arguments"`
}
re := req{
Execute: "guest-ssh-remove-authorized-keys",
Args: Args{
Username: Username,
Keys: Keys,
},
}
reS, err := json.Marshal(&re)
if err != nil {
return err
}
_, err = l.QEMUDomainAgentCommand(domain, string(reS), 100, 0)
if err != nil {
return err
}
return nil
}
func GuestGetSshKeys(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, Username string) ([]string, error) {
if !checkSupportedFeature(AgentInfo, "guest-ssh-get-authorized-keys") {
return nil, fmt.Errorf("guest-ssh-get-authorized-keys not supported by agent")
}
type Args struct {
Username string `json:"username"`
}
type req struct {
Execute string `json:"execute"`
Args Args `json:"arguments"`
}
re := req{
Execute: "guest-ssh-get-authorized-keys",
Args: Args{
Username: Username,
},
}
reS, err := json.Marshal(&re)
if err != nil {
return nil, err
}
r, err := l.QEMUDomainAgentCommand(domain, string(reS), 100, 0)
if err != nil {
return nil, err
}
var rt struct {
Result struct {
Keys []string `json:"keys"`
} `json:"return"`
}
if len(r) > 0 {
err = json.Unmarshal([]byte(r[0]), &rt)
if err != nil {
return nil, err
}
return rt.Result.Keys, nil
}
return nil, fmt.Errorf("empty response")
}
func GuestFileWrite(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, filename string, content []byte) error {
if !checkSupportedFeature(AgentInfo, "guest-file-open") {
return fmt.Errorf("guest-file-open not supported by agent")
}
if !checkSupportedFeature(AgentInfo, "guest-file-close") {
return fmt.Errorf("guest-file-close not supported by agent")
}
if !checkSupportedFeature(AgentInfo, "guest-file-write") {
return fmt.Errorf("guest-file-write not supported by agent")
}
var fop struct {
Execute string `json:"execute"`
Args struct {
Path string `json:"path"`
Mode string `json:"mode"`
} `json:"arguments"`
}
fop.Execute = "guest-file-open"
fop.Args.Path = filename
fop.Args.Mode = "w"
fopS, err := json.Marshal(fop)
if err != nil {
return err
}
r, err := l.QEMUDomainAgentCommand(domain, string(fopS), 100, 0)
if err != nil {
return err
}
if len(r) > 0 {
var hdl struct {
Handle int `json:"return"`
}
err = json.Unmarshal([]byte(r[0]), &hdl)
if err != nil {
return err
}
var wrq struct {
Execute string `json:"execute"`
Args struct {
Handle int `json:"handle"`
BufB64 string `json:"buf-b64"`
} `json:"arguments"`
}
wrq.Execute = "guest-file-write"
wrq.Args.Handle = hdl.Handle
chunkSize := 4096
for i := 0; i < len(content); i += chunkSize {
end := i + chunkSize
if end > len(content) {
end = len(content)
}
chunk := content[i:end]
wrq.Args.BufB64 = base64.StdEncoding.EncodeToString(chunk)
wrqS, err := json.Marshal(wrq)
if err != nil {
return err
}
_, err = l.QEMUDomainAgentCommand(domain, string(wrqS), 100, 0)
if err != nil {
return err
}
}
var crq struct {
Execute string `json:"execute"`
Args struct {
Handle int `json:"handle"`
} `json:"arguments"`
}
crq.Execute = "guest-file-close"
crq.Args.Handle = hdl.Handle
crqS, err := json.Marshal(crq)
if err != nil {
return err
}
_, err = l.QEMUDomainAgentCommand(domain, string(crqS), 100, 0)
return err
}
return fmt.Errorf("empty response")
}
func GuestFileRead(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, filename string) ([]byte, error) {
if !checkSupportedFeature(AgentInfo, "guest-file-open") {
return nil, fmt.Errorf("guest-file-open not supported by agent")
}
if !checkSupportedFeature(AgentInfo, "guest-file-close") {
return nil, fmt.Errorf("guest-file-close not supported by agent")
}
if !checkSupportedFeature(AgentInfo, "guest-file-read") {
return nil, fmt.Errorf("guest-file-read not supported by agent")
}
var fop struct {
Execute string `json:"execute"`
Args struct {
Path string `json:"path"`
Mode string `json:"mode"`
} `json:"arguments"`
}
fop.Execute = "guest-file-open"
fop.Args.Path = filename
fop.Args.Mode = "r"
fopS, err := json.Marshal(fop)
if err != nil {
return nil, err
}
r, err := l.QEMUDomainAgentCommand(domain, string(fopS), 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
var hdl struct {
Handle int `json:"return"`
}
err = json.Unmarshal([]byte(r[0]), &hdl)
if err != nil {
return nil, err
}
var rrq struct {
Execute string `json:"execute"`
Args struct {
Handle int `json:"handle"`
} `json:"arguments"`
}
rrq.Execute = "guest-file-read"
rrq.Args.Handle = hdl.Handle
rrqS, err := json.Marshal(rrq)
if err != nil {
return nil, err
}
var gfr struct {
Return struct {
Count int `json:"count"`
BufB64 string `json:"buf-b64"`
Eof bool `json:"eof"`
} `json:"return"`
}
var ret []byte
for {
r, err = l.QEMUDomainAgentCommand(domain, string(rrqS), 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
err = json.Unmarshal([]byte(r[0]), &gfr)
if err != nil {
return nil, err
}
if gfr.Return.Count != 0 {
dec, err := base64.StdEncoding.DecodeString(gfr.Return.BufB64)
if err != nil {
return nil, err
}
ret = append(ret, dec...)
}
if gfr.Return.Eof {
break
}
} else {
break
}
}
var crq struct {
Execute string `json:"execute"`
Args struct {
Handle int `json:"handle"`
} `json:"arguments"`
}
crq.Execute = "guest-file-close"
crq.Args.Handle = hdl.Handle
crqS, err := json.Marshal(crq)
if err != nil {
return nil, err
}
_, err = l.QEMUDomainAgentCommand(domain, string(crqS), 100, 0)
if err != nil {
return nil, err
}
return ret, nil
}
return nil, fmt.Errorf("empty response")
}
// Starts command on guest using guest-exec
// Returns PID of started process
// User MUST call GuestCommandStatus after command finished running to get output
// and reap process.
func GuestExecStart(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, path string, args []string, env []string, stdin []byte) (int, error) {
if !checkSupportedFeature(AgentInfo, "guest-exec") {
return 0, fmt.Errorf("guest-exec not supported by agent")
}
if !checkSupportedFeature(AgentInfo, "guest-exec-status") {
return 0, fmt.Errorf("guest-exec-status not supported by agent")
}
var exec struct {
Execute string `json:"execute"`
Arguments struct {
Path string `json:"path"`
Arg []string `json:"arg"`
Env []string `json:"env"`
InputData string `json:"input-data,omitempty"`
CaptureOutput bool `json:"capture-output"`
} `json:"arguments"`
}
exec.Execute = "guest-exec"
exec.Arguments.Path = path
exec.Arguments.Arg = args
exec.Arguments.Env = env
exec.Arguments.InputData = base64.StdEncoding.EncodeToString(stdin)
exec.Arguments.CaptureOutput = true
execS, err := json.Marshal(exec)
if err != nil {
return 0, err
}
r, err := l.QEMUDomainAgentCommand(domain, string(execS), 100, 0)
if err != nil {
return 0, err
}
if len(r) > 0 {
var hdl struct {
Return struct {
Pid int `json:"pid"`
}
}
err = json.Unmarshal([]byte(r[0]), &hdl)
if err != nil {
return 0, err
}
return hdl.Return.Pid, nil
}
return 0, fmt.Errorf("empty response for %s", execS)
}
type GuestExecStatus struct {
Exited bool `json:"exited"`
ExitCode int `json:"exitcode"`
Signal int `json:"signal"`
OutDataB64 string `json:"out-data"` //only populated after process exits
ErrDataB64 string `json:"err-data"` //only populated after process exits
OutTruncated bool `json:"out-truncated"`
ErrTruncated bool `json:"err-truncated"`
}
type GuestExecStatusWrapper struct {
Return GuestExecStatus `json:"return"`
}
// Checks status of executed command.
func GuestCommandStatus(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, pid int) (GuestExecStatus, error) {
if !checkSupportedFeature(AgentInfo, "guest-exec-status") {
return GuestExecStatus{}, fmt.Errorf("guest-exec-status not supported by agent")
}
var ges struct {
Execute string `json:"execute"`
Args struct {
Pid int `json:"pid"`
} `json:"arguments"`
}
ges.Execute = "guest-exec-status"
ges.Args.Pid = pid
gesS, err := json.Marshal(ges)
if err != nil {
return GuestExecStatus{}, err
}
r, err := l.QEMUDomainAgentCommand(domain, string(gesS), 100, 0)
if err != nil {
return GuestExecStatus{}, err
}
if len(r) > 0 {
var gesw GuestExecStatusWrapper
err = json.Unmarshal([]byte(r[0]), &gesw)
if err != nil {
return GuestExecStatus{}, err
}
return gesw.Return, nil
}
return GuestExecStatus{}, fmt.Errorf("empty response for %s", gesS)
}
// Periodically calls GuestCommandStatus untill command finishes running.
// Polls for status immediately, then every 50 milliseconds with exponential backoff.
func GuestWaitForCommand(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo, pid int) (GuestExecStatus, error) {
delay := 50 * time.Millisecond
for {
ret, err := GuestCommandStatus(l, domain, AgentInfo, pid)
if err != nil {
return GuestExecStatus{}, err
}
if ret.Exited {
return ret, nil
}
time.Sleep(delay)
delay *= 2
}
}
func GuestGetHostName(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) (string, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-host-name") {
return "", fmt.Errorf("guest-get-host-name not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-host-name\"}", 100, 0)
if err != nil {
return "", err
}
if len(r) > 0 {
var ret struct {
Return struct {
Hostname string `json:"host-name"`
} `json:"return"`
}
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return "", err
}
return ret.Return.Hostname, nil
}
return "", fmt.Errorf("empty response from guest-get-host-name")
}
func GuestGetTime(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) (int64, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-time") {
return 0, fmt.Errorf("guest-get-time not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-time\"}", 100, 0)
if err != nil {
return 0, err
}
if len(r) > 0 {
var ret struct {
Return int64 `json:"return"`
}
err := json.Unmarshal([]byte(r[0]), &ret)
if err != nil {
return 0, err
}
return ret.Return, nil
}
return 0, fmt.Errorf("empty response from guest-get-time")
}
type GuestDiskInfo struct {
Name string `json:"name"`
Partition bool `json:"partition"`
Dependencies []string `json:"dependencies"`
Alias string `json:"alias"`
Address GuestDiskAddress `json:"address"`
}
func (gdi *GuestDiskInfo) Dump() {
fmt.Printf("Disk:\n")
fmt.Printf("\tName: %s\n", gdi.Name)
fmt.Printf("\tPartition: %v\n", gdi.Partition)
fmt.Printf("\tAlias: %s\n", gdi.Alias)
fmt.Printf("\tAddress: %s\n", gdi.Address.String())
for _, d := range gdi.Dependencies {
fmt.Printf("\t\tDependency: %s\n", d)
}
}
type GuestDiskInfoWrapper struct {
Return []GuestDiskInfo `json:"return"`
}
func GuestGetDisks(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) ([]GuestDiskInfo, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-disks") {
return nil, fmt.Errorf("guest-get-disks not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-disks\"}", 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
var gdiw GuestDiskInfoWrapper
err = json.Unmarshal([]byte(r[0]), &gdiw)
if err != nil {
return nil, err
}
return gdiw.Return, nil
}
return nil, fmt.Errorf("empty response from guest-get-disks")
}
type GuestDiskStats struct {
ReadSectors int `json:"read-sectors"`
ReadIos int `json:"read-ios"`
ReadMerges int `json:"read-merges"`
WriteSectors int `json:"write-sectors"`
WriteIos int `json:"write-ios"`
WriteMerges int `json:"write-merges"`
DiscardSectors int `json:"discard-sectors"`
DiscardIos int `json:"discard-ios"`
DiscardMerges int `json:"discard-merges"`
FlushIos int `json:"flush-ios"`
ReadTicks int `json:"read-ticks"`
WriteTicks int `json:"write-ticks"`
DiscardTicks int `json:"discard-ticks"`
FlushTicks int `json:"flush-ticks"`
IosPgr int `json:"ios-pgr"`
TotalTicks int `json:"total-ticks"`
WeightTicks int `json:"weight-ticks"`
}
type GuestDiskStatInfo struct {
Name string `json:"name"`
Major int `json:"major"`
Minor int `json:"minor"`
Stats GuestDiskStats `json:"stats"`
}
func (gdsi *GuestDiskStatInfo) Dump() {
fmt.Printf("Disk I/O stats: %s (%d:%d)\n", gdsi.Name, gdsi.Major, gdsi.Minor)
fmt.Printf("\tRead sectors: %d\n", gdsi.Stats.ReadSectors)
fmt.Printf("\tRead Ios: %d\n", gdsi.Stats.ReadIos)
fmt.Printf("\tRead Merges: %d\n", gdsi.Stats.ReadMerges)
fmt.Printf("\tWrite sectors: %d\n", gdsi.Stats.WriteSectors)
fmt.Printf("\tWrite Ios: %d\n", gdsi.Stats.WriteIos)
fmt.Printf("\tWrite Merges: %d\n", gdsi.Stats.WriteMerges)
fmt.Printf("\tDiscard sectors: %d\n", gdsi.Stats.DiscardSectors)
fmt.Printf("\tDiscard Ios: %d\n", gdsi.Stats.DiscardIos)
fmt.Printf("\tDiscard Merges: %d\n", gdsi.Stats.DiscardMerges)
fmt.Printf("\tFlush Ios: %d\n", gdsi.Stats.FlushIos)
fmt.Printf("\tRead Ticks: %d\n", gdsi.Stats.ReadTicks)
fmt.Printf("\tWrite Ticks: %d\n", gdsi.Stats.WriteTicks)
fmt.Printf("\tDiscard Ticks: %d\n", gdsi.Stats.DiscardTicks)
fmt.Printf("\tFlush Ticks: %d\n", gdsi.Stats.FlushTicks)
fmt.Printf("\tIosPgr: %d\n", gdsi.Stats.IosPgr)
fmt.Printf("\tTotal Ticks: %d\n", gdsi.Stats.TotalTicks)
fmt.Printf("\tWeight Ticks: %d\n", gdsi.Stats.WeightTicks)
}
type GuestDiskStatInfoWrapper struct {
Return []GuestDiskStatInfo `json:"return"`
}
func GuestGetDiskstats(l *libvirt.Libvirt, domain libvirt.Domain, AgentInfo GuestAgentInfo) ([]GuestDiskStatInfo, error) {
if !checkSupportedFeature(AgentInfo, "guest-get-diskstats") {
return nil, fmt.Errorf("guest-get-diskstats not supported by agent")
}
r, err := l.QEMUDomainAgentCommand(domain, "{\"execute\":\"guest-get-diskstats\"}", 100, 0)
if err != nil {
return nil, err
}
if len(r) > 0 {
var gdisw GuestDiskStatInfoWrapper
err = json.Unmarshal([]byte(r[0]), &gdisw)
if err != nil {
return nil, err
}
return gdisw.Return, nil
}
return nil, fmt.Errorf("empty response from guest-get-diskstats")
}