diff --git a/build.gradle b/build.gradle index 58075b40..7bee0133 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation 'org.codehaus.groovy:groovy-all:3.0.15' pluginLibs group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.16.1' pluginLibs group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' + implementation "org.yaml:snakeyaml:2.2" compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java index 46649b92..fb80c3be 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java @@ -241,7 +241,7 @@ public static String[] getValues() { public static Property GATHER_FACTS_PROP = PropertyUtil.bool( ANSIBLE_GATHER_FACTS, "Gather Facts", - "Gather fresh facts before importing? (recommended)", + "Gather fresh facts before importing? (Not recommended for large inventories)", true, "true" ); diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleInventoryList.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleInventoryList.java new file mode 100644 index 00000000..df756276 --- /dev/null +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleInventoryList.java @@ -0,0 +1,152 @@ +package com.rundeck.plugins.ansible.ansible; + +import com.rundeck.plugins.ansible.util.AnsibleUtil; +import com.rundeck.plugins.ansible.util.ProcessExecutor; +import com.rundeck.plugins.ansible.util.VaultPrompt; +import lombok.Builder; +import lombok.Data; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class AnsibleInventoryList { + + private String inventory; + private String configFile; + private boolean debug; + + private AnsibleVault ansibleVault; + private VaultPrompt vaultPrompt; + private List limits; + + private File tempInternalVaultFile; + private File tempVaultFile; + private File vaultPromptFile; + private File tempLimitFile; + + public static final String ANSIBLE_INVENTORY = "ansible-inventory"; + + /** + * Executes Ansible command to bring all nodes from inventory + * @return output in yaml format + */ + public String getNodeList() throws IOException, AnsibleException { + + List procArgs = new ArrayList<>(); + procArgs.add(ANSIBLE_INVENTORY); + procArgs.add("--inventory-file" + "=" + inventory); + procArgs.add("--list"); + procArgs.add("-y"); + + Map processEnvironment = new HashMap<>(); + if (configFile != null && !configFile.isEmpty()) { + if (debug) { + System.out.println(" ANSIBLE_CONFIG: " + configFile); + } + processEnvironment.put("ANSIBLE_CONFIG", configFile); + } + //set STDIN variables + List stdinVariables = new ArrayList<>(); + + processAnsibleVault(stdinVariables, procArgs); + processLimit(procArgs); + + if(debug){ + System.out.println("getNodeList " + procArgs); + } + + Process proc = null; + + try { + proc = ProcessExecutor.builder().procArgs(procArgs) + .redirectErrorStream(true) + .environmentVariables(processEnvironment) + .stdinVariables(stdinVariables) + .build().run(); + + StringBuilder stringBuilder = new StringBuilder(); + + final InputStream stdoutInputStream = proc.getInputStream(); + final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + + String line1; + while ((line1 = stdoutReader.readLine()) != null) { + stringBuilder.append(line1).append("\n"); + } + + int exitCode = proc.waitFor(); + + if (exitCode != 0) { + System.err.println("ERROR: getNodeList: " + procArgs); + return null; + } + return stringBuilder.toString(); + + } catch (IOException e) { + throw new AnsibleException("ERROR: Ansible IO failure: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.IOFailure); + } catch (InterruptedException e) { + if (proc != null) { + proc.destroy(); + } + Thread.currentThread().interrupt(); + throw new AnsibleException("ERROR: Ansible Execution Interrupted: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.Interrupted); + } catch (Exception e) { + if (proc != null) { + proc.destroy(); + } + throw new AnsibleException("ERROR: Ansible execution returned with non zero code: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.Unknown); + } finally { + if (proc != null) { + proc.getErrorStream().close(); + proc.getInputStream().close(); + proc.getOutputStream().close(); + proc.destroy(); + } + } + } + + private void processAnsibleVault(List stdinVariables, List procArgs) + throws IOException { + + if(ansibleVault == null){ + tempInternalVaultFile = AnsibleVault.createVaultScriptAuth("ansible-script-vault"); + ansibleVault = AnsibleVault.builder() + .masterPassword(vaultPrompt.getVaultPassword()) + .vaultPasswordScriptFile(tempInternalVaultFile) + .debug(debug).build(); + } + + if (vaultPrompt != null) { + stdinVariables.add(vaultPrompt); + tempVaultFile = ansibleVault.getVaultPasswordScriptFile(); + procArgs.add("--vault-id"); + procArgs.add(tempVaultFile.getAbsolutePath()); + } + } + + private void processLimit(List procArgs) throws IOException { + if (limits != null && limits.size() == 1) { + procArgs.add("-l"); + procArgs.add(limits.get(0)); + + } else if (limits != null && limits.size() > 1) { + StringBuilder sb = new StringBuilder(); + for (String limit : limits) { + sb.append(limit).append("\n"); + } + tempLimitFile = AnsibleUtil.createTemporaryFile("targets", sb.toString()); + + procArgs.add("-l"); + procArgs.add("@" + tempLimitFile.getAbsolutePath()); + } + } +} diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/InventoryList.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/InventoryList.java new file mode 100644 index 00000000..4de139ca --- /dev/null +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/InventoryList.java @@ -0,0 +1,166 @@ +package com.rundeck.plugins.ansible.ansible; + +import com.dtolabs.rundeck.core.common.NodeEntryImpl; +import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; +import lombok.Data; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; + +@Data +public class InventoryList { + + public static final String ALL = "all"; + public static final String CHILDREN = "children"; + public static final String HOSTS = "hosts"; + public static final String ERROR_MISSING_FIELD = "Error: Missing tag '%s'"; + public static final String ERROR_MISSING_TAG = "Error: Not tag found among these searched: '%s'"; + + /** + * Gets value from a tag from Ansible inventory + * @param tag tag from yaml output + * @param field field to look for + * @return value from tag yaml output + * @param type you want to receive + */ + public static T getValue(Map tag, final String field) { + Object obj = null; + if (Optional.ofNullable(tag.get(field)).isPresent()) { + obj = tag.get(field); + } + return getType(obj); + } + + /** + * Casts an object you want to receive + * @param obj generic object + * @return cast object + * @param type you want to receive + */ + @SuppressWarnings("unchecked") + public static T getType(Object obj) { + return (T) obj; + } + + /** + * Process an Ansible tag + * @param nodeTag Ansible node tag + * @param node Rundeck node to build + * @param tags tags to evaluate + * @throws ResourceModelSourceException + */ + public static void tagHandle(NodeTag nodeTag, NodeEntryImpl node, Map tags) + throws ResourceModelSourceException { + nodeTag.handle(node, tags); + } + + /** + * Finds a tag if it exists in a map of tags + * @param nameTags map of tags to find + * @param tags tags from Ansible + * @return a value if it exists + */ + private static String findTag(List nameTags, Map tags) { + String found = null; + for (String nameTag : nameTags) { + if (tags.containsKey(nameTag)) { + Object value = tags.get(nameTag); + found = valueString(value); + break; + } + } + return found; + } + + /** + * Casts an object to String + * @param obj object to convert + * @return a String object + */ + private static String valueString(Object obj) { + return getType(obj); + } + + /** + * Enum to manage Ansible tags + */ + public enum NodeTag { + + HOSTNAME { + @Override + public void handle(NodeEntryImpl node, Map tags) throws ResourceModelSourceException{ + final List hostnames = List.of("hostname", "ansible_host", "ansible_ssh_host"); + String nameTag = InventoryList.findTag(hostnames, tags); + node.setHostname(Optional.ofNullable(nameTag) + .orElseThrow(() -> new ResourceModelSourceException(format(ERROR_MISSING_TAG, hostnames)))); + } + }, + USERNAME { + @Override + public void handle(NodeEntryImpl node, Map tags) { + final List usernames = List.of("username", "ansible_user", "ansible_ssh_user", "ansible_user_id"); + String nameTag = InventoryList.findTag(usernames, tags); + Optional.ofNullable(nameTag).ifPresent(node::setUsername); + } + }, + OS_FAMILY { + @Override + public void handle(NodeEntryImpl node, Map tags) { + final List osNames = List.of("osFamily", "ansible_os_family"); + String nameTag = InventoryList.findTag(osNames, tags); + Optional.ofNullable(nameTag).ifPresent(node::setOsFamily); + } + }, + OS_NAME { + @Override + public void handle(NodeEntryImpl node, Map tags) { + final List familyNames = List.of("osName", "ansible_os_name"); + String nameTag = InventoryList.findTag(familyNames, tags); + Optional.ofNullable(nameTag).ifPresent(node::setOsName); + } + }, + OS_ARCHITECTURE { + @Override + public void handle(NodeEntryImpl node, Map tags) { + final List architectureNames = List.of("osArch", "ansible_architecture"); + String nameTag = InventoryList.findTag(architectureNames, tags); + Optional.ofNullable(nameTag).ifPresent(node::setOsArch); + } + }, + OS_VERSION { + @Override + public void handle(NodeEntryImpl node, Map tags) { + final List versionNames = List.of("osVersion", "ansible_kernel"); + String nameTag = InventoryList.findTag(versionNames, tags); + Optional.ofNullable(nameTag).ifPresent(node::setOsVersion); + } + }, + DESCRIPTION { + @Override + public void handle(NodeEntryImpl node, Map tags) { + Map lsbMap = InventoryList.getValue(tags, "ansible_lsb"); + if (lsbMap != null) { + String desc = InventoryList.valueString(lsbMap.get("description")); + Optional.ofNullable(desc).ifPresent(node::setDescription); + } + else { + Optional.ofNullable(InventoryList.getValue(tags, "ansible_distribution")) + .ifPresent(x -> node.setDescription(x + " ")); + Optional.ofNullable(InventoryList.getValue(tags, "ansible_distribution_version")) + .ifPresent(x -> node.setDescription(x + " ")); + } + } + }; + + /** + * Processes an Ansible tag to build a Rundeck tag + * @param node Rundeck node + * @param tags Ansible tags + * @throws ResourceModelSourceException + */ + public abstract void handle(NodeEntryImpl node, Map tags) throws ResourceModelSourceException; + } +} diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java index 553d58cf..db252f8c 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -1,43 +1,65 @@ package com.rundeck.plugins.ansible.plugin; -import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; -import com.dtolabs.rundeck.core.storage.ResourceMeta; -import com.dtolabs.rundeck.core.storage.StorageTree; -import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; -import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; -import com.rundeck.plugins.ansible.ansible.AnsibleDescribable.AuthenticationType; -import com.rundeck.plugins.ansible.ansible.AnsibleException; -import com.rundeck.plugins.ansible.ansible.AnsibleRunner; import com.dtolabs.rundeck.core.common.Framework; import com.dtolabs.rundeck.core.common.INodeSet; import com.dtolabs.rundeck.core.common.NodeEntryImpl; import com.dtolabs.rundeck.core.common.NodeSetImpl; import com.dtolabs.rundeck.core.dispatcher.DataContextUtils; -import com.dtolabs.rundeck.core.resources.ResourceModelSource; -import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; +import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; import com.dtolabs.rundeck.core.plugins.ScriptDataContextUtil; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; +import com.dtolabs.rundeck.core.resources.ResourceModelSource; +import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; +import com.dtolabs.rundeck.core.storage.ResourceMeta; +import com.dtolabs.rundeck.core.storage.StorageTree; +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; +import com.rundeck.plugins.ansible.ansible.AnsibleDescribable.AuthenticationType; +import com.rundeck.plugins.ansible.ansible.AnsibleException; +import com.rundeck.plugins.ansible.ansible.AnsibleInventoryList; +import com.rundeck.plugins.ansible.ansible.AnsibleRunner; +import com.rundeck.plugins.ansible.ansible.InventoryList; +import com.rundeck.plugins.ansible.util.VaultPrompt; import org.rundeck.app.spi.Services; import org.rundeck.storage.api.PathUtil; import org.rundeck.storage.api.StorageException; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.*; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +import static com.rundeck.plugins.ansible.ansible.InventoryList.*; public class AnsibleResourceModelSource implements ResourceModelSource, ProxyRunnerPlugin { + public static final String HOST_TPL_J2 = "host-tpl.j2"; + public static final String GATHER_HOSTS_YML = "gather-hosts.yml"; + private Framework framework; private Services services; @@ -96,11 +118,16 @@ public class AnsibleResourceModelSource implements ResourceModelSource, ProxyRun protected boolean encryptExtraVars = false; + private AnsibleInventoryList.AnsibleInventoryListBuilder ansibleInventoryListBuilder = null; public AnsibleResourceModelSource(final Framework framework) { this.framework = framework; } + public void setAnsibleInventoryListBuilder(AnsibleInventoryList.AnsibleInventoryListBuilder builder) { + this.ansibleInventoryListBuilder = builder; + } + private static String resolveProperty( final String attribute, final String defaultValue, @@ -162,7 +189,7 @@ public void configure(Properties configuration) throws ConfigurationException { try { sshTimeout = Integer.parseInt(str_sshTimeout); } catch (NumberFormatException e) { - throw new ConfigurationException("Can't parse timeout value : " + e.getMessage()); + throw new ConfigurationException("Can't parse timeout value : " + e.getMessage(), e); } } @@ -197,9 +224,9 @@ public void configure(Properties configuration) throws ConfigurationException { } - public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceModelSourceException{ + public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceModelSourceException { - AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = AnsibleRunner.playbookPath("gather-hosts.yml"); + AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = AnsibleRunner.playbookPath(GATHER_HOSTS_YML); if ("true".equals(System.getProperty("ansible.debug"))) { runnerBuilder.debug(true); @@ -220,7 +247,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo try { sshPrivateKey = new String(Files.readAllBytes(Paths.get(sshPrivateKeyFile))); } catch (IOException e) { - throw new ResourceModelSourceException("Could not read privatekey file " + sshPrivateKeyFile,e); + throw new ResourceModelSourceException("Could not read privatekey file " + sshPrivateKeyFile +": "+ e.getMessage(),e); } runnerBuilder.sshPrivateKey(sshPrivateKey); } @@ -230,7 +257,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo String sshPrivateKey = getStorageContentString(sshPrivateKeyPath, storageTree); runnerBuilder.sshPrivateKey(sshPrivateKey); } catch (ConfigurationException e) { - throw new ResourceModelSourceException("Could not read private key from storage path " + sshPrivateKeyPath,e); + throw new ResourceModelSourceException("Could not read private key from storage path " + sshPrivateKeyPath +": "+ e.getMessage(),e); } } @@ -242,7 +269,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo String sshPassphrase = getStorageContentString(sshPassphraseStoragePath, storageTree); runnerBuilder.sshPassphrase(sshPassphrase); } catch (ConfigurationException e) { - throw new ResourceModelSourceException("Could not read passphrase from storage path " + sshPassphraseStoragePath,e); + throw new ResourceModelSourceException("Could not read passphrase from storage path " + sshPassphraseStoragePath +": "+ e.getMessage(),e); } } } @@ -257,7 +284,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo sshPassword = getStorageContentString(sshPasswordPath, storageTree); runnerBuilder.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); } catch (ConfigurationException e) { - throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); + throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath +": "+ e.getMessage(),e); } } } @@ -298,7 +325,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo becomePassword = getStorageContentString(becamePasswordStoragePath, storageTree); runnerBuilder.becomePassword(becomePassword); } catch (Exception e) { - throw new ResourceModelSourceException("Could not read becomePassword from storage path " + becamePasswordStoragePath,e); + throw new ResourceModelSourceException("Could not read becomePassword from storage path " + becamePasswordStoragePath +": "+ e.getMessage(),e); } } @@ -314,7 +341,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo try { vaultPassword = getStorageContentString(vaultPasswordPath, storageTree); } catch (Exception e) { - throw new ResourceModelSourceException("Could not read vaultPassword " + vaultPasswordPath,e); + throw new ResourceModelSourceException("Could not read vaultPassword " + vaultPasswordPath +": "+ e.getMessage(),e); } runnerBuilder.vaultPass(vaultPassword); } @@ -324,7 +351,7 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo try { vaultPassword = new String(Files.readAllBytes(Paths.get(vaultFile))); } catch (IOException e) { - throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); + throw new ResourceModelSourceException("Could not read vault file " + vaultFile +": "+ e.getMessage(),e); } runnerBuilder.vaultPass(vaultPassword); } @@ -345,47 +372,56 @@ public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceMo return runnerBuilder; } - @Override public INodeSet getNodes() throws ResourceModelSourceException { NodeSetImpl nodes = new NodeSetImpl(); - final Gson gson = new Gson(); + AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = buildAnsibleRunner(); + + if (gatherFacts) { + processWithGatherFacts(nodes, runnerBuilder); + } else { + ansibleInventoryList(nodes, runnerBuilder); + } + + return nodes; + } + + public void processWithGatherFacts(NodeSetImpl nodes, AnsibleRunner.AnsibleRunnerBuilder runnerBuilder) throws ResourceModelSourceException { + final Gson gson = new Gson(); Path tempDirectory; try { tempDirectory = Files.createTempDirectory("ansible-hosts"); } catch (IOException e) { - throw new ResourceModelSourceException("Error creating temporary directory.", e); + throw new ResourceModelSourceException("Error creating temporary directory: " + e.getMessage(), e); } try { - Files.copy(this.getClass().getClassLoader().getResourceAsStream("host-tpl.j2"), tempDirectory.resolve("host-tpl.j2")); - Files.copy(this.getClass().getClassLoader().getResourceAsStream("gather-hosts.yml"), tempDirectory.resolve("gather-hosts.yml")); + Files.copy(this.getClass().getClassLoader().getResourceAsStream(HOST_TPL_J2), tempDirectory.resolve(HOST_TPL_J2)); + Files.copy(this.getClass().getClassLoader().getResourceAsStream(GATHER_HOSTS_YML), tempDirectory.resolve(GATHER_HOSTS_YML)); } catch (IOException e) { - throw new ResourceModelSourceException("Error copying files."); + throw new ResourceModelSourceException("Error copying files: " + e.getMessage(), e); } - AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = buildAnsibleRunner(); - runnerBuilder.tempDirectory(tempDirectory); runnerBuilder.retainTempDirectory(true); StringBuilder args = new StringBuilder(); args.append("facts: ") - .append(gatherFacts ? "True" : "False") - .append("\n") - .append("tmpdir: '") - .append(tempDirectory.toFile().getAbsolutePath()) - .append("'"); + .append(gatherFacts ? "True" : "False") + .append("\n") + .append("tmpdir: '") + .append(tempDirectory.toFile().getAbsolutePath()) + .append("'"); runnerBuilder.extraVars(args.toString()); AnsibleRunner runner = runnerBuilder.build(); try { - runner.run(); + runner.run(); } catch (Exception e) { - throw new ResourceModelSourceException(e.getMessage(),e); + throw new ResourceModelSourceException("Failed Ansible Runner execution: " + e.getMessage(),e); } try { @@ -543,8 +579,8 @@ public INodeSet getNodes() throws ResourceModelSourceException { } } else { if (root.has(item.getKey()) - && root.get(item.getKey()).isJsonPrimitive() - && root.get(item.getKey()).getAsString().length() > 0) { + && root.get(item.getKey()).isJsonPrimitive() + && root.get(item.getKey()).getAsString().length() > 0) { node.setAttribute(item.getValue(), root.get(item.getKey()).getAsString()); } } @@ -605,7 +641,7 @@ public INodeSet getNodes() throws ResourceModelSourceException { directoryStream.close(); } } catch (IOException e) { - throw new ResourceModelSourceException("Error reading facts.", e); + throw new ResourceModelSourceException("Error reading facts: " + e.getMessage(), e); } try { @@ -623,13 +659,83 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } }); } catch (IOException e) { - throw new ResourceModelSourceException("Error deleting temporary directory.", e); + throw new ResourceModelSourceException("Error deleting temporary directory: " + e.getMessage(), e); } + } - return nodes; + /** + * Process nodes coming from Ansible to convert them to Rundeck node + * @param nodes Rundeck nodes + * @throws ResourceModelSourceException + */ + public void ansibleInventoryList(NodeSetImpl nodes, AnsibleRunner.AnsibleRunnerBuilder runnerBuilder) throws ResourceModelSourceException { + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + + String listResp = getNodesFromInventory(runnerBuilder); + + Map allInventory = yaml.load(listResp); + Map all = InventoryList.getValue(allInventory, ALL); + Map children = InventoryList.getValue(all, CHILDREN); + + for (Map.Entry pair : children.entrySet()) { + String hostGroup = pair.getKey(); + Map hostNames = InventoryList.getType(pair.getValue()); + Map hosts = InventoryList.getValue(hostNames, HOSTS); + + for (Map.Entry hostNode : hosts.entrySet()) { + NodeEntryImpl node = new NodeEntryImpl(); + node.setTags(Set.of(hostGroup)); + String hostName = hostNode.getKey(); + node.setHostname(hostName); + node.setNodename(hostName); + Map nodeValues = InventoryList.getType(hostNode.getValue()); + InventoryList.tagHandle(NodeTag.USERNAME, node, nodeValues); + InventoryList.tagHandle(NodeTag.OS_FAMILY, node, nodeValues); + InventoryList.tagHandle(NodeTag.OS_NAME, node, nodeValues); + InventoryList.tagHandle(NodeTag.OS_ARCHITECTURE, node, nodeValues); + InventoryList.tagHandle(NodeTag.OS_VERSION, node, nodeValues); + InventoryList.tagHandle(NodeTag.DESCRIPTION, node, nodeValues); + + nodes.putNode(node); + } + } } + /** + * Gets Ansible nodes from inventory + * @return Ansible nodes + */ + public String getNodesFromInventory(AnsibleRunner.AnsibleRunnerBuilder runnerBuilder) throws ResourceModelSourceException { + + AnsibleRunner runner = runnerBuilder.build(); + + if (this.ansibleInventoryListBuilder == null) { + this.ansibleInventoryListBuilder = AnsibleInventoryList.builder() + .inventory(inventory) + .configFile(configFile) + .debug(debug); + } + + if(runner.getVaultPass() != null){ + VaultPrompt vaultPrompt = VaultPrompt.builder() + .vaultId("None") + .vaultPassword(runner.getVaultPass() + "\n") + .build(); + ansibleInventoryListBuilder.vaultPrompt(vaultPrompt); + } + if (runner.getLimits() != null) { + ansibleInventoryListBuilder.limits(runner.getLimits()); + } + + AnsibleInventoryList inventoryList = this.ansibleInventoryListBuilder.build(); + + try { + return inventoryList.getNodeList(); + } catch (IOException | AnsibleException e) { + throw new ResourceModelSourceException("Failed to get node list from ansible: " + e.getMessage(), e); + } + } private String getStorageContentString(String storagePath, StorageTree storageTree) throws ConfigurationException { return new String(this.getStorageContent(storagePath, storageTree)); @@ -644,10 +750,10 @@ private byte[] getStorageContent(String storagePath, StorageTree storageTree) th return byteArrayOutputStream.toByteArray(); } catch (StorageException e) { throw new ConfigurationException("Failed to read the ssh private key for " + - "storage path: " + storagePath + ": " + e.getMessage()); + "storage path: " + storagePath + ": " + e.getMessage(), e); } catch (IOException e) { throw new ConfigurationException("Failed to read the ssh private key for " + - "storage path: " + storagePath + ": " + e.getMessage()); + "storage path: " + storagePath + ": " + e.getMessage(), e); } } diff --git a/src/test/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceSpec.groovy b/src/test/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceSpec.groovy new file mode 100644 index 00000000..540ea6fe --- /dev/null +++ b/src/test/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceSpec.groovy @@ -0,0 +1,97 @@ +package com.rundeck.plugins.ansible.plugin + +import com.dtolabs.rundeck.core.storage.StorageTree +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree +import org.rundeck.app.spi.Services + +import static com.rundeck.plugins.ansible.ansible.AnsibleInventoryList.AnsibleInventoryListBuilder + +import com.dtolabs.rundeck.core.common.Framework +import com.dtolabs.rundeck.core.common.INodeEntry +import com.dtolabs.rundeck.core.common.INodeSet +import com.dtolabs.rundeck.core.resources.ResourceModelSource +import com.rundeck.plugins.ansible.ansible.AnsibleDescribable +import com.rundeck.plugins.ansible.ansible.AnsibleInventoryList +import org.yaml.snakeyaml.Yaml +import spock.lang.Specification + +/** + * AnsibleResourceModelSource test + */ +class AnsibleResourceModelSourceSpec extends Specification { + + void "get nodes gather facts false"() { + given: + Yaml yaml = new Yaml() + String familyValue = 'Linux' + String nameValue = 'RED HAT' + String versionValue = "'7.9'" + String archValue = 'x64' + String usernameValue = 'test' + String descValue = 'CentOS Linux' + Framework framework = Mock(Framework) { + getBaseDir() >> Mock(File) { + getAbsolutePath() >> '/tmp' + } + } + ResourceModelSource plugin = new AnsibleResourceModelSource(framework) + Properties config = new Properties() + config.put('project', 'project_1') + config.put(AnsibleDescribable.ANSIBLE_GATHER_FACTS, 'false') + plugin.configure(config) + Services services = Mock(Services) { + getService(KeyStorageTree.class) >> Mock(KeyStorageTree) + } + plugin.setServices(services) + + when: + Map nodeData = [:] + if (osFamily) { nodeData.put(osFamily, familyValue) } + if (osName) { nodeData.put(osName, nameValue) } + if (osVersion) { nodeData.put(osVersion, versionValue) } + if (osArch) { nodeData.put(osArch, archValue) } + if (username) { nodeData.put(username, usernameValue) } + if (description) { + nodeData.put(description, descValue) + } + + def host = [(nodeName) : nodeData] + def hosts = ['hosts' : host] + def groups = ['ungrouped' : hosts] + def children = ['children' : groups] + def all = ['all' : children] + + String result = yaml.dump(all) + + AnsibleInventoryListBuilder inventoryListBuilder = Mock(AnsibleInventoryListBuilder) { + build() >> Mock(AnsibleInventoryList) { + getNodeList() >> result + } + } + + plugin.ansibleInventoryListBuilder = inventoryListBuilder + INodeSet nodes = plugin.getNodes() + + then: + nodes.size() == 1 + INodeEntry node = nodes[0] + node.tags.size() == 1 + node.tags[0] == 'ungrouped' + if (node.hostname) { node.hostname == nodeName } + if (node.nodename) { node.nodename == nodeName } + if (node.osFamily) { node.osFamily == familyValue } + if (node.osName) { node.osName == nameValue } + if (node.osVersion) { node.osVersion == versionValue } + if (node.osArch) { node.osArch == archValue } + if (node.username) { node.username == usernameValue } + if (node.description) { node.description == descValue } + + where: + nodeName | osFamily | osName | osVersion | osArch | username | description + 'NODE_0' | 'osFamily' | 'osName' | 'osVersion' | 'osArch' | 'username' | 'description' + 'NODE_1' | 'ansible_os_family' | 'ansible_os_name' | 'ansible_kernel' | 'ansible_architecture' | 'ansible_user' | 'ansible_distribution' + 'NODE_2' | 'ansible_os_family' | 'ansible_os_name' | 'ansible_kernel' | 'ansible_architecture' | 'ansible_ssh_user' | 'ansible_distribution' + 'NODE_3' | 'ansible_os_family' | 'ansible_os_name' | 'ansible_kernel' | 'ansible_architecture' | 'ansible_user_id' | 'ansible_distribution' + } + +}