204 lines
6 KiB
Go
204 lines
6 KiB
Go
package parser
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
// rawGroup — форма группы прямо как она лежит в YAML-инвентаре.
|
||
type rawGroup struct {
|
||
Hosts map[string]map[string]interface{} `yaml:"hosts,omitempty"`
|
||
Vars map[string]interface{} `yaml:"vars,omitempty"`
|
||
Children map[string]rawGroup `yaml:"children,omitempty"`
|
||
}
|
||
|
||
// LoadInventory читает YAML-инвентарь (в стиле `all: children: ...`)
|
||
// и разворачивает его в плоскую карту групп с явным списком children.
|
||
func LoadInventory(path string) (*Inventory, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("читаю инвентарь: %w", err)
|
||
}
|
||
|
||
var root map[string]rawGroup
|
||
if err := yaml.Unmarshal(data, &root); err != nil {
|
||
return nil, fmt.Errorf("парсинг инвентаря: %w", err)
|
||
}
|
||
|
||
inv := &Inventory{
|
||
Groups: map[string]*Group{},
|
||
Dir: filepath.Dir(path),
|
||
}
|
||
for name, rg := range root {
|
||
flatten(inv, name, rg)
|
||
}
|
||
return inv, nil
|
||
}
|
||
|
||
// flatten рекурсивно разворачивает rawGroup (с вложенными children)
|
||
// в плоский inv.Groups, попутно строя список Children для каждой группы —
|
||
// это то дерево, по которому потом считается порядок наложения group_vars.
|
||
func flatten(inv *Inventory, name string, rg rawGroup) {
|
||
group, ok := inv.Groups[name]
|
||
if !ok {
|
||
group = &Group{Name: name, Hosts: map[string]*Host{}, Vars: map[string]interface{}{}}
|
||
inv.Groups[name] = group
|
||
}
|
||
for k, v := range rg.Vars {
|
||
group.Vars[k] = v
|
||
}
|
||
for hostName, hostVars := range rg.Hosts {
|
||
h, ok := group.Hosts[hostName]
|
||
if !ok {
|
||
h = &Host{Name: hostName, Vars: map[string]interface{}{}}
|
||
group.Hosts[hostName] = h
|
||
}
|
||
for k, v := range hostVars {
|
||
h.Vars[k] = v
|
||
}
|
||
}
|
||
for childName, childGroup := range rg.Children {
|
||
group.Children = append(group.Children, childName)
|
||
flatten(inv, childName, childGroup)
|
||
}
|
||
}
|
||
|
||
// GroupsOf возвращает имена всех групп, в которые входит хост,
|
||
// включая родительские группы через children (транзитивно).
|
||
func (inv *Inventory) GroupsOf(hostName string) []string {
|
||
seen := map[string]bool{}
|
||
for name, g := range inv.Groups {
|
||
if _, ok := g.Hosts[hostName]; ok {
|
||
markAncestors(inv, name, seen)
|
||
}
|
||
}
|
||
out := make([]string, 0, len(seen))
|
||
for name := range seen {
|
||
out = append(out, name)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// markAncestors помечает саму группу и всех её "родителей" (группы,
|
||
// у которых она указана в children) как содержащие данный хост.
|
||
func markAncestors(inv *Inventory, group string, seen map[string]bool) {
|
||
if seen[group] {
|
||
return
|
||
}
|
||
seen[group] = true
|
||
for name, g := range inv.Groups {
|
||
for _, child := range g.Children {
|
||
if child == group {
|
||
markAncestors(inv, name, seen)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// depth — расстояние группы от "all" по дереву children (all = 0).
|
||
func (inv *Inventory) depth(group string) int {
|
||
if group == "all" {
|
||
return 0
|
||
}
|
||
best := -1
|
||
for name, g := range inv.Groups {
|
||
for _, child := range g.Children {
|
||
if child == group {
|
||
d := inv.depth(name) + 1
|
||
if best == -1 || d < best {
|
||
best = d
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if best == -1 {
|
||
return 0 // группа без родителя — считаем что она на уровне all
|
||
}
|
||
return best
|
||
}
|
||
|
||
// GroupChain возвращает группы хоста в порядке применения group_vars:
|
||
// от самых общих (all, глубина 0) к самым специфичным; на одном уровне
|
||
// глубины — по алфавиту, чтобы результат был детерминирован.
|
||
func (inv *Inventory) GroupChain(hostName string) []string {
|
||
groups := inv.GroupsOf(hostName)
|
||
sort.Slice(groups, func(i, j int) bool {
|
||
di, dj := inv.depth(groups[i]), inv.depth(groups[j])
|
||
if di != dj {
|
||
return di < dj
|
||
}
|
||
return groups[i] < groups[j]
|
||
})
|
||
return groups
|
||
}
|
||
|
||
// GroupVars отдаёт переменные группы из самого инвентаря, смешанные
|
||
// с файлами group_vars/<name>.yml и group_vars/<name>/*.yml с диска.
|
||
func (inv *Inventory) GroupVars(name string) map[string]interface{} {
|
||
out := map[string]interface{}{}
|
||
if g, ok := inv.Groups[name]; ok {
|
||
for k, v := range g.Vars {
|
||
out[k] = v
|
||
}
|
||
}
|
||
for k, v := range inv.loadVarsFromDisk("group_vars", name) {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// HostVars — аналогично GroupVars, но для конкретного хоста
|
||
// (host_vars/<name>.yml или host_vars/<name>/*.yml).
|
||
func (inv *Inventory) HostVars(name string) map[string]interface{} {
|
||
out := map[string]interface{}{}
|
||
for _, g := range inv.Groups {
|
||
if h, ok := g.Hosts[name]; ok {
|
||
for k, v := range h.Vars {
|
||
out[k] = v
|
||
}
|
||
}
|
||
}
|
||
for k, v := range inv.loadVarsFromDisk("host_vars", name) {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// loadVarsFromDisk грузит либо <kind>/<name>.yml, либо все файлы
|
||
// в директории <kind>/<name>/*.yml по алфавиту, мерджа их по порядку.
|
||
func (inv *Inventory) loadVarsFromDisk(kind, name string) map[string]interface{} {
|
||
out := map[string]interface{}{}
|
||
|
||
singleFile := filepath.Join(inv.Dir, kind, name+".yml")
|
||
if vars, err := loadVarsFile(singleFile); err == nil {
|
||
for k, v := range vars {
|
||
out[k] = v
|
||
}
|
||
}
|
||
|
||
dir := filepath.Join(inv.Dir, kind, name)
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return out
|
||
}
|
||
names := make([]string, 0, len(entries))
|
||
for _, e := range entries {
|
||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".yml") {
|
||
names = append(names, e.Name())
|
||
}
|
||
}
|
||
sort.Strings(names)
|
||
for _, n := range names {
|
||
if vars, err := loadVarsFile(filepath.Join(dir, n)); err == nil {
|
||
for k, v := range vars {
|
||
out[k] = v
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|