From 949ad11a2d6374c518ed50a628a1f069a05345f3 Mon Sep 17 00:00:00 2001 From: Dmitriy Matrenichev Date: Thu, 21 Mar 2024 20:54:50 +0300 Subject: [PATCH] chore: import siderolink as `siderolink-launch` subcommand This PR ensures that we can test our siderolink communication using embedded siderolink-agent. If `--with-siderolink` provided during `talos cluster create` talosctl will embed proper kernel string and setup `siderolink-agent` as a separate process. It should be used with combination of `--skip-injecting-config` and `--with-apply-config` (the latter will use newly generated IPv6 siderolink addresses which talosctl passes to the agent as a "pre-bind"). Fixes #8392 Signed-off-by: Dmitriy Matrenichev --- .drone.jsonnet | 8 + cmd/talosctl/cmd/mgmt/cluster/create.go | 171 +++++++++++++++++- cmd/talosctl/cmd/mgmt/cluster/wg_linux.go | 42 +++++ cmd/talosctl/cmd/mgmt/cluster/wg_other.go | 26 +++ .../cmd/mgmt/siderolink_launch_linux.go | 104 +++++++++++ cmd/talosctl/cmd/root.go | 3 +- go.mod | 2 +- go.sum | 4 +- hack/test/e2e-qemu.sh | 8 + .../pkg/controllers/runtime/kmsg_log_test.go | 6 +- pkg/cluster/apply-config.go | 13 +- pkg/machinery/resources/network/ula.go | 3 + pkg/provision/options.go | 11 ++ pkg/provision/providers/qemu/create.go | 10 + pkg/provision/providers/qemu/destroy.go | 6 + .../providers/vm/siderolink-agent.go | 75 ++++++++ pkg/provision/request.go | 52 ++++++ website/content/v1.7/reference/cli.md | 1 + 18 files changed, 534 insertions(+), 11 deletions(-) create mode 100644 cmd/talosctl/cmd/mgmt/cluster/wg_linux.go create mode 100644 cmd/talosctl/cmd/mgmt/cluster/wg_other.go create mode 100644 cmd/talosctl/cmd/mgmt/siderolink_launch_linux.go create mode 100644 pkg/provision/providers/vm/siderolink-agent.go diff --git a/.drone.jsonnet b/.drone.jsonnet index 1c7405fb0a..4a0d534d28 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -644,6 +644,13 @@ local integration_cloud_images = Step('cloud-images', depends_on=[integration_im local integration_reproducibility_test = Step('reproducibility-test', target='reproducibility-test', depends_on=[load_artifacts], environment={ IMAGE_REGISTRY: local_registry }); +local integration_siderolink = Step('e2e-siderolink', target='e2e-qemu', privileged=true, depends_on=[integration_default_hostname], environment={ + SHORT_INTEGRATION_TEST: 'yes', + WITH_SIDEROLINK_AGENT: 'true', + VIA_MAINTENANCE_MODE: 'true', + REGISTRY: local_registry, +}); + local push_edge = { name: 'push-edge', image: 'autonomy/build-container:latest', @@ -697,6 +704,7 @@ local integration_pipelines = [ integration_no_cluster_discovery, integration_kubespan, integration_default_hostname, + integration_siderolink, ]) + integration_trigger(['integration-misc']), Pipeline('integration-extensions', default_pipeline_steps + integration_extensions) + integration_trigger(['integration-extensions']), Pipeline('integration-cilium', default_pipeline_steps + [integration_cilium, integration_cilium_strict, integration_cilium_strict_kubespan]) + integration_trigger(['integration-cilium']), diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index 8ad46690ec..7770e1d225 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -9,11 +9,13 @@ import ( "errors" "fmt" "math/big" + "net" "net/netip" "net/url" "os" "path/filepath" stdruntime "runtime" + "slices" "strconv" "strings" "time" @@ -172,6 +174,7 @@ var ( diskEncryptionKeyTypes []string withFirewall string withUUIDHostnames bool + withSiderolinkAgent bool ) // createCmd represents the cluster up command. @@ -422,6 +425,7 @@ func create(ctx context.Context, flags *pflag.FlagSet) error { provision.WithTPM2(tpm2Enabled), provision.WithExtraUEFISearchPaths(extraUEFISearchPaths), provision.WithTargetArch(targetArch), + provision.WithSiderolinkAgent(withSiderolinkAgent), } var configBundleOpts []bundle.Option @@ -746,6 +750,40 @@ func create(ctx context.Context, flags *pflag.FlagSet) error { extraKernelArgs = procfs.NewCmdline(extraBootKernelArgs) } + wgNodeGen := makeNodeAddrGenerator() + + if withSiderolinkAgent { + if extraKernelArgs == nil { + extraKernelArgs = procfs.NewCmdline("") + } + + if extraKernelArgs.Get("siderolink.api") != nil || extraKernelArgs.Get("talos.events.sink") != nil || extraKernelArgs.Get("talos.logging.kernel") != nil { + return errors.New("siderolink kernel arguments are already set, cannot run with --with-siderolink") + } + + wgHost := gatewayIPs[0].String() + + ports, err := getDynamicPorts() + if err != nil { + return err + } + + request.SiderolinkRequest.WireguardEndpoint = net.JoinHostPort(wgHost, ports.wgPort) + request.SiderolinkRequest.APIEndpoint = ":" + ports.apiPort + request.SiderolinkRequest.SinkEndpoint = ":" + ports.sinkPort + request.SiderolinkRequest.LogEndpoint = ":" + ports.logPort + + agentNodeAddr := wgNodeGen.GetAgentNodeAddr() + + apiLink := "grpc://" + net.JoinHostPort(wgHost, ports.apiPort) + "?jointoken=foo" + sinkURL := net.JoinHostPort(agentNodeAddr, ports.sinkPort) + kernelURL := "tcp://" + net.JoinHostPort(agentNodeAddr, ports.logPort) + + extraKernelArgs.Append("siderolink.api", apiLink) + extraKernelArgs.Append("talos.events.sink", sinkURL) + extraKernelArgs.Append("talos.logging.kernel", kernelURL) + } + // Add talosconfig to provision options, so we'll have it to parse there provisionOptions = append(provisionOptions, provision.WithTalosConfig(configBundle.TalosConfig())) @@ -760,6 +798,17 @@ func create(ctx context.Context, flags *pflag.FlagSet) error { nodeUUID := uuid.New() + if withSiderolinkAgent { + var generated netip.Addr + + generated, err = wgNodeGen.GenerateRandomNodeAddr() + if err != nil { + return err + } + + request.SiderolinkRequest.AddBind(nodeUUID, generated) + } + nodeReq := provision.NodeRequest{ Name: nodeName(clusterName, "controlplane", i+1, nodeUUID), Type: machine.TypeControlPlane, @@ -820,6 +869,17 @@ func create(ctx context.Context, flags *pflag.FlagSet) error { nodeUUID := uuid.New() + if withSiderolinkAgent { + var generated netip.Addr + + generated, err = wgNodeGen.GenerateRandomNodeAddr() + if err != nil { + return err + } + + request.SiderolinkRequest.AddBind(nodeUUID, generated) + } + request.Nodes = append(request.Nodes, provision.NodeRequest{ Name: nodeName(clusterName, "worker", i, nodeUUID), @@ -855,7 +915,7 @@ func create(ctx context.Context, flags *pflag.FlagSet) error { defer clusterAccess.Close() //nolint:errcheck if applyConfigEnabled { - err = clusterAccess.ApplyConfig(ctx, request.Nodes, os.Stdout) + err = clusterAccess.ApplyConfig(ctx, request.Nodes, request.SiderolinkRequest, os.Stdout) if err != nil { return err } @@ -1153,6 +1213,7 @@ func init() { createCmd.Flags().IntVar(&bandwidth, "with-network-bandwidth", 0, "specify bandwidth restriction (in kbps) on the bridge interface when creating a qemu cluster") createCmd.Flags().StringVar(&withFirewall, firewallFlag, "", "inject firewall rules into the cluster, value is default policy - accept/block (QEMU only)") createCmd.Flags().BoolVar(&withUUIDHostnames, "with-uuid-hostnames", false, "use machine UUIDs as default hostnames (QEMU only)") + createCmd.Flags().BoolVar(&withSiderolinkAgent, "with-siderolink", false, "enables the use of siderolink agent as configuration apply mechanism") Cmd.AddCommand(createCmd) } @@ -1192,3 +1253,111 @@ func checkForDefinedGenFlag(flags *pflag.FlagSet) string { return "" } + +type generatedPorts struct { + wgPort string + apiPort string + sinkPort string + logPort string +} + +func getDynamicPorts() (generatedPorts, error) { + var resultErr error + + for range 10 { + wgPort, err := getDynamicPort("udp") + if err != nil { + return generatedPorts{}, fmt.Errorf("failed to get dynamic port for WireGuard: %w", err) + } + + apiPort, err := getDynamicPort("tcp") + if err != nil { + return generatedPorts{}, fmt.Errorf("failed to get dynamic port for GRPC API: %w", err) + } + + sinkPort, err := getDynamicPort("tcp") + if err != nil { + return generatedPorts{}, fmt.Errorf("failed to get dynamic port for Sink: %w", err) + } + + logPort, err := getDynamicPort("tcp") + if err != nil { + return generatedPorts{}, fmt.Errorf("failed to get dynamic port for Log: %w", err) + } + + resultErr = checkPortsDontOverlap(wgPort, apiPort, sinkPort, logPort) + if resultErr != nil { + continue + } + + return generatedPorts{ + wgPort: strconv.Itoa(wgPort), + apiPort: strconv.Itoa(apiPort), + sinkPort: strconv.Itoa(sinkPort), + logPort: strconv.Itoa(logPort), + }, nil + } + + return generatedPorts{}, fmt.Errorf("failed to get non-overlapping dynamic ports in 10 attempts: %w", resultErr) +} + +func getDynamicPort(network string) (int, error) { + var ( + closeFn func() error + addrFn func() net.Addr + ) + + switch network { + case "tcp", "tcp4", "tcp6": + l, err := net.Listen(network, "127.0.0.1:0") + if err != nil { + return 0, err + } + + addrFn, closeFn = l.Addr, l.Close + case "udp", "udp4", "udp6": + l, err := net.ListenPacket(network, "127.0.0.1:0") + if err != nil { + return 0, err + } + + addrFn, closeFn = l.LocalAddr, l.Close + default: + return 0, fmt.Errorf("unsupported network: %s", network) + } + + _, portStr, err := net.SplitHostPort(addrFn().String()) + if err != nil { + return 0, handleCloseErr(err, closeFn()) + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return 0, err + } + + return port, handleCloseErr(nil, closeFn()) +} + +func handleCloseErr(err error, closeErr error) error { + switch { + case err != nil && closeErr != nil: + return fmt.Errorf("error: %w, close error: %w", err, closeErr) + case err == nil && closeErr != nil: + return closeErr + case err != nil && closeErr == nil: + return err + default: + return nil + } +} + +func checkPortsDontOverlap(ports ...int) error { + slices.Sort(ports) + + if len(ports) != len(slices.Compact(ports)) { + return errors.New("generated ports overlap") + } + + return nil +} diff --git a/cmd/talosctl/cmd/mgmt/cluster/wg_linux.go b/cmd/talosctl/cmd/mgmt/cluster/wg_linux.go new file mode 100644 index 0000000000..93d3a131ed --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/cluster/wg_linux.go @@ -0,0 +1,42 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build linux + +package cluster + +import ( + "fmt" + "net/netip" + + "github.com/siderolabs/siderolink/pkg/wireguard" +) + +type nodeAddrGenerator struct { + prefix netip.Prefix + nodeAddr netip.Addr +} + +func makeNodeAddrGenerator() nodeAddrGenerator { + prefix := wireguard.NetworkPrefix("") + nodeAddr := prefix.Addr().Next() + + return nodeAddrGenerator{ + prefix: prefix, + nodeAddr: nodeAddr, + } +} + +func (ng *nodeAddrGenerator) GenerateRandomNodeAddr() (netip.Addr, error) { + result, err := wireguard.GenerateRandomNodeAddr(ng.prefix) + if err != nil { + return netip.Addr{}, fmt.Errorf("failed to generate random node address: %w", err) + } + + return result.Addr(), nil +} + +func (ng *nodeAddrGenerator) GetAgentNodeAddr() string { + return ng.nodeAddr.String() +} diff --git a/cmd/talosctl/cmd/mgmt/cluster/wg_other.go b/cmd/talosctl/cmd/mgmt/cluster/wg_other.go new file mode 100644 index 0000000000..30d4388492 --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/cluster/wg_other.go @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build !linux + +package cluster + +import ( + "errors" + "net/netip" +) + +type nodeAddrGenerator struct{} + +func (ng *nodeAddrGenerator) GenerateRandomNodeAddr() (netip.Addr, error) { + return netip.Addr{}, errors.New("unsupported platform") +} + +func (ng *nodeAddrGenerator) GetAgentNodeAddr() string { + return "" +} + +func makeNodeAddrGenerator() nodeAddrGenerator { + return nodeAddrGenerator{} +} diff --git a/cmd/talosctl/cmd/mgmt/siderolink_launch_linux.go b/cmd/talosctl/cmd/mgmt/siderolink_launch_linux.go new file mode 100644 index 0000000000..78b2488ff7 --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/siderolink_launch_linux.go @@ -0,0 +1,104 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mgmt + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/siderolabs/siderolink/pkg/agent" + "github.com/siderolabs/siderolink/pkg/wireguard" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "go.uber.org/zap" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +var siderolinkFlags struct { + joinToken string + wireguardEndpoint string + sinkEndpoint string + apiEndpoint string + logEndpoint string + predefinedPairs []string +} + +var siderolinkCmd = &cobra.Command{ + Use: "siderolink-launch", + Short: "Internal command used by cluster create to launch siderolink agent", + Long: ``, + Args: cobra.NoArgs, + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt) + defer cancel() + + return run(ctx) + }, +} + +func init() { + siderolinkCmd.PersistentFlags().StringVar(&siderolinkFlags.joinToken, "sidero-link-join-token", "", "join token for the cluster") + siderolinkCmd.PersistentFlags().StringVar(&siderolinkFlags.wireguardEndpoint, "sidero-link-wireguard-endpoint", "", "advertised Wireguard endpoint") + siderolinkCmd.PersistentFlags().StringVar(&siderolinkFlags.sinkEndpoint, "event-sink-endpoint", "", "gRPC API endpoint for the Event Sink") + siderolinkCmd.PersistentFlags().StringVar(&siderolinkFlags.apiEndpoint, "sidero-link-api-endpoint", "", "gRPC API endpoint for the SideroLink") + siderolinkCmd.PersistentFlags().StringVar(&siderolinkFlags.logEndpoint, "log-receiver-endpoint", "", "TCP log receiver endpoint") + siderolinkCmd.PersistentFlags().StringArrayVar(&siderolinkFlags.predefinedPairs, "predefined-pair", nil, "predefined pairs of UUID=IPv6 addrs for the nodes") + + siderolinkCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) { + err := siderolinkCmd.PersistentFlags().MarkHidden(flag.Name) + if err != nil { + panic(err) + } + }) + + addCommand(siderolinkCmd) +} + +func run(ctx context.Context) error { + logger, err := zap.NewDevelopment() + if err != nil { + return err + } + + logger.Info("starting embedded siderolink agent") + defer logger.Info("stopping embedded siderolink agent") + + err = agent.Run( + ctx, + agent.Config{ + WireguardEndpoint: siderolinkFlags.wireguardEndpoint, + APIEndpoint: siderolinkFlags.apiEndpoint, + JoinToken: siderolinkFlags.joinToken, + SinkEndpoint: siderolinkFlags.sinkEndpoint, + LogEndpoint: siderolinkFlags.logEndpoint, + UUIDIPv6Pairs: siderolinkFlags.predefinedPairs, + ForceUserspace: true, + }, + &handler{l: logger}, + logger, + ) + if err != nil { + return fmt.Errorf("failed to run siderolink agent: %w", err) + } + + return nil +} + +type handler struct { + l *zap.Logger +} + +func (h *handler) HandlePeerAdded(event wireguard.PeerEvent) error { + h.l.Info("talos agent sees peer added", zap.String("address", event.Address.String())) + + return nil +} + +func (h *handler) HandlePeerRemoved(wgtypes.Key) error { + return nil +} diff --git a/cmd/talosctl/cmd/root.go b/cmd/talosctl/cmd/root.go index ec14ea4e43..b105fd6934 100644 --- a/cmd/talosctl/cmd/root.go +++ b/cmd/talosctl/cmd/root.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "github.com/spf13/cobra" @@ -67,7 +68,7 @@ func Execute() error { } func init() { - for _, cmd := range append(talos.Commands, mgmt.Commands...) { + for _, cmd := range slices.Concat(talos.Commands, mgmt.Commands) { rootCmd.AddCommand(cmd) } } diff --git a/go.mod b/go.mod index 83c6c357b8..fa0553eea4 100644 --- a/go.mod +++ b/go.mod @@ -136,7 +136,7 @@ require ( github.com/siderolabs/grpc-proxy v0.4.0 github.com/siderolabs/kms-client v0.1.0 github.com/siderolabs/net v0.4.0 - github.com/siderolabs/siderolink v0.3.5-0.20240312123444-8866351abf8d + github.com/siderolabs/siderolink v0.3.5 github.com/siderolabs/talos/pkg/machinery v1.7.0-alpha.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index e6b0d18d04..6044f5c12a 100644 --- a/go.sum +++ b/go.sum @@ -702,8 +702,8 @@ github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I= github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM= github.com/siderolabs/protoenc v0.2.1 h1:BqxEmeWQeMpNP3R6WrPqDatX8sM/r4t97OP8mFmg6GA= github.com/siderolabs/protoenc v0.2.1/go.mod h1:StTHxjet1g11GpNAWiATgc8K0HMKiFSEVVFOa/H0otc= -github.com/siderolabs/siderolink v0.3.5-0.20240312123444-8866351abf8d h1:KSNg9hpF//WW+uZ7+ersxyo7/EW/NjkZoOiah2yKt9A= -github.com/siderolabs/siderolink v0.3.5-0.20240312123444-8866351abf8d/go.mod h1:/7Dg0Nkh4q/8yqsY/VirDOTOFOqRvPikagCoyf3+Mf4= +github.com/siderolabs/siderolink v0.3.5 h1:sU4WNGCRGQYZ/sQZaVQbGfUNOqS561oL4kafKlo4FDY= +github.com/siderolabs/siderolink v0.3.5/go.mod h1:/7Dg0Nkh4q/8yqsY/VirDOTOFOqRvPikagCoyf3+Mf4= github.com/siderolabs/tcpproxy v0.1.0 h1:IbkS9vRhjMOscc1US3M5P1RnsGKFgB6U5IzUk+4WkKA= github.com/siderolabs/tcpproxy v0.1.0/go.mod h1:onn6CPPj/w1UNqQ0U97oRPF0CqbrgEApYCw4P9IiCW8= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/hack/test/e2e-qemu.sh b/hack/test/e2e-qemu.sh index af7244396d..e761b2bef0 100755 --- a/hack/test/e2e-qemu.sh +++ b/hack/test/e2e-qemu.sh @@ -153,6 +153,14 @@ case "${WITH_TRUSTED_BOOT_ISO:-false}" in ;; esac +case "${WITH_SIDEROLINK_AGENT:-false}" in + false) + ;; + *) + QEMU_FLAGS+=("--with-siderolink") + ;; +esac + function create_cluster { build_registry_mirrors diff --git a/internal/app/machined/pkg/controllers/runtime/kmsg_log_test.go b/internal/app/machined/pkg/controllers/runtime/kmsg_log_test.go index 454a71a337..80eee0ea52 100644 --- a/internal/app/machined/pkg/controllers/runtime/kmsg_log_test.go +++ b/internal/app/machined/pkg/controllers/runtime/kmsg_log_test.go @@ -88,11 +88,9 @@ func (suite *KmsgLogDeliverySuite) SetupTest() { suite.listener2, err = net.Listen("tcp", "localhost:0") suite.Require().NoError(err) - suite.srv1, err = logreceiver.NewServer(logger, suite.listener1, suite.handler1.HandleLog) - suite.Require().NoError(err) + suite.srv1 = logreceiver.NewServer(logger, suite.listener1, suite.handler1.HandleLog) - suite.srv2, err = logreceiver.NewServer(logger, suite.listener2, suite.handler2.HandleLog) - suite.Require().NoError(err) + suite.srv2 = logreceiver.NewServer(logger, suite.listener2, suite.handler2.HandleLog) suite.wg.Add(1) diff --git a/pkg/cluster/apply-config.go b/pkg/cluster/apply-config.go index be8f2cabdf..ec60e8c84e 100644 --- a/pkg/cluster/apply-config.go +++ b/pkg/cluster/apply-config.go @@ -7,6 +7,7 @@ package cluster import ( "context" "crypto/tls" + "fmt" "io" "time" @@ -24,12 +25,20 @@ type ApplyConfigClient struct { } // ApplyConfig on the node via the API using insecure mode. -func (s *APIBootstrapper) ApplyConfig(ctx context.Context, nodes []provision.NodeRequest, out io.Writer) error { +func (s *APIBootstrapper) ApplyConfig(ctx context.Context, nodes []provision.NodeRequest, sl provision.SiderolinkRequest, out io.Writer) error { for _, node := range nodes { configureNode := func() error { + ep := node.IPs[0].String() + + if addr, ok := sl.GetAddr(node.UUID); ok { + fmt.Fprintln(out, "using SideroLink node address for 'with-apply-config'", node.UUID, "=", addr.String()) + + ep = addr.String() + } + c, err := client.New(ctx, client.WithTLSConfig(&tls.Config{ InsecureSkipVerify: true, - }), client.WithEndpoints(node.IPs[0].String())) + }), client.WithEndpoints(ep)) if err != nil { return err } diff --git a/pkg/machinery/resources/network/ula.go b/pkg/machinery/resources/network/ula.go index 4d8d81f010..29aa6a31d3 100644 --- a/pkg/machinery/resources/network/ula.go +++ b/pkg/machinery/resources/network/ula.go @@ -24,6 +24,9 @@ const ( // ULASideroLink is the Unique Local Addressing space key for the SideroLink feature. ULASideroLink = 0x03 + + // ULAVirtualSideroLink is the Unique Local Addressing space key for the Virtual SideroLink over GRPC feature. + ULAVirtualSideroLink = 0x04 ) // ULAPrefix calculates and returns a Talos-specific Unique Local Address prefix for the given purpose. diff --git a/pkg/provision/options.go b/pkg/provision/options.go index f0ac5fdb22..95e5c8fb40 100644 --- a/pkg/provision/options.go +++ b/pkg/provision/options.go @@ -133,6 +133,15 @@ func WithKMS(endpoint string) Option { } } +// WithSiderolinkAgent enables or disables siderolink agent. +func WithSiderolinkAgent(v bool) Option { + return func(o *Options) error { + o.SiderolinkEnabled = v + + return nil + } +} + // Options describes Provisioner parameters. type Options struct { LogWriter io.Writer @@ -157,6 +166,8 @@ type Options struct { DeleteStateOnErr bool KMSEndpoint string + + SiderolinkEnabled bool } // DefaultOptions returns default options. diff --git a/pkg/provision/providers/qemu/create.go b/pkg/provision/providers/qemu/create.go index 076350e9c2..a2b960e8a1 100644 --- a/pkg/provision/providers/qemu/create.go +++ b/pkg/provision/providers/qemu/create.go @@ -47,6 +47,16 @@ func (p *provisioner) Create(ctx context.Context, request provision.ClusterReque return nil, err } + if options.SiderolinkEnabled { + fmt.Fprintln(options.LogWriter, "creating siderolink agent") + + if err = p.CreateSiderolinkAgent(state, request); err != nil { + return nil, err + } + + fmt.Fprintln(options.LogWriter, "created siderolink agent") + } + fmt.Fprintln(options.LogWriter, "creating network", request.Network.Name) if err = p.CreateNetwork(ctx, state, request.Network, options); err != nil { diff --git a/pkg/provision/providers/qemu/destroy.go b/pkg/provision/providers/qemu/destroy.go index 9135c4a88c..5f770bdcd9 100644 --- a/pkg/provision/providers/qemu/destroy.go +++ b/pkg/provision/providers/qemu/destroy.go @@ -82,6 +82,12 @@ func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, op return err } + fmt.Fprintln(options.LogWriter, "removing siderolink agent") + + if err := p.DestroySiderolinkAgent(state); err != nil { + return err + } + fmt.Fprintln(options.LogWriter, "removing state directory") return deleteStateDirectory(true) diff --git a/pkg/provision/providers/vm/siderolink-agent.go b/pkg/provision/providers/vm/siderolink-agent.go new file mode 100644 index 0000000000..bc98ea3095 --- /dev/null +++ b/pkg/provision/providers/vm/siderolink-agent.go @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package vm + +import ( + "errors" + "fmt" + "os" + "os/exec" + "strconv" + "syscall" + + "github.com/siderolabs/talos/pkg/provision" +) + +const ( + siderolinkAgentPid = "siderolink-agent.pid" + siderolinkAgentLog = "siderolink-agent.log" +) + +// CreateSiderolinkAgent creates siderlink agent. +func (p *Provisioner) CreateSiderolinkAgent(state *State, clusterReq provision.ClusterRequest) error { + pidPath := state.GetRelativePath(siderolinkAgentPid) + + logFile, err := os.OpenFile(state.GetRelativePath(siderolinkAgentLog), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o666) + if err != nil { + return err + } + + defer logFile.Close() //nolint:errcheck + + args := []string{ + "siderolink-launch", + "--sidero-link-join-token", "foo", + "--sidero-link-wireguard-endpoint", clusterReq.SiderolinkRequest.WireguardEndpoint, + "--event-sink-endpoint", clusterReq.SiderolinkRequest.SinkEndpoint, + "--sidero-link-api-endpoint", clusterReq.SiderolinkRequest.APIEndpoint, + "--log-receiver-endpoint", clusterReq.SiderolinkRequest.LogEndpoint, + } + + for _, bind := range clusterReq.SiderolinkRequest.SiderolinkBind { + args = append(args, "--predefined-pair", bind.UUID.String()+"="+bind.Addr.String()) + } + + cmd := exec.Command(clusterReq.SelfExecutable, args...) + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // daemonize + } + + if err = cmd.Start(); err != nil { + return err + } + + if err = os.WriteFile(pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), os.ModePerm); err != nil { + return fmt.Errorf("error writing SA PID file: %w", err) + } + + return nil +} + +// DestroySiderolinkAgent destroys siderolink agent. +func (p *Provisioner) DestroySiderolinkAgent(state *State) error { + pidPath := state.GetRelativePath(siderolinkAgentPid) + + if _, err := os.Stat(pidPath); errors.Is(err, os.ErrNotExist) { + // If the pid file does not exist, the process was not started. + return nil + } + + return StopProcessByPidfile(pidPath) +} diff --git a/pkg/provision/request.go b/pkg/provision/request.go index 33c218bc9b..83c2bb658f 100644 --- a/pkg/provision/request.go +++ b/pkg/provision/request.go @@ -6,7 +6,9 @@ package provision import ( "errors" + "fmt" "net/netip" + "slices" "time" "github.com/google/uuid" @@ -42,6 +44,8 @@ type ClusterRequest struct { // Path to root of state directory (~/.talos/clusters by default). StateDirectory string + + SiderolinkRequest SiderolinkRequest } // CNIConfig describes CNI part of NetworkRequest. @@ -199,3 +203,51 @@ type NodeRequest struct { TFTPServer string IPXEBootFilename string } + +// SiderolinkRequest describes a request for SideroLink agent. +type SiderolinkRequest struct { + WireguardEndpoint string + APIEndpoint string + SinkEndpoint string + LogEndpoint string + SiderolinkBind []SiderolinkBind +} + +// AddBind adds a pair of prebinded UUID->Addr for SideroLink agent. +func (sr *SiderolinkRequest) AddBind(id uuid.UUID, addr netip.Addr) { + idx := slices.IndexFunc(sr.SiderolinkBind, func(b SiderolinkBind) bool { return b.UUID == id }) + if idx != -1 { + panic(fmt.Errorf("duplicate UUID %s in SideroLink bind", id)) + } + + idx = slices.IndexFunc(sr.SiderolinkBind, func(b SiderolinkBind) bool { return b.Addr == addr }) + if idx != -1 { + panic(fmt.Errorf("duplicate address %s in SideroLink bind", addr)) + } + + sr.SiderolinkBind = append(sr.SiderolinkBind, SiderolinkBind{ + UUID: id, + Addr: addr, + }) +} + +// GetAddr returns the address for the given UUID. +func (sr *SiderolinkRequest) GetAddr(u *uuid.UUID) (netip.Addr, bool) { + if u == nil { + return netip.Addr{}, false + } + + for _, b := range sr.SiderolinkBind { + if b.UUID == *u { + return b.Addr, true + } + } + + return netip.Addr{}, false +} + +// SiderolinkBind describes a pair of prebinded UUID->Addr for SideroLink agent. +type SiderolinkBind struct { + UUID uuid.UUID + Addr netip.Addr +} diff --git a/website/content/v1.7/reference/cli.md b/website/content/v1.7/reference/cli.md index a6b1657cdc..a14b947c1a 100644 --- a/website/content/v1.7/reference/cli.md +++ b/website/content/v1.7/reference/cli.md @@ -167,6 +167,7 @@ talosctl cluster create [flags] --with-network-packet-corrupt float specify percent of corrupt packets on the bridge interface when creating a qemu cluster. e.g. 50% = 0.50 (default: 0.0) --with-network-packet-loss float specify percent of packet loss on the bridge interface when creating a qemu cluster. e.g. 50% = 0.50 (default: 0.0) --with-network-packet-reorder float specify percent of reordered packets on the bridge interface when creating a qemu cluster. e.g. 50% = 0.50 (default: 0.0) + --with-siderolink enables the use of siderolink agent as configuration apply mechanism --with-tpm2 enable TPM2 emulation support using swtpm --with-uefi enable UEFI on x86_64 architecture (default true) --with-uuid-hostnames use machine UUIDs as default hostnames (QEMU only)