"fmt"
"log"
"os"
+ "os/signal"
"sort"
"strings"
"sync"
optionDryRun := flag.Bool("n", false,
"dry-run, show diff but don't perform any changes")
+ optionQuiet := flag.Bool("q", false,
+ "hide successful, non-trigger commands with no output from host changes listing")
optionLog := flag.String("log", "info", "set log `level`; "+
"levels: error, info, verbose, debug, debug2, debug3")
+ optionSshConfig := flag.String("sshconfig", "",
+ "`path` to ssh configuration file; used for tests")
flag.CommandLine.Parse(args[2:])
return err
}
cfg.DryRun = *optionDryRun
+ cfg.Quiet = *optionQuiet
cfg.LogLevel = level
+ cfg.SshConfig = *optionSshConfig
toSync, err := hostsToSync(names, allHosts, allGroups)
if err != nil {
return fmt.Errorf("no hosts found")
}
- isTTY := term.IsTerminal(int(os.Stdout.Fd()))
+ isTTY := term.IsTerminal(int(os.Stdout.Fd())) &&
+ term.IsTerminal(int(os.Stderr.Fd()))
done := make(chan bool)
// Collect events from all hosts and print them
done <- failed
}()
+ hostsLeft := make(map[string]bool)
+ for _, x := range toSync {
+ hostsLeft[x.Name] = true
+ }
+ var hostsLeftMutex sync.Mutex // protects hostsLeft
+
+ // Show unfinished hosts on Ctrl-C
+ sigint := make(chan os.Signal, 1) // buffered for Notify()
+ signal.Notify(sigint, os.Interrupt) // = SIGINT = Ctrl-C
+ go func() {
+ // Running `ssh` processes get killed by SIGINT which is sent
+ // to all processes
+
+ <-sigint
+ log.Print("Received SIGINT, aborting ...")
+
+ // Print all queued events
+ events <- Event{} // poison pill
+ <-done
+ // "races" with <-done in the main function and will hang here
+ // if the other is faster. This is fine because then all hosts
+ // were synced successfully.
+
+ hostsLeftMutex.Lock()
+ var hosts []string
+ for x := range hostsLeft {
+ hosts = append(hosts, x)
+ }
+ sort.Strings(hosts)
+ log.Fatalf("Failed to sync %s", strings.Join(hosts, ", "))
+ }()
+
// Sync all hosts concurrently
var wg sync.WaitGroup
for _, x := range toSync {
}
}
wg.Done()
+
+ hostsLeftMutex.Lock()
+ defer hostsLeftMutex.Unlock()
+ delete(hostsLeft, x.Name)
}()
}
func hostsToSync(names []string, allHosts *config.Hosts,
allGroups map[string][]string) ([]*config.Host, error) {
+ detectedMap := config.TransitivelyDetectedGroups(allGroups)
+
+ const detectedErr = `
+
+Groups depending on "detected" groups cannot be used to select hosts as these
+are only available after the hosts were contacted.
+`
+
nameMap := make(map[string]bool)
for _, x := range names {
+ if detectedMap[x] {
+ return nil, fmt.Errorf(
+ "group %q depends on \"detected\" groups%s",
+ x, detectedErr)
+ }
nameMap[x] = true
}
nameMatched := make(map[string]bool)
- // To detect typos we must check all given names but only want to add
- // each match once
- hostMatched := make(map[string]bool)
+ // To detect typos we must check all given names but one host can be
+ // matched by multiple names (e.g. two groups with overlapping hosts)
+ hostAdded := make(map[string]bool)
var res []*config.Host
for _, host := range allHosts.List {
if nameMap[host.Name] {
res = append(res, host)
- hostMatched[host.Name] = true
+ hostAdded[host.Name] = true
nameMatched[host.Name] = true
}
- // TODO: don't permit groups which contain "detected" groups
- // because these are not available yet
groups, err := config.ResolveHostGroups(host.Name,
allGroups, nil)
if err != nil {
}
for _, x := range groups {
if nameMap[x] {
- if !hostMatched[host.Name] {
+ if !hostAdded[host.Name] {
res = append(res, host)
- hostMatched[host.Name] = true
+ hostAdded[host.Name] = true
}
nameMatched[x] = true
}
}()
// Connect to remote host
- err := conn.DialSSH(s.host.SshUser, s.host.Name)
+ err := conn.DialSSH(s.host.SshUser, s.host.Name, s.config.SshConfig)
if err != nil {
return err
}
return nil
}
-func (s *Sync) logf(level safcm.LogLevel, escaped bool,
- format string, a ...interface{}) {
-
+func (s *Sync) log(level safcm.LogLevel, escaped bool, msg string) {
if s.config.LogLevel < level {
return
}
Host: s.host,
Log: Log{
Level: level,
- Text: fmt.Sprintf(format, a...),
+ Text: msg,
},
Escaped: escaped,
}
}
func (s *Sync) logDebugf(format string, a ...interface{}) {
- s.logf(safcm.LogDebug, false, format, a...)
+ s.log(safcm.LogDebug, false, fmt.Sprintf(format, a...))
}
func (s *Sync) logVerbosef(format string, a ...interface{}) {
- s.logf(safcm.LogVerbose, false, format, a...)
+ s.log(safcm.LogVerbose, false, fmt.Sprintf(format, a...))
}
// sendRecv sends a message over conn and waits for the response. Any MsgLog
}
log, ok := x.(safcm.MsgLog)
if ok {
- s.logf(log.Level, false, "%s", log.Text)
+ s.log(log.Level, false, log.Text)
continue
}
return x, nil