1 // Config: parse groups.yaml
3 // SPDX-License-Identifier: GPL-3.0-or-later
4 // Copyright (C) 2021-2024 Simon Ruderich
20 GroupDetectedPrefix = "detected"
21 GroupSpecialSeparator = ":"
22 GroupRemoveSuffix = GroupSpecialSeparator + "remove"
25 // Keep in sync with cmd/safcm/sync_info.go:infoGroupDetectedRegexp
26 var groupNameRegexp = regexp.MustCompile(`^[a-z0-9_-]+$`)
28 func LoadGroups(cfg *Config, hosts *Hosts) (map[string][]string, error) {
29 const path = "groups.yaml"
31 var groups map[string][]string
32 x, err := os.ReadFile(path)
36 err = yaml.UnmarshalStrict(x, &groups)
38 return nil, fmt.Errorf("%s: failed to load: %v", path, err)
41 // Sanity checks; cannot expand groups yet because detected groups are
42 // only known after connecting to the host
43 for name, members := range groups {
44 errPrefix := fmt.Sprintf("%s: group %q:", path, name)
46 if name == GroupAll || name == GroupAll+GroupRemoveSuffix {
47 return nil, fmt.Errorf(
48 "%s conflict with pre-defined group %q",
51 if hosts.Map[name] != nil ||
52 hosts.Map[strings.TrimSuffix(name,
53 GroupRemoveSuffix)] != nil {
54 return nil, fmt.Errorf(
55 "%s conflict with existing host",
59 if strings.HasPrefix(name, GroupDetectedPrefix) {
60 return nil, fmt.Errorf(
61 "%s name must not start with %q "+
62 "(reserved for detected groups)",
63 errPrefix, GroupDetectedPrefix)
65 if !groupNameRegexp.MatchString(
66 strings.TrimSuffix(name, GroupRemoveSuffix)) {
67 return nil, fmt.Errorf(
68 "%s name contains invalid characters "+
70 errPrefix, groupNameRegexp)
73 for _, x := range members {
77 // Don't validate against groupNameRegexp because
78 // hosts have less strict restrictions.
79 if strings.Contains(x, GroupSpecialSeparator) {
80 return nil, fmt.Errorf(
81 "%s member %q must not contain %q",
82 errPrefix, x, GroupSpecialSeparator)
84 if strings.HasPrefix(x, GroupDetectedPrefix) {
87 if hosts.Map[x] != nil || groups[x] != nil {
90 return nil, fmt.Errorf("%s member %q not found",
95 // Sanity check for global configuration
96 for _, x := range cfg.GroupPriority {
97 const errPrefix = "config.yaml: group_priority:"
102 if strings.Contains(x, GroupSpecialSeparator) {
103 return nil, fmt.Errorf("%s invalid group name %q",
106 if strings.HasPrefix(x, GroupDetectedPrefix) {
109 if groups[x] != nil {
112 return nil, fmt.Errorf("%s group %q does not exist",
119 func ResolveHostGroups(host string, groups map[string][]string,
120 detectedGroups []string) ([]string, error) {
122 const maxRecursionDepth = 100
124 detectedGroupsMap := make(map[string]bool)
125 for _, x := range detectedGroups {
126 detectedGroupsMap[x] = true
130 // Recursively check if host belongs to this group (or any referenced
132 var lookup func(string, int) bool
133 lookup = func(group string, depth int) bool {
134 if depth > maxRecursionDepth {
138 for _, x := range groups[group] {
139 if x == host || detectedGroupsMap[x] || x == GroupAll {
142 if lookup(x, depth+1) &&
143 !lookup(x+GroupRemoveSuffix, depth+1) {
150 // Deterministic iteration order for error messages and tests
152 for x := range groups {
153 names = append(names, x)
158 for _, x := range names {
159 if strings.HasSuffix(x, GroupRemoveSuffix) {
163 if lookup(x, 0) && !lookup(x+GroupRemoveSuffix, 0) {
168 return nil, fmt.Errorf(
169 "groups.yaml: cycle while expanding group %q",
173 res = append(res, detectedGroups...)
174 res = append(res, GroupAll) // contains all hosts
175 res = append(res, host) // host itself is also group
181 // TransitivelyDetectedGroups returns all groups which depend on "detected"
182 // groups, either directly or by depending on groups which transitively depend
183 // on "detected" groups.
184 func TransitivelyDetectedGroups(groups map[string][]string) map[string]bool {
185 work := make(map[string][]string)
186 for k, v := range groups {
190 // Mark all groups which contain "detected" groups as long as new
191 // (transitive) "detected" groups are found.
192 detected := make(map[string]bool)
195 for group, members := range work {
196 for _, x := range members {
197 if !detected[x] && !strings.HasPrefix(x,
198 GroupDetectedPrefix) {
201 detected[strings.TrimSuffix(group,
202 GroupRemoveSuffix)] = true