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 0919418f..5b8696b1 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java @@ -100,6 +100,7 @@ public static String[] getValues() { public static final String ANSIBLE_INVENTORY_INLINE = "ansible-inventory-inline"; public static final String ANSIBLE_INVENTORY = "ansible-inventory"; public static final String ANSIBLE_GENERATE_INVENTORY = "ansible-generate-inventory"; + public static final String ANSIBLE_GENERATE_INVENTORY_NODES_AUTH = "ansible-generate-inventory-nodes-auth"; public static final String ANSIBLE_MODULE = "ansible-module"; public static final String ANSIBLE_MODULE_ARGS = "ansible-module-args"; public static final String ANSIBLE_DEBUG = "ansible-debug"; @@ -233,6 +234,13 @@ public static String[] getValues() { .description("Generate Ansible inventory from Rundeck nodes.") .build(); + static final Property GENERATE_INVENTORY_NODES_AUTH = PropertyBuilder.builder() + .booleanType(ANSIBLE_GENERATE_INVENTORY_NODES_AUTH) + .required(false) + .title("Generate inventory, pass node authentication from rundeck nodes") + .description("Pass authentication from rundeck nodes.") + .build(); + public static Property EXECUTABLE_PROP = PropertyUtil.freeSelect( ANSIBLE_EXECUTABLE, "Executable", diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java index ae653aea..8947afb6 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -127,6 +127,15 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte ansibleRunnerBuilder.inventory(inventory); } + Boolean generateInventoryNodeAuth = contextBuilder.generateInventoryNodesAuth(); + if(generateInventoryNodeAuth != null && generateInventoryNodeAuth){ + Map> nodesAuth = contextBuilder.getNodesAuthenticationMap(); + if (nodesAuth != null && !nodesAuth.isEmpty()) { + ansibleRunnerBuilder.addNodeAuthToInventory(true); + ansibleRunnerBuilder.nodesAuthentication(nodesAuth); + } + } + String limit = contextBuilder.getLimit(); if (limit != null) { ansibleRunnerBuilder.limits(limit); @@ -291,9 +300,15 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte File tempSshVarsFile ; File tempBecameVarsFile ; File vaultPromptFile; + File tempNodeAuthFile; + File groupVarsDir; String customTmpDirPath; + @Builder.Default + Boolean addNodeAuthToInventory = false; + Map> nodesAuthentication; + public void deleteTempDirectory(Path tempDirectory) throws IOException { Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { @Override @@ -396,6 +411,63 @@ public int run() throws Exception { if (inventory != null && !inventory.isEmpty()) { procArgs.add("-i"); procArgs.add(inventory); + + if(addNodeAuthToInventory != null && addNodeAuthToInventory && nodesAuthentication != null && !nodesAuthentication.isEmpty()) { + Map hostUsers = new LinkedHashMap<>(); + Map hostPasswords = new LinkedHashMap<>(); + nodesAuthentication.forEach((nodeName, authValues) -> { + String user = authValues.get("ansible_user"); + String password = authValues.get("ansible_password"); + if (user != null) { + hostUsers.put(nodeName, user); + } + if (password != null) { + String encryptedPassword = password; + if (useAnsibleVault) { + try { + encryptedPassword = encryptExtraVarsKey(password); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + hostPasswords.put(nodeName, encryptedPassword); + } + }); + // Build YAML structure + Map yamlData = new LinkedHashMap<>(); + yamlData.put("host_passwords", hostPasswords); + yamlData.put("host_users", hostUsers); + try { + String yamlContent = mapperYaml.writeValueAsString(yamlData); + + // Create group_vars directory structure + File inventoryFile = new File(inventory); + File inventoryParentDir = inventoryFile.getParentFile(); + + if (inventoryParentDir != null) { + groupVarsDir = new File(inventoryParentDir, "group_vars"); + + if (!groupVarsDir.exists()) { + if (!groupVarsDir.mkdirs()) { + throw new RuntimeException("Failed to create group_vars directory at: " + groupVarsDir.getAbsolutePath()); + } + } + + // Create all.yaml in group_vars directory + tempNodeAuthFile = new File(groupVarsDir, "all.yaml"); + java.nio.file.Files.writeString(tempNodeAuthFile.toPath(), yamlContent); + tempNodeAuthFile.deleteOnExit(); + groupVarsDir.deleteOnExit(); + } else { + // Fallback to temp file if inventory has no parent directory + tempNodeAuthFile = AnsibleUtil.createTemporaryFile("group_vars", "all.yaml", yamlContent, customTmpDirPath); + } + + } catch (IOException e) { + throw new RuntimeException("Failed to write all.yaml for node auth", e); + } + } + } if (limits != null && limits.size() == 1) { @@ -513,8 +585,6 @@ public int run() throws Exception { //SET env variables Map processEnvironment = new HashMap<>(); - - if (configFile != null && !configFile.isEmpty()) { if (debug) { System.out.println(" ANSIBLE_CONFIG: " + configFile); @@ -663,6 +733,16 @@ public int run() throws Exception { vaultPromptFile.deleteOnExit(); } + if (tempNodeAuthFile != null && !tempNodeAuthFile.delete()) { + tempNodeAuthFile.deleteOnExit(); + } + + if (groupVarsDir != null && groupVarsDir.exists()) { + if (!groupVarsDir.delete()) { + groupVarsDir.deleteOnExit(); + } + } + if (usingTempDirectory && !retainTempDirectory) { deleteTempDirectory(baseDirectory); } @@ -824,4 +904,5 @@ public String encryptExtraVarsKey(String extraVars) throws Exception { } -} \ No newline at end of file +} + diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java index ef74733b..3fa916bb 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java @@ -193,17 +193,7 @@ public String getSshPassword() throws ConfigurationException { if (null != storagePath) { //look up storage value - Path path = PathUtil.asPath(storagePath); - try { - ResourceMeta contents = context.getStorageTree().getResource(path) - .getContents(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - contents.writeContent(byteArrayOutputStream); - return byteArrayOutputStream.toString(); - } catch (StorageException | IOException e) { - throw new ConfigurationException("Failed to read the ssh password for " + - "storage path: " + storagePath + ": " + e.getMessage()); - } + return getPasswordFromPath(storagePath); } else { return null; @@ -211,6 +201,21 @@ public String getSshPassword() throws ConfigurationException { } } + public String getPasswordFromPath(String storagePath) throws ConfigurationException { + //look up storage value + Path path = PathUtil.asPath(storagePath); + try { + ResourceMeta contents = context.getStorageTree().getResource(path) + .getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + return byteArrayOutputStream.toString(); + } catch (StorageException | IOException e) { + throw new ConfigurationException("Failed to read the ssh password for " + + "storage path: " + storagePath + ": " + e.getMessage()); + } + } + public Integer getSSHTimeout() throws ConfigurationException { Integer timeout = null; final String stimeout = PropertyResolver.resolveProperty( @@ -249,6 +254,23 @@ public String getSshUser() { return user; } + public String getSshNodeUser(INodeEntry node) { + final String user; + user = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_SSH_USER, + null, + getFrameworkProject(), + getFramework(), + node, + getJobConf() + ); + + if (null != user && user.contains("${")) { + return DataContextUtils.replaceDataReferencesInString(user, getContext().getDataContext()); + } + return user; + } + public AuthenticationType getSshAuthenticationType() { String authType = PropertyResolver.resolveProperty( @@ -880,4 +902,83 @@ public Map getListOptions(){ } return options; } + + public Map> getNodesAuthenticationMap(){ + + Map> authenticationNodesMap = new HashMap<>(); + + this.context.getNodes().forEach((node) -> { + String keyPath = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH, + null, + getFrameworkProject(), + getFramework(), + node, + getJobConf() + ); + + Map auth = new HashMap<>(); + + if(null!=keyPath){ + try { + auth.put("ansible_password",getPasswordFromPath(keyPath) ); + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } + + } + String userName = getSshNodeUser(node); + + if(null!=userName){ + auth.put("ansible_user",userName ); + } + + authenticationNodesMap.put(node.getNodename(), auth); + }); + + return authenticationNodesMap; + } + + + public List getListNodesKeyPath(){ + + List secretPaths = new ArrayList<>(); + + this.context.getNodes().forEach((node) -> { + String keyPath = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH, + null, + getFrameworkProject(), + getFramework(), + node, + getJobConf() + ); + + if(null!=keyPath){ + if(!secretPaths.contains(keyPath)){ + secretPaths.add(keyPath); + } + } + }); + + return secretPaths; + } + + + public Boolean generateInventoryNodesAuth() { + Boolean generateInventoryNodesAuth = null; + String sgenerateInventoryNodesAuth = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_GENERATE_INVENTORY_NODES_AUTH, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); + + if (null != sgenerateInventoryNodesAuth) { + generateInventoryNodesAuth = Boolean.parseBoolean(sgenerateInventoryNodesAuth); + } + return generateInventoryNodesAuth; + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java index f7421c3b..e75ae84e 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java @@ -36,6 +36,7 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.property(WINDOWS_EXECUTABLE_PROP); builder.property(CONFIG_FILE_PATH); builder.property(GENERATE_INVENTORY_PROP); + builder.property(GENERATE_INVENTORY_NODES_AUTH); builder.property(SSH_AUTH_TYPE_PROP); builder.property(SSH_USER_PROP); builder.property(SSH_PASSWORD_STORAGE_PROP); @@ -63,6 +64,8 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.frameworkMapping(ANSIBLE_CONFIG_FILE_PATH,FWK_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); builder.mapping(ANSIBLE_GENERATE_INVENTORY,PROJ_PROP_PREFIX + ANSIBLE_GENERATE_INVENTORY); builder.frameworkMapping(ANSIBLE_GENERATE_INVENTORY,FWK_PROP_PREFIX + ANSIBLE_GENERATE_INVENTORY); + builder.mapping(ANSIBLE_GENERATE_INVENTORY_NODES_AUTH,PROJ_PROP_PREFIX + ANSIBLE_GENERATE_INVENTORY_NODES_AUTH); + builder.frameworkMapping(ANSIBLE_GENERATE_INVENTORY_NODES_AUTH,FWK_PROP_PREFIX + ANSIBLE_GENERATE_INVENTORY_NODES_AUTH); builder.mapping(ANSIBLE_SSH_AUTH_TYPE,PROJ_PROP_PREFIX + ANSIBLE_SSH_AUTH_TYPE); builder.frameworkMapping(ANSIBLE_SSH_AUTH_TYPE,FWK_PROP_PREFIX + ANSIBLE_SSH_AUTH_TYPE); builder.mapping(ANSIBLE_SSH_USER,PROJ_PROP_PREFIX + ANSIBLE_SSH_USER); @@ -197,7 +200,13 @@ public List listSecretsPath(ExecutionContext context, INodeEntry node) { jobConf.put(AnsibleDescribable.ANSIBLE_LIMIT,node.getNodename()); AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), jobConf); - return AnsibleUtil.getSecretsPath(builder); + List secretPaths = AnsibleUtil.getSecretsPath(builder); + List secretPathsNodes = builder.getListNodesKeyPath(); + + if(secretPathsNodes != null && !secretPathsNodes.isEmpty()){ + secretPaths.addAll(secretPathsNodes); + } + return secretPaths; } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java index a9a1d40c..5827ccc6 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java @@ -43,6 +43,8 @@ public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, An builder.property(PLAYBOOK_INLINE_PROP); builder.property(EXTRA_VARS_PROP); builder.property(CONFIG_ENCRYPT_EXTRA_VARS); + builder.property(GENERATE_INVENTORY_PROP); + builder.property(GENERATE_INVENTORY_NODES_AUTH); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); builder.property(EXTRA_ATTRS_PROP); @@ -121,7 +123,13 @@ public void executeNodeStep( @Override public List listSecretsPathWorkflowNodeStep(ExecutionContext context, INodeEntry node, Map configuration) { AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), configuration); - return AnsibleUtil.getSecretsPath(builder); + List secretPaths = AnsibleUtil.getSecretsPath(builder); + List secretPathsNodes = builder.getListNodesKeyPath(); + + if(secretPathsNodes != null && !secretPathsNodes.isEmpty()){ + secretPaths.addAll(secretPathsNodes); + } + return secretPaths; } @Override diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java index 1f5ba521..762ad794 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java @@ -127,7 +127,7 @@ public Description getDescription() { @Override public List listSecretsPathWorkflowStep(ExecutionContext context, Map configuration) { AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(context, context.getFramework(), context.getNodes(), configuration); - return AnsibleUtil.getSecretsPath(builder); + return AnsibleUtil.getSecretsPathWorkflowSteps(builder); } @Override public Map getRuntimeProperties(ExecutionContext context) { diff --git a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java index 126a36f7..9be0accd 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java @@ -86,6 +86,16 @@ public static List getSecretsPath(AnsibleRunnerContextBuilder builder){ } + public static List getSecretsPathWorkflowSteps(AnsibleRunnerContextBuilder builder){ + List secretPaths = getSecretsPath(builder); + List secretPathsNodes =builder.getListNodesKeyPath(); + + if(secretPathsNodes!=null && !secretPathsNodes.isEmpty()){ + secretPaths.addAll(secretPathsNodes); + } + return secretPaths; + } + public static Map getRuntimeProperties(ExecutionContext context, String propertyPrefix) { Map properties = null;