1 // "sync" sub-command: sync data to remote hosts
3 // Copyright (C) 2021 Simon Ruderich
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program. If not, see <http://www.gnu.org/licenses/>.
31 "ruderich.org/simon/safcm"
32 "ruderich.org/simon/safcm/cmd/safcm/config"
33 "ruderich.org/simon/safcm/rpc"
39 config *config.Config // global configuration
40 allHosts *config.Hosts // known hosts
41 allGroups map[string][]string // known groups
43 events chan<- Event // all events generated by/for this host
51 // Only one of Error, Log and ConnEvent is set in a single event
54 ConnEvent rpc.ConnEvent
56 Escaped bool // true if untrusted input is already escaped
64 func MainSync(args []string) error {
66 fmt.Fprintf(os.Stderr,
67 "usage: %s sync [<options>] <host|group...>\n",
72 optionDryRun := flag.Bool("n", false,
73 "dry-run, show diff but don't perform any changes")
74 optionLog := flag.String("log", "info", "set log `level`; "+
75 "levels: error, info, verbose, debug, debug2, debug3")
77 flag.CommandLine.Parse(args[2:])
79 var level safcm.LogLevel
82 level = safcm.LogError
86 level = safcm.LogVerbose
88 level = safcm.LogDebug
90 level = safcm.LogDebug2
92 level = safcm.LogDebug3
94 return fmt.Errorf("invalid -log value %q", *optionLog)
103 cfg, allHosts, allGroups, err := LoadBaseFiles()
107 cfg.DryRun = *optionDryRun
110 toSync, err := hostsToSync(names, allHosts, allGroups)
114 if len(toSync) == 0 {
115 return fmt.Errorf("no hosts found")
118 isTTY := term.IsTerminal(int(os.Stdout.Fd()))
120 done := make(chan bool)
121 // Collect events from all hosts and print them
122 events := make(chan Event)
130 logEvent(x, cfg.LogLevel, isTTY, &failed)
135 // Sync all hosts concurrently
136 var wg sync.WaitGroup
137 for _, x := range toSync {
140 // Once in sync.Host() and once in the go func below
148 allGroups: allGroups,
152 err := sync.Host(&wg)
164 events <- Event{} // poison pill
168 // Exit instead of returning an error to prevent an extra log
169 // message from main()
175 // hostsToSync returns the list of hosts to sync based on the command line
178 // Full host and group matches are required to prevent unexpected behavior. No
179 // arguments does not expand to all hosts to prevent accidents; "all" can be
180 // used instead. Both host and group names are permitted as these are unique.
182 // TODO: Add option to permit partial/glob matches
183 func hostsToSync(names []string, allHosts *config.Hosts,
184 allGroups map[string][]string) ([]*config.Host, error) {
186 nameMap := make(map[string]bool)
187 for _, x := range names {
190 nameMatched := make(map[string]bool)
191 // To detect typos we must check all given names but only want to add
193 hostMatched := make(map[string]bool)
195 var res []*config.Host
196 for _, host := range allHosts.List {
197 if nameMap[host.Name] {
198 res = append(res, host)
199 hostMatched[host.Name] = true
200 nameMatched[host.Name] = true
203 // TODO: don't permit groups which contain "detected" groups
204 // because these are not available yet
205 groups, err := config.ResolveHostGroups(host.Name,
210 for _, x := range groups {
212 if !hostMatched[host.Name] {
213 res = append(res, host)
214 hostMatched[host.Name] = true
216 nameMatched[x] = true
221 // Warn about unmatched names to detect typos
222 if len(nameMap) != len(nameMatched) {
223 var unmatched []string
224 for x := range nameMap {
226 unmatched = append(unmatched,
227 fmt.Sprintf("%q", x))
230 sort.Strings(unmatched)
231 return nil, fmt.Errorf("hosts/groups not found: %s",
232 strings.Join(unmatched, " "))
238 func logEvent(x Event, level safcm.LogLevel, isTTY bool, failed *bool) {
239 // We have multiple event sources so this is somewhat ugly.
240 var prefix, data string
244 data = x.Error.Error()
246 // We logged an error, tell the caller
248 } else if x.Log.Level != 0 {
249 // LogError and LogDebug3 should not occur here
253 case safcm.LogVerbose:
257 case safcm.LogDebug2:
260 prefix = fmt.Sprintf("[INVALID=%d]", x.Log.Level)
265 switch x.ConnEvent.Type {
266 case rpc.ConnEventStderr:
268 case rpc.ConnEventDebug:
270 case rpc.ConnEventUpload:
271 if level < safcm.LogInfo {
275 x.ConnEvent.Data = "remote helper upload in progress"
277 prefix = fmt.Sprintf("[INVALID=%d]", x.ConnEvent.Type)
280 data = x.ConnEvent.Data
285 host = ColorString(isTTY, color, host)
287 // Make sure to escape control characters to prevent terminal
290 data = EscapeControlCharacters(isTTY, data)
292 log.Printf("%-9s [%s] %s", prefix, host, data)
295 func (s *Sync) Host(wg *sync.WaitGroup) error {
296 conn := rpc.NewConn(s.config.LogLevel >= safcm.LogDebug3)
297 // Pass all connection events to main loop
300 x, ok := <-conn.Events
312 // Connect to remote host
313 err := conn.DialSSH(s.host.Name)
319 // Collect information about remote host
320 detectedGroups, err := s.hostInfo(conn)
325 // Sync state to remote host
326 err = s.hostSync(conn, detectedGroups)
331 // Terminate connection to remote host
332 err = conn.Send(safcm.MsgQuitReq{})
348 func (s *Sync) logf(level safcm.LogLevel, escaped bool,
349 format string, a ...interface{}) {
351 if s.config.LogLevel < level {
358 Text: fmt.Sprintf(format, a...),
363 func (s *Sync) logDebugf(format string, a ...interface{}) {
364 s.logf(safcm.LogDebug, false, format, a...)
366 func (s *Sync) logVerbosef(format string, a ...interface{}) {
367 s.logf(safcm.LogVerbose, false, format, a...)
370 // sendRecv sends a message over conn and waits for the response. Any MsgLog
371 // messages received before the final (non MsgLog) response are passed to
373 func (s *Sync) sendRecv(conn *rpc.Conn, msg safcm.Msg) (safcm.Msg, error) {
374 err := conn.Send(msg)
379 x, err := conn.Recv()
383 log, ok := x.(safcm.MsgLog)
385 s.logf(log.Level, false, "%s", log.Text)