X-Git-Url: https://ruderich.org/simon/gitweb/?p=safcm%2Fsafcm.git;a=blobdiff_plain;f=cmd%2Fsafcm%2Fsync.go;h=0f7c54ff309f360ac0e0bc9cbe4071a467c7c02b;hp=7fa2c936b637c7059e5429b273f310233ee92ed4;hb=6a40d84afc959f404f243b1c00ab95dc9dd9c721;hpb=4473e968425319e6beae558643bb047a6b01c17a diff --git a/cmd/safcm/sync.go b/cmd/safcm/sync.go index 7fa2c93..0f7c54f 100644 --- a/cmd/safcm/sync.go +++ b/cmd/safcm/sync.go @@ -22,6 +22,8 @@ import ( "fmt" "log" "os" + "os/signal" + "runtime" "sort" "strings" "sync" @@ -75,6 +77,8 @@ func MainSync(args []string) error { "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:]) @@ -102,6 +106,10 @@ func MainSync(args []string) error { os.Exit(1) } + if runtime.GOOS == "windows" { + log.Print("WARNING: Windows support is experimental!") + } + cfg, allHosts, allGroups, err := LoadBaseFiles() if err != nil { return err @@ -109,6 +117,7 @@ func MainSync(args []string) error { cfg.DryRun = *optionDryRun cfg.Quiet = *optionQuiet cfg.LogLevel = level + cfg.SshConfig = *optionSshConfig toSync, err := hostsToSync(names, allHosts, allGroups) if err != nil { @@ -118,7 +127,8 @@ func MainSync(args []string) error { 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 @@ -135,6 +145,38 @@ func MainSync(args []string) error { 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 { @@ -160,6 +202,10 @@ func MainSync(args []string) error { } } wg.Done() + + hostsLeftMutex.Lock() + defer hostsLeftMutex.Unlock() + delete(hostsLeft, x.Name) }() } @@ -186,10 +232,7 @@ func MainSync(args []string) error { func hostsToSync(names []string, allHosts *config.Hosts, allGroups map[string][]string) ([]*config.Host, error) { - detectedMap := make(map[string]bool) - for _, x := range config.TransitivelyDetectedGroups(allGroups) { - detectedMap[x] = true - } + detectedMap := config.TransitivelyDetectedGroups(allGroups) const detectedErr = ` @@ -207,15 +250,15 @@ are only available after the hosts were contacted. 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 } @@ -226,9 +269,9 @@ are only available after the hosts were contacted. } 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 } @@ -327,7 +370,7 @@ func (s *Sync) Host(wg *sync.WaitGroup) error { }() // 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 } @@ -362,9 +405,7 @@ func (s *Sync) Host(wg *sync.WaitGroup) error { 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 } @@ -372,16 +413,16 @@ func (s *Sync) logf(level safcm.LogLevel, escaped bool, 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 @@ -399,7 +440,7 @@ func (s *Sync) sendRecv(conn *rpc.Conn, msg safcm.Msg) (safcm.Msg, error) { } 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