Skip to content

Commit 56431d2

Browse files
Merge pull request #400 from rundeck-plugins/issue_rpl-36_child-groups
RPL-36: Fix - Ansible process sub children
2 parents 9ed488a + 74b41ff commit 56431d2

File tree

7 files changed

+245
-32
lines changed

7 files changed

+245
-32
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package functional
2+
3+
import functional.base.BaseTestConfiguration
4+
import org.testcontainers.spock.Testcontainers
5+
6+
@Testcontainers
7+
class ChildGroupsSpec extends BaseTestConfiguration {
8+
9+
static String NODENAME = 'nodename'
10+
static String HOSTNAME = 'hostname'
11+
static String TAGS = 'tags'
12+
static String PROJ_NAME = 'ansible-child-groups'
13+
static String NODE_1 = 'one.example.com'
14+
static String TAGS_1 = 'dbservers, east, prod'
15+
static String NODE_2 = 'three.example.com'
16+
static String TAGS_2 = 'dbservers, test, west'
17+
static String NODE_3 = 'mail.example.com'
18+
static String TAGS_3 = 'ungrouped'
19+
20+
def setupSpec() {
21+
startCompose()
22+
configureRundeck(PROJ_NAME, NODE_1)
23+
}
24+
25+
void "child groups"() {
26+
when:
27+
def result = client.apiCall {api-> api.listNodes(PROJ_NAME,'.*')}
28+
29+
then:
30+
result != null
31+
result.size() == 7
32+
result.get(NODE_1) != null
33+
result.get(NODE_1).getAttributes().get(NODENAME) == NODE_1
34+
result.get(NODE_1).getAttributes().get(HOSTNAME) == NODE_1
35+
result.get(NODE_1).getAttributes().get(TAGS) == TAGS_1
36+
result.get(NODE_2) != null
37+
result.get(NODE_2).getAttributes().get(NODENAME) == NODE_2
38+
result.get(NODE_2).getAttributes().get(HOSTNAME) == NODE_2
39+
result.get(NODE_2).getAttributes().get(TAGS) == TAGS_2
40+
result.get(NODE_3) != null
41+
result.get(NODE_3).getAttributes().get(NODENAME) == NODE_3
42+
result.get(NODE_3).getAttributes().get(HOSTNAME) == NODE_3
43+
result.get(NODE_3).getAttributes().get(TAGS) == TAGS_3
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[defaults]
2+
inventory=/home/rundeck/ansible-child-groups/inventory_child.yaml
3+
interpreter_python=/usr/bin/python3
4+
5+
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
ungrouped:
2+
hosts:
3+
mail.example.com:
4+
webservers:
5+
hosts:
6+
foo.example.com:
7+
bar.example.com:
8+
dbservers:
9+
hosts:
10+
one.example.com:
11+
two.example.com:
12+
three.example.com:
13+
east:
14+
hosts:
15+
foo.example.com:
16+
one.example.com:
17+
two.example.com:
18+
west:
19+
hosts:
20+
bar.example.com:
21+
three.example.com:
22+
prod:
23+
children:
24+
east:
25+
test:
26+
children:
27+
west:

functional-test/src/test/resources/docker/docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ services:
3535
- ./ansible:/home/rundeck/ansible:rw
3636
- ./ansible-list:/home/rundeck/ansible-list:rw
3737
- ./ansible-yaml-parsing:/home/rundeck/ansible-yaml-parsing:rw
38+
- ./ansible-child-groups:/home/rundeck/ansible-child-groups:rw
3839

3940
volumes:
4041
rundeck-data:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
by:
2+
urn: project:ansible-yaml-parsing
3+
for:
4+
storage:
5+
- match:
6+
path: 'keys/.*'
7+
allow: [read]
8+
description: Allow access to key storage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#edit below
2+
project.disable.executions=false
3+
project.disable.schedule=false
4+
project.execution.history.cleanup.batch=500
5+
project.execution.history.cleanup.enabled=false
6+
project.execution.history.cleanup.retention.days=60
7+
project.execution.history.cleanup.retention.minimum=50
8+
project.execution.history.cleanup.schedule=0 0 0 1/1 * ? *
9+
project.jobs.gui.groupExpandLevel=1
10+
project.later.executions.disable.value=0
11+
project.later.executions.disable=false
12+
project.later.executions.enable.value=
13+
project.later.executions.enable=false
14+
project.later.schedule.disable.value=
15+
project.later.schedule.disable=false
16+
project.later.schedule.enable.value=
17+
project.later.schedule.enable=false
18+
project.name=ansible-yaml-parsing
19+
project.nodeCache.enabled=false
20+
project.nodeCache.firstLoadSynch=true
21+
project.output.allowUnsanitized=false
22+
project.retry-counter=3
23+
project.ssh-authentication=privateKey
24+
resources.source.1.type=local
25+
resources.source.2.config.ansible-config-file-path=/home/rundeck/ansible-child-groups/ansible.cfg
26+
resources.source.2.config.ansible-gather-facts=false
27+
resources.source.2.config.ansible-ignore-errors=true
28+
resources.source.2.config.ansible-inventory=/home/rundeck/ansible-child-groups/inventory_child.yaml
29+
resources.source.2.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory
30+
service.FileCopier.default.provider=sshj-scp
31+
service.NodeExecutor.default.provider=sshj-ssh

src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java

+127-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.rundeck.plugins.ansible.plugin;
22

33
import com.dtolabs.rundeck.core.common.Framework;
4+
import com.dtolabs.rundeck.core.common.INodeEntry;
45
import com.dtolabs.rundeck.core.common.INodeSet;
56
import com.dtolabs.rundeck.core.common.NodeEntryImpl;
67
import com.dtolabs.rundeck.core.common.NodeSetImpl;
@@ -48,13 +49,15 @@
4849
import java.nio.file.SimpleFileVisitor;
4950
import java.nio.file.attribute.BasicFileAttributes;
5051
import java.util.ArrayList;
52+
import java.util.Arrays;
5153
import java.util.HashMap;
5254
import java.util.HashSet;
5355
import java.util.List;
5456
import java.util.Map;
5557
import java.util.Map.Entry;
5658
import java.util.Properties;
5759
import java.util.Set;
60+
import java.util.stream.Collectors;
5861

5962
import static com.rundeck.plugins.ansible.ansible.InventoryList.ALL;
6063
import static com.rundeck.plugins.ansible.ansible.InventoryList.CHILDREN;
@@ -132,6 +135,8 @@ public class AnsibleResourceModelSource implements ResourceModelSource, ProxyRun
132135
@Setter
133136
private AnsibleInventoryList.AnsibleInventoryListBuilder ansibleInventoryListBuilder = null;
134137

138+
private Map<String, NodeEntryImpl> ansibleNodes = new HashMap<>();
139+
135140
public AnsibleResourceModelSource(final Framework framework) {
136141
this.framework = framework;
137142
}
@@ -714,44 +719,134 @@ public void ansibleInventoryList(NodeSetImpl nodes, AnsibleRunner.AnsibleRunnerB
714719

715720
if (isTagMapValid(all, ALL)) {
716721
Map<String, Object> children = InventoryList.getValue(all, CHILDREN);
722+
processChildren(children, new HashSet<>());
723+
}
717724

718-
if (isTagMapValid(children, CHILDREN)) {
719-
for (Map.Entry<String, Object> pair : children.entrySet()) {
720-
String hostGroup = pair.getKey();
721-
Map<String, Object> hostNames = InventoryList.getType(pair.getValue());
722-
Map<String, Object> hosts = InventoryList.getValue(hostNames, HOSTS);
723-
724-
if (isTagMapValid(hosts, HOSTS)) {
725-
for (Map.Entry<String, Object> hostNode : hosts.entrySet()) {
726-
NodeEntryImpl node = new NodeEntryImpl();
727-
node.setTags(Set.of(hostGroup));
728-
String hostName = hostNode.getKey();
729-
node.setHostname(hostName);
730-
node.setNodename(hostName);
731-
Map<String, Object> nodeValues = InventoryList.getType(hostNode.getValue());
732-
733-
InventoryList.tagHandle(NodeTag.HOSTNAME, node, nodeValues);
734-
InventoryList.tagHandle(NodeTag.USERNAME, node, nodeValues);
735-
InventoryList.tagHandle(NodeTag.OS_FAMILY, node, nodeValues);
736-
InventoryList.tagHandle(NodeTag.OS_NAME, node, nodeValues);
737-
InventoryList.tagHandle(NodeTag.OS_ARCHITECTURE, node, nodeValues);
738-
InventoryList.tagHandle(NodeTag.OS_VERSION, node, nodeValues);
739-
InventoryList.tagHandle(NodeTag.DESCRIPTION, node, nodeValues);
740-
741-
nodeValues.forEach((key, value) -> {
742-
if (value != null) {
743-
node.setAttribute(key, value.toString());
744-
}
745-
});
725+
ansibleNodes.forEach((k, node) -> nodes.putNode(node));
726+
ansibleNodes.clear();
727+
}
746728

747-
nodes.putNode(node);
748-
}
749-
}
750-
}
729+
/**
730+
* Processes the given set of nodes and populates the children map with the results.
731+
*
732+
* @param children a map to be populated with the processed children nodes
733+
* @param tags a set of tags to filter the nodes
734+
* @throws ResourceModelSourceException if an error occurs while processing the nodes
735+
*/
736+
public void processChildren(Map<String, Object> children, HashSet<String> tags) throws ResourceModelSourceException {
737+
if (!isTagMapValid(children, CHILDREN)) {
738+
return;
739+
}
740+
741+
for (Map.Entry<String, Object> pair : children.entrySet()) {
742+
743+
String hostGroup = pair.getKey();
744+
tags.add(hostGroup);
745+
Map<String, Object> hostNames = InventoryList.getType(pair.getValue());
746+
747+
if (hostNames.containsKey(CHILDREN)) {
748+
Map<String, Object> subChildren = InventoryList.getValue(hostNames, CHILDREN);
749+
processChildren(subChildren, tags);
750+
} else {
751+
processHosts(hostNames, tags);
752+
tags.clear();
751753
}
752754
}
753755
}
754756

757+
/**
758+
* Processes the hosts within the given host names map and adds them to the nodes set.
759+
*
760+
* @param hostNames the map containing host names and their attributes
761+
* @param tags the set of tags to apply to the nodes
762+
* @throws ResourceModelSourceException if an error occurs while processing the nodes
763+
*/
764+
public void processHosts(Map<String, Object> hostNames, HashSet<String> tags) throws ResourceModelSourceException {
765+
Map<String, Object> hosts = InventoryList.getValue(hostNames, HOSTS);
766+
767+
if (!isTagMapValid(hosts, HOSTS)) {
768+
return;
769+
}
770+
771+
for (Map.Entry<String, Object> hostNode : hosts.entrySet()) {
772+
NodeEntryImpl node = createNodeEntry(hostNode);
773+
addNode(node, tags);
774+
}
775+
}
776+
777+
/**
778+
* Creates a NodeEntryImpl object from the given host node entry and tags.
779+
*
780+
* @param hostNode the entry containing the host name and its attributes
781+
* @return the created NodeEntryImpl object
782+
*/
783+
public NodeEntryImpl createNodeEntry(Map.Entry<String, Object> hostNode) throws ResourceModelSourceException {
784+
NodeEntryImpl node = new NodeEntryImpl();
785+
String hostName = hostNode.getKey();
786+
node.setHostname(hostName);
787+
node.setNodename(hostName);
788+
Map<String, Object> nodeValues = InventoryList.getType(hostNode.getValue());
789+
790+
applyNodeTags(node, nodeValues);
791+
nodeValues.forEach((key, value) -> {
792+
if (value != null) {
793+
node.setAttribute(key, value.toString());
794+
}
795+
});
796+
797+
return node;
798+
}
799+
800+
/**
801+
* Applies predefined tags to the given node based on the provided node values.
802+
*
803+
* @param node the node to which the tags will be applied
804+
* @param nodeValues the map containing the node's attributes
805+
*/
806+
public void applyNodeTags(NodeEntryImpl node, Map<String, Object> nodeValues) throws ResourceModelSourceException {
807+
InventoryList.tagHandle(NodeTag.HOSTNAME, node, nodeValues);
808+
InventoryList.tagHandle(NodeTag.USERNAME, node, nodeValues);
809+
InventoryList.tagHandle(NodeTag.OS_FAMILY, node, nodeValues);
810+
InventoryList.tagHandle(NodeTag.OS_NAME, node, nodeValues);
811+
InventoryList.tagHandle(NodeTag.OS_ARCHITECTURE, node, nodeValues);
812+
InventoryList.tagHandle(NodeTag.OS_VERSION, node, nodeValues);
813+
InventoryList.tagHandle(NodeTag.DESCRIPTION, node, nodeValues);
814+
}
815+
816+
/**
817+
* Adds a node to the ansibleNodes map, merging tags if the node already exists.
818+
*
819+
* @param node The node to add.
820+
* @param tags The tags to associate with the node.
821+
*/
822+
public void addNode(NodeEntryImpl node, Set<String> tags) {
823+
ansibleNodes.compute(node.getNodename(), (key, existingNode) -> {
824+
if (existingNode != null) {
825+
Set<String> mergedTags = new HashSet<>(getStringTags(existingNode));
826+
mergedTags.addAll(tags);
827+
existingNode.setTags(Set.copyOf(mergedTags));
828+
return existingNode;
829+
} else {
830+
node.setTags(Set.copyOf(tags));
831+
return node;
832+
}
833+
});
834+
}
835+
836+
/**
837+
* Retrieves the tags from a node and converts them to strings.
838+
*
839+
* @param node The node whose tags are to be retrieved.
840+
* @return A set of strings representing the node's tags. Returns an empty set if the node has no tags.
841+
*/
842+
public Set<String> getStringTags(NodeEntryImpl node) {
843+
Set<String> tags = new HashSet<>();
844+
for (Object tag : node.getTags()) {
845+
tags.add(tag.toString());
846+
}
847+
return tags;
848+
}
849+
755850
/**
756851
* Gets Ansible nodes from inventory
757852
* @return Ansible nodes

0 commit comments

Comments
 (0)