1 // "sync" sub-command: sync files
3 // SPDX-License-Identifier: GPL-3.0-or-later
4 // Copyright (C) 2021-2024 Simon Ruderich
15 "ruderich.org/simon/safcm"
16 "ruderich.org/simon/safcm/cmd/safcm/config"
17 "ruderich.org/simon/safcm/frontend"
18 "ruderich.org/simon/safcm/rpc"
21 func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
22 req, err := s.hostSyncReq(detectedGroups)
26 resp, err := s.loop.HostSyncMsg(s, conn, req)
32 c := frontend.Changes{
33 DryRun: s.config.DryRun,
34 Quiet: s.config.Quiet,
37 changes := c.FormatChanges(resp)
39 s.log(safcm.LogInfo, true, changes)
43 return fmt.Errorf("%s", resp.Error)
48 func (s *Sync) hostSyncReq(detectedGroups []string) (
49 safcm.MsgSyncReq, error) {
51 var empty safcm.MsgSyncReq
53 groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
58 // Don't leak internal group priority which is confusing
59 // without knowing the implementation details.
60 groupsSorted := make([]string, len(groups))
61 copy(groupsSorted, groups)
62 sort.Strings(groupsSorted)
63 s.logVerbosef("host groups: %s",
64 strings.Join(groupsSorted, " "))
66 // Don't leak internal priority values. Instead, order groups
68 var priorities []string
69 for x := range groupPriority {
70 priorities = append(priorities, x)
72 sort.Slice(priorities, func(i, j int) bool {
75 return groupPriority[a] > groupPriority[b]
77 s.logVerbosef("host group priorities (descending): %v",
78 strings.Join(priorities, " "))
81 allFiles := make(map[string]*safcm.File)
82 allPackagesMap := make(map[string]bool) // map to deduplicate
83 allServicesMap := make(map[string]bool) // map to deduplicate
84 var allCommands []*safcm.Command
86 for _, group := range groups {
87 // Skip non-existent group directories
88 _, err := os.Stat(group)
89 if os.IsNotExist(err) {
93 files, err := config.LoadFiles(group)
97 err = config.LoadPermissions(group, files)
101 err = config.LoadTemplates(group, files,
102 s.host.Name, groups, s.allHosts, s.allGroups)
106 err = config.LoadTriggers(group, files)
110 for k, v := range files {
111 err := s.checkFileConflict(group, k, v,
112 allFiles, groupPriority)
120 packages, err := config.LoadPackages(group)
124 for _, x := range packages {
125 allPackagesMap[x] = true
128 services, err := config.LoadServices(group)
132 for _, x := range services {
133 allServicesMap[x] = true
136 commands, err := config.LoadCommands(group)
140 allCommands = append(allCommands, commands...)
143 resolveFileDirConflicts(allFiles)
145 var allPackages []string
146 var allServices []string
147 for x := range allPackagesMap {
148 allPackages = append(allPackages, x)
150 for x := range allServicesMap {
151 allServices = append(allServices, x)
153 // Sort for deterministic results
154 sort.Strings(allPackages)
155 sort.Strings(allServices)
157 return safcm.MsgSyncReq{
158 DryRun: s.config.DryRun,
161 Packages: allPackages,
162 Services: allServices,
163 Commands: allCommands,
167 // resolveHostGroups returns the groups and group priorities of the current
169 func (s *Sync) resolveHostGroups(detectedGroups []string) (
170 []string, map[string]int, error) {
172 groups, err := config.ResolveHostGroups(s.host.Name,
173 s.allGroups, detectedGroups)
178 // Early entries in "group_priority" have higher priorities
179 groupPriority := make(map[string]int)
180 for i, x := range s.config.GroupPriority {
181 groupPriority[x] = len(s.config.GroupPriority) - i
183 // Host itself always has highest priority
184 groupPriority[s.host.Name] = math.MaxInt32
186 // Sort groups after priority and name
187 sort.Slice(groups, func(i, j int) bool {
190 if groupPriority[a] < groupPriority[b] {
192 } else if groupPriority[a] > groupPriority[b] {
199 return groups, groupPriority, nil
202 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
203 allFiles map[string]*safcm.File, groupPriority map[string]int) error {
205 old, ok := allFiles[path]
210 newPrio := groupPriority[group]
211 oldPrio := groupPriority[old.OrigGroup]
212 if oldPrio < newPrio {
213 if old.Mode.IsDir() && file.Mode.IsDir() &&
214 old.TriggerCommands != nil {
215 s.logDebugf("files: %q: "+
216 "group %s overwrites triggers from group %s",
217 path, group, old.OrigGroup)
220 } else if oldPrio > newPrio {
221 // Should never happen, groups are sorted by priority
222 panic("invalid group priorities")
225 // Directories with default permissions and no triggers do not count
227 if file.Mode.IsDir() && file.Mode == old.Mode &&
228 config.FileModeToFullPerm(file.Mode) == 0755 &&
229 file.TriggerCommands == nil && old.TriggerCommands == nil {
233 return fmt.Errorf("groups %s and %s both provide %q\n"+
234 "Use 'group_priority' in config.yaml to declare preference",
235 group, old.OrigGroup, path)
238 func resolveFileDirConflicts(files map[string]*safcm.File) {
240 for x := range files {
241 paths = append(paths, x)
243 sort.Slice(paths, func(i, j int) bool {
244 return paths[i] < paths[j]
247 // Slash separated paths are used for the configuration
250 // Remove invalid paths which can result from group_priority
251 // overriding paths from another group (e.g. "/foo" as file from one
252 // group and "/foo/bar" from another).
254 for _, x := range paths {
257 !last.Mode.IsDir() &&
258 strings.HasPrefix(file.Path, last.Path+sep) {