diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6b27041 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,160 @@ +# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. +# +# Generated on 2024-03-28T09:36:34Z by kres 88d1199. + +# options for analysis running +run: + timeout: 10m + issues-exit-code: 1 + tests: true + build-tags: [ ] + modules-download-mode: readonly + +# output configuration options +output: + formats: + - format: colored-line-number + path: stdout + print-issued-lines: true + print-linter-name: true + uniq-by-line: true + path-prefix: "" + +# all available settings of specific linters +linters-settings: + dogsled: + max-blank-identifiers: 2 + dupl: + threshold: 150 + errcheck: + check-type-assertions: true + check-blank: true + exhaustive: + default-signifies-exhaustive: false + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/siderolabs/talos-vmtoolsd/) # Custom section: groups all imports with the specified Prefix. + gocognit: + min-complexity: 30 + nestif: + min-complexity: 5 + goconst: + min-len: 3 + min-occurrences: 3 + gocritic: + disabled-checks: [ ] + gocyclo: + min-complexity: 20 + godot: + scope: declarations + gofmt: + simplify: true + goimports: + local-prefixes: github.com/siderolabs/talos-vmtoolsd/ + gomodguard: { } + gomnd: { } + govet: + enable-all: true + lll: + line-length: 200 + tab-width: 4 + misspell: + locale: US + ignore-words: [ ] + nakedret: + max-func-lines: 30 + prealloc: + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + nolintlint: + allow-unused: false + allow-no-explanation: [ ] + require-explanation: false + require-specific: true + rowserrcheck: { } + testpackage: { } + unparam: + check-exported: false + unused: + local-variables-are-used: false + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + wsl: + strict-append: true + allow-assign-and-call: true + allow-multiline-assign: true + allow-cuddle-declarations: false + allow-trailing-comment: false + force-case-trailing-whitespace: 0 + force-err-cuddling: false + allow-separated-leading-comment: false + gofumpt: + extra-rules: false + cyclop: + # the maximal code complexity to report + max-complexity: 20 + # depguard: + # Main: + # deny: + # - github.com/OpenPeeDeeP/depguard # this is just an example + +linters: + enable-all: true + disable-all: false + fast: false + disable: + - exhaustivestruct + - exhaustruct + - forbidigo + - funlen + - gochecknoglobals + - gochecknoinits + - godox + - goerr113 + - gomnd + - gomoddirectives + - gosec + - inamedparam + - ireturn + - nestif + - nonamedreturns + - nosnakecase + - paralleltest + - tagalign + - tagliatelle + - thelper + - typecheck + - varnamelen + - wrapcheck + - depguard # Disabled because starting with golangci-lint 1.53.0 it doesn't allow denylist alone anymore + - testifylint # complains about our assert recorder and has a number of false positives for assert.Greater(t, thing, 1) + - protogetter # complains about us using Value field on typed spec, instead of GetValue which has a different signature + - perfsprint # complains about us using fmt.Sprintf in non-performance critical code, updating just kres took too long + # abandoned linters for which golangci shows the warning that the repo is archived by the owner + - deadcode + - golint + - ifshort + - interfacer + - maligned + - scopelint + - structcheck + - varcheck + # disabled as it seems to be broken - goes into imported libraries and reports issues there + - musttag + +issues: + exclude: [ ] + exclude-rules: [ ] + exclude-use-default: false + exclude-case-sensitive: false + max-issues-per-linter: 10 + max-same-issues: 3 + new: false + +severity: + default-severity: error + case-sensitive: false diff --git a/cmd/talos-vmtoolsd/main.go b/cmd/talos-vmtoolsd/main.go index a1d03a6..e0c7485 100644 --- a/cmd/talos-vmtoolsd/main.go +++ b/cmd/talos-vmtoolsd/main.go @@ -1,3 +1,4 @@ +// Package main implements the main entry point for the Talos VMware Tools Daemon. package main import ( @@ -8,13 +9,20 @@ import ( "os/signal" "syscall" - vmtoolsd "github.com/siderolabs/talos-vmtoolsd" - "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" - "github.com/siderolabs/talos-vmtoolsd/internal/talosapi" - "github.com/siderolabs/talos-vmtoolsd/internal/tboxcmds" "github.com/sirupsen/logrus" vmguestmsg "github.com/vmware/vmw-guestinfo/message" "github.com/vmware/vmw-guestinfo/vmcheck" + + "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" + "github.com/siderolabs/talos-vmtoolsd/internal/talosapi" + "github.com/siderolabs/talos-vmtoolsd/internal/tboxcmds" + "github.com/siderolabs/talos-vmtoolsd/internal/version" +) + +// Debug flags. +var ( + talosTestQuery string + useMachinedSocket bool ) func main() { @@ -24,9 +32,6 @@ func main() { DisableHTMLEscape: true, }) - // Debug flags - var talosTestQuery string - var useMachinedSocket bool flag.StringVar(&talosTestQuery, "test-apid-query", "", "query apid") flag.BoolVar(&useMachinedSocket, "use-machined", false, "use machined unix socket") flag.Parse() @@ -45,7 +50,7 @@ func main() { l.Infof("talos-vmtoolsd version %v\n"+ "Copyright 2020-2022 Oliver Kuckertz \n"+ "This program is free software and available under the Apache 2.0 license.", - vmtoolsd.Version) + version.Version) // Simplify deployment to mixed vSphere and non-vSphere clusters by detecting ESXi and stopping // early for other platforms. Admins can avoid the overhead of this idle process by labeling @@ -58,14 +63,18 @@ func main() { ctx, ctxCancel := context.WithCancel(context.Background()) - var api *talosapi.LocalClient - var err error + var ( + api *talosapi.LocalClient + err error + ) + if !useMachinedSocket { // Our spec file passes the secret path and K8s host IP via env vars. configPath := os.Getenv("TALOS_CONFIG_PATH") if len(configPath) == 0 { l.Fatal("error: TALOS_CONFIG_PATH is a required path to a Talos configuration file") } + k8sHost := os.Getenv("TALOS_HOST") if len(k8sHost) == 0 { l.Fatal("error: TALOS_HOST is required to point to a node's internal IP") @@ -94,10 +103,11 @@ func main() { if talosTestQuery != "" { if err := testQuery(api, talosTestQuery); err != nil { l.WithField("test_query", talosTestQuery).WithError(err).Fatal("test query failed") - os.Exit(1) - } else { - os.Exit(0) + + os.Exit(1) //nolint:gocritic } + + os.Exit(0) } // Wires up VMware Toolbox commands to Talos apid. @@ -116,6 +126,7 @@ func main() { // Graceful shutdown on SIGINT/SIGTERM sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + go func() { l.Debugf("signal: %s", <-sig) ctxCancel() @@ -127,6 +138,7 @@ func main() { func testQuery(api *talosapi.LocalClient, query string) error { w := os.Stdout + switch query { case "net-interfaces": for idx, intf := range api.NetInterfaces() { @@ -134,15 +146,19 @@ func testQuery(api *talosapi.LocalClient, query string) error { _, _ = fmt.Fprintf(w, "%d: name=%s mac=%s addr=%s\n", idx, intf.Name, intf.MAC, addr) } } + return nil case "hostname": _, _ = fmt.Fprintln(w, api.Hostname()) + return nil case "os-version": _, _ = fmt.Fprintln(w, api.OSVersion()) + return nil case "os-version-short": _, _ = fmt.Fprintln(w, api.OSVersionShort()) + return nil default: return fmt.Errorf("unknown test query %q", query) diff --git a/internal/nanotoolbox/channel.go b/internal/nanotoolbox/channel.go index 874f00b..065ed0e 100644 --- a/internal/nanotoolbox/channel.go +++ b/internal/nanotoolbox/channel.go @@ -17,6 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package nanotoolbox provides a minimal set of tools for communicating with the vmx. package nanotoolbox import ( @@ -33,9 +34,10 @@ const ( tcloProtocol uint32 = 0x4f4c4354 ) +// ErrNotVirtualWorld is returned when the current process is not running in a virtual world. var ErrNotVirtualWorld = errors.New("not in a virtual world") -// Channel abstracts the guest<->vmx RPC transport +// Channel abstracts the guest<->vmx RPC transport. type Channel interface { Start() error Stop() error @@ -44,16 +46,18 @@ type Channel interface { } var ( - RpciOK = []byte{'1', ' '} + // RpciOK is the return code for a successful RPCI request. + RpciOK = []byte{'1', ' '} + // RpciERR is the return code for a failed RPCI request. RpciERR = []byte{'0', ' '} ) -// ChannelOut extends Channel to provide RPCI protocol helpers +// ChannelOut extends Channel to provide RPCI protocol helpers. type ChannelOut struct { Channel } -// Request sends an RPC command to the vmx and checks the return code for success or error +// Request sends an RPC command to the vmx and checks the return code for success or error. func (c *ChannelOut) Request(request []byte) ([]byte, error) { if c.Channel == nil { return nil, fmt.Errorf("no channel available for request %q", request) @@ -75,7 +79,7 @@ func (c *ChannelOut) Request(request []byte) ([]byte, error) { return nil, fmt.Errorf("failed request %q: %q", request, reply) } -type hypervisorChannel struct { +type hypervisorChannel struct { //nolint:govet protocol uint32 *message.Channel @@ -108,8 +112,10 @@ func (b *hypervisorChannel) Stop() error { return err } +// NewHypervisorChannelPair returns a pair of channels for communicating with the vmx. func NewHypervisorChannelPair() (Channel, Channel) { in := &hypervisorChannel{protocol: tcloProtocol} out := &hypervisorChannel{protocol: rpciProtocol} + return in, out } diff --git a/internal/nanotoolbox/service.go b/internal/nanotoolbox/service.go index b467c80..1cca6df 100644 --- a/internal/nanotoolbox/service.go +++ b/internal/nanotoolbox/service.go @@ -29,10 +29,10 @@ import ( ) const ( - // TOOLS_VERSION_UNMANAGED as defined in open-vm-tools/lib/include/vm_tools_version.h + // TOOLS_VERSION_UNMANAGED as defined in open-vm-tools/lib/include/vm_tools_version.h. toolsVersionUnmanaged = 0x7fffffff - // RPCIN_MAX_DELAY as defined in rpcChannelInt.h + // RPCIN_MAX_DELAY as defined in rpcChannelInt.h. maxDelay = 100 * time.Millisecond // If we have an RPCI send error, the channels will be reset. @@ -40,8 +40,8 @@ const ( resetDelay = 5 * time.Second ) -// Service receives and dispatches incoming RPC requests from the vmx -type Service struct { +// Service receives and dispatches incoming RPC requests from the vmx. +type Service struct { //nolint:govet Log logrus.FieldLogger Out *ChannelOut @@ -58,7 +58,7 @@ type Service struct { capabilities []string } -// NewService initializes a Service instance +// NewService initializes a Service instance. func NewService(log logrus.FieldLogger, rpcIn Channel, rpcOut Channel) *Service { s := &Service{ Log: log.WithField("module", "nanotoolbox"), @@ -84,7 +84,7 @@ func NewService(log logrus.FieldLogger, rpcIn Channel, rpcOut Channel) *Service return s } -// backoff exponentially increases the RPC poll delay up to maxDelay +// backoff exponentially increases the RPC poll delay up to maxDelay. func (s *Service) backoff() { if s.delay < maxDelay { if s.delay > 0 { @@ -101,8 +101,8 @@ func (s *Service) backoff() { } func (s *Service) stopChannel() { - _ = s.in.Stop() - _ = s.Out.Stop() + s.in.Stop() //nolint:errcheck + s.Out.Stop() //nolint:errcheck } func (s *Service) startChannel() error { @@ -117,18 +117,21 @@ func (s *Service) startChannel() error { func (s *Service) checkReset() error { if s.rpcError { s.stopChannel() + err := s.startChannel() if err != nil { s.delay = resetDelay + return err } + s.rpcError = false } return nil } -// Start initializes the RPC channels and starts a goroutine to listen for incoming RPC requests +// Start initializes the RPC channels and starts a goroutine to listen for incoming RPC requests. func (s *Service) Start() error { err := s.startChannel() if err != nil { @@ -136,6 +139,7 @@ func (s *Service) Start() error { } s.wg.Add(1) + go func() { defer s.wg.Done() @@ -150,6 +154,7 @@ func (s *Service) Start() error { select { case <-s.stop: s.stopChannel() + return case <-time.After(s.delay): if err = s.checkReset(); err != nil { @@ -158,13 +163,15 @@ func (s *Service) Start() error { err = s.in.Send(response) response = nil + if err != nil { s.delay = resetDelay s.rpcError = true + continue } - request, _ := s.in.Receive() + request, _ := s.in.Receive() //nolint:errcheck if len(request) > 0 { response = s.Dispatch(request) @@ -179,7 +186,7 @@ func (s *Service) Start() error { return nil } -// Stop cancels the RPC listener routine created via Start +// Stop cancels the RPC listener routine created via Start. func (s *Service) Stop() { close(s.stop) } @@ -189,29 +196,33 @@ func (s *Service) Wait() { s.wg.Wait() } -// CommandHandler is given the raw argument portion of an RPC request and returns a response +// CommandHandler is given the raw argument portion of an RPC request and returns a response. type CommandHandler func([]byte) ([]byte, error) -// OptionHandler is given the raw key and value of Set_Option requests +// OptionHandler is given the raw key and value of Set_Option requests. type OptionHandler func(key, value string) +// AddCapability adds a capability to the Service. func (s *Service) AddCapability(name string) { s.capabilities = append(s.capabilities, name) } +// RegisterCommandHandler adds a CommandHandler to the Service. func (s *Service) RegisterCommandHandler(name string, handler CommandHandler) { s.commandHandlers[name] = handler } +// RegisterOptionHandler adds an OptionHandler to the Service. func (s *Service) RegisterOptionHandler(key string, handler OptionHandler) { s.optionHandlers[key] = handler } +// RegisterResetHandler adds a function to be called when the Service is reset. func (s *Service) RegisterResetHandler(f func()) { s.resetHandlers = append(s.resetHandlers, f) } -// Dispatch an incoming RPC request to a CommandHandler +// Dispatch an incoming RPC request to a CommandHandler. func (s *Service) Dispatch(request []byte) []byte { msg := bytes.SplitN(request, []byte{' '}, 2) name := msg[0] @@ -226,6 +237,7 @@ func (s *Service) Dispatch(request []byte) []byte { if !ok { l.Debug("unknown command") + return []byte("ERROR Unknown Command") } @@ -239,39 +251,48 @@ func (s *Service) Dispatch(request []byte) []byte { response = append([]byte("OK "), response...) } else { l.WithError(err).Warn("error calling handler") + response = append([]byte("ERROR "), response...) } return response } +// HandleReset resets the Service. func (s *Service) HandleReset([]byte) ([]byte, error) { for _, f := range s.resetHandlers { f() } + return []byte("ATR " + s.name), nil } +// HandlePing responds to a ping request. func (s *Service) HandlePing([]byte) ([]byte, error) { return nil, nil } +// HandleSetOption handles Set_Option requests. func (s *Service) HandleSetOption(args []byte) ([]byte, error) { opts := bytes.SplitN(args, []byte{' '}, 2) key := string(opts[0]) val := string(opts[1]) + if handler, ok := s.optionHandlers[key]; ok { handler(key, val) } + return nil, nil } +// HandleCapabilitiesRegister sends the Service's capabilities to the vmx. func (s *Service) HandleCapabilitiesRegister([]byte) ([]byte, error) { for _, capability := range s.capabilities { _, err := s.Out.Request([]byte(capability)) if err != nil { - return nil, fmt.Errorf("error sending %q: %s", capability, err) + return nil, fmt.Errorf("error sending %q: %w", capability, err) } } + return nil, nil } diff --git a/internal/talosapi/client.go b/internal/talosapi/client.go index c8eb975..0c0af4b 100644 --- a/internal/talosapi/client.go +++ b/internal/talosapi/client.go @@ -1,12 +1,12 @@ +// Package talosapi represents the Talos API client. package talosapi import ( "context" "fmt" - "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" "github.com/golang/protobuf/ptypes/empty" - "github.com/siderolabs/talos-vmtoolsd/internal/tboxcmds" "github.com/siderolabs/talos/pkg/grpc/middleware/authz" "github.com/siderolabs/talos/pkg/machinery/api/machine" talosclient "github.com/siderolabs/talos/pkg/machinery/client" @@ -18,14 +18,18 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + + "github.com/siderolabs/talos-vmtoolsd/internal/tboxcmds" ) +// LocalClient represents the Talos API client. type LocalClient struct { - ctx context.Context + ctx context.Context //nolint:containedctx log logrus.FieldLogger api *talosclient.Client } +// Close closes the client. func (c *LocalClient) Close() error { return c.api.Close() } @@ -35,14 +39,17 @@ func (c *LocalClient) connectToApid(configPath string, k8sHost string) (*taloscl if err != nil { return nil, fmt.Errorf("failed to open config file %q: %w", configPath, err) } + opts := []talosclient.OptionFunc{ talosclient.WithConfig(cfg), talosclient.WithEndpoints(k8sHost), } + api, err := talosclient.New(c.ctx, opts...) if err != nil { return nil, fmt.Errorf("failed to construct client: %w", err) } + return api, nil } @@ -54,17 +61,21 @@ func (c *LocalClient) connectToMachined() (*talosclient.Client, error) { md := metadata.New(nil) authz.SetMetadata(md, talosrole.MakeSet(talosrole.Admin)) c.ctx = metadata.NewOutgoingContext(c.ctx, md) + api, err := talosclient.New(c.ctx, opts...) if err != nil { return nil, fmt.Errorf("failed to construct client: %w", err) } + return api, nil } +// Shutdown shuts down the machine. func (c *LocalClient) Shutdown() error { return c.api.Shutdown(c.ctx) } +// Reboot reboots the machine. func (c *LocalClient) Reboot() error { return c.api.Reboot(c.ctx) } @@ -73,106 +84,126 @@ func (c *LocalClient) osVersionInfo() (*machine.VersionInfo, error) { resp, err := c.api.Version(c.ctx) if err != nil || len(resp.Messages) == 0 { return nil, err - } else { - return resp.Messages[0].Version, nil } + + return resp.Messages[0].Version, nil } +// OSVersion returns the OS version. func (c *LocalClient) OSVersion() string { v, err := c.osVersionInfo() if err != nil { c.log.WithError(err).Error("error retrieving OS version information") + return "Talos" } + return fmt.Sprintf("Talos %s-%s", v.Tag, v.Sha) } +// OSVersionShort returns the short OS version. func (c *LocalClient) OSVersionShort() string { v, err := c.osVersionInfo() if err != nil { c.log.WithError(err).Error("error retrieving OS version information") + return "Talos" } + return fmt.Sprintf("Talos %s", v.Tag) } +// Hostname returns the hostname. func (c *LocalClient) Hostname() string { resp, err := c.api.MachineClient.Hostname(c.ctx, &empty.Empty{}) if err != nil || len(resp.Messages) == 0 { c.log.WithError(err).Error("error retrieving hostname") + return "" - } else { - return resp.Messages[0].Hostname } + + return resp.Messages[0].Hostname } +// NetInterfaces returns the network interfaces. func (c *LocalClient) NetInterfaces() (result []tboxcmds.NetInterface) { addrMap := make(map[string][]*network.AddressStatusSpec) - addrStatusMap, err := c.api.COSI.List(c.ctx, resource.NewMetadata( - network.NamespaceName, - network.AddressStatusType, - "", - resource.VersionUndefined, - )) + + networkAddresses, err := safe.StateListAll[*network.AddressStatus](c.ctx, c.api.COSI) if err != nil { c.log.WithError(err).Error("error listing address status resources") + return nil } - for _, res := range addrStatusMap.Items { - spec := res.(*network.AddressStatus).Spec().(*network.AddressStatusSpec) - addrMap[spec.LinkName] = append(addrMap[spec.LinkName], spec) + + iter := networkAddresses.Iterator() + + for iter.Next() { + linkName := iter.Value().TypedSpec().LinkName + addrMap[linkName] = append(addrMap[linkName], iter.Value().TypedSpec()) } - linkStatusList, err := c.api.COSI.List(c.ctx, resource.NewMetadata( - network.NamespaceName, - network.LinkStatusType, - "", - resource.VersionUndefined, - )) + linkStatuses, err := safe.StateListAll[*network.LinkStatus](c.ctx, c.api.COSI) if err != nil { c.log.WithError(err).Error("error listing link status resources") + return nil } - for _, res := range linkStatusList.Items { - spec := res.(*network.LinkStatus).Spec().(*network.LinkStatusSpec) - if !spec.Physical() { + + linksIter := linkStatuses.Iterator() + + for linksIter.Next() { + link := linksIter.Value().TypedSpec() + + if !link.Physical() { continue } + intf := tboxcmds.NetInterface{ - Name: res.Metadata().ID(), - MAC: spec.HardwareAddr.String(), + Name: linksIter.Value().Metadata().ID(), + MAC: link.HardwareAddr.String(), } + for _, addr := range addrMap[intf.Name] { intf.Addrs = append(intf.Addrs, addr.Address) } + result = append(result, intf) } - return + return result } +// NewLocalClient creates a new Talos API client. func NewLocalClient(ctx context.Context, log logrus.FieldLogger, configPath string, k8sHost string) (*LocalClient, error) { var err error + c := &LocalClient{ ctx: ctx, log: log.WithField("module", "talosapi"), } + c.api, err = c.connectToApid(configPath, k8sHost) if err != nil { - return nil, fmt.Errorf("failed to connect to apid: %v", err) + return nil, fmt.Errorf("failed to connect to apid: %w", err) } + return c, nil } +// NewLocalSocketClient creates a new Talos API client using a local socket. func NewLocalSocketClient(ctx context.Context, log logrus.FieldLogger) (*LocalClient, error) { var err error + c := &LocalClient{ ctx: ctx, log: log.WithField("module", "talosapi"), } + c.api, err = c.connectToMachined() if err != nil { - return nil, fmt.Errorf("failed to connect to machined: %v", err) + return nil, fmt.Errorf("failed to connect to machined: %w", err) } + return c, nil } diff --git a/internal/tboxcmds/guestinfo.go b/internal/tboxcmds/guestinfo.go index 858e35d..025a7ef 100644 --- a/internal/tboxcmds/guestinfo.go +++ b/internal/tboxcmds/guestinfo.go @@ -3,36 +3,46 @@ package tboxcmds import ( "bytes" "fmt" - "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" + "net/netip" + "github.com/sirupsen/logrus" xdr "github.com/stellar/go-xdr/xdr3" - "net/netip" + + "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" ) const ( _ = iota + // GuestInfoDNSName is the guest info kind for the DNS name. GuestInfoDNSName _ // IP v1 _ // free disk space _ // build number + // GuestInfoOSNameFull is the guest info kind for the full OS name. GuestInfoOSNameFull + // GuestInfoOSName is the guest info kind for the OS name. GuestInfoOSName _ // uptime _ // memory _ // IP v2 + // GuestInfoIPAddressV3 is the guest info kind for the IP address. GuestInfoIPAddressV3 _ // OS detailed ) -const unknownIP = "unknown" -const maxNICs = 16 +const ( + unknownIP = "unknown" + maxNICs = 16 +) +// NetInterface represents a network interface. type NetInterface struct { Name string MAC string // xx:xx:xx:xx:xx:xx Addrs []netip.Prefix } +// NicDelegate is the interface that must be implemented by the delegate. type NicDelegate interface { NetInterfaces() []NetInterface Hostname() string @@ -40,48 +50,61 @@ type NicDelegate interface { OSVersionShort() string } +// GuestInfoCommands provides a set of commands for the vmx. type GuestInfoCommands struct { log logrus.FieldLogger out *nanotoolbox.ChannelOut delegate NicDelegate } +// PrimaryIP returns the primary IP address. func (cmd *GuestInfoCommands) PrimaryIP() string { ifs := cmd.delegate.NetInterfaces() if len(ifs) < 1 { cmd.log.Warn("not sending primary IP: no interfaces received from upstream") + return unknownIP } + addrs := ifs[0].Addrs if len(addrs) < 1 { cmd.log.Warn("not sending primary IP: first upstream adapter has no addresses") + return unknownIP } + return addrs[0].String() } +// GuestNicInfo represents the guest NIC info. func (cmd *GuestInfoCommands) GuestNicInfo() *GuestNicInfo { // NB: this is polled by vSphere roughly every 30s info := NewGuestNicInfo() ifs := cmd.delegate.NetInterfaces() + for _, nic := range ifs { nicDesc := GuestNicV3{MacAddress: nic.MAC} for _, addr := range nic.Addrs { nicDesc.AddIP(addr) cmd.log.Debugf("GuestNicInfo: adding name=%v mac=%v ip=%v", nic.Name, nic.MAC, addr) } + info.V3.Nics = append(info.V3.Nics, nicDesc) if len(info.V3.Nics) >= maxNICs { cmd.log.Debugf("GuestNicInfo: truncating NIC list to %v NICs", maxNICs) + break } } + if hostname := cmd.delegate.Hostname(); hostname != "" { info.V3.DNSConfigInfo = &DNSConfigInfo{HostName: &hostname} } + return info } +// SendGuestInfo sends the guest info. func (cmd *GuestInfoCommands) SendGuestInfo(kind int, buf []byte) { // NB: intentionally using two spaces as separator to match open-vm-tools msg := append([]byte(fmt.Sprintf("SetGuestInfo %d ", kind)), buf...) @@ -90,20 +113,26 @@ func (cmd *GuestInfoCommands) SendGuestInfo(kind int, buf []byte) { } } +// SendGuestInfoString sends the guest info string. func (cmd *GuestInfoCommands) SendGuestInfoString(kind int, str string) { cmd.SendGuestInfo(kind, []byte(str)) } +// SendGuestInfoXDR sends the guest info XDR. func (cmd *GuestInfoCommands) SendGuestInfoXDR(kind int, v interface{}) { var buf bytes.Buffer + _, err := xdr.Marshal(&buf, v) if err != nil { cmd.log.WithError(err).WithField("guest_info_kind", kind).Error("error encoding guest info") + return } + cmd.SendGuestInfo(kind, buf.Bytes()) } +// SendGuestInfoDNSName sends the guest info DNS name. func (cmd *GuestInfoCommands) SendGuestInfoDNSName() { if hostname := cmd.delegate.Hostname(); hostname != "" { cmd.log.Debugf("sending hostname: %v", hostname) @@ -111,6 +140,7 @@ func (cmd *GuestInfoCommands) SendGuestInfoDNSName() { } } +// SendGuestInfoOSNameFull sends the guest info OS full name. func (cmd *GuestInfoCommands) SendGuestInfoOSNameFull() { if name := cmd.delegate.OSVersion(); name != "" { cmd.log.Debugf("sending OS full name: %v", name) @@ -118,6 +148,7 @@ func (cmd *GuestInfoCommands) SendGuestInfoOSNameFull() { } } +// SendGuestInfoOSName sends the guest info OS name. func (cmd *GuestInfoCommands) SendGuestInfoOSName() { if name := cmd.delegate.OSVersionShort(); name != "" { cmd.log.Debugf("sending OS short name: %v", name) @@ -125,18 +156,22 @@ func (cmd *GuestInfoCommands) SendGuestInfoOSName() { } } +// SendGuestInfoNIC sends the guest info NIC. func (cmd *GuestInfoCommands) SendGuestInfoNIC() { cmd.SendGuestInfoXDR(GuestInfoIPAddressV3, cmd.GuestNicInfo()) } +// BroadcastIPOptionHandler handles the broadcast IP option. func (cmd *GuestInfoCommands) BroadcastIPOptionHandler(string, string) { msg := fmt.Sprintf("info-set guestinfo.ip %s", cmd.PrimaryIP()) if _, err := cmd.out.Request([]byte(msg)); err != nil { cmd.log.WithError(err).Error("error sending IP message") } + cmd.SendGuestInfoNIC() } +// PushGuestInfo pushes the guest info. func (cmd *GuestInfoCommands) PushGuestInfo() { cmd.SendGuestInfoDNSName() cmd.SendGuestInfoOSNameFull() @@ -144,6 +179,7 @@ func (cmd *GuestInfoCommands) PushGuestInfo() { cmd.SendGuestInfoNIC() } +// RegisterGuestInfoCommands registers the guest info commands. func RegisterGuestInfoCommands(svc *nanotoolbox.Service, delegate NicDelegate) { cmd := &GuestInfoCommands{ log: svc.Log.WithField("module", "tboxcmds"), diff --git a/internal/tboxcmds/nicinfo.go b/internal/tboxcmds/nicinfo.go index 2c4f57a..01dde2a 100644 --- a/internal/tboxcmds/nicinfo.go +++ b/internal/tboxcmds/nicinfo.go @@ -20,19 +20,22 @@ package tboxcmds import "net/netip" -type TypedIPAddress struct { +// TypedIPAddress represents an IP address with a type. +type TypedIPAddress struct { //nolint:govet Type int32 Address []byte } -type IPAddressEntry struct { +// IPAddressEntry represents an IP address with prefix length, origin, and status. +type IPAddressEntry struct { //nolint:govet Address TypedIPAddress PrefixLength uint32 Origin *int32 `xdr:"optional"` Status *int32 `xdr:"optional"` } -type InetCidrRouteEntry struct { +// InetCidrRouteEntry represents a route entry. +type InetCidrRouteEntry struct { //nolint:govet Dest TypedIPAddress PrefixLength uint32 NextHop *TypedIPAddress `xdr:"optional"` @@ -41,24 +44,28 @@ type InetCidrRouteEntry struct { Metric uint32 } -type DNSConfigInfo struct { +// DNSConfigInfo represents DNS configuration. +type DNSConfigInfo struct { //nolint:govet HostName *string `xdr:"optional"` DomainName *string `xdr:"optional"` Servers []TypedIPAddress Search *string `xdr:"optional"` } +// WinsConfigInfo represents WINS configuration. type WinsConfigInfo struct { Primary TypedIPAddress Secondary TypedIPAddress } -type DhcpConfigInfo struct { +// DhcpConfigInfo represents DHCP configuration. +type DhcpConfigInfo struct { //nolint:govet Enabled bool Settings string } -type GuestNicV3 struct { +// GuestNicV3 represents NIC information. +type GuestNicV3 struct { //nolint:govet MacAddress string IPs []IPAddressEntry DNSConfigInfo *DNSConfigInfo `xdr:"optional"` @@ -67,14 +74,17 @@ type GuestNicV3 struct { DhcpConfigInfov6 *DhcpConfigInfo `xdr:"optional"` } -type GuestNicInfo struct { +// GuestNicInfo represents NIC information. +type GuestNicInfo struct { //nolint:govet Version int32 V3 *NicInfoV3 `xdr:"optional"` } +// AddIP adds an IP address to the NIC. func (nic *GuestNicV3) AddIP(prefix netip.Prefix) { addr := prefix.Addr() kind := int32(1) // IAT_IPV4 + if addr.Is6() { kind = 2 // IAT_IPV6 } else if addr.Is4In6() { @@ -96,7 +106,8 @@ func (nic *GuestNicV3) AddIP(prefix netip.Prefix) { nic.IPs = append(nic.IPs, e) } -type NicInfoV3 struct { +// NicInfoV3 contains NIC information. +type NicInfoV3 struct { //nolint:govet Nics []GuestNicV3 Routes []InetCidrRouteEntry DNSConfigInfo *DNSConfigInfo `xdr:"optional"` @@ -105,6 +116,7 @@ type NicInfoV3 struct { DhcpConfigInfov6 *DhcpConfigInfo `xdr:"optional"` } +// NewGuestNicInfo creates a new GuestNicInfo. func NewGuestNicInfo() *GuestNicInfo { return &GuestNicInfo{ Version: 3, diff --git a/internal/tboxcmds/power.go b/internal/tboxcmds/power.go index e6611dd..a2f3dcf 100644 --- a/internal/tboxcmds/power.go +++ b/internal/tboxcmds/power.go @@ -2,11 +2,13 @@ package tboxcmds import ( "fmt" - "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" + "github.com/sirupsen/logrus" + + "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" ) -// vmware/guestrpc/powerops.h +// vmware/guestrpc/powerops.h. const ( _ = iota PowerStateHalt @@ -24,14 +26,16 @@ var powerCmdName = map[int]string{ PowerStateSuspend: "OS_Suspend", } +// PowerDelegate is the interface that must be implemented by the delegate. type PowerDelegate interface { Shutdown() error Reboot() error } +// PowerHandler is the type of the function that handles power operations. type PowerHandler func() error -type powerOp struct { +type powerOp struct { //nolint:govet log logrus.FieldLogger out *nanotoolbox.ChannelOut state int @@ -47,9 +51,11 @@ func (op powerOp) HandleCommand([]byte) ([]byte, error) { l.Debug("handling power operation") rc := nanotoolbox.RpciOK + if op.handler != nil { if err := op.handler(); err != nil { l.WithError(err).Error("error handling power operation") + rc = nanotoolbox.RpciERR } } @@ -69,9 +75,11 @@ func powerOpHandler(svc *nanotoolbox.Service, state int, handler PowerHandler) ( state: state, handler: handler, } + return op.Name(), op.HandleCommand } +// RegisterPowerDelegate registers the power operations with the service. func RegisterPowerDelegate(svc *nanotoolbox.Service, delegate PowerDelegate) { svc.AddCapability("tools.capability.statechange") svc.RegisterCommandHandler(powerOpHandler(svc, PowerStateHalt, delegate.Shutdown)) diff --git a/internal/tboxcmds/vix.go b/internal/tboxcmds/vix.go index 691e3fb..659ea1e 100644 --- a/internal/tboxcmds/vix.go +++ b/internal/tboxcmds/vix.go @@ -17,6 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package tboxcmds provides a set of commands for the vmx. package tboxcmds import ( @@ -24,25 +25,32 @@ import ( "encoding/base64" "encoding/binary" "fmt" - tvmtoolsd "github.com/siderolabs/talos-vmtoolsd" - "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" + "github.com/sirupsen/logrus" "github.com/vmware/govmomi/toolbox/vix" + + "github.com/siderolabs/talos-vmtoolsd/internal/nanotoolbox" + "github.com/siderolabs/talos-vmtoolsd/internal/version" ) const ( + // VixToolsFeatureSupportGetHandleState defines the VixToolsFeatureSupportGetHandleState feature. VixToolsFeatureSupportGetHandleState = 1 - VixGuestOfFamilyLinux = 1 + // VixGuestOfFamilyLinux defines the VixGuestOfFamilyLinux family. + VixGuestOfFamilyLinux = 1 ) +// VixCommandHandler is a function that handles a Vix command. type VixCommandHandler func(vix.CommandRequestHeader, []byte) ([]byte, error) +// VixDelegate is the interface that must be implemented by the delegate. type VixDelegate interface { OSVersion() string OSVersionShort() string Hostname() string } +// VixCommandServer provides a set of commands for the vmx. type VixCommandServer struct { log logrus.FieldLogger out *nanotoolbox.ChannelOut @@ -50,6 +58,7 @@ type VixCommandServer struct { handlers map[uint32]VixCommandHandler } +// RegisterVixCommand registers the Vix command. func RegisterVixCommand(svc *nanotoolbox.Service, delegate VixDelegate) { svr := &VixCommandServer{ log: svc.Log.WithField("command", "vix"), @@ -86,42 +95,53 @@ func commandResult(header vix.CommandRequestHeader, rc int, err error, response return buf.Bytes() } +// Dispatch dispatches the Vix command. func (c *VixCommandServer) Dispatch(data []byte) ([]byte, error) { // See ToolsDaemonTcloGetQuotedString if data[0] == '"' { data = data[1:] } + name := "" + ix := bytes.IndexByte(data, '"') if ix > 0 { name = string(data[:ix]) data = data[ix+1:] } + if data[0] == 0 { data = data[1:] } + l := c.log.WithField("command_name", name) var header vix.CommandRequestHeader + buf := bytes.NewBuffer(data) + err := binary.Read(buf, binary.LittleEndian, &header) if err != nil { l.WithError(err).Print("decoding command failed") + return nil, err } if header.Magic != vix.CommandMagicWord { l.Print("invalid magic header for command") + return commandResult(header, vix.InvalidMessageHeader, nil, nil), nil } handler, ok := c.handlers[header.OpCode] if !ok { l.Warn("unhandled command") + return commandResult(header, vix.UnrecognizedCommandInGuest, nil, nil), nil } rc := vix.OK + response, err := handler(header, buf.Bytes()) if err != nil { l.WithError(err).Error("command handler failed") @@ -131,28 +151,32 @@ func (c *VixCommandServer) Dispatch(data []byte) ([]byte, error) { return commandResult(header, rc, err, response), nil } +// RegisterHandler registers the Vix command handler. func (c *VixCommandServer) RegisterHandler(op uint32, handler VixCommandHandler) { c.handlers[op] = handler } +// GetToolsState returns the tools state. func (c *VixCommandServer) GetToolsState(_ vix.CommandRequestHeader, _ []byte) ([]byte, error) { - version := c.delegate.OSVersion() + osVersion := c.delegate.OSVersion() versionShort := c.delegate.OSVersionShort() hostname := c.delegate.Hostname() c.log.Debugf("sending tools state version=%q versionShort=%q hostname=%q", - version, versionShort, hostname) + osVersion, versionShort, hostname) + props := vix.PropertyList{ - vix.NewStringProperty(vix.PropertyGuestOsVersion, version), + vix.NewStringProperty(vix.PropertyGuestOsVersion, osVersion), vix.NewStringProperty(vix.PropertyGuestOsVersionShort, versionShort), vix.NewStringProperty(vix.PropertyGuestToolsProductNam, "Talos Tools"), - vix.NewStringProperty(vix.PropertyGuestToolsVersion, tvmtoolsd.Version), + vix.NewStringProperty(vix.PropertyGuestToolsVersion, version.Version), vix.NewStringProperty(vix.PropertyGuestName, hostname), vix.NewInt32Property(vix.PropertyGuestToolsAPIOptions, VixToolsFeatureSupportGetHandleState), vix.NewInt32Property(vix.PropertyGuestOsFamily, VixGuestOfFamilyLinux), } - src, _ := props.MarshalBinary() + src, _ := props.MarshalBinary() //nolint:errcheck enc := base64.StdEncoding buf := make([]byte, enc.EncodedLen(len(src))) enc.Encode(buf, src) + return buf, nil } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..c5053fb --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,5 @@ +// Package version provides the Talos VMware Tools Daemon +package version + +// Version is the version of the Talos VMware Tools Daemon. +const Version = "0.5.0" diff --git a/version.go b/version.go deleted file mode 100644 index 6485128..0000000 --- a/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package tvmtoolsd - -const Version = "0.5.0"