feat: Initial comimt

This commit is contained in:
ptrcnull 2021-11-10 09:06:53 +01:00
commit 3d97c863d1
10 changed files with 841 additions and 0 deletions

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# govmtools
> Reimplementation of [open-vm-tools](https://github.com/vmware/open-vm-tools) in pure Go
##
## DataMap
### Field types
- empty = 0
- int64 = 1
- string = 2
- int64list = 3
- stringlist = 4
- max = 5
### Fields
- type = 1
- payload = 2
- fast_close = 3
### Types
- data = 1
- ping = 2

38
cmd/govmtoolsd/main.go Normal file
View file

@ -0,0 +1,38 @@
package main
import (
"fmt"
"github.com/ptrcnull/govmtools"
)
func main() {
sock, err := govmtools.NewSocket()
if err != nil {
panic(err)
}
fmt.Printf("connected! %#v\n", sock)
err = sock.ReportVersionData()
if err != nil {
panic(err)
}
sock.MustSendCommand("vmx.capability.unified_loop toolbox\x00")
sock.MustSendCommand("log toolbox: Version: 11.2.5.26209 (build-17337674)\x00")
sock.MustSendCommand("tools.capability.statechange ")
sock.MustSendCommand("tools.capability.softpowerop_retry ")
sock.MustSendCommand("tools.capability.guest_conf_directory /etc/vmware-tools\x00")
sock.MustSendCommand("tools.set.versiontype 11333 4\x00")
sock.MustSendCommand("info-set guestinfo.ip 10.99.0.6\x00")
guestInfoNetwork, err := govmtools.GetGuestInfoNetwork()
if err != nil {
panic(err)
}
sock.SetGuestInfo(govmtools.GuestInfoDnsName, "openldap")
sock.SetGuestInfo(govmtools.GuestInfoUptime, "26642699")
sock.SetGuestInfo(govmtools.GuestInfoIpAddressV3, guestInfoNetwork)
sock.SetGuestInfo(govmtools.GuestInfoBuildNumber, "build-" + govmtools.BuildNumber)
sock.MustSendCommand("info-set guestinfo.appInfo \x00")
select {}
}

12
go.mod Normal file
View file

@ -0,0 +1,12 @@
module github.com/ptrcnull/govmtools
go 1.17
require (
github.com/ptrcnull/vsock v0.0.0-20211110040213-bace62f83228
github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee
github.com/vishvananda/netlink v1.1.0
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
)
require github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect

20
go.sum Normal file
View file

@ -0,0 +1,20 @@
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/ptrcnull/vsock v0.0.0-20211110040213-bace62f83228 h1:ApLO3fuH29Nslm8s0G9tVe8UVG61Yg9ob4J80RxNX5U=
github.com/ptrcnull/vsock v0.0.0-20211110040213-bace62f83228/go.mod h1:1bEx6UYqCMOhYlho1+7LgWEJroZcFMSpAt7iesq/uiQ=
github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee h1:fbVs0xmXpBvVS4GBeiRmAE3Le70ofAqFMch1GTiq/e8=
github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

288
network.go Normal file
View file

@ -0,0 +1,288 @@
package govmtools
import (
"bytes"
"encoding/hex"
"fmt"
"io/ioutil"
"strings"
xdr "github.com/stellar/go-xdr/xdr3"
"github.com/vishvananda/netlink"
)
func GetGuestNicProto() (*GuestNicProto, error) {
res := NicInfoV3{
Nics: []GuestNicV3{},
Routes: []InetCidrRouteEntry{},
DnsConfigInfo: []DnsConfigInfo{},
}
ifaces, err := netlink.LinkList()
if err != nil {
return nil, fmt.Errorf("get ifaces: %w", err)
}
ignoredPrefixes := []string{"veth", "docker", "virbr", "lo", "br-"}
for _, iface := range ifaces {
attrs := iface.Attrs()
skip := false
for _, prefix := range ignoredPrefixes {
if strings.HasPrefix(attrs.Name, prefix) {
skip = true
}
}
if skip || attrs.HardwareAddr.String() == "" {
continue
}
nic := GuestNicV3{
MacAddress: attrs.HardwareAddr.String(),
Ips: []IpAddressEntry{},
}
addrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL)
if err != nil {
fmt.Println("error getting addresses:", err)
continue
}
if len(addrs) < 1 {
continue
}
for _, addr := range addrs {
// ugly
ip := addr.IP
var address InetAddress
var addrType InetAddressType
if ip.To4() != nil {
address = InetAddress(ip.To4())
addrType = IatIpv4
} else {
address = InetAddress(ip.To16())
addrType = IatIpv6
}
prefixLength, _ := addr.Mask.Size()
entry := IpAddressEntry{
IpAddressAddr: TypedIpAddress{
IpAddressAddrType: addrType,
IpAddressAddr: address,
},
IpAddressPrefixLength: InetAddressPrefixLength(prefixLength),
IpAddressOrigin: nil,
IpAddressStatus: nil,
}
nic.Ips = append(nic.Ips, entry)
}
routes, err := netlink.RouteList(iface, netlink.FAMILY_ALL)
if err != nil {
fmt.Println("error getting routes:", err)
continue
}
for _, route := range routes {
var addr TypedIpAddress
if route.Dst.IP.To4() != nil {
addr = TypedIpAddress{
IpAddressAddrType: IatIpv4,
IpAddressAddr: InetAddress(route.Dst.IP.To4()),
}
} else {
addr = TypedIpAddress{
IpAddressAddrType: IatIpv6,
IpAddressAddr: InetAddress(route.Dst.IP.To16()),
}
}
routePrefix, _ := route.Dst.Mask.Size()
r := InetCidrRouteEntry{
InetCidrRouteDest: addr,
InetCidrRoutePfxLen: InetAddressPrefixLength(routePrefix),
InetCidrRouteNextHop: route.Gw.,
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 0,
}
}
res.Nics = append(res.Nics, nic)
}
return &GuestNicProto{
Type: ProtoTypeV3,
NicInfoV3: &NicInfoV3Wrapper{
NicInfoV3: []NicInfoV3{},
},
}, nil
}
func GetGuestInfoNetwork() (string, error) {
data, err := GetGuestNicProto()
if err != nil {
return "", err
}
buf := bytes.NewBuffer(nil)
_, err = xdr.Marshal(buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func DoStuff() {
buf := bytes.NewBuffer(nil)
xdr.Marshal(buf, &GuestNicProto{
Type: ProtoTypeV3,
NicInfoV3: &NicInfoV3Wrapper{
NicInfoV3: []NicInfoV3{
{
Nics: []GuestNicV3{
{
MacAddress: "00:50:56:9f:41:dd",
Ips: []IpAddressEntry{
{
IpAddressAddr: TypedIpAddress{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 0, 4},
},
IpAddressPrefixLength: 16,
IpAddressOrigin: nil,
IpAddressStatus: []IpAddressStatus{
IasPreferred,
},
},
{
IpAddressAddr: TypedIpAddress{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 1, 51},
},
IpAddressPrefixLength: 16,
IpAddressOrigin: nil,
IpAddressStatus: []IpAddressStatus{
IasPreferred,
},
},
{
IpAddressAddr: TypedIpAddress{
IpAddressAddrType: IatIpv6,
IpAddressAddr: []byte{
0xfe, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x50, 0x56, 0xff,
0xfe, 0x9f, 0x41, 0xdd,
},
},
IpAddressPrefixLength: 64,
IpAddressOrigin: nil,
IpAddressStatus: []IpAddressStatus{
IasUnknown,
},
},
},
},
},
Routes: []InetCidrRouteEntry{
{
InetCidrRouteDest: TypedIpAddress{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{0, 0, 0, 0},
},
InetCidrRoutePfxLen: 0,
InetCidrRouteNextHop: []TypedIpAddress{
{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 0, 1},
},
},
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 0,
},
{
InetCidrRouteDest: TypedIpAddress{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 0, 0},
},
InetCidrRoutePfxLen: 16,
InetCidrRouteNextHop: nil,
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 0,
},
{
InetCidrRouteDest: TypedIpAddress{
IpAddressAddrType: IatIpv6,
IpAddressAddr: []byte{
0xfe, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
},
},
InetCidrRoutePfxLen: 64,
InetCidrRouteNextHop: nil,
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 0x100,
},
{
InetCidrRouteDest: TypedIpAddress{
IpAddressAddrType: IatIpv6,
IpAddressAddr: []byte{
0xfe, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x50, 0x56, 0xff,
0xfe, 0x9f, 0x41, 0xdd,
},
},
InetCidrRoutePfxLen: 128,
InetCidrRouteNextHop: nil,
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 0,
},
{
InetCidrRouteDest: TypedIpAddress{
IpAddressAddrType: IatIpv6,
IpAddressAddr: []byte{
0xff, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
},
},
InetCidrRoutePfxLen: 8,
InetCidrRouteNextHop: nil,
InetCidrRouteIfIndex: 0,
InetCidrRouteType: 0,
InetCidrRouteMetric: 256,
},
},
DnsConfigInfo: []DnsConfigInfo{
{
HostName: []string{"imagepacker"},
DomainName: []string{"t2hack.internal"},
ServerList: []TypedIpAddress{
{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 0, 8},
},
{
IpAddressAddrType: IatIpv4,
IpAddressAddr: []byte{10, 99, 0, 8},
},
},
SearchSuffixes: []string{"t2hack.internal"},
},
},
},
},
},
})
fmt.Println(hex.Dump(buf.Bytes()))
ioutil.WriteFile("/home/patrycja/newdata", buf.Bytes(), 0755)
}

