]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/sync_sync.go
First working version
[safcm/safcm.git] / cmd / safcm / sync_sync.go
1 // "sync" sub-command: sync files
2
3 // Copyright (C) 2021  Simon Ruderich
4 //
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.
9 //
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.
14 //
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/>.
17
18 package main
19
20 import (
21         "fmt"
22         "os"
23         "path/filepath"
24         "sort"
25         "strings"
26
27         "ruderich.org/simon/safcm"
28         "ruderich.org/simon/safcm/cmd/safcm/config"
29         "ruderich.org/simon/safcm/rpc"
30 )
31
32 func (s *Sync) hostSync(conn *rpc.Conn, detectedGroups []string) error {
33         req, err := s.hostSyncReq(detectedGroups)
34         if err != nil {
35                 return err
36         }
37         x, err := s.sendRecv(conn, req)
38         if err != nil {
39                 return err
40         }
41         resp, ok := x.(safcm.MsgSyncResp)
42         if !ok {
43                 return fmt.Errorf("unexpected response %v", x)
44         }
45
46         // Display changes
47         var changes []string
48         if len(resp.FileChanges) > 0 {
49                 changes = append(changes,
50                         s.formatFileChanges(resp.FileChanges))
51         }
52         if len(resp.PackageChanges) > 0 {
53                 changes = append(changes,
54                         s.formatPackageChanges(resp.PackageChanges))
55         }
56         if len(resp.ServiceChanges) > 0 {
57                 changes = append(changes,
58                         s.formatServiceChanges(resp.ServiceChanges))
59         }
60         if len(resp.CommandChanges) > 0 {
61                 changes = append(changes,
62                         s.formatCommandChanges(resp.CommandChanges))
63         }
64         if len(changes) > 0 {
65                 s.logf(safcm.LogInfo, true, "%s",
66                         "\n"+strings.Join(changes, "\n"))
67         }
68
69         if resp.Error != "" {
70                 return fmt.Errorf("%s", resp.Error)
71         }
72         return nil
73 }
74
75 func (s *Sync) hostSyncReq(detectedGroups []string) (
76         safcm.MsgSyncReq, error) {
77
78         var empty safcm.MsgSyncReq
79
80         groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
81         if err != nil {
82                 return empty, err
83         }
84         {
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, " "))
92
93                 // Don't leak internal priority values. Instead, order groups
94                 // by priority.
95                 var priorities []string
96                 for x := range groupPriority {
97                         priorities = append(priorities, x)
98                 }
99                 sort.Slice(priorities, func(i, j int) bool {
100                         a := priorities[i]
101                         b := priorities[j]
102                         return groupPriority[a] < groupPriority[b]
103                 })
104                 s.logVerbosef("host group priorities (desc. order): %v",
105                         strings.Join(priorities, " "))
106         }
107
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
112
113         for _, group := range groups {
114                 // Skip non-existent group directories
115                 _, err := os.Stat(group)
116                 if os.IsNotExist(err) {
117                         continue
118                 }
119
120                 files, err := config.LoadFiles(group)
121                 if err != nil {
122                         return empty, err
123                 }
124                 err = config.LoadPermissions(group, files)
125                 if err != nil {
126                         return empty, err
127                 }
128                 err = config.LoadTemplates(group, files,
129                         s.host.Name, groups, s.allHosts, s.allGroups)
130                 if err != nil {
131                         return empty, err
132                 }
133                 err = config.LoadTriggers(group, files)
134                 if err != nil {
135                         return empty, err
136                 }
137                 for k, v := range files {
138                         err := s.checkFileConflict(group, k, v,
139                                 allFiles, groupPriority)
140                         if err != nil {
141                                 return empty, err
142                         }
143                         v.OrigGroup = group
144                         allFiles[k] = v
145                 }
146
147                 packages, err := config.LoadPackages(group)
148                 if err != nil {
149                         return empty, err
150                 }
151                 for _, x := range packages {
152                         allPackagesMap[x] = true
153                 }
154
155                 services, err := config.LoadServices(group)
156                 if err != nil {
157                         return empty, err
158                 }
159                 for _, x := range services {
160                         allServicesMap[x] = true
161                 }
162
163                 commands, err := config.LoadCommands(group)
164                 if err != nil {
165                         return empty, err
166                 }
167                 allCommands = append(allCommands, commands...)
168         }
169
170         resolveFileDirConflicts(allFiles)
171
172         var allPackages []string
173         var allServices []string
174         for x := range allPackagesMap {
175                 allPackages = append(allPackages, x)
176         }
177         for x := range allServicesMap {
178                 allServices = append(allServices, x)
179         }
180         // Sort for deterministic results
181         sort.Strings(allPackages)
182         sort.Strings(allServices)
183
184         return safcm.MsgSyncReq{
185                 DryRun:   s.config.DryRun,
186                 Groups:   groups,
187                 Files:    allFiles,
188                 Packages: allPackages,
189                 Services: allServices,
190                 Commands: allCommands,
191         }, nil
192 }
193
194 // resolveHostGroups returns the groups and group priorities of the current
195 // host.
196 func (s *Sync) resolveHostGroups(detectedGroups []string) (
197         []string, map[string]int, error) {
198
199         groups, err := config.ResolveHostGroups(s.host.Name,
200                 s.allGroups, detectedGroups)
201         if err != nil {
202                 return nil, nil, err
203         }
204
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
209         }
210         // Host itself always has highest priority
211         groupPriority[s.host.Name] = -1
212
213         // Sort groups after priority and name
214         sort.Slice(groups, func(i, j int) bool {
215                 a := groups[i]
216                 b := groups[j]
217                 if groupPriority[a] > groupPriority[b] {
218                         return true
219                 } else if groupPriority[a] < groupPriority[b] {
220                         return false
221                 } else {
222                         return a < b
223                 }
224         })
225
226         return groups, groupPriority, nil
227 }
228
229 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
230         allFiles map[string]*safcm.File, groupPriority map[string]int) error {
231
232         old, ok := allFiles[path]
233         if !ok {
234                 return nil
235         }
236
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)
245                 }
246                 return nil
247         } else if oldPrio < newPrio {
248                 // Should never happen, groups are sorted by priority
249                 panic("invalid group priorities")
250         }
251
252         // Directories with default permissions and no triggers do not count
253         // as conflict
254         if file.Mode.IsDir() && file.Mode == old.Mode &&
255                 config.FileModeToFullPerm(file.Mode) == 0755 &&
256                 file.TriggerCommands == nil && old.TriggerCommands == nil {
257                 return nil
258         }
259
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)
263 }
264
265 func resolveFileDirConflicts(files map[string]*safcm.File) {
266         var paths []string
267         for x := range files {
268                 paths = append(paths, x)
269         }
270         sort.Slice(paths, func(i, j int) bool {
271                 return paths[i] < paths[j]
272         })
273
274         const sep = string(filepath.Separator)
275
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).
279         var last *safcm.File
280         for _, x := range paths {
281                 file := files[x]
282                 if last != nil &&
283                         !last.Mode.IsDir() &&
284                         strings.HasPrefix(file.Path, last.Path+sep) {
285                         delete(files, x)
286                         continue
287                 }
288                 last = file
289         }
290 }