diff --git a/commands.go b/cli_commands.go similarity index 94% rename from commands.go rename to cli_commands.go index 07735d8..3cba588 100644 --- a/commands.go +++ b/cli_commands.go @@ -5,6 +5,7 @@ import ( "os" "github.com/zshamrock/vmx/command" + "github.com/zshamrock/vmx/config" "gopkg.in/urfave/cli.v1" ) @@ -44,9 +45,9 @@ var Commands = []cli.Command{ BashComplete: func(c *cli.Context) { var names []string if c.NArg() == 0 || (c.NArg() == 1 && command.ContainsFollow(c)) { - names = command.GetHostNames() + names = config.GetHostNames() } else { - names = command.GetCommandNames() + names = config.GetCommandNames() } for _, name := range names { fmt.Println(name) diff --git a/command/command_test.go b/command/command_test.go index 9fb25a9..2a6c202 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -18,10 +18,10 @@ func TestGetCommand(t *testing.T) { context := cli.NewContext(app, &flags, nil) command, extraArgs := getCommand(context, true) if !command.IsAdHoc() { - t.Errorf("Command name should be ad-hoc, but got %s", command.name) + t.Errorf("Command name should be ad-hoc, but got %s", command.Name) } - if command.command != commandText { - t.Errorf("Command should be %s, but got %s", commandText, command.command) + if command.Command != commandText { + t.Errorf("Command should be %s, but got %s", commandText, command.Command) } if extraArgs != "" { t.Errorf("Extra args should be empty, but got %s", extraArgs) diff --git a/command/list.go b/command/list.go index 744d3cd..47a30ff 100644 --- a/command/list.go +++ b/command/list.go @@ -3,17 +3,19 @@ package command import ( "fmt" + "github.com/zshamrock/vmx/config" "gopkg.in/urfave/cli.v1" ) // CmdList lists available custom command func CmdList(c *cli.Context) { CheckUpdate(c) - names := GetCommandNames() + names := config.GetCommandNames() + commands := config.GetCommands() for _, name := range names { fmt.Print(name) - if commands[name].requiresConfirmation { - fmt.Printf(" (%s)\n", commandNameConfirmationSuffix) + if commands[name].RequiresConfirmation { + fmt.Printf(" (%s)\n", config.CommandNameConfirmationSuffix) } else { fmt.Println("") } diff --git a/command/run.go b/command/run.go index 90ebabb..a317339 100644 --- a/command/run.go +++ b/command/run.go @@ -11,18 +11,22 @@ import ( "github.com/kevinburke/ssh_config" "github.com/zshamrock/vmx/config" + "github.com/zshamrock/vmx/core" "gopkg.in/urfave/cli.v1" ) const ( - optionalFollowArgsIndex = 0 - hostsGroupArgsIndex = 0 - commandNameArgsIndex = 1 - hostsGroupChildrenSuffix = ":children" - allHostsGroup = "all" - FollowArgName = "follow" + optionalFollowArgsIndex = 0 + hostsGroupArgsIndex = 0 + commandNameArgsIndex = 1 + FollowArgName = "follow" ) +type execOutput struct { + name, host string + output string +} + // CmdRun runs custom command func CmdRun(c *cli.Context) { CheckUpdate(c) @@ -30,36 +34,36 @@ func CmdRun(c *cli.Context) { command, extraArgs := getCommand(c, follow) hosts := getHosts(c, follow) var confirmation string - if command.requiresConfirmation { - fmt.Printf("Confirm to run \"%s\" command on %v - yes/no or y/n: ", command.name, hosts) + if command.RequiresConfirmation { + fmt.Printf("Confirm to run \"%s\" command on %v - yes/no or y/n: ", command.Name, hosts) fmt.Scanln(&confirmation) } confirmation = strings.ToLower(confirmation) - if command.requiresConfirmation && confirmation != "yes" && confirmation != "y" { + if command.RequiresConfirmation && confirmation != "yes" && confirmation != "y" { return } - cmd := command.command - if command.workingDir != "" { - cmd = strings.TrimSpace(fmt.Sprintf("cd %s && %s %s", command.workingDir, cmd, extraArgs)) + cmd := command.Command + if command.WorkingDir != "" { + cmd = strings.TrimSpace(fmt.Sprintf("cd %s && %s %s", command.WorkingDir, cmd, extraArgs)) } - fmt.Printf("Running command: %s from %s on %v\n", command.command, command.workingDir, hosts) + fmt.Printf("Running command: %s from %s on %v\n", command.Command, command.WorkingDir, hosts) sshConfig := readSSHConfig(config.DefaultConfig) - ch := make(chan ExecOutput, len(hosts)) + ch := make(chan execOutput, len(hosts)) for _, host := range hosts { - if command.workingDir == "" && !strings.Contains(cmd, "cd ") { + if command.WorkingDir == "" && !strings.Contains(cmd, "cd ") { // Try to extend the command with the working dir from the defaults config, unless the command already has // have one, which takes the precedence. Also avoid to extend the command with the working dir from the // defaults config, if the command has "cd " in it, assuming user configured the working dir explicitly. - defaults := getDefaults(host) - workingDir, ok := defaults[SectionWorkingDirKeyName] + defaults := config.GetDefaults(host) + workingDir, ok := defaults[config.SectionWorkingDirKeyName] if ok { fmt.Printf("Using working dir %s from the defaults config\n", workingDir) cmd = fmt.Sprintf("cd %s && %s", workingDir, cmd) } } - go SSH(sshConfig, host, cmd, follow, ch) + go ssh(sshConfig, host, cmd, follow, ch) } - outputs := make([]ExecOutput, 0, len(hosts)) + outputs := make([]execOutput, 0, len(hosts)) for i := 0; i < len(hosts); i++ { outputs = append(outputs, <-ch) } @@ -70,16 +74,21 @@ func CmdRun(c *cli.Context) { fmt.Println(output.output) } } -func getCommand(c *cli.Context, follow bool) (Command, string) { +func getCommand(c *cli.Context, follow bool) (core.Command, string) { args := c.Args() actualCommandNameArgsIndex := getActualArgsIndex(commandNameArgsIndex, follow) commandName := strings.TrimSpace(args.Get(actualCommandNameArgsIndex)) - command, ok := commands[commandName] + command, ok := config.GetCommands()[commandName] if !ok { adhocCommand := strings.Join(args[actualCommandNameArgsIndex:], " ") fmt.Printf("%s: custom command \"%s\" is not defined, interpret it as the ad-hoc command: %s\n", c.App.Name, commandName, adhocCommand) - command = Command{adHocCommandName, adhocCommand, "", false} + command = core.Command{ + Name: core.AdHocCommandName, + Command: adhocCommand, + WorkingDir: "", + RequiresConfirmation: false, + } } extraArgs := "" if ok && c.NArg() > 2 { @@ -113,7 +122,8 @@ func getHosts(c *cli.Context, follow bool) []string { } func getHostsByGroup(c *cli.Context, hostsGroup string) []string { - if hostsGroup == allHostsGroup { + hostsGroups := config.GetHostsGroups() + if hostsGroup == config.AllHostsGroup { allHosts := make([]string, 0, len(hostsGroups)) for _, hosts := range hostsGroups { for _, host := range hosts { @@ -125,11 +135,11 @@ func getHostsByGroup(c *cli.Context, hostsGroup string) []string { hosts, ok := hostsGroups[hostsGroup] if !ok { // First then try whether host:children exists - hosts, ok = hostsGroups[hostsGroup+hostsGroupChildrenSuffix] + hosts, ok = hostsGroups[hostsGroup+config.HostsGroupChildrenSuffix] if ok { children := make([]string, 0, len(hosts)) for _, group := range hosts { - _, ok = hostsGroups[group+hostsGroupChildrenSuffix] + _, ok = hostsGroups[group+config.HostsGroupChildrenSuffix] if ok { children = append(children, getHostsByGroup(c, group)...) } else { @@ -146,17 +156,6 @@ func getHostsByGroup(c *cli.Context, hostsGroup string) []string { return hosts } -func getDefaults(host string) map[string]string { - values := defaults[host] - if values == nil { - values = defaults[allHostsGroup] - } - if values == nil { - return map[string]string{} - } - return values -} - func readSSHConfig(cfg config.VMXConfig) *ssh_config.Config { sshConfigFilePath := filepath.Join(cfg.SSHConfigDir, "config") data, err := ioutil.ReadFile(sshConfigFilePath) diff --git a/command/ssh.go b/command/ssh.go index 3f6ae30..eeb4d13 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/kevinburke/ssh_config" - "golang.org/x/crypto/ssh" + cryptoSSH "golang.org/x/crypto/ssh" ) const ( @@ -19,8 +19,8 @@ const ( ignoredIdentitySshFile = "~/.ssh/identity" ) -// SSH implements scp connection to the remote instance -func SSH(sshConfig *ssh_config.Config, host, command string, follow bool, ch chan ExecOutput) { +// ssh implements scp connection to the remote instance +func ssh(sshConfig *ssh_config.Config, host, command string, follow bool, ch chan execOutput) { fmt.Printf("Running command: %s on host %s\n", command, host) user, _ := sshConfig.Get(host, SshConfigUserKey) hostname, _ := sshConfig.Get(host, SshConfigHostnameKey) @@ -32,14 +32,14 @@ func SSH(sshConfig *ssh_config.Config, host, command string, follow bool, ch cha identityFilePath = os.ExpandEnv(strings.Replace(identityFile, "~", "${HOME}", -1)) } pk, _ := ioutil.ReadFile(identityFilePath) - signer, _ := ssh.ParsePrivateKey([]byte(pk)) - config := &ssh.ClientConfig{ + signer, _ := cryptoSSH.ParsePrivateKey([]byte(pk)) + config := &cryptoSSH.ClientConfig{ User: user, - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(signer), + Auth: []cryptoSSH.AuthMethod{ + cryptoSSH.PublicKeys(signer), }, } - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", hostname), config) + client, err := cryptoSSH.Dial("tcp", fmt.Sprintf("%s:22", hostname), config) if err != nil { log.Panicf("Failed to dial to the host %s: %v\n", host, err.Error()) } @@ -60,7 +60,7 @@ func SSH(sshConfig *ssh_config.Config, host, command string, follow bool, ch cha log.Panicf("Failed to run command \"%s\" on the host %s: %v\n", command, host, err.Error()) } fmt.Fprintf(&output, "Command completed on the host %s\n", host) - ch <- ExecOutput{ + ch <- execOutput{ command, host, output.String(), diff --git a/config/config.go b/config/config.go index be36162..d102d9d 100644 --- a/config/config.go +++ b/config/config.go @@ -10,6 +10,10 @@ const ( defaultVmxHome = "${HOME}/.vmx" vmxSSHConfigHomeEnvVar = "VMX_SSH_CONFIG_HOME" defaultSSHConfigHome = "${HOME}/.ssh" + + CommandNameConfirmationSuffix = "!" + HostsGroupChildrenSuffix = ":children" + AllHostsGroup = "all" ) type VMXConfig struct { diff --git a/command/init.go b/config/init.go similarity index 72% rename from command/init.go rename to config/init.go index 4b3245f..136b8ed 100644 --- a/command/init.go +++ b/config/init.go @@ -1,4 +1,4 @@ -package command +package config import ( "os" @@ -7,7 +7,7 @@ import ( "strings" "github.com/kevinburke/ssh_config" - "github.com/zshamrock/vmx/config" + "github.com/zshamrock/vmx/core" "gopkg.in/ini.v1" ) @@ -18,40 +18,24 @@ const ( SectionCommandKeyName = "command" SectionWorkingDirKeyName = "workingdir" - defaultSectionName = "DEFAULT" - commandNameConfirmationSuffix = "!" - adHocCommandName = "ad-hoc" + defaultSectionName = "DEFAULT" ) -type Command struct { - name, command, workingDir string - requiresConfirmation bool -} - -type ExecOutput struct { - name, host string - output string -} - -var commands map[string]Command +var commands map[string]core.Command var hostsGroups map[string][]string var commandNames []string var hostNames []string var defaults map[string]map[string]string -func (c Command) IsAdHoc() bool { - return c.name == adHocCommandName -} - func Init(profile string) { - cfg := config.DefaultConfig + cfg := DefaultConfig commands = readCommands(cfg, profile) hostsGroups = readHostsGroups(cfg, profile) defaults = readDefaults(cfg, profile) } -func readCommands(config config.VMXConfig, profile string) map[string]Command { - commands := make(map[string]Command) +func readCommands(config VMXConfig, profile string) map[string]core.Command { + commands := make(map[string]core.Command) cfg, err := ini.Load(filepath.Join(config.GetDir(profile), CommandsConfigFileName)) cfg.BlockMode = false if err != nil { @@ -64,22 +48,23 @@ func readCommands(config config.VMXConfig, profile string) map[string]Command { if name == defaultSectionName { continue } - requiresConfirmation := strings.HasSuffix(name, commandNameConfirmationSuffix) - name = strings.TrimSuffix(name, commandNameConfirmationSuffix) + requiresConfirmation := strings.HasSuffix(name, CommandNameConfirmationSuffix) + name = strings.TrimSuffix(name, CommandNameConfirmationSuffix) workingDir := "" if section.HasKey(SectionWorkingDirKeyName) { workingDir = section.Key(SectionWorkingDirKeyName).String() } - commands[name] = Command{ - name, - section.Key(SectionCommandKeyName).String(), - workingDir, - requiresConfirmation} + commands[name] = core.Command{ + Name: name, + Command: section.Key(SectionCommandKeyName).String(), + WorkingDir: workingDir, + RequiresConfirmation: requiresConfirmation, + } } return commands } -func readHostsGroups(config config.VMXConfig, profile string) map[string][]string { +func readHostsGroups(config VMXConfig, profile string) map[string][]string { groups := make(map[string][]string) cfg, err := ini.LoadSources( ini.LoadOptions{AllowBooleanKeys: true}, @@ -100,7 +85,7 @@ func readHostsGroups(config config.VMXConfig, profile string) map[string][]strin return groups } -func readDefaults(config config.VMXConfig, profile string) map[string]map[string]string { +func readDefaults(config VMXConfig, profile string) map[string]map[string]string { defaults := make(map[string]map[string]string) cfg, err := ini.Load(filepath.Join(config.GetDir(profile), DefaultsConfigFileName)) cfg.BlockMode = false @@ -132,7 +117,7 @@ func GetCommandNames() []string { if commandNames == nil { commandNames = make([]string, 0, len(commands)) for _, command := range commands { - commandNames = append(commandNames, command.name) + commandNames = append(commandNames, command.Name) } sort.Strings(commandNames) } @@ -140,14 +125,18 @@ func GetCommandNames() []string { return commandNames } +func GetCommands() map[string]core.Command { + return commands +} + func GetHostNames() []string { if hostNames == nil { names := make(map[string]int) // Reading hosts from the ~/.vmx/hosts for group := range hostsGroups { var name string - if strings.HasSuffix(group, hostsGroupChildrenSuffix) { - name = strings.TrimSuffix(group, hostsGroupChildrenSuffix) + if strings.HasSuffix(group, HostsGroupChildrenSuffix) { + name = strings.TrimSuffix(group, HostsGroupChildrenSuffix) } else { name = group } @@ -179,3 +168,18 @@ func GetHostNames() []string { } return hostNames } + +func GetHostsGroups() map[string][]string { + return hostsGroups +} + +func GetDefaults(host string) map[string]string { + values := defaults[host] + if values == nil { + values = defaults[AllHostsGroup] + } + if values == nil { + return map[string]string{} + } + return values +} diff --git a/core/command.go b/core/command.go new file mode 100644 index 0000000..797ead7 --- /dev/null +++ b/core/command.go @@ -0,0 +1,14 @@ +package core + +const ( + AdHocCommandName = "ad-hoc" +) + +type Command struct { + Name, Command, WorkingDir string + RequiresConfirmation bool +} + +func (c Command) IsAdHoc() bool { + return c.Name == AdHocCommandName +} diff --git a/main.go b/main.go index 8b7ca60..a9788f0 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/zshamrock/vmx/command" + "github.com/zshamrock/vmx/config" "gopkg.in/urfave/cli.v1" ) @@ -21,7 +21,7 @@ func main() { app.CommandNotFound = CommandNotFound app.Before = func(c *cli.Context) error { profile := getProfile(c) - command.Init(profile) + config.Init(profile) return nil } app.Run(os.Args)