140
network_xdr.go Normal file
View file

@ -0,0 +1,140 @@
package govmtools
import (
xdr "github.com/stellar/go-xdr/xdr3"
)
type InetAddressType int
const (
IatUnknown InetAddressType = iota
IatIpv4
IatIpv6
IatIpv4Z
IatIpv6Z
IatDns
)
type IpAddressOrigin int
const (
IaoOther IpAddressOrigin = 1
IaoManual IpAddressOrigin = 2
IaoDhcp IpAddressOrigin = 4
IaoLinkLayer IpAddressOrigin = 5
IaoRandom IpAddressOrigin = 6
)
type IpAddressStatus int
const (
IasPreferred = iota + 1
IasDeprecated
IasInvalid
IasInaccessible
IasUnknown
IasTentative
IasDuplicate
IasOptimistic
)
type InetAddress []byte
type TypedIpAddress struct {
IpAddressAddrType InetAddressType
IpAddressAddr InetAddress
}
type InetAddressPrefixLength uint
type IpAddressEntry struct {
IpAddressAddr TypedIpAddress
IpAddressPrefixLength InetAddressPrefixLength
IpAddressOrigin []IpAddressOrigin
IpAddressStatus []IpAddressStatus
}
type DnsConfigInfo struct {
HostName []string
DomainName []string
ServerList []TypedIpAddress
SearchSuffixes []string
}
type WinsConfigInfo struct {
Primary TypedIpAddress
Secondary TypedIpAddress
}
type DhcpConfigInfo struct {
Enabled bool
DhcpSettings []string
}
type GuestNicV3 struct {
MacAddress string
Ips []IpAddressEntry
DnsConfigInfo []DnsConfigInfo
WinsConfigInfo []WinsConfigInfo
DhcpConfigInfoV4 []DhcpConfigInfo
DhcpConfigInfoV6 []DhcpConfigInfo
}
type InetCidrRouteType int
const (
IcrtOther InetCidrRouteType = iota + 1
IcrtReject
IcrtLocal
IcrtRemote
)
type InetCidrRouteEntry struct {
InetCidrRouteDest TypedIpAddress
InetCidrRoutePfxLen InetAddressPrefixLength
InetCidrRouteNextHop []TypedIpAddress
InetCidrRouteIfIndex uint32
InetCidrRouteType InetCidrRouteType
InetCidrRouteMetric uint32
}
type NicInfoV3Wrapper struct {
NicInfoV3 []NicInfoV3
}
type NicInfoV3 struct {
Nics []GuestNicV3
Routes []InetCidrRouteEntry
DnsConfigInfo []DnsConfigInfo
WinsConfigInfo []WinsConfigInfo
DhcpConfigInfoV4 []DhcpConfigInfo
DhcpConfigInfoV6 []DhcpConfigInfo
}
type GuestNicProtoType int
const (
ProtoTypeV3 = 3
)
func (GuestNicProtoType) ValidEnum(i int32) bool {
return i == ProtoTypeV3
}
type GuestNicProto struct {
Type GuestNicProtoType
NicInfoV3 *NicInfoV3Wrapper
}
var _ xdr.Union = (*GuestNicProto)(nil)
func (g GuestNicProto) ArmForSwitch(i int32) (string, bool) {
if i != 3 {
return "", false
}
return "NicInfoV3", true
}
func (g GuestNicProto) SwitchFieldName() string {
return "Type"
}

