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