commit 799ea9a90a9e4d01eeb59ea23e398a283e3c9bfd Author: Łukasz Moskała Date: Sun Jul 7 18:51:49 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d433cd2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2024 Łukasz Moskała + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e11a18 --- /dev/null +++ b/Makefile @@ -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/$@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea8435b --- /dev/null +++ b/README.md @@ -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) + diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..3ed8df5 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,3 @@ +cp2guest +guestrun +libguestd-cli diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/cmd/cp2guest/cp2guest.go b/cmd/cp2guest/cp2guest.go new file mode 100644 index 0000000..45991c3 --- /dev/null +++ b/cmd/cp2guest/cp2guest.go @@ -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) + } +} diff --git a/cmd/guestrun/guestrun.go b/cmd/guestrun/guestrun.go new file mode 100644 index 0000000..73dc69a --- /dev/null +++ b/cmd/guestrun/guestrun.go @@ -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) + } +} diff --git a/cmd/libguestd-cli/libguestd-cli.go b/cmd/libguestd-cli/libguestd-cli.go new file mode 100644 index 0000000..6c3cdeb --- /dev/null +++ b/cmd/libguestd-cli/libguestd-cli.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..010443d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..426651b --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/libguestd/libguestd.go b/pkg/libguestd/libguestd.go new file mode 100644 index 0000000..28df412 --- /dev/null +++ b/pkg/libguestd/libguestd.go @@ -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") + +}