1 // Config: parse groups.yaml
3 // Copyright (C) 2021 Simon Ruderich
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.
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.
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/>.
32 GroupDetectedPrefix = "detected"
33 GroupSpecialSeparator = ":"
34 GroupRemoveSuffix = GroupSpecialSeparator + "remove"
37 // Keep in sync with sync_info.go:infoGroupDetectedRegexp
38 var groupNameRegexp = regexp.MustCompile(`^[a-z0-9_-]+$`)
40 func LoadGroups(cfg *Config, hosts *Hosts) (map[string][]string, error) {
41 const path = "groups.yaml"
43 var groups map[string][]string
44 x, err := os.ReadFile(path)
48 err = yaml.UnmarshalStrict(x, &groups)
50 return nil, fmt.Errorf("%s: failed to load: %v", path, err)
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)
58 if name == GroupAll || name == GroupAll+GroupRemoveSuffix {
59 return nil, fmt.Errorf(
60 "%s conflict with pre-defined group %q",
63 if hosts.Map[name] != nil ||
64 hosts.Map[strings.TrimSuffix(name,
65 GroupRemoveSuffix)] != nil {
66 return nil, fmt.Errorf(
67 "%s conflict with existing host",
71 if strings.HasPrefix(name, GroupDetectedPrefix) {
72 return nil, fmt.Errorf(
73 "%s name must not start with %q "+
74 "(reserved for detected groups)",
75 errPrefix, GroupDetectedPrefix)
77 if !groupNameRegexp.MatchString(
78 strings.TrimSuffix(name, GroupRemoveSuffix)) {
79 return nil, fmt.Errorf(
80 "%s name contains invalid characters "+
82 errPrefix, groupNameRegexp)
85 for _, x := range members {
89 if strings.Contains(x, GroupSpecialSeparator) {
90 return nil, fmt.Errorf(
91 "%s member %q must not contain %q",
92 errPrefix, x, GroupSpecialSeparator)
94 if strings.HasPrefix(x, GroupDetectedPrefix) {
97 if hosts.Map[x] != nil || groups[x] != nil {
100 return nil, fmt.Errorf("%s group %q not found",
105 // Sanity check for global configuration
106 for _, x := range cfg.GroupOrder {
107 const errPrefix = "config.yaml: group_order:"
112 if strings.Contains(x, GroupSpecialSeparator) {
113 return nil, fmt.Errorf("%s invalid group name %q",
116 if strings.HasPrefix(x, GroupDetectedPrefix) {
119 if groups[x] != nil {
122 return nil, fmt.Errorf("%s group %q does not exist",
129 func ResolveHostGroups(host string,
130 groups map[string][]string,
131 detectedGroups []string) ([]string, error) {
135 detectedGroupsMap := make(map[string]bool)
136 for _, x := range detectedGroups {
137 detectedGroupsMap[x] = true
141 // Recursively check if host belongs to this group (or any referenced
143 var lookup func(string, int) bool
144 lookup = func(group string, depth int) bool {
145 if depth > maxDepth {
149 for _, x := range groups[group] {
150 if x == host || detectedGroupsMap[x] || x == GroupAll {
153 if lookup(x, depth+1) &&
154 !lookup(x+GroupRemoveSuffix, depth+1) {
161 // Deterministic iteration order for error messages and tests
163 for x := range groups {
164 names = append(names, x)
169 for _, x := range names {
170 if strings.HasSuffix(x, GroupRemoveSuffix) {
174 if lookup(x, 0) && !lookup(x+GroupRemoveSuffix, 0) {
179 return nil, fmt.Errorf(
180 "groups.yaml: cycle while expanding group %q",
184 res = append(res, detectedGroups...)
185 res = append(res, GroupAll) // contains all hosts
186 res = append(res, host) // host itself is also group
192 // TransitivelyDetectedGroups returns all groups which depend on "detected"
193 // groups, either directly or by depending on groups which transitively depend
194 // on "detected" groups.
195 func TransitivelyDetectedGroups(groups map[string][]string) []string {
196 work := make(map[string][]string)
197 for k, v := range groups {
201 // Mark all groups which contain "detected" groups as long as new
202 // (transitive) "detected" groups are found.
203 detected := make(map[string]bool)
206 for group, members := range work {
207 for _, x := range members {
208 if !detected[x] && !strings.HasPrefix(x,
209 GroupDetectedPrefix) {
212 detected[strings.TrimSuffix(group,
213 GroupRemoveSuffix)] = true
224 for x := range detected {