diff --git a/core/collector/config.go b/core/collector/config.go new file mode 100644 index 000000000..a521bda9f --- /dev/null +++ b/core/collector/config.go @@ -0,0 +1,44 @@ +package collector + +import ( + "errors" + "time" +) + +type ( + Config struct { + FeederUrl string `json:"feeder_url"` + ServerUrl string `json:"server_url"` + Timeout time.Duration `json:"timeout"` + Insecure bool `json:"insecure"` + + // Hidden fields + Password string `json:"-"` + } +) + +var ( + ErrConfig = errors.New("collector is not configured: empty configuration keyword node.dbopensvc") + ErrUnregistered = errors.New("this node is not registered. try 'om node register'") +) + +func (t *Config) Equal(o *Config) bool { + if t == nil && o != nil { + return false + } + if t != nil && o == nil { + return false + } + if t != nil && o != nil && *t != *o { + return false + } + return true +} + +func (t *Config) DeepCopy() *Config { + if t == nil { + return nil + } + n := *t + return &n +} diff --git a/core/collector/rpc.go b/core/collector/rpc.go index 72b3b66e1..28f4942ba 100644 --- a/core/collector/rpc.go +++ b/core/collector/rpc.go @@ -3,6 +3,7 @@ package collector import ( "context" "crypto/tls" + "encoding/base64" "fmt" "net/http" "net/url" @@ -14,6 +15,7 @@ import ( "github.com/ybbus/jsonrpc" "github.com/opensvc/om3/v3/util/hostname" + "github.com/opensvc/om3/v3/util/httphelper" "github.com/opensvc/om3/v3/util/plog" ) @@ -186,13 +188,24 @@ func BaseURL(s string) (*url.URL, error) { return u, nil } +func (c *Config) NewFeedRequester() (*httphelper.T, error) { + if c.FeederUrl == "" { + return nil, ErrConfig + } else if c.Password == "" { + return nil, ErrUnregistered + } + + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+c.Password)) + return NewRequester(c.FeederUrl, auth, c.Insecure) +} + // NewFeedClient returns a Client to call the collector feed app jsonrpc2 methods. -func NewFeedClient(endpoint, secret string) (*Client, error) { - u, err := FeedURL(endpoint) +func (c *Config) NewFeedClient() (*Client, error) { + u, err := FeedURL(c.FeederUrl) if err != nil { return nil, err } - return newClient(u, secret) + return newClient(u, c.Password) } // NewComplianceClient returns a Client to call the collector init app jsonrpc2 methods. diff --git a/core/commoncmd/node_events.go b/core/commoncmd/node_events.go index a665424c9..fb19ea9a6 100644 --- a/core/commoncmd/node_events.go +++ b/core/commoncmd/node_events.go @@ -17,6 +17,7 @@ import ( "github.com/opensvc/om3/v3/core/output" "github.com/opensvc/om3/v3/core/rawconfig" "github.com/opensvc/om3/v3/daemon/msgbus" + "github.com/opensvc/om3/v3/util/flatten" "github.com/opensvc/om3/v3/util/pubsub" ) @@ -36,7 +37,7 @@ type ( NodeSelector string errC chan error evC chan *event.Event - diff *output.Delta + diff *flatten.Delta } templateHelper struct { @@ -156,7 +157,7 @@ func (t *CmdNodeEvents) DoNodes() error { nodenames []string ) if t.Output == "diff" { - t.diff = output.NewDiff() + t.diff = flatten.NewDiff() } t.evC = make(chan *event.Event) diff --git a/core/commoncmd/text/node-events/flag/filter b/core/commoncmd/text/node-events/flag/filter index 3a98a7235..6de86ed2b 100644 --- a/core/commoncmd/text/node-events/flag/filter +++ b/core/commoncmd/text/node-events/flag/filter @@ -64,13 +64,14 @@ Receive only events matching the filtering expression formatted as: NodeAlive, NodeStale NodeConfigUpdated, NodeDataUpdated, NodeFrozen NodeFrozenFileRemoved, NodeFrozenFileUpdated + NodeLabelsUpdated, NodeLabelsCommited NodeMonitorDeleted, NodeMonitorUpdated NodeOsPathsUpdated NodePoolStatusUpdated, NodePoolStatusDeleted NodeRejoin, NodeSplitAction NodeStatsUpdated NodeStatusUpdated - NodeStatusArbitratorsUpdated, NodeStatusGenUpdates, NodeStatusLabelsUpdated + NodeStatusArbitratorsUpdated, NodeStatusGenUpdates, SetNodeMonitor ### Object diff --git a/core/node/config.go b/core/node/config.go index 08fa52601..387ad20c0 100644 --- a/core/node/config.go +++ b/core/node/config.go @@ -1,14 +1,24 @@ package node import ( + "encoding/json" + "maps" + "slices" "time" + "github.com/opensvc/om3/v3/core/collector" "github.com/opensvc/om3/v3/core/schedule" + "github.com/opensvc/om3/v3/util/flatten" + "github.com/opensvc/om3/v3/util/label" + "github.com/opensvc/om3/v3/util/xmap" ) type ( Config struct { + Collector *collector.Config `json:"collector,omitempty"` Env string `json:"env"` + Hooks Hooks `json:"hooks"` + Labels label.M `json:"labels"` MaintenanceGracePeriod time.Duration `json:"maintenance_grace_period"` MaxParallel int `json:"max_parallel"` MaxKeySize int64 `json:"max_key_size"` @@ -21,16 +31,27 @@ type ( SSHKey string `json:"sshkey"` PRKey string `json:"prkey"` } + + Hooks []Hook + + Hook struct { + Name string `json:"name"` + Events []string `json:"events"` + Command []string `json:"command"` + } ) func (cfg *Config) DeepCopy() *Config { newCfg := *cfg newCfg.Schedules = append([]schedule.Config{}, cfg.Schedules...) + newCfg.Labels = cfg.Labels.DeepCopy() + newCfg.Hooks = cfg.Hooks.DeepCopy() + newCfg.Collector = cfg.Collector.DeepCopy() return &newCfg } -func (c Config) Equals(other Config) bool { +func (c Config) Equal(other Config) bool { if c.Env != other.Env || c.MaintenanceGracePeriod != other.MaintenanceGracePeriod || c.MaxParallel != other.MaxParallel || @@ -45,6 +66,13 @@ func (c Config) Equals(other Config) bool { return false } + if !c.Collector.Equal(other.Collector) { + return false + } + if !maps.Equal(c.Labels, other.Labels) { + return false + } + // Compare Schedules slice if len(c.Schedules) != len(other.Schedules) { return false @@ -55,5 +83,52 @@ func (c Config) Equals(other Config) bool { } } + // Compare Hook slice + if len(c.Hooks) != len(other.Hooks) { + return false + } + for i := range c.Hooks { + if !c.Hooks[i].Equal(&other.Hooks[i]) { + return false + } + } + return true +} + +func (t Hooks) DeepCopy() Hooks { + l := make(Hooks, len(t)) + for i, hook := range t { + l[i] = *hook.DeepCopy() + } + return l +} + +func (t *Hook) Equal(o *Hook) bool { + if t.Name != o.Name { + return false + } else if !slices.Equal(t.Events, o.Events) { + return false + } else if !slices.Equal(t.Command, o.Command) { + return false + } return true } + +func (t *Hook) DeepCopy() *Hook { + n := *t + n.Events = append([]string{}, t.Events...) + n.Command = append([]string{}, t.Command...) + return &n +} + +func (t *Hook) Diff(other Hook) string { + flattenable := func(hook Hook) map[string]any { + var m map[string]any + b, _ := json.Marshal(hook) + json.Unmarshal(b, &m) + return m + } + m1 := flatten.Flatten(flattenable(*t)) + m2 := flatten.Flatten(flattenable(other)) + return xmap.Diff(m1, m2) +} diff --git a/core/node/status.go b/core/node/status.go index 34a596202..fdb2178d3 100644 --- a/core/node/status.go +++ b/core/node/status.go @@ -5,7 +5,6 @@ import ( "github.com/opensvc/om3/v3/core/instance" "github.com/opensvc/om3/v3/core/status" - "github.com/opensvc/om3/v3/util/label" ) type ( @@ -18,7 +17,6 @@ type ( Gen Gen `json:"gen"` IsLeader bool `json:"is_leader"` IsOverloaded bool `json:"is_overloaded"` - Labels label.M `json:"labels"` BootedAt time.Time `json:"booted_at"` } @@ -59,7 +57,6 @@ func (t *Status) DeepCopy() *Status { newGen[n] = v } result.Gen = newGen - result.Labels = t.Labels.DeepCopy() return &result } diff --git a/core/node/status_test.go b/core/node/status_test.go index 35200f833..6519d3cf2 100644 --- a/core/node/status_test.go +++ b/core/node/status_test.go @@ -28,10 +28,6 @@ func Test_TStatus_DeepCopy(t *testing.T) { }, IsOverloaded: false, IsLeader: true, - Labels: map[string]string{ - "node1": "abc", - "node2": "efg", - }, } copyValue := value.DeepCopy() diff --git a/core/object/node_collector.go b/core/object/node_collector.go index a9bf55d9d..26df060e8 100644 --- a/core/object/node_collector.go +++ b/core/object/node_collector.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "encoding/base64" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -28,25 +27,12 @@ type ( uuid string } - CollectorConfig struct { - FeederUrl string `json:"feeder_url"` - ServerUrl string `json:"server_url"` - Timeout time.Duration `json:"timeout"` - Insecure bool `json:"insecure"` - - // private field - password string - } - CollectorProblem struct { text string `json:"text"` } ) var ( - ErrNodeCollectorConfig = errors.New("collector is not configured: empty configuration keyword node.dbopensvc") - ErrNodeCollectorUnregistered = errors.New("this node is not registered. try 'om node register'") - defaultPostCollectorTimeout = 1 * time.Second ) @@ -104,23 +90,23 @@ func (t *CollectorConfigRaw) ServerUrl() string { } } -func (t *CollectorConfigRaw) AsConfig() *CollectorConfig { +func (t *CollectorConfigRaw) AsConfig() *collector.Config { var timeout time.Duration if t.timeout != nil { timeout = *t.timeout } - return &CollectorConfig{ + return &collector.Config{ FeederUrl: t.FeederUrl(), ServerUrl: t.ServerUrl(), Timeout: timeout, Insecure: t.insecure, - password: t.uuid, + Password: t.uuid, } } func (t *Node) CollectorFeedClient() (*collector.Client, error) { - collectorCfg := t.CollectorRawConfig().AsConfig() - return collector.NewFeedClient(collectorCfg.FeederUrl, collectorCfg.password) + cfg := t.CollectorRawConfig().AsConfig() + return cfg.NewFeedClient() } func (t Node) CollectorInitClient() (*collector.Client, error) { @@ -171,11 +157,11 @@ func (t *Node) CollectorClient() (*httphelper.T, error) { pass := t.MergedConfig().GetString(key.Parse("node.uuid")) if dbopensvc == "" || dbopensvc == "none" { - return nil, ErrNodeCollectorConfig + return nil, collector.ErrConfig } if dbopensvc != "" && pass == "" { - return nil, ErrNodeCollectorUnregistered + return nil, collector.ErrUnregistered } auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+pass)) @@ -187,12 +173,12 @@ func (t *Node) CollectorFeeder() (*httphelper.T, error) { cfg := t.CollectorRawConfig().AsConfig() if cfg.FeederUrl == "" { - return nil, ErrNodeCollectorConfig - } else if cfg.password == "" { - return nil, ErrNodeCollectorUnregistered + return nil, collector.ErrConfig + } else if cfg.Password == "" { + return nil, collector.ErrUnregistered } - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+cfg.password)) + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+cfg.Password)) return collector.NewRequester(cfg.FeederUrl, auth, cfg.Insecure) } @@ -201,12 +187,12 @@ func (t *Node) CollectorServer() (*httphelper.T, error) { cfg := t.CollectorRawConfig().AsConfig() if cfg.ServerUrl == "" { - return nil, ErrNodeCollectorConfig - } else if cfg.password == "" { - return nil, ErrNodeCollectorUnregistered + return nil, collector.ErrConfig + } else if cfg.Password == "" { + return nil, collector.ErrUnregistered } - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+cfg.password)) + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(hostname.Hostname()+":"+cfg.Password)) return collector.NewRequester(cfg.ServerUrl, auth, cfg.Insecure) } @@ -215,7 +201,7 @@ func (t *Node) CollectorServerWithAuth(auth string) (*httphelper.T, error) { cfg := t.CollectorRawConfig().AsConfig() if cfg.ServerUrl == "" { - return nil, ErrNodeCollectorConfig + return nil, collector.ErrConfig } return collector.NewRequester(cfg.ServerUrl, auth, cfg.Insecure) @@ -226,7 +212,7 @@ func (t *Node) CollectorServerWithoutAuth() (*httphelper.T, error) { cfg := t.CollectorRawConfig().AsConfig() if cfg.ServerUrl == "" { - return nil, ErrNodeCollectorConfig + return nil, collector.ErrConfig } return collector.NewRequester(cfg.ServerUrl, "", cfg.Insecure) diff --git a/core/object/node_config.go b/core/object/node_config.go index 147b4594c..879255f9a 100644 --- a/core/object/node_config.go +++ b/core/object/node_config.go @@ -176,7 +176,3 @@ func (t *Node) CNIPlugins() (string, error) { return s.(string), nil } } - -func (t *Node) Labels() map[string]string { - return t.config.SectionMap("labels") -} diff --git a/core/omcmd/node_schedule_list.go b/core/omcmd/node_schedule_list.go index 129400cc0..06f2d3fe7 100644 --- a/core/omcmd/node_schedule_list.go +++ b/core/omcmd/node_schedule_list.go @@ -67,7 +67,6 @@ func (t *CmdNodeScheduleList) extractLocal() (api.ScheduleItems, error) { RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, - StatefileKey: e.StatefileKey, }, } items = append(items, item) diff --git a/core/omcmd/object_schedule_list.go b/core/omcmd/object_schedule_list.go index bc0037bd5..e576b4761 100644 --- a/core/omcmd/object_schedule_list.go +++ b/core/omcmd/object_schedule_list.go @@ -79,7 +79,6 @@ func (t *CmdObjectScheduleList) extractLocal(selector string) (api.ScheduleList, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, - StatefileKey: e.StatefileKey, }, } data.Items = append(data.Items, item) diff --git a/core/output/renderer.go b/core/output/renderer.go index 47b112b65..942714e52 100644 --- a/core/output/renderer.go +++ b/core/output/renderer.go @@ -15,6 +15,7 @@ import ( "k8s.io/client-go/util/jsonpath" "sigs.k8s.io/yaml" + "github.com/opensvc/om3/v3/util/flatten" "github.com/opensvc/om3/v3/util/render" "github.com/opensvc/om3/v3/util/render/palette" "github.com/opensvc/om3/v3/util/unstructured" @@ -104,9 +105,9 @@ func (t Renderer) Sprint() (string, error) { panic(err) } if color.NoColor { - return sep + SprintFlat(b), nil + return sep + flatten.SprintFlat(b), nil } else { - return sep + SprintFlatColor(b, t.Colorize), nil + return sep + flatten.SprintFlatColor(b, t.Colorize), nil } case JSON: b, err := json.MarshalIndent(t.Data, "", indent) diff --git a/core/schedule/main.go b/core/schedule/main.go index ef02ccce1..fb2aee089 100644 --- a/core/schedule/main.go +++ b/core/schedule/main.go @@ -22,10 +22,11 @@ type ( Require string `json:"require,omitempty"` RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` + RunDir string `json:"-"` // StatefileKey is used in the last run filename and last run success formatters. // Defaults to Action if empty. - StatefileKey string `json:"statefile_key,omitempty"` + StatefileKey string `json:"-"` } Entry struct { @@ -34,7 +35,6 @@ type ( NextRunAt time.Time `json:"next_run_at"` Node string `json:"node"` Path naming.Path `json:"path"` - RunDir string `json:"run_dir"` } ) @@ -114,6 +114,7 @@ func (t Table) DeepCopy() *Table { Require: x.Require, RequireCollector: x.RequireCollector, RequireProvisioned: x.RequireProvisioned, + RunDir: x.RunDir, Schedule: x.Schedule, StatefileKey: x.StatefileKey, }, @@ -121,7 +122,6 @@ func (t Table) DeepCopy() *Table { NextRunAt: x.NextRunAt, Node: x.Node, Path: x.Path, - RunDir: x.RunDir, }) } return &r diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index bd8b3e1e8..41e2b65fe 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -6077,6 +6077,8 @@ components: type: object required: - env + - hooks + - labels - maintenance_grace_period - max_parallel - min_avail_mem_pct @@ -6087,8 +6089,17 @@ components: - sshkey - prkey properties: + collector: + $ref: '#/components/schemas/NodeConfigCollector' env: type: string + hooks: + type: array + items: + $ref: '#/components/schemas/NodeConfigHook' + labels: + additionalProperties: + type: string maintenance_grace_period: x-go-type: time.Duration max_parallel: @@ -6110,6 +6121,41 @@ components: type: string x-go-name: PRKey + NodeConfigCollector: + type: object + required: + - feeder_url + - insecure + - server_url + - timeout + properties: + feeder_url: + type: string + insecure: + type: boolean + server_url: + type: string + timeout: + x-go-type: time.Duration + + NodeConfigHook: + type: object + required: + - command + - events + - name + properties: + command: + type: array + items: + type: string + events: + type: array + items: + type: string + name: + type: string + NodeInfo: type: object required: @@ -6243,7 +6289,6 @@ components: - gen - is_leader - is_overloaded - - labels properties: agent: type: string @@ -6268,9 +6313,6 @@ components: type: boolean is_overloaded: type: boolean - labels: - additionalProperties: - type: string NodesInfo: x-go-type: nodesinfo.M @@ -7438,7 +7480,6 @@ components: - key - max_parallel - last_run_at - - statefile_key - next_run_at - require - require_collector @@ -7453,8 +7494,6 @@ components: last_run_at: type: string format: date-time - statefile_key: - type: string max_parallel: type: integer next_run_at: @@ -7473,7 +7512,6 @@ components: - action - schedule - key - - statefile_key - max_parallel - require - require_collector @@ -7485,8 +7523,6 @@ components: type: string key: type: string - statefile_key: - type: string max_parallel: type: integer require: diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 73685a19f..e91b4fe7c 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -6230,281 +6230,282 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "88cl8Cxpfby5ubn5PwEAAP//G2JFs459AgA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index 1f00365a5..32b9727cd 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -847,16 +847,34 @@ type NodeActionAccepted struct { // NodeConfig defines model for NodeConfig. type NodeConfig struct { - Env string `json:"env"` - MaintenanceGracePeriod time.Duration `json:"maintenance_grace_period"` - MaxParallel int `json:"max_parallel"` - MinAvailMemPct int `json:"min_avail_mem_pct"` - MinAvailSwapPct int `json:"min_avail_swap_pct"` - PRKey string `json:"prkey"` - ReadyPeriod time.Duration `json:"ready_period"` - RejoinGracePeriod time.Duration `json:"rejoin_grace_period"` - SplitAction string `json:"split_action"` - SSHKey string `json:"sshkey"` + Collector *NodeConfigCollector `json:"collector,omitempty"` + Env string `json:"env"` + Hooks []NodeConfigHook `json:"hooks"` + Labels map[string]string `json:"labels"` + MaintenanceGracePeriod time.Duration `json:"maintenance_grace_period"` + MaxParallel int `json:"max_parallel"` + MinAvailMemPct int `json:"min_avail_mem_pct"` + MinAvailSwapPct int `json:"min_avail_swap_pct"` + PRKey string `json:"prkey"` + ReadyPeriod time.Duration `json:"ready_period"` + RejoinGracePeriod time.Duration `json:"rejoin_grace_period"` + SplitAction string `json:"split_action"` + SSHKey string `json:"sshkey"` +} + +// NodeConfigCollector defines model for NodeConfigCollector. +type NodeConfigCollector struct { + FeederUrl string `json:"feeder_url"` + Insecure bool `json:"insecure"` + ServerUrl string `json:"server_url"` + Timeout time.Duration `json:"timeout"` +} + +// NodeConfigHook defines model for NodeConfigHook. +type NodeConfigHook struct { + Command []string `json:"command"` + Events []string `json:"events"` + Name string `json:"name"` } // NodeInfo defines model for NodeInfo. @@ -932,7 +950,6 @@ type NodeStatus struct { Gen map[string]uint64 `json:"gen"` IsLeader bool `json:"is_leader"` IsOverloaded bool `json:"is_overloaded"` - Labels map[string]string `json:"labels"` } // NodesInfo defines model for NodesInfo. @@ -1556,7 +1573,6 @@ type Schedule struct { RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` Schedule string `json:"schedule"` - StatefileKey string `json:"statefile_key"` } // ScheduleConfig defines model for ScheduleConfig. @@ -1568,7 +1584,6 @@ type ScheduleConfig struct { RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` Schedule string `json:"schedule"` - StatefileKey string `json:"statefile_key"` } // ScheduleItem defines model for ScheduleItem. diff --git a/daemon/api/unstructured.go b/daemon/api/unstructured.go index 271cfc4c3..2da4effbd 100644 --- a/daemon/api/unstructured.go +++ b/daemon/api/unstructured.go @@ -85,7 +85,6 @@ func (t Schedule) Unstructured() map[string]any { "require": t.Require, "require_collector": t.RequireCollector, "require_Provisioned": t.RequireProvisioned, - "statefile_key": t.StatefileKey, } } @@ -115,9 +114,23 @@ func (t Node) Unstructured() map[string]any { } } +func (t *NodeConfigHook) Unstructured() map[string]any { + return map[string]any{ + "command": t.Command, + "events": t.Events, + "name": t.Name, + } +} + func (t *NodeConfig) Unstructured() map[string]any { + hooks := make([]any, 0) + for _, hook := range t.Hooks { + hooks = append(hooks, hook.Unstructured()) + } return map[string]any{ "env": t.Env, + "hooks": hooks, + "labels": t.Labels, "maintenance_grace_period": t.MaintenanceGracePeriod, "min_avail_mem_pct": t.MinAvailMemPct, "min_avail_swap_pct": t.MinAvailSwapPct, @@ -140,7 +153,6 @@ func (t *NodeStatus) Unstructured() map[string]any { "gen": t.Gen, "is_leader": t.IsLeader, "is_overloaded": t.IsOverloaded, - "labels": t.Labels, } } diff --git a/daemon/collector/main.go b/daemon/collector/main.go index 74e140d80..0a26365b9 100644 --- a/daemon/collector/main.go +++ b/daemon/collector/main.go @@ -213,10 +213,12 @@ func New(ctx context.Context, subQS pubsub.QueueSizer, opts ...funcopt.O) *T { return t } -func (t *T) setNodeFeedClient() error { - if node, err := object.NewNode(); err != nil { - return err - } else if client, err := node.CollectorFeedClient(); err != nil { +func (t *T) setNodeFeedClient(c *collector.Config) error { + if c == nil { + t.feedClient = nil + return nil + } else if client, err := c.NewFeedClient(); err != nil { + t.feedClient = nil return err } else { t.feedClient = client @@ -225,16 +227,18 @@ func (t *T) setNodeFeedClient() error { } } -func (t *T) setupRequester() error { +func (t *T) setupRequester(c *collector.Config) error { // TODO: pickup dbopensvc, auth, insecure from config update message // to create requester from core/collector.NewRequester t.status.ConfiguredAt = time.Now() - if node, err := object.NewNode(); err != nil { + if c == nil { t.client = nil - return err - } else if cli, err := node.CollectorFeeder(); err != nil { + t.disable = true + t.status.Url = "" + return nil + } else if cli, err := c.NewFeedRequester(); err != nil { t.client = nil - if errors.Is(err, object.ErrNodeCollectorConfig) { + if errors.Is(err, collector.ErrConfig) { t.disable = true t.status.Url = "" err = nil @@ -260,9 +264,12 @@ func (t *T) Start(ctx context.Context) error { t.publisher = pubsub.PubFromContext(t.ctx) t.wg.Add(1) + + initialNodeConfig := node.ConfigData.GetByNode(t.localhost) + go func(errC chan<- error) { defer t.wg.Done() - if err := t.setNodeFeedClient(); err != nil { + if err := t.setNodeFeedClient(initialNodeConfig.Collector); err != nil { t.log.Infof("the collector routine is dormant: %s", err) } else { t.log.Infof("feeding %s", t.feedClient) @@ -270,7 +277,7 @@ func (t *T) Start(ctx context.Context) error { t.feedPinger.Start(t.ctx, FeedPingerInterval) defer t.feedPinger.Stop() } - if err := t.setupRequester(); err != nil { + if err := t.setupRequester(initialNodeConfig.Collector); err != nil { t.log.Errorf("can't setup requester: %s", err) } errC <- nil diff --git a/daemon/collector/on_events.go b/daemon/collector/on_events.go index ba3cd04e3..32f49d791 100644 --- a/daemon/collector/on_events.go +++ b/daemon/collector/on_events.go @@ -8,7 +8,6 @@ import ( "github.com/opensvc/om3/v3/core/collector" "github.com/opensvc/om3/v3/core/instance" "github.com/opensvc/om3/v3/core/naming" - "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/daemon/msgbus" ) @@ -42,32 +41,6 @@ func (t *T) onClusterConfigUpdated(c *msgbus.ClusterConfigUpdated) { } } -func (t *T) onConfigUpdated() { - t.log.Tracef("reconfigure") - if collector.Alive.Load() { - t.log.Infof("disable collector clients") - collector.Alive.Store(false) - } - err := t.setNodeFeedClient() - if t.feedPinger != nil { - t.feedPinger.Stop() - } - if err := t.setupRequester(); err != nil { - if !errors.Is(err, object.ErrNodeCollectorConfig) { - t.log.Errorf("can't setup requester: %s", err) - } - } - if err != nil { - t.log.Infof("the collector routine is dormant: %s", err) - } else { - t.log.Infof("feeding %s", t.feedClient) - t.feedPinger = t.feedClient.NewPinger() - time.Sleep(time.Microsecond * 10) - t.feedPinger.Start(t.ctx, FeedPingerInterval) - } - t.publishOnChange(t.getState()) -} - func (t *T) onInstanceConfigDeleted(c *msgbus.InstanceConfigDeleted) { if instanceConfig, ok := t.objectConfigToSend[c.Path]; ok { if instanceConfig == nil { @@ -145,7 +118,29 @@ func (t *T) onInstanceStatusUpdated(c *msgbus.InstanceStatusUpdated) { } func (t *T) onNodeConfigUpdated(c *msgbus.NodeConfigUpdated) { - t.onConfigUpdated() + t.log.Tracef("reconfigure") + if collector.Alive.Load() { + t.log.Infof("disable collector clients") + collector.Alive.Store(false) + } + err := t.setNodeFeedClient(c.Value.Collector) + if t.feedPinger != nil { + t.feedPinger.Stop() + } + if err := t.setupRequester(c.Value.Collector); err != nil { + if !errors.Is(err, collector.ErrConfig) { + t.log.Errorf("can't setup requester: %s", err) + } + } + if err != nil { + t.log.Infof("the collector routine is dormant: %s", err) + } else { + t.log.Infof("feeding %s", t.feedClient) + t.feedPinger = t.feedClient.NewPinger() + time.Sleep(time.Microsecond * 10) + t.feedPinger.Start(t.ctx, FeedPingerInterval) + } + t.publishOnChange(t.getState()) } func (t *T) onNodeMonitorDeleted(c *msgbus.NodeMonitorDeleted) { diff --git a/daemon/daemonapi/get_daemon_events.go b/daemon/daemonapi/get_daemon_events.go index e5ac3ec9b..cc45c8425 100644 --- a/daemon/daemonapi/get_daemon_events.go +++ b/daemon/daemonapi/get_daemon_events.go @@ -19,11 +19,11 @@ import ( "github.com/opensvc/om3/v3/core/naming" "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/objectselector" - "github.com/opensvc/om3/v3/core/output" "github.com/opensvc/om3/v3/daemon/api" "github.com/opensvc/om3/v3/daemon/msgbus" "github.com/opensvc/om3/v3/daemon/rbac" "github.com/opensvc/om3/v3/util/converters" + "github.com/opensvc/om3/v3/util/flatten" "github.com/opensvc/om3/v3/util/funcopt" "github.com/opensvc/om3/v3/util/pubsub" ) @@ -609,7 +609,7 @@ func (f Filter) IsZero() bool { } func (df DataFilters) match(i any) bool { - flatten := output.Flatten(i) + keys := flatten.Flatten(i) intLessOrEqual := func(str1, str2 string) (bool, error) { num1, err1 := strconv.Atoi(str1) @@ -684,7 +684,7 @@ func (df DataFilters) match(i any) bool { } matchDataFilter := func(m DataFilter) bool { - s, ok := flatten[m.Key] + s, ok := keys[m.Key] if !ok { return false } diff --git a/daemon/daemonapi/get_instance_schedule.go b/daemon/daemonapi/get_instance_schedule.go index ac8d88adc..4c6bc9ead 100644 --- a/daemon/daemonapi/get_instance_schedule.go +++ b/daemon/daemonapi/get_instance_schedule.go @@ -57,7 +57,6 @@ func (a *DaemonAPI) getLocalInstanceSchedule(ctx echo.Context, namespace string, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, - StatefileKey: e.StatefileKey, }, } resp.Items = append(resp.Items, item) diff --git a/daemon/daemonapi/get_node.go b/daemon/daemonapi/get_node.go index 757039bd3..0d1304d08 100644 --- a/daemon/daemonapi/get_node.go +++ b/daemon/daemonapi/get_node.go @@ -42,6 +42,8 @@ func (a *DaemonAPI) GetNodes(ctx echo.Context, params api.GetNodesParams) error if config.Value != nil { d.Data.Config = &api.NodeConfig{ Env: config.Value.Env, + Hooks: make([]api.NodeConfigHook, len(config.Value.Hooks)), + Labels: make(map[string]string), MaintenanceGracePeriod: config.Value.MaintenanceGracePeriod, MaxParallel: config.Value.MaxParallel, MinAvailMemPct: config.Value.MinAvailMemPct, @@ -52,6 +54,24 @@ func (a *DaemonAPI) GetNodes(ctx echo.Context, params api.GetNodesParams) error RejoinGracePeriod: config.Value.RejoinGracePeriod, SplitAction: config.Value.SplitAction, } + if c := config.Value.Collector; c != nil { + d.Data.Config.Collector = &api.NodeConfigCollector{ + FeederUrl: c.FeederUrl, + Insecure: c.Insecure, + ServerUrl: c.ServerUrl, + Timeout: c.Timeout, + } + } + for i, hook := range config.Value.Hooks { + d.Data.Config.Hooks[i] = api.NodeConfigHook{ + Name: hook.Name, + Events: hook.Events, + Command: hook.Command, + } + } + for k, v := range config.Value.Labels { + d.Data.Config.Labels[k] = v + } } if status != nil { d.Data.Status = &api.NodeStatus{ @@ -63,7 +83,6 @@ func (a *DaemonAPI) GetNodes(ctx echo.Context, params api.GetNodesParams) error Gen: make(map[string]uint64), IsLeader: status.IsLeader, IsOverloaded: status.IsOverloaded, - Labels: make(map[string]string), } for k, v := range status.Arbitrators { d.Data.Status.Arbitrators[k] = api.ArbitratorStatus{ @@ -75,9 +94,6 @@ func (a *DaemonAPI) GetNodes(ctx echo.Context, params api.GetNodesParams) error for k, v := range status.Gen { d.Data.Status.Gen[k] = v } - for k, v := range status.Labels { - d.Data.Status.Labels[k] = v - } } if monitor != nil { d.Data.Monitor = &api.NodeMonitor{ diff --git a/daemon/daemonapi/get_node_schedule.go b/daemon/daemonapi/get_node_schedule.go index a34c92136..9b7426530 100644 --- a/daemon/daemonapi/get_node_schedule.go +++ b/daemon/daemonapi/get_node_schedule.go @@ -47,7 +47,6 @@ func (a *DaemonAPI) getLocalSchedule(ctx echo.Context) error { RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, - StatefileKey: e.StatefileKey, }, } resp.Items = append(resp.Items, item) diff --git a/daemon/daemondata/apply_full.go b/daemon/daemondata/apply_full.go index ec492eca4..0e5635a1b 100644 --- a/daemon/daemondata/apply_full.go +++ b/daemon/daemondata/apply_full.go @@ -113,17 +113,31 @@ func (d *data) pubMsgFromNodeConfigDiffForNode(peer string) { prevTime, hasPrev = d.previousRemoteInfo[peer] prev = prevTime.nodeConfig onUpdate := func() { - if !reflect.DeepEqual(prev, next) { + var changed bool + if !reflect.DeepEqual(prev.Labels, next.Labels) { + d.publisher.Pub(&msgbus.NodeLabelsUpdated{Node: peer, Value: next.Labels.DeepCopy()}, + pubsub.Label{"node", peer}, + labelFromPeer, + ) + changed = true + } + if changed || !reflect.DeepEqual(prev, next) { node.ConfigData.Set(peer, next.DeepCopy()) d.publisher.Pub(&msgbus.NodeConfigUpdated{Node: peer, Value: *next.DeepCopy()}, pubsub.Label{"node", peer}, + labelFromPeer, ) } } onCreate := func() { + d.publisher.Pub(&msgbus.NodeLabelsUpdated{Node: peer, Value: next.Labels.DeepCopy()}, + pubsub.Label{"node", peer}, + labelFromPeer, + ) node.ConfigData.Set(peer, next.DeepCopy()) d.publisher.Pub(&msgbus.NodeConfigUpdated{Node: peer, Value: *next.DeepCopy()}, pubsub.Label{"node", peer}, + labelFromPeer, ) } @@ -189,18 +203,12 @@ func (d *data) pubMsgFromNodeStatusDiffForNode(peer string) { labelFromPeer, } onUpdate := func() { - var changed bool - if !reflect.DeepEqual(prev.Labels, next.Labels) { - d.publisher.Pub(&msgbus.NodeStatusLabelsUpdated{Node: peer, Value: next.Labels.DeepCopy()}, labels...) - changed = true - } - if changed || !reflect.DeepEqual(prev, next) { + if !reflect.DeepEqual(prev, next) { node.StatusData.Set(peer, next.DeepCopy()) d.publisher.Pub(&msgbus.NodeStatusUpdated{Node: peer, Value: *next.DeepCopy()}, labels...) } } onCreate := func() { - d.publisher.Pub(&msgbus.NodeStatusLabelsUpdated{Node: peer, Value: next.Labels.DeepCopy()}, labels...) node.StatusData.Set(peer, next.DeepCopy()) d.publisher.Pub(&msgbus.NodeStatusUpdated{Node: peer, Value: *next.DeepCopy()}, labels...) } diff --git a/daemon/daemondata/apply_patch.go b/daemon/daemondata/apply_patch.go index 67e77b015..60e47a601 100644 --- a/daemon/daemondata/apply_patch.go +++ b/daemon/daemondata/apply_patch.go @@ -201,7 +201,7 @@ func (d *data) setCacheAndPublish(ev event.Event) error { case *msgbus.NodeStatsUpdated: node.StatsData.Set(c.Node, &c.Value) d.publisher.Pub(c, labelFromPeer) - case *msgbus.NodeStatusLabelsUpdated: + case *msgbus.NodeLabelsUpdated: d.publisher.Pub(c, labelFromPeer) case *msgbus.NodeStatusUpdated: node.StatusData.Set(c.Node, &c.Value) diff --git a/daemon/daemondata/data.go b/daemon/daemondata/data.go index 9cd69d179..4b379ab39 100644 --- a/daemon/daemondata/data.go +++ b/daemon/daemondata/data.go @@ -430,7 +430,7 @@ func (d *data) startSubscriptions(ctx context.Context, qs pubsub.QueueSizer) { sub.AddFilter(&msgbus.NodeOsPathsUpdated{}, d.labelLocalhost) sub.AddFilter(&msgbus.NodeStatsUpdated{}, d.labelLocalhost) sub.AddFilter(&msgbus.NodeStatusUpdated{}, d.labelLocalhost) - sub.AddFilter(&msgbus.NodeStatusLabelsUpdated{}, d.labelLocalhost) + sub.AddFilter(&msgbus.NodeLabelsUpdated{}, d.labelLocalhost) // need forward to peers sub.AddFilter(&msgbus.ObjectCreated{}, d.labelLocalhost) @@ -499,7 +499,7 @@ func localEventMustBeForwarded(i interface{}) bool { case *msgbus.NodeOsPathsUpdated: case *msgbus.NodeStatsUpdated: case *msgbus.NodeStatusUpdated: - case *msgbus.NodeStatusLabelsUpdated: + case *msgbus.NodeLabelsUpdated: // object... case *msgbus.ObjectCreated: diff --git a/daemon/daemondata/data_init.go b/daemon/daemondata/data_init.go index 7d390b7af..de11fe06a 100644 --- a/daemon/daemondata/data_init.go +++ b/daemon/daemondata/data_init.go @@ -83,6 +83,7 @@ func newNodeData(localNode string) node.Node { nodeStatus := node.Node{ Config: node.Config{ // use initial default value + Labels: label.M{}, MaxParallel: object.DefaultNodeMaxParallel, MinAvailMemPct: 0, MinAvailSwapPct: 0, @@ -103,7 +104,6 @@ func newNodeData(localNode string) node.Node { Compat: 12, FrozenAt: frozen, Gen: node.Gen{localNode: 1}, - Labels: label.M{}, }, Os: node.Os{ Paths: san.Paths{}, diff --git a/daemon/hook/main.go b/daemon/hook/main.go index c2304bd4f..41503ca20 100644 --- a/daemon/hook/main.go +++ b/daemon/hook/main.go @@ -6,19 +6,16 @@ import ( "fmt" "os/exec" "slices" - "strings" "sync" "sync/atomic" "syscall" "time" "github.com/opensvc/om3/v3/core/event" - "github.com/opensvc/om3/v3/core/object" + "github.com/opensvc/om3/v3/core/node" "github.com/opensvc/om3/v3/core/xconfig" "github.com/opensvc/om3/v3/daemon/msgbus" - "github.com/opensvc/om3/v3/util/command" "github.com/opensvc/om3/v3/util/hostname" - "github.com/opensvc/om3/v3/util/key" "github.com/opensvc/om3/v3/util/plog" "github.com/opensvc/om3/v3/util/pubsub" "github.com/opensvc/om3/v3/util/xmap" @@ -40,17 +37,15 @@ type ( sub *pubsub.Subscription subQS pubsub.QueueSizer - hooks hooks + hooks map[string]hook wg sync.WaitGroup } hook struct { - sig string + node.Hook cancel func() } - - hooks map[string]hook ) var ( @@ -79,37 +74,35 @@ func NewManager(drainDuration time.Duration, subQS pubsub.QueueSizer) *Manager { labelLocalhost: pubsub.Label{"node", localhost}, subQS: subQS, - hooks: make(hooks), + hooks: make(map[string]hook), } } func (t *Manager) Start(parent context.Context) error { t.log.Infof("starting") t.ctx, t.cancel = context.WithCancel(parent) - t.update() + initialNodeConfig := node.ConfigData.GetByNode(t.localhost) + if initialNodeConfig == nil { + return fmt.Errorf("node config not found for localhost: %s", t.localhost) + } + t.update(initialNodeConfig.Hooks) t.startSubscriptions() t.startUpdateLoop() t.log.Infof("started") return nil } -func (t *Manager) loadConfig() error { - n, err := object.NewNode(object.WithVolatile(false)) - if err != nil { - return err - } - t.config = n.MergedConfig() - return nil -} - func (t *Manager) startUpdateLoop() { go func() { for { select { case <-t.ctx.Done(): return - case <-t.sub.C: - t.update() + case i := <-t.sub.C: + switch ev := i.(type) { + case *msgbus.NodeConfigUpdated: + t.update(ev.Value.Hooks) + } } } }() @@ -130,28 +123,22 @@ func (t *Manager) Stop() error { return nil } -func (t *Manager) update() { - if err := t.loadConfig(); err != nil { - t.log.Warnf("%s", err) - return - } +func (t *Manager) update(hooks []node.Hook) { currentHookNames := xmap.Keys(t.hooks) var hooksToStop, scannedHooks []string - hooksToStart := make(map[string]string) - for _, name := range t.config.SectionStrings() { - if !strings.HasPrefix(name, "hook#") { - continue - } + hooksToStart := make(map[string]node.Hook) + for _, h := range hooks { + name := h.Name scannedHooks = append(scannedHooks, name) - currentHook, ok := t.hooks[name] + current, ok := t.hooks[name] if !ok { - hooksToStart[name] = "" + hooksToStart[name] = h continue } - sig := t.config.SectionSig(name) - if sig != currentHook.sig { + if diff := current.Hook.Diff(h); diff != "" { + t.log.Infof("%s: %s", name, diff) hooksToStop = append(hooksToStop, name) - hooksToStart[name] = sig + hooksToStart[name] = h } } @@ -166,23 +153,16 @@ func (t *Manager) update() { } delete(t.hooks, name) } - for name, sig := range hooksToStart { - kinds := t.config.GetStrings(key.New(name, "events")) + for name, hookToStart := range hooksToStart { + kinds := hookToStart.Events h := hook{ - sig: sig, - } - t.hooks[name] = h - s := t.config.Get(key.New(name, "command")) - args, err := command.CmdArgsFromString(s) - if err != nil { - t.log.Warnf("%s: failed to split command: %s", name, err) - continue + Hook: hookToStart, } - if len(args) < 1 { + if len(hookToStart.Command) < 1 { t.log.Warnf("%s: empty command", name) continue } - h.cancel = t.startHook(name, kinds, args) + h.cancel = t.startHook(name, kinds, hookToStart.Command) t.hooks[name] = h } } diff --git a/daemon/icfg/main.go b/daemon/icfg/main.go index 087c7ccb2..feed02ba3 100644 --- a/daemon/icfg/main.go +++ b/daemon/icfg/main.go @@ -159,7 +159,7 @@ func (t *Manager) startSubscriptions() { // the scope value may depend on cluster nodes values: *, clusternodes ... // so we must also watch for cluster config updates to configFileCheckRefresh non cluster instance config scope t.sub.AddFilter(&msgbus.InstanceConfigUpdated{}, labelPathCluster, labelLocalhost) - t.sub.AddFilter(&msgbus.NodeStatusLabelsCommited{}) + t.sub.AddFilter(&msgbus.NodeLabelsCommited{}) } else { // Special note for cluster instance config: we don't subscribe for ConfigFileUpdated, instead we subscribe for // ClusterConfigUpdated. @@ -199,8 +199,8 @@ func (t *Manager) worker() { t.onConfigFileUpdated() case *msgbus.InstanceConfigUpdated: t.onLocalClusterInstanceConfigUpdated() - case *msgbus.NodeStatusLabelsCommited: - t.onNodeStatusLabelsCommited() + case *msgbus.NodeLabelsCommited: + t.onNodeLabelsCommited() } } } @@ -219,22 +219,22 @@ func (t *Manager) configFileCheckRefresh(force bool) error { } func (t *Manager) onClusterConfigUpdated() { - t.log.Infof("cluster config updated => refresh") + t.log.Tracef("cluster config updated => refresh") _ = t.configFileCheckRefresh(true) } func (t *Manager) onConfigFileUpdated() { - t.log.Infof("config file updated => refresh if csum changed") + t.log.Tracef("config file updated => refresh if csum changed") _ = t.configFileCheckRefresh(false) } func (t *Manager) onLocalClusterInstanceConfigUpdated() { - t.log.Infof("cluster instance config changed => refresh") + t.log.Tracef("cluster instance config changed => refresh") _ = t.configFileCheckRefresh(true) } -func (t *Manager) onNodeStatusLabelsCommited() { - t.log.Infof("node labels changed => refresh") +func (t *Manager) onNodeLabelsCommited() { + t.log.Tracef("node labels changed => refresh") _ = t.configFileCheckRefresh(true) } diff --git a/daemon/msgbus/messages.go b/daemon/msgbus/messages.go index 07d84c356..f400c981d 100644 --- a/daemon/msgbus/messages.go +++ b/daemon/msgbus/messages.go @@ -208,9 +208,9 @@ var ( "NodeStatusGenUpdates": func() any { return &NodeStatusGenUpdates{} }, - "NodeStatusLabelsCommited": func() any { return &NodeStatusLabelsCommited{} }, + "NodeLabelsCommited": func() any { return &NodeLabelsCommited{} }, - "NodeStatusLabelsUpdated": func() any { return &NodeStatusLabelsUpdated{} }, + "NodeLabelsUpdated": func() any { return &NodeLabelsUpdated{} }, "NodeSplitAction": func() any { return &NodeSplitAction{} }, @@ -775,13 +775,13 @@ type ( Value node.Gen `json:"gens" yaml:"gens"` } - NodeStatusLabelsCommited struct { + NodeLabelsCommited struct { pubsub.Msg `yaml:",inline"` Node string `json:"node" yaml:"node"` Value label.M `json:"node_labels" yaml:"node_labels"` } - NodeStatusLabelsUpdated struct { + NodeLabelsUpdated struct { pubsub.Msg `yaml:",inline"` Node string `json:"node" yaml:"node"` Value label.M `json:"node_labels" yaml:"node_labels"` @@ -1329,16 +1329,16 @@ func (e *NodeStatusGenUpdates) Key() string { return fmt.Sprintf("NodeStatusGenUpdates,node=%s", e.Node) } -func (e *NodeStatusLabelsCommited) Kind() string { - return "NodeStatusLabelsCommited" +func (e *NodeLabelsCommited) Kind() string { + return "NodeLabelsCommited" } -func (e *NodeStatusLabelsUpdated) Kind() string { - return "NodeStatusLabelsUpdated" +func (e *NodeLabelsUpdated) Kind() string { + return "NodeLabelsUpdated" } -func (e *NodeStatusLabelsUpdated) Key() string { - return fmt.Sprintf("NodeStatusLabelsUpdated,node=%s", e.Node) +func (e *NodeLabelsUpdated) Key() string { + return fmt.Sprintf("NodeLabelsUpdated,node=%s", e.Node) } func (e *NodeStale) Kind() string { diff --git a/daemon/msgbus/node_config.go b/daemon/msgbus/node_config.go index e91958288..454185cc3 100644 --- a/daemon/msgbus/node_config.go +++ b/daemon/msgbus/node_config.go @@ -6,7 +6,7 @@ import "github.com/opensvc/om3/v3/util/pubsub" func (data *ClusterData) onNodeConfigUpdated(m *NodeConfigUpdated) { newConfig := m.Value v := data.Cluster.Node[m.Node] - if v.Config.Equals(newConfig) { + if v.Config.Equal(newConfig) { return } v.Config = m.Value diff --git a/daemon/nmon/config.go b/daemon/nmon/config.go new file mode 100644 index 000000000..1cbd0378a --- /dev/null +++ b/daemon/nmon/config.go @@ -0,0 +1,90 @@ +package nmon + +import ( + "runtime" + "strings" + + "github.com/opensvc/om3/v3/core/node" + "github.com/opensvc/om3/v3/core/object" + "github.com/opensvc/om3/v3/util/key" +) + +func (t *Manager) getNodeConfig() node.Config { + var ( + keyMaintenanceGracePeriod = key.New("node", "maintenance_grace_period") + keyMaxParallel = key.New("node", "max_parallel") + keyMaxKeySize = key.New("node", "max_key_size") + keyReadyPeriod = key.New("node", "ready_period") + keyRejoinGracePeriod = key.New("node", "rejoin_grace_period") + keyEnv = key.New("node", "env") + keySplitAction = key.New("node", "split_action") + keySSHKey = key.New("node", "sshkey") + keyPRKey = key.New("node", "prkey") + keyMinAvailMemPct = key.New("node", "min_avail_mem_pct") + keyMinAvailSwapPct = key.New("node", "min_avail_swap_pct") + ) + cfg := node.Config{} + cfg.Labels = t.config.SectionMap("labels") + if d := t.config.GetDuration(keyMaintenanceGracePeriod); d != nil { + cfg.MaintenanceGracePeriod = *d + } + if d := t.config.GetDuration(keyReadyPeriod); d != nil { + cfg.ReadyPeriod = *d + } + if d := t.config.GetDuration(keyRejoinGracePeriod); d != nil { + cfg.RejoinGracePeriod = *d + } + if d := t.config.GetSize(keyMaxKeySize); d != nil { + cfg.MaxKeySize = *d + } + cfg.MinAvailMemPct = t.config.GetInt(keyMinAvailMemPct) + cfg.MinAvailSwapPct = t.config.GetInt(keyMinAvailSwapPct) + cfg.MaxParallel = t.config.GetInt(keyMaxParallel) + cfg.Env = t.config.GetString(keyEnv) + cfg.SplitAction = t.config.GetString(keySplitAction) + cfg.SSHKey = t.config.GetString(keySSHKey) + cfg.PRKey = t.config.GetString(keyPRKey) + + if cfg.MaxParallel == 0 { + cfg.MaxParallel = runtime.NumCPU() + } + if cfg.MaxParallel < MinMaxParallel { + cfg.MaxParallel = MinMaxParallel + } + + for _, s := range t.config.SectionStrings() { + if !strings.HasPrefix(s, "hook#") { + continue + } + t.log.Tracef("analyse config: %s", s) + hook := node.Hook{Name: s[5:]} + if hook.Name == "" { + t.log.Debugf("skip empty hook name for %s", s) + continue + } + hook.Events = t.config.GetStrings(key.New(s, "events")) + if len(hook.Events) == 0 { + t.log.Debugf("skip empty hook events for %s", s) + continue + } + hook.Command = t.config.GetStrings(key.New(s, "command")) + if len(hook.Command) == 0 { + t.log.Debugf("skip empty hook command for %s", s) + continue + } + cfg.Hooks = append(cfg.Hooks, hook) + t.log.Tracef("hook %s: %#v", hook.Name, hook) + } + + node, err := object.NewNode(object.WithVolatile(true)) + if err != nil { + t.log.Warnf("load node config: %s", err) + } else { + for _, e := range node.Schedules() { + cfg.Schedules = append(cfg.Schedules, e.Config) + } + cfg.Collector = node.CollectorRawConfig().AsConfig() + } + + return cfg +} diff --git a/daemon/nmon/main.go b/daemon/nmon/main.go index 5d23b0864..2ac9d6eed 100644 --- a/daemon/nmon/main.go +++ b/daemon/nmon/main.go @@ -30,7 +30,6 @@ import ( "os" "path/filepath" "runtime" - "slices" "sync" "time" @@ -59,6 +58,7 @@ import ( "github.com/opensvc/om3/v3/util/pubsub" "github.com/opensvc/om3/v3/util/san" "github.com/opensvc/om3/v3/util/version" + "github.com/opensvc/om3/v3/util/xmap" ) type ( @@ -118,7 +118,7 @@ type ( // cacheNodesInfo is a map of nodes to node.NodeInfo, it is used to // maintain the nodes_info.json file. // local values are computed by nmon. - // peer values are updated from msgbus events NodeStatusLabelsUpdated, NodeConfigUpdated, NodeOsPathsUpdated + // peer values are updated from msgbus events NodeLabelsUpdated, NodeConfigUpdated, NodeOsPathsUpdated // and ForgetPeer. cacheNodesInfo nodesinfo.M @@ -346,7 +346,7 @@ func (t *Manager) startSubscriptions() { sub.AddFilter(&msgbus.NodeOsPathsUpdated{}, pubsub.Label{"from", "peer"}) sub.AddFilter(&msgbus.NodeRejoin{}, t.labelLocalhost) sub.AddFilter(&msgbus.NodeStatusGenUpdates{}, t.labelLocalhost) - sub.AddFilter(&msgbus.NodeStatusLabelsUpdated{}, pubsub.Label{"from", "peer"}) + sub.AddFilter(&msgbus.NodeLabelsUpdated{}, pubsub.Label{"from", "peer"}) sub.AddFilter(&msgbus.SetNodeMonitor{}) sub.Start() t.sub = sub @@ -468,8 +468,8 @@ func (t *Manager) worker() { t.onNodeFrozenFileRemoved(c) case *msgbus.NodeFrozenFileUpdated: t.onNodeFrozenFileUpdated(c) - case *msgbus.NodeStatusLabelsUpdated: - t.onPeerNodeStatusLabelsUpdated(c) + case *msgbus.NodeLabelsUpdated: + t.onPeerNodeLabelsUpdated(c) case *msgbus.NodeStatusGenUpdates: t.onNodeStatusGenUpdates(c) case *msgbus.LeaveRequest: @@ -706,14 +706,14 @@ func (t *Manager) onPeerNodeOsPathsUpdated(m *msgbus.NodeOsPathsUpdated) { t.saveNodesInfo() } -func (t *Manager) onPeerNodeStatusLabelsUpdated(m *msgbus.NodeStatusLabelsUpdated) { +func (t *Manager) onPeerNodeLabelsUpdated(m *msgbus.NodeLabelsUpdated) { peerNodeInfo := t.cacheNodesInfo[m.Node] changed := !maps.Equal(peerNodeInfo.Labels, m.Value) peerNodeInfo.Labels = m.Value t.cacheNodesInfo[m.Node] = peerNodeInfo t.saveNodesInfo() if changed { - t.publisher.Pub(&msgbus.NodeStatusLabelsCommited{Node: m.Node, Value: m.Value.DeepCopy()}, t.labelLocalhost) + t.publisher.Pub(&msgbus.NodeLabelsCommited{Node: m.Node, Value: m.Value.DeepCopy()}, t.labelLocalhost) } } @@ -733,7 +733,7 @@ func (t *Manager) saveNodesInfo() { if err := nodesinfo.Save(t.cacheNodesInfo); err != nil { t.log.Errorf("save nodes info: %s", err) } else { - t.log.Infof("nodes info cache refreshed %s", t.cacheNodesInfo.Keys()) + t.log.Infof("saved nodes info %s", t.cacheNodesInfo.Keys()) } } @@ -768,10 +768,10 @@ func (t *Manager) loadConfig() error { if err != nil { return err } - localNodeInfo := t.cacheNodesInfo[t.localhost] - localNodeInfo.Labels = n.Labels() t.config = n.MergedConfig() t.nodeConfig = t.getNodeConfig() + localNodeInfo := t.cacheNodesInfo[t.localhost] + localNodeInfo.Labels = t.nodeConfig.Labels localNodeInfo.Env = t.nodeConfig.Env if lsnr := daemonsubsystem.DataListener.Get(t.localhost); lsnr != nil { @@ -788,40 +788,43 @@ func (t *Manager) loadConfigAndPublish() error { return err } - if !prevNodeConfig.Equals(t.nodeConfig) { - node.ConfigData.Set(t.localhost, t.nodeConfig.DeepCopy()) - t.publisher.Pub(&msgbus.NodeConfigUpdated{Node: t.localhost, Value: t.nodeConfig}, t.labelLocalhost) - } - if stats := node.StatsData.GetByNode(t.localhost); stats != nil && stats.MemTotalMB != 0 { t.updateIsOverloaded(*stats) } var labelsChanged, pathsChanged bool - localNodeInfo := t.cacheNodesInfo[t.localhost] - if !maps.Equal(localNodeInfo.Labels, t.nodeStatus.Labels) { - t.nodeStatus.Labels = localNodeInfo.Labels + + if diff := xmap.Diff(prevNodeConfig.Labels, t.nodeConfig.Labels); diff != "" { labelsChanged = true + t.log.Infof("labels changed: %s", diff) } paths := node.OsPathsData.GetByNode(t.localhost) - if paths == nil || !slices.Equal(localNodeInfo.Paths, *paths) { + if paths == nil { + paths = &san.Paths{} + } + if diff := paths.Diff(localNodeInfo.Paths); diff != "" { node.OsPathsData.Set(t.localhost, localNodeInfo.Paths.DeepCopy()) pathsChanged = true + t.log.Infof("paths changed: %s", diff) } if labelsChanged || pathsChanged { t.saveNodesInfo() } + if !prevNodeConfig.Equal(t.nodeConfig) { + node.ConfigData.Set(t.localhost, t.nodeConfig.DeepCopy()) + t.publisher.Pub(&msgbus.NodeConfigUpdated{Node: t.localhost, Value: t.nodeConfig}, t.labelLocalhost) + } + if labelsChanged { - t.publisher.Pub(&msgbus.NodeStatusLabelsUpdated{Node: t.localhost, Value: localNodeInfo.Labels.DeepCopy()}, t.labelLocalhost) - t.publisher.Pub(&msgbus.NodeStatusLabelsCommited{Node: t.localhost, Value: localNodeInfo.Labels.DeepCopy()}, t.labelLocalhost) + t.publisher.Pub(&msgbus.NodeLabelsUpdated{Node: t.localhost, Value: localNodeInfo.Labels.DeepCopy()}, t.labelLocalhost) + t.publisher.Pub(&msgbus.NodeLabelsCommited{Node: t.localhost, Value: localNodeInfo.Labels.DeepCopy()}, t.labelLocalhost) } if pathsChanged { t.publisher.Pub(&msgbus.NodeOsPathsUpdated{Node: t.localhost, Value: *localNodeInfo.Paths.DeepCopy()}, t.labelLocalhost) } t.updateSpeaker() - t.publishNodeStatus() select { case t.poolC <- nil: diff --git a/daemon/nmon/main_cmd.go b/daemon/nmon/main_cmd.go index 46c70a80c..a31545a7f 100644 --- a/daemon/nmon/main_cmd.go +++ b/daemon/nmon/main_cmd.go @@ -4,19 +4,16 @@ import ( "errors" "fmt" "os" - "runtime" "slices" "strings" "time" "github.com/opensvc/om3/v3/core/clusternode" "github.com/opensvc/om3/v3/core/node" - "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/rawconfig" "github.com/opensvc/om3/v3/daemon/msgbus" "github.com/opensvc/om3/v3/util/errcontext" "github.com/opensvc/om3/v3/util/file" - "github.com/opensvc/om3/v3/util/key" "github.com/opensvc/om3/v3/util/toc" ) @@ -67,56 +64,6 @@ func (t *Manager) onConfigFileUpdated(_ *msgbus.ConfigFileUpdated) { t.checkRejoinTicker() } -func (t *Manager) getNodeConfig() node.Config { - var ( - keyMaintenanceGracePeriod = key.New("node", "maintenance_grace_period") - keyMaxParallel = key.New("node", "max_parallel") - keyMaxKeySize = key.New("node", "max_key_size") - keyReadyPeriod = key.New("node", "ready_period") - keyRejoinGracePeriod = key.New("node", "rejoin_grace_period") - keyEnv = key.New("node", "env") - keySplitAction = key.New("node", "split_action") - keySSHKey = key.New("node", "sshkey") - keyPRKey = key.New("node", "prkey") - keyMinAvailMemPct = key.New("node", "min_avail_mem_pct") - keyMinAvailSwapPct = key.New("node", "min_avail_swap_pct") - ) - cfg := node.Config{} - if d := t.config.GetDuration(keyMaintenanceGracePeriod); d != nil { - cfg.MaintenanceGracePeriod = *d - } - if d := t.config.GetDuration(keyReadyPeriod); d != nil { - cfg.ReadyPeriod = *d - } - if d := t.config.GetDuration(keyRejoinGracePeriod); d != nil { - cfg.RejoinGracePeriod = *d - } - if d := t.config.GetSize(keyMaxKeySize); d != nil { - cfg.MaxKeySize = *d - } - cfg.MinAvailMemPct = t.config.GetInt(keyMinAvailMemPct) - cfg.MinAvailSwapPct = t.config.GetInt(keyMinAvailSwapPct) - cfg.MaxParallel = t.config.GetInt(keyMaxParallel) - cfg.Env = t.config.GetString(keyEnv) - cfg.SplitAction = t.config.GetString(keySplitAction) - cfg.SSHKey = t.config.GetString(keySSHKey) - cfg.PRKey = t.config.GetString(keyPRKey) - - if cfg.MaxParallel == 0 { - cfg.MaxParallel = runtime.NumCPU() - } - if cfg.MaxParallel < MinMaxParallel { - cfg.MaxParallel = MinMaxParallel - } - - node, _ := object.NewNode(object.WithVolatile(true)) - for _, e := range node.Schedules() { - cfg.Schedules = append(cfg.Schedules, e.Config) - } - - return cfg -} - func (t *Manager) checkRejoinTicker() { if t.state.State != node.MonitorStateRejoin { return diff --git a/core/output/flatten.go b/util/flatten/main.go similarity index 99% rename from core/output/flatten.go rename to util/flatten/main.go index a9a31346a..c4ab7cc44 100644 --- a/core/output/flatten.go +++ b/util/flatten/main.go @@ -1,4 +1,4 @@ -package output +package flatten import ( "bytes" diff --git a/util/san/main.go b/util/san/main.go index 71c7effe9..fde1d24be 100644 --- a/util/san/main.go +++ b/util/san/main.go @@ -34,6 +34,39 @@ type ( } ) +func (t Paths) Diff(other Paths) string { + oldSet := make(map[string]any) + newSet := make(map[string]any) + + for _, item := range t { + oldSet[item.String()] = nil + } + for _, item := range other { + newSet[item.String()] = nil + } + + var changes []string + + // Check for added + for s := range newSet { + if _, exists := oldSet[s]; !exists { + changes = append(changes, fmt.Sprintf("+%s", s)) + } + } + + // Check for removed + for s := range oldSet { + if _, exists := newSet[s]; !exists { + changes = append(changes, fmt.Sprintf("-%s", s)) + } + } + + if len(changes) == 0 { + return "" + } + return strings.Join(changes, ", ") +} + func (t Paths) Mapping() string { return strings.Join(t.MappingList(), ",") } @@ -41,7 +74,7 @@ func (t Paths) Mapping() string { func (t Paths) MappingList() []string { l := make([]string, 0) for _, p := range t { - s := p.Initiator.Name + ":" + p.Target.Name + s := p.String() l = append(l, s) } return l @@ -210,6 +243,10 @@ func (t Paths) DeepCopy() *Paths { return &l } +func (t Path) String() string { + return fmt.Sprintf("%s:%s", t.Target.Name, t.Initiator.Name) +} + func (t Path) DeepCopy() Path { return Path{ Initiator: t.Initiator.DeepCopy(), diff --git a/util/xmap/main.go b/util/xmap/main.go index ba9cddc08..a7169db30 100644 --- a/util/xmap/main.go +++ b/util/xmap/main.go @@ -1,6 +1,10 @@ package xmap -import "reflect" +import ( + "fmt" + "reflect" + "strings" +) // Keys returns the slice of a map string keys. func Keys(i interface{}) []string { @@ -19,3 +23,28 @@ func Copy[K, V comparable](m map[K]V) map[K]V { } return result } + +func Diff(old, new map[string]string) string { + var changes []string + + // Check for added and changed + for k, newV := range new { + if oldV, exists := old[k]; !exists { + changes = append(changes, fmt.Sprintf("+%s=%s", k, newV)) + } else if oldV != newV { + changes = append(changes, fmt.Sprintf("~%s=%s->%s", k, oldV, newV)) + } + } + + // Check for removed + for k, oldV := range old { + if _, exists := new[k]; !exists { + changes = append(changes, fmt.Sprintf("-%s=%s", k, oldV)) + } + } + + if len(changes) == 0 { + return "" + } + return strings.Join(changes, ", ") +}