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 <utku.ozdemir@siderolabs.com>
This commit is contained in:
Utku Ozdemir 2023-02-21 23:35:53 +01:00
parent 36e077ead4
commit f55f5df739
No known key found for this signature in database
GPG Key ID: 65933E76F0549B0D
42 changed files with 510 additions and 54 deletions

View File

@ -476,6 +476,8 @@ RUN ln /rootfs/sbin/init /rootfs/sbin/poweroff
RUN chmod +x /rootfs/sbin/poweroff RUN chmod +x /rootfs/sbin/poweroff
RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd
RUN chmod +x /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 # NB: We run the cleanup step before creating extra directories, files, and
# symlinks to avoid accidentally cleaning them up. # symlinks to avoid accidentally cleaning them up.
COPY ./hack/cleanup.sh /toolchain/bin/cleanup.sh 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 chmod +x /rootfs/sbin/poweroff
RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd RUN ln /rootfs/sbin/init /rootfs/sbin/wrapperd
RUN chmod +x /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 # NB: We run the cleanup step before creating extra directories, files, and
# symlinks to avoid accidentally cleaning them up. # symlinks to avoid accidentally cleaning them up.
COPY ./hack/cleanup.sh /toolchain/bin/cleanup.sh COPY ./hack/cleanup.sh /toolchain/bin/cleanup.sh

View File

@ -10,7 +10,7 @@ import (
"github.com/spf13/cobra" "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" "github.com/siderolabs/talos/pkg/machinery/client"
) )
@ -38,7 +38,7 @@ Keyboard shortcuts:
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return WithClient(func(ctx context.Context, c *client.Client) 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)
}) })
}, },
} }

View File

@ -77,6 +77,19 @@ machine:
title = "Machine Configuration" title = "Machine Configuration"
description="""\ description="""\
Strategic merge config patches correctly support merging `.vlans` sections of the network interface. 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] [make_deps]

View File

@ -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)
}

View File

@ -24,6 +24,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/siderolabs/talos/internal/app/apid" "github.com/siderolabs/talos/internal/app/apid"
"github.com/siderolabs/talos/internal/app/dashboard"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
v1alpha1runtime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1" v1alpha1runtime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader"
@ -316,6 +317,10 @@ func main() {
case "/sbin/wrapperd": case "/sbin/wrapperd":
wrapperd.Main() wrapperd.Main()
return
case "/sbin/dashboard":
dashboard.Main()
return return
default: default:
} }

View File

