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

View File

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

View File

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

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"
"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:
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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/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) {

View File

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

View File

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

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"
)
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.

View File

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

View File

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

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"
"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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", "<C-c>":
return nil
if allowExitKeys {
return nil
}
case "<Resize>":
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))
}
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...),

View File

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

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`.
> 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.