Skip to content

Commit 203039c

Browse files
author
Sergei Gureev
committed
fix: Group loop handling and other fixes
* Group loops no longer cause parsing to hang * "all" group no longer contains self * Add String() methods to group and hosts * Fix wrong method call in populateInventoryVars * Reconsile inventory method is now much more robust
1 parent 96c13c6 commit 203039c

File tree

5 files changed

+178
-23
lines changed

5 files changed

+178
-23
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type Group struct {
3333
func GroupMapListValues(mymap map[string]*Group) []*Group
3434
GroupMapListValues transforms map of Groups into Group list in lexical order
3535
36+
func (group Group) String() string
37+
3638
type Host struct {
3739
Name string
3840
Port int
@@ -46,6 +48,8 @@ type Host struct {
4648
func HostMapListValues(mymap map[string]*Host) []*Host
4749
HostMapListValues transforms map of Hosts into Host list in lexical order
4850
51+
func (host Host) String() string
52+
4953
type InventoryData struct {
5054
Groups map[string]*Group
5155
Hosts map[string]*Host
@@ -82,8 +86,13 @@ func (inventory *InventoryData) Match(m string) []*Host
8286
8387
func (inventory *InventoryData) Reconcile()
8488
Reconcile ensures inventory basic rules, run after updates After initial
85-
inventory file processing, only direct relationships are set This method
86-
sets Children and Parents
89+
inventory file processing, only direct relationships are set
90+
91+
This method:
92+
93+
* (re)sets Children and Parents for hosts and groups
94+
* ensures that mandatory groups exist
95+
* calculates variables for hosts and groups
8796
8897
```
8998

aini.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ func (inventory *InventoryData) GroupsToLower() {
147147
}
148148
}
149149

150+
func (group Group) String() string {
151+
return group.Name
152+
}
153+
154+
func (host Host) String() string {
155+
return host.Name
156+
}
157+
150158
func groupMapToLower(groups map[string]*Group, keysOnly bool) map[string]*Group {
151159
newGroups := make(map[string]*Group, len(groups))
152160
for groupname, group := range groups {

aini_test.go

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func TestGroupStructure(t *testing.T) {
7878
assert.Contains(t, v.Groups, "web")
7979
assert.Contains(t, v.Groups, "apache")
8080
assert.Contains(t, v.Groups, "nginx")
81+
assert.Contains(t, v.Groups, "all")
82+
assert.Contains(t, v.Groups, "ungrouped")
8183

8284
assert.Len(t, v.Groups, 5, "Five groups must be present: web, apache, nginx, all, ungrouped")
8385

@@ -111,6 +113,8 @@ func TestGroupNotExplicitlyDefined(t *testing.T) {
111113

112114
assert.Contains(t, v.Groups, "web")
113115
assert.Contains(t, v.Groups, "nginx")
116+
assert.Contains(t, v.Groups, "all")
117+
assert.Contains(t, v.Groups, "ungrouped")
114118

115119
assert.Len(t, v.Groups, 4, "Four groups must present: web, nginx, all, ungrouped")
116120

@@ -126,17 +130,60 @@ func TestGroupNotExplicitlyDefined(t *testing.T) {
126130
assert.Empty(t, v.Groups["ungrouped"].Hosts, "Group ungrouped should be empty")
127131
}
128132

133+
func TestAllGroup(t *testing.T) {
134+
v := parseString(t, `
135+
host7
136+
host5
137+
138+
[web:children]
139+
nginx
140+
apache
141+
142+
[web]
143+
host1
144+
host2
145+
146+
[nginx]
147+
host1
148+
host3
149+
host4
150+
151+
[apache]
152+
host5
153+
host6
154+
`)
155+
156+
allGroup := v.Groups["all"]
157+
assert.NotNil(t, allGroup)
158+
assert.Empty(t, allGroup.Parents)
159+
assert.NotContains(t, allGroup.Children, "all")
160+
assert.Len(t, allGroup.Children, 4)
161+
assert.Len(t, allGroup.Hosts, 7)
162+
for _, group := range v.Groups {
163+
if group.Name == "all" {
164+
continue
165+
}
166+
assert.Contains(t, allGroup.Children, group.Name)
167+
assert.Contains(t, group.Parents, allGroup.Name)
168+
}
169+
for _, host := range v.Hosts {
170+
assert.Contains(t, allGroup.Hosts, host.Name)
171+
assert.Contains(t, host.Groups, allGroup.Name)
172+
173+
}
174+
}
175+
129176
func TestHostExpansionFullNumericPattern(t *testing.T) {
130177
v := parseString(t, `
131178
host-[001:015:3]-web:23
132179
`)
133180

134-
assert.Len(t, v.Hosts, 5)
135181
assert.Contains(t, v.Hosts, "host-001-web")
136182
assert.Contains(t, v.Hosts, "host-004-web")
137183
assert.Contains(t, v.Hosts, "host-007-web")
138184
assert.Contains(t, v.Hosts, "host-010-web")
139185
assert.Contains(t, v.Hosts, "host-013-web")
186+
assert.Len(t, v.Hosts, 5)
140187

141188
for _, host := range v.Hosts {
142189
assert.Equalf(t, 23, host.Port, "%s port is set", host.Name)
@@ -148,46 +195,46 @@ func TestHostExpansionFullAlphabeticPattern(t *testing.T) {
148195
host-[a:o:3]-web
149196
`)
150197

151-
assert.Len(t, v.Hosts, 5)
152198
assert.Contains(t, v.Hosts, "host-a-web")
153199
assert.Contains(t, v.Hosts, "host-d-web")
154200
assert.Contains(t, v.Hosts, "host-g-web")
155201
assert.Contains(t, v.Hosts, "host-j-web")
156202
assert.Contains(t, v.Hosts, "host-m-web")
203+
assert.Len(t, v.Hosts, 5)
157204
}
158205

159206
func TestHostExpansionShortNumericPattern(t *testing.T) {
160207
v := parseString(t, `
161208
host-[:05]-web
162209
`)
163-
assert.Len(t, v.Hosts, 6)
164210
assert.Contains(t, v.Hosts, "host-00-web")
165211
assert.Contains(t, v.Hosts, "host-01-web")
166212
assert.Contains(t, v.Hosts, "host-02-web")
167213
assert.Contains(t, v.Hosts, "host-03-web")
168214
assert.Contains(t, v.Hosts, "host-04-web")
169215
assert.Contains(t, v.Hosts, "host-05-web")
216+
assert.Len(t, v.Hosts, 6)
170217
}
171218

172219
func TestHostExpansionShortAlphabeticPattern(t *testing.T) {
173220
v := parseString(t, `
174221
host-[a:c]-web
175222
`)
176-
assert.Len(t, v.Hosts, 3)
177223
assert.Contains(t, v.Hosts, "host-a-web")
178224
assert.Contains(t, v.Hosts, "host-b-web")
179225
assert.Contains(t, v.Hosts, "host-c-web")
226+
assert.Len(t, v.Hosts, 3)
180227
}
181228

182229
func TestHostExpansionMultiplePatterns(t *testing.T) {
183230
v := parseString(t, `
184231
host-[1:2]-[a:b]-web
185232
`)
186-
assert.Len(t, v.Hosts, 4)
187233
assert.Contains(t, v.Hosts, "host-1-a-web")
188234
assert.Contains(t, v.Hosts, "host-1-b-web")
189235
assert.Contains(t, v.Hosts, "host-2-a-web")
190236
assert.Contains(t, v.Hosts, "host-2-b-web")
237+
assert.Len(t, v.Hosts, 4)
191238
}
192239

193240
func TestVariablesPriority(t *testing.T) {
@@ -324,6 +371,27 @@ func TestGroupsAndHostsToLower(t *testing.T) {
324371
assert.Contains(t, v.Groups["tomcat"].Hosts, "tomcat-1")
325372
}
326373

374+
func TestGroupLoops(t *testing.T) {
375+
v := parseString(t, `
376+
[group1]
377+
host1
378+
379+
[group1:children]
380+
group2
381+
382+
[group2:children]
383+
group1
384+
`)
385+
386+
assert.Contains(t, v.Groups, "group1")
387+
assert.Contains(t, v.Groups, "group2")
388+
assert.Contains(t, v.Groups["group1"].Parents, "all")
389+
assert.Contains(t, v.Groups["group1"].Parents, "group2")
390+
assert.NotContains(t, v.Groups["group1"].Parents, "group1")
391+
assert.Len(t, v.Groups["group1"].Parents, 2)
392+
assert.Contains(t, v.Groups["group2"].Parents, "group1")
393+
}
394+
327395
func TestVariablesEscaping(t *testing.T) {
328396
v := parseString(t, `
329397
host ansible_ssh_common_args="-o ProxyCommand='ssh -W %h:%p somehost'" other_var_same_value="-o ProxyCommand='ssh -W %h:%p somehost'" # comment

inventory.go

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,89 @@ package aini
22

33
// Inventory-related helper methods
44

5-
// Reconcile ensures inventory basic rules, run after updates
6-
// After initial inventory file processing, only direct relationships are set
7-
// This method sets Children and Parents
5+
// Reconcile ensures inventory basic rules, run after updates.
6+
// After initial inventory file processing, only direct relationships are set.
7+
//
8+
// This method:
9+
// * (re)sets Children and Parents for hosts and groups
10+
// * ensures that mandatory groups exist
11+
// * calculates variables for hosts and groups
812
func (inventory *InventoryData) Reconcile() {
13+
// Clear all computed data
14+
for _, host := range inventory.Hosts {
15+
host.clearData()
16+
}
17+
// a group can be empty (with no hosts in it), so the previous method will not clean it
18+
// on the other hand, a group could have been attached to a host by a user, but not added to the inventory.Groups map
19+
// so it's safer just to clean everything
20+
for _, group := range inventory.Groups {
21+
group.clearData(make(map[string]struct{}, len(inventory.Groups)))
22+
}
23+
924
allGroup := inventory.getOrCreateGroup("all")
10-
allGroup.Hosts = inventory.Hosts
11-
allGroup.Children = inventory.Groups
25+
ungroupedGroup := inventory.getOrCreateGroup("ungrouped")
26+
ungroupedGroup.directParents[allGroup.Name] = allGroup
27+
28+
// First, ensure that inventory.Groups contains all the groups
29+
for _, host := range inventory.Hosts {
30+
for _, group := range host.directGroups {
31+
inventory.Groups[group.Name] = group
32+
for _, ancestor := range group.getAncestors() {
33+
inventory.Groups[ancestor.Name] = ancestor
34+
}
35+
}
36+
}
37+
38+
// Calculate intergroup relationships
39+
for _, group := range inventory.Groups {
40+
group.directParents[allGroup.Name] = allGroup
41+
for _, ancestor := range group.getAncestors() {
42+
group.Parents[ancestor.Name] = ancestor
43+
ancestor.Children[group.Name] = group
44+
}
45+
}
1246

47+
// Now set hosts for groups and groups for hosts
1348
for _, host := range inventory.Hosts {
49+
host.Groups[allGroup.Name] = allGroup
1450
for _, group := range host.directGroups {
1551
group.Hosts[host.Name] = host
1652
host.Groups[group.Name] = group
17-
group.directParents[allGroup.Name] = allGroup
18-
for _, ancestor := range group.getAncestors() {
19-
group.Parents[ancestor.Name] = ancestor
20-
ancestor.Children[group.Name] = group
21-
ancestor.Hosts[host.Name] = host
22-
host.Groups[ancestor.Name] = ancestor
53+
for _, parent := range group.Parents {
54+
group.Parents[parent.Name] = parent
55+
parent.Children[group.Name] = group
56+
parent.Hosts[host.Name] = host
57+
host.Groups[parent.Name] = parent
2358
}
2459
}
2560
}
2661
inventory.reconcileVars()
2762
}
2863

64+
func (host *Host) clearData() {
65+
host.Groups = make(map[string]*Group)
66+
host.Vars = make(map[string]string)
67+
for _, group := range host.directGroups {
68+
group.clearData(make(map[string]struct{}, len(host.Groups)))
69+
}
70+
}
71+
72+
func (group *Group) clearData(visited map[string]struct{}) {
73+
if _, ok := visited[group.Name]; ok {
74+
return
75+
}
76+
group.Hosts = make(map[string]*Host)
77+
group.Parents = make(map[string]*Group)
78+
group.Children = make(map[string]*Group)
79+
group.Vars = make(map[string]string)
80+
group.allInventoryVars = nil
81+
group.allFileVars = nil
82+
visited[group.Name] = struct{}{}
83+
for _, parent := range group.directParents {
84+
parent.clearData(visited)
85+
}
86+
}
87+
2988
// getOrCreateGroup return group from inventory if exists or creates empty Group with given name
3089
func (inventory *InventoryData) getOrCreateGroup(groupName string) *Group {
3190
if group, ok := inventory.Groups[groupName]; ok {
@@ -68,13 +127,24 @@ func (inventory *InventoryData) getOrCreateHost(hostName string) *Host {
68127
// getAncestors returns all Ancestors of a given group in level order
69128
func (group *Group) getAncestors() []*Group {
70129
result := make([]*Group, 0)
130+
if len(group.directParents) == 0 {
131+
return result
132+
}
133+
visited := map[string]struct{}{group.Name: {}}
71134

72-
for queue := []*Group{group}; ; {
135+
for queue := GroupMapListValues(group.directParents); ; {
73136
group := queue[0]
74-
parentList := GroupMapListValues(group.directParents)
75-
result = append(result, parentList...)
76137
copy(queue, queue[1:])
77138
queue = queue[:len(queue)-1]
139+
if _, ok := visited[group.Name]; ok {
140+
if len(queue) == 0 {
141+
return result
142+
}
143+
continue
144+
}
145+
visited[group.Name] = struct{}{}
146+
parentList := GroupMapListValues(group.directParents)
147+
result = append(result, group)
78148
queue = append(queue, parentList...)
79149

80150
if len(queue) == 0 {

vars.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (inventory *InventoryData) AddVars(path string) error {
1919
return inventory.doAddVars(path, false)
2020
}
2121

22-
// AddVarsLowerCased does the same as AddVars, but converts hostnames and groups name to lowercase
22+
// AddVarsLowerCased does the same as AddVars, but converts hostnames and groups name to lowercase.
2323
// Use this function if you've executed `inventory.HostsToLower` or `inventory.GroupsToLower`
2424
func (inventory *InventoryData) AddVarsLowerCased(path string) error {
2525
return inventory.doAddVars(path, true)
@@ -176,7 +176,7 @@ func (group *Group) populateInventoryVars() {
176176
}
177177
group.allInventoryVars = make(map[string]string)
178178
for _, parent := range GroupMapListValues(group.directParents) {
179-
parent.populateFileVars()
179+
parent.populateInventoryVars()
180180
addValues(group.allInventoryVars, parent.allInventoryVars)
181181
}
182182
addValues(group.allInventoryVars, group.inventoryVars)

0 commit comments

Comments
 (0)