126
proto.go Normal file
View file

@ -0,0 +1,126 @@
package govmtools
import (
"bytes"
"encoding/binary"
"fmt"
)
type Field struct {
Type FieldType
ID FieldID
Value interface{}
}
func (s *Socket) WritePacket(pkt []Field) error {
buf := bytes.NewBuffer(nil)
for _, field := range pkt {
err := binary.Write(buf, order, field.Type)
if err != nil {
return fmt.Errorf("write field type: %w", err)
}
if field.Type == FieldEmpty {
continue
}
err = binary.Write(buf, order, field.ID)
if err != nil {
return fmt.Errorf("write field ID: %w", err)
}
switch field.Type {
case FieldInt64:
err = binary.Write(buf, order, field.Value)
if err != nil {
return fmt.Errorf("write int64: %w", err)
}
case FieldString:
val := field.Value.(string)
err = binary.Write(buf, order, int32(len(val)))
if err != nil {
return fmt.Errorf("write string len: %w", err)
}
_, err = buf.WriteString(val)
if err != nil {
return fmt.Errorf("write string: %w", err)
}
}
}
res := make([]byte, buf.Len()+4)
order.PutUint32(res, uint32(buf.Len()))
copy(res[4:], buf.Bytes())
_, err := s.conn.Write(res)
if err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}
func (s *Socket) ReadPacket() ([]Field, error) {
length := make([]byte, 4)
_, err := s.conn.Read(length)
if err != nil {
return nil, fmt.Errorf("read len: %w", err)
}
response := make([]byte, order.Uint32(length))
_, err = s.conn.Read(response)
if err != nil {
return nil, fmt.Errorf("read: %w", err)
}
buf := bytes.NewBuffer(response)
var fields []Field
for buf.Len() > 0 {
field := Field{}
err = binary.Read(buf, order, &field.Type)
if err != nil {
return nil, fmt.Errorf("parse field type: %w", err)
}
if field.Type == FieldEmpty {
continue
}
err = binary.Read(buf, order, &field.ID)
if err != nil {
return nil, fmt.Errorf("parse field ID: %w", err)
}
switch field.Type {
case FieldInt64:
var val int64
err = binary.Read(buf, order, val)
if err != nil {
return nil, fmt.Errorf("parse int64: %w", err)
}
field.Value = val
case FieldString:
var length uint32
err = binary.Read(buf, order, &length)
if err != nil {
return nil, fmt.Errorf("parse string length: %w", err)
}
byt := make([]byte, length)
_, err := buf.Read(byt)
if err != nil {
return nil, fmt.Errorf("parse string: %w", err)
}
field.Value = string(byt)
}
fields = append(fields, field)
}
return fields, nil
}

