]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/sync_sync.go
changes: refactor into separate function and add basic tests
[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         changes := s.formatChanges(resp)
48         if changes != "" {
49                 s.logf(safcm.LogInfo, true, "%s", changes)
50         }
51
52         if resp.Error != "" {
53                 return fmt.Errorf("%s", resp.Error)
54         }
55         return nil
56 }
57
58 func (s *Sync) hostSyncReq(detectedGroups []string) (
59         safcm.MsgSyncReq, error) {
60
61         var empty safcm.MsgSyncReq
62
63         groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
64         if err != nil {
65                 return empty, err
66         }
67         {
68                 // Don't leak internal group order which is confusing without
69                 // knowing the implementation details.
70                 groupsSorted := make([]string, len(groups))
71                 copy(groupsSorted, groups)
72                 sort.Strings(groupsSorted)
73                 s.logVerbosef("host groups: %s",
74                         strings.Join(groupsSorted, " "))
75
76                 // Don't leak internal priority values. Instead, order groups
77                 // by priority.
78                 var priorities []string
79                 for x := range groupPriority {
80                         priorities = append(priorities, x)
81                 }
82                 sort.Slice(priorities, func(i, j int) bool {
83                         a := priorities[i]
84                         b := priorities[j]
85                         return groupPriority[a] < groupPriority[b]
86                 })
87                 s.logVerbosef("host group priorities (desc. order): %v",
88                         strings.Join(priorities, " "))
89         }
90
91         allFiles := make(map[string]*safcm.File)
92         allPackagesMap := make(map[string]bool) // map to deduplicate
93         allServicesMap := make(map[string]bool) // map to deduplicate
94         var allCommands []string
95
96         for _, group := range groups {
97                 // Skip non-existent group directories
98                 _, err := os.Stat(group)
99                 if os.IsNotExist(err) {
100                         continue
101                 }
102
103                 files, err := config.LoadFiles(group)
104                 if err != nil {
105                         return empty, err
106                 }
107                 err = config.LoadPermissions(group, files)
108                 if err != nil {
109                         return empty, err
110                 }
111                 err = config.LoadTemplates(group, files,
112                         s.host.Name, groups, s.allHosts, s.allGroups)
113                 if err != nil {
114                         return empty, err
115                 }
116                 err = config.LoadTriggers(group, files)
117                 if err != nil {
118                         return empty, err
119                 }
120                 for k, v := range files {
121                         err := s.checkFileConflict(group, k, v,
122                                 allFiles, groupPriority)
123                         if err != nil {
124                                 return empty, err
125                         }
126                         v.OrigGroup = group
127                         allFiles[k] = v
128                 }
129
130                 packages, err := config.LoadPackages(group)
131                 if err != nil {
132                         return empty, err
133                 }
134                 for _, x := range packages {
135                         allPackagesMap[x] = true
136                 }
137
138                 services, err := config.LoadServices(group)
139                 if err != nil {
140                         return empty, err
141                 }
142                 for _, x := range services {
143                         allServicesMap[x] = true
144                 }
145
146                 commands, err := config.LoadCommands(group)
147                 if err != nil {
148                         return empty, err
149                 }
150                 allCommands = append(allCommands, commands...)
151         }
152
153         resolveFileDirConflicts(allFiles)
154
155         var allPackages []string
156         var allServices []string
157         for x := range allPackagesMap {
158                 allPackages = append(allPackages, x)
159         }
160         for x := range allServicesMap {
161                 allServices = append(allServices, x)
162         }
163         // Sort for deterministic results
164         sort.Strings(allPackages)
165         sort.Strings(allServices)
166
167         return safcm.MsgSyncReq{
168                 DryRun:   s.config.DryRun,
169                 Groups:   groups,
170                 Files:    allFiles,
171                 Packages: allPackages,
172                 Services: allServices,
173                 Commands: allCommands,
174         }, nil
175 }
176
177 // resolveHostGroups returns the groups and group priorities of the current
178 // host.
179 func (s *Sync) resolveHostGroups(detectedGroups []string) (
180         []string, map[string]int, error) {
181
182         groups, err := config.ResolveHostGroups(s.host.Name,
183                 s.allGroups, detectedGroups)
184         if err != nil {
185                 return nil, nil, err
186         }
187
188         // Early entries have higher priorities
189         groupPriority := make(map[string]int)
190         for i, x := range s.config.GroupOrder {
191                 groupPriority[x] = i + 1
192         }
193         // Host itself always has highest priority
194         groupPriority[s.host.Name] = -1
195
196         // Sort groups after priority and name
197         sort.Slice(groups, func(i, j int) bool {
198                 a := groups[i]
199                 b := groups[j]
200                 if groupPriority[a] > groupPriority[b] {
201                         return true
202                 } else if groupPriority[a] < groupPriority[b] {
203                         return false
204                 } else {
205                         return a < b
206                 }
207         })
208
209         return groups, groupPriority, nil
210 }
211
212 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
213         allFiles map[string]*safcm.File, groupPriority map[string]int) error {
214
215         old, ok := allFiles[path]
216         if !ok {
217                 return nil
218         }
219
220         newPrio := groupPriority[group]
221         oldPrio := groupPriority[old.OrigGroup]
222         if oldPrio > newPrio {
223                 if old.Mode.IsDir() && file.Mode.IsDir() &&
224                         old.TriggerCommands != nil {
225                         s.logDebugf("files: %q: "+
226                                 "group %s overwrites triggers from group %s",
227                                 path, group, old.OrigGroup)
228                 }
229                 return nil
230         } else if oldPrio < newPrio {
231                 // Should never happen, groups are sorted by priority
232                 panic("invalid group priorities")
233         }
234
235         // Directories with default permissions and no triggers do not count
236         // as conflict
237         if file.Mode.IsDir() && file.Mode == old.Mode &&
238                 config.FileModeToFullPerm(file.Mode) == 0755 &&
239                 file.TriggerCommands == nil && old.TriggerCommands == nil {
240                 return nil
241         }
242
243         return fmt.Errorf("groups %s and %s both provide file %q\n"+
244                 "Use 'group_order' in config.yaml to declare preference",
245                 group, old.OrigGroup, path)
246 }
247
248 func resolveFileDirConflicts(files map[string]*safcm.File) {
249         var paths []string
250         for x := range files {
251                 paths = append(paths, x)
252         }
253         sort.Slice(paths, func(i, j int) bool {
254                 return paths[i] < paths[j]
255         })
256
257         const sep = string(filepath.Separator)
258
259         // Remove invalid paths which can result from group_order overriding
260         // paths from another group (e.g. "/foo" as file from one group and
261         // "/foo/bar" from another).
262         var last *safcm.File
263         for _, x := range paths {
264                 file := files[x]
265                 if last != nil &&
266                         !last.Mode.IsDir() &&
267                         strings.HasPrefix(file.Path, last.Path+sep) {
268                         delete(files, x)
269                         continue
270                 }
271                 last = file
272         }
273 }