From 5e90f62f9f746abdef4b18859a32130d716dc7d3 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 5 Nov 2024 08:11:54 +0100 Subject: [PATCH 1/7] Use a second api client for tui logs and event streaming The main client has a 5s timeout. The streaming client has no timeout. --- core/tui/main.go | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/core/tui/main.go b/core/tui/main.go index af29afe73..00d6be228 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -56,7 +56,8 @@ type ( command *tview.InputField contexts *tview.List - client *client.T + client *client.T + streamClient *client.T lastDraw time.Time @@ -530,13 +531,19 @@ func (t *App) listContexts() { t.listContexts() } else if resp.StatusCode() == http.StatusOK { t.client = cli - t.user = resp.JSON200.Name - t.reconnect() - t.flex.Clear() - t.flex.AddItem(t.head, 1, 0, false) - t.flex.AddItem(t.objects, 0, 1, true) - t.app.SetFocus(t.objects) - t.updateHead() + if streamClient, err := client.New(client.WithTimeout(0)); err != nil { + t.errorf("new stream client: %s", err) + t.listContexts() + } else { + t.streamClient = streamClient + t.user = resp.JSON200.Name + t.reconnect() + t.flex.Clear() + t.flex.AddItem(t.head, 1, 0, false) + t.flex.AddItem(t.objects, 0, 1, true) + t.app.SetFocus(t.objects) + t.updateHead() + } } }) @@ -557,15 +564,21 @@ func (t *App) Run() error { } func (t *App) initContext() { - if cli, err := client.New(client.WithTimeout(0)); err != nil { + if cli, err := client.New(); err != nil { t.errorf("%s", err) } else if resp, err := cli.GetwhoamiWithResponse(context.Background()); err != nil { t.errorf("%s", err) t.listContexts() } else if resp.StatusCode() == http.StatusOK { t.client = cli - t.user = resp.JSON200.Name - t.reconnect() + if streamClient, err := client.New(client.WithTimeout(0)); err != nil { + t.errorf("new stream client: %s", err) + t.listContexts() + } else { + t.streamClient = streamClient + t.user = resp.JSON200.Name + t.reconnect() + } } else { t.listContexts() } @@ -574,9 +587,9 @@ func (t *App) initContext() { func (t *App) runEventReader() { <-t.restartC for { - evReader, err := t.client.NewGetEvents().SetSelector(t.Selector).GetReader() + evReader, err := t.streamClient.NewGetEvents().SetSelector(t.Selector).GetReader() if err != nil { - t.errorf("%s", err) + t.errorf("new reader: %s", err) if t.exitFlag.Load() { return } @@ -591,6 +604,7 @@ func (t *App) runEventReader() { return } if err != nil { + t.errorf("do with reader: %s", err) time.Sleep(10 * time.Millisecond) } } @@ -1653,7 +1667,7 @@ func (t *App) onRuneL(event *tcell.EventKey) { lines := 50 follow := true - log := t.client.NewGetLogs(t.viewNode). + log := t.streamClient.NewGetLogs(t.viewNode). //SetFilters(nil). SetLines(&lines). SetFollow(&follow) From ea2ae4a814ab9bdacdfd5275a929e0ca706b485f Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 12 Nov 2024 09:29:43 +0100 Subject: [PATCH 2/7] Use "ghcr.io/opensvc/pause" as the default container.image value Thus make the kw optional. --- core/object/type_status.go | 6 ++-- core/tui/main.go | 39 ++++++++++++++++++++--- drivers/rescontainerocibase/manifest.go | 12 +------ drivers/rescontainerocibase/text/kw/pidns | 2 +- drivers/restaskdocker/keywords.go | 2 +- drivers/restaskdocker/text/kw/pidns | 2 +- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/core/object/type_status.go b/core/object/type_status.go index 6acea8745..104018639 100644 --- a/core/object/type_status.go +++ b/core/object/type_status.go @@ -73,7 +73,7 @@ func (t Digest) LoadTreeNode(head *tree.Node, nodes []string) { head.AddColumn().AddText(t.Path.String()).SetColor(rawconfig.Color.Bold) head.AddColumn() head.AddColumn().AddText(colorstatus.Sprint(t.Object.Avail, rawconfig.Colorize)) - head.AddColumn().AddText(t.descString()) + head.AddColumn().AddText(t.ObjectWarningsString()) instances := head.AddNode() instances.AddColumn().AddText("instances") openMap := make(map[string]any) @@ -106,9 +106,9 @@ func (t Digest) LoadTreeNode(head *tree.Node, nodes []string) { } } -// descString returns a string presenting notable information at the object, +// ObjectWarningsString returns a string presenting notable information at the object, // instances-aggregated, level. -func (t Digest) descString() string { +func (t Digest) ObjectWarningsString() string { l := make([]string, 0) // Overall if warn. Else no need to repeat an info we can guess from Avail. diff --git a/core/tui/main.go b/core/tui/main.go index 00d6be228..14f174a15 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -18,6 +18,7 @@ import ( "github.com/opensvc/om3/core/client" "github.com/opensvc/om3/core/clientcontext" "github.com/opensvc/om3/core/clusterdump" + "github.com/opensvc/om3/core/colorstatus" "github.com/opensvc/om3/core/event" "github.com/opensvc/om3/core/monitor" "github.com/opensvc/om3/core/naming" @@ -1792,12 +1793,40 @@ func (t *App) updateInstanceView() { } digest := t.Frame.Current.GetObjectStatus(t.viewPath) text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) - t.initTextView() + _ = text + /* + t.initTextView() + title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) + t.textView.SetDynamicColors(true) + t.textView.SetTitle(title) + t.textView.Clear() + fmt.Fprint(t.textView, text) + */ title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) - t.textView.SetDynamicColors(true) - t.textView.SetTitle(title) - t.textView.Clear() - fmt.Fprint(t.textView, text) + table := tview.NewTable() + table.SetTitle(title) + table.SetBorder(false) + + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyEnter: + //onEnter(event) + return nil // prevents the default select behaviour + } + return event + }) + + table.SetCell(0, 0, tview.NewTableCell(t.viewPath.String()).SetAttributes(tcell.AttrBold).SetSelectable(true)) + table.SetCell(0, 1, tview.NewTableCell("").SetSelectable(false)) + table.SetCell(0, 2, tview.NewTableCell(tview.TranslateANSI(colorstatus.Sprint(digest.Object.Avail, rawconfig.Colorize))).SetSelectable(false)) + table.SetCell(0, 3, tview.NewTableCell(tview.TranslateANSI(digest.ObjectWarningsString())).SetSelectable(false)) + + t.flex.Clear() + t.flex.AddItem(t.head, 1, 0, false) + t.flex.AddItem(table, 0, 1, true) + t.app.SetFocus(table) } func (t *App) onRuneE(event *tcell.EventKey) { diff --git a/drivers/rescontainerocibase/manifest.go b/drivers/rescontainerocibase/manifest.go index afc11e5d3..9d352a175 100644 --- a/drivers/rescontainerocibase/manifest.go +++ b/drivers/rescontainerocibase/manifest.go @@ -56,17 +56,7 @@ func (t *BT) ManifestWithID(drvID driver.ID) *manifest.T { Attr: "Image", Aliases: []string{"run_image"}, Scopable: true, - Required: true, - Example: "google/pause", - Text: keywords.NewText(fs, "text/kw/image"), - }, - keywords.Keyword{ - Option: "image", - Attr: "Image", - Aliases: []string{"run_image"}, - Scopable: true, - Required: true, - Example: "google/pause", + Default: "ghcr.io/opensvc/pause", Text: keywords.NewText(fs, "text/kw/image"), }, keywords.Keyword{ diff --git a/drivers/rescontainerocibase/text/kw/pidns b/drivers/rescontainerocibase/text/kw/pidns index b6b9b8885..38833db01 100644 --- a/drivers/rescontainerocibase/text/kw/pidns +++ b/drivers/rescontainerocibase/text/kw/pidns @@ -1,7 +1,7 @@ * empty The container has a private pidns other containers can share. - Usually a pidns sharer will run a `google/pause` image to reap zombies. + Usually a pidns sharer will run a `pause` image to reap zombies. * `container#` diff --git a/drivers/restaskdocker/keywords.go b/drivers/restaskdocker/keywords.go index dfb78abd3..b85246b12 100644 --- a/drivers/restaskdocker/keywords.go +++ b/drivers/restaskdocker/keywords.go @@ -40,7 +40,7 @@ var ( { Aliases: []string{"run_image"}, Attr: "Image", - Example: "google/pause", + Example: "ghcr.io/opensvc/pause", Option: "image", Required: true, Scopable: true, diff --git a/drivers/restaskdocker/text/kw/pidns b/drivers/restaskdocker/text/kw/pidns index b6b9b8885..38833db01 100644 --- a/drivers/restaskdocker/text/kw/pidns +++ b/drivers/restaskdocker/text/kw/pidns @@ -1,7 +1,7 @@ * empty The container has a private pidns other containers can share. - Usually a pidns sharer will run a `google/pause` image to reap zombies. + Usually a pidns sharer will run a `pause` image to reap zombies. * `container#` From f156bf4e057768e14d4b86a3ab2c37980f12cefb Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 12 Nov 2024 11:37:34 +0100 Subject: [PATCH 3/7] Don't force the volume stop from the volume resource stop Always forcing ended up with drbd legs 'down' instead of 'stdby up', and making the switch action a one-time success. --- drivers/resvol/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/resvol/main.go b/drivers/resvol/main.go index 985b85660..6db3b4a42 100644 --- a/drivers/resvol/main.go +++ b/drivers/resvol/main.go @@ -86,7 +86,7 @@ func (t T) startVolume(ctx context.Context, volume object.Vol) error { } func (t T) stopVolume(ctx context.Context, volume object.Vol, force bool) error { - ctx = actioncontext.WithForce(ctx, true) + ctx = actioncontext.WithForce(ctx, force) holders := volume.HoldersExcept(ctx, t.Path) if len(holders) > 0 { t.Log().Infof("skip volume %s stop: active users: %s", volume.Path(), holders) From 9997eb1dfb53a94c46e620514bec2626118d3418 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 14 Nov 2024 18:45:56 +0100 Subject: [PATCH 4/7] Colorize gray the remaining restart resource flag if local_expect!=started --- core/instance/monitor.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/instance/monitor.go b/core/instance/monitor.go index a058050e2..2e0b728d6 100644 --- a/core/instance/monitor.go +++ b/core/instance/monitor.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" + "github.com/opensvc/om3/core/rawconfig" "github.com/opensvc/om3/core/resource" "github.com/opensvc/om3/core/resourceid" "github.com/opensvc/om3/core/status" @@ -441,7 +442,14 @@ func (mon Monitor) ResourceFlagRestartString(rid resourceid.T, r resource.Status if rmon := mon.Resources.Get(rid.Name); rmon != nil { retries = rmon.Restart.Remaining } - return r.Restart.FlagString(retries) + s := r.Restart.FlagString(retries) + if s == "." { + return s + } + if mon.LocalExpect != MonitorLocalExpectStarted { + s = rawconfig.Colorize.Secondary(s) + } + return s } func (mon Monitor) DeepCopy() *Monitor { From 238a20e1ba5c96558c593ceff6ecb136ef89b7ee Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 15 Nov 2024 17:07:50 +0100 Subject: [PATCH 5/7] Fix key install codepaths * Fix key install not populating the expected file tree corresponding to the keynames matching the pattern. The "sec1/*:/toto" volume.secrets expression was missing subtree files. * Fix the intermediate directories permissions and ownership. * Also simplify keystore install codepaths func prototype, passing these new structs instead of passing all arguments as a vector: KVInstall struct { ToHead string ToPath string FromPattern string FromStore naming.Path AccessControl KVInstallAccessControl } KVInstallAccessControl struct { User string Group string Perm *os.FileMode DirPerm *os.FileMode } --- core/object/keystore.go | 2 +- core/object/keystore_install.go | 174 ++++++++++++++++++++------------ core/object/keystore_keys.go | 18 +++- daemon/listener/certs.go | 55 +++++++--- drivers/resvol/data.go | 84 +++++++-------- 5 files changed, 201 insertions(+), 132 deletions(-) diff --git a/core/object/keystore.go b/core/object/keystore.go index 04a0cc560..1386b4839 100644 --- a/core/object/keystore.go +++ b/core/object/keystore.go @@ -31,7 +31,7 @@ type ( DecodeKey(name string) ([]byte, error) EditKey(name string) error InstallKey(name string) error - InstallKeyTo(string, string, *os.FileMode, *os.FileMode, string, string) error + InstallKeyTo(KVInstall) error RemoveKey(name string) error RenameKey(name, to string) error diff --git a/core/object/keystore_install.go b/core/object/keystore_install.go index 9a71861d9..37cfe740e 100644 --- a/core/object/keystore_install.go +++ b/core/object/keystore_install.go @@ -24,9 +24,22 @@ type ( vKey struct { Key string Type vKeyType - Path string Keys []vKey } + + KVInstall struct { + ToHead string + ToPath string + FromPattern string + FromStore naming.Path + AccessControl KVInstallAccessControl + } + KVInstallAccessControl struct { + User string + Group string + Perm *os.FileMode + DirPerm *os.FileMode + } ) const ( @@ -34,10 +47,15 @@ const ( vKeyDir ) +func (t KVInstall) IsEmpty() bool { + return t.ToPath == "" && t.FromPattern == "" +} + func (t *keystore) resolveKey(k string) ([]vKey, error) { var ( dirs, keys []string err error + recurse func(string) []vKey ) if dirs, err = t.AllDirs(); err != nil { return []vKey{}, err @@ -45,8 +63,38 @@ func (t *keystore) resolveKey(k string) ([]vKey, error) { if keys, err = t.AllKeys(); err != nil { return []vKey{}, err } - data, _ := resolveKeyRecurse(k, make(map[string]interface{}), dirs, keys) - return data, nil + done := make(map[string]any) + + recurse = func(k string) []vKey { + data := make([]vKey, 0) + for _, p := range dirs { + if p != k && !fnmatch.Match(k, p, fnmatch.FNM_PATHNAME) { + continue + } + vks := recurse(p + "/*") + data = append(data, vKey{ + Key: p, + Type: vKeyDir, + Keys: vks, + }) + } + for _, p := range keys { + if p != k && !fnmatch.Match(k, p, fnmatch.FNM_PATHNAME) { + continue + } + if _, ok := done[p]; ok { + continue + } + done[p] = nil + data = append(data, vKey{ + Key: p, + Type: vKeyFile, + }) + } + return data + } + + return recurse(k), nil } func mergeMapsets(m1 map[string]interface{}, m2 map[string]interface{}) map[string]interface{} { @@ -56,38 +104,6 @@ func mergeMapsets(m1 map[string]interface{}, m2 map[string]interface{}) map[stri return m2 } -func resolveKeyRecurse(k string, done map[string]interface{}, dirs, keys []string) ([]vKey, map[string]interface{}) { - data := make([]vKey, 0) - for _, p := range dirs { - if p != k && !fnmatch.Match(p, k, fnmatch.FNM_PATHNAME|fnmatch.FNM_LEADING_DIR) { - continue - } - vks, rdone := resolveKeyRecurse(p+"/*", done, dirs, keys) - done = mergeMapsets(done, rdone) - data = append(data, vKey{ - Key: k, - Type: vKeyDir, - Path: p, - Keys: vks, - }) - } - for _, p := range keys { - if p != k && !fnmatch.Match(p, k, fnmatch.FNM_PATHNAME|fnmatch.FNM_LEADING_DIR) { - continue - } - if _, ok := done[p]; ok { - continue - } - done[p] = nil - data = append(data, vKey{ - Key: k, - Type: vKeyFile, - Path: p, - }) - } - return data, done -} - func (t *keystore) _install(k string, dst string) error { keys, err := t.resolveKey(k) if err != nil { @@ -97,7 +113,10 @@ func (t *keystore) _install(k string, dst string) error { return fmt.Errorf("%s key=%s not found", t.path, k) } for _, vk := range keys { - if _, err := t.installKey(vk, dst, nil, nil, "", ""); err != nil { + opt := KVInstall{ + ToPath: dst, + } + if _, err := t.installKey(vk, opt); err != nil { return err } } @@ -107,48 +126,48 @@ func (t *keystore) _install(k string, dst string) error { // keyPath returns the full path to host's file containing the key decoded data. func (t *keystore) keyPath(vk vKey, dst string) string { if strings.HasSuffix(dst, "/") { - name := filepath.Base(strings.TrimRight(vk.Path, "/")) + name := filepath.Base(strings.TrimRight(vk.Key, "/")) return filepath.Join(dst, name) } return dst } -func (t *keystore) installKey(vk vKey, dst string, mode *os.FileMode, dirmode *os.FileMode, usr, grp string) (bool, error) { +func (t *keystore) installKey(vk vKey, opt KVInstall) (bool, error) { switch vk.Type { case vKeyFile: - vpath := t.keyPath(vk, dst) - return t.installFileKey(vk, vpath, mode, dirmode, usr, grp) + opt.ToPath = t.keyPath(vk, opt.ToPath) + return t.installFileKey(vk, opt) case vKeyDir: - return t.installDirKey(vk, dst, mode, dirmode, usr, grp) + return t.installDirKey(vk, opt) default: return false, nil } } // installFileKey installs a key content in the host storage -func (t *keystore) installFileKey(vk vKey, dst string, mode *os.FileMode, dirmode *os.FileMode, usr, grp string) (bool, error) { - if strings.Contains(dst, "..") { +func (t *keystore) installFileKey(vk vKey, opt KVInstall) (bool, error) { + if strings.Contains(opt.ToPath, "..") { // paranoid checks before RemoveAll() and Remove() - return false, fmt.Errorf("install file key not allowed: %s contains \"..\"", dst) + return false, fmt.Errorf("install file key not allowed: %s contains \"..\"", opt.ToPath) } b, err := t.decode(vk.Key) if err != nil { return false, err } - if v, err := file.ExistsAndDir(dst); err != nil { - t.Log().Errorf("install %s key=%s directory at location %s: %s", t.path, vk.Key, dst, err) + if v, err := file.ExistsAndDir(opt.ToPath); err != nil { + t.Log().Errorf("install %s key=%s directory at location %s: %s", t.path, vk.Key, opt.ToPath, err) } else if v { - t.Log().Infof("remove %s key=%s directory at location %s", t.path, vk.Key, dst) - if err := os.RemoveAll(dst); err != nil { + t.Log().Infof("remove %s key=%s directory at location %s", t.path, vk.Key, opt.ToPath) + if err := os.RemoveAll(opt.ToPath); err != nil { return false, err } } - vdir := filepath.Dir(dst) + vdir := filepath.Dir(opt.ToPath) info, err := os.Stat(vdir) switch { case os.IsNotExist(err): t.Log().Infof("create directory %s to host %s key=%s", vdir, t.path, vk.Key) - if err := os.MkdirAll(vdir, *dirmode); err != nil { + if err := t.makedir(vdir, opt.AccessControl); err != nil { return false, err } case file.IsNotDir(err): @@ -160,21 +179,21 @@ func (t *keystore) installFileKey(vk vKey, dst string, mode *os.FileMode, dirmod return false, err } } - return t.writeKey(vk, dst, b, mode, usr, grp) + return t.writeKey(vk, opt.ToPath, b, opt.AccessControl.DirPerm, opt.AccessControl.User, opt.AccessControl.Group) } // installDirKey creates a directory to host projected keys -func (t *keystore) installDirKey(vk vKey, dst string, mode *os.FileMode, dirmode *os.FileMode, usr, grp string) (bool, error) { - if strings.HasSuffix(dst, "/") { - dirname := filepath.Base(vk.Path) - dst = filepath.Join(dst, dirname, "") +func (t *keystore) installDirKey(vk vKey, opt KVInstall) (bool, error) { + if strings.HasSuffix(opt.ToPath, "/") { + dirname := filepath.Base(vk.Key) + opt.ToPath = filepath.Join(opt.ToPath, dirname) + "/" } - if err := os.MkdirAll(dst, *dirmode); err != nil { + if err := t.makedir(opt.ToPath, opt.AccessControl); err != nil { return false, err } changed := false for _, k := range vk.Keys { - v, err := t.installKey(k, dst, mode, dirmode, usr, grp) + v, err := t.installKey(k, opt) if err != nil { return changed, err } @@ -256,17 +275,46 @@ func (t *keystore) InstallKey(keyName string) error { return t.postInstall(keyName) } -func (t *keystore) InstallKeyTo(keyName string, dst string, mode *os.FileMode, dirmode *os.FileMode, usr, grp string) error { - t.log.Debugf("install %s key %s to %s", t.path, keyName, dst) - keys, err := t.resolveKey(keyName) +func (t *keystore) makedir(path string, opt KVInstallAccessControl) error { + if err := os.MkdirAll(path, *opt.DirPerm); err != nil { + return err + } + if err := t.chmod(path, opt.DirPerm); err != nil { + return err + } + if err := t.chown(path, opt.User, opt.Group); err != nil { + return err + } + return nil +} + +func (t *keystore) makedirs(opt KVInstall) error { + if opt.ToHead == "" || !strings.HasSuffix(opt.ToPath, "/") { + return nil + } + relPath := strings.TrimPrefix(opt.ToPath, opt.ToHead) + for _, dir := range pathChain(relPath) { + if err := t.makedir(filepath.Join(opt.ToHead, dir), opt.AccessControl); err != nil { + return err + } + } + return nil +} + +func (t *keystore) InstallKeyTo(opt KVInstall) error { + t.log.Debugf("install %s key %s to %s", t.path, opt.FromPattern, opt.ToPath) + keys, err := t.resolveKey(opt.FromPattern) if err != nil { - return fmt.Errorf("resolve %s key %s: %w", t.path, keyName, err) + return fmt.Errorf("resolve %s key %s: %w", t.path, opt.FromPattern, err) } if len(keys) == 0 { - return fmt.Errorf("resolve %s key %s: no key found", t.path, keyName) + return fmt.Errorf("resolve %s key %s: no key found", t.path, opt.FromPattern) + } + if err := t.makedirs(opt); err != nil { + return err } for _, vk := range keys { - if _, err := t.installKey(vk, dst, mode, dirmode, usr, grp); err != nil { + if _, err := t.installKey(vk, opt); err != nil { return fmt.Errorf("install key %s at path %s: %w", vk.Key, t.path, err) } } diff --git a/core/object/keystore_keys.go b/core/object/keystore_keys.go index 815b6a772..ce64b3e29 100644 --- a/core/object/keystore_keys.go +++ b/core/object/keystore_keys.go @@ -8,12 +8,26 @@ import ( "github.com/opensvc/om3/util/xmap" ) +func pathChain(k string) []string { + m := make(map[string]any) + for { + k = filepath.Dir(k) + if k == "" || k == "/" || k == "." { + break + } + m[k] = nil + } + dirs := xmap.Keys(m) + sort.Strings(dirs) + return dirs +} + // MatchingDirs returns the list of all directories and parent directories // hosting keys in the store's virtual filesystem. // // Example: []key{"a/b/c", "a/c/b"} => []dir{"a", "a/b", "a/c"} func (t *keystore) MatchingDirs(pattern string) ([]string, error) { - m := make(map[string]interface{}) + m := make(map[string]any) keys, err := t.MatchingKeys(pattern) if err != nil { return []string{}, err @@ -42,7 +56,7 @@ func (t *keystore) AllKeys() ([]string, error) { func (t *keystore) MatchingKeys(pattern string) ([]string, error) { data := make([]string, 0) - f := fnmatch.FNM_PATHNAME | fnmatch.FNM_LEADING_DIR + f := fnmatch.FNM_PATHNAME for _, s := range t.config.Keys(dataSectionName) { if pattern == "" || fnmatch.Match(pattern, s, f) { diff --git a/daemon/listener/certs.go b/daemon/listener/certs.go index 157f4d2d0..79987e115 100644 --- a/daemon/listener/certs.go +++ b/daemon/listener/certs.go @@ -95,20 +95,31 @@ func (t *T) installCaFiles(clusterName string) error { return fmt.Errorf("install ca files can't get %s: %w", caPath, err) } + opt := object.KVInstall{ + AccessControl: object.KVInstallAccessControl{ + Perm: &certFileMode, + DirPerm: &certDirMode, + User: certUsr, + Group: certGrp, + }, + } + // ca_certificates for jwt - dst := daemonenv.CAKeyFile() - if err := caSec.InstallKeyTo("private_key", dst, &certFileMode, &certDirMode, certUsr, certGrp); err != nil { - return fmt.Errorf("install ca files can't dump ca private_key to %s: %w", dst, err) + opt.FromPattern = "private_key" + opt.ToPath = daemonenv.CAKeyFile() + if err := caSec.InstallKeyTo(opt); err != nil { + return fmt.Errorf("install ca files can't dump ca private_key to %s: %w", opt.ToPath, err) } else { - t.log.Infof("install ca files dump ca private_key to %s", dst) + t.log.Infof("install ca files dump ca private_key to %s", opt.ToPath) } - dst = daemonenv.CACertChainFile() - if err := caSec.InstallKeyTo("certificate_chain", dst, &certFileMode, &certDirMode, certUsr, certGrp); err != nil { - return fmt.Errorf("install ca files can't dump ca certificate_chain to %s: %w", dst, err) + opt.FromPattern = "certificate_chain" + opt.ToPath = daemonenv.CACertChainFile() + if err := caSec.InstallKeyTo(opt); err != nil { + return fmt.Errorf("install ca files can't dump ca certificate_chain to %s: %w", opt.ToPath, err) } else { - t.log.Infof("install ca files dump ca certificate_chain to %s", dst) + t.log.Infof("install ca files dump ca certificate_chain to %s", opt.ToPath) } // ca_certificates @@ -165,17 +176,29 @@ func (t *T) installCertFiles(clusterName string) error { return fmt.Errorf("install cert files can't get %s: %w", certPath, err) } - dst := daemonenv.KeyFile() - if err := certSec.InstallKeyTo("private_key", dst, &certFileMode, &certDirMode, certUsr, certGrp); err != nil { - return fmt.Errorf("install cert files can't dump cert private_key to %s: %w", dst, err) + opt := object.KVInstall{ + AccessControl: object.KVInstallAccessControl{ + Perm: &certFileMode, + DirPerm: &certDirMode, + User: certUsr, + Group: certGrp, + }, + } + + opt.FromPattern = "private_key" + opt.ToPath = daemonenv.KeyFile() + if err := certSec.InstallKeyTo(opt); err != nil { + return fmt.Errorf("install cert files can't dump cert private_key to %s: %w", opt.ToPath, err) } else { - t.log.Infof("install cert files dump cert private_key to %s", dst) + t.log.Infof("install cert files dump cert private_key to %s", opt.ToPath) } - dst = daemonenv.CertChainFile() - if err := certSec.InstallKeyTo("certificate_chain", dst, &certFileMode, &certDirMode, certUsr, certGrp); err != nil { - return fmt.Errorf("install cert files can't dump cert certificate_chain to %s: %w", dst, err) + + opt.FromPattern = "certificate_chain" + opt.ToPath = daemonenv.CertChainFile() + if err := certSec.InstallKeyTo(opt); err != nil { + return fmt.Errorf("install cert files can't dump cert certificate_chain to %s: %w", opt.ToPath, err) } else { - t.log.Infof("install cert files dump cert certificate_chain to %s", dst) + t.log.Infof("install cert files dump cert certificate_chain to %s", opt.ToPath) } return nil diff --git a/drivers/resvol/data.go b/drivers/resvol/data.go index 06665516a..1231cac3c 100644 --- a/drivers/resvol/data.go +++ b/drivers/resvol/data.go @@ -20,13 +20,6 @@ type ( // This type exists to host the parsing functions. Reference string - // Metadata is the result of a Reference parsing. - Metadata struct { - ToPath string - FromKey string - FromStore naming.Path - } - // SigRoute is a relation between a signal number and the id of a resource supporting signaling SigRoute struct { Signum syscall.Signal @@ -34,10 +27,6 @@ type ( } ) -func (t Metadata) IsEmpty() bool { - return t.ToPath == "" && t.FromKey == "" -} - func (t T) getRefs() []string { refs := make([]string, 0) refs = append(refs, t.getRefsByKind(naming.KindSec)...) @@ -56,23 +45,23 @@ func (t T) getRefsByKind(filter naming.Kind) []string { return refs } -func (t T) getMetadata() []Metadata { - l := make([]Metadata, 0) +func (t T) getMetadata() []object.KVInstall { + l := make([]object.KVInstall, 0) l = append(l, t.getMetadataByKind(naming.KindSec)...) l = append(l, t.getMetadataByKind(naming.KindCfg)...) return l } -func (t T) getMetadataByKind(kind naming.Kind) []Metadata { - l := make([]Metadata, 0) +func (t T) getMetadataByKind(kind naming.Kind) []object.KVInstall { + l := make([]object.KVInstall, 0) refs := t.getRefsByKind(kind) if len(refs) == 0 { // avoid the Head() call when possible - return []Metadata{} + return l } head := t.Head() if head == "" { - return []Metadata{} + return l } for _, ref := range refs { md := t.parseReference(ref, kind, head) @@ -91,22 +80,22 @@ func (t T) HasMetadata(p naming.Path, k string) bool { if md.FromStore.Name != p.Name { continue } - if k == "" || md.FromKey == k { + if k == "" || md.FromPattern == k { return true } } return false } -func (t T) parseReference(s string, filter naming.Kind, head string) Metadata { +func (t T) parseReference(s string, filter naming.Kind, head string) object.KVInstall { if head == "" { - return Metadata{} + return object.KVInstall{} } // s = "sec/s1/k[12]:/here/" l := strings.SplitN(s, ":", 2) if len(l) != 2 { - return Metadata{} + return object.KVInstall{} } toPath := filepath.Join(head, l[1]) // toPath = "/here" @@ -127,19 +116,19 @@ func (t T) parseReference(s string, filter naming.Kind, head string) Metadata { kind = naming.KindUsr from = from[4:] if filter == naming.KindCfg { - return Metadata{} + return object.KVInstall{} } case strings.HasPrefix(from, "sec/"): kind = naming.KindSec from = from[4:] if filter == naming.KindCfg { - return Metadata{} + return object.KVInstall{} } case strings.HasPrefix(from, "cfg/"): kind = naming.KindCfg from = from[4:] if filter == naming.KindSec { - return Metadata{} + return object.KVInstall{} } } // kind = path.KindSec @@ -147,15 +136,22 @@ func (t T) parseReference(s string, filter naming.Kind, head string) Metadata { l = strings.SplitN(from, "/", 2) if len(l) != 2 { - return Metadata{} + return object.KVInstall{} } if p, err := naming.NewPath(t.Path.Namespace, kind, l[0]); err != nil { - return Metadata{} + return object.KVInstall{} } else { - return Metadata{ - ToPath: toPath, // /here - FromKey: l[1], // k[12] - FromStore: p, // /sec/s1 + return object.KVInstall{ + ToPath: toPath, // /here + ToHead: head, + FromPattern: l[1], // k[12] + FromStore: p, // /sec/s1 + AccessControl: object.KVInstallAccessControl{ + User: t.User, + Group: t.Group, + Perm: t.Perm, + DirPerm: t.DirPerm, + }, } } } @@ -163,7 +159,7 @@ func (t T) parseReference(s string, filter naming.Kind, head string) Metadata { func (t *T) statusData() { for _, md := range t.getMetadata() { if !md.FromStore.Exists() { - t.StatusLog().Warn("store %s does not exist: key %s data can not be installed in the volume", md.FromStore, md.FromKey) + t.StatusLog().Warn("store %s does not exist: key %s data can not be installed in the volume", md.FromStore, md.FromPattern) continue } keystore, err := object.NewKeystore(md.FromStore, object.WithVolatile(true)) @@ -171,13 +167,13 @@ func (t *T) statusData() { t.StatusLog().Warn("store %s init error: %s", md.FromStore, err) continue } - matches, err := keystore.MatchingKeys(md.FromKey) + matches, err := keystore.MatchingKeys(md.FromPattern) if err != nil { - t.StatusLog().Error("store %s keymatch %s: %s", md.FromStore, md.FromKey, err) + t.StatusLog().Error("store %s keymatch %s: %s", md.FromStore, md.FromPattern, err) continue } if len(matches) == 0 { - t.StatusLog().Warn("store %s has no keys matching %s: data can not be installed in the volume", md.FromStore, md.FromKey) + t.StatusLog().Warn("store %s has no keys matching %s: data can not be installed in the volume", md.FromStore, md.FromPattern) } } } @@ -242,29 +238,17 @@ func (t T) InstallDataByKind(filter naming.Kind) (bool, error) { for _, md := range t.getMetadataByKind(filter) { if !md.FromStore.Exists() { - t.Log().Warnf("store %s does not exist: key %s data can not be installed in the volume", md.FromStore, md.FromKey) + t.Log().Warnf("store %s does not exist: key %s data can not be installed in the volume", md.FromStore, md.FromPattern) continue } keystore, err := object.NewKeystore(md.FromStore, object.WithVolatile(true)) if err != nil { t.Log().Warnf("store %s init error: %s", md.FromStore, err) } - var matches []string - matches, err = keystore.MatchingKeys(md.FromKey) - if err != nil { - t.Log().Warnf("store %s keymatch %s: %s", md.FromStore, md.FromKey, err) - continue - } - if len(matches) == 0 { - t.Log().Warnf("store %s has no keys matching %s: data can not be installed in the volume", md.FromStore, md.FromKey) - continue - } - for _, k := range matches { - if err = keystore.InstallKeyTo(k, md.ToPath, t.Perm, t.DirPerm, t.User, t.Group); err != nil { - return changed, err - } - changed = true + if err = keystore.InstallKeyTo(md); err != nil { + return changed, err } + changed = true } return changed, nil } From 91d1e7dcf54d7c187eb7cc3fce02d39404b33bf5 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 15 Nov 2024 17:13:39 +0100 Subject: [PATCH 6/7] Split tui main file into per view files And change the instance view to be a selectable resource table. Add resource actions: enable, disable, stop, start, provision, unprovision, run, restart --- core/tui/instance.go | 148 ++++++++++ core/tui/keys.go | 67 +++++ core/tui/main.go | 677 +++++++++++++++---------------------------- core/tui/objects.go | 338 +++++++++++++++++++++ 4 files changed, 794 insertions(+), 436 deletions(-) create mode 100644 core/tui/instance.go create mode 100644 core/tui/keys.go create mode 100644 core/tui/objects.go diff --git a/core/tui/instance.go b/core/tui/instance.go new file mode 100644 index 000000000..417e8f779 --- /dev/null +++ b/core/tui/instance.go @@ -0,0 +1,148 @@ +package tui + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/opensvc/om3/core/colorstatus" + "github.com/opensvc/om3/core/instance" + "github.com/opensvc/om3/core/rawconfig" + "github.com/opensvc/om3/core/resource" + "github.com/rivo/tview" +) + +func (t *App) updateInstanceView() { + if t.viewPath.IsZero() { + return + } + if t.viewNode == "" { + return + } + if t.skipIfInstanceNotUpdated() { + return + } + digest := t.Frame.Current.GetObjectStatus(t.viewPath) + + title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) + + table := tview.NewTable() + table.SetTitle(title) + table.SetBorder(false) + table.SetEvaluateAllRows(true) + + table.SetSelectionChangedFunc(func(row, col int) { + t.viewRID = "" + if row == 0 { + return + } + if col == 0 { + t.viewRID = table.GetCell(row, col).Text + } + + }) + + selectedFunc := func(row, col int) { + cell := table.GetCell(row, col) + rid := table.GetCell(row, 0).Text + var selected *bool + switch { + case row == 0: + case col == 0: + v := t.toggleRID(t.viewPath.String(), t.viewNode, rid) + selected = &v + } + if selected != nil && *selected { + cell.SetBackgroundColor(colorSelected) + } else { + cell.SetBackgroundColor(colorNone) + } + } + + table.SetSelectedFunc(selectedFunc) + + setSelection := func(table *tview.Table) { + row, col := table.GetSelection() + cell := table.GetCell(row, col) + cell.SetBackgroundColor(colorSelected) + table.SetCell(row, col, cell) + selectedFunc(row, col) + } + + selectAll := func() { + for i := 1; i < table.GetRowCount(); i++ { + selectedFunc(i, 0) + } + } + + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyCtrlA: + selectAll() + case tcell.KeyEnter: + //onEnter(event) + return nil // prevents the default select behaviour + } + switch event.Rune() { + case ' ': + setSelection(table) + } + return event + }) + + cellResourceId := func(rid string) *tview.TableCell { + cell := tview.NewTableCell(rid).SetAttributes(tcell.AttrBold).SetSelectable(true) + if t.isResourceSelected(t.viewPath.String(), t.viewNode, rid) { + cell.SetBackgroundColor(colorSelected) + } + return cell + } + cellLog := func(entry *resource.StatusLogEntry) *tview.TableCell { + s := entry.String() + switch entry.Level { + case "error": + s = tview.TranslateANSI(rawconfig.Colorize.Error(s)) + case "warn": + s = tview.TranslateANSI(rawconfig.Colorize.Warning(s)) + } + return tview.NewTableCell(s).SetSelectable(false) + } + cellFlags := func(resourceStatus resource.Status, instanceState instance.States) *tview.TableCell { + s := instanceState.Status.ResourceFlagsString(*resourceStatus.ResourceID, resourceStatus) + s += instanceState.Monitor.ResourceFlagRestartString(*resourceStatus.ResourceID, resourceStatus) + s = tview.TranslateANSI(s) + return tview.NewTableCell(s).SetSelectable(false) + } + + i := 0 + instanceState, ok := digest.Instances.ByNode()[t.viewNode] + if !ok { + goto end + } + + table.SetCell(i, 0, tview.NewTableCell("RID").SetTextColor(colorTitle).SetSelectable(false)) + table.SetCell(i, 1, tview.NewTableCell("FLAGS").SetTextColor(colorTitle).SetSelectable(false)) + table.SetCell(i, 2, tview.NewTableCell("STATUS").SetTextColor(colorTitle).SetSelectable(false)) + table.SetCell(i, 3, tview.NewTableCell("LABEL").SetTextColor(colorTitle).SetSelectable(false)) + for _, resourceStatus := range instanceState.Status.SortedResources() { + i += 1 + table.SetCell(i, 0, cellResourceId(resourceStatus.ResourceID.String())) + table.SetCell(i, 1, cellFlags(resourceStatus, instanceState)) + table.SetCell(i, 2, tview.NewTableCell(tview.TranslateANSI(colorstatus.Sprint(resourceStatus.Status, rawconfig.Colorize))).SetSelectable(false)) + table.SetCell(i, 3, tview.NewTableCell(resourceStatus.Label).SetSelectable(false)) + for _, entry := range resourceStatus.Log { + i += 1 + table.SetCell(i, 0, tview.NewTableCell("").SetSelectable(false)) + table.SetCell(i, 1, tview.NewTableCell("").SetSelectable(false)) + table.SetCell(i, 2, tview.NewTableCell("").SetSelectable(false)) + table.SetCell(i, 3, cellLog(entry)) + } + } + +end: + t.flex.Clear() + t.flex.AddItem(t.head, 1, 0, false) + t.flex.AddItem(table, 0, 1, true) + t.app.SetFocus(table) +} diff --git a/core/tui/keys.go b/core/tui/keys.go new file mode 100644 index 000000000..fd275e3f2 --- /dev/null +++ b/core/tui/keys.go @@ -0,0 +1,67 @@ +package tui + +import ( + "context" + "fmt" + "net/http" + + "github.com/gdamore/tcell/v2" + "github.com/opensvc/om3/util/sizeconv" + "github.com/rivo/tview" +) + +func (t *App) initKeysTable() { + table := tview.NewTable() + table.SetBorder(false) + + onEnter := func(event *tcell.EventKey) { + t.nav(viewKey) + } + + table.SetSelectionChangedFunc(func(row, col int) { + t.viewKey = "" + if row == 0 { + return + } + if col == 0 { + t.viewKey = table.GetCell(row, col).Text + } + + }) + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, false) + case tcell.KeyEnter: + onEnter(event) + return nil // prevents the default select behaviour + } + return event + }) + t.keys = table +} + +func (t *App) updateKeysView() { + if t.viewPath.IsZero() { + return + } + if t.skipIfConfigNotUpdated() { + return + } + resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) + if err != nil { + return + } + if resp.StatusCode() != http.StatusOK { + return + } + t.keys.Clear() + t.keys.SetTitle(fmt.Sprintf("%s keys", t.viewPath)) + t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle).SetSelectable(false)) + t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle).SetSelectable(false)) + for i, key := range resp.JSON200.Items { + row := 1 + i + t.keys.SetCell(row, 0, tview.NewTableCell(key.Key).SetSelectable(true)) + t.keys.SetCell(row, 1, tview.NewTableCell(sizeconv.BSizeCompact(float64(key.Size))).SetSelectable(false)) + } +} diff --git a/core/tui/main.go b/core/tui/main.go index 14f174a15..87ba315e3 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -18,7 +18,6 @@ import ( "github.com/opensvc/om3/core/client" "github.com/opensvc/om3/core/clientcontext" "github.com/opensvc/om3/core/clusterdump" - "github.com/opensvc/om3/core/colorstatus" "github.com/opensvc/om3/core/event" "github.com/opensvc/om3/core/monitor" "github.com/opensvc/om3/core/naming" @@ -26,10 +25,8 @@ import ( "github.com/opensvc/om3/core/rawconfig" "github.com/opensvc/om3/core/streamlog" "github.com/opensvc/om3/daemon/api" - "github.com/opensvc/om3/daemon/daemonsubsystem" "github.com/opensvc/om3/daemon/msgbus" "github.com/opensvc/om3/util/hostname" - "github.com/opensvc/om3/util/sizeconv" "github.com/rivo/tview" "github.com/rs/zerolog" ) @@ -65,6 +62,7 @@ type ( viewPath naming.Path viewNode string viewKey string + viewRID string lastUpdatedAt time.Time @@ -77,6 +75,7 @@ type ( selectedNodes map[string]any selectedPaths map[string]any selectedInstances map[[2]string]any + selectedRIDs map[[3]string]any errC chan error restartC chan error @@ -218,15 +217,28 @@ func NewApp() *App { selectedNodes: make(map[string]any), selectedPaths: make(map[string]any), selectedInstances: make(map[[2]string]any), + selectedRIDs: make(map[[3]string]any), errC: make(chan error), restartC: make(chan error), } } +func (t *App) resetSelected() int { + switch t.focus() { + case viewInstance: + n := len(t.selectedRIDs) + t.resetSelectedRIDs() + return n + default: + return 0 + } +} + func (t *App) resetAllSelected() { t.resetSelectedNodes() t.resetSelectedPaths() t.resetSelectedInstances() + t.resetSelectedRIDs() } func (t *App) updateKeyTextView() { @@ -259,132 +271,6 @@ func (t *App) updateKeyTextView() { fmt.Fprint(t.textView, text) } -func (t *App) initKeysTable() { - table := tview.NewTable() - table.SetBorder(false) - - onEnter := func(event *tcell.EventKey) { - t.nav(viewKey) - } - - table.SetSelectionChangedFunc(func(row, col int) { - t.viewKey = "" - if row == 0 { - return - } - if col == 0 { - t.viewKey = table.GetCell(row, col).Text - } - - }) - table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: - table.SetSelectable(true, false) - case tcell.KeyEnter: - onEnter(event) - return nil // prevents the default select behaviour - } - return event - }) - t.keys = table -} - -func (t *App) initObjectsTable() { - table := tview.NewTable() - table.SetEvaluateAllRows(true) - - onEnter := func(event *tcell.EventKey) { - row, col := table.GetSelection() - switch { - case !t.viewPath.IsZero() && t.viewNode != "": - t.initTextView() - t.nav(viewInstance) - case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: - t.nav(viewKeys) - case row == 0 && col == 1: - t.listContexts() - } - } - - selectedFunc := func(row, col int) { - cell := table.GetCell(row, col) - path := table.GetCell(row, 0).Text - node := table.GetCell(0, col).Text - var selected *bool - switch { - case row == 0 && col >= t.firstInstanceCol: - v := t.toggleNode(node) - selected = &v - case row < t.firstObjectRow: - case col == 0: - v := t.togglePath(path) - selected = &v - case col >= t.firstInstanceCol: - v := t.toggleInstance(path, node) - selected = &v - } - if selected != nil && *selected { - cell.SetBackgroundColor(colorSelected) - } else { - cell.SetBackgroundColor(colorNone) - } - } - - table.SetSelectedFunc(selectedFunc) - - setSelection := func(table *tview.Table) { - row, col := table.GetSelection() - cell := table.GetCell(row, col) - cell.SetBackgroundColor(colorSelected) - table.SetCell(row, col, cell) - selectedFunc(row, col) - } - - selectAll := func() { - for i := t.firstObjectRow; i < table.GetRowCount(); i++ { - selectedFunc(i, 0) - } - } - - table.SetSelectionChangedFunc(func(row, col int) { - t.viewNode = "" - t.viewPath = naming.Path{} - if row >= t.firstObjectRow { - path := t.objects.GetCell(row, 0).Text - p, err := naming.ParsePath(path) - if err != nil { - return - } - t.viewPath = p - } - if col >= t.firstInstanceCol { - t.viewNode = t.objects.GetCell(0, col).Text - } - }) - table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: - table.SetSelectable(true, true) - case tcell.KeyESC: - t.resetSelectedNodes() - t.resetSelectedPaths() - t.resetSelectedInstances() - case tcell.KeyCtrlA: - selectAll() - case tcell.KeyEnter: - onEnter(event) - return nil // prevents the default select behaviour - } - switch event.Rune() { - case ' ': - setSelection(table) - } - return event - }) - t.objects = table -} - func (t *App) initHeadTextView() { t.head = tview.NewTable() t.head.SetBorder(false) @@ -421,6 +307,9 @@ func (t *App) initApp() { t.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyESC: + if n := t.resetSelected(); n > 0 { + return nil + } t.back() case tcell.KeyBacktab: colorHead2-- @@ -443,7 +332,7 @@ func (t *App) initApp() { case 'h': t.onRuneH(event) case 'l': - t.onRuneL(event) + t.nav(viewLog) case 'q': t.stop() case 'r': @@ -736,234 +625,18 @@ func (t *App) paths() []string { } -func (t *App) updateObjects() { - nodesCells := func(row int, selectable bool) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - t.objects.SetCell(row, t.firstInstanceCol+i, t.cellNode(nodename, selectable)) - } - } - - nodesScoreCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeScore(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesLoadCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeLoad(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesMemCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeMem(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesSwapCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeSwap(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesStateCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeStates(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesHbCells := func(row int) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeHbMode(nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesHb1Cells := func(row int, stream daemonsubsystem.HeartbeatStream) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeHbStatus(stream, nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - nodesArbitratorCells := func(row int, arbitratorName string) { - for i, nodename := range t.Current.Cluster.Config.Nodes { - s := tview.TranslateANSI(t.StrNodeArbitratorStatus(arbitratorName, nodename)) - t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) - } - } - - t.lastDraw = time.Now() - - t.objects.Clear() - t.objects.SetTitle(fmt.Sprintf("%s objects", t.Frame.Selector)) - - row := 0 - t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell(t.Current.Cluster.Config.Name).SetSelectable(true)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("NODE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesCells(row, true) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("EVENT").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", t.eventCount)).SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("SCORE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesScoreCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("LAST").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("0s").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("│LOAD").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesLoadCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("│MEM").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesMemCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("│SWAP").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesSwapCells(row) - - if len(t.Current.Cluster.Config.Nodes) > 1 { - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("HB").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesHbCells(row) - - for _, hbStatus := range t.Current.Cluster.Node[t.Frame.Nodename].Daemon.Heartbeat.Streams { - name := "│" + strings.TrimPrefix(hbStatus.ID, "hb#") + monitor.StrThreadAlerts(hbStatus.Alerts) - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell(name).SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesHb1Cells(row, hbStatus) - } - } - - arbitratorNames := t.Current.ArbitratorNames() - if len(arbitratorNames) > 0 { - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("ARBITRATORS").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - - for _, arbitratorName := range arbitratorNames { - name := "│" + arbitratorName - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell(name).SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesArbitratorCells(row, arbitratorName) - } - } - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("STATE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesStateCells(row) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("").SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("┼").SetTextColor(colorTitle).SetSelectable(false)) - - row++ - t.objects.SetCell(row, 0, tview.NewTableCell("PATH").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 1, tview.NewTableCell("AVAIL").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 2, tview.NewTableCell("ORCHESTRATE").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 3, tview.NewTableCell("UP").SetTextColor(colorTitle).SetSelectable(false)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - nodesCells(row, false) - - t.firstObjectRow = row + 1 - - t.objects.SetFixed(t.firstObjectRow, 2) - - for _, path := range t.paths() { - row++ - t.objects.SetCell(row, 0, t.cellObjectPath(path)) - t.objects.SetCell(row, 1, t.cellObjectStatus(path)) - t.objects.SetCell(row, 2, t.cellObjectOrchestrate(path)) - t.objects.SetCell(row, 3, t.cellObjectRunning(path)) - t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) - for j, nodename := range t.Current.Cluster.Config.Nodes { - t.objects.SetCell(row, 5+j, t.cellInstanceStatus(path, nodename)) - } - } -} - -func (t *App) cellObjectOrchestrate(path string) *tview.TableCell { - s := t.Current.Cluster.Object[path].Orchestrate - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellObjectRunning(path string) *tview.TableCell { - s := tview.TranslateANSI(t.StrObjectRunning(path)) - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellObjectStatus(path string) *tview.TableCell { - s := tview.TranslateANSI(monitor.StrObjectStatus(t.Current.Cluster.Object[path])) - return tview.NewTableCell(s).SetSelectable(false) -} - -func (t *App) cellInstanceStatus(path, node string) *tview.TableCell { - s := tview.TranslateANSI(t.StrObjectInstance(path, node, t.Current.Cluster.Object[path].Scope)) - cell := tview.NewTableCell(s) - if t.isInstanceSelected(path, node) { - cell.SetBackgroundColor(colorSelected) - } - return cell -} - -func (t *App) cellNode(node string, selectable bool) *tview.TableCell { - cell := tview.NewTableCell(node).SetAttributes(tcell.AttrBold).SetSelectable(selectable) - if selectable && t.isNodeSelected(node) { - cell.SetBackgroundColor(colorSelected) - } - return cell -} - -func (t *App) cellObjectPath(path string) *tview.TableCell { - cell := tview.NewTableCell(path).SetAttributes(tcell.AttrBold) - if t.isPathSelected(path) { - cell.SetBackgroundColor(colorSelected) +func (t *App) toggleRID(path, node, rid string) bool { + key := [3]string{path, node, rid} + if _, ok := t.selectedRIDs[key]; ok { + delete(t.selectedRIDs, key) + return false + } else { + t.selectedRIDs[key] = nil + t.resetSelectedPaths() + t.resetSelectedNodes() + t.resetSelectedInstances() + return true } - return cell } func (t *App) toggleInstance(path, node string) bool { @@ -975,6 +648,7 @@ func (t *App) toggleInstance(path, node string) bool { t.selectedInstances[key] = nil t.resetSelectedPaths() t.resetSelectedNodes() + t.resetSelectedRIDs() return true } } @@ -987,6 +661,7 @@ func (t *App) togglePath(key string) bool { t.selectedPaths[key] = nil t.resetSelectedInstances() t.resetSelectedNodes() + t.resetSelectedRIDs() return true } } @@ -999,10 +674,32 @@ func (t *App) toggleNode(key string) bool { t.selectedNodes[key] = nil t.resetSelectedInstances() t.resetSelectedPaths() + t.resetSelectedRIDs() return true } } +func (t *App) resetSelectedRIDs() { + if len(t.selectedRIDs) == 0 { + return + } + t.selectedRIDs = make(map[[3]string]any) + if t.flex.GetItemCount() < 2 { + return + } + primitive := t.flex.GetItem(1) + table, ok := primitive.(*tview.Table) + if !ok { + return + } + if table.GetCell(0, 0).Text != "RID" { + return + } + for i := 1; i < table.GetRowCount(); i += 1 { + table.GetCell(i, 0).SetBackgroundColor(colorNone) + } +} + func (t *App) resetSelectedNodes() { if len(t.selectedNodes) == 0 { return @@ -1035,6 +732,11 @@ func (t *App) resetSelectedPaths() { } } +func (t *App) isResourceSelected(path, node, rid string) bool { + _, ok := t.selectedRIDs[[3]string{path, node, rid}] + return ok +} + func (t *App) isInstanceSelected(path, node string) bool { _, ok := t.selectedInstances[[2]string{path, node}] return ok @@ -1095,7 +797,29 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { case "restart": t.actionRestart(paths) default: - t.errorf("unknown command: %s", action) + t.errorf("unknown object action: %s", action) + } + } + resourceAction := func(args []string, keys map[[3]string]any) { + switch args[0] { + case "stop": + t.actionResourceStop(keys) + case "start": + t.actionResourceStart(keys) + case "provision": + t.actionResourceProvision(keys) + case "unprovision": + t.actionResourceUnprovision(keys) + case "restart": + t.actionResourceRestart(keys) + case "run": + t.actionResourceRun(keys) + case "enable": + t.actionResourceEnable(keys) + case "disable": + t.actionResourceDisable(keys) + default: + t.errorf("unknown resource action: %s", args[0]) } } instanceAction := func(action string, keys map[[2]string]any) { @@ -1123,7 +847,7 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { // case "clear": // t.actionInstanceClear(keys) default: - t.errorf("unknown command: %s", action) + t.errorf("unknown instance action: %s", action) } } nodeAction := func(args []string, nodes map[string]any) { @@ -1143,7 +867,7 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { case "drain": t.actionNodeDrain(nodes) default: - t.errorf("unknown command: %s", args[0]) + t.errorf("unknown node action: %s", args[0]) } } t.command = tview.NewInputField(). @@ -1209,6 +933,8 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { } action = args[1] switch { + case len(t.selectedRIDs) > 0: + resourceAction(args[1:], t.selectedRIDs) case len(t.selectedPaths) > 0: objectAction(action, t.selectedPaths) case len(t.selectedInstances) > 0: @@ -1218,6 +944,14 @@ func (t *App) onRuneColumn(event *tcell.EventKey) { default: row, col := t.objects.GetSelection() switch { + case t.focus() == viewInstance && row > 1: + if table, ok := t.flex.GetItem(1).(*tview.Table); ok { + row, col := table.GetSelection() + rid := table.GetCell(row, col).Text + selection := make(map[[3]string]any) + selection[[3]string{t.viewPath.String(), t.viewNode, rid}] = nil + resourceAction(args[1:], selection) + } case row == 0 && col == 1: clusterAction(action) case row == 0 && col >= t.firstInstanceCol: @@ -1447,6 +1181,148 @@ func (t *App) actionInstanceSwitch(keys map[[2]string]any) { } } +func groupByInstance(in map[[3]string]any) map[[2]string][]string { + out := make(map[[2]string][]string) + for key := range in { + k := [2]string{key[0], key[1]} + rid := key[2] + if l, ok := out[k]; ok { + l = append(l, rid) + out[k] = l + } else { + l := []string{rid} + out[k] = l + } + } + return out +} + +func (t *App) actionResourceRestart(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + rid := strings.Join(rids, ",") + params := api.PostInstanceActionRestartParams{Rid: &rid} + _, _ = t.client.PostInstanceActionRestartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceEnable(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + unset := make(api.InQueryUnsets, len(rids)) + for i, rid := range rids { + unset[i] = rid + ".disable" + } + params := api.PostObjectConfigUpdateParams{Unset: &unset} + _, _ = t.client.PostObjectConfigUpdateWithResponse(ctx, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceDisable(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + set := make(api.InQuerySets, len(rids)) + for i, rid := range rids { + set[i] = rid + ".disable=true" + } + params := api.PostObjectConfigUpdateParams{Set: &set} + _, _ = t.client.PostObjectConfigUpdateWithResponse(ctx, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceRun(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + rid := strings.Join(rids, ",") + confirm := true + params := api.PostInstanceActionRunParams{Rid: &rid, Confirm: &confirm} + _, _ = t.client.PostInstanceActionRunWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceProvision(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + rid := strings.Join(rids, ",") + params := api.PostInstanceActionProvisionParams{Rid: &rid} + _, _ = t.client.PostInstanceActionProvisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceUnprovision(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + + rid := strings.Join(rids, ",") + params := api.PostInstanceActionUnprovisionParams{Rid: &rid} + _, _ = t.client.PostInstanceActionUnprovisionWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceStart(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + rid := strings.Join(rids, ",") + params := api.PostInstanceActionStartParams{Rid: &rid} + _, _ = t.client.PostInstanceActionStartWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + +func (t *App) actionResourceStop(keys map[[3]string]any) { + ctx := context.Background() + for key, rids := range groupByInstance(keys) { + path := key[0] + node := key[1] + p, err := naming.ParsePath(path) + if err != nil { + continue + } + rid := strings.Join(rids, ",") + params := api.PostInstanceActionStopParams{Rid: &rid} + _, _ = t.client.PostInstanceActionStopWithResponse(ctx, node, p.Namespace, p.Kind, p.Name, ¶ms) + } +} + func (t *App) actionStop(paths map[string]any) { ctx := context.Background() for path, _ := range paths { @@ -1610,6 +1486,9 @@ func (t *App) onRuneH(event *tcell.EventKey) { clear, delete, freeze, provision, refresh, start, stop, switch, unfreeze, unprovision + resource actions: + disable, enable, provision, run, start, stop, unprovision + node actions: drain freeze, unfreeze @@ -1643,7 +1522,7 @@ func (t *App) onRuneH(event *tcell.EventKey) { t.app.SetFocus(v) } -func (t *App) onRuneL(event *tcell.EventKey) { +func (t *App) updateLogTextView() { title := func() string { switch { case !t.viewPath.IsZero() && t.viewNode != "": @@ -1657,14 +1536,12 @@ func (t *App) onRuneL(event *tcell.EventKey) { } } - t.initTextView() t.textView.SetTitle(title()) t.textView.SetDynamicColors(true) t.textView.SetChangedFunc(func() { t.textView.ScrollToEnd() }) t.textView.Clear() - t.nav(viewLog) lines := 50 follow := true @@ -1756,79 +1633,6 @@ func (t *App) skipIfInstanceNotUpdated() bool { return true } -func (t *App) updateKeysView() { - if t.viewPath.IsZero() { - return - } - if t.skipIfConfigNotUpdated() { - return - } - resp, err := t.client.GetObjectKVStoreKeysWithResponse(context.Background(), t.viewPath.Namespace, t.viewPath.Kind, t.viewPath.Name) - if err != nil { - return - } - if resp.StatusCode() != http.StatusOK { - return - } - t.keys.Clear() - t.keys.SetTitle(fmt.Sprintf("%s keys", t.viewPath)) - t.keys.SetCell(0, 0, tview.NewTableCell("NAME").SetTextColor(colorTitle).SetSelectable(false)) - t.keys.SetCell(0, 1, tview.NewTableCell("SIZE").SetTextColor(colorTitle).SetSelectable(false)) - for i, key := range resp.JSON200.Items { - row := 1 + i - t.keys.SetCell(row, 0, tview.NewTableCell(key.Key).SetSelectable(true)) - t.keys.SetCell(row, 1, tview.NewTableCell(sizeconv.BSizeCompact(float64(key.Size))).SetSelectable(false)) - } -} - -func (t *App) updateInstanceView() { - if t.viewPath.IsZero() { - return - } - if t.viewNode == "" { - return - } - if t.skipIfInstanceNotUpdated() { - return - } - digest := t.Frame.Current.GetObjectStatus(t.viewPath) - text := tview.TranslateANSI(digest.Render([]string{t.viewNode})) - _ = text - /* - t.initTextView() - title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) - t.textView.SetDynamicColors(true) - t.textView.SetTitle(title) - t.textView.Clear() - fmt.Fprint(t.textView, text) - */ - title := fmt.Sprintf("%s@%s status", t.viewPath, t.viewNode) - table := tview.NewTable() - table.SetTitle(title) - table.SetBorder(false) - - table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: - table.SetSelectable(true, true) - case tcell.KeyEnter: - //onEnter(event) - return nil // prevents the default select behaviour - } - return event - }) - - table.SetCell(0, 0, tview.NewTableCell(t.viewPath.String()).SetAttributes(tcell.AttrBold).SetSelectable(true)) - table.SetCell(0, 1, tview.NewTableCell("").SetSelectable(false)) - table.SetCell(0, 2, tview.NewTableCell(tview.TranslateANSI(colorstatus.Sprint(digest.Object.Avail, rawconfig.Colorize))).SetSelectable(false)) - table.SetCell(0, 3, tview.NewTableCell(tview.TranslateANSI(digest.ObjectWarningsString())).SetSelectable(false)) - - t.flex.Clear() - t.flex.AddItem(t.head, 1, 0, false) - t.flex.AddItem(table, 0, 1, true) - t.app.SetFocus(table) -} - func (t *App) onRuneE(event *tcell.EventKey) { t.app.Suspend(func() { row, col := t.objects.GetSelection() @@ -2014,6 +1818,7 @@ func (t *App) navFromTo(from, to viewId) { t.initTextView() t.flex.AddItem(t.textView, 0, 1, true) t.app.SetFocus(t.textView) + t.updateLogTextView() case viewConfig: t.initTextView() t.flex.AddItem(t.textView, 0, 1, true) diff --git a/core/tui/objects.go b/core/tui/objects.go new file mode 100644 index 000000000..bb74bb99d --- /dev/null +++ b/core/tui/objects.go @@ -0,0 +1,338 @@ +package tui + +import ( + "fmt" + "strings" + "time" + + "github.com/gdamore/tcell/v2" + "github.com/opensvc/om3/core/monitor" + "github.com/opensvc/om3/core/naming" + "github.com/opensvc/om3/daemon/daemonsubsystem" + "github.com/rivo/tview" +) + +func (t *App) initObjectsTable() { + table := tview.NewTable() + table.SetEvaluateAllRows(true) + + onEnter := func(event *tcell.EventKey) { + row, col := table.GetSelection() + switch { + case !t.viewPath.IsZero() && t.viewNode != "": + t.initTextView() + t.nav(viewInstance) + case t.viewPath.Kind == naming.KindCfg || t.viewPath.Kind == naming.KindSec: + t.nav(viewKeys) + case row == 0 && col == 1: + t.listContexts() + } + } + + selectedFunc := func(row, col int) { + cell := table.GetCell(row, col) + path := table.GetCell(row, 0).Text + node := table.GetCell(0, col).Text + var selected *bool + switch { + case row == 0 && col >= t.firstInstanceCol: + v := t.toggleNode(node) + selected = &v + case row < t.firstObjectRow: + case col == 0: + v := t.togglePath(path) + selected = &v + case col >= t.firstInstanceCol: + v := t.toggleInstance(path, node) + selected = &v + } + if selected != nil && *selected { + cell.SetBackgroundColor(colorSelected) + } else { + cell.SetBackgroundColor(colorNone) + } + } + + table.SetSelectedFunc(selectedFunc) + + setSelection := func(table *tview.Table) { + row, col := table.GetSelection() + cell := table.GetCell(row, col) + cell.SetBackgroundColor(colorSelected) + table.SetCell(row, col, cell) + selectedFunc(row, col) + } + + selectAll := func() { + for i := t.firstObjectRow; i < table.GetRowCount(); i++ { + selectedFunc(i, 0) + } + } + + table.SetSelectionChangedFunc(func(row, col int) { + t.viewNode = "" + t.viewPath = naming.Path{} + if row >= t.firstObjectRow { + path := t.objects.GetCell(row, 0).Text + p, err := naming.ParsePath(path) + if err != nil { + return + } + t.viewPath = p + } + if col >= t.firstInstanceCol { + t.viewNode = t.objects.GetCell(0, col).Text + } + }) + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown: + table.SetSelectable(true, true) + case tcell.KeyESC: + t.resetSelectedNodes() + t.resetSelectedPaths() + t.resetSelectedInstances() + case tcell.KeyCtrlA: + selectAll() + case tcell.KeyEnter: + onEnter(event) + return nil // prevents the default select behaviour + } + switch event.Rune() { + case ' ': + setSelection(table) + } + return event + }) + t.objects = table +} + +func (t *App) updateObjects() { + nodesCells := func(row int, selectable bool) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, t.firstInstanceCol+i, t.cellNode(nodename, selectable)) + } + } + + nodesScoreCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeScore(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesLoadCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeLoad(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesMemCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeMem(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesSwapCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeSwap(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesStateCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeStates(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesHbCells := func(row int) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeHbMode(nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesHb1Cells := func(row int, stream daemonsubsystem.HeartbeatStream) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeHbStatus(stream, nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + nodesArbitratorCells := func(row int, arbitratorName string) { + for i, nodename := range t.Current.Cluster.Config.Nodes { + s := tview.TranslateANSI(t.StrNodeArbitratorStatus(arbitratorName, nodename)) + t.objects.SetCell(row, t.firstInstanceCol+i, tview.NewTableCell(s).SetSelectable(false)) + } + } + + t.lastDraw = time.Now() + + t.objects.Clear() + t.objects.SetTitle(fmt.Sprintf("%s objects", t.Frame.Selector)) + + row := 0 + t.objects.SetCell(row, 0, tview.NewTableCell("CLUSTER").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(t.Current.Cluster.Config.Name).SetSelectable(true)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("NODE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, true) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("EVENT").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", t.eventCount)).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("SCORE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesScoreCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("LAST").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("0s").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("│LOAD").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesLoadCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("│MEM").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesMemCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("│SWAP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesSwapCells(row) + + if len(t.Current.Cluster.Config.Nodes) > 1 { + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("HB").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesHbCells(row) + + for _, hbStatus := range t.Current.Cluster.Node[t.Frame.Nodename].Daemon.Heartbeat.Streams { + name := "│" + strings.TrimPrefix(hbStatus.ID, "hb#") + monitor.StrThreadAlerts(hbStatus.Alerts) + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(name).SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesHb1Cells(row, hbStatus) + } + } + + arbitratorNames := t.Current.ArbitratorNames() + if len(arbitratorNames) > 0 { + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("ARBITRATORS").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + + for _, arbitratorName := range arbitratorNames { + name := "│" + arbitratorName + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell(name).SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesArbitratorCells(row, arbitratorName) + } + } + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("STATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesStateCells(row) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("").SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("┼").SetTextColor(colorTitle).SetSelectable(false)) + + row++ + t.objects.SetCell(row, 0, tview.NewTableCell("PATH").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 1, tview.NewTableCell("AVAIL").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 2, tview.NewTableCell("ORCHESTRATE").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 3, tview.NewTableCell("UP").SetTextColor(colorTitle).SetSelectable(false)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + nodesCells(row, false) + + t.firstObjectRow = row + 1 + + t.objects.SetFixed(t.firstObjectRow, 2) + + for _, path := range t.paths() { + row++ + t.objects.SetCell(row, 0, t.cellObjectPath(path)) + t.objects.SetCell(row, 1, t.cellObjectStatus(path)) + t.objects.SetCell(row, 2, t.cellObjectOrchestrate(path)) + t.objects.SetCell(row, 3, t.cellObjectRunning(path)) + t.objects.SetCell(row, 4, tview.NewTableCell("│").SetTextColor(colorTitle).SetSelectable(false)) + for j, nodename := range t.Current.Cluster.Config.Nodes { + t.objects.SetCell(row, 5+j, t.cellInstanceStatus(path, nodename)) + } + } +} + +func (t *App) cellObjectOrchestrate(path string) *tview.TableCell { + s := t.Current.Cluster.Object[path].Orchestrate + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectRunning(path string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectRunning(path)) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellObjectStatus(path string) *tview.TableCell { + s := tview.TranslateANSI(monitor.StrObjectStatus(t.Current.Cluster.Object[path])) + return tview.NewTableCell(s).SetSelectable(false) +} + +func (t *App) cellInstanceStatus(path, node string) *tview.TableCell { + s := tview.TranslateANSI(t.StrObjectInstance(path, node, t.Current.Cluster.Object[path].Scope)) + cell := tview.NewTableCell(s) + if t.isInstanceSelected(path, node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellNode(node string, selectable bool) *tview.TableCell { + cell := tview.NewTableCell(node).SetAttributes(tcell.AttrBold).SetSelectable(selectable) + if selectable && t.isNodeSelected(node) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} + +func (t *App) cellObjectPath(path string) *tview.TableCell { + cell := tview.NewTableCell(path).SetAttributes(tcell.AttrBold) + if t.isPathSelected(path) { + cell.SetBackgroundColor(colorSelected) + } + return cell +} From 3bcba34d2647a0dfbdd56f7bf80db11b665b7711 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Fri, 15 Nov 2024 17:40:17 +0100 Subject: [PATCH 7/7] Fix the "om keys" command and test --- core/object/keystore_keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/object/keystore_keys.go b/core/object/keystore_keys.go index ce64b3e29..2cc0e9a4b 100644 --- a/core/object/keystore_keys.go +++ b/core/object/keystore_keys.go @@ -56,7 +56,7 @@ func (t *keystore) AllKeys() ([]string, error) { func (t *keystore) MatchingKeys(pattern string) ([]string, error) { data := make([]string, 0) - f := fnmatch.FNM_PATHNAME + f := fnmatch.FNM_PATHNAME | fnmatch.FNM_LEADING_DIR for _, s := range t.config.Keys(dataSectionName) { if pattern == "" || fnmatch.Match(pattern, s, f) {