// Config: parse groups.yaml // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2021-2024 Simon Ruderich package config import ( "fmt" "os" "regexp" "sort" "strings" "gopkg.in/yaml.v2" ) const ( GroupAll = "all" GroupDetectedPrefix = "detected" GroupSpecialSeparator = ":" GroupRemoveSuffix = GroupSpecialSeparator + "remove" ) // Keep in sync with cmd/safcm/sync_info.go:infoGroupDetectedRegexp var groupNameRegexp = regexp.MustCompile(`^[a-z0-9_-]+$`) func LoadGroups(cfg *Config, hosts *Hosts) (map[string][]string, error) { const path = "groups.yaml" var groups map[string][]string x, err := os.ReadFile(path) if err != nil { return nil, err } err = yaml.UnmarshalStrict(x, &groups) if err != nil { return nil, fmt.Errorf("%s: failed to load: %v", path, err) } // Sanity checks; cannot expand groups yet because detected groups are // only known after connecting to the host for name, members := range groups { errPrefix := fmt.Sprintf("%s: group %q:", path, name) if name == GroupAll || name == GroupAll+GroupRemoveSuffix { return nil, fmt.Errorf( "%s conflict with pre-defined group %q", errPrefix, name) } if hosts.Map[name] != nil || hosts.Map[strings.TrimSuffix(name, GroupRemoveSuffix)] != nil { return nil, fmt.Errorf( "%s conflict with existing host", errPrefix) } if strings.HasPrefix(name, GroupDetectedPrefix) { return nil, fmt.Errorf( "%s name must not start with %q "+ "(reserved for detected groups)", errPrefix, GroupDetectedPrefix) } if !groupNameRegexp.MatchString( strings.TrimSuffix(name, GroupRemoveSuffix)) { return nil, fmt.Errorf( "%s name contains invalid characters "+ "(must match %s)", errPrefix, groupNameRegexp) } for _, x := range members { if x == GroupAll { continue } // Don't validate against groupNameRegexp because // hosts have less strict restrictions. if strings.Contains(x, GroupSpecialSeparator) { return nil, fmt.Errorf( "%s member %q must not contain %q", errPrefix, x, GroupSpecialSeparator) } if strings.HasPrefix(x, GroupDetectedPrefix) { continue } if hosts.Map[x] != nil || groups[x] != nil { continue } return nil, fmt.Errorf("%s member %q not found", errPrefix, x) } } // Sanity check for global configuration for _, x := range cfg.GroupPriority { const errPrefix = "config.yaml: group_priority:" if x == GroupAll { continue } if strings.Contains(x, GroupSpecialSeparator) { return nil, fmt.Errorf("%s invalid group name %q", errPrefix, x) } if strings.HasPrefix(x, GroupDetectedPrefix) { continue } if groups[x] != nil { continue } return nil, fmt.Errorf("%s group %q does not exist", errPrefix, x) } return groups, nil } func ResolveHostGroups(host string, groups map[string][]string, detectedGroups []string) ([]string, error) { const maxRecursionDepth = 100 detectedGroupsMap := make(map[string]bool) for _, x := range detectedGroups { detectedGroupsMap[x] = true } var cycle *string // Recursively check if host belongs to this group (or any referenced // groups). var lookup func(string, int) bool lookup = func(group string, depth int) bool { if depth > maxRecursionDepth { cycle = &group return false } for _, x := range groups[group] { if x == host || detectedGroupsMap[x] || x == GroupAll { return true } if lookup(x, depth+1) && !lookup(x+GroupRemoveSuffix, depth+1) { return true } } return false } // Deterministic iteration order for error messages and tests var names []string for x := range groups { names = append(names, x) } sort.Strings(names) var res []string for _, x := range names { if strings.HasSuffix(x, GroupRemoveSuffix) { continue } if lookup(x, 0) && !lookup(x+GroupRemoveSuffix, 0) { res = append(res, x) } } if cycle != nil { return nil, fmt.Errorf( "groups.yaml: cycle while expanding group %q", *cycle) } res = append(res, detectedGroups...) res = append(res, GroupAll) // contains all hosts res = append(res, host) // host itself is also group sort.Strings(res) return res, nil } // TransitivelyDetectedGroups returns all groups which depend on "detected" // groups, either directly or by depending on groups which transitively depend // on "detected" groups. func TransitivelyDetectedGroups(groups map[string][]string) map[string]bool { work := make(map[string][]string) for k, v := range groups { work[k] = v } // Mark all groups which contain "detected" groups as long as new // (transitive) "detected" groups are found. detected := make(map[string]bool) for { change := false for group, members := range work { for _, x := range members { if !detected[x] && !strings.HasPrefix(x, GroupDetectedPrefix) { continue } detected[strings.TrimSuffix(group, GroupRemoveSuffix)] = true delete(work, group) change = true } } if !change { break } } return detected }