49
rpc.go Normal file
View file

@ -0,0 +1,49 @@
package govmtools
import (
"bytes"
"encoding/binary"
"fmt"
)
var order = binary.BigEndian
func (s *Socket) RpcSend(data []byte) ([]byte, error) {
// TODO error handling
buf := bytes.NewBuffer(nil)
binary.Write(buf, order, FieldInt64)
binary.Write(buf, order, FieldIDType)
binary.Write(buf, order, PacketTypeData)
binary.Write(buf, order, FieldEmpty)
binary.Write(buf, order, FieldString)
binary.Write(buf, order, FieldIDPayload)
binary.Write(buf, order, uint32(len(data)))
buf.Write(data)
res := make([]byte, buf.Len()+4)
order.PutUint32(res, uint32(buf.Len()))
copy(res[4:], buf.Bytes())
_, err := s.conn.Write(res)
if err != nil {
return nil, fmt.Errorf("write: %w", err)
}
responseLength := make([]byte, 4)
_, err = s.conn.Read(responseLength)
if err != nil {
return nil, fmt.Errorf("read len: %w", err)
}
response := make([]byte, order.Uint32(responseLength))
_, err = s.conn.Read(response)
if err != nil {
return nil, fmt.Errorf("read: %w", err)
}
return response, nil
}

