mirror of
https://github.com/kemko/xc.git
synced 2026-01-06 18:25:45 +03:00
project move
This commit is contained in:
411
cli/cli.go
Normal file
411
cli/cli.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/viert/xc/config"
|
||||
"github.com/viert/xc/log"
|
||||
"github.com/viert/xc/remote"
|
||||
"github.com/viert/xc/store"
|
||||
"github.com/viert/xc/term"
|
||||
)
|
||||
|
||||
type cmdHandler func(string, string, ...string)
|
||||
type execMode int
|
||||
|
||||
// Cli is the comand line interface object
|
||||
type Cli struct {
|
||||
rl *readline.Instance
|
||||
stopped bool
|
||||
|
||||
handlers map[string]cmdHandler
|
||||
aliases map[string]*alias
|
||||
completer *completer
|
||||
store *store.Store
|
||||
mode execMode
|
||||
user string
|
||||
raiseType remote.RaiseType
|
||||
raisePasswd string
|
||||
remoteTmpDir string
|
||||
delay int
|
||||
sshThreads int
|
||||
connectTimeout int
|
||||
|
||||
exitConfirm bool
|
||||
execConfirm bool
|
||||
prependHostnames bool
|
||||
progressBar bool
|
||||
debug bool
|
||||
|
||||
interpreter string
|
||||
sudoInterpreter string
|
||||
suInterpreter string
|
||||
|
||||
curDir string
|
||||
outputFile *os.File
|
||||
outputFileName string
|
||||
aliasRecursionCount int
|
||||
}
|
||||
|
||||
const (
|
||||
emSerial execMode = iota
|
||||
emParallel
|
||||
emCollapse
|
||||
|
||||
maxAliasRecursion = 10
|
||||
maxSSHThreadsSane = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
whitespace = regexp.MustCompile(`\s+`)
|
||||
modeMap = map[execMode]string{
|
||||
emSerial: "serial",
|
||||
emParallel: "parallel",
|
||||
emCollapse: "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
// New creates a new instance of CLI
|
||||
func New(cfg *config.XCConfig, backend store.Backend) (*Cli, error) {
|
||||
var err error
|
||||
|
||||
err = log.Initialize(cfg.LogFile)
|
||||
if err != nil {
|
||||
term.Errorf("Error initializing logger: %s\n", err)
|
||||
}
|
||||
|
||||
cli := new(Cli)
|
||||
st, err := store.CreateStore(backend)
|
||||
if err != nil {
|
||||
term.Errorf("Error initializing backend: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
cli.store = st
|
||||
cli.stopped = false
|
||||
cli.aliases = make(map[string]*alias)
|
||||
cli.setupCmdHandlers()
|
||||
|
||||
cfg.Readline.AutoComplete = cli.completer
|
||||
cli.rl, err = readline.NewEx(cfg.Readline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cli.exitConfirm = cfg.ExitConfirm
|
||||
cli.execConfirm = cfg.ExecConfirm
|
||||
cli.delay = cfg.Delay
|
||||
cli.user = cfg.User
|
||||
cli.sshThreads = cfg.SSHThreads
|
||||
cli.prependHostnames = cfg.PrependHostnames
|
||||
cli.progressBar = cfg.ProgressBar
|
||||
cli.debug = cfg.Debug
|
||||
cli.connectTimeout = cfg.SSHConnectTimeout
|
||||
cli.remoteTmpDir = cfg.RemoteTmpdir
|
||||
|
||||
// output
|
||||
cli.outputFileName = ""
|
||||
cli.outputFile = nil
|
||||
|
||||
remote.Initialize(cli.sshThreads, cli.user)
|
||||
remote.SetPrependHostnames(cli.prependHostnames)
|
||||
remote.SetRemoteTmpdir(cfg.RemoteTmpdir)
|
||||
remote.SetProgressBar(cli.progressBar)
|
||||
remote.SetConnectTimeout(cli.connectTimeout)
|
||||
remote.SetDebug(cli.debug)
|
||||
|
||||
// interpreter
|
||||
cli.setInterpreter("none", cfg.Interpreter)
|
||||
cli.setInterpreter("sudo", cfg.SudoInterpreter)
|
||||
cli.setInterpreter("su", cfg.SuInterpreter)
|
||||
|
||||
cli.curDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
term.Errorf("Error determining current directory: %s\n", err)
|
||||
cli.curDir = "."
|
||||
}
|
||||
|
||||
cli.doMode("mode", "mode", cfg.Mode)
|
||||
|
||||
cli.setPrompt()
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func (c *Cli) setPrompt() {
|
||||
rts := ""
|
||||
rtbold := false
|
||||
rtcolor := term.CGreen
|
||||
|
||||
pr := fmt.Sprintf("[%s]", strings.Title(modeMap[c.mode]))
|
||||
switch c.mode {
|
||||
case emSerial:
|
||||
if c.delay > 0 {
|
||||
pr = fmt.Sprintf("[Serial:%d]", c.delay)
|
||||
}
|
||||
pr = term.Cyan(pr)
|
||||
case emParallel:
|
||||
pr = term.Yellow(pr)
|
||||
case emCollapse:
|
||||
pr = term.Green(pr)
|
||||
}
|
||||
|
||||
pr += " " + term.Colored(c.user, term.CLightBlue, true)
|
||||
switch c.raiseType {
|
||||
case remote.RTSu:
|
||||
rts = "(su"
|
||||
rtcolor = term.CRed
|
||||
case remote.RTSudo:
|
||||
rts = "(sudo"
|
||||
rtcolor = term.CGreen
|
||||
default:
|
||||
rts = ""
|
||||
}
|
||||
|
||||
if rts != "" {
|
||||
if c.raisePasswd == "" {
|
||||
rts += "*"
|
||||
rtbold = true
|
||||
}
|
||||
rts += ")"
|
||||
pr += term.Colored(rts, rtcolor, rtbold)
|
||||
}
|
||||
pr += "> "
|
||||
c.rl.SetPrompt(pr)
|
||||
}
|
||||
|
||||
func (c *Cli) setInterpreter(iType string, interpreter string) {
|
||||
switch iType {
|
||||
case "none":
|
||||
c.interpreter = interpreter
|
||||
remote.SetInterpreter(interpreter)
|
||||
case "sudo":
|
||||
c.sudoInterpreter = interpreter
|
||||
remote.SetSudoInterpreter(interpreter)
|
||||
case "su":
|
||||
c.suInterpreter = interpreter
|
||||
remote.SetSuInterpreter(interpreter)
|
||||
default:
|
||||
term.Errorf("Invalid raise type: %s\n", iType)
|
||||
}
|
||||
term.Warnf("Using \"%s\" for commands with %s-type raise\n", interpreter, iType)
|
||||
}
|
||||
|
||||
// Finalize closes resources at xc's exit. Must be called explicitly
|
||||
func (c *Cli) Finalize() {
|
||||
if c.outputFile != nil {
|
||||
c.outputFile.Close()
|
||||
c.outputFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// OneCmd is the main method which literally runs one command
|
||||
// according to line given in arguments
|
||||
func (c *Cli) OneCmd(line string) {
|
||||
var args []string
|
||||
var argsLine string
|
||||
|
||||
line = strings.Trim(line, " \n\t")
|
||||
|
||||
cmdRunes, rest := split([]rune(line))
|
||||
cmd := string(cmdRunes)
|
||||
|
||||
if cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if rest == nil {
|
||||
args = make([]string, 0)
|
||||
argsLine = ""
|
||||
} else {
|
||||
argsLine = string(rest)
|
||||
args = whitespace.Split(argsLine, -1)
|
||||
}
|
||||
|
||||
if handler, ok := c.handlers[cmd]; ok {
|
||||
handler(cmd, argsLine, args...)
|
||||
} else {
|
||||
term.Errorf("Unknown command: %s\n", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// CmdLoop reads commands and runs OneCmd
|
||||
func (c *Cli) CmdLoop() {
|
||||
for !c.stopped {
|
||||
// Python cmd-style run setPrompt every time in case something has changed
|
||||
c.setPrompt()
|
||||
|
||||
line, err := c.rl.Readline()
|
||||
if err == readline.ErrInterrupt {
|
||||
continue
|
||||
} else if err == io.EOF {
|
||||
if !c.exitConfirm || c.confirm("Are you sure to exit?") {
|
||||
c.stopped = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.aliasRecursionCount = maxAliasRecursion
|
||||
c.OneCmd(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) confirm(msg string) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Printf("%s [Y/n] ", msg)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err == nil {
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
switch response {
|
||||
case "":
|
||||
fallthrough
|
||||
case "y":
|
||||
return true
|
||||
case "n":
|
||||
return false
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) acquirePasswd() {
|
||||
if c.raiseType == remote.RTNone {
|
||||
return
|
||||
}
|
||||
if c.raisePasswd == "" {
|
||||
c.doPasswd("passwd", "")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) setOutput(filename string) error {
|
||||
var err error
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err == nil {
|
||||
if c.outputFile != nil {
|
||||
c.outputFile.Close()
|
||||
}
|
||||
c.outputFile = f
|
||||
remote.SetOutputFile(c.outputFile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Cli) doexec(mode execMode, argsLine string) {
|
||||
var r *remote.ExecResult
|
||||
|
||||
expr, rest := split([]rune(argsLine))
|
||||
if rest == nil {
|
||||
term.Errorf("Usage: exec <inventoree_expr> commands...\n")
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := c.store.HostList(expr)
|
||||
if err != nil {
|
||||
term.Errorf("Error parsing expression %s: %s\n", string(expr), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
term.Errorf("Empty hostlist\n")
|
||||
return
|
||||
}
|
||||
|
||||
c.acquirePasswd()
|
||||
cmd := string(rest)
|
||||
remote.SetUser(c.user)
|
||||
remote.SetRaise(c.raiseType)
|
||||
remote.SetPassword(c.raisePasswd)
|
||||
|
||||
if c.execConfirm {
|
||||
fmt.Printf("%s\n", term.Yellow(term.HR(len(cmd)+5)))
|
||||
fmt.Printf("%s\n%s\n\n", term.Yellow("Hosts:"), strings.Join(hosts, ", "))
|
||||
fmt.Printf("%s\n%s\n\n", term.Yellow("Command:"), cmd)
|
||||
if !c.confirm("Are you sure?") {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\n\n", term.Yellow(term.HR(len(cmd)+5)))
|
||||
}
|
||||
|
||||
remote.WriteOutput(fmt.Sprintf("==== exec %s\n", argsLine))
|
||||
|
||||
switch mode {
|
||||
case emParallel:
|
||||
r = remote.RunParallel(hosts, cmd)
|
||||
case emCollapse:
|
||||
r = remote.RunCollapse(hosts, cmd)
|
||||
r.PrintOutputMap()
|
||||
case emSerial:
|
||||
r = remote.RunSerial(hosts, cmd, c.delay)
|
||||
}
|
||||
r.Print()
|
||||
}
|
||||
|
||||
func (c *Cli) dorunscript(mode execMode, argsLine string) {
|
||||
var (
|
||||
r *remote.ExecResult
|
||||
expr []rune
|
||||
rest []rune
|
||||
hosts []string
|
||||
localFilename string
|
||||
remoteFilename string
|
||||
err error
|
||||
st os.FileInfo
|
||||
)
|
||||
|
||||
expr, rest = split([]rune(argsLine))
|
||||
if rest == nil {
|
||||
term.Errorf("Usage: runscript <inventoree_expr> filename\n")
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err = c.store.HostList(expr)
|
||||
if err != nil {
|
||||
term.Errorf("Error parsing expression %s: %s\n", string(expr), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
term.Errorf("Empty hostlist\n")
|
||||
return
|
||||
}
|
||||
|
||||
c.acquirePasswd()
|
||||
localFilename = string(rest)
|
||||
st, err = os.Stat(localFilename)
|
||||
if err != nil {
|
||||
term.Errorf("Error stat %s: %s\n", localFilename, err)
|
||||
return
|
||||
}
|
||||
if st.IsDir() {
|
||||
term.Errorf("%s is a directory\n", localFilename)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Format("20060102-150405")
|
||||
remoteFilename = fmt.Sprintf("tmp.xc.%s_%s", now, filepath.Base(localFilename))
|
||||
remoteFilename = filepath.Join(c.remoteTmpDir, remoteFilename)
|
||||
|
||||
dr := remote.Distribute(hosts, localFilename, remoteFilename, false)
|
||||
copyError := dr.ErrorHosts
|
||||
hosts = dr.SuccessHosts
|
||||
|
||||
cmd := fmt.Sprintf("%s; rm %s", remoteFilename, remoteFilename)
|
||||
switch mode {
|
||||
case emParallel:
|
||||
r = remote.RunParallel(hosts, cmd)
|
||||
case emCollapse:
|
||||
r = remote.RunCollapse(hosts, cmd)
|
||||
r.PrintOutputMap()
|
||||
case emSerial:
|
||||
r = remote.RunSerial(hosts, cmd, c.delay)
|
||||
}
|
||||
r.ErrorHosts = append(r.ErrorHosts, copyError...)
|
||||
r.Print()
|
||||
}
|
||||
Reference in New Issue
Block a user