From f55f5df7396b7073e75267c7e10a35814f1185c9 Mon Sep 17 00:00:00 2001 From: Utku Ozdemir Date: Tue, 21 Feb 2023 23:35:53 +0100 Subject: [PATCH] feat: move dashboard package & run it in tty2 Move dashboard package into a common location where both Talos and talosctl can use it. Add support for overriding stdin, stdout, stderr and ctt in process runner. Create a dashboard service which runs the dashboard on /dev/tty2. Redirect kernel messages to tty1 and switch to tty2 after starting the dashboard on it. Related to siderolabs/talos#6841, siderolabs/talos#4791. Signed-off-by: Utku Ozdemir --- Dockerfile | 4 + cmd/talosctl/cmd/talos/dashboard.go | 4 +- hack/release.toml | 13 ++ internal/app/dashboard/main.go | 49 ++++++++ internal/app/machined/main.go | 5 + .../board/bananapi_m64/bananapi_m64.go | 1 + .../v1alpha1/board/jetson_nano/jetson_nano.go | 1 + .../libretech_all_h3_cc_h5.go | 1 + .../v1alpha1/board/nanopi_r4s/nanopi_r4s.go | 1 + .../runtime/v1alpha1/board/pine64/pine64.go | 1 + .../runtime/v1alpha1/board/rock64/rock64.go | 1 + .../runtime/v1alpha1/board/rockpi4/rockpi4.go | 1 + .../v1alpha1/board/rockpi4c/rockpi4c.go | 1 + .../pkg/runtime/v1alpha1/board/rpi_4/rpi_4.go | 1 + .../v1alpha1/board/rpi_generic/rpi_generic.go | 1 + .../runtime/v1alpha1/v1alpha1_sequencer.go | 12 ++ .../v1alpha1/v1alpha1_sequencer_tasks.go | 23 ++++ .../pkg/system/runner/process/process.go | 115 +++++++++++++++--- .../app/machined/pkg/system/runner/runner.go | 46 +++++++ .../machined/pkg/system/services/dashboard.go | 73 +++++++++++ .../machined/pkg/system/services/machined.go | 10 +- internal/app/wrapperd/main.go | 22 +++- internal/pkg/capability/capability.go | 40 ++++-- internal/pkg/console/console.go | 66 ++++++++++ .../pkg}/dashboard/components/components.go | 0 .../pkg}/dashboard/components/gauges.go | 2 +- .../pkg}/dashboard/components/graphs.go | 2 +- .../pkg}/dashboard/components/info.go | 2 +- .../pkg}/dashboard/components/nodetabs.go | 2 +- .../pkg}/dashboard/components/sparklines.go | 2 +- .../pkg}/dashboard/components/tables.go | 2 +- .../pkg}/dashboard/components/tables_test.go | 4 +- .../pkg}/dashboard/components/topline.go | 2 +- .../pkg}/dashboard/dashboard.go | 6 +- .../pkg}/dashboard/data/data.go | 0 .../pkg}/dashboard/data/diff.go | 0 .../pkg}/dashboard/data/node.go | 0 .../pkg}/dashboard/source.go | 2 +- .../talos => internal/pkg}/dashboard/ui.go | 10 +- internal/pkg/install/install.go | 4 + pkg/machinery/constants/constants.go | 22 ++++ website/content/v1.4/reference/kernel.md | 10 ++ 42 files changed, 510 insertions(+), 54 deletions(-) create mode 100644 internal/app/dashboard/main.go create mode 100644 internal/app/machined/pkg/system/services/dashboard.go create mode 100644 internal/pkg/console/console.go rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/components.go (100%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/gauges.go (96%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/graphs.go (96%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/info.go (98%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/nodetabs.go (92%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/sparklines.go (96%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/tables.go (97%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/tables_test.go (86%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/components/topline.go (94%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/dashboard.go (81%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/data/data.go (100%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/data/diff.go (100%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/data/node.go (100%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/source.go (98%) rename {cmd/talosctl/cmd/talos => internal/pkg}/dashboard/ui.go (94%) diff --git a/Dockerfile b/Dockerfile index de3287ca2..9cf6a2446 100644 --- a/Dockerfile +++ b/Dockerfile @@ -476,6 +476,8 @@ RUN ln /rootfs/sbin/init /rootfs/sbin/poweroff RUN chmod +x /rootfs/sbin/poweroff RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd RUN chmod +x /rootfs/sbin/wrapperd +RUN ln /rootfs/sbin/init /rootfs/sbin/dashboard +RUN chmod +x /rootfs/sbin/dashboard # NB: We run the cleanup step before creating extra directories, files, and # symlinks to avoid accidentally cleaning them up. COPY ./hack/cleanup.sh /toolchain/bin/cleanup.sh @@ -525,6 +527,8 @@ RUN ln /rootfs/sbin/init /rootfs/sbin/poweroff RUN chmod +x /rootfs/sbin/poweroff RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd RUN chmod +x /rootfs/sbin/wrapperd +RUN ln /rootfs/sbin/init /rootfs/sbin/dashboard +RUN chmod +x /rootfs/sbin/dashboard # NB: We run the cleanup step before creating extra directories, files, and # symlinks to avoid accidentally cleaning them up. COPY ./hack/cleanup.sh /toolchain/bin/cleanup.sh diff --git a/cmd/talosctl/cmd/talos/dashboard.go b/cmd/talosctl/cmd/talos/dashboard.go index 07e919716..430eb3b19 100644 --- a/cmd/talosctl/cmd/talos/dashboard.go +++ b/cmd/talosctl/cmd/talos/dashboard.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard" + "github.com/siderolabs/talos/internal/pkg/dashboard" "github.com/siderolabs/talos/pkg/machinery/client" ) @@ -38,7 +38,7 @@ Keyboard shortcuts: Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return WithClient(func(ctx context.Context, c *client.Client) error { - return dashboard.Main(ctx, c, dashboardCmdFlags.interval) + return dashboard.Main(ctx, c, dashboardCmdFlags.interval, true) }) }, } diff --git a/hack/release.toml b/hack/release.toml index 0b7fbbca5..cdbe6101e 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -77,6 +77,19 @@ machine: title = "Machine Configuration" description="""\ Strategic merge config patches correctly support merging `.vlans` sections of the network interface. +""" + + [notes.dashboard] + title = "Talos Dashboard on TTY2" + description="""\ +Talos now starts a text-based UI dashboard on virtual console `/dev/tty2` and switches to it by default upon boot. +Kernel logs remain available on `/dev/tty1`. + +To switch TTYs, use the `Alt+F1` through `Alt+F2` keys. + +You can disable this behavior by setting the kernel parameter `talos.dashboard.disabled=1`. + +This behavior is disabled by default on SBCs. """ [make_deps] diff --git a/internal/app/dashboard/main.go b/internal/app/dashboard/main.go new file mode 100644 index 000000000..4d5c0e88a --- /dev/null +++ b/internal/app/dashboard/main.go @@ -0,0 +1,49 @@ +// 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 dashboard implements dashboard functionality. +package dashboard + +import ( + "context" + "fmt" + "log" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + + "github.com/siderolabs/talos/internal/pkg/dashboard" + "github.com/siderolabs/talos/pkg/grpc/middleware/authz" + "github.com/siderolabs/talos/pkg/machinery/client" + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/machinery/role" + "github.com/siderolabs/talos/pkg/startup" +) + +// Main is the entrypoint into dashboard. +func Main() { + if err := dashboardMain(); err != nil { + log.Fatal(err) + } +} + +func dashboardMain() error { + startup.LimitMaxProcs(constants.DashboardMaxProcs) + + md := metadata.Pairs() + authz.SetMetadata(md, role.MakeSet(role.Admin)) + adminCtx := metadata.NewOutgoingContext(context.Background(), md) + + c, err := client.New(adminCtx, + client.WithUnixSocket(constants.MachineSocketPath), + client.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials())), + ) + if err != nil { + return fmt.Errorf("error connecting to the machine service: %w", err) + } + + return dashboard.Main(adminCtx, c, 5*time.Second, false) +} diff --git a/internal/app/machined/main.go b/internal/app/machined/main.go index 6c78a005d..4aaf791ae 100644 --- a/internal/app/machined/main.go +++ b/internal/app/machined/main.go @@ -24,6 +24,7 @@ import ( "golang.org/x/sys/unix" "github.com/siderolabs/talos/internal/app/apid" + "github.com/siderolabs/talos/internal/app/dashboard" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" v1alpha1runtime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader" @@ -316,6 +317,10 @@ func main() { case "/sbin/wrapperd": wrapperd.Main() + return + case "/sbin/dashboard": + dashboard.Main() + return default: } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/bananapi_m64/bananapi_m64.go b/internal/app/machined/pkg/runtime/v1alpha1/board/bananapi_m64/bananapi_m64.go index 2cb21c3b3..32d5fe982 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/bananapi_m64/bananapi_m64.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/bananapi_m64/bananapi_m64.go @@ -94,6 +94,7 @@ func (b *BananaPiM64) Install(disk string) (err error) { func (b *BananaPiM64) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/jetson_nano/jetson_nano.go b/internal/app/machined/pkg/runtime/v1alpha1/board/jetson_nano/jetson_nano.go index 326fadb5f..ef3a2853a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/jetson_nano/jetson_nano.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/jetson_nano/jetson_nano.go @@ -80,6 +80,7 @@ func (b JetsonNano) KernelArgs() procfs.Parameters { // trying to kexec. Seems the drivers state is not reset properly. // disabling kexec until we have further knowledge on this procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/libretech_all_h3_cc_h5/libretech_all_h3_cc_h5.go b/internal/app/machined/pkg/runtime/v1alpha1/board/libretech_all_h3_cc_h5/libretech_all_h3_cc_h5.go index e4df77893..910cccbd5 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/libretech_all_h3_cc_h5/libretech_all_h3_cc_h5.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/libretech_all_h3_cc_h5/libretech_all_h3_cc_h5.go @@ -91,6 +91,7 @@ func (l *LibretechAllH3CCH5) Install(disk string) (err error) { func (l *LibretechAllH3CCH5) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/nanopi_r4s/nanopi_r4s.go b/internal/app/machined/pkg/runtime/v1alpha1/board/nanopi_r4s/nanopi_r4s.go index de2fb3a19..b46d6788e 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/nanopi_r4s/nanopi_r4s.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/nanopi_r4s/nanopi_r4s.go @@ -80,6 +80,7 @@ func (n *NanoPiR4S) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/pine64/pine64.go b/internal/app/machined/pkg/runtime/v1alpha1/board/pine64/pine64.go index fe9f2cfda..26d395ba1 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/pine64/pine64.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/pine64/pine64.go @@ -92,6 +92,7 @@ func (b Pine64) Install(disk string) (err error) { func (b Pine64) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/rock64/rock64.go b/internal/app/machined/pkg/runtime/v1alpha1/board/rock64/rock64.go index 5c6c159a9..57ce8626a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/rock64/rock64.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/rock64/rock64.go @@ -91,6 +91,7 @@ func (r *Rock64) Install(disk string) (err error) { func (r *Rock64) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS2,115200n8"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4/rockpi4.go b/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4/rockpi4.go index 9deb45abb..224060303 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4/rockpi4.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4/rockpi4.go @@ -87,6 +87,7 @@ func (r *Rockpi4) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4c/rockpi4c.go b/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4c/rockpi4c.go index 8b00f76f4..1a1361620 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4c/rockpi4c.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/rockpi4c/rockpi4c.go @@ -86,6 +86,7 @@ func (r *Rockpi4c) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_4/rpi_4.go b/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_4/rpi_4.go index 65cc7ed82..55869dee1 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_4/rpi_4.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_4/rpi_4.go @@ -49,6 +49,7 @@ func (r *RPi4) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_generic/rpi_generic.go b/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_generic/rpi_generic.go index cd791d54e..107bd624b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_generic/rpi_generic.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/rpi_generic/rpi_generic.go @@ -49,6 +49,7 @@ func (r *RPiGeneric) KernelArgs() procfs.Parameters { return []*procfs.Parameter{ procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), + procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"), } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go index 56c47f9ad..9d7b80ca5 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer.go @@ -5,6 +5,9 @@ package v1alpha1 import ( + "strconv" + + "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-procfs/procfs" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" @@ -106,6 +109,15 @@ func (*Sequencer) Initialize(r runtime.Runtime) []runtime.Phase { "earlyServices", StartUdevd, StartMachined, + ).AppendWithDeferredCheck( + func() bool { + disabledStr := procfs.ProcCmdline().Get(constants.KernelParamDashboardDisabled).First() + disabled, _ := strconv.ParseBool(pointer.SafeDeref(disabledStr)) //nolint:errcheck + + return !disabled + }, + "dashboard", + StartDashboard, ).AppendWithDeferredCheck( func() bool { return r.State().Machine().Installed() diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go index 414879683..7414ebcf0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go @@ -54,6 +54,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/system/events" "github.com/siderolabs/talos/internal/app/machined/pkg/system/services" "github.com/siderolabs/talos/internal/app/maintenance" + "github.com/siderolabs/talos/internal/pkg/console" "github.com/siderolabs/talos/internal/pkg/cri" "github.com/siderolabs/talos/internal/pkg/etcd" "github.com/siderolabs/talos/internal/pkg/install" @@ -203,6 +204,15 @@ func CreateSystemCgroups(seq runtime.Sequence, data interface{}) (runtime.TaskEx }, }, }, + { + name: constants.CgroupDashboard, + resources: &cgroupsv2.Resources{ + Memory: &cgroupsv2.Memory{ + Min: pointer.To[int64](constants.CgroupDashboardReservedMemory), + Low: pointer.To[int64](constants.CgroupDashboardLowMemory), + }, + }, + }, } for _, c := range groups { @@ -792,6 +802,19 @@ func StartMachined(_ runtime.Sequence, _ interface{}) (runtime.TaskExecutionFunc }, "startMachined" } +// StartDashboard represents the task to start dashboard. +func StartDashboard(_ runtime.Sequence, _ interface{}) (runtime.TaskExecutionFunc, string) { + return func(_ context.Context, _ *log.Logger, r runtime.Runtime) error { + ttyNumber := constants.DashboardTTY + + system.Services(r).LoadAndStart(&services.Dashboard{ + TTYNumber: ttyNumber, + }) + + return console.Switch(ttyNumber) + }, "startDashboard" +} + // StartUdevd represents the task to start udevd. func StartUdevd(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { diff --git a/internal/app/machined/pkg/system/runner/process/process.go b/internal/app/machined/pkg/system/runner/process/process.go index 668ac325e..b68d1519b 100644 --- a/internal/app/machined/pkg/system/runner/process/process.go +++ b/internal/app/machined/pkg/system/runner/process/process.go @@ -76,16 +76,25 @@ func (p *processRunner) Close() error { return nil } -func (p *processRunner) build() (cmd *exec.Cmd, logCloser io.Closer, err error) { +type commandWrapper struct { + cmd *exec.Cmd + afterStart func() + afterTermination func() error +} + +//nolint:gocyclo +func (p *processRunner) build() (commandWrapper, error) { args := []string{ fmt.Sprintf("-name=%s", p.args.ID), fmt.Sprintf("-dropped-caps=%s", strings.Join(p.opts.DroppedCapabilities, ",")), fmt.Sprintf("-cgroup-path=%s", p.opts.CgroupPath), fmt.Sprintf("-oom-score=%d", p.opts.OOMScoreAdj), + fmt.Sprintf("-uid=%d", p.opts.UID), } + args = append(args, p.args.ProcessArgs...) - cmd = exec.Command("/sbin/wrapperd", args...) + cmd := exec.Command("/sbin/wrapperd", args...) // Set the environment for the service. cmd.Env = append([]string{fmt.Sprintf("PATH=%s", constants.PATH)}, p.opts.Env...) @@ -93,9 +102,7 @@ func (p *processRunner) build() (cmd *exec.Cmd, logCloser io.Closer, err error) // Setup logging. w, err := p.opts.LoggingManager.ServiceLog(p.args.ID).Writer() if err != nil { - err = fmt.Errorf("service log handler: %w", err) - - return + return commandWrapper{}, fmt.Errorf("service log handler: %w", err) } var writer io.Writer @@ -105,20 +112,92 @@ func (p *processRunner) build() (cmd *exec.Cmd, logCloser io.Closer, err error) writer = w } - cmd.Stdout = writer - cmd.Stderr = writer + // close the writer if we exit early due to an error + closeWriter := true - return cmd, w, nil + defer func() { + if closeWriter { + w.Close() //nolint:errcheck + } + }() + + var afterStartFuncs []func() + + if p.opts.StdinFile != "" { + stdin, err := os.Open(p.opts.StdinFile) + if err != nil { + return commandWrapper{}, err + } + + cmd.Stdin = stdin + + afterStartFuncs = append(afterStartFuncs, func() { + stdin.Close() //nolint:errcheck + }) + } + + if p.opts.StdoutFile != "" { + stdout, err := os.OpenFile(p.opts.StdoutFile, os.O_WRONLY, 0) + if err != nil { + return commandWrapper{}, err + } + + cmd.Stdout = stdout + + afterStartFuncs = append(afterStartFuncs, func() { + stdout.Close() //nolint:errcheck + }) + } else { + cmd.Stdout = writer + } + + if p.opts.StderrFile != "" { + stderr, err := os.OpenFile(p.opts.StderrFile, os.O_WRONLY, 0) + if err != nil { + return commandWrapper{}, err + } + + cmd.Stderr = stderr + + afterStartFuncs = append(afterStartFuncs, func() { + stderr.Close() //nolint:errcheck + }) + } else { + cmd.Stderr = writer + } + + ctty, cttySet := p.opts.Ctty.Get() + if cttySet { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + Setctty: true, + Ctty: ctty, + } + } + + closeWriter = false + + return commandWrapper{ + cmd: cmd, + afterStart: func() { + for _, f := range afterStartFuncs { + f() + } + }, + afterTermination: func() error { + return w.Close() + }, + }, nil } //nolint:gocyclo func (p *processRunner) run(eventSink events.Recorder) error { - cmd, logCloser, err := p.build() + cmdWrapper, err := p.build() if err != nil { return fmt.Errorf("error building command: %w", err) } - defer logCloser.Close() //nolint:errcheck + defer cmdWrapper.afterTermination() //nolint:errcheck notifyCh := make(chan reaper.ProcessInfo, 8) @@ -127,16 +206,20 @@ func (p *processRunner) run(eventSink events.Recorder) error { defer reaper.Stop(notifyCh) } - if err = cmd.Start(); err != nil { + err = cmdWrapper.cmd.Start() + + cmdWrapper.afterStart() + + if err != nil { return fmt.Errorf("error starting process: %w", err) } - eventSink(events.StateRunning, "Process %s started with PID %d", p, cmd.Process.Pid) + eventSink(events.StateRunning, "Process %s started with PID %d", p, cmdWrapper.cmd.Process.Pid) waitCh := make(chan error) go func() { - waitCh <- reaper.WaitWrapper(usingReaper, notifyCh, cmd) + waitCh <- reaper.WaitWrapper(usingReaper, notifyCh, cmdWrapper.cmd) }() select { @@ -148,7 +231,7 @@ func (p *processRunner) run(eventSink events.Recorder) error { eventSink(events.StateStopping, "Sending SIGTERM to %s", p) //nolint:errcheck - _ = cmd.Process.Signal(syscall.SIGTERM) + _ = cmdWrapper.cmd.Process.Signal(syscall.SIGTERM) } select { @@ -160,13 +243,13 @@ func (p *processRunner) run(eventSink events.Recorder) error { eventSink(events.StateStopping, "Sending SIGKILL to %s", p) //nolint:errcheck - _ = cmd.Process.Signal(syscall.SIGKILL) + _ = cmdWrapper.cmd.Process.Signal(syscall.SIGKILL) } // wait for process to terminate <-waitCh - return logCloser.Close() + return cmdWrapper.afterTermination() } func (p *processRunner) String() string { diff --git a/internal/app/machined/pkg/system/runner/runner.go b/internal/app/machined/pkg/system/runner/runner.go index f04f0d508..764be076a 100644 --- a/internal/app/machined/pkg/system/runner/runner.go +++ b/internal/app/machined/pkg/system/runner/runner.go @@ -14,6 +14,7 @@ import ( "github.com/containerd/containerd/oci" "github.com/opencontainers/runtime-spec/specs-go" "github.com/siderolabs/gen/maps" + "github.com/siderolabs/gen/optional" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/logging" @@ -66,6 +67,16 @@ type Options struct { OverrideSeccompProfile func(*specs.LinuxSeccomp) // DroppedCapabilities is the list of capabilities to drop. DroppedCapabilities []string + // StdinFile is the path to the file to use as stdin. + StdinFile string + // StdoutFile is the path to the file to use as stdout. + StdoutFile string + // StderrFile is the path to the file to use as stderr. + StderrFile string + // Ctty is the controlling tty. + Ctty optional.Optional[int] + // UID is the user id of the process. + UID uint32 } // Option is the functional option func. @@ -174,3 +185,38 @@ func WithDroppedCapabilities(caps map[string]struct{}) Option { args.DroppedCapabilities = maps.Keys(caps) } } + +// WithStdinFile sets the path to the file to use as stdin. +func WithStdinFile(path string) Option { + return func(args *Options) { + args.StdinFile = path + } +} + +// WithStdoutFile sets the path to the file to use as stdout. +func WithStdoutFile(path string) Option { + return func(args *Options) { + args.StdoutFile = path + } +} + +// WithStderrFile sets the path to the file to use as stderr. +func WithStderrFile(path string) Option { + return func(args *Options) { + args.StdoutFile = path + } +} + +// WithCtty sets the controlling tty. +func WithCtty(ctty int) Option { + return func(args *Options) { + args.Ctty = optional.Some(ctty) + } +} + +// WithUID sets the user id of the process. +func WithUID(uid uint32) Option { + return func(args *Options) { + args.UID = uid + } +} diff --git a/internal/app/machined/pkg/system/services/dashboard.go b/internal/app/machined/pkg/system/services/dashboard.go new file mode 100644 index 000000000..5174a2ccf --- /dev/null +++ b/internal/app/machined/pkg/system/services/dashboard.go @@ -0,0 +1,73 @@ +// 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/. + +//nolint:golint,dupl +package services + +import ( + "context" + "fmt" + + "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" + "github.com/siderolabs/talos/internal/app/machined/pkg/system/events" + "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner" + "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/process" + "github.com/siderolabs/talos/internal/app/machined/pkg/system/runner/restart" + "github.com/siderolabs/talos/internal/pkg/capability" + "github.com/siderolabs/talos/pkg/conditions" + "github.com/siderolabs/talos/pkg/machinery/constants" +) + +// Dashboard implements the Service interface. It serves as the concrete type with +// the required methods. +type Dashboard struct { + TTYNumber int +} + +// ID implements the Service interface. +func (d *Dashboard) ID(_ runtime.Runtime) string { + return "dashboard" +} + +// PreFunc implements the Service interface. +func (d *Dashboard) PreFunc(_ context.Context, _ runtime.Runtime) error { + return nil +} + +// PostFunc implements the Service interface. +func (d *Dashboard) PostFunc(_ runtime.Runtime, _ events.ServiceState) error { + return nil +} + +// Condition implements the Service interface. +func (d *Dashboard) Condition(_ runtime.Runtime) conditions.Condition { + return conditions.WaitForFileToExist(constants.MachineSocketPath) +} + +// DependsOn implements the Service interface. +func (d *Dashboard) DependsOn(_ runtime.Runtime) []string { + return []string{machinedServiceID} +} + +// Runner implements the Service interface. +func (d *Dashboard) Runner(r runtime.Runtime) (runner.Runner, error) { + tty := fmt.Sprintf("/dev/tty%d", d.TTYNumber) + + return restart.New(process.NewRunner(false, &runner.Args{ + ID: d.ID(r), + ProcessArgs: []string{"/sbin/dashboard"}, + }, + runner.WithLoggingManager(r.Logging()), + runner.WithEnv([]string{"TERM=linux"}), + runner.WithStdinFile(tty), + runner.WithStdoutFile(tty), + runner.WithCtty(1), + runner.WithOOMScoreAdj(-400), + runner.WithDroppedCapabilities(capability.AllCapabilitiesSetLowercase()), + runner.WithCgroupPath(constants.CgroupDashboard), + runner.WithUID(constants.DashboardUserID), + ), + restart.WithType(restart.Forever), + ), nil +} diff --git a/internal/app/machined/pkg/system/services/machined.go b/internal/app/machined/pkg/system/services/machined.go index d2d0c3dbe..cfb5a5f15 100644 --- a/internal/app/machined/pkg/system/services/machined.go +++ b/internal/app/machined/pkg/system/services/machined.go @@ -27,6 +27,8 @@ import ( "github.com/siderolabs/talos/pkg/machinery/role" ) +const machinedServiceID = "machined" + var rules = map[string]role.Set{ "/cluster.ClusterService/HealthCheck": role.MakeSet(role.Admin, role.Reader), @@ -132,12 +134,12 @@ func (s *machinedService) Main(ctx context.Context, r runtime.Runtime, logWriter ) // ensure socket dir exists - if err := os.MkdirAll(filepath.Dir(constants.MachineSocketPath), 0o750); err != nil { + if err := os.MkdirAll(filepath.Dir(constants.MachineSocketPath), 0o770); err != nil { return err } // set the final leaf to be world-executable to make apid connect to the socket - if err := os.Chmod(filepath.Dir(constants.MachineSocketPath), 0o751); err != nil { + if err := os.Chmod(filepath.Dir(constants.MachineSocketPath), 0o771); err != nil { return err } @@ -176,7 +178,7 @@ type Machined struct { // ID implements the Service interface. func (m *Machined) ID(r runtime.Runtime) string { - return "machined" + return machinedServiceID } // PreFunc implements the Service interface. @@ -203,7 +205,7 @@ func (m *Machined) DependsOn(r runtime.Runtime) []string { func (m *Machined) Runner(r runtime.Runtime) (runner.Runner, error) { svc := &machinedService{m.Controller} - return goroutine.NewRunner(r, "machined", svc.Main, runner.WithLoggingManager(r.Logging())), nil + return goroutine.NewRunner(r, machinedServiceID, svc.Main, runner.WithLoggingManager(r.Logging())), nil } // HealthFunc implements the HealthcheckedService interface. diff --git a/internal/app/wrapperd/main.go b/internal/app/wrapperd/main.go index fb393fa88..8db49359e 100644 --- a/internal/app/wrapperd/main.go +++ b/internal/app/wrapperd/main.go @@ -28,15 +28,18 @@ var ( droppedCaps string cgroupPath string oomScore int + uid int ) // Main is the entrypoint into /sbin/wrapperd. -// nolint: gocyclo +// +//nolint:gocyclo func Main() { flag.StringVar(&name, "name", "", "process name") flag.StringVar(&droppedCaps, "dropped-caps", "", "comma-separated list of capabilities to drop") flag.StringVar(&cgroupPath, "cgroup-path", "", "cgroup path to use") flag.IntVar(&oomScore, "oom-score", 0, "oom score to set") + flag.IntVar(&uid, "uid", 0, "uid to set for the process") flag.Parse() currentPid := os.Getpid() @@ -78,9 +81,9 @@ func Main() { } else if droppedCaps != "" { caps := strings.Split(droppedCaps, ",") dropCaps := slices.Map(caps, func(c string) cap.Value { - capability, err := cap.FromName(c) - if err != nil { - log.Fatalf("failed to parse capability: %v", err) + capability, capErr := cap.FromName(c) + if capErr != nil { + log.Fatalf("failed to parse capability: %v", capErr) } return capability @@ -88,15 +91,22 @@ func Main() { // drop capabilities iab := cap.IABGetProc() - if err := iab.SetVector(cap.Bound, true, dropCaps...); err != nil { + if err = iab.SetVector(cap.Bound, true, dropCaps...); err != nil { log.Fatalf("failed to set capabilities: %v", err) } - if err := iab.SetProc(); err != nil { + if err = iab.SetProc(); err != nil { log.Fatalf("failed to apply capabilities: %v", err) } } + if uid > 0 { + err = unix.Setuid(uid) + if err != nil { + log.Fatalf("failed to setuid: %v", err) + } + } + if err := unix.Exec(flag.Args()[0], flag.Args()[0:], os.Environ()); err != nil { log.Fatalf("failed to exec: %v", err) } diff --git a/internal/pkg/capability/capability.go b/internal/pkg/capability/capability.go index e552473e6..3180952c7 100644 --- a/internal/pkg/capability/capability.go +++ b/internal/pkg/capability/capability.go @@ -8,25 +8,47 @@ package capability import ( "strings" + "github.com/siderolabs/gen/maps" "kernel.org/pub/linux/libs/security/libcap/cap" "github.com/siderolabs/talos/pkg/machinery/constants" ) -// AllGrantableCapabilities returns list of capabilities that can be granted to the container based on -// process bounding capabilities. -func AllGrantableCapabilities() []string { - capabilities := []string{} +// AllCapabilitiesSet returns the set of all available capabilities. +// +// Returned capabilities are in UPPERCASE. +func AllCapabilitiesSet() map[string]struct{} { + capabilities := make(map[string]struct{}) for v := cap.Value(0); v < cap.MaxBits(); v++ { if set, _ := cap.GetBound(v); set { //nolint:errcheck - if _, ok := constants.DefaultDroppedCapabilities[v.String()]; ok { - continue - } - - capabilities = append(capabilities, strings.ToUpper(v.String())) + capabilities[strings.ToUpper(v.String())] = struct{}{} } } return capabilities } + +// AllCapabilitiesSetLowercase returns the set of all available capabilities. +// +// Returned capabilities are in lowercase. +func AllCapabilitiesSetLowercase() map[string]struct{} { + return maps.Map(AllCapabilitiesSet(), + func(capability string, _ struct{}) (string, struct{}) { + return strings.ToLower(capability), struct{}{} + }) +} + +// AllGrantableCapabilities returns list of capabilities that can be granted to the container based on +// process bounding capabilities. +// +// Returned capabilities are in UPPERCASE. +func AllGrantableCapabilities() []string { + allCapabilities := AllCapabilitiesSet() + + for dropped := range constants.DefaultDroppedCapabilities { + delete(allCapabilities, strings.ToUpper(dropped)) + } + + return maps.Keys(allCapabilities) +} diff --git a/internal/pkg/console/console.go b/internal/pkg/console/console.go new file mode 100644 index 000000000..b21e438fa --- /dev/null +++ b/internal/pkg/console/console.go @@ -0,0 +1,66 @@ +// 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 console contains console-related functionality. +package console + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +const ( + // vtActivate activates the specified virtual terminal. + // See VT_ACTIVATE: + // https://man7.org/linux/man-pages/man2/ioctl_console.2.html + // https://github.com/torvalds/linux/blob/v6.2/include/uapi/linux/vt.h#L42 + vtActivate uintptr = 0x5606 + + // tioclSetKmsgRedirect redirects kernel messages to the specified tty. + // See TIOCL_SETKMSGREDIRECT: + // https://github.com/torvalds/linux/blob/v6.2/include/uapi/linux/tiocl.h#L33 + // https://github.com/torvalds/linux/blob/v6.2/drivers/tty/vt/vt.c#L3242 + tioclSetKmsgRedirect byte = 11 +) + +// Switch switches the active console to the specified tty. +func Switch(ttyNumber int) error { + // redirect the kernel logs to tty1 instead of the currently used one, + // so that dashboard on tty2 does not get flooded with kernel logs + if err := redirectKernelLogs(1); err != nil { + return err + } + + // we need a valid fd to any tty because ioctl requires it + tty0, err := os.OpenFile("/dev/tty0", os.O_RDWR, 0) + if err != nil { + return err + } + + defer tty0.Close() //nolint: errcheck + + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tty0.Fd(), vtActivate, uintptr(ttyNumber)); errno != 0 { + return fmt.Errorf("failed to activate console: %w", errno) + } + + return nil +} + +// redirectKernelLogs redirects kernel logs to the specified tty. +func redirectKernelLogs(ttyNumber int) error { + tty, err := os.OpenFile(fmt.Sprintf("/dev/tty%d", ttyNumber), os.O_RDWR, 0) + if err != nil { + return err + } + + args := [2]byte{tioclSetKmsgRedirect, byte(ttyNumber)} + + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tty.Fd(), syscall.TIOCLINUX, uintptr(unsafe.Pointer(&args))); errno != 0 { + return fmt.Errorf("failed to set redirect for kmsg: %w", errno) + } + + return tty.Close() +} diff --git a/cmd/talosctl/cmd/talos/dashboard/components/components.go b/internal/pkg/dashboard/components/components.go similarity index 100% rename from cmd/talosctl/cmd/talos/dashboard/components/components.go rename to internal/pkg/dashboard/components/components.go diff --git a/cmd/talosctl/cmd/talos/dashboard/components/gauges.go b/internal/pkg/dashboard/components/gauges.go similarity index 96% rename from cmd/talosctl/cmd/talos/dashboard/components/gauges.go rename to internal/pkg/dashboard/components/gauges.go index 4595eb355..e5574ce7b 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/gauges.go +++ b/internal/pkg/dashboard/components/gauges.go @@ -11,7 +11,7 @@ import ( ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // SystemGauges quickly show CPU/mem load. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/graphs.go b/internal/pkg/dashboard/components/graphs.go similarity index 96% rename from cmd/talosctl/cmd/talos/dashboard/components/graphs.go rename to internal/pkg/dashboard/components/graphs.go index 6a72ec1d4..8706d4b20 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/graphs.go +++ b/internal/pkg/dashboard/components/graphs.go @@ -8,7 +8,7 @@ import ( "github.com/gizak/termui/v3/widgets" "github.com/siderolabs/gen/slices" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // BaseGraph represents the widget with some usage graph. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/info.go b/internal/pkg/dashboard/components/info.go similarity index 98% rename from cmd/talosctl/cmd/talos/dashboard/components/info.go rename to internal/pkg/dashboard/components/info.go index aa8b716a9..0e501c45e 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/info.go +++ b/internal/pkg/dashboard/components/info.go @@ -10,7 +10,7 @@ import ( "github.com/dustin/go-humanize" "github.com/gizak/termui/v3/widgets" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // LoadAvgInfo represents the widget with load average info. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/nodetabs.go b/internal/pkg/dashboard/components/nodetabs.go similarity index 92% rename from cmd/talosctl/cmd/talos/dashboard/components/nodetabs.go rename to internal/pkg/dashboard/components/nodetabs.go index 8f3941a89..e1ec4163e 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/nodetabs.go +++ b/internal/pkg/dashboard/components/nodetabs.go @@ -10,7 +10,7 @@ import ( "github.com/gizak/termui/v3/widgets" "github.com/siderolabs/gen/maps" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // NodeTabs represents the bottom bar with node list. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/sparklines.go b/internal/pkg/dashboard/components/sparklines.go similarity index 96% rename from cmd/talosctl/cmd/talos/dashboard/components/sparklines.go rename to internal/pkg/dashboard/components/sparklines.go index 58ee6cad8..55a88d432 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/sparklines.go +++ b/internal/pkg/dashboard/components/sparklines.go @@ -8,7 +8,7 @@ import ( ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // BaseSparklineGroup represents the widget with some sparklines. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/tables.go b/internal/pkg/dashboard/components/tables.go similarity index 97% rename from cmd/talosctl/cmd/talos/dashboard/components/tables.go rename to internal/pkg/dashboard/components/tables.go index 224467ddb..55b16adb3 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/tables.go +++ b/internal/pkg/dashboard/components/tables.go @@ -15,7 +15,7 @@ import ( ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // ProcessTable represents the widget with process info. diff --git a/cmd/talosctl/cmd/talos/dashboard/components/tables_test.go b/internal/pkg/dashboard/components/tables_test.go similarity index 86% rename from cmd/talosctl/cmd/talos/dashboard/components/tables_test.go rename to internal/pkg/dashboard/components/tables_test.go index 26906a43d..4e837154a 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/tables_test.go +++ b/internal/pkg/dashboard/components/tables_test.go @@ -7,8 +7,8 @@ package components_test import ( "testing" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/components" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/components" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" "github.com/siderolabs/talos/pkg/machinery/api/machine" ) diff --git a/cmd/talosctl/cmd/talos/dashboard/components/topline.go b/internal/pkg/dashboard/components/topline.go similarity index 94% rename from cmd/talosctl/cmd/talos/dashboard/components/topline.go rename to internal/pkg/dashboard/components/topline.go index 8975f5937..6d6f4c328 100644 --- a/cmd/talosctl/cmd/talos/dashboard/components/topline.go +++ b/internal/pkg/dashboard/components/topline.go @@ -10,7 +10,7 @@ import ( "github.com/gizak/termui/v3/widgets" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // TopLine represents the top bar with host info. diff --git a/cmd/talosctl/cmd/talos/dashboard/dashboard.go b/internal/pkg/dashboard/dashboard.go similarity index 81% rename from cmd/talosctl/cmd/talos/dashboard/dashboard.go rename to internal/pkg/dashboard/dashboard.go index bd66f410f..a69e71c17 100644 --- a/cmd/talosctl/cmd/talos/dashboard/dashboard.go +++ b/internal/pkg/dashboard/dashboard.go @@ -12,8 +12,8 @@ import ( "github.com/siderolabs/talos/pkg/machinery/client" ) -// Main is the entrypoint into talosctl dashboard command. -func Main(ctx context.Context, c *client.Client, interval time.Duration) error { +// Main is the entrypoint into the dashboard. +func Main(ctx context.Context, c *client.Client, interval time.Duration, allowExitKeys bool) error { ui := &UI{} source := &APISource{ @@ -24,5 +24,5 @@ func Main(ctx context.Context, c *client.Client, interval time.Duration) error { dataCh := source.Run(ctx) defer source.Stop() - return ui.Main(ctx, dataCh) + return ui.Main(ctx, dataCh, allowExitKeys) } diff --git a/cmd/talosctl/cmd/talos/dashboard/data/data.go b/internal/pkg/dashboard/data/data.go similarity index 100% rename from cmd/talosctl/cmd/talos/dashboard/data/data.go rename to internal/pkg/dashboard/data/data.go diff --git a/cmd/talosctl/cmd/talos/dashboard/data/diff.go b/internal/pkg/dashboard/data/diff.go similarity index 100% rename from cmd/talosctl/cmd/talos/dashboard/data/diff.go rename to internal/pkg/dashboard/data/diff.go diff --git a/cmd/talosctl/cmd/talos/dashboard/data/node.go b/internal/pkg/dashboard/data/node.go similarity index 100% rename from cmd/talosctl/cmd/talos/dashboard/data/node.go rename to internal/pkg/dashboard/data/node.go diff --git a/cmd/talosctl/cmd/talos/dashboard/source.go b/internal/pkg/dashboard/source.go similarity index 98% rename from cmd/talosctl/cmd/talos/dashboard/source.go rename to internal/pkg/dashboard/source.go index 67e843f9e..da263c944 100644 --- a/cmd/talosctl/cmd/talos/dashboard/source.go +++ b/internal/pkg/dashboard/source.go @@ -12,7 +12,7 @@ import ( "golang.org/x/sync/errgroup" "google.golang.org/protobuf/types/known/emptypb" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" "github.com/siderolabs/talos/pkg/machinery/client" ) diff --git a/cmd/talosctl/cmd/talos/dashboard/ui.go b/internal/pkg/dashboard/ui.go similarity index 94% rename from cmd/talosctl/cmd/talos/dashboard/ui.go rename to internal/pkg/dashboard/ui.go index b8305d74b..d52aaa645 100644 --- a/cmd/talosctl/cmd/talos/dashboard/ui.go +++ b/internal/pkg/dashboard/ui.go @@ -9,8 +9,8 @@ import ( ui "github.com/gizak/termui/v3" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/components" - "github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" + "github.com/siderolabs/talos/internal/pkg/dashboard/components" + "github.com/siderolabs/talos/internal/pkg/dashboard/data" ) // DataWidget is a widget which consumes Data to draw itself. @@ -49,7 +49,7 @@ type UI struct { // Main is the UI entrypoint. // //nolint:gocyclo -func (u *UI) Main(ctx context.Context, dataCh <-chan *data.Data) error { +func (u *UI) Main(ctx context.Context, dataCh <-chan *data.Data, allowExitKeys bool) error { if err := ui.Init(); err != nil { return err } @@ -131,7 +131,9 @@ func (u *UI) Main(ctx context.Context, dataCh <-chan *data.Data) error { case e := <-uiEvents: switch e.ID { case "q", "": - return nil + if allowExitKeys { + return nil + } case "": payload := e.Payload.(ui.Resize) //nolint:errcheck,forcetypeassert diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go index f0e2d7b3d..417fa79a1 100644 --- a/internal/pkg/install/install.go +++ b/internal/pkg/install/install.go @@ -181,6 +181,10 @@ func RunInstallerContainer(disk, platform, ref string, cfg config.Provider, opts args = append(args, "--extra-kernel-arg", fmt.Sprintf("%s=%s", constants.KernelParamEquinixMetalEvents, *c)) } + if c := procfs.ProcCmdline().Get(constants.KernelParamDashboardDisabled).First(); c != nil { + args = append(args, "--extra-kernel-arg", fmt.Sprintf("%s=%s", constants.KernelParamDashboardDisabled, *c)) + } + specOpts := []oci.SpecOpts{ oci.WithImageConfig(img), oci.WithProcessArgs(args...), diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index df59eaee0..b9969e9f7 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -49,6 +49,9 @@ const ( // cgroups version to use (default is cgroupsv2, setting this kernel arg to '0' forces cgroupsv1). KernelParamCGroups = "talos.unified_cgroup_hierarchy" + // KernelParamDashboardDisabled is the kernel parameter name for disabling the dashboard. + KernelParamDashboardDisabled = "talos.dashboard.disabled" + // BoardNone indicates that the install is not for a specific board. BoardNone = "none" @@ -422,6 +425,10 @@ const ( // ApidUserID is the user ID for apid. ApidUserID = 50 + // DashboardUserID is the user ID for dashboard. + // We use the same user ID as apid so that the dashboard can write to the machined unix socket. + DashboardUserID = ApidUserID + // TrustdPort is the port for the trustd service. TrustdPort = 50001 @@ -546,6 +553,9 @@ const ( // CgroupExtensions is the cgroup name for system extension processes. CgroupExtensions = CgroupSystem + "/extensions" + // CgroupDashboard is the cgroup name for dashboard process. + CgroupDashboard = CgroupSystem + "/dashboard" + // CgroupPodRuntime is the cgroup name for kubernetes containerd runtime processes. CgroupPodRuntime = "/podruntime/runtime" @@ -558,6 +568,12 @@ const ( // CgroupKubeletReservedMemory is the hard memory protection for the kubelet processes. CgroupKubeletReservedMemory = 64 * 1024 * 1024 + // CgroupDashboardReservedMemory is the hard memory protection for the dashboard process. + CgroupDashboardReservedMemory = 85 * 1024 * 1024 + + // CgroupDashboardLowMemory is the low memory value for the dashboard process. + CgroupDashboardLowMemory = 100 * 1024 * 1024 + // FlannelCNI is the string to use Tanos-managed Flannel CNI (default). FlannelCNI = "flannel" @@ -790,8 +806,14 @@ const ( // TrustdMaxProcs is the maximum number of GOMAXPROCS for trustd. TrustdMaxProcs = 2 + // DashboardMaxProcs is the maximum number of GOMAXPROCS for dashboard. + DashboardMaxProcs = 2 + // APIAuthzRoleMetadataKey is the gRPC metadata key used to submit a role with os:impersonator. APIAuthzRoleMetadataKey = "talos-role" + + // DashboardTTY is the number of the TTY device (/dev/ttyN) for dashboard. + DashboardTTY = 2 ) // See https://linux.die.net/man/3/klogctl diff --git a/website/content/v1.4/reference/kernel.md b/website/content/v1.4/reference/kernel.md index 03075ce1c..5237d3db2 100644 --- a/website/content/v1.4/reference/kernel.md +++ b/website/content/v1.4/reference/kernel.md @@ -214,3 +214,13 @@ Talos defaults to always using the unified cgroup hierarchy (`cgroupsv2`), but ` can be forced with `talos.unified_cgroup_hierarchy=0`. > Note: `cgroupsv1` is deprecated and it should be used only for compatibility with workloads which don't support `cgroupsv2` yet. + +#### `talos.dashboard.disabled` + +By default, Talos redirects kernel logs to virtual console `/dev/tty1` and starts the dashboard on `/dev/tty2`, +then switches to the dashboard tty. + +If you set `talos.dashboard.disabled=1`, this behavior will be disabled. +Kernel logs will be sent to the currently active console and the dashboard will not be started. + +It is set to be `1` by default on SBCs.