@ -94,6 +94,7 @@ func (b *BananaPiM64) Install(disk string) (err error) {
func (b *BananaPiM64) KernelArgs() procfs.Parameters { func (b *BananaPiM64) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -80,6 +80,7 @@ func (b JetsonNano) KernelArgs() procfs.Parameters {
// trying to kexec. Seems the drivers state is not reset properly. // trying to kexec. Seems the drivers state is not reset properly.
// disabling kexec until we have further knowledge on this // disabling kexec until we have further knowledge on this
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -91,6 +91,7 @@ func (l *LibretechAllH3CCH5) Install(disk string) (err error) {
func (l *LibretechAllH3CCH5) KernelArgs() procfs.Parameters { func (l *LibretechAllH3CCH5) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -80,6 +80,7 @@ func (n *NanoPiR4S) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"),
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -92,6 +92,7 @@ func (b Pine64) Install(disk string) (err error) {
func (b Pine64) KernelArgs() procfs.Parameters { func (b Pine64) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"), procfs.NewParameter("console").Append("tty0").Append("ttyS0,115200"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -91,6 +91,7 @@ func (r *Rock64) Install(disk string) (err error) {
func (r *Rock64) KernelArgs() procfs.Parameters { func (r *Rock64) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS2,115200n8"), procfs.NewParameter("console").Append("tty0").Append("ttyS2,115200n8"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -87,6 +87,7 @@ func (r *Rockpi4) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"),
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -86,6 +86,7 @@ func (r *Rockpi4c) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"), procfs.NewParameter("console").Append("tty0").Append("ttyS2,1500000n8"),
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -49,6 +49,7 @@ func (r *RPi4) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"), procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"),
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -49,6 +49,7 @@ func (r *RPiGeneric) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{ return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"), procfs.NewParameter("console").Append("tty0").Append("ttyAMA0,115200"),
procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"), procfs.NewParameter("sysctl.kernel.kexec_load_disabled").Append("1"),
procfs.NewParameter(constants.KernelParamDashboardDisabled).Append("1"),
} }
} }

View File

@ -5,6 +5,9 @@
package v1alpha1 package v1alpha1
import ( import (
"strconv"
"github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-procfs/procfs" "github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
@ -106,6 +109,15 @@ func (*Sequencer) Initialize(r runtime.Runtime) []runtime.Phase {
"earlyServices", "earlyServices",
StartUdevd, StartUdevd,
StartMachined, StartMachined,
).AppendWithDeferredCheck(
func() bool {
disabledStr := procfs.ProcCmdline().Get(constants.KernelParamDashboardDisabled).First()
disabled, _ := strconv.ParseBool(pointer.SafeDeref(disabledStr)) //nolint:errcheck
return !disabled
},
"dashboard",
StartDashboard,
).AppendWithDeferredCheck( ).AppendWithDeferredCheck(
func() bool { func() bool {
return r.State().Machine().Installed() return r.State().Machine().Installed()

View File

@ -54,6 +54,7 @@ import (
"github.com/siderolabs/talos/internal/app/machined/pkg/system/events" "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/machined/pkg/system/services"
"github.com/siderolabs/talos/internal/app/maintenance" "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/cri"
"github.com/siderolabs/talos/internal/pkg/etcd" "github.com/siderolabs/talos/internal/pkg/etcd"
"github.com/siderolabs/talos/internal/pkg/install" "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 { for _, c := range groups {
@ -792,6 +802,19 @@ func StartMachined(_ runtime.Sequence, _ interface{}) (runtime.TaskExecutionFunc
}, "startMachined" }, "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. // StartUdevd represents the task to start udevd.
func StartUdevd(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) { func StartUdevd(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) {
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) { return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) {

View File

@ -76,16 +76,25 @@ func (p *processRunner) Close() error {
return nil 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{ args := []string{
fmt.Sprintf("-name=%s", p.args.ID), fmt.Sprintf("-name=%s", p.args.ID),
fmt.Sprintf("-dropped-caps=%s", strings.Join(p.opts.DroppedCapabilities, ",")), fmt.Sprintf("-dropped-caps=%s", strings.Join(p.opts.DroppedCapabilities, ",")),
fmt.Sprintf("-cgroup-path=%s", p.opts.CgroupPath), fmt.Sprintf("-cgroup-path=%s", p.opts.CgroupPath),
fmt.Sprintf("-oom-score=%d", p.opts.OOMScoreAdj), fmt.Sprintf("-oom-score=%d", p.opts.OOMScoreAdj),
fmt.Sprintf("-uid=%d", p.opts.UID),
} }
args = append(args, p.args.ProcessArgs...) args = append(args, p.args.ProcessArgs...)
cmd = exec.Command("/sbin/wrapperd", args...) cmd := exec.Command("/sbin/wrapperd", args...)
// Set the environment for the service. // Set the environment for the service.
cmd.Env = append([]string{fmt.Sprintf("PATH=%s", constants.PATH)}, p.opts.Env...) 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. // Setup logging.
w, err := p.opts.LoggingManager.ServiceLog(p.args.ID).Writer() w, err := p.opts.LoggingManager.ServiceLog(p.args.ID).Writer()
if err != nil { if err != nil {
err = fmt.Errorf("service log handler: %w", err) return commandWrapper{}, fmt.Errorf("service log handler: %w", err)
return
} }
var writer io.Writer var writer io.Writer
@ -105,20 +112,92 @@ func (p *processRunner) build() (cmd *exec.Cmd, logCloser io.Closer, err error)
writer = w writer = w
} }
cmd.Stdout = writer // close the writer if we exit early due to an error
cmd.Stderr = writer 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 //nolint:gocyclo
func (p *processRunner) run(eventSink events.Recorder) error { func (p *processRunner) run(eventSink events.Recorder) error {
cmd, logCloser, err := p.build() cmdWrapper, err := p.build()
if err != nil { if err != nil {
return fmt.Errorf("error building command: %w", err) return fmt.Errorf("error building command: %w", err)
} }
defer logCloser.Close() //nolint:errcheck defer cmdWrapper.afterTermination() //nolint:errcheck
notifyCh := make(chan reaper.ProcessInfo, 8) notifyCh := make(chan reaper.ProcessInfo, 8)
@ -127,16 +206,20 @@ func (p *processRunner) run(eventSink events.Recorder) error {
defer reaper.Stop(notifyCh) 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) 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) waitCh := make(chan error)
go func() { go func() {
waitCh <- reaper.WaitWrapper(usingReaper, notifyCh, cmd) waitCh <- reaper.WaitWrapper(usingReaper, notifyCh, cmdWrapper.cmd)
}() }()
select { select {
@ -148,7 +231,7 @@ func (p *processRunner) run(eventSink events.Recorder) error {
eventSink(events.StateStopping, "Sending SIGTERM to %s", p) eventSink(events.StateStopping, "Sending SIGTERM to %s", p)
//nolint:errcheck //nolint:errcheck
_ = cmd.Process.Signal(syscall.SIGTERM) _ = cmdWrapper.cmd.Process.Signal(syscall.SIGTERM)
} }
select { select {
@ -160,13 +243,13 @@ func (p *processRunner) run(eventSink events.Recorder) error {
eventSink(events.StateStopping, "Sending SIGKILL to %s", p) eventSink(events.StateStopping, "Sending SIGKILL to %s", p)
//nolint:errcheck //nolint:errcheck
_ = cmd.Process.Signal(syscall.SIGKILL) _ = cmdWrapper.cmd.Process.Signal(syscall.SIGKILL)
} }
// wait for process to terminate // wait for process to terminate
<-waitCh <-waitCh
return logCloser.Close() return cmdWrapper.afterTermination()
} }
func (p *processRunner) String() string { func (p *processRunner) String() string {

View File

@ -14,6 +14,7 @@ import (
"github.com/containerd/containerd/oci" "github.com/containerd/containerd/oci"
"github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-spec/specs-go"
"github.com/siderolabs/gen/maps" "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"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/logging" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/logging"
@ -66,6 +67,16 @@ type Options struct {
OverrideSeccompProfile func(*specs.LinuxSeccomp) OverrideSeccompProfile func(*specs.LinuxSeccomp)
// DroppedCapabilities is the list of capabilities to drop. // DroppedCapabilities is the list of capabilities to drop.
DroppedCapabilities []string 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. // Option is the functional option func.
@ -174,3 +185,38 @@ func WithDroppedCapabilities(caps map[string]struct{}) Option {
args.DroppedCapabilities = maps.Keys(caps) 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
}
}

View File

@ -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
}

View File

@ -27,6 +27,8 @@ import (
"github.com/siderolabs/talos/pkg/machinery/role" "github.com/siderolabs/talos/pkg/machinery/role"
) )
const machinedServiceID = "machined"
var rules = map[string]role.Set{ var rules = map[string]role.Set{
"/cluster.ClusterService/HealthCheck": role.MakeSet(role.Admin, role.Reader), "/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 // 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 return err
} }
// set the final leaf to be world-executable to make apid connect to the socket // 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 return err
} }
@ -176,7 +178,7 @@ type Machined struct {
// ID implements the Service interface. // ID implements the Service interface.
func (m *Machined) ID(r runtime.Runtime) string { func (m *Machined) ID(r runtime.Runtime) string {
return "machined" return machinedServiceID
} }
// PreFunc implements the Service interface. // 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) { func (m *Machined) Runner(r runtime.Runtime) (runner.Runner, error) {
svc := &machinedService{m.Controller} 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. // HealthFunc implements the HealthcheckedService interface.

View File

@ -28,15 +28,18 @@ var (
droppedCaps string droppedCaps string
cgroupPath string cgroupPath string
oomScore int oomScore int
uid int
) )
// Main is the entrypoint into /sbin/wrapperd. // Main is the entrypoint into /sbin/wrapperd.
//
//nolint:gocyclo //nolint:gocyclo
func Main() { func Main() {
flag.StringVar(&name, "name", "", "process name") flag.StringVar(&name, "name", "", "process name")
flag.StringVar(&droppedCaps, "dropped-caps", "", "comma-separated list of capabilities to drop") flag.StringVar(&droppedCaps, "dropped-caps", "", "comma-separated list of capabilities to drop")
flag.StringVar(&cgroupPath, "cgroup-path", "", "cgroup path to use") flag.StringVar(&cgroupPath, "cgroup-path", "", "cgroup path to use")
flag.IntVar(&oomScore, "oom-score", 0, "oom score to set") flag.IntVar(&oomScore, "oom-score", 0, "oom score to set")
flag.IntVar(&uid, "uid", 0, "uid to set for the process")
flag.Parse() flag.Parse()
currentPid := os.Getpid() currentPid := os.Getpid()
@ -78,9 +81,9 @@ func Main() {
} else if droppedCaps != "" { } else if droppedCaps != "" {
caps := strings.Split(droppedCaps, ",") caps := strings.Split(droppedCaps, ",")
dropCaps := slices.Map(caps, func(c string) cap.Value { dropCaps := slices.Map(caps, func(c string) cap.Value {
capability, err := cap.FromName(c) capability, capErr := cap.FromName(c)
if err != nil { if capErr != nil {
log.Fatalf("failed to parse capability: %v", err) log.Fatalf("failed to parse capability: %v", capErr)
} }
return capability return capability
@ -88,15 +91,22 @@ func Main() {
// drop capabilities // drop capabilities
iab := cap.IABGetProc() 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) 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) 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 { if err := unix.Exec(flag.Args()[0], flag.Args()[0:], os.Environ()); err != nil {
log.Fatalf("failed to exec: %v", err) log.Fatalf("failed to exec: %v", err)
} }

View File

@ -8,25 +8,47 @@ package capability
import ( import (
"strings" "strings"
"github.com/siderolabs/gen/maps"
"kernel.org/pub/linux/libs/security/libcap/cap" "kernel.org/pub/linux/libs/security/libcap/cap"
"github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/constants"
) )
// AllGrantableCapabilities returns list of capabilities that can be granted to the container based on // AllCapabilitiesSet returns the set of all available capabilities.
// process bounding capabilities. //
func AllGrantableCapabilities() []string { // Returned capabilities are in UPPERCASE.
capabilities := []string{} func AllCapabilitiesSet() map[string]struct{} {
capabilities := make(map[string]struct{})
for v := cap.Value(0); v < cap.MaxBits(); v++ { for v := cap.Value(0); v < cap.MaxBits(); v++ {
if set, _ := cap.GetBound(v); set { //nolint:errcheck if set, _ := cap.GetBound(v); set { //nolint:errcheck
if _, ok := constants.DefaultDroppedCapabilities[v.String()]; ok { capabilities[strings.ToUpper(v.String())] = struct{}{}
continue
}
capabilities = append(capabilities, strings.ToUpper(v.String()))
} }
} }
return capabilities 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)
}

View File

@ -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()
}

View File

@ -11,7 +11,7 @@ import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets" "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. // SystemGauges quickly show CPU/mem load.

View File

@ -8,7 +8,7 @@ import (
"github.com/gizak/termui/v3/widgets" "github.com/gizak/termui/v3/widgets"
"github.com/siderolabs/gen/slices" "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. // BaseGraph represents the widget with some usage graph.

View File

@ -10,7 +10,7 @@ import (
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/gizak/termui/v3/widgets" "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. // LoadAvgInfo represents the widget with load average info.

View File

@ -10,7 +10,7 @@ import (
"github.com/gizak/termui/v3/widgets" "github.com/gizak/termui/v3/widgets"
"github.com/siderolabs/gen/maps" "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. // NodeTabs represents the bottom bar with node list.

View File

@ -8,7 +8,7 @@ import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets" "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. // BaseSparklineGroup represents the widget with some sparklines.

View File

@ -15,7 +15,7 @@ import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets" "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. // ProcessTable represents the widget with process info.

View File

@ -7,8 +7,8 @@ package components_test
import ( import (
"testing" "testing"
"github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/components" "github.com/siderolabs/talos/internal/pkg/dashboard/components"
"github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" "github.com/siderolabs/talos/internal/pkg/dashboard/data"
"github.com/siderolabs/talos/pkg/machinery/api/machine" "github.com/siderolabs/talos/pkg/machinery/api/machine"
) )

View File

@ -10,7 +10,7 @@ import (
"github.com/gizak/termui/v3/widgets" "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. // TopLine represents the top bar with host info.

View File

@ -12,8 +12,8 @@ import (
"github.com/siderolabs/talos/pkg/machinery/client" "github.com/siderolabs/talos/pkg/machinery/client"
) )
// Main is the entrypoint into talosctl dashboard command. // Main is the entrypoint into the dashboard.
func Main(ctx context.Context, c *client.Client, interval time.Duration) error { func Main(ctx context.Context, c *client.Client, interval time.Duration, allowExitKeys bool) error {
ui := &UI{} ui := &UI{}
source := &APISource{ source := &APISource{
@ -24,5 +24,5 @@ func Main(ctx context.Context, c *client.Client, interval time.Duration) error {
dataCh := source.Run(ctx) dataCh := source.Run(ctx)
defer source.Stop() defer source.Stop()
return ui.Main(ctx, dataCh) return ui.Main(ctx, dataCh, allowExitKeys)
} }

View File

@ -12,7 +12,7 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/emptypb" "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" "github.com/siderolabs/talos/pkg/machinery/client"
) )

View File

@ -9,8 +9,8 @@ import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/components" "github.com/siderolabs/talos/internal/pkg/dashboard/components"
"github.com/siderolabs/talos/cmd/talosctl/cmd/talos/dashboard/data" "github.com/siderolabs/talos/internal/pkg/dashboard/data"
) )
// DataWidget is a widget which consumes Data to draw itself. // DataWidget is a widget which consumes Data to draw itself.
@ -49,7 +49,7 @@ type UI struct {
// Main is the UI entrypoint. // Main is the UI entrypoint.
// //
//nolint:gocyclo //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 { if err := ui.Init(); err != nil {
return err return err
} }
@ -131,7 +131,9 @@ func (u *UI) Main(ctx context.Context, dataCh <-chan *data.Data) error {
case e := <-uiEvents: case e := <-uiEvents:
switch e.ID { switch e.ID {
case "q", "<C-c>": case "q", "<C-c>":
if allowExitKeys {
return nil return nil
}
case "<Resize>": case "<Resize>":
payload := e.Payload.(ui.Resize) //nolint:errcheck,forcetypeassert payload := e.Payload.(ui.Resize) //nolint:errcheck,forcetypeassert

View File

@ -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)) 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{ specOpts := []oci.SpecOpts{
oci.WithImageConfig(img), oci.WithImageConfig(img),
oci.WithProcessArgs(args...), oci.WithProcessArgs(args...),

View File

@ -49,6 +49,9 @@ const (
// cgroups version to use (default is cgroupsv2, setting this kernel arg to '0' forces cgroupsv1). // cgroups version to use (default is cgroupsv2, setting this kernel arg to '0' forces cgroupsv1).
KernelParamCGroups = "talos.unified_cgroup_hierarchy" 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 indicates that the install is not for a specific board.
BoardNone = "none" BoardNone = "none"
@ -422,6 +425,10 @@ const (
// ApidUserID is the user ID for apid. // ApidUserID is the user ID for apid.
ApidUserID = 50 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 is the port for the trustd service.
TrustdPort = 50001 TrustdPort = 50001
@ -546,6 +553,9 @@ const (
// CgroupExtensions is the cgroup name for system extension processes. // CgroupExtensions is the cgroup name for system extension processes.
CgroupExtensions = CgroupSystem + "/extensions" 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 is the cgroup name for kubernetes containerd runtime processes.
CgroupPodRuntime = "/podruntime/runtime" CgroupPodRuntime = "/podruntime/runtime"
@ -558,6 +568,12 @@ const (
// CgroupKubeletReservedMemory is the hard memory protection for the kubelet processes. // CgroupKubeletReservedMemory is the hard memory protection for the kubelet processes.
CgroupKubeletReservedMemory = 64 * 1024 * 1024 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 is the string to use Tanos-managed Flannel CNI (default).
FlannelCNI = "flannel" FlannelCNI = "flannel"
@ -790,8 +806,14 @@ const (
// TrustdMaxProcs is the maximum number of GOMAXPROCS for trustd. // TrustdMaxProcs is the maximum number of GOMAXPROCS for trustd.
TrustdMaxProcs = 2 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 is the gRPC metadata key used to submit a role with os:impersonator.
APIAuthzRoleMetadataKey = "talos-role" 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 // See https://linux.die.net/man/3/klogctl

View File

@ -214,3 +214,13 @@ Talos defaults to always using the unified cgroup hierarchy (`cgroupsv2`), but `
can be forced with `talos.unified_cgroup_hierarchy=0`. 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. > 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.