]> ruderich.org/simon Gitweb - safcm/safcm.git/blob - cmd/safcm/config/groups.go
First working version
[safcm/safcm.git] / cmd / safcm / config / groups.go
1 // Config: parse groups.yaml
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 config
19
20 import (
21         "fmt"
22         "os"
23         "regexp"
24         "sort"
25         "strings"
26
27         "gopkg.in/yaml.v2"
28 )
29
30 const (
31         GroupAll              = "all"
32         GroupDetectedPrefix   = "detected"
33         GroupSpecialSeparator = ":"
34         GroupRemoveSuffix     = GroupSpecialSeparator + "remove"
35 )
36
37 // Keep in sync with sync_info.go:infoGroupDetectedRegexp
38 var groupNameRegexp = regexp.MustCompile(`^[a-z0-9_-]+$`)
39
40 func LoadGroups(cfg *Config, hosts *Hosts) (map[string][]string, error) {
41         const path = "groups.yaml"
42
43         var groups map[string][]string
44         x, err := os.ReadFile(path)
45         if err != nil {
46                 return nil, err
47         }
48         err = yaml.UnmarshalStrict(x, &groups)
49         if err != nil {
50                 return nil, fmt.Errorf("%s: failed to load: %v", path, err)
51         }
52
53         // Sanity checks; cannot expand groups yet because detected groups are
54         // only known after connecting to the host
55         for name, members := range groups {
56                 errPrefix := fmt.Sprintf("%s: group %q:", path, name)
57
58                 if name == GroupAll || name == GroupAll+GroupRemoveSuffix {
59                         return nil, fmt.Errorf(
60                                 "%s conflict with pre-defined group %q",
61                                 errPrefix, name)
62                 }
63                 if hosts.Map[name] != nil {
64                         return nil, fmt.Errorf(
65                                 "%s conflict with existing host",
66                                 errPrefix)
67                 }
68
69                 if strings.HasPrefix(name, GroupDetectedPrefix) {
70                         return nil, fmt.Errorf(
71                                 "%s name must not start with %q "+
72                                         "(reserved for detected groups)",
73                                 errPrefix, GroupDetectedPrefix)
74                 }
75                 if !groupNameRegexp.MatchString(
76                         strings.TrimSuffix(name, GroupRemoveSuffix)) {
77                         return nil, fmt.Errorf(
78                                 "%s name contains invalid characters "+
79                                         "(must match %s)",
80                                 errPrefix, groupNameRegexp)
81                 }
82
83                 for _, x := range members {
84                         if x == GroupAll {
85                                 continue
86                         }
87                         if strings.Contains(x, GroupSpecialSeparator) {
88                                 return nil, fmt.Errorf(
89                                         "%s member %q must not contain %q",
90                                         errPrefix, x, GroupSpecialSeparator)
91                         }
92                         if strings.HasPrefix(x, GroupDetectedPrefix) {
93                                 continue
94                         }
95                         if hosts.Map[x] != nil || groups[x] != nil {
96                                 continue
97                         }
98                         return nil, fmt.Errorf("%s group %q not found",
99                                 errPrefix, x)
100                 }
101         }
102
103         // Sanity check for global configuration
104         for _, x := range cfg.GroupOrder {
105                 const errPrefix = "config.yaml: group_order:"
106
107                 if x == GroupAll {
108                         continue
109                 }
110                 if strings.Contains(x, GroupSpecialSeparator) {
111                         return nil, fmt.Errorf("%s invalid group name %q",
112                                 errPrefix, x)
113                 }
114                 if strings.HasPrefix(x, GroupDetectedPrefix) {
115                         continue
116                 }
117                 if groups[x] != nil {
118                         continue
119                 }
120                 return nil, fmt.Errorf("%s group %q does not exist",
121                         errPrefix, x)
122         }
123
124         return groups, nil
125 }
126
127 func ResolveHostGroups(host string,
128         groups map[string][]string,
129         detectedGroups []string) ([]string, error) {
130
131         const maxDepth = 100
132
133         detectedGroupsMap := make(map[string]bool)
134         for _, x := range detectedGroups {
135                 detectedGroupsMap[x] = true
136         }
137
138         var cycle *string
139         // Recursively check if host belongs to this group (or any referenced
140         // groups).
141         var lookup func(string, int) bool
142         lookup = func(group string, depth int) bool {
143                 if depth > maxDepth {
144                         cycle = &group
145                         return false
146                 }
147                 for _, x := range groups[group] {
148                         if x == host || detectedGroupsMap[x] || x == GroupAll {
149                                 return true
150                         }
151                         if lookup(x, depth+1) &&
152                                 !lookup(x+GroupRemoveSuffix, depth+1) {
153                                 return true
154                         }
155                 }
156                 return false
157         }
158
159         // Deterministic iteration order for error messages and tests
160         var names []string
161         for x := range groups {
162                 names = append(names, x)
163         }
164         sort.Strings(names)
165
166         var res []string
167         for _, x := range names {
168                 if strings.HasSuffix(x, GroupRemoveSuffix) {
169                         continue
170                 }
171
172                 if lookup(x, 0) && !lookup(x+GroupRemoveSuffix, 0) {
173                         res = append(res, x)
174                 }
175         }
176         if cycle != nil {
177                 return nil, fmt.Errorf(
178                         "groups.yaml: cycle while expanding group %q",
179                         *cycle)
180         }
181
182         res = append(res, detectedGroups...)
183         res = append(res, GroupAll) // contains all hosts
184         res = append(res, host)     // host itself is also group
185
186         sort.Strings(res)
187         return res, nil
188 }