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