From 4fd53d3b202b4ea728d3ef1813e49687f02be0ca Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 2 Sep 2026 18:46:01 +0200 Subject: [PATCH 1/7] Refuse a heartbeat name no heartbeat answers to "om daemon hb stop 1.rxx" exited zero and did nothing. The action is published as a message the heartbeat janitor subscribes to by component name, so a name no stream answers to was accepted, queued, and acted on by nobody. The name went through untouched: "9.rx", a stream of a heartbeat the cluster does not configure, was the same silence, and "om daemon hb sign 1.rx" read hb#1.rx.type, a key no configuration has. The listener actions were given that check when they were aligned with the audit subsystem names. The heartbeat ones have it now, from the same place and in the same shape: the name is checked against the heartbeats the cluster configuration defines, and the api answers 400 naming what it would have accepted. The two actions take two different things, which is what the name to refuse loudest was: start, stop and restart address one direction of a heartbeat, "1.rx", and sign and wipe address the heartbeat itself, "1". The "hb#" prefix the id column of "om daemon hb status" shows is accepted in both, so a name read there can be typed back rather than edited first. The commands say all of that now. Every heartbeat and listener action has a description of what NAME is and examples of it being used, the listener ones name the two listeners that exist, and the heartbeat ones, whose names are per cluster, complete from the local configuration: "om daemon hb start " offers the streams, "om daemon hb sign " the heartbeats. The examples render under the name the binary was invoked as, so an ox help does not tell its reader to type om. Checked on a live three node cluster: the refusals name the streams, "om daemon hb stop 4.rx" then "om daemon hb start hb#4.rx" stops and restarts the receiver, and the listener refusal is unchanged. --- core/commoncmd/daemon_hb_restart.go | 10 +- core/commoncmd/daemon_hb_sign.go | 10 +- core/commoncmd/daemon_hb_start.go | 16 +- core/commoncmd/daemon_hb_stop.go | 13 +- core/commoncmd/daemon_hb_wipe.go | 10 +- core/commoncmd/daemon_listener_restart.go | 12 +- core/commoncmd/daemon_listener_start.go | 12 +- core/commoncmd/daemon_listener_stop.go | 12 +- core/commoncmd/daemon_subsystems.go | 86 ++++ core/commoncmd/daemon_subsystems_test.go | 38 ++ daemon/api/api.yaml | 13 +- daemon/api/codegen_server_gen.go | 513 +++++++++++---------- daemon/api/codegen_type_gen.go | 24 +- daemon/daemonapi/lib_heartbeat.go | 92 ++++ daemon/daemonapi/lib_heartbeat_test.go | 88 ++++ daemon/daemonapi/post_daemon_hb_restart.go | 7 +- daemon/daemonapi/post_daemon_hb_sign.go | 23 +- daemon/daemonapi/post_daemon_hb_start.go | 7 +- daemon/daemonapi/post_daemon_hb_stop.go | 7 +- daemon/daemonapi/post_daemon_hb_wipe.go | 27 +- 20 files changed, 716 insertions(+), 304 deletions(-) create mode 100644 daemon/daemonapi/lib_heartbeat.go create mode 100644 daemon/daemonapi/lib_heartbeat_test.go diff --git a/core/commoncmd/daemon_hb_restart.go b/core/commoncmd/daemon_hb_restart.go index 1ef940dea..50ec70770 100644 --- a/core/commoncmd/daemon_hb_restart.go +++ b/core/commoncmd/daemon_hb_restart.go @@ -21,7 +21,15 @@ func NewCmdDaemonHeartbeatRestart() *cobra.Command { cmd := &cobra.Command{ Use: "restart NAME", Short: "restart a daemon heartbeat rx or tx", - Args: cobra.ExactArgs(1), + Long: ForProgram("Stop then start one direction of a configured heartbeat.\n\n" + + HeartbeatStreamNameHelp), + Example: ForProgram(` # restart the receiver of hb#1 on the local node + om daemon hb restart 1.rx + + # restart the sender of hb#1 on every node + om daemon hb restart 1.tx --node '*'`), + Args: cobra.ExactArgs(1), + ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] return options.Run() diff --git a/core/commoncmd/daemon_hb_sign.go b/core/commoncmd/daemon_hb_sign.go index 2f3bb6694..2abdc03d2 100644 --- a/core/commoncmd/daemon_hb_sign.go +++ b/core/commoncmd/daemon_hb_sign.go @@ -21,7 +21,15 @@ func NewCmdHeartbeatSign() *cobra.Command { cmd := &cobra.Command{ Use: "sign NAME", Short: "sign a heartbeat disk", - Args: cobra.ExactArgs(1), + Long: ForProgram("Write the signature the nodes of a disk heartbeat claim their slot with.\n\n" + + HeartbeatNameHelp + "\n\nThe heartbeat must be of type disk: this action writes to its dev."), + Example: ForProgram(` # sign the disk of hb#2 on the local node + om daemon hb sign 2 + + # sign it on every node + om daemon hb sign 2 --node '*'`), + Args: cobra.ExactArgs(1), + ValidArgsFunction: validHeartbeatNames, RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] return options.Run() diff --git a/core/commoncmd/daemon_hb_start.go b/core/commoncmd/daemon_hb_start.go index 6ffd3fe62..244053075 100644 --- a/core/commoncmd/daemon_hb_start.go +++ b/core/commoncmd/daemon_hb_start.go @@ -2,7 +2,6 @@ package commoncmd import ( "context" - "fmt" "net/http" "github.com/spf13/cobra" @@ -21,8 +20,19 @@ func NewCmdDaemonHeartbeatStart() *cobra.Command { options := CmdDaemonHeartbeatStart{} cmd := &cobra.Command{ Use: "start NAME", - Short: fmt.Sprintf("start a daemon heartbeat rx or tx"), - Args: cobra.ExactArgs(1), + Short: "start a daemon heartbeat rx or tx", + Long: ForProgram("Start one direction of a configured heartbeat.\n\n" + + HeartbeatStreamNameHelp), + Example: ForProgram(` # start the receiver of hb#1 on the local node + om daemon hb start 1.rx + + # start the sender of hb#1 on every node + om daemon hb start 1.tx --node '*' + + # the id "om daemon hb status" shows is accepted as it reads + om daemon hb start hb#1.rx`), + Args: cobra.ExactArgs(1), + ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] return options.Run() diff --git a/core/commoncmd/daemon_hb_stop.go b/core/commoncmd/daemon_hb_stop.go index 8e858d61f..0a9eee0f2 100644 --- a/core/commoncmd/daemon_hb_stop.go +++ b/core/commoncmd/daemon_hb_stop.go @@ -2,7 +2,6 @@ package commoncmd import ( "context" - "fmt" "net/http" "github.com/spf13/cobra" @@ -21,8 +20,16 @@ func NewCmdDaemonHeartbeatStop() *cobra.Command { options := CmdDaemonHeartbeatStop{} cmd := &cobra.Command{ Use: "stop NAME", - Short: fmt.Sprintf("stop a daemon heartbeat rx or tx"), - Args: cobra.ExactArgs(1), + Short: "stop a daemon heartbeat rx or tx", + Long: ForProgram("Stop one direction of a configured heartbeat.\n\n" + + HeartbeatStreamNameHelp), + Example: ForProgram(` # stop the receiver of hb#1 on the local node + om daemon hb stop 1.rx + + # stop the sender of hb#1 on every node + om daemon hb stop 1.tx --node '*'`), + Args: cobra.ExactArgs(1), + ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] return options.Run() diff --git a/core/commoncmd/daemon_hb_wipe.go b/core/commoncmd/daemon_hb_wipe.go index f40035832..a09b0be63 100644 --- a/core/commoncmd/daemon_hb_wipe.go +++ b/core/commoncmd/daemon_hb_wipe.go @@ -21,7 +21,15 @@ func NewCmdHeartbeatWipe() *cobra.Command { cmd := &cobra.Command{ Use: "wipe NAME", Short: "wipe a heartbeat disk", - Args: cobra.ExactArgs(1), + Long: ForProgram("Remove the signature the nodes of a disk heartbeat claim their slot with.\n\n" + + HeartbeatNameHelp + "\n\nThe heartbeat must be of type disk: this action writes to its dev."), + Example: ForProgram(` # wipe the disk of hb#2 on the local node + om daemon hb wipe 2 + + # wipe it on every node + om daemon hb wipe 2 --node '*'`), + Args: cobra.ExactArgs(1), + ValidArgsFunction: validHeartbeatNames, RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] return options.Run() diff --git a/core/commoncmd/daemon_listener_restart.go b/core/commoncmd/daemon_listener_restart.go index 6436ba509..4fcbce235 100644 --- a/core/commoncmd/daemon_listener_restart.go +++ b/core/commoncmd/daemon_listener_restart.go @@ -2,7 +2,6 @@ package commoncmd import ( "context" - "fmt" "net/http" "github.com/spf13/cobra" @@ -21,8 +20,15 @@ type ( func NewCmdDaemonListenerRestart() *cobra.Command { options := CmdDaemonListenerRestart{} cmd := &cobra.Command{ - Use: "restart NAME", - Short: fmt.Sprintf("restart a daemon listener"), + Use: "restart NAME", + Short: "restart a daemon listener", + Long: ForProgram("Stop then start one of the listeners the daemon serves the api with.\n\n" + + ListenerNameHelp), + Example: ForProgram(` # restart the tcp listener on the local node + om daemon listener restart api.inet + + # restart it on every node + om daemon listener restart api.inet --node '*'`), Args: cobra.ExactArgs(1), ValidArgsFunction: validListenerNames, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/core/commoncmd/daemon_listener_start.go b/core/commoncmd/daemon_listener_start.go index 1da8616ae..bbfe1a762 100644 --- a/core/commoncmd/daemon_listener_start.go +++ b/core/commoncmd/daemon_listener_start.go @@ -2,7 +2,6 @@ package commoncmd import ( "context" - "fmt" "net/http" "github.com/spf13/cobra" @@ -21,8 +20,15 @@ type ( func NewCmdDaemonListenerStart() *cobra.Command { options := CmdDaemonListenerStart{} cmd := &cobra.Command{ - Use: "start NAME", - Short: fmt.Sprintf("start a daemon a listener"), + Use: "start NAME", + Short: "start a daemon listener", + Long: ForProgram("Start one of the listeners the daemon serves the api with.\n\n" + + ListenerNameHelp), + Example: ForProgram(` # start the tcp listener on the local node + om daemon listener start api.inet + + # start it on every node + om daemon listener start api.inet --node '*'`), Args: cobra.ExactArgs(1), ValidArgsFunction: validListenerNames, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/core/commoncmd/daemon_listener_stop.go b/core/commoncmd/daemon_listener_stop.go index d6e0988a8..4cf2cd2a6 100644 --- a/core/commoncmd/daemon_listener_stop.go +++ b/core/commoncmd/daemon_listener_stop.go @@ -2,7 +2,6 @@ package commoncmd import ( "context" - "fmt" "net/http" "github.com/spf13/cobra" @@ -21,8 +20,15 @@ type ( func NewCmdDaemonListenerStop() *cobra.Command { options := CmdDaemonListenerStop{} cmd := &cobra.Command{ - Use: "stop NAME", - Short: fmt.Sprintf("stop a daemon listener"), + Use: "stop NAME", + Short: "stop a daemon listener", + Long: ForProgram("Stop one of the listeners the daemon serves the api with.\n\n" + + ListenerNameHelp), + Example: ForProgram(` # stop the tcp listener on the local node + om daemon listener stop api.inet + + # stop it on every node + om daemon listener stop api.inet --node '*'`), Args: cobra.ExactArgs(1), ValidArgsFunction: validListenerNames, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/core/commoncmd/daemon_subsystems.go b/core/commoncmd/daemon_subsystems.go index 85893ce06..003bd824c 100644 --- a/core/commoncmd/daemon_subsystems.go +++ b/core/commoncmd/daemon_subsystems.go @@ -1,10 +1,14 @@ package commoncmd import ( + "os" + "path/filepath" + "regexp" "strings" "github.com/spf13/cobra" + "github.com/opensvc/om3/v3/core/clusterhb" "github.com/opensvc/om3/v3/daemon/daemonenv" ) @@ -96,3 +100,85 @@ func filterPrefix(candidates []string, toComplete string) []string { } return l } + +// HeartbeatNameHelp is the NAME paragraph of the actions addressing a +// heartbeat as a whole, which all take the same argument. +const HeartbeatNameHelp = `NAME is a heartbeat: the index of a hb# section of the cluster +configuration, "1" for "hb#1". The "hb#" prefix the ID column of +"om daemon hb status" shows is accepted too. A heartbeat the node does +not configure is refused.` + +// HeartbeatStreamNameHelp is the NAME paragraph of the actions addressing +// one direction of a heartbeat, which is a component of its own. +const HeartbeatStreamNameHelp = `NAME is a heartbeat stream: the index of a hb# section of the +cluster configuration, suffixed with .rx for the receiver or .tx for the +sender, "1.rx" for the receiver of "hb#1". The "hb#" prefix the ID column +of "om daemon hb status" shows is accepted too. A stream the node does +not configure is refused.` + +// ListenerNameHelp is the NAME paragraph of the listener actions, naming +// the listeners that exist. There are only two, and they are the same on +// every node, so they are named here rather than looked up. +var ListenerNameHelp = `NAME is one of: + + ` + daemonenv.ListenerNameUX + ` the listener serving the unix socket + ` + daemonenv.ListenerNameInet + ` the listener serving the tcp port` + +// HeartbeatNames returns the heartbeats the local cluster configuration +// defines, named as the actions take them: "1" for the "hb#1" section. +func HeartbeatNames() []string { + n, err := clusterhb.New() + if err != nil { + return nil + } + sections := n.HbNames() + l := make([]string, 0, len(sections)) + for _, section := range sections { + l = append(l, strings.TrimPrefix(section, "hb#")) + } + return l +} + +// HeartbeatStreamNames returns the two streams of each heartbeat the local +// cluster configuration defines. +func HeartbeatStreamNames() []string { + names := HeartbeatNames() + l := make([]string, 0, len(names)*2) + for _, name := range names { + l = append(l, name+".rx", name+".tx") + } + return l +} + +// validHeartbeatNames completes the heartbeats an action may address. +func validHeartbeatNames(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return filterPrefix(HeartbeatNames(), toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// validHeartbeatStreamNames completes the heartbeat streams an action may +// address. +func validHeartbeatStreamNames(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return filterPrefix(HeartbeatStreamNames(), toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// omWord matches the name of the om command where a help text names it, +// which the word boundaries keep apart from a word ending with it. +var omWord = regexp.MustCompile(`\bom\b`) + +// ForProgram renders a help text written for the om command under the name +// the binary was invoked as, so an ox help does not tell its reader to type +// om. The commands of the two binaries are built here, from one source, and +// the root command names itself the same way. +func ForProgram(s string) string { + name := filepath.Base(os.Args[0]) + if name == "om" { + return s + } + return omWord.ReplaceAllString(s, name) +} diff --git a/core/commoncmd/daemon_subsystems_test.go b/core/commoncmd/daemon_subsystems_test.go index 3492cb398..793164ad1 100644 --- a/core/commoncmd/daemon_subsystems_test.go +++ b/core/commoncmd/daemon_subsystems_test.go @@ -1,6 +1,7 @@ package commoncmd import ( + "os" "strings" "testing" @@ -38,3 +39,40 @@ func TestFilterPrefix(t *testing.T) { assert.Equal(t, []string{"icfg", "icfg:"}, filterPrefix(AuditSubsystems, "icf")) assert.Empty(t, filterPrefix(AuditSubsystems, "nosuch")) } + +// TestListenerNameHelpNamesThemAll pins that the help a user reads names +// every listener an action may be addressed to, rather than a list kept +// beside them by hand. +func TestListenerNameHelpNamesThemAll(t *testing.T) { + for _, name := range daemonenv.ListenerNames { + assert.Containsf(t, strings.Fields(ListenerNameHelp), name, "%s is missing from the help", name) + } +} + +func TestForProgram(t *testing.T) { + withProgram := func(t *testing.T, path string) { + t.Helper() + saved := os.Args[0] + os.Args[0] = path + t.Cleanup(func() { os.Args[0] = saved }) + } + const help = ` # start it + om daemon hb start 1.rx + + the id "om daemon hb status" shows, from the om command` + + t.Run("om reads its own name", func(t *testing.T) { + withProgram(t, "/usr/bin/om") + assert.Equal(t, help, ForProgram(help)) + }) + t.Run("ox reads its own name", func(t *testing.T) { + withProgram(t, "/usr/bin/ox") + got := ForProgram(help) + assert.NotContains(t, strings.Fields(got), "om", "an ox help must not tell its reader to type om") + assert.Contains(t, got, "ox daemon hb start 1.rx") + assert.Contains(t, got, `"ox daemon hb status"`) + + // A word ending with the command name is not the command name. + assert.Contains(t, got, "from the ox command") + }) +} diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index 2067035e5..8471e952b 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -5266,7 +5266,18 @@ components: DaemonHeartbeatName: type: string example: 1.rx - description: Heartbeat name, example '1.rx' for heartbeat receiver of 'hb#1' section + description: | + Heartbeat name. + + A stream action (start, stop, restart) takes a stream: the index of a + 'hb#' section of the cluster configuration suffixed with '.rx' + for the receiver or '.tx' for the sender, '1.rx' for the receiver of + 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for + 'hb#1'. + + The 'hb#' prefix a heartbeat status shows in a stream id is accepted + in both, so a name read there can be sent back. A name the node does + not configure is refused. DaemonHeartbeatStream: allOf: diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 8e605b4cd..3feff69ae 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -6756,261 +6756,264 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index 0bf64f98e..433997895 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -1066,7 +1066,17 @@ type DRBDConfig struct { Data []byte `json:"data"` } -// DaemonHeartbeatName Heartbeat name, example '1.rx' for heartbeat receiver of 'hb#1' section +// DaemonHeartbeatName Heartbeat name. +// +// A stream action (start, stop, restart) takes a stream: the index of a +// 'hb#' section of the cluster configuration suffixed with '.rx' +// for the receiver or '.tx' for the sender, '1.rx' for the receiver of +// 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for +// 'hb#1'. +// +// The 'hb#' prefix a heartbeat status shows in a stream id is accepted +// in both, so a name read there can be sent back. A name the node does +// not configure is refused. type DaemonHeartbeatName = string // DaemonListener defines model for DaemonListener. @@ -2376,7 +2386,17 @@ type RidOptional = string // Roles defines model for Roles. type Roles = []Role -// InPathHeartbeatName Heartbeat name, example '1.rx' for heartbeat receiver of 'hb#1' section +// InPathHeartbeatName Heartbeat name. +// +// A stream action (start, stop, restart) takes a stream: the index of a +// 'hb#' section of the cluster configuration suffixed with '.rx' +// for the receiver or '.tx' for the sender, '1.rx' for the receiver of +// 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for +// 'hb#1'. +// +// The 'hb#' prefix a heartbeat status shows in a stream id is accepted +// in both, so a name read there can be sent back. A name the node does +// not configure is refused. type InPathHeartbeatName = DaemonHeartbeatName // InPathKind defines model for inPathKind. diff --git a/daemon/daemonapi/lib_heartbeat.go b/daemon/daemonapi/lib_heartbeat.go new file mode 100644 index 000000000..2b96d2afd --- /dev/null +++ b/daemon/daemonapi/lib_heartbeat.go @@ -0,0 +1,92 @@ +package daemonapi + +import ( + "fmt" + "slices" + "strings" + + "github.com/opensvc/om3/v3/core/clusterhb" + "github.com/opensvc/om3/v3/daemon/api" +) + +// heartbeatStreamSuffixes are the two directions a heartbeat runs in, each a +// component of its own, started and stopped on its own. +var heartbeatStreamSuffixes = []string{"rx", "tx"} + +// configuredHeartbeatNames returns the heartbeat section names the merged node +// and cluster configuration defines, as "hb#1". +// +// It is a variable so a test can pin the names a node has no configuration to +// hold. +var configuredHeartbeatNames = func() ([]string, error) { + n, err := clusterhb.New() + if err != nil { + return nil, err + } + return n.HbNames(), nil +} + +// heartbeatName returns the configuration section of the heartbeat an action +// addresses, "hb#1" for "1". +// +// The "hb#" prefix "om daemon hb status" shows in a stream id is accepted too, +// so a name read there can be typed back. +func heartbeatName(name api.InPathHeartbeatName) (string, error) { + names, err := configuredHeartbeatNames() + if err != nil { + return "", err + } + section := sectionOfHeartbeatName(name) + if slices.Contains(names, section) { + return section, nil + } + return "", fmt.Errorf("unknown heartbeat: %s%s", name, expectedHeartbeatNames(names, false)) +} + +// heartbeatStreamName returns the component name of the heartbeat stream an +// action addresses, "hb#1.rx" for "1.rx". +// +// The action is published as a message the heartbeat janitor subscribes to by +// component name, so a name no stream answers to is accepted, queued and acted +// on by nobody: "1.rxx" was a no-op the caller was told nothing about. +func heartbeatStreamName(name api.InPathHeartbeatName) (string, error) { + names, err := configuredHeartbeatNames() + if err != nil { + return "", err + } + stream := sectionOfHeartbeatName(name) + for _, section := range names { + for _, suffix := range heartbeatStreamSuffixes { + if stream == section+"."+suffix { + return stream, nil + } + } + } + return "", fmt.Errorf("unknown heartbeat stream: %s%s", name, expectedHeartbeatNames(names, true)) +} + +// sectionOfHeartbeatName returns the name as the configuration spells it, +// whether the caller typed the "hb#" prefix or not. +func sectionOfHeartbeatName(name api.InPathHeartbeatName) string { + return "hb#" + strings.TrimPrefix(string(name), "hb#") +} + +// expectedHeartbeatNames renders the tail of the error a refused name is +// reported with, naming what the node would have accepted. +func expectedHeartbeatNames(names []string, streams bool) string { + l := make([]string, 0, len(names)*2) + for _, section := range names { + short := strings.TrimPrefix(section, "hb#") + if !streams { + l = append(l, short) + continue + } + for _, suffix := range heartbeatStreamSuffixes { + l = append(l, short+"."+suffix) + } + } + if len(l) == 0 { + return ", and this node configures no heartbeat" + } + return ", expected one of " + strings.Join(l, ", ") +} diff --git a/daemon/daemonapi/lib_heartbeat_test.go b/daemon/daemonapi/lib_heartbeat_test.go new file mode 100644 index 000000000..fd0ed3008 --- /dev/null +++ b/daemon/daemonapi/lib_heartbeat_test.go @@ -0,0 +1,88 @@ +package daemonapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/opensvc/om3/v3/daemon/api" +) + +// withConfiguredHeartbeats pins the heartbeats the node configures, so the +// names are checked against a known list rather than against whatever +// configuration the test host carries. +func withConfiguredHeartbeats(t *testing.T, names ...string) { + t.Helper() + saved := configuredHeartbeatNames + configuredHeartbeatNames = func() ([]string, error) { return names, nil } + t.Cleanup(func() { configuredHeartbeatNames = saved }) +} + +func TestHeartbeatStreamName(t *testing.T) { + withConfiguredHeartbeats(t, "hb#1", "hb#11") + for _, tc := range []struct { + name string + want string + }{ + {"1.rx", "hb#1.rx"}, + {"1.tx", "hb#1.tx"}, + {"11.rx", "hb#11.rx"}, + + // The stream id "om daemon hb status" shows carries the prefix, so + // a name read there is typed back as it was read. + {"hb#1.rx", "hb#1.rx"}, + } { + got, err := heartbeatStreamName(api.InPathHeartbeatName(tc.name)) + require.NoErrorf(t, err, "%s must be accepted", tc.name) + assert.Equal(t, tc.want, got) + } +} + +func TestHeartbeatStreamNameRefusesWhatNoStreamAnswersTo(t *testing.T) { + withConfiguredHeartbeats(t, "hb#1", "hb#11") + // "1.rxx" is the one to refuse loudest: it was accepted, queued, and + // acted on by nobody. "1" is the heartbeat, not a stream of it, and + // "2.rx" is a stream of a heartbeat this node does not configure. + for _, name := range []string{"", "1", "hb#1", "1.rxx", "1.RX", "2.rx", "1.rx.tx"} { + _, err := heartbeatStreamName(api.InPathHeartbeatName(name)) + require.Errorf(t, err, "%s must not be accepted", name) + assert.Containsf(t, err.Error(), "1.rx, 1.tx, 11.rx, 11.tx", "the error must name the streams that exist, got %s", err) + } +} + +func TestHeartbeatName(t *testing.T) { + withConfiguredHeartbeats(t, "hb#1", "hb#11") + for _, tc := range []struct { + name string + want string + }{ + {"1", "hb#1"}, + {"11", "hb#11"}, + {"hb#1", "hb#1"}, + } { + got, err := heartbeatName(api.InPathHeartbeatName(tc.name)) + require.NoErrorf(t, err, "%s must be accepted", tc.name) + assert.Equal(t, tc.want, got) + } + for _, name := range []string{"", "2", "1.rx", "hb#"} { + _, err := heartbeatName(api.InPathHeartbeatName(name)) + require.Errorf(t, err, "%s must not be accepted", name) + assert.Containsf(t, err.Error(), "expected one of 1, 11", "the error must name the heartbeats that exist, got %s", err) + } +} + +func TestHeartbeatNameOnANodeWithNoHeartbeat(t *testing.T) { + withConfiguredHeartbeats(t) + for _, err := range []error{ + second(heartbeatName(api.InPathHeartbeatName("1"))), + second(heartbeatStreamName(api.InPathHeartbeatName("1.rx"))), + } { + require.Error(t, err) + assert.Contains(t, err.Error(), "configures no heartbeat") + } +} + +func second(_ string, err error) error { + return err +} diff --git a/daemon/daemonapi/post_daemon_hb_restart.go b/daemon/daemonapi/post_daemon_hb_restart.go index 08fa965c7..2d23dd385 100644 --- a/daemon/daemonapi/post_daemon_hb_restart.go +++ b/daemon/daemonapi/post_daemon_hb_restart.go @@ -1,7 +1,6 @@ package daemonapi import ( - "fmt" "net/http" "github.com/labstack/echo/v4" @@ -15,7 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatRestart(ctx echo.Context, nodename api.In return err } nodename = a.parseNodename(nodename) - return a.postDaemonSubAction(ctx, nodename, "restart", fmt.Sprintf("hb#%s", name), func(c *client.T) (*http.Response, error) { + localName, err := heartbeatStreamName(name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) + } + return a.postDaemonSubAction(ctx, nodename, "restart", localName, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatRestart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_hb_sign.go b/daemon/daemonapi/post_daemon_hb_sign.go index 28db439d1..73dd8b694 100644 --- a/daemon/daemonapi/post_daemon_hb_sign.go +++ b/daemon/daemonapi/post_daemon_hb_sign.go @@ -28,33 +28,36 @@ func (a *DaemonAPI) PostDaemonHeartbeatSign(ctx echo.Context, nodename api.InPat func localPostDaemonHeartbeatSign(ctx echo.Context, name api.InPathHeartbeatName) error { log := LogHandler(ctx, "postDaemonHeartbeatSign") + section, err := heartbeatName(name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) + } var i any - i, err := object.NewCluster(object.WithVolatile(true)) + i, err = object.NewCluster(object.WithVolatile(true)) if err != nil { log.Warnf("new cluster object failed: %v", err) return JSONProblemf(ctx, http.StatusInternalServerError, "new cluster object failed", "%s", err) } config := (i.(configProvider)).Config() - section := "hb#" + string(name) hbType := config.GetString(key.New(section, "type")) if hbType != "disk" { - log.Tracef("sign heartbeat disk refused: unexpected hb#%s.type %s", name, hbType) - return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sign heartbeat disk refused: unexpected hb#%s.type %s", name, hbType) + log.Tracef("sign heartbeat disk refused: unexpected %s.type %s", section, hbType) + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sign heartbeat disk refused: unexpected %s.type %s", section, hbType) } devPath := config.GetString(key.New(section, "dev")) if devPath == "" { - log.Warnf("sign heartbeat disk refused: unexpected empty hb#%s.dev", name) - return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sign heartbeat disk refused: unexpected empty hb#%s.dev", name) + log.Warnf("sign heartbeat disk refused: unexpected empty %s.dev", section) + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sign heartbeat disk refused: unexpected empty %s.dev", section) } - log.Infof("sign heartbeat disk %s dev %s", name, devPath) + log.Infof("sign heartbeat disk %s dev %s", section, devPath) err = sign.CreateAndFillDisk(devPath) if err != nil { - log.Warnf("sign heartbeat disk %s dev %s: %s", name, devPath, err) - return JSONProblemf(ctx, http.StatusInternalServerError, "Heartbeat disk sign error", "sign heartbeat disk %s dev %s: %s", name, devPath, err) + log.Warnf("sign heartbeat disk %s dev %s: %s", section, devPath, err) + return JSONProblemf(ctx, http.StatusInternalServerError, "Heartbeat disk sign error", "sign heartbeat disk %s dev %s: %s", section, devPath, err) } - return JSONProblemf(ctx, http.StatusOK, "Heartbeat disk signed", "sign heartbeat %s on %s", name, devPath) + return JSONProblemf(ctx, http.StatusOK, "Heartbeat disk signed", "sign heartbeat %s on %s", section, devPath) } diff --git a/daemon/daemonapi/post_daemon_hb_start.go b/daemon/daemonapi/post_daemon_hb_start.go index 0b2a8ffac..0d0e37e95 100644 --- a/daemon/daemonapi/post_daemon_hb_start.go +++ b/daemon/daemonapi/post_daemon_hb_start.go @@ -1,7 +1,6 @@ package daemonapi import ( - "fmt" "net/http" "github.com/labstack/echo/v4" @@ -15,7 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatStart(ctx echo.Context, nodename api.InPa return err } nodename = a.parseNodename(nodename) - return a.postDaemonSubAction(ctx, nodename, "start", fmt.Sprintf("hb#%s", name), func(c *client.T) (*http.Response, error) { + localName, err := heartbeatStreamName(name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) + } + return a.postDaemonSubAction(ctx, nodename, "start", localName, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatStart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_hb_stop.go b/daemon/daemonapi/post_daemon_hb_stop.go index 35f5b1eb1..172e50761 100644 --- a/daemon/daemonapi/post_daemon_hb_stop.go +++ b/daemon/daemonapi/post_daemon_hb_stop.go @@ -1,7 +1,6 @@ package daemonapi import ( - "fmt" "net/http" "github.com/labstack/echo/v4" @@ -15,7 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatStop(ctx echo.Context, nodename api.InPat return err } nodename = a.parseNodename(nodename) - return a.postDaemonSubAction(ctx, nodename, "stop", fmt.Sprintf("hb#%s", name), func(c *client.T) (*http.Response, error) { + localName, err := heartbeatStreamName(name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) + } + return a.postDaemonSubAction(ctx, nodename, "stop", localName, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatStop(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_hb_wipe.go b/daemon/daemonapi/post_daemon_hb_wipe.go index 017998c1c..b2ba8d11c 100644 --- a/daemon/daemonapi/post_daemon_hb_wipe.go +++ b/daemon/daemonapi/post_daemon_hb_wipe.go @@ -28,25 +28,28 @@ func (a *DaemonAPI) PostDaemonHeartbeatWipe(ctx echo.Context, nodename api.InPat func localPostDaemonHeartbeatWipe(ctx echo.Context, name api.InPathHeartbeatName) error { log := LogHandler(ctx, "postDaemonHeartbeatWipe") + section, err := heartbeatName(name) + if err != nil { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) + } var i any - i, err := object.NewCluster(object.WithVolatile(true)) + i, err = object.NewCluster(object.WithVolatile(true)) if err != nil { log.Warnf("new cluster object failed: %v", err) return JSONProblemf(ctx, http.StatusInternalServerError, "NewCluster", "new cluster object failed: %v", err) } config := (i.(configProvider)).Config() - section := "hb#" + string(name) hbType := config.GetString(key.New(section, "type")) if hbType != "disk" { - log.Tracef("refuse to wipe heartbeat disk: unexpected hb#%s.type %s", name, hbType) - return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "refuse to wipe heartbeat disk: unexpected hb#%s.type %s", name, hbType) + log.Tracef("refuse to wipe heartbeat disk: unexpected %s.type %s", section, hbType) + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "refuse to wipe heartbeat disk: unexpected %s.type %s", section, hbType) } devPath := config.GetString(key.New(section, "dev")) if devPath == "" { - log.Warnf("refuse to wipe heartbeat disk: unexpected empty hb#%s.dev", name) - return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "refuse to wipe heartbeat disk: unexpected empty hb#%s.dev", name) + log.Warnf("refuse to wipe heartbeat disk: unexpected empty %s.dev", section) + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "refuse to wipe heartbeat disk: unexpected empty %s.dev", section) } hasSignature, err := sign.EnsureSignature(devPath) @@ -56,16 +59,16 @@ func localPostDaemonHeartbeatWipe(ctx echo.Context, name api.InPathHeartbeatName } if !hasSignature { - log.Infof("heartbeat %s dev %s has no signature, nothing to wipe", name, devPath) - return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "heartbeat %s dev %s has no signature, nothing to wipe", name, devPath) + log.Infof("heartbeat %s dev %s has no signature, nothing to wipe", section, devPath) + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "heartbeat %s dev %s has no signature, nothing to wipe", section, devPath) } - log.Infof("wipe heartbeat %s dev %s", name, devPath) + log.Infof("wipe heartbeat %s dev %s", section, devPath) err = sign.RemoveHeaderFromDisk(devPath) if err != nil { - log.Warnf("wipe heartbeat disk %s dev %s failed: remove header: %s", name, devPath, err) - return JSONProblemf(ctx, http.StatusInternalServerError, "RemoveHeaderFromDisk", "wipe heartbeat disk %s dev %s failed: remove header: %s", name, devPath, err) + log.Warnf("wipe heartbeat disk %s dev %s failed: remove header: %s", section, devPath, err) + return JSONProblemf(ctx, http.StatusInternalServerError, "RemoveHeaderFromDisk", "wipe heartbeat disk %s dev %s failed: remove header: %s", section, devPath, err) } - return JSONProblemf(ctx, http.StatusOK, "heartbeat disk wiped", "wipe heartbeat %s on %s", name, devPath) + return JSONProblemf(ctx, http.StatusOK, "heartbeat disk wiped", "wipe heartbeat %s on %s", section, devPath) } From e64c894505f8b77ef55e7a9ccde15d81affc1266 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 2 Sep 2026 21:47:28 +0200 Subject: [PATCH 2/7] Refuse an action the unix socket listener answers to none of "om daemon listener restart api.ux" exited zero and logged, on the node it was asked of: listener: ux: daemon control restart asked, ignored api: ux: ask to restart component: api.ux The name is a listener, so the check the actions gained when they were aligned with the audit subsystem names let it through, and the api answered "daemon action queued". The listener then dropped it. That listener has no start, stop or restart of its own: it lives as long as the daemon does, and the request asking for its restart travels through it. The three actions used to be its log level actions, and when those were dropped the branch stayed, to log that it ignored what was left. So the api refuses them now, saying what it would have accepted, the way an unknown name is refused. The name check runs first: a name no listener answers to is not a listener with no lifecycle, it is no listener at all. The listener subscribes to no daemon control message any more. The message it was handed was the only one it could receive, and it existed to be ignored. The commands say it: the help of the three actions names api.inet as the value, and names api.ux too, with the reason it is not one, because a reader who knows it exists from an audit or a status would read its absence as an oversight. The completion offers what the api accepts. The api schema carries the same paragraph. Checked on a live cluster: the three actions on api.ux exit 1 with the reason, a restart of api.inet stops and starts it, and an unknown name is refused as before. --- core/commoncmd/daemon_subsystems.go | 25 +- core/commoncmd/daemon_subsystems_test.go | 14 +- daemon/api/api.yaml | 5 + daemon/api/codegen_server_gen.go | 504 +++++++++--------- daemon/api/codegen_type_gen.go | 10 + daemon/daemonapi/lib_listener.go | 19 + daemon/daemonapi/lib_listener_test.go | 25 + .../daemonapi/post_daemon_listener_restart.go | 2 +- .../daemonapi/post_daemon_listener_start.go | 2 +- daemon/daemonapi/post_daemon_listener_stop.go | 2 +- daemon/daemonenv/main.go | 9 + daemon/listener/lsnrhttpux/main.go | 16 +- 12 files changed, 359 insertions(+), 274 deletions(-) diff --git a/core/commoncmd/daemon_subsystems.go b/core/commoncmd/daemon_subsystems.go index 003bd824c..5c98d6004 100644 --- a/core/commoncmd/daemon_subsystems.go +++ b/core/commoncmd/daemon_subsystems.go @@ -80,12 +80,13 @@ func validAuditSubsystems(_ *cobra.Command, args []string, toComplete string) ([ return filterPrefix(AuditSubsystems, toComplete), cobra.ShellCompDirectiveNoFileComp } -// validListenerNames completes the listeners an action may address. +// validListenerNames completes the listeners a start, stop or restart may +// address, which the unix socket listener is not one of. func validListenerNames(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } - return filterPrefix(daemonenv.ListenerNames, toComplete), cobra.ShellCompDirectiveNoFileComp + return filterPrefix(daemonenv.LifecycleListenerNames, toComplete), cobra.ShellCompDirectiveNoFileComp } func filterPrefix(candidates []string, toComplete string) []string { @@ -116,13 +117,21 @@ sender, "1.rx" for the receiver of "hb#1". The "hb#" prefix the ID column of "om daemon hb status" shows is accepted too. A stream the node does not configure is refused.` -// ListenerNameHelp is the NAME paragraph of the listener actions, naming -// the listeners that exist. There are only two, and they are the same on -// every node, so they are named here rather than looked up. -var ListenerNameHelp = `NAME is one of: +// ListenerNameHelp is the NAME paragraph of the listener lifecycle actions, +// naming the listeners they may be addressed to. There are only two +// listeners, and they are the same on every node, so they are named here +// rather than looked up. +// +// The unix socket listener is named too, saying why it is not a value: a +// reader who knows it exists, from an audit or from a status, would take its +// absence for an oversight. +var ListenerNameHelp = `NAME is: + + ` + daemonenv.ListenerNameInet + ` the listener serving the tcp port - ` + daemonenv.ListenerNameUX + ` the listener serving the unix socket - ` + daemonenv.ListenerNameInet + ` the listener serving the tcp port` +` + daemonenv.ListenerNameUX + `, the listener serving the unix socket, has no start, stop or restart +of its own: it lives as long as the daemon does, and the request asking +for it travels through it. Restart the daemon to restart it.` // HeartbeatNames returns the heartbeats the local cluster configuration // defines, named as the actions take them: "1" for the "hb#1" section. diff --git a/core/commoncmd/daemon_subsystems_test.go b/core/commoncmd/daemon_subsystems_test.go index 793164ad1..696d67e8b 100644 --- a/core/commoncmd/daemon_subsystems_test.go +++ b/core/commoncmd/daemon_subsystems_test.go @@ -41,12 +41,20 @@ func TestFilterPrefix(t *testing.T) { } // TestListenerNameHelpNamesThemAll pins that the help a user reads names -// every listener an action may be addressed to, rather than a list kept -// beside them by hand. +// every listener, rather than a list kept beside them by hand: the ones a +// start, stop or restart may address, and the one that answers to none of +// the three, whose absence would otherwise read as an oversight. func TestListenerNameHelpNamesThemAll(t *testing.T) { for _, name := range daemonenv.ListenerNames { - assert.Containsf(t, strings.Fields(ListenerNameHelp), name, "%s is missing from the help", name) + assert.Containsf(t, ListenerNameHelp, name, "%s is missing from the help", name) } + for _, name := range daemonenv.LifecycleListenerNames { + assert.Containsf(t, daemonenv.ListenerNames, name, "%s is addressable but is no listener", name) + } + assert.NotContains(t, daemonenv.LifecycleListenerNames, daemonenv.ListenerNameUX, + "the unix socket listener lives as long as the daemon does") + assert.Contains(t, ListenerNameHelp, "lives as long as the daemon", + "the help must say why the unix socket listener is not a value") } func TestForProgram(t *testing.T) { diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index 8471e952b..a04738036 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -5352,6 +5352,11 @@ components: The listeners are named the same here and in the audit subsystem list: api.ux serves the unix socket, api.inet the tcp port. + Only api.inet answers to a start, stop or restart. api.ux lives as + long as the daemon does, and the request asking for it travels + through it, so the three actions refuse it: restart the daemon to + restart it. + DaemonLocal: type: object required: diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 3feff69ae..2dc125951 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -6763,257 +6763,259 @@ var swaggerSpec = []string{ "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", + "51Drw6dPnz79nwAAAP//", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index 433997895..c88d6162d 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -1094,6 +1094,11 @@ type DaemonListener struct { // // The listeners are named the same here and in the audit subsystem // list: api.ux serves the unix socket, api.inet the tcp port. +// +// Only api.inet answers to a start, stop or restart. api.ux lives as +// long as the daemon does, and the request asking for it travels +// through it, so the three actions refuse it: restart the daemon to +// restart it. type DaemonListenerName string // DaemonPid defines model for DaemonPid. @@ -2406,6 +2411,11 @@ type InPathKind = Kind // // The listeners are named the same here and in the audit subsystem // list: api.ux serves the unix socket, api.inet the tcp port. +// +// Only api.inet answers to a start, stop or restart. api.ux lives as +// long as the daemon does, and the request asking for it travels +// through it, so the three actions refuse it: restart the daemon to +// restart it. type InPathListenerName = DaemonListenerName // InPathName defines model for inPathName. diff --git a/daemon/daemonapi/lib_listener.go b/daemon/daemonapi/lib_listener.go index 1329c8269..f76f9b413 100644 --- a/daemon/daemonapi/lib_listener.go +++ b/daemon/daemonapi/lib_listener.go @@ -22,3 +22,22 @@ func listenerName(name api.InPathListenerName) (string, error) { } return s, nil } + +// lifecycleListenerName returns the name of the listener a start, stop or +// restart addresses. +// +// The unix socket listener answers to none of the three: it lives as long as +// the daemon does, and the request asking for its restart travels through it. +// It used to be handed the message, log that it ignored it, and leave the +// caller told the action was queued. +func lifecycleListenerName(name api.InPathListenerName, action string) (string, error) { + s, err := listenerName(name) + if err != nil { + return "", err + } + if !slices.Contains(daemonenv.LifecycleListenerNames, s) { + return "", fmt.Errorf("%s has no %s of its own: it lives as long as the daemon does, expected one of %s", + s, action, strings.Join(daemonenv.LifecycleListenerNames, ", ")) + } + return s, nil +} diff --git a/daemon/daemonapi/lib_listener_test.go b/daemon/daemonapi/lib_listener_test.go index 2bf048c3d..92b1fe8a8 100644 --- a/daemon/daemonapi/lib_listener_test.go +++ b/daemon/daemonapi/lib_listener_test.go @@ -35,3 +35,28 @@ func TestListenerNameRefusesWhatNoListenerAnswersTo(t *testing.T) { assert.Contains(t, err.Error(), daemonenv.ListenerNameInet, "the error must name the listeners that exist") } } + +// TestLifecycleListenerNameRefusesTheOneWithNoLifecycle pins that the +// three actions refuse the unix socket listener rather than publishing a +// message it receives, ignores, and leaves the caller told was queued. +func TestLifecycleListenerNameRefusesTheOneWithNoLifecycle(t *testing.T) { + for _, action := range []string{"start", "stop", "restart"} { + got, err := lifecycleListenerName(api.InPathListenerName(daemonenv.ListenerNameInet), action) + require.NoErrorf(t, err, "%s must be addressable for a %s", daemonenv.ListenerNameInet, action) + assert.Equal(t, daemonenv.ListenerNameInet, got) + + _, err = lifecycleListenerName(api.InPathListenerName(daemonenv.ListenerNameUX), action) + require.Errorf(t, err, "%s must refuse a %s", daemonenv.ListenerNameUX, action) + assert.Contains(t, err.Error(), "lives as long as the daemon", "the error must say why") + assert.Contains(t, err.Error(), daemonenv.ListenerNameInet, "the error must name the listener that answers") + } +} + +// TestLifecycleListenerNameStillRefusesAnUnknownName keeps the name check +// in front of the lifecycle check: an unknown name is not a listener with +// no lifecycle, it is no listener at all. +func TestLifecycleListenerNameStillRefusesAnUnknownName(t *testing.T) { + _, err := lifecycleListenerName(api.InPathListenerName("nope"), "restart") + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown listener") +} diff --git a/daemon/daemonapi/post_daemon_listener_restart.go b/daemon/daemonapi/post_daemon_listener_restart.go index b610feda8..788a64b85 100644 --- a/daemon/daemonapi/post_daemon_listener_restart.go +++ b/daemon/daemonapi/post_daemon_listener_restart.go @@ -14,7 +14,7 @@ func (a *DaemonAPI) PostDaemonListenerRestart(ctx echo.Context, nodename api.InP return err } nodename = a.parseNodename(nodename) - localName, err := listenerName(name) + localName, err := lifecycleListenerName(name, "restart") if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } diff --git a/daemon/daemonapi/post_daemon_listener_start.go b/daemon/daemonapi/post_daemon_listener_start.go index ef8e460fb..4f188861c 100644 --- a/daemon/daemonapi/post_daemon_listener_start.go +++ b/daemon/daemonapi/post_daemon_listener_start.go @@ -16,7 +16,7 @@ func (a *DaemonAPI) PostDaemonListenerStart(ctx echo.Context, nodename api.InPat // parseNodename here too: start was the one action of the three not // normalizing the nodename it was given. nodename = a.parseNodename(nodename) - localName, err := listenerName(name) + localName, err := lifecycleListenerName(name, "start") if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } diff --git a/daemon/daemonapi/post_daemon_listener_stop.go b/daemon/daemonapi/post_daemon_listener_stop.go index 819fa9485..637589ad1 100644 --- a/daemon/daemonapi/post_daemon_listener_stop.go +++ b/daemon/daemonapi/post_daemon_listener_stop.go @@ -14,7 +14,7 @@ func (a *DaemonAPI) PostDaemonListenerStop(ctx echo.Context, nodename api.InPath return err } nodename = a.parseNodename(nodename) - localName, err := listenerName(name) + localName, err := lifecycleListenerName(name, "stop") if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } diff --git a/daemon/daemonenv/main.go b/daemon/daemonenv/main.go index 9b7922d21..bba50b764 100644 --- a/daemon/daemonenv/main.go +++ b/daemon/daemonenv/main.go @@ -30,6 +30,15 @@ const ( // ListenerNames are the listeners an api action may be addressed to. var ListenerNames = []string{ListenerNameUX, ListenerNameInet} +// LifecycleListenerNames are the listeners a start, stop or restart may be +// addressed to. +// +// The unix socket listener is not one of them: it lives as long as the daemon +// does, and the request asking for its restart travels through it. It used to +// accept the three actions, log that it ignored them, and answer the caller +// that they were queued. +var LifecycleListenerNames = []string{ListenerNameInet} + var ( HTTPPort = 1215 diff --git a/daemon/listener/lsnrhttpux/main.go b/daemon/listener/lsnrhttpux/main.go index 9b008b8ae..2293c197c 100644 --- a/daemon/listener/lsnrhttpux/main.go +++ b/daemon/listener/lsnrhttpux/main.go @@ -101,15 +101,18 @@ func (t *T) serve(ctx context.Context, errC chan<- error) { t.log.Infof("stopped") } -// janitor startup initial http ux listener, then watch events to stop, start or restart listener. -// events are: DaemonCtl,name=api.ux, ClusterConfigUpdated,node= with changed lsnr addr or port -// TODO: also watch for tls setting changed +// janitor watches the events the ux listener acts on, which are the audit +// sessions starting and stopping. +// +// It subscribes to no daemon control message: this listener has no start, +// stop or restart of its own, it lives as long as the daemon does, and the +// api refuses those actions rather than publishing a message it would +// receive and ignore. func (t *T) janitor(ctx context.Context, errC chan<- error) { defer t.wg.Done() sub := pubsub.SubFromContext(ctx, "daemon.lsnr.http.ux") sub.AddFilter(&msgbus.AuditStart{}) sub.AddFilter(&msgbus.AuditStop{}) - sub.AddFilter(&msgbus.DaemonCtl{}, pubsub.Label{"id", daemonenv.ListenerNameUX}) sub.Start() defer func() { if err := sub.Stop(); err != nil { @@ -127,11 +130,6 @@ func (t *T) janitor(ctx context.Context, errC chan<- error) { t.log.HandleAuditStart(m.Q, m.Subsystems, daemonenv.ListenerNameFamily, daemonenv.ListenerNameUX) case *msgbus.AuditStop: t.log.HandleAuditStop(m.Q, m.Subsystems, daemonenv.ListenerNameFamily, daemonenv.ListenerNameUX) - case *msgbus.DaemonCtl: - // The log level actions were the only ones this - // listener acted on. It has no start, stop or restart - // of its own: it lives as long as the daemon does. - t.log.Infof("daemon control %s asked, ignored", m.Action) } } } From e21cc9ddd5d65c843b1d8bc4ce910da84321f656 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 2 Sep 2026 22:04:26 +0200 Subject: [PATCH 3/7] Take a heartbeat as both its streams, and several of them The two directions of a heartbeat run and stop on their own, which is why an action addresses one of them. They are one thing to a reader of "om daemon hb status", who saw hb#2 beating in both directions and had to type its two streams to stop it. "om daemon hb stop hb#2" answered that hb#2 was not a stream, listing the ten that are. A heartbeat named without a suffix now addresses both of its streams. The expansion is done where the name is checked, so it is the api that takes it and not just the command: the request publishes the message its name resolves to, one or two, and answers with what it queued. The actions take several names too. They ran one component each, and a reader stopping a heartbeat pair, or the same stream of two heartbeats, ran the command twice. Each name is attempted, and the failures are joined rather than the first ending the run: two unknown names are two errors, not one and a silence. The refusal names the heartbeats after the streams, so the name that prompted this reads as the shorthand it is rather than as unsupported. The completion offers both, and keeps offering after the first name, less the ones already typed. The help and the api schema say the two rules. Checked on a live cluster: "om daemon hb stop hb#2.tx 2.rx" stops the pair, whichever way each is spelled, "om daemon hb start hb#2" starts it back, and two unknown names exit 1 with both reasons. --- core/commoncmd/daemon_hb_restart.go | 28 +- core/commoncmd/daemon_hb_sign.go | 21 +- core/commoncmd/daemon_hb_start.go | 28 +- core/commoncmd/daemon_hb_stop.go | 28 +- core/commoncmd/daemon_hb_wipe.go | 21 +- core/commoncmd/daemon_sub_action.go | 11 + core/commoncmd/daemon_subsystems.go | 62 ++- core/commoncmd/daemon_subsystems_test.go | 18 + daemon/api/api.yaml | 5 +- daemon/api/codegen_server_gen.go | 516 +++++++++--------- daemon/api/codegen_type_gen.go | 10 +- daemon/daemonapi/lib_heartbeat.go | 76 ++- daemon/daemonapi/lib_heartbeat_test.go | 36 +- .../daemonapi/lib_post_daemon_sub_action.go | 24 +- daemon/daemonapi/post_daemon_hb_restart.go | 4 +- daemon/daemonapi/post_daemon_hb_start.go | 4 +- daemon/daemonapi/post_daemon_hb_stop.go | 4 +- .../daemonapi/post_daemon_listener_restart.go | 2 +- .../daemonapi/post_daemon_listener_start.go | 2 +- daemon/daemonapi/post_daemon_listener_stop.go | 2 +- 20 files changed, 515 insertions(+), 387 deletions(-) diff --git a/core/commoncmd/daemon_hb_restart.go b/core/commoncmd/daemon_hb_restart.go index 50ec70770..2004eba7d 100644 --- a/core/commoncmd/daemon_hb_restart.go +++ b/core/commoncmd/daemon_hb_restart.go @@ -12,26 +12,29 @@ import ( type ( CmdDaemonHeartbeatRestart struct { CmdDaemonSubAction - Name string + Names []string } ) func NewCmdDaemonHeartbeatRestart() *cobra.Command { options := CmdDaemonHeartbeatRestart{} cmd := &cobra.Command{ - Use: "restart NAME", - Short: "restart a daemon heartbeat rx or tx", - Long: ForProgram("Stop then start one direction of a configured heartbeat.\n\n" + + Use: "restart NAME...", + Short: "restart daemon heartbeat rx or tx streams", + Long: ForProgram("Stop then start the named directions of the configured heartbeats.\n\n" + HeartbeatStreamNameHelp), Example: ForProgram(` # restart the receiver of hb#1 on the local node om daemon hb restart 1.rx - # restart the sender of hb#1 on every node - om daemon hb restart 1.tx --node '*'`), - Args: cobra.ExactArgs(1), + # restart both streams of hb#1 + om daemon hb restart 1 + + # restart both streams of hb#1 and hb#2 on every node + om daemon hb restart 1 2 --node '*'`), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { - options.Name = args[0] + options.Names = args return options.Run() }, } @@ -41,8 +44,9 @@ func NewCmdDaemonHeartbeatRestart() *cobra.Command { } func (t *CmdDaemonHeartbeatRestart) Run() error { - fn := func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { - return c.PostDaemonHeartbeatRestart(ctx, nodename, t.Name) - } - return t.CmdDaemonSubAction.Run(fn) + return t.CmdDaemonSubAction.RunForEach(t.Names, func(name string) apiFuncWithNode { + return func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { + return c.PostDaemonHeartbeatRestart(ctx, nodename, name) + } + }) } diff --git a/core/commoncmd/daemon_hb_sign.go b/core/commoncmd/daemon_hb_sign.go index 2abdc03d2..e8c20e9a7 100644 --- a/core/commoncmd/daemon_hb_sign.go +++ b/core/commoncmd/daemon_hb_sign.go @@ -12,26 +12,26 @@ import ( type ( CmdDaemonHeartbeatSign struct { CmdDaemonSubAction - Name string + Names []string } ) func NewCmdHeartbeatSign() *cobra.Command { options := CmdDaemonHeartbeatSign{} cmd := &cobra.Command{ - Use: "sign NAME", + Use: "sign NAME...", Short: "sign a heartbeat disk", Long: ForProgram("Write the signature the nodes of a disk heartbeat claim their slot with.\n\n" + HeartbeatNameHelp + "\n\nThe heartbeat must be of type disk: this action writes to its dev."), Example: ForProgram(` # sign the disk of hb#2 on the local node om daemon hb sign 2 - # sign it on every node - om daemon hb sign 2 --node '*'`), - Args: cobra.ExactArgs(1), + # sign the disks of hb#2 and hb#3 on every node + om daemon hb sign 2 3 --node '*'`), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatNames, RunE: func(cmd *cobra.Command, args []string) error { - options.Name = args[0] + options.Names = args return options.Run() }, } @@ -41,8 +41,9 @@ func NewCmdHeartbeatSign() *cobra.Command { } func (t *CmdDaemonHeartbeatSign) Run() error { - fn := func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { - return c.PostDaemonHeartbeatSign(ctx, nodename, t.Name) - } - return t.CmdDaemonSubAction.Run(fn) + return t.CmdDaemonSubAction.RunForEach(t.Names, func(name string) apiFuncWithNode { + return func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { + return c.PostDaemonHeartbeatSign(ctx, nodename, name) + } + }) } diff --git a/core/commoncmd/daemon_hb_start.go b/core/commoncmd/daemon_hb_start.go index 244053075..a65fd1747 100644 --- a/core/commoncmd/daemon_hb_start.go +++ b/core/commoncmd/daemon_hb_start.go @@ -12,29 +12,32 @@ import ( type ( CmdDaemonHeartbeatStart struct { CmdDaemonSubAction - Name string + Names []string } ) func NewCmdDaemonHeartbeatStart() *cobra.Command { options := CmdDaemonHeartbeatStart{} cmd := &cobra.Command{ - Use: "start NAME", - Short: "start a daemon heartbeat rx or tx", - Long: ForProgram("Start one direction of a configured heartbeat.\n\n" + + Use: "start NAME...", + Short: "start daemon heartbeat rx or tx streams", + Long: ForProgram("Start the named directions of the configured heartbeats.\n\n" + HeartbeatStreamNameHelp), Example: ForProgram(` # start the receiver of hb#1 on the local node om daemon hb start 1.rx - # start the sender of hb#1 on every node - om daemon hb start 1.tx --node '*' + # start both streams of hb#1 + om daemon hb start 1 + + # start the sender of hb#1 and the receiver of hb#2 on every node + om daemon hb start 1.tx 2.rx --node '*' # the id "om daemon hb status" shows is accepted as it reads om daemon hb start hb#1.rx`), - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { - options.Name = args[0] + options.Names = args return options.Run() }, } @@ -44,8 +47,9 @@ func NewCmdDaemonHeartbeatStart() *cobra.Command { } func (t *CmdDaemonHeartbeatStart) Run() error { - fn := func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { - return c.PostDaemonHeartbeatStart(ctx, nodename, t.Name) - } - return t.CmdDaemonSubAction.Run(fn) + return t.CmdDaemonSubAction.RunForEach(t.Names, func(name string) apiFuncWithNode { + return func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { + return c.PostDaemonHeartbeatStart(ctx, nodename, name) + } + }) } diff --git a/core/commoncmd/daemon_hb_stop.go b/core/commoncmd/daemon_hb_stop.go index 0a9eee0f2..eb8d4324e 100644 --- a/core/commoncmd/daemon_hb_stop.go +++ b/core/commoncmd/daemon_hb_stop.go @@ -12,26 +12,29 @@ import ( type ( CmdDaemonHeartbeatStop struct { CmdDaemonSubAction - Name string + Names []string } ) func NewCmdDaemonHeartbeatStop() *cobra.Command { options := CmdDaemonHeartbeatStop{} cmd := &cobra.Command{ - Use: "stop NAME", - Short: "stop a daemon heartbeat rx or tx", - Long: ForProgram("Stop one direction of a configured heartbeat.\n\n" + + Use: "stop NAME...", + Short: "stop daemon heartbeat rx or tx streams", + Long: ForProgram("Stop the named directions of the configured heartbeats.\n\n" + HeartbeatStreamNameHelp), Example: ForProgram(` # stop the receiver of hb#1 on the local node om daemon hb stop 1.rx - # stop the sender of hb#1 on every node - om daemon hb stop 1.tx --node '*'`), - Args: cobra.ExactArgs(1), + # stop both streams of hb#2 + om daemon hb stop hb#2 + + # stop the sender of hb#2 and the receiver of hb#1 on every node + om daemon hb stop hb#2.tx 1.rx --node '*'`), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatStreamNames, RunE: func(cmd *cobra.Command, args []string) error { - options.Name = args[0] + options.Names = args return options.Run() }, } @@ -41,8 +44,9 @@ func NewCmdDaemonHeartbeatStop() *cobra.Command { } func (t *CmdDaemonHeartbeatStop) Run() error { - fn := func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { - return c.PostDaemonHeartbeatStop(ctx, nodename, t.Name) - } - return t.CmdDaemonSubAction.Run(fn) + return t.CmdDaemonSubAction.RunForEach(t.Names, func(name string) apiFuncWithNode { + return func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { + return c.PostDaemonHeartbeatStop(ctx, nodename, name) + } + }) } diff --git a/core/commoncmd/daemon_hb_wipe.go b/core/commoncmd/daemon_hb_wipe.go index a09b0be63..bc204c280 100644 --- a/core/commoncmd/daemon_hb_wipe.go +++ b/core/commoncmd/daemon_hb_wipe.go @@ -12,26 +12,26 @@ import ( type ( CmdDaemonHeartbeatWipe struct { CmdDaemonSubAction - Name string + Names []string } ) func NewCmdHeartbeatWipe() *cobra.Command { options := CmdDaemonHeartbeatWipe{} cmd := &cobra.Command{ - Use: "wipe NAME", + Use: "wipe NAME...", Short: "wipe a heartbeat disk", Long: ForProgram("Remove the signature the nodes of a disk heartbeat claim their slot with.\n\n" + HeartbeatNameHelp + "\n\nThe heartbeat must be of type disk: this action writes to its dev."), Example: ForProgram(` # wipe the disk of hb#2 on the local node om daemon hb wipe 2 - # wipe it on every node - om daemon hb wipe 2 --node '*'`), - Args: cobra.ExactArgs(1), + # wipe the disks of hb#2 and hb#3 on every node + om daemon hb wipe 2 3 --node '*'`), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatNames, RunE: func(cmd *cobra.Command, args []string) error { - options.Name = args[0] + options.Names = args return options.Run() }, } @@ -41,8 +41,9 @@ func NewCmdHeartbeatWipe() *cobra.Command { } func (t *CmdDaemonHeartbeatWipe) Run() error { - fn := func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { - return c.PostDaemonHeartbeatWipe(ctx, nodename, t.Name) - } - return t.CmdDaemonSubAction.Run(fn) + return t.CmdDaemonSubAction.RunForEach(t.Names, func(name string) apiFuncWithNode { + return func(ctx context.Context, c *client.T, nodename string) (response *http.Response, err error) { + return c.PostDaemonHeartbeatWipe(ctx, nodename, name) + } + }) } diff --git a/core/commoncmd/daemon_sub_action.go b/core/commoncmd/daemon_sub_action.go index d364c7b45..08f33510d 100644 --- a/core/commoncmd/daemon_sub_action.go +++ b/core/commoncmd/daemon_sub_action.go @@ -33,6 +33,17 @@ type ( // answers are a problem document or a small json object. const maxSubActionBodySize = 1 << 20 +// RunForEach runs the action once per name, so a command taking several +// names attempts them all rather than stopping on the first that fails, +// and reports every failure. +func (t *CmdDaemonSubAction) RunForEach(names []string, fn func(name string) apiFuncWithNode) error { + var errs error + for _, name := range names { + errs = errors.Join(errs, t.Run(fn(name))) + } + return errs +} + // Run daemon sub-component action func (t *CmdDaemonSubAction) Run(fn apiFuncWithNode) error { if t.NodeSelector == "" { diff --git a/core/commoncmd/daemon_subsystems.go b/core/commoncmd/daemon_subsystems.go index 5c98d6004..7e584d864 100644 --- a/core/commoncmd/daemon_subsystems.go +++ b/core/commoncmd/daemon_subsystems.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strings" "github.com/spf13/cobra" @@ -107,7 +108,9 @@ func filterPrefix(candidates []string, toComplete string) []string { const HeartbeatNameHelp = `NAME is a heartbeat: the index of a hb# section of the cluster configuration, "1" for "hb#1". The "hb#" prefix the ID column of "om daemon hb status" shows is accepted too. A heartbeat the node does -not configure is refused.` +not configure is refused. + +Several names may be given, and the action is attempted on each.` // HeartbeatStreamNameHelp is the NAME paragraph of the actions addressing // one direction of a heartbeat, which is a component of its own. @@ -115,7 +118,11 @@ const HeartbeatStreamNameHelp = `NAME is a heartbeat stream: the index of a hb#< cluster configuration, suffixed with .rx for the receiver or .tx for the sender, "1.rx" for the receiver of "hb#1". The "hb#" prefix the ID column of "om daemon hb status" shows is accepted too. A stream the node does -not configure is refused.` +not configure is refused. + +A heartbeat named without a suffix, "1", addresses both of its streams. + +Several names may be given, and the action is attempted on each.` // ListenerNameHelp is the NAME paragraph of the listener lifecycle actions, // naming the listeners they may be addressed to. There are only two @@ -133,14 +140,23 @@ var ListenerNameHelp = `NAME is: of its own: it lives as long as the daemon does, and the request asking for it travels through it. Restart the daemon to restart it.` -// HeartbeatNames returns the heartbeats the local cluster configuration -// defines, named as the actions take them: "1" for the "hb#1" section. -func HeartbeatNames() []string { +// heartbeatSectionNames returns the heartbeat sections of the local cluster +// configuration, as "hb#1". +// +// It is a variable so a test can pin the names a host has no configuration +// to hold. +var heartbeatSectionNames = func() []string { n, err := clusterhb.New() if err != nil { return nil } - sections := n.HbNames() + return n.HbNames() +} + +// HeartbeatNames returns the heartbeats the local cluster configuration +// defines, named as the actions take them: "1" for the "hb#1" section. +func HeartbeatNames() []string { + sections := heartbeatSectionNames() l := make([]string, 0, len(sections)) for _, section := range sections { l = append(l, strings.TrimPrefix(section, "hb#")) @@ -148,32 +164,40 @@ func HeartbeatNames() []string { return l } -// HeartbeatStreamNames returns the two streams of each heartbeat the local -// cluster configuration defines. +// HeartbeatStreamNames returns the names a stream action may be addressed +// to: each heartbeat the local cluster configuration defines, which stands +// for both of its streams, and each of those streams. func HeartbeatStreamNames() []string { names := HeartbeatNames() - l := make([]string, 0, len(names)*2) + l := make([]string, 0, len(names)*3) for _, name := range names { - l = append(l, name+".rx", name+".tx") + l = append(l, name, name+".rx", name+".tx") } return l } -// validHeartbeatNames completes the heartbeats an action may address. +// validHeartbeatNames completes the heartbeats an action may address. The +// actions take several names, so it keeps completing after the first, less +// the ones already on the line. func validHeartbeatNames(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return filterPrefix(HeartbeatNames(), toComplete), cobra.ShellCompDirectiveNoFileComp + return filterPrefix(without(HeartbeatNames(), args), toComplete), cobra.ShellCompDirectiveNoFileComp } // validHeartbeatStreamNames completes the heartbeat streams an action may -// address. +// address, and the heartbeats standing for both of theirs. func validHeartbeatStreamNames(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp + return filterPrefix(without(HeartbeatStreamNames(), args), toComplete), cobra.ShellCompDirectiveNoFileComp +} + +// without returns the candidates the command line does not already hold. +func without(candidates, taken []string) []string { + l := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + if !slices.Contains(taken, candidate) { + l = append(l, candidate) + } } - return filterPrefix(HeartbeatStreamNames(), toComplete), cobra.ShellCompDirectiveNoFileComp + return l } // omWord matches the name of the om command where a help text names it, diff --git a/core/commoncmd/daemon_subsystems_test.go b/core/commoncmd/daemon_subsystems_test.go index 696d67e8b..1e7858095 100644 --- a/core/commoncmd/daemon_subsystems_test.go +++ b/core/commoncmd/daemon_subsystems_test.go @@ -84,3 +84,21 @@ func TestForProgram(t *testing.T) { assert.Contains(t, got, "from the ox command") }) } + +// TestHeartbeatStreamNamesHoldTheHeartbeats pins that the completion of a +// stream action offers the heartbeats too, since naming one addresses both +// of its streams. +func TestHeartbeatStreamNamesHoldTheHeartbeats(t *testing.T) { + saved := heartbeatSectionNames + heartbeatSectionNames = func() []string { return []string{"hb#1", "hb#11"} } + t.Cleanup(func() { heartbeatSectionNames = saved }) + + assert.Equal(t, []string{"1", "11"}, HeartbeatNames()) + assert.Equal(t, []string{"1", "1.rx", "1.tx", "11", "11.rx", "11.tx"}, HeartbeatStreamNames()) +} + +func TestWithout(t *testing.T) { + assert.Equal(t, []string{"1.rx"}, without([]string{"1.rx", "1.tx"}, []string{"1.tx"})) + assert.Equal(t, []string{"1.rx", "1.tx"}, without([]string{"1.rx", "1.tx"}, nil)) + assert.Empty(t, without([]string{"1.rx"}, []string{"1.rx"})) +} diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index a04738036..daf9f54a7 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -5272,8 +5272,9 @@ components: A stream action (start, stop, restart) takes a stream: the index of a 'hb#' section of the cluster configuration suffixed with '.rx' for the receiver or '.tx' for the sender, '1.rx' for the receiver of - 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for - 'hb#1'. + 'hb#1'. A heartbeat named without a suffix, '1', addresses both of + its streams. A disk action (sign, wipe) takes the heartbeat itself, + '1' for 'hb#1'. The 'hb#' prefix a heartbeat status shows in a stream id is accepted in both, so a name read there can be sent back. A name the node does diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 2dc125951..5c16286c5 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -6758,264 +6758,264 @@ var swaggerSpec = []string{ "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", - "51Drw6dPnz79nwAAAP//", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index c88d6162d..8f7735947 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -1071,8 +1071,9 @@ type DRBDConfig struct { // A stream action (start, stop, restart) takes a stream: the index of a // 'hb#' section of the cluster configuration suffixed with '.rx' // for the receiver or '.tx' for the sender, '1.rx' for the receiver of -// 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for -// 'hb#1'. +// 'hb#1'. A heartbeat named without a suffix, '1', addresses both of +// its streams. A disk action (sign, wipe) takes the heartbeat itself, +// '1' for 'hb#1'. // // The 'hb#' prefix a heartbeat status shows in a stream id is accepted // in both, so a name read there can be sent back. A name the node does @@ -2396,8 +2397,9 @@ type Roles = []Role // A stream action (start, stop, restart) takes a stream: the index of a // 'hb#' section of the cluster configuration suffixed with '.rx' // for the receiver or '.tx' for the sender, '1.rx' for the receiver of -// 'hb#1'. A disk action (sign, wipe) takes the heartbeat itself, '1' for -// 'hb#1'. +// 'hb#1'. A heartbeat named without a suffix, '1', addresses both of +// its streams. A disk action (sign, wipe) takes the heartbeat itself, +// '1' for 'hb#1'. // // The 'hb#' prefix a heartbeat status shows in a stream id is accepted // in both, so a name read there can be sent back. A name the node does diff --git a/daemon/daemonapi/lib_heartbeat.go b/daemon/daemonapi/lib_heartbeat.go index 2b96d2afd..23c568670 100644 --- a/daemon/daemonapi/lib_heartbeat.go +++ b/daemon/daemonapi/lib_heartbeat.go @@ -40,29 +40,41 @@ func heartbeatName(name api.InPathHeartbeatName) (string, error) { if slices.Contains(names, section) { return section, nil } - return "", fmt.Errorf("unknown heartbeat: %s%s", name, expectedHeartbeatNames(names, false)) + return "", fmt.Errorf("unknown heartbeat: %s%s", name, expectedHeartbeatNames(names)) } -// heartbeatStreamName returns the component name of the heartbeat stream an -// action addresses, "hb#1.rx" for "1.rx". +// heartbeatStreamNames returns the component names of the heartbeat streams an +// action addresses: the one a stream name designates, "hb#1.rx" for "1.rx", or +// both of the ones a heartbeat name does, "hb#1.rx" and "hb#1.tx" for "1". +// +// The two streams of a heartbeat run and stop on their own, which is why they +// are addressed on their own. They are one thing to a caller naming the +// heartbeat, who had to name both to say so. // // The action is published as a message the heartbeat janitor subscribes to by // component name, so a name no stream answers to is accepted, queued and acted // on by nobody: "1.rxx" was a no-op the caller was told nothing about. -func heartbeatStreamName(name api.InPathHeartbeatName) (string, error) { +func heartbeatStreamNames(name api.InPathHeartbeatName) ([]string, error) { names, err := configuredHeartbeatNames() if err != nil { - return "", err + return nil, err } stream := sectionOfHeartbeatName(name) for _, section := range names { + if stream == section { + l := make([]string, 0, len(heartbeatStreamSuffixes)) + for _, suffix := range heartbeatStreamSuffixes { + l = append(l, section+"."+suffix) + } + return l, nil + } for _, suffix := range heartbeatStreamSuffixes { if stream == section+"."+suffix { - return stream, nil + return []string{stream}, nil } } } - return "", fmt.Errorf("unknown heartbeat stream: %s%s", name, expectedHeartbeatNames(names, true)) + return nil, fmt.Errorf("unknown heartbeat stream: %s%s", name, expectedHeartbeatStreamNames(names)) } // sectionOfHeartbeatName returns the name as the configuration spells it, @@ -71,22 +83,44 @@ func sectionOfHeartbeatName(name api.InPathHeartbeatName) string { return "hb#" + strings.TrimPrefix(string(name), "hb#") } -// expectedHeartbeatNames renders the tail of the error a refused name is -// reported with, naming what the node would have accepted. -func expectedHeartbeatNames(names []string, streams bool) string { - l := make([]string, 0, len(names)*2) - for _, section := range names { - short := strings.TrimPrefix(section, "hb#") - if !streams { - l = append(l, short) - continue - } +// expectedHeartbeatNames renders the tail of the error a refused heartbeat +// name is reported with, naming the heartbeats the node would have accepted. +func expectedHeartbeatNames(names []string) string { + l := shortHeartbeatNames(names) + if len(l) == 0 { + return noHeartbeatConfigured + } + return ", expected one of " + strings.Join(l, ", ") +} + +// expectedHeartbeatStreamNames renders the same tail for a refused stream +// name, naming the streams and then the heartbeats, which stand for both of +// their streams. +func expectedHeartbeatStreamNames(names []string) string { + short := shortHeartbeatNames(names) + if len(short) == 0 { + return noHeartbeatConfigured + } + l := make([]string, 0, len(short)*len(heartbeatStreamSuffixes)) + for _, name := range short { for _, suffix := range heartbeatStreamSuffixes { - l = append(l, short+"."+suffix) + l = append(l, name+"."+suffix) } } - if len(l) == 0 { - return ", and this node configures no heartbeat" + return ", expected one of " + strings.Join(l, ", ") + + ", or " + strings.Join(short, ", ") + " for both streams of a heartbeat" +} + +// noHeartbeatConfigured is the tail of the error a name is refused with when +// no name at all would have been accepted. +const noHeartbeatConfigured = ", and this node configures no heartbeat" + +// shortHeartbeatNames renders the section names as the api takes them, "1" +// for "hb#1". +func shortHeartbeatNames(names []string) []string { + l := make([]string, 0, len(names)) + for _, section := range names { + l = append(l, strings.TrimPrefix(section, "hb#")) } - return ", expected one of " + strings.Join(l, ", ") + return l } diff --git a/daemon/daemonapi/lib_heartbeat_test.go b/daemon/daemonapi/lib_heartbeat_test.go index fd0ed3008..c4ca8f0db 100644 --- a/daemon/daemonapi/lib_heartbeat_test.go +++ b/daemon/daemonapi/lib_heartbeat_test.go @@ -19,35 +19,41 @@ func withConfiguredHeartbeats(t *testing.T, names ...string) { t.Cleanup(func() { configuredHeartbeatNames = saved }) } -func TestHeartbeatStreamName(t *testing.T) { +func TestHeartbeatStreamNames(t *testing.T) { withConfiguredHeartbeats(t, "hb#1", "hb#11") for _, tc := range []struct { name string - want string + want []string }{ - {"1.rx", "hb#1.rx"}, - {"1.tx", "hb#1.tx"}, - {"11.rx", "hb#11.rx"}, + {"1.rx", []string{"hb#1.rx"}}, + {"1.tx", []string{"hb#1.tx"}}, + {"11.rx", []string{"hb#11.rx"}}, // The stream id "om daemon hb status" shows carries the prefix, so // a name read there is typed back as it was read. - {"hb#1.rx", "hb#1.rx"}, + {"hb#1.rx", []string{"hb#1.rx"}}, + + // A heartbeat is both of its streams: naming it is naming them, + // which the caller had to spell out. + {"1", []string{"hb#1.rx", "hb#1.tx"}}, + {"hb#11", []string{"hb#11.rx", "hb#11.tx"}}, } { - got, err := heartbeatStreamName(api.InPathHeartbeatName(tc.name)) + got, err := heartbeatStreamNames(api.InPathHeartbeatName(tc.name)) require.NoErrorf(t, err, "%s must be accepted", tc.name) assert.Equal(t, tc.want, got) } } -func TestHeartbeatStreamNameRefusesWhatNoStreamAnswersTo(t *testing.T) { +func TestHeartbeatStreamNamesRefusesWhatNoStreamAnswersTo(t *testing.T) { withConfiguredHeartbeats(t, "hb#1", "hb#11") // "1.rxx" is the one to refuse loudest: it was accepted, queued, and - // acted on by nobody. "1" is the heartbeat, not a stream of it, and - // "2.rx" is a stream of a heartbeat this node does not configure. - for _, name := range []string{"", "1", "hb#1", "1.rxx", "1.RX", "2.rx", "1.rx.tx"} { - _, err := heartbeatStreamName(api.InPathHeartbeatName(name)) + // acted on by nobody. "2.rx" is a stream of a heartbeat this node does + // not configure, and "2" a heartbeat it does not configure at all. + for _, name := range []string{"", "1.rxx", "1.RX", "2.rx", "2", "1.rx.tx"} { + _, err := heartbeatStreamNames(api.InPathHeartbeatName(name)) require.Errorf(t, err, "%s must not be accepted", name) assert.Containsf(t, err.Error(), "1.rx, 1.tx, 11.rx, 11.tx", "the error must name the streams that exist, got %s", err) + assert.Containsf(t, err.Error(), "or 1, 11 for both streams", "the error must say a heartbeat names both, got %s", err) } } @@ -76,7 +82,7 @@ func TestHeartbeatNameOnANodeWithNoHeartbeat(t *testing.T) { withConfiguredHeartbeats(t) for _, err := range []error{ second(heartbeatName(api.InPathHeartbeatName("1"))), - second(heartbeatStreamName(api.InPathHeartbeatName("1.rx"))), + secondOfSlice(heartbeatStreamNames(api.InPathHeartbeatName("1.rx"))), } { require.Error(t, err) assert.Contains(t, err.Error(), "configures no heartbeat") @@ -86,3 +92,7 @@ func TestHeartbeatNameOnANodeWithNoHeartbeat(t *testing.T) { func second(_ string, err error) error { return err } + +func secondOfSlice(_ []string, err error) error { + return err +} diff --git a/daemon/daemonapi/lib_post_daemon_sub_action.go b/daemon/daemonapi/lib_post_daemon_sub_action.go index 579f2ae4c..cd1c36eeb 100644 --- a/daemon/daemonapi/lib_post_daemon_sub_action.go +++ b/daemon/daemonapi/lib_post_daemon_sub_action.go @@ -2,6 +2,7 @@ package daemonapi import ( "net/http" + "strings" "github.com/labstack/echo/v4" @@ -11,10 +12,21 @@ import ( "github.com/opensvc/om3/v3/util/pubsub" ) -func (a *DaemonAPI) postDaemonSubAction(ctx echo.Context, nodename api.InPathNodeName, action, localName string, fn func(c *client.T) (*http.Response, error)) error { - if len(localName) == 0 { +// postDaemonSubAction publishes the action for each of the components the +// request names. +// +// A name can designate more than one component: a heartbeat is both of its +// streams, which run and stop on their own but are one thing to the caller +// naming the heartbeat. +func (a *DaemonAPI) postDaemonSubAction(ctx echo.Context, nodename api.InPathNodeName, action string, localNames []string, fn func(c *client.T) (*http.Response, error)) error { + if len(localNames) == 0 { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sub component localName is empty") } + for _, localName := range localNames { + if len(localName) == 0 { + return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "sub component localName is empty") + } + } switch action { case "restart": case "start": @@ -24,9 +36,11 @@ func (a *DaemonAPI) postDaemonSubAction(ctx echo.Context, nodename api.InPathNod } if nodename == a.localhost || nodename == "localhost" { log := LogHandler(ctx, "postDaemonSubAction") - log.Infof("ask to %s component: %s", action, localName) - a.Bus.Pub(&msgbus.DaemonCtl{Component: localName, Action: action}, pubsub.Label{"id", localName}, labelOriginAPI) - return JSONProblemf(ctx, http.StatusOK, "daemon action queued", "%s %s", action, localName) + for _, localName := range localNames { + log.Infof("ask to %s component: %s", action, localName) + a.Bus.Pub(&msgbus.DaemonCtl{Component: localName, Action: action}, pubsub.Label{"id", localName}, labelOriginAPI) + } + return JSONProblemf(ctx, http.StatusOK, "daemon action queued", "%s %s", action, strings.Join(localNames, ", ")) } return a.proxy(ctx, nodename, fn) } diff --git a/daemon/daemonapi/post_daemon_hb_restart.go b/daemon/daemonapi/post_daemon_hb_restart.go index 2d23dd385..36226fe4d 100644 --- a/daemon/daemonapi/post_daemon_hb_restart.go +++ b/daemon/daemonapi/post_daemon_hb_restart.go @@ -14,11 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatRestart(ctx echo.Context, nodename api.In return err } nodename = a.parseNodename(nodename) - localName, err := heartbeatStreamName(name) + localNames, err := heartbeatStreamNames(name) if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "restart", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "restart", localNames, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatRestart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_hb_start.go b/daemon/daemonapi/post_daemon_hb_start.go index 0d0e37e95..744651fb1 100644 --- a/daemon/daemonapi/post_daemon_hb_start.go +++ b/daemon/daemonapi/post_daemon_hb_start.go @@ -14,11 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatStart(ctx echo.Context, nodename api.InPa return err } nodename = a.parseNodename(nodename) - localName, err := heartbeatStreamName(name) + localNames, err := heartbeatStreamNames(name) if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "start", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "start", localNames, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatStart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_hb_stop.go b/daemon/daemonapi/post_daemon_hb_stop.go index 172e50761..4a6738067 100644 --- a/daemon/daemonapi/post_daemon_hb_stop.go +++ b/daemon/daemonapi/post_daemon_hb_stop.go @@ -14,11 +14,11 @@ func (a *DaemonAPI) PostDaemonHeartbeatStop(ctx echo.Context, nodename api.InPat return err } nodename = a.parseNodename(nodename) - localName, err := heartbeatStreamName(name) + localNames, err := heartbeatStreamNames(name) if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "stop", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "stop", localNames, func(c *client.T) (*http.Response, error) { return c.PostDaemonHeartbeatStop(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_listener_restart.go b/daemon/daemonapi/post_daemon_listener_restart.go index 788a64b85..3c6181c9f 100644 --- a/daemon/daemonapi/post_daemon_listener_restart.go +++ b/daemon/daemonapi/post_daemon_listener_restart.go @@ -18,7 +18,7 @@ func (a *DaemonAPI) PostDaemonListenerRestart(ctx echo.Context, nodename api.InP if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "restart", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "restart", []string{localName}, func(c *client.T) (*http.Response, error) { return c.PostDaemonListenerRestart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_listener_start.go b/daemon/daemonapi/post_daemon_listener_start.go index 4f188861c..930cd8fe9 100644 --- a/daemon/daemonapi/post_daemon_listener_start.go +++ b/daemon/daemonapi/post_daemon_listener_start.go @@ -20,7 +20,7 @@ func (a *DaemonAPI) PostDaemonListenerStart(ctx echo.Context, nodename api.InPat if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "start", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "start", []string{localName}, func(c *client.T) (*http.Response, error) { return c.PostDaemonListenerStart(ctx.Request().Context(), nodename, name) }) } diff --git a/daemon/daemonapi/post_daemon_listener_stop.go b/daemon/daemonapi/post_daemon_listener_stop.go index 637589ad1..e6759d293 100644 --- a/daemon/daemonapi/post_daemon_listener_stop.go +++ b/daemon/daemonapi/post_daemon_listener_stop.go @@ -18,7 +18,7 @@ func (a *DaemonAPI) PostDaemonListenerStop(ctx echo.Context, nodename api.InPath if err != nil { return JSONProblemf(ctx, http.StatusBadRequest, "Invalid parameter", "%s", err) } - return a.postDaemonSubAction(ctx, nodename, "stop", localName, func(c *client.T) (*http.Response, error) { + return a.postDaemonSubAction(ctx, nodename, "stop", []string{localName}, func(c *client.T) (*http.Response, error) { return c.PostDaemonListenerStop(ctx.Request().Context(), nodename, name) }) } From 1fc100d5d8a0ec2f757d2b625fd89640ea157600 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 2 Sep 2026 22:37:34 +0200 Subject: [PATCH 4/7] Name a listing list, and say when a view is a status "om daemon hb status" renders a row per stream, from a tab= column spec, filtered by --hb, read back by -o json. It is a listing. So are "om daemon relay status" and "om node relay status", from the same kind of spec. The tree has some forty listings and they are named list, so three of them answered to a word the rest do not, and a reader looking for the heartbeats had no way to guess which. The word is worth keeping, for the two commands that earn it. An instance status is a resource tree, where the nesting is the subsets and the resources. A cluster status is a board, where the nodes are the columns. Rendering either as rows would lose what the layout says. Neither is a listing, and neither should become one. So the rule is what the command renders, not what it is about: a row per thing is a list, aliased ls, and a composed view whose layout carries meaning is a status. A tab= column spec is the mechanical test, so the next command does not need the judgement call. It is written in CONTRIBUTING.md, which is the copy the repository tracks. The three are renamed. The name each answered to is built from the new one with Use overridden, the aliases cleared and Hidden set, as the deprecation policy asks and as the cluster status command already did: nothing that works today stops working, "status" only leaves the help and the completion. The help that pointed a reader at "om daemon hb status" to read a stream id points at "om daemon hb ls". Left alone: the cluster board still answers to om monitor, om ccfg status, a hidden om daemon status and om monitor. One view with four spellings is the larger duplication, and its own change. --- CONTRIBUTING.md | 25 +++++++++++++++++++ ...{daemon_hb_status.go => daemon_hb_list.go} | 25 +++++++++++++------ core/commoncmd/daemon_hb_start.go | 2 +- ...n_relay_status.go => daemon_relay_list.go} | 24 +++++++++++++----- core/commoncmd/daemon_subsystems.go | 8 +++--- core/commoncmd/daemon_subsystems_test.go | 24 ++++++++++++++++-- core/om/daemon.go | 2 ++ core/om/factory.go | 20 ++++++++++++--- core/om/node.go | 1 + ...ode_relay_status.go => node_relay_list.go} | 4 +-- core/ox/daemon.go | 2 ++ core/ox/factory.go | 20 ++++++++++++--- core/ox/node.go | 1 + ...ode_relay_status.go => node_relay_list.go} | 4 +-- daemon/daemonapi/lib_heartbeat.go | 2 +- daemon/daemonapi/lib_heartbeat_test.go | 2 +- 16 files changed, 132 insertions(+), 34 deletions(-) rename core/commoncmd/{daemon_hb_status.go => daemon_hb_list.go} (82%) rename core/commoncmd/{daemon_relay_status.go => daemon_relay_list.go} (72%) rename core/omcmd/{node_relay_status.go => node_relay_list.go} (93%) rename core/oxcmd/{node_relay_status.go => node_relay_list.go} (93%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b064a9a2..db768969b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,28 @@ And do not terminate with a dot. ## Do not capitalize pflag description string And do not terminate with a dot. + +# Commands + +## Name a command that shows things after what it renders + +`list`, aliased `ls`, when the answer is a row per thing: a flat table whose +columns are selectable, that `-o json`, `-o tab` and `-o flat` reshape, and +that a script reads. Most of the tree is this. + +`status` when the answer is a composed view of one subject, whose layout +carries meaning that rows cannot: the resource tree of `om instance +status`, the node-column board of `om ccfg status`. Reformatting one of those +as a table would lose information. + +The test is mechanical: a command whose renderer is a `tab=` column spec is a +listing, whatever its subject. `om daemon hb status`, `om daemon relay status` +and `om node relay status` were listings wearing the other name, and are +`list` now. + +## Keep the name a renamed command answered to + +Give the new name to the command, and build the old one from it with `Use` +overridden, the aliases cleared and `Hidden: true`. It stays out of the help +and out of the completion, and the scripts that type it keep working. + diff --git a/core/commoncmd/daemon_hb_status.go b/core/commoncmd/daemon_hb_list.go similarity index 82% rename from core/commoncmd/daemon_hb_status.go rename to core/commoncmd/daemon_hb_list.go index f428b5bfc..362628338 100644 --- a/core/commoncmd/daemon_hb_status.go +++ b/core/commoncmd/daemon_hb_list.go @@ -2,7 +2,6 @@ package commoncmd import ( "encoding/json" - "fmt" "sort" "strings" @@ -17,7 +16,7 @@ import ( ) type ( - CmdDaemonHeartbeatStatus struct { + CmdDaemonHeartbeatList struct { Color string Output string NodeSelector string @@ -26,13 +25,14 @@ type ( } ) -func NewCmdDaemonHeartbeatStatus(defaultNodeSelectorFilter string) *cobra.Command { - options := CmdDaemonHeartbeatStatus{ +func NewCmdDaemonHeartbeatList(defaultNodeSelectorFilter string) *cobra.Command { + options := CmdDaemonHeartbeatList{ NodeSelector: defaultNodeSelectorFilter, } cmd := &cobra.Command{ - Use: "status", - Short: fmt.Sprintf("daemon heartbeat status"), + Use: "list", + Short: "list the heartbeat streams and their state", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { return options.Run() }, @@ -46,7 +46,7 @@ func NewCmdDaemonHeartbeatStatus(defaultNodeSelectorFilter string) *cobra.Comman return cmd } -func (t *CmdDaemonHeartbeatStatus) Run() error { +func (t *CmdDaemonHeartbeatList) Run() error { cli, err := client.New() if err != nil { return err @@ -128,3 +128,14 @@ func (t *CmdDaemonHeartbeatStatus) Run() error { return nil } + +// NewCmdDaemonHeartbeatStatus is the name the list command answered to +// before the listings were named after what they render: a row per stream. +// It is kept for the readers whose fingers and scripts type it. +func NewCmdDaemonHeartbeatStatus(defaultNodeSelectorFilter string) *cobra.Command { + cmd := NewCmdDaemonHeartbeatList(defaultNodeSelectorFilter) + cmd.Use = "status" + cmd.Aliases = nil + cmd.Hidden = true + return cmd +} diff --git a/core/commoncmd/daemon_hb_start.go b/core/commoncmd/daemon_hb_start.go index a65fd1747..81304d7de 100644 --- a/core/commoncmd/daemon_hb_start.go +++ b/core/commoncmd/daemon_hb_start.go @@ -32,7 +32,7 @@ func NewCmdDaemonHeartbeatStart() *cobra.Command { # start the sender of hb#1 and the receiver of hb#2 on every node om daemon hb start 1.tx 2.rx --node '*' - # the id "om daemon hb status" shows is accepted as it reads + # the id "om daemon hb ls" shows is accepted as it reads om daemon hb start hb#1.rx`), Args: cobra.MinimumNArgs(1), ValidArgsFunction: validHeartbeatStreamNames, diff --git a/core/commoncmd/daemon_relay_status.go b/core/commoncmd/daemon_relay_list.go similarity index 72% rename from core/commoncmd/daemon_relay_status.go rename to core/commoncmd/daemon_relay_list.go index 85f650351..74ab7498c 100644 --- a/core/commoncmd/daemon_relay_status.go +++ b/core/commoncmd/daemon_relay_list.go @@ -13,17 +13,18 @@ import ( ) type ( - CmdDaemonRelayStatus struct { + CmdDaemonRelayList struct { Color string Output string } ) -func NewCmdDaemonRelayStatus() *cobra.Command { - var options CmdDaemonRelayStatus +func NewCmdDaemonRelayList() *cobra.Command { + var options CmdDaemonRelayList cmd := &cobra.Command{ - Use: "status", - Short: "show the local daemon relay clients and last data update time", + Use: "list", + Short: "list the local daemon relay clients and their last data update time", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { return options.Run() }, @@ -34,7 +35,7 @@ func NewCmdDaemonRelayStatus() *cobra.Command { return cmd } -func (t *CmdDaemonRelayStatus) Run() error { +func (t *CmdDaemonRelayList) Run() error { cli, err := client.New() if err != nil { return err @@ -64,3 +65,14 @@ func (t *CmdDaemonRelayStatus) Run() error { }.Print() return nil } + +// NewCmdDaemonRelayStatus is the name the list command answered to before +// the listings were named after what they render: a row per relay client. +// It is kept for the readers whose fingers and scripts type it. +func NewCmdDaemonRelayStatus() *cobra.Command { + cmd := NewCmdDaemonRelayList() + cmd.Use = "status" + cmd.Aliases = nil + cmd.Hidden = true + return cmd +} diff --git a/core/commoncmd/daemon_subsystems.go b/core/commoncmd/daemon_subsystems.go index 7e584d864..fe9b822bb 100644 --- a/core/commoncmd/daemon_subsystems.go +++ b/core/commoncmd/daemon_subsystems.go @@ -107,8 +107,8 @@ func filterPrefix(candidates []string, toComplete string) []string { // heartbeat as a whole, which all take the same argument. const HeartbeatNameHelp = `NAME is a heartbeat: the index of a hb# section of the cluster configuration, "1" for "hb#1". The "hb#" prefix the ID column of -"om daemon hb status" shows is accepted too. A heartbeat the node does -not configure is refused. +"om daemon hb ls" shows is accepted too. A heartbeat the node does not +configure is refused. Several names may be given, and the action is attempted on each.` @@ -117,8 +117,8 @@ Several names may be given, and the action is attempted on each.` const HeartbeatStreamNameHelp = `NAME is a heartbeat stream: the index of a hb# section of the cluster configuration, suffixed with .rx for the receiver or .tx for the sender, "1.rx" for the receiver of "hb#1". The "hb#" prefix the ID column -of "om daemon hb status" shows is accepted too. A stream the node does -not configure is refused. +of "om daemon hb ls" shows is accepted too. A stream the node does not +configure is refused. A heartbeat named without a suffix, "1", addresses both of its streams. diff --git a/core/commoncmd/daemon_subsystems_test.go b/core/commoncmd/daemon_subsystems_test.go index 1e7858095..a45b6cbb1 100644 --- a/core/commoncmd/daemon_subsystems_test.go +++ b/core/commoncmd/daemon_subsystems_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/opensvc/om3/v3/daemon/daemonenv" @@ -67,7 +68,7 @@ func TestForProgram(t *testing.T) { const help = ` # start it om daemon hb start 1.rx - the id "om daemon hb status" shows, from the om command` + the id "om daemon hb ls" shows, from the om command` t.Run("om reads its own name", func(t *testing.T) { withProgram(t, "/usr/bin/om") @@ -78,7 +79,7 @@ func TestForProgram(t *testing.T) { got := ForProgram(help) assert.NotContains(t, strings.Fields(got), "om", "an ox help must not tell its reader to type om") assert.Contains(t, got, "ox daemon hb start 1.rx") - assert.Contains(t, got, `"ox daemon hb status"`) + assert.Contains(t, got, `"ox daemon hb ls"`) // A word ending with the command name is not the command name. assert.Contains(t, got, "from the ox command") @@ -102,3 +103,22 @@ func TestWithout(t *testing.T) { assert.Equal(t, []string{"1.rx", "1.tx"}, without([]string{"1.rx", "1.tx"}, nil)) assert.Empty(t, without([]string{"1.rx"}, []string{"1.rx"})) } + +// TestListingsAreNamedList pins the rule CONTRIBUTING.md states: a command whose +// answer is a row per thing is named list, aliased ls, and the name it +// answered to before stays, hidden. +func TestListingsAreNamedList(t *testing.T) { + for name, cmds := range map[string][2]*cobra.Command{ + "daemon hb": {NewCmdDaemonHeartbeatList(""), NewCmdDaemonHeartbeatStatus("")}, + "daemon relay": {NewCmdDaemonRelayList(), NewCmdDaemonRelayStatus()}, + } { + list, legacy := cmds[0], cmds[1] + assert.Equalf(t, "list", list.Use, "%s renders a row per thing", name) + assert.Containsf(t, list.Aliases, "ls", "%s list must answer to ls", name) + assert.Falsef(t, list.Hidden, "%s list must be advertised", name) + + assert.Equalf(t, "status", legacy.Use, "%s must still answer to its former name", name) + assert.Truef(t, legacy.Hidden, "%s status must not be advertised", name) + assert.Emptyf(t, legacy.Aliases, "%s status must not carry the aliases of the name that replaced it", name) + } +} diff --git a/core/om/daemon.go b/core/om/daemon.go index 9581ff62b..6a985b1aa 100644 --- a/core/om/daemon.go +++ b/core/om/daemon.go @@ -47,6 +47,7 @@ func init() { ) cmdDaemonHeartbeat.AddCommand( + commoncmd.NewCmdDaemonHeartbeatList(hostname.Hostname()), commoncmd.NewCmdDaemonHeartbeatStatus(hostname.Hostname()), commoncmd.NewCmdDaemonHeartbeatRestart(), commoncmd.NewCmdDaemonHeartbeatStart(), @@ -63,6 +64,7 @@ func init() { ) cmdDaemonRelay.AddCommand( + commoncmd.NewCmdDaemonRelayList(), commoncmd.NewCmdDaemonRelayStatus(), ) } diff --git a/core/om/factory.go b/core/om/factory.go index 65f4db06e..0354cbb3b 100644 --- a/core/om/factory.go +++ b/core/om/factory.go @@ -1224,11 +1224,12 @@ func newCmdNodeRegister() *cobra.Command { return cmd } -func newCmdNodeRelayStatus() *cobra.Command { - var options commands.CmdNodeRelayStatus +func newCmdNodeRelayList() *cobra.Command { + var options commands.CmdNodeRelayList cmd := &cobra.Command{ - Use: "status", - Short: "show the clients and last data update time of the configured relays", + Use: "list", + Short: "list the clients of the configured relays and their last data update time", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { return options.Run() }, @@ -1239,6 +1240,17 @@ func newCmdNodeRelayStatus() *cobra.Command { return cmd } +// newCmdNodeRelayStatus is the name the list command answered to before the +// listings were named after what they render: a row per relay client. It is +// kept for the readers whose fingers and scripts type it. +func newCmdNodeRelayStatus() *cobra.Command { + cmd := newCmdNodeRelayList() + cmd.Use = "status" + cmd.Aliases = nil + cmd.Hidden = true + return cmd +} + func newCmdNodeConfigUpdate() *cobra.Command { var options commands.CmdNodeConfigUpdate cmd := commoncmd.NewCmdAnyConfigUpdate() diff --git a/core/om/node.go b/core/om/node.go index 24bd650cb..febd875f8 100644 --- a/core/om/node.go +++ b/core/om/node.go @@ -217,6 +217,7 @@ func init() { newCmdNodePushPkg(), ) cmdNodeRelay.AddCommand( + newCmdNodeRelayList(), newCmdNodeRelayStatus(), ) cmdNodeScan.AddCommand( diff --git a/core/omcmd/node_relay_status.go b/core/omcmd/node_relay_list.go similarity index 93% rename from core/omcmd/node_relay_status.go rename to core/omcmd/node_relay_list.go index 966a38e9a..98f8b0af7 100644 --- a/core/omcmd/node_relay_status.go +++ b/core/omcmd/node_relay_list.go @@ -13,13 +13,13 @@ import ( ) type ( - CmdNodeRelayStatus struct { + CmdNodeRelayList struct { OptsGlobal Relays string } ) -func (t *CmdNodeRelayStatus) Run() error { +func (t *CmdNodeRelayList) Run() error { cli, err := client.New() if err != nil { return err diff --git a/core/ox/daemon.go b/core/ox/daemon.go index d734e39db..22b33b4ef 100644 --- a/core/ox/daemon.go +++ b/core/ox/daemon.go @@ -43,6 +43,7 @@ func init() { commoncmd.NewCmdDaemonHeartbeatRestart(), commoncmd.NewCmdDaemonHeartbeatStart(), commoncmd.NewCmdDaemonHeartbeatStop(), + commoncmd.NewCmdDaemonHeartbeatList(""), commoncmd.NewCmdDaemonHeartbeatStatus(""), commoncmd.NewCmdHeartbeatSign(), commoncmd.NewCmdHeartbeatWipe(), @@ -56,6 +57,7 @@ func init() { ) cmdDaemonRelay.AddCommand( + commoncmd.NewCmdDaemonRelayList(), commoncmd.NewCmdDaemonRelayStatus(), ) } diff --git a/core/ox/factory.go b/core/ox/factory.go index 12df754ef..817c49404 100644 --- a/core/ox/factory.go +++ b/core/ox/factory.go @@ -1247,11 +1247,12 @@ func newCmdNodeRegister() *cobra.Command { return cmd } -func newCmdNodeRelayStatus() *cobra.Command { - var options commands.CmdNodeRelayStatus +func newCmdNodeRelayList() *cobra.Command { + var options commands.CmdNodeRelayList cmd := &cobra.Command{ - Use: "status", - Short: "show the clients and last data update time of the configured relays", + Use: "list", + Short: "list the clients of the configured relays and their last data update time", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { return options.Run() }, @@ -1262,6 +1263,17 @@ func newCmdNodeRelayStatus() *cobra.Command { return cmd } +// newCmdNodeRelayStatus is the name the list command answered to before the +// listings were named after what they render: a row per relay client. It is +// kept for the readers whose fingers and scripts type it. +func newCmdNodeRelayStatus() *cobra.Command { + cmd := newCmdNodeRelayList() + cmd.Use = "status" + cmd.Aliases = nil + cmd.Hidden = true + return cmd +} + func newCmdNodeSysreport() *cobra.Command { var options commands.CmdNodeSysreport cmd := &cobra.Command{ diff --git a/core/ox/node.go b/core/ox/node.go index 33009604d..3048b2d5e 100644 --- a/core/ox/node.go +++ b/core/ox/node.go @@ -203,6 +203,7 @@ func init() { newCmdNodeValidateConfig(), ) cmdNodeRelay.AddCommand( + newCmdNodeRelayList(), newCmdNodeRelayStatus(), ) cmdNodeSchedule.AddCommand( diff --git a/core/oxcmd/node_relay_status.go b/core/oxcmd/node_relay_list.go similarity index 93% rename from core/oxcmd/node_relay_status.go rename to core/oxcmd/node_relay_list.go index 6aac3decd..4d92f4aec 100644 --- a/core/oxcmd/node_relay_status.go +++ b/core/oxcmd/node_relay_list.go @@ -13,13 +13,13 @@ import ( ) type ( - CmdNodeRelayStatus struct { + CmdNodeRelayList struct { OptsGlobal Relays string } ) -func (t *CmdNodeRelayStatus) Run() error { +func (t *CmdNodeRelayList) Run() error { cli, err := client.New() if err != nil { return err diff --git a/daemon/daemonapi/lib_heartbeat.go b/daemon/daemonapi/lib_heartbeat.go index 23c568670..1602a8673 100644 --- a/daemon/daemonapi/lib_heartbeat.go +++ b/daemon/daemonapi/lib_heartbeat.go @@ -29,7 +29,7 @@ var configuredHeartbeatNames = func() ([]string, error) { // heartbeatName returns the configuration section of the heartbeat an action // addresses, "hb#1" for "1". // -// The "hb#" prefix "om daemon hb status" shows in a stream id is accepted too, +// The "hb#" prefix "om daemon hb ls" shows in a stream id is accepted too, // so a name read there can be typed back. func heartbeatName(name api.InPathHeartbeatName) (string, error) { names, err := configuredHeartbeatNames() diff --git a/daemon/daemonapi/lib_heartbeat_test.go b/daemon/daemonapi/lib_heartbeat_test.go index c4ca8f0db..87686b039 100644 --- a/daemon/daemonapi/lib_heartbeat_test.go +++ b/daemon/daemonapi/lib_heartbeat_test.go @@ -29,7 +29,7 @@ func TestHeartbeatStreamNames(t *testing.T) { {"1.tx", []string{"hb#1.tx"}}, {"11.rx", []string{"hb#11.rx"}}, - // The stream id "om daemon hb status" shows carries the prefix, so + // The stream id "om daemon hb ls" shows carries the prefix, so // a name read there is typed back as it was read. {"hb#1.rx", []string{"hb#1.rx"}}, From 486089a80c269c48a5be134da5763e82f2d22a8d Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 3 Sep 2026 08:43:41 +0200 Subject: [PATCH 5/7] Render a configuration in the bytes returned, not beside them "om config show --section fs#1" drew the "[fs#1]" line in italic when the section held a comment, and "om node config show" drew its "[DEFAULT]" line in italic when the file held one anywhere. The colorizer set every element with color.Set, which is the imperative form: it writes the escape sequence to the process output as a side effect and returns the color to render with. So each colored element left a bare, never reset sequence on stdout, and they all landed there ahead of the buffer the function returns: ^[[93;1m^[[36m^[[90m^[[90;3m^[[93;1m[DEFAULT]^[[0;22m nine of those, from one small config the line itself A comment leaks grey and italic. A section header opens with foreground and bold, which says nothing about italic, so the italic was still on when the header was drawn. Its terminator is a full reset, which is why only the first line was ever wrong, and why the section shown by --section is the one a reader notices. color.New builds the same color and writes nothing, which is what a function returning its rendering wants. The escape sequences are now the ones each element opens and closes for itself. One test swaps the package output for a buffer and reads it back empty, the other reads the first rendered line, with a comment inside the section, before it, and further down the file. --- core/commoncmd/config.go | 16 +++++----- core/commoncmd/config_test.go | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 core/commoncmd/config_test.go diff --git a/core/commoncmd/config.go b/core/commoncmd/config.go index 5092851dc..b55437e96 100644 --- a/core/commoncmd/config.go +++ b/core/commoncmd/config.go @@ -128,13 +128,13 @@ func ColorizeINI(b []byte) []byte { // Section header if sectionRE.MatchString(line) { - color.Set(color.FgHiYellow, color.Bold).Fprintln(out, line) + color.New(color.FgHiYellow, color.Bold).Fprintln(out, line) continue } // Comment if commentRE.MatchString(line) { - color.Set(color.FgHiBlack, color.Italic).Fprintln(out, line) + color.New(color.FgHiBlack, color.Italic).Fprintln(out, line) continue } @@ -150,13 +150,13 @@ func ColorizeINI(b []byte) []byte { // Colorize key key, scope, scopeFound := strings.Cut(key, "@") - color.Set(color.FgCyan).Fprint(out, key) + color.New(color.FgCyan).Fprint(out, key) if scopeFound { - color.Set(color.FgHiMagenta).Fprint(out, "@"+scope) + color.New(color.FgHiMagenta).Fprint(out, "@"+scope) } // Colorize delimiter - color.Set(color.FgHiBlack).Fprint(out, delim) + color.New(color.FgHiBlack).Fprint(out, delim) // Highlight references in the value referenceMatches := referenceRE.FindAllStringIndex(value, -1) @@ -168,7 +168,7 @@ func ColorizeINI(b []byte) []byte { // Write reference part in green + bold referenceText := value[match[0]:match[1]] - color.Set(color.FgGreen, color.Bold).Fprint(out, referenceText) + color.New(color.FgGreen, color.Bold).Fprint(out, referenceText) lastPos = match[1] } // Write remaining part after last reference @@ -179,7 +179,7 @@ func ColorizeINI(b []byte) []byte { } if inlineComment != "" { - color.Set(color.FgHiBlack, color.Italic).Fprint(out, inlineComment) + color.New(color.FgHiBlack, color.Italic).Fprint(out, inlineComment) } out.WriteString("\n") @@ -204,7 +204,7 @@ func ColorizeINI(b []byte) []byte { // Write reference part in green + bold referenceText := line[match[0]:match[1]] - color.Set(color.FgGreen, color.Bold).Fprint(out, referenceText) + color.New(color.FgGreen, color.Bold).Fprint(out, referenceText) lastPos = match[1] } // Write remaining part after last reference diff --git a/core/commoncmd/config_test.go b/core/commoncmd/config_test.go new file mode 100644 index 000000000..af4323add --- /dev/null +++ b/core/commoncmd/config_test.go @@ -0,0 +1,56 @@ +package commoncmd + +import ( + "bytes" + "testing" + + "github.com/fatih/color" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// colorized renders src with the colors on, and returns what ColorizeINI +// returned and what it wrote past its return, which is meant to be nothing. +func colorized(t *testing.T, src string) (rendered string, leaked string) { + t.Helper() + t.Setenv("NO_COLOR", "") + savedOutput, savedNoColor := color.Output, color.NoColor + buf := bytes.NewBuffer(nil) + color.Output, color.NoColor = buf, false + t.Cleanup(func() { color.Output, color.NoColor = savedOutput, savedNoColor }) + b := ColorizeINI([]byte(src)) + return string(b), buf.String() +} + +// TestColorizeINIWritesNothingOfItsOwn pins that the rendering is the +// returned bytes and nothing else. +// +// It used to set each color with color.Set, which writes the escape sequence +// to the process output as a side effect and returns the color to render +// with. Every colored element therefore left a bare, never reset sequence +// ahead of the whole rendering. +func TestColorizeINIWritesNothingOfItsOwn(t *testing.T) { + _, leaked := colorized(t, "[fs#1]\n# why this flag\ntype = flag # inline\nnodes = {clusternodes}\n") + assert.Empty(t, leaked, "the rendering must be the returned bytes, not a side effect") +} + +// TestColorizeINISectionHeaderCarriesNoCommentAttribute pins the symptom the +// leak showed: the italic of a comment was still on when the first line was +// drawn, so the section header of a commented section was rendered italic. +func TestColorizeINISectionHeaderCarriesNoCommentAttribute(t *testing.T) { + const italic = "\x1b[3m" + for _, src := range []string{ + "[fs#1]\n# why this flag\ntype = flag\n", + "# why this section\n[fs#1]\ntype = flag\n", + "[DEFAULT]\nnodes = *\n\n# why this section\n[fs#1]\ntype = flag\n", + } { + rendered, leaked := colorized(t, src) + require.Empty(t, leaked) + + // The first thing drawn opens with its own attributes, so nothing a + // later line sets can reach it. + first, _, _ := bytes.Cut([]byte(rendered), []byte("\n")) + assert.Truef(t, bytes.HasPrefix(first, []byte("\x1b[")), "the first line must open its own sequence, got %q", first) + assert.NotContainsf(t, string(first), italic, "the first line of %q must not be italic", src) + } +} From 939996ac99833768eea8851a7e1c6708339b19c8 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 3 Sep 2026 09:02:13 +0200 Subject: [PATCH 6/7] Draw the heartbeat icons where the output is The heartbeat table entry composed the green "O" and the red "X" a listing draws its RUNNING and BEATING columns with. It is a daemon data type: it is published, an api client reads it, and it was reaching for an ansi library to hold escape sequences in two of the keys its Unstructured method returns. Nothing but "om daemon hb ls" ever read them. The tui builds the same two cells from the same values, e.State and e.IsBeating, and paints them with the markup its own widget takes, which is what the values are for. So the listing draws them. The row it renders embeds the entry and adds the two icon columns its default output names, from the state and the beating flag the entry carries. The output is unchanged, and the daemon tree imports no color library any more. One test sweeps the states, the beating flag and the single node case and reads every value of the entry back free of escape bytes, with the keys the icons are drawn from named, so removing one fails a test rather than emptying a column. The other reads the icons the listing composes, the single node among them: it beats with no peer, and is not stale for having none. --- core/commoncmd/daemon_hb_list.go | 48 ++++++++++++++- core/commoncmd/daemon_hb_list_test.go | 86 +++++++++++++++++++++++++++ daemon/daemonsubsystem/hb.go | 29 ++------- daemon/daemonsubsystem/hb_test.go | 64 ++++++++++++++++++++ 4 files changed, 202 insertions(+), 25 deletions(-) create mode 100644 core/commoncmd/daemon_hb_list_test.go create mode 100644 daemon/daemonsubsystem/hb_test.go diff --git a/core/commoncmd/daemon_hb_list.go b/core/commoncmd/daemon_hb_list.go index 362628338..18dc0748a 100644 --- a/core/commoncmd/daemon_hb_list.go +++ b/core/commoncmd/daemon_hb_list.go @@ -5,6 +5,7 @@ import ( "sort" "strings" + "github.com/fatih/color" "github.com/spf13/cobra" "github.com/opensvc/om3/v3/core/client" @@ -25,6 +26,49 @@ type ( } ) +// heartbeatStreamRow is a row of the heartbeat listing. +// +// The state and the beating flag travel from the node as values, and the +// icons standing for them are drawn here, where the output is. The daemon +// type carrying the values holds no escape sequence: it is published, and +// the tui reads the same values to paint cells of its own. +type heartbeatStreamRow struct { + daemonsubsystem.HeartbeatStreamPeerStatusTableEntry +} + +type heartbeatStreamRows []heartbeatStreamRow + +// Unstructured returns the row the tab renderer reads: what the entry +// carries, plus the two icon columns the default output names. +func (t heartbeatStreamRow) Unstructured() map[string]any { + m := t.HeartbeatStreamPeerStatusTableEntry.Unstructured() + m["state_icon"] = heartbeatStateIcon(t.State) + m["beating_icon"] = heartbeatBeatingIcon(t.IsSingleNode || t.IsBeating) + return m +} + +// heartbeatStateIcon draws the run state of a stream. +func heartbeatStateIcon(state string) string { + switch state { + case "running": + return color.New(color.FgGreen).Sprint("O") + case "stopped", "failed": + return color.New(color.FgRed).Sprint("X") + case "warning": + return color.New(color.FgYellow).Sprint("!") + default: + return color.New(color.FgHiBlack).Sprint("?") + } +} + +// heartbeatBeatingIcon draws whether a stream is beating. +func heartbeatBeatingIcon(beating bool) string { + if beating { + return color.New(color.FgGreen).Sprint("O") + } + return color.New(color.FgRed).Sprint("X") +} + func NewCmdDaemonHeartbeatList(defaultNodeSelectorFilter string) *cobra.Command { options := CmdDaemonHeartbeatList{ NodeSelector: defaultNodeSelectorFilter, @@ -79,7 +123,7 @@ func (t *CmdDaemonHeartbeatList) Run() error { isSingleNode := len(data.Cluster.Node) == 1 - table := make(daemonsubsystem.HeartbeatStreamPeerStatusTable, 0) + table := make(heartbeatStreamRows, 0) for nodename, nodeData := range data.Cluster.Node { if nodeMap != nil { if _, ok := nodeMap[nodename]; !ok { @@ -103,7 +147,7 @@ func (t *CmdDaemonHeartbeatList) Run() error { } } } - table = append(table, e) + table = append(table, heartbeatStreamRow{e}) } } diff --git a/core/commoncmd/daemon_hb_list_test.go b/core/commoncmd/daemon_hb_list_test.go new file mode 100644 index 000000000..0b81249a7 --- /dev/null +++ b/core/commoncmd/daemon_hb_list_test.go @@ -0,0 +1,86 @@ +package commoncmd + +import ( + "strings" + "testing" + + "github.com/fatih/color" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/opensvc/om3/v3/daemon/daemonsubsystem" +) + +func heartbeatRow(t *testing.T, state string, isBeating, isSingleNode bool) map[string]any { + t.Helper() + t.Setenv("NO_COLOR", "") + savedNoColor := color.NoColor + color.NoColor = false + t.Cleanup(func() { color.NoColor = savedNoColor }) + + entry := daemonsubsystem.HeartbeatStreamPeerStatusTableEntry{ + HeartbeatStreamPeerStatus: daemonsubsystem.HeartbeatStreamPeerStatus{ + IsBeating: isBeating, + }, + IsSingleNode: isSingleNode, + } + entry.Status.State = state + return heartbeatStreamRow{entry}.Unstructured() +} + +// TestHeartbeatStreamRowDrawsTheIcons pins that the listing composes the +// two icon columns its default output names, from the values the daemon +// type carries. +func TestHeartbeatStreamRowDrawsTheIcons(t *testing.T) { + for _, tc := range []struct { + state string + isBeating bool + isSingleNode bool + stateIcon string + beatingIcon string + }{ + {"running", true, false, "O", "O"}, + {"stopped", false, false, "X", "X"}, + {"failed", false, false, "X", "X"}, + {"warning", true, false, "!", "O"}, + {"", false, false, "?", "X"}, + + // A single node has no peer to beat with, and is not stale for it. + {"running", false, true, "O", "O"}, + } { + m := heartbeatRow(t, tc.state, tc.isBeating, tc.isSingleNode) + stateIcon, ok := m["state_icon"].(string) + require.Truef(t, ok, "state %q has no state_icon", tc.state) + beatingIcon, ok := m["beating_icon"].(string) + require.Truef(t, ok, "state %q has no beating_icon", tc.state) + + assert.Equalf(t, tc.stateIcon, stripANSI(stateIcon), "state icon of %q", tc.state) + assert.Equalf(t, tc.beatingIcon, stripANSI(beatingIcon), "beating icon of %q", tc.state) + assert.Containsf(t, stateIcon, "\x1b[", "the state icon of %q must be colored", tc.state) + assert.Containsf(t, beatingIcon, "\x1b[", "the beating icon of %q must be colored", tc.state) + } +} + +// TestHeartbeatStreamRowKeepsWhatTheEntryCarries pins that the row adds to +// the entry rather than replacing it: the other columns of the default +// output are still resolved. +func TestHeartbeatStreamRowKeepsWhatTheEntryCarries(t *testing.T) { + m := heartbeatRow(t, "running", true, false) + for _, key := range []string{"id", "node", "peer", "type", "desc", "changed_at", "state", "is_beating"} { + assert.Containsf(t, m, key, "%s is named by the default output", key) + } +} + +func stripANSI(s string) string { + for { + i := strings.Index(s, "\x1b[") + if i < 0 { + return s + } + j := strings.IndexByte(s[i:], 'm') + if j < 0 { + return s + } + s = s[:i] + s[i+j+1:] + } +} diff --git a/daemon/daemonsubsystem/hb.go b/daemon/daemonsubsystem/hb.go index 05e796034..e2924e0cd 100644 --- a/daemon/daemonsubsystem/hb.go +++ b/daemon/daemonsubsystem/hb.go @@ -2,8 +2,6 @@ package daemonsubsystem import ( "time" - - "github.com/fatih/color" ) type ( @@ -81,6 +79,12 @@ type ( } ) +// Unstructured returns the entry as the tab renderer reads it, which is +// values and the words standing for them. +// +// The icons a listing draws the state and the beating flag as are not here: +// they are escape sequences, and this type is published by the daemon and +// read by the tui, which paints its own cells from these values. func (t HeartbeatStreamPeerStatusTableEntry) Unstructured() map[string]any { stateText := t.Status.State if stateText == "" { @@ -97,25 +101,6 @@ func (t HeartbeatStreamPeerStatusTableEntry) Unstructured() map[string]any { } } - var stateIcon string - switch t.Status.State { - case "running": - stateIcon = color.New(color.FgGreen).Sprint("O") - case "stopped", "failed": - stateIcon = color.New(color.FgRed).Sprint("X") - case "warning": - stateIcon = color.New(color.FgYellow).Sprint("!") - default: - stateIcon = color.New(color.FgHiBlack).Sprint("?") - } - - var beatingIcon string - if t.IsSingleNode || t.IsBeating { - beatingIcon = color.New(color.FgGreen).Sprint("O") - } else { - beatingIcon = color.New(color.FgRed).Sprint("X") - } - peer := t.Peer if peer == "" { peer = "N/A" @@ -132,7 +117,6 @@ func (t HeartbeatStreamPeerStatusTableEntry) Unstructured() map[string]any { "alerts": t.Alerts, "id": t.Status.ID, "state": stateText, - "state_icon": stateIcon, "state_text": stateText, "configured_at": t.Status.ConfiguredAt, "updated_at": t.Status.UpdatedAt, @@ -145,7 +129,6 @@ func (t HeartbeatStreamPeerStatusTableEntry) Unstructured() map[string]any { "last_beating_at": t.LastBeatingAt, "is_beating": t.IsBeating, "beating": beatingText, - "beating_icon": beatingIcon, } } diff --git a/daemon/daemonsubsystem/hb_test.go b/daemon/daemonsubsystem/hb_test.go new file mode 100644 index 000000000..f0fa9d765 --- /dev/null +++ b/daemon/daemonsubsystem/hb_test.go @@ -0,0 +1,64 @@ +package daemonsubsystem + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestHeartbeatUnstructuredHoldsNoEscapeSequence pins the layer this type +// sits in. It is published by the daemon, read by the tui and by the api +// clients, and it used to compose the green "O" and the red "X" a listing +// draws, which are escape sequences. The listing draws them now, from the +// values here. +func TestHeartbeatUnstructuredHoldsNoEscapeSequence(t *testing.T) { + for _, state := range []string{"running", "stopped", "failed", "warning", "", "unknown"} { + for _, isBeating := range []bool{true, false} { + for _, isSingleNode := range []bool{true, false} { + entry := HeartbeatStreamPeerStatusTableEntry{ + Node: "n1", + Peer: "n2", + Type: "unicast", + HeartbeatStreamPeerStatus: HeartbeatStreamPeerStatus{ + IsBeating: isBeating, + }, + IsSingleNode: isSingleNode, + } + entry.Status.State = state + for key, value := range entry.Unstructured() { + s, ok := value.(string) + if !ok { + continue + } + assert.NotContainsf(t, s, "\x1b", "%s holds an escape sequence: %q", key, s) + } + } + } + } +} + +// TestHeartbeatUnstructuredKeepsTheValuesTheListingDrawsFrom pins the keys +// the icons are drawn from, so removing one is a test failure rather than +// an empty column. +func TestHeartbeatUnstructuredKeepsTheValuesTheListingDrawsFrom(t *testing.T) { + entry := HeartbeatStreamPeerStatusTableEntry{} + entry.Status.State = "running" + entry.IsBeating = true + m := entry.Unstructured() + assert.Equal(t, "running", m["state"]) + assert.Equal(t, true, m["is_beating"]) + assert.Equal(t, "beating", m["beating"]) + + entry.IsBeating = false + assert.Equal(t, "stale", entry.Unstructured()["beating"]) +} + +// TestHeartbeatUnstructuredNamesTheUnknownState pins that an empty state +// reads as a word rather than as nothing. +func TestHeartbeatUnstructuredNamesTheUnknownState(t *testing.T) { + entry := HeartbeatStreamPeerStatusTableEntry{} + m := entry.Unstructured() + assert.Equal(t, "unknown", m["state"]) + assert.Equal(t, "N/A", m["peer"]) + assert.Equal(t, "N/A", m["desc"]) +} From 786b3e01b19381edde64b260de0aace7c8b07f7d Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 3 Sep 2026 09:13:49 +0200 Subject: [PATCH 7/7] Say where a color is built, and where it is drawn Two conventions the tree now follows, written down so they survive. color.Set writes its escape sequence to the process output as a side effect and returns the color to render with, so a function composing a string leaves a sequence on the terminal ahead of what it returns. That is what drew a commented section italic in "om config show". color.New builds the same color and writes nothing. And an escape sequence belongs to the code writing to the terminal, not to the type carrying the value: the heartbeat table entry used to hold the icons a listing draws, and it is published and read back by clients that paint their own. There is no color.Set call left to remove, so the first rule starts from a tree that keeps it. --- CONTRIBUTING.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db768969b..ca9c4195c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,3 +58,25 @@ Give the new name to the command, and build the old one from it with `Use` overridden, the aliases cleared and `Hidden: true`. It stays out of the help and out of the completion, and the scripts that type it keep working. +# Rendering + +## Do not use color.Set + +`color.Set` is the imperative form of the fatih/color api: it writes the escape +sequence to the package output, which is the process stdout, as a side effect, +and returns the color to render with. A function composing a string therefore +leaves a bare, never reset sequence on the terminal ahead of whatever it +returns, and the first line rendered afterwards inherits it. That is how a +section holding a comment came out italic in `om config show`. + +Use `color.New`. It builds the same color and writes nothing, and its `Fprint`, +`Sprint` and `SprintFunc` open and close the sequence around the text they +render. + +## Paint where the output is + +An escape sequence belongs to the code writing to the terminal, not to the type +carrying the value. A daemon type is published, read back by api clients and by +the tui, which paints cells of its own: it hands the state over, and the +renderer draws the icon standing for it. +