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/rpc"
32 func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
33 req, err := s.hostSyncReq(detectedGroups)
37 x, err := s.sendRecv(conn, req)
41 resp, ok := x.(safcm.MsgSyncResp)
43 return fmt.Errorf("unexpected response %v", x)
48 if len(resp.FileChanges) > 0 {
49 changes = append(changes,
50 s.formatFileChanges(resp.FileChanges))
52 if len(resp.PackageChanges) > 0 {
53 changes = append(changes,
54 s.formatPackageChanges(resp.PackageChanges))
56 if len(resp.ServiceChanges) > 0 {
57 changes = append(changes,
58 s.formatServiceChanges(resp.ServiceChanges))
60 if len(resp.CommandChanges) > 0 {
61 changes = append(changes,
62 s.formatCommandChanges(resp.CommandChanges))
65 s.logf(safcm.LogInfo, true, "%s",
66 "\n"+strings.Join(changes, "\n"))
70 return fmt.Errorf("%s", resp.Error)
75 func (s *Sync) hostSyncReq(detectedGroups []string) (
76 safcm.MsgSyncReq, error) {
78 var empty safcm.MsgSyncReq
80 groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
85 // Don't leak internal group order which is confusing without
86 // knowing the implementation details.
87 groupsSorted := make([]string, len(groups))
88 copy(groupsSorted, groups)
89 sort.Strings(groupsSorted)
90 s.logVerbosef("host groups: %s",
91 strings.Join(groupsSorted, " "))
93 // Don't leak internal priority values. Instead, order groups
95 var priorities []string
96 for x := range groupPriority {
97 priorities = append(priorities, x)
99 sort.Slice(priorities, func(i, j int) bool {
102 return groupPriority[a] < groupPriority[b]
104 s.logVerbosef("host group priorities (desc. order): %v",
105 strings.Join(priorities, " "))
108 allFiles := make(map[string]*safcm.File)
109 allPackagesMap := make(map[string]bool) // map to deduplicate
110 allServicesMap := make(map[string]bool) // map to deduplicate
111 var allCommands []string
113 for _, group := range groups {
114 // Skip non-existent group directories
115 _, err := os.Stat(group)
116 if os.IsNotExist(err) {
120 files, err := config.LoadFiles(group)
124 err = config.LoadPermissions(group, files)
128 err = config.LoadTemplates(group, files,
129 s.host.Name, groups, s.allHosts, s.allGroups)
133 err = config.LoadTriggers(group, files)
137 for k, v := range files {
138 err := s.checkFileConflict(group, k, v,
139 allFiles, groupPriority)
147 packages, err := config.LoadPackages(group)
151 for _, x := range packages {
152 allPackagesMap[x] = true
155 services, err := config.LoadServices(group)
159 for _, x := range services {
160 allServicesMap[x] = true
163 commands, err := config.LoadCommands(group)
167 allCommands = append(allCommands, commands...)
170 resolveFileDirConflicts(allFiles)
172 var allPackages []string
173 var allServices []string
174 for x := range allPackagesMap {
175 allPackages = append(allPackages, x)
177 for x := range allServicesMap {
178 allServices = append(allServices, x)
180 // Sort for deterministic results
181 sort.Strings(allPackages)
182 sort.Strings(allServices)
184 return safcm.MsgSyncReq{
185 DryRun: s.config.DryRun,
188 Packages: allPackages,
189 Services: allServices,
190 Commands: allCommands,
194 // resolveHostGroups returns the groups and group priorities of the current
196 func (s *Sync) resolveHostGroups(detectedGroups []string) (
197 []string, map[string]int, error) {
199 groups, err := config.ResolveHostGroups(s.host.Name,
200 s.allGroups, detectedGroups)
205 // Early entries have higher priorities
206 groupPriority := make(map[string]int)
207 for i, x := range s.config.GroupOrder {
208 groupPriority[x] = i + 1
210 // Host itself always has highest priority
211 groupPriority[s.host.Name] = -1
213 // Sort groups after priority and name
214 sort.Slice(groups, func(i, j int) bool {
217 if groupPriority[a] > groupPriority[b] {
219 } else if groupPriority[a] < groupPriority[b] {
226 return groups, groupPriority, nil
229 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
230 allFiles map[string]*safcm.File, groupPriority map[string]int) error {
232 old, ok := allFiles[path]
237 newPrio := groupPriority[group]
238 oldPrio := groupPriority[old.OrigGroup]
239 if oldPrio > newPrio {
240 if old.Mode.IsDir() && file.Mode.IsDir() &&
241 old.TriggerCommands != nil {
242 s.logDebugf("files: %q: "+
243 "group %s overwrites triggers from group %s",
244 path, group, old.OrigGroup)
247 } else if oldPrio < newPrio {
248 // Should never happen, groups are sorted by priority
249 panic("invalid group priorities")
252 // Directories with default permissions and no triggers do not count
254 if file.Mode.IsDir() && file.Mode == old.Mode &&
255 config.FileModeToFullPerm(file.Mode) == 0755 &&
256 file.TriggerCommands == nil && old.TriggerCommands == nil {
260 return fmt.Errorf("groups %s and %s both provide file %q\n"+
261 "Use 'group_order' in config.yaml to declare preference",
262 group, old.OrigGroup, path)
265 func resolveFileDirConflicts(files map[string]*safcm.File) {
267 for x := range files {
268 paths = append(paths, x)
270 sort.Slice(paths, func(i, j int) bool {
271 return paths[i] < paths[j]
274 const sep = string(filepath.Separator)
276 // Remove invalid paths which can result from group_order overriding
277 // paths from another group (e.g. "/foo" as file from one group and
278 // "/foo/bar" from another).
280 for _, x := range paths {
283 !last.Mode.IsDir() &&
284 strings.HasPrefix(file.Path, last.Path+sep) {