]> ruderich.org/simon Gitweb - safcm/safcm.git/blobdiff - cmd/safcm/sync_sync.go
First working version
[safcm/safcm.git] / cmd / safcm / sync_sync.go
diff --git a/cmd/safcm/sync_sync.go b/cmd/safcm/sync_sync.go
new file mode 100644 (file)
index 0000000..e84b7f4
--- /dev/null
@@ -0,0 +1,290 @@
+// "sync" sub-command: sync files
+
+// Copyright (C) 2021  Simon Ruderich
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "sort"
+       "strings"
+
+       "ruderich.org/simon/safcm"
+       "ruderich.org/simon/safcm/cmd/safcm/config"
+       "ruderich.org/simon/safcm/rpc"
+)
+
+func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
+       req, err := s.hostSyncReq(detectedGroups)
+       if err != nil {
+               return err
+       }
+       x, err := s.sendRecv(conn, req)
+       if err != nil {
+               return err
+       }
+       resp, ok := x.(safcm.MsgSyncResp)
+       if !ok {
+               return fmt.Errorf("unexpected response %v", x)
+       }
+
+       // Display changes
+       var changes []string
+       if len(resp.FileChanges) > 0 {
+               changes = append(changes,
+                       s.formatFileChanges(resp.FileChanges))
+       }
+       if len(resp.PackageChanges) > 0 {
+               changes = append(changes,
+                       s.formatPackageChanges(resp.PackageChanges))
+       }
+       if len(resp.ServiceChanges) > 0 {
+               changes = append(changes,
+                       s.formatServiceChanges(resp.ServiceChanges))
+       }
+       if len(resp.CommandChanges) > 0 {
+               changes = append(changes,
+                       s.formatCommandChanges(resp.CommandChanges))
+       }
+       if len(changes) > 0 {
+               s.logf(safcm.LogInfo, true, "%s",
+                       "\n"+strings.Join(changes, "\n"))
+       }
+
+       if resp.Error != "" {
+               return fmt.Errorf("%s", resp.Error)
+       }
+       return nil
+}
+
+func (s *Sync) hostSyncReq(detectedGroups []string) (
+       safcm.MsgSyncReq, error) {
+
+       var empty safcm.MsgSyncReq
+
+       groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
+       if err != nil {
+               return empty, err
+       }
+       {
+               // Don't leak internal group order which is confusing without
+               // knowing the implementation details.
+               groupsSorted := make([]string, len(groups))
+               copy(groupsSorted, groups)
+               sort.Strings(groupsSorted)
+               s.logVerbosef("host groups: %s",
+                       strings.Join(groupsSorted, " "))
+
+               // Don't leak internal priority values. Instead, order groups
+               // by priority.
+               var priorities []string
+               for x := range groupPriority {
+                       priorities = append(priorities, x)
+               }
+               sort.Slice(priorities, func(i, j int) bool {
+                       a := priorities[i]
+                       b := priorities[j]
+                       return groupPriority[a] < groupPriority[b]
+               })
+               s.logVerbosef("host group priorities (desc. order): %v",
+                       strings.Join(priorities, " "))
+       }
+
+       allFiles := make(map[string]*safcm.File)
+       allPackagesMap := make(map[string]bool) // map to deduplicate
+       allServicesMap := make(map[string]bool) // map to deduplicate
+       var allCommands []string
+
+       for _, group := range groups {
+               // Skip non-existent group directories
+               _, err := os.Stat(group)
+               if os.IsNotExist(err) {
+                       continue
+               }
+
+               files, err := config.LoadFiles(group)
+               if err != nil {
+                       return empty, err
+               }
+               err = config.LoadPermissions(group, files)
+               if err != nil {
+                       return empty, err
+               }
+               err = config.LoadTemplates(group, files,
+                       s.host.Name, groups, s.allHosts, s.allGroups)
+               if err != nil {
+                       return empty, err
+               }
+               err = config.LoadTriggers(group, files)
+               if err != nil {
+                       return empty, err
+               }
+               for k, v := range files {
+                       err := s.checkFileConflict(group, k, v,
+                               allFiles, groupPriority)
+                       if err != nil {
+                               return empty, err
+                       }
+                       v.OrigGroup = group
+                       allFiles[k] = v
+               }
+
+               packages, err := config.LoadPackages(group)
+               if err != nil {
+                       return empty, err
+               }
+               for _, x := range packages {
+                       allPackagesMap[x] = true
+               }
+
+               services, err := config.LoadServices(group)
+               if err != nil {
+                       return empty, err
+               }
+               for _, x := range services {
+                       allServicesMap[x] = true
+               }
+
+               commands, err := config.LoadCommands(group)
+               if err != nil {
+                       return empty, err
+               }
+               allCommands = append(allCommands, commands...)
+       }
+
+       resolveFileDirConflicts(allFiles)
+
+       var allPackages []string
+       var allServices []string
+       for x := range allPackagesMap {
+               allPackages = append(allPackages, x)
+       }
+       for x := range allServicesMap {
+               allServices = append(allServices, x)
+       }
+       // Sort for deterministic results
+       sort.Strings(allPackages)
+       sort.Strings(allServices)
+
+       return safcm.MsgSyncReq{
+               DryRun:   s.config.DryRun,
+               Groups:   groups,
+               Files:    allFiles,
+               Packages: allPackages,
+               Services: allServices,
+               Commands: allCommands,
+       }, nil
+}
+
+// resolveHostGroups returns the groups and group priorities of the current
+// host.
+func (s *Sync) resolveHostGroups(detectedGroups []string) (
+       []string, map[string]int, error) {
+
+       groups, err := config.ResolveHostGroups(s.host.Name,
+               s.allGroups, detectedGroups)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       // Early entries have higher priorities
+       groupPriority := make(map[string]int)
+       for i, x := range s.config.GroupOrder {
+               groupPriority[x] = i + 1
+       }
+       // Host itself always has highest priority
+       groupPriority[s.host.Name] = -1
+
+       // Sort groups after priority and name
+       sort.Slice(groups, func(i, j int) bool {
+               a := groups[i]
+               b := groups[j]
+               if groupPriority[a] > groupPriority[b] {
+                       return true
+               } else if groupPriority[a] < groupPriority[b] {
+                       return false
+               } else {
+                       return a < b
+               }
+       })
+
+       return groups, groupPriority, nil
+}
+
+func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
+       allFiles map[string]*safcm.File, groupPriority map[string]int) error {
+
+       old, ok := allFiles[path]
+       if !ok {
+               return nil
+       }
+
+       newPrio := groupPriority[group]
+       oldPrio := groupPriority[old.OrigGroup]
+       if oldPrio > newPrio {
+               if old.Mode.IsDir() && file.Mode.IsDir() &&
+                       old.TriggerCommands != nil {
+                       s.logDebugf("files: %q: "+
+                               "group %s overwrites triggers from group %s",
+                               path, group, old.OrigGroup)
+               }
+               return nil
+       } else if oldPrio < newPrio {
+               // Should never happen, groups are sorted by priority
+               panic("invalid group priorities")
+       }
+
+       // Directories with default permissions and no triggers do not count
+       // as conflict
+       if file.Mode.IsDir() && file.Mode == old.Mode &&
+               config.FileModeToFullPerm(file.Mode) == 0755 &&
+               file.TriggerCommands == nil && old.TriggerCommands == nil {
+               return nil
+       }
+
+       return fmt.Errorf("groups %s and %s both provide file %q\n"+
+               "Use 'group_order' in config.yaml to declare preference",
+               group, old.OrigGroup, path)
+}
+
+func resolveFileDirConflicts(files map[string]*safcm.File) {
+       var paths []string
+       for x := range files {
+               paths = append(paths, x)
+       }
+       sort.Slice(paths, func(i, j int) bool {
+               return paths[i] < paths[j]
+       })
+
+       const sep = string(filepath.Separator)
+
+       // Remove invalid paths which can result from group_order overriding
+       // paths from another group (e.g. "/foo" as file from one group and
+       // "/foo/bar" from another).
+       var last *safcm.File
+       for _, x := range paths {
+               file := files[x]
+               if last != nil &&
+                       !last.Mode.IsDir() &&
+                       strings.HasPrefix(file.Path, last.Path+sep) {
+                       delete(files, x)
+                       continue
+               }
+               last = file
+       }
+}