]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/sync_sync.go
safcm: move sync_changes.go and term.go to frontend package
[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         "sort"
25         "strings"
26
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"
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         c := frontend.Changes{
49                 DryRun: s.config.DryRun,
50                 Quiet:  s.config.Quiet,
51                 IsTTY:  s.isTTY,
52         }
53         changes := c.FormatChanges(resp)
54         if changes != "" {
55                 s.log(safcm.LogInfo, true, changes)
56         }
57
58         if resp.Error != "" {
59                 return fmt.Errorf("%s", resp.Error)
60         }
61         return nil
62 }
63
64 func (s *Sync) hostSyncReq(detectedGroups []string) (
65         safcm.MsgSyncReq, error) {
66
67         var empty safcm.MsgSyncReq
68
69         groups, groupPriority, err := s.resolveHostGroups(detectedGroups)
70         if err != nil {
71                 return empty, err
72         }
73         {
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, " "))
81
82                 // Don't leak internal priority values. Instead, order groups
83                 // by priority.
84                 var priorities []string
85                 for x := range groupPriority {
86                         priorities = append(priorities, x)
87                 }
88                 sort.Slice(priorities, func(i, j int) bool {
89                         a := priorities[i]
90                         b := priorities[j]
91                         return groupPriority[a] > groupPriority[b]
92                 })
93                 s.logVerbosef("host group priorities (descending): %v",
94                         strings.Join(priorities, " "))
95         }
96
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
101
102         for _, group := range groups {
103                 // Skip non-existent group directories
104                 _, err := os.Stat(group)
105                 if os.IsNotExist(err) {
106                         continue
107                 }
108
109                 files, err := config.LoadFiles(group)
110                 if err != nil {
111                         return empty, err
112                 }
113                 err = config.LoadPermissions(group, files)
114                 if err != nil {
115                         return empty, err
116                 }
117                 err = config.LoadTemplates(group, files,
118                         s.host.Name, groups, s.allHosts, s.allGroups)
119                 if err != nil {
120                         return empty, err
121                 }
122                 err = config.LoadTriggers(group, files)
123                 if err != nil {
124                         return empty, err
125                 }
126                 for k, v := range files {
127                         err := s.checkFileConflict(group, k, v,
128                                 allFiles, groupPriority)
129                         if err != nil {
130                                 return empty, err
131                         }
132                         v.OrigGroup = group
133                         allFiles[k] = v
134                 }
135
136                 packages, err := config.LoadPackages(group)
137                 if err != nil {
138                         return empty, err
139                 }
140                 for _, x := range packages {
141                         allPackagesMap[x] = true
142                 }
143
144                 services, err := config.LoadServices(group)
145                 if err != nil {
146                         return empty, err
147                 }
148                 for _, x := range services {
149                         allServicesMap[x] = true
150                 }
151
152                 commands, err := config.LoadCommands(group)
153                 if err != nil {
154                         return empty, err
155                 }
156                 allCommands = append(allCommands, commands...)
157         }
158
159         resolveFileDirConflicts(allFiles)
160
161         var allPackages []string
162         var allServices []string
163         for x := range allPackagesMap {
164                 allPackages = append(allPackages, x)
165         }
166         for x := range allServicesMap {
167                 allServices = append(allServices, x)
168         }
169         // Sort for deterministic results
170         sort.Strings(allPackages)
171         sort.Strings(allServices)
172
173         return safcm.MsgSyncReq{
174                 DryRun:   s.config.DryRun,
175                 Groups:   groups,
176                 Files:    allFiles,
177                 Packages: allPackages,
178                 Services: allServices,
179                 Commands: allCommands,
180         }, nil
181 }
182
183 // resolveHostGroups returns the groups and group priorities of the current
184 // host.
185 func (s *Sync) resolveHostGroups(detectedGroups []string) (
186         []string, map[string]int, error) {
187
188         groups, err := config.ResolveHostGroups(s.host.Name,
189                 s.allGroups, detectedGroups)
190         if err != nil {
191                 return nil, nil, err
192         }
193
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
198         }
199         // Host itself always has highest priority
200         groupPriority[s.host.Name] = math.MaxInt32
201
202         // Sort groups after priority and name
203         sort.Slice(groups, func(i, j int) bool {
204                 a := groups[i]
205                 b := groups[j]
206                 if groupPriority[a] < groupPriority[b] {
207                         return true
208                 } else if groupPriority[a] > groupPriority[b] {
209                         return false
210                 } else {
211                         return a < b
212                 }
213         })
214
215         return groups, groupPriority, nil
216 }
217
218 func (s *Sync) checkFileConflict(group string, path string, file *safcm.File,
219         allFiles map[string]*safcm.File, groupPriority map[string]int) error {
220
221         old, ok := allFiles[path]
222         if !ok {
223                 return nil
224         }
225
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)
234                 }
235                 return nil
236         } else if oldPrio > newPrio {
237                 // Should never happen, groups are sorted by priority
238                 panic("invalid group priorities")
239         }
240
241         // Directories with default permissions and no triggers do not count
242         // as conflict
243         if file.Mode.IsDir() && file.Mode == old.Mode &&
244                 config.FileModeToFullPerm(file.Mode) == 0755 &&
245                 file.TriggerCommands == nil && old.TriggerCommands == nil {
246                 return nil
247         }
248
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)
252 }
253
254 func resolveFileDirConflicts(files map[string]*safcm.File) {
255         var paths []string
256         for x := range files {
257                 paths = append(paths, x)
258         }
259         sort.Slice(paths, func(i, j int) bool {
260                 return paths[i] < paths[j]
261         })
262
263         // Slash separated paths are used for the configuration
264         const sep = "/"
265
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).
269         var last *safcm.File
270         for _, x := range paths {
271                 file := files[x]
272                 if last != nil &&
273                         !last.Mode.IsDir() &&
274                         strings.HasPrefix(file.Path, last.Path+sep) {
275                         delete(files, x)
276                         continue
277                 }
278                 last = file
279         }
280 }