183 lines
4.1 KiB
Go
183 lines
4.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"pxmon/internal/cluster"
|
|
)
|
|
|
|
type observerCommandOptions struct {
|
|
AllowShellEscape bool
|
|
StatsAutoOnce bool
|
|
BlockBotRun bool
|
|
StripANSI bool
|
|
EmbeddedConsole bool
|
|
}
|
|
|
|
var ansiEscapeRE = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
|
|
|
func stripANSI(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
return ansiEscapeRE.ReplaceAllString(s, "")
|
|
}
|
|
|
|
func runObserverScopedCommand(svc *cluster.Service, configPath, line string, opts observerCommandOptions) (string, int) {
|
|
out, code := runObserverScopedCommandInner(svc, configPath, line, opts)
|
|
if opts.StripANSI {
|
|
out = stripANSI(out)
|
|
}
|
|
return out, code
|
|
}
|
|
|
|
func runObserverScopedCommandInner(svc *cluster.Service, configPath, line string, opts observerCommandOptions) (string, int) {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
return "empty command", 2
|
|
}
|
|
|
|
if strings.HasPrefix(line, "!") {
|
|
if !opts.AllowShellEscape {
|
|
return "shell escape is disabled for this channel", 2
|
|
}
|
|
cmdline := strings.TrimSpace(strings.TrimPrefix(line, "!"))
|
|
if cmdline == "" {
|
|
return "usage: !<shell command>", 2
|
|
}
|
|
cmd := exec.Command("/bin/sh", "-lc", cmdline)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
return string(out), exitErr.ExitCode()
|
|
}
|
|
if len(out) == 0 {
|
|
return err.Error(), 1
|
|
}
|
|
return string(out), 1
|
|
}
|
|
return string(out), 0
|
|
}
|
|
|
|
args, err := parseShellArgs(line)
|
|
if err != nil || len(args) == 0 {
|
|
if err != nil {
|
|
return err.Error(), 2
|
|
}
|
|
return "empty command", 2
|
|
}
|
|
|
|
norm := normalizeObserverConsoleArgs(args)
|
|
if len(norm) == 0 {
|
|
return "empty command", 2
|
|
}
|
|
|
|
if opts.BlockBotRun && len(norm) >= 3 &&
|
|
strings.EqualFold(norm[0], "bot") &&
|
|
strings.EqualFold(norm[1], "telegram") &&
|
|
strings.EqualFold(norm[2], "run") {
|
|
return "running `bot telegram run` from bot channel is blocked", 2
|
|
}
|
|
|
|
if opts.StatsAutoOnce && len(norm) >= 2 &&
|
|
strings.EqualFold(norm[0], "cluster") &&
|
|
strings.EqualFold(norm[1], "stats") &&
|
|
!hasArg(norm[2:], "--once") {
|
|
norm = append(norm, "--once")
|
|
}
|
|
|
|
if handled, out, code := runPluginArgs(svc, norm); handled {
|
|
return out, code
|
|
}
|
|
|
|
return runObserverCommand(configPath, norm, opts.EmbeddedConsole)
|
|
}
|
|
|
|
func runPluginArgs(svc *cluster.Service, args []string) (bool, string, int) {
|
|
if len(args) == 0 || svc == nil {
|
|
return false, "", 0
|
|
}
|
|
|
|
tool := strings.ToLower(strings.TrimSpace(args[0]))
|
|
switch tool {
|
|
case "kvm", "lxc", "lxd", "bird", "frr":
|
|
default:
|
|
return false, "", 0
|
|
}
|
|
|
|
selector, rest, err := parseClusterSelectorArg(args[1:])
|
|
if err != nil {
|
|
return true, err.Error(), 2
|
|
}
|
|
|
|
action := ""
|
|
params := []string{}
|
|
if len(rest) > 0 {
|
|
action = strings.ToLower(strings.TrimSpace(rest[0]))
|
|
params = rest[1:]
|
|
}
|
|
if action == "" {
|
|
switch tool {
|
|
case "kvm", "lxc", "lxd":
|
|
action = "list"
|
|
default:
|
|
action = "status"
|
|
}
|
|
}
|
|
if tool == "kvm" && action == "top" {
|
|
for _, p := range params {
|
|
if strings.EqualFold(strings.TrimSpace(p), "--live") || strings.EqualFold(strings.TrimSpace(p), "-L") {
|
|
return true, "kvm top --live is removed; use `kvm top` for allocated VM specs", 2
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), pluginActionTimeout(tool, action, false))
|
|
defer cancel()
|
|
|
|
out, execErr := svc.RunPluginAction(ctx, selector, tool, action, params)
|
|
if execErr != nil {
|
|
return true, execErr.Error(), 1
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
out = "ok"
|
|
}
|
|
return true, out, 0
|
|
}
|
|
|
|
func pluginActionTimeout(tool, action string, live bool) time.Duration {
|
|
tool = strings.ToLower(strings.TrimSpace(tool))
|
|
action = strings.ToLower(strings.TrimSpace(action))
|
|
switch tool {
|
|
case "kvm":
|
|
switch action {
|
|
case "top":
|
|
if live {
|
|
return 2 * time.Minute
|
|
}
|
|
return 90 * time.Second
|
|
case "net-top", "net":
|
|
if live {
|
|
return 90 * time.Second
|
|
}
|
|
return 60 * time.Second
|
|
default:
|
|
return 45 * time.Second
|
|
}
|
|
case "lxc", "lxd":
|
|
if action == "top" || action == "net-top" || action == "net" {
|
|
return 45 * time.Second
|
|
}
|
|
}
|
|
if live {
|
|
return 35 * time.Second
|
|
}
|
|
return 25 * time.Second
|
|
}
|