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