1 // "sync" sub-command: sync files
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/>.
27 "ruderich.org/simon/safcm"
28 "ruderich.org/simon/safcm/cmd/safcm/config"
29 "ruderich.org/simon/safcm/frontend"
30 "ruderich.org/simon/safcm/rpc"
33 func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
34 req, err := s.hostSyncReq(detectedGroups)
38 x, err := s.loop.SendRecv(s, conn, req)
42 resp, ok := x.(safcm.MsgSyncResp)
44 return fmt.Errorf("unexpected response %v", x)
48 c := frontend.Changes{
49 DryRun: s.config.DryRun,
50 Quiet: s.config.Quiet,
53 changes := c.FormatChanges(resp)
55 s.log(safcm.LogInfo, true, changes)
59 return fmt.Errorf("%s", resp.Error)
64 func (s *Sync) hostSyncReq(detectedGroups []string) (
65 safcm.MsgSyncReq, error) {
67 var empty safcm.MsgSyncReq
69 groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
74 // Don't leak internal group priority which is confusing
75 // without knowing the implementation details.
76 groupsSorted := make([]string, len(groups))
77 copy(groupsSorted, groups)
78 sort.Strings(groupsSorted)
79 s.logVerbosef("host groups: %s",
80 strings.Join(groupsSorted, " "))
82 // Don't leak internal priority values. Instead, order groups
84 var priorities []string
85 for x := range groupPriority {
86 priorities = append(priorities, x)
88 sort.Slice(priorities, func(i, j int) bool {
91 return groupPriority[a] > groupPriority[b]
93 s.logVerbosef("host group priorities (descending): %v",
94 strings.Join(priorities, " "))
97 allFiles := make(map[string]*safcm.File)
98 allPackagesMap := make(map[string]bool) // map to deduplicate
99 allServicesMap := make(map[string]bool) // map to deduplicate
100 var allCommands []*safcm.Command
102 for _, group := range groups {
103 // Skip non-existent group directories
104 _, err := os.Stat(group)
105 if os.IsNotExist(err) {
109 files, err := config.LoadFiles(group)
113 err = config.LoadPermissions(group, files)
117 err = config.LoadTemplates(group, files,
118 s.host.Name, groups, s.allHosts, s.allGroups)
122 err = config.LoadTriggers(group, files)
126 for k, v := range files {
127 err := s.checkFileConflict(group, k, v,
128 allFiles, groupPriority)
136 packages, err := config.LoadPackages(group)
140 for _, x := range packages {
141 allPackagesMap[x] = true
144 services, err := config.LoadServices(group)
148 for _, x := range services {
149 allServicesMap[x] = true
152 commands, err := config.LoadCommands(group)
156 allCommands = append(allCommands, commands...)
159 resolveFileDirConflicts(allFiles)
161 var allPackages []string
162 var allServices []string
163 for x := range allPackagesMap {
164 allPackages = append(allPackages, x)
166 for x := range allServicesMap {
167 allServices = append(allServices, x)
169 // Sort for deterministic results
170 sort.Strings(allPackages)
171 sort.Strings(allServices)
173 return safcm.MsgSyncReq{
174 DryRun: s.config.DryRun,
177 Packages: allPackages,
178 Services: allServices,
179 Commands: allCommands,
183 // resolveHostGroups returns the groups and group priorities of the current
185 func (s *Sync) resolveHostGroups(detectedGroups []string) (
186 []string, map[string]int, error) {
188 groups, err := config.ResolveHostGroups(s.host.Name,
189 s.allGroups, detectedGroups)
194 // Early entries in "group_priority" have higher priorities
195 groupPriority := make(map[string]int)
196 for i, x := range s.config.GroupPriority {
197 groupPriority[x] = len(s.config.GroupPriority) - i
199 // Host itself always has highest priority
200 groupPriority[s.host.Name] = math.MaxInt32
202 // Sort groups after priority and name
203 sort.Slice(groups, func(i, j int) bool {
206 if groupPriority[a] < groupPriority[b] {
208 } else if groupPriority[a] > groupPriority[b] {
215 return groups, groupPriority, nil
218 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
219 allFiles map[string]*safcm.File, groupPriority map[string]int) error {
221 old, ok := allFiles[path]
226 newPrio := groupPriority[group]
227 oldPrio := groupPriority[old.OrigGroup]
228 if oldPrio < newPrio {
229 if old.Mode.IsDir() && file.Mode.IsDir() &&
230 old.TriggerCommands != nil {
231 s.logDebugf("files: %q: "+
232 "group %s overwrites triggers from group %s",
233 path, group, old.OrigGroup)
236 } else if oldPrio > newPrio {
237 // Should never happen, groups are sorted by priority
238 panic("invalid group priorities")
241 // Directories with default permissions and no triggers do not count
243 if file.Mode.IsDir() && file.Mode == old.Mode &&
244 config.FileModeToFullPerm(file.Mode) == 0755 &&
245 file.TriggerCommands == nil && old.TriggerCommands == nil {
249 return fmt.Errorf("groups %s and %s both provide %q\n"+
250 "Use 'group_priority' in config.yaml to declare preference",
251 group, old.OrigGroup, path)
254 func resolveFileDirConflicts(files map[string]*safcm.File) {
256 for x := range files {
257 paths = append(paths, x)
259 sort.Slice(paths, func(i, j int) bool {
260 return paths[i] < paths[j]
263 // Slash separated paths are used for the configuration
266 // Remove invalid paths which can result from group_priority
267 // overriding paths from another group (e.g. "/foo" as file from one
268 // group and "/foo/bar" from another).
270 for _, x := range paths {
273 !last.Mode.IsDir() &&
274 strings.HasPrefix(file.Path, last.Path+sep) {