X-Git-Url: https://ruderich.org/simon/gitweb/?p=safcm%2Fsafcm.git;a=blobdiff_plain;f=cmd%2Fsafcm%2Fsync.go;h=77e83c4187592ca1bd5cc31e44da3f4acfb4830d;hp=7f1e090593b6c42dd14f028de056dd24990c9812;hb=5d6cc7f14a4bacc36bf3a23cd735a75ad4a90f1d;hpb=023e863fbd9c6cf5bebd18e19c05ae9e60fbbe21 diff --git a/cmd/safcm/sync.go b/cmd/safcm/sync.go index 7f1e090..77e83c4 100644 --- a/cmd/safcm/sync.go +++ b/cmd/safcm/sync.go @@ -22,6 +22,7 @@ import ( "fmt" "log" "os" + "os/signal" "sort" "strings" "sync" @@ -71,8 +72,12 @@ func MainSync(args []string) error { 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:]) @@ -105,7 +110,9 @@ func MainSync(args []string) error { return err } cfg.DryRun = *optionDryRun + cfg.Quiet = *optionQuiet cfg.LogLevel = level + cfg.SshConfig = *optionSshConfig toSync, err := hostsToSync(names, allHosts, allGroups) if err != nil { @@ -132,6 +139,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 { @@ -157,6 +196,10 @@ func MainSync(args []string) error { } } wg.Done() + + hostsLeftMutex.Lock() + defer hostsLeftMutex.Unlock() + delete(hostsLeft, x.Name) }() } @@ -183,13 +226,29 @@ 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 + } + + 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 + // 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) hostMatched := make(map[string]bool) var res []*config.Host @@ -200,8 +259,6 @@ func hostsToSync(names []string, allHosts *config.Hosts, 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 { @@ -310,7 +367,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 }