137
socket.go Normal file
View file

@ -0,0 +1,137 @@
package govmtools
import (
"fmt"
"github.com/ptrcnull/vsock"
"golang.org/x/sys/unix"
)
type PacketType uint32
const (
PacketTypeData PacketType = iota + 1
PacketTypePing
)
type FieldID uint32
const (
FieldIDType FieldID = iota + 1
FieldIDPayload
FieldIDFastClose
)
type FieldType uint32
const (
FieldEmpty FieldType = iota
FieldInt64
FieldString
FieldInt64List
FieldStringList
FieldMax
)
type GuestInfoType uint32
const (
GuestInfoError GuestInfoType = iota
GuestInfoDnsName
GuestInfoIpAddress
GuestInfoDiskFreeSpace
GuestInfoBuildNumber
GuestInfoOsNameFull
GuestInfoOsName
GuestInfoUptime
GuestInfoMemory
GuestInfoIpAddressV2
GuestInfoIpAddressV3
GuestInfoOsDetailed
GuestInfoMax
)
const GuestConnectPort = 976
const ContextID = unix.VMADDR_CID_HYPERVISOR
type Socket struct {
conn *vsock.Conn
}
func NewSocket() (*Socket, error) {
conn, err := vsock.Dial(ContextID, GuestConnectPort)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
return &Socket{
conn: conn,
}, nil
}
func (s *Socket) SendCommand(cmd string) error {
err := s.WritePacket([]Field{
{
Type: FieldInt64,
ID: FieldIDType,
Value: PacketTypeData,
},
{
Type: FieldEmpty,
},
{
Type: FieldString,
ID: FieldIDPayload,
Value: cmd,
},
})
if err != nil {
return fmt.Errorf("write packet: %w", err)
}
res, err := s.ReadPacket()
if err != nil {
return fmt.Errorf("read packet: %w", err)
}
if len(res) != 1 {
return fmt.Errorf("unexpected response length: %d", len(res))
}
field := res[0]
if field.Type != FieldString {
return fmt.Errorf("unexpected response type: %d", field.Type)
}
if field.ID != FieldIDPayload {
return fmt.Errorf("unexpected response id: %d", field.ID)
}
response := field.Value.(string)
if response != "1 " {
return fmt.Errorf("unexpected response: %s", response)
}
return nil
}
func (s *Socket) SetGuestInfo(messageType GuestInfoType, message string) {
s.MustSendCommand(fmt.Sprintf("SetGuestInfo %d %s", messageType, message))
}
func (s *Socket) MustSendCommand(cmd string) {
err := s.SendCommand(cmd)
if err != nil {
panic(err)
}
}
func (s *Socket) ReportVersionData() error {
data := []string{
"description " + Name + " " + Version + " build " + BuildNumber,
"versionString " + Version,
"versionNumber " + VersionNumber,
"buildNumber " + BuildNumber,
}
for _, value := range data {
err := s.SendCommand("info-set guestinfo.vmtools." + value + "\x00")
if err != nil {
return fmt.Errorf("send: %w", err)
}
}
return nil
}

6
version.go Normal file
View file

@ -0,0 +1,6 @@
package govmtools
const Name = "open-vm-tools"
const Version = "11.2.5"
const VersionNumber = "11333"
const BuildNumber = "17337674"