Initial commit
This commit is contained in:
commit
799ea9a90a
12 changed files with 1332 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
8
Makefile
Normal 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
77
README.md
Normal 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
3
bin/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
cp2guest
|
||||||
|
guestrun
|
||||||
|
libguestd-cli
|
0
bin/.gitkeep
Normal file
0
bin/.gitkeep
Normal file
87
cmd/cp2guest/cp2guest.go
Normal file
87
cmd/cp2guest/cp2guest.go
Normal 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
77
cmd/guestrun/guestrun.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
172
cmd/libguestd-cli/libguestd-cli.go
Normal file
172
cmd/libguestd-cli/libguestd-cli.go
Normal 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
9
go.mod
Normal 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
6
go.sum
Normal 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
873
pkg/libguestd/libguestd.go
Normal 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")
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue