From 35089c52545c2b6ce9c316d354a153c18e48e7db Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 1 Mar 2024 12:24:57 -0300 Subject: [PATCH 01/23] use ansible vault to encrypt temporary files --- .../ansible/ansible/AnsibleRunner.java | 104 +++++++++++++++--- 1 file changed, 89 insertions(+), 15 deletions(-) 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 a8140151..fdf49570 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -107,10 +107,15 @@ public static List tokenizeCommand(String commandline) { private Listener listener; + private String generatedVaultPassword; + + private boolean encryptVarsFiles = false; + private AnsibleRunner(AnsibleCommand type) { this.type = type; } + public AnsibleRunner setInventory(String inv) { if (inv != null && inv.length() > 0) { inventory = inv; @@ -424,8 +429,9 @@ public int run() throws Exception { } if (extraVars != null && extraVars.length() > 0) { - tempVarsFile = File.createTempFile("ansible-runner", "extra-vars"); - Files.write(tempVarsFile.toPath(), extraVars.getBytes()); + tempVarsFile = this.createTemporaryFile("id_rsa",extraVars , true); + //File.createTempFile("ansible-runner", "extra-vars"); + //Files.write(tempVarsFile.toPath(), extraVars.getBytes()); procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); } @@ -436,19 +442,23 @@ public int run() throws Exception { } if (sshPrivateKey != null && sshPrivateKey.length() > 0) { - tempPkFile = File.createTempFile("ansible-runner", "id_rsa"); - // Only the owner can read and write - Set perms = new HashSet(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(tempPkFile.toPath(), perms); - - Files.write(tempPkFile.toPath(), sshPrivateKey.replaceAll("\r\n","\n").getBytes()); - procArgs.add("--private-key" + "=" + tempPkFile.toPath()); - - if(sshUseAgent){ - registerKeySshAgent(tempPkFile.getAbsolutePath()); - } + + String privateKeyData = sshPrivateKey.replaceAll("\r\n","\n"); + tempPkFile = this.createTemporaryFile("id_rsa",privateKeyData , true); + + //File.createTempFile("ansible-runner", "id_rsa"); + // Only the owner can read and write + Set perms = new HashSet(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(tempPkFile.toPath(), perms); + + //Files.write(tempPkFile.toPath(), sshPrivateKey.replaceAll("\r\n","\n").getBytes()); + procArgs.add("--private-key" + "=" + tempPkFile.toPath()); + + if(sshUseAgent){ + registerKeySshAgent(tempPkFile.getAbsolutePath()); + } } if (sshUser != null && sshUser.length() > 0) { @@ -491,6 +501,12 @@ public int run() throws Exception { System.out.println(" procArgs: " + procArgs); } + if(encryptVarsFiles){ + tempVaultFile = File.createTempFile("ansible-runner", "vault"); + Files.write(tempVaultFile.toPath(), generatedVaultPassword.getBytes()); + procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); + } + // execute the ansible process ProcessBuilder processBuilder = new ProcessBuilder() .command(procArgs) @@ -713,4 +729,62 @@ public boolean registerKeySshAgent(String keyPath) throws AnsibleException, Exce return true; } + + public File createTemporaryFile(String suffix, String data, boolean encrypt) throws IOException { + File tempVarsFile = File.createTempFile("ansible-runner", "extra-vars"); + Files.write(tempVarsFile.toPath(), data.getBytes()); + + if(encrypt){ + encryptFileAnsibleVault(tempVarsFile); + } + + return tempVarsFile; + + } + + public void encryptFileAnsibleVault(File file) throws IOException { + + List procArgs = new ArrayList<>(); + procArgs.add("ansible-vault"); + procArgs.add("encrypt"); + procArgs.add(file.getAbsolutePath()); + + // execute the ssh-agent add process + ProcessBuilder processBuilder = new ProcessBuilder() + .command(procArgs) + .directory(baseDirectory.toFile()); + Process proc = null; + + try { + proc = processBuilder.start(); + + OutputStream stdin = proc.getOutputStream(); + OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + + try { + stdinw.write(generatedVaultPassword + "\n"); + stdinw.write(generatedVaultPassword + "\n"); + stdinw.flush(); + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + } + + int exitCode = proc.waitFor(); + + if (exitCode != 0) { + throw new AnsibleException("ERROR: encryptFileAnsibleVault:" + procArgs.toString(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + + + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + }finally { + // Make sure to always cleanup on failure and success + if(proc!=null) { + proc.destroy(); + } + } + } + } From 2a36b4bbfd31d7d94a1586d070b5da55e93976e5 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 1 Mar 2024 16:07:22 -0300 Subject: [PATCH 02/23] add field enable to encrypt temporary fields generate random password for encrypt temp files --- .../ansible/ansible/AnsibleDescribable.java | 9 ++++ .../ansible/ansible/AnsibleRunner.java | 42 ++++++++++++------- .../ansible/ansible/AnsibleRunnerBuilder.java | 19 +++++++++ .../ansible/plugin/AnsibleFileCopier.java | 5 +++ .../ansible/plugin/AnsibleNodeExecutor.java | 8 +++- ...AnsiblePlaybookInlineWorkflowNodeStep.java | 1 + .../AnsiblePlaybookInlineWorkflowStep.java | 1 + .../AnsiblePlaybookWorflowNodeStep.java | 1 + .../plugin/AnsiblePlaybookWorkflowStep.java | 1 + .../plugin/AnsibleResourceModelSource.java | 8 +++- .../AnsibleResourceModelSourceFactory.java | 1 + 11 files changed, 77 insertions(+), 19 deletions(-) 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 5c352c6c..a514ea56 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java @@ -156,6 +156,8 @@ public static String[] getValues() { public static final String PROJ_PROP_PREFIX = "project."; public static final String FWK_PROP_PREFIX = "framework."; + public static final String ENCRYPT_TEMP_FILES = "ansible-encrypt-temp-files"; + public static Property PLAYBOOK_PATH_PROP = PropertyUtil.string( ANSIBLE_PLAYBOOK_PATH, "Playbook", @@ -519,4 +521,11 @@ public static String[] getValues() { .title("Ansible binaries directory path") .description("Set ansible binaries directory path.") .build(); + + public static final Property CONFIG_ENCRYPT_TEMP_FILES = PropertyBuilder.builder() + .booleanType(ENCRYPT_TEMP_FILES) + .required(false) + .title("Encrypt temporary files.") + .description("Encrypt temporary files used for authentication and extra vars.") + .build(); } 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 fdf49570..17e2755e 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -109,7 +109,7 @@ public static List tokenizeCommand(String commandline) { private String generatedVaultPassword; - private boolean encryptVarsFiles = false; + private boolean encryptTempFiles = false; private AnsibleRunner(AnsibleCommand type) { this.type = type; @@ -341,6 +341,11 @@ public AnsibleRunner executable(String executable) { return this; } + public AnsibleRunner encryptTemporaryFiles(boolean encryptVarsFiles){ + this.encryptTempFiles = encryptVarsFiles; + return this; + } + public void deleteTempDirectory(Path tempDirectory) throws IOException { Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { @Override @@ -376,6 +381,16 @@ public int run() throws Exception { File tempVarsFile = null; List procArgs = new ArrayList<>(); + + if(encryptTempFiles){ + UUID uuid = UUID.randomUUID(); + generatedVaultPassword = uuid.toString(); + + tempVaultFile = File.createTempFile("ansible-runner", "internal-vault"); + Files.write(tempVaultFile.toPath(), generatedVaultPassword.getBytes()); + procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); + } + String ansibleCommand = type.command; if (ansibleBinariesDirectory != null) { ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); @@ -429,7 +444,7 @@ public int run() throws Exception { } if (extraVars != null && extraVars.length() > 0) { - tempVarsFile = this.createTemporaryFile("id_rsa",extraVars , true); + tempVarsFile = this.createTemporaryFile("extra-vars",extraVars , encryptTempFiles); //File.createTempFile("ansible-runner", "extra-vars"); //Files.write(tempVarsFile.toPath(), extraVars.getBytes()); procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); @@ -444,21 +459,22 @@ public int run() throws Exception { if (sshPrivateKey != null && sshPrivateKey.length() > 0) { String privateKeyData = sshPrivateKey.replaceAll("\r\n","\n"); - tempPkFile = this.createTemporaryFile("id_rsa",privateKeyData , true); + tempPkFile = this.createTemporaryFile("id_rsa",privateKeyData , false); - //File.createTempFile("ansible-runner", "id_rsa"); // Only the owner can read and write Set perms = new HashSet(); perms.add(PosixFilePermission.OWNER_READ); perms.add(PosixFilePermission.OWNER_WRITE); Files.setPosixFilePermissions(tempPkFile.toPath(), perms); - //Files.write(tempPkFile.toPath(), sshPrivateKey.replaceAll("\r\n","\n").getBytes()); - procArgs.add("--private-key" + "=" + tempPkFile.toPath()); - if(sshUseAgent){ - registerKeySshAgent(tempPkFile.getAbsolutePath()); + registerKeySshAgent(tempPkFile.getAbsolutePath()); + } + + if(encryptTempFiles){ + encryptFileAnsibleVault(tempPkFile); } + procArgs.add("--private-key" + "=" + tempPkFile.toPath()); } if (sshUser != null && sshUser.length() > 0) { @@ -501,12 +517,6 @@ public int run() throws Exception { System.out.println(" procArgs: " + procArgs); } - if(encryptVarsFiles){ - tempVaultFile = File.createTempFile("ansible-runner", "vault"); - Files.write(tempVaultFile.toPath(), generatedVaultPassword.getBytes()); - procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); - } - // execute the ansible process ProcessBuilder processBuilder = new ProcessBuilder() .command(procArgs) @@ -731,7 +741,7 @@ public boolean registerKeySshAgent(String keyPath) throws AnsibleException, Exce public File createTemporaryFile(String suffix, String data, boolean encrypt) throws IOException { - File tempVarsFile = File.createTempFile("ansible-runner", "extra-vars"); + File tempVarsFile = File.createTempFile("ansible-runner", suffix); Files.write(tempVarsFile.toPath(), data.getBytes()); if(encrypt){ @@ -742,7 +752,7 @@ public File createTemporaryFile(String suffix, String data, boolean encrypt) thr } - public void encryptFileAnsibleVault(File file) throws IOException { + public void encryptFileAnsibleVault(File file){ List procArgs = new ArrayList<>(); procArgs.add("ansible-vault"); diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java index 0e133af9..627d2e35 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java @@ -843,6 +843,11 @@ public AnsibleRunner buildAnsibleRunner() throws ConfigurationException{ runner = runner.ansibleBinariesDirectory(binariesFilePath); } + boolean encryptTempFiles = encryptTempFiles(); + if(encryptTempFiles){ + runner = runner.encryptTemporaryFiles(true); + } + return runner; } @@ -938,6 +943,9 @@ public String getPassphraseStoragePath(){ } public String getPassphraseStorageData(String storagePath) throws ConfigurationException { + if(storagePath == null){ + return null; + } Path path = PathUtil.asPath(storagePath); try { @@ -951,4 +959,15 @@ public String getPassphraseStorageData(String storagePath) throws ConfigurationE "storage path: " + storagePath + ": " + e.getMessage()); } } + + public boolean encryptTempFiles() throws ConfigurationException { + return PropertyResolver.resolveBooleanProperty( + AnsibleDescribable.ENCRYPT_TEMP_FILES, + false, + getFrameworkProject(), + getFramework(), + getNode(), + getjobConf() + ); + } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java index 2f2da759..b3455b39 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java @@ -54,6 +54,8 @@ public class AnsibleFileCopier implements FileCopier, AnsibleDescribable, ProxyR builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); + builder.mapping(ANSIBLE_CONFIG_FILE_PATH,PROJ_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); builder.frameworkMapping(ANSIBLE_CONFIG_FILE_PATH,FWK_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); builder.mapping(ANSIBLE_VAULT_PATH,PROJ_PROP_PREFIX + ANSIBLE_VAULT_PATH); @@ -66,6 +68,9 @@ public class AnsibleFileCopier implements FileCopier, AnsibleDescribable, ProxyR builder.frameworkMapping(ANSIBLE_SSH_PASSPHRASE_OPTION,FWK_PROP_PREFIX + ANSIBLE_SSH_PASSPHRASE_OPTION); builder.mapping(ANSIBLE_SSH_USE_AGENT,PROJ_PROP_PREFIX + ANSIBLE_SSH_USE_AGENT); builder.frameworkMapping(ANSIBLE_SSH_USE_AGENT,FWK_PROP_PREFIX + ANSIBLE_SSH_USE_AGENT); + builder.mapping(ENCRYPT_TEMP_FILES,PROJ_PROP_PREFIX + ENCRYPT_TEMP_FILES); + builder.frameworkMapping(ENCRYPT_TEMP_FILES,FWK_PROP_PREFIX + ENCRYPT_TEMP_FILES); + DESC=builder.build(); } 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 cf3f4161..81516e9c 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java @@ -54,6 +54,9 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); + + builder.mapping(ANSIBLE_BINARIES_DIR_PATH,PROJ_PROP_PREFIX + ANSIBLE_BINARIES_DIR_PATH); builder.frameworkMapping(ANSIBLE_BINARIES_DIR_PATH,FWK_PROP_PREFIX + ANSIBLE_BINARIES_DIR_PATH); builder.mapping(ANSIBLE_EXECUTABLE,PROJ_PROP_PREFIX + ANSIBLE_EXECUTABLE); @@ -95,7 +98,10 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.mapping(ANSIBLE_VAULTSTORE_PATH,PROJ_PROP_PREFIX + ANSIBLE_VAULTSTORE_PATH); builder.frameworkMapping(ANSIBLE_VAULTSTORE_PATH,FWK_PROP_PREFIX + ANSIBLE_VAULTSTORE_PATH); - DESC=builder.build(); + builder.mapping(ENCRYPT_TEMP_FILES,PROJ_PROP_PREFIX + ENCRYPT_TEMP_FILES); + builder.frameworkMapping(ENCRYPT_TEMP_FILES,FWK_PROP_PREFIX + ENCRYPT_TEMP_FILES); + + DESC=builder.build(); } @Override 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 de42a775..d9675ff2 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java @@ -55,6 +55,7 @@ public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, An builder.property(BECOME_AUTH_TYPE_PROP); builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC=builder.build(); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java index 7c6c7e8b..b4138515 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java @@ -56,6 +56,7 @@ public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDes builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(DISABLE_LIMIT_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC = builder.build(); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java index 227540ec..6a754a1f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java @@ -55,6 +55,7 @@ public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDe builder.property(BECOME_AUTH_TYPE_PROP); builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC=builder.build(); } 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 80e0bc55..73dce781 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java @@ -56,6 +56,7 @@ public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribab builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(DISABLE_LIMIT_PROP); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC = builder.build(); } 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 bad3aedb..012492a1 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -94,6 +94,8 @@ public class AnsibleResourceModelSource implements ResourceModelSource, ProxyRun protected String becamePasswordStoragePath; + protected boolean encryptTempFiles = false; + public AnsibleResourceModelSource(final Framework framework) { this.framework = framework; @@ -191,6 +193,9 @@ public void configure(Properties configuration) throws ConfigurationException { vaultPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); becamePasswordStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); + + encryptTempFiles = "true".equals(resolveProperty(AnsibleDescribable.ENCRYPT_TEMP_FILES,"false",configuration,executionDataContext)); + } public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ @@ -336,8 +341,7 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner.extraParams(extraParameters); } - - + runner.encryptTemporaryFiles(encryptTempFiles); return runner; } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java index a49366df..377c8515 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java @@ -72,6 +72,7 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { builder.mapping(ANSIBLE_VAULT_PATH,PROJ_PROP_PREFIX + ANSIBLE_VAULT_PATH); builder.frameworkMapping(ANSIBLE_VAULT_PATH,FWK_PROP_PREFIX + ANSIBLE_VAULT_PATH); + builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC=builder.build(); } From d4d0463c29d22d4c8d40df19226e387203c4edd6 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 7 Mar 2024 15:15:57 -0300 Subject: [PATCH 03/23] refactor --- build.gradle | 14 +- .../ansible/ansible/AnsibleDescribable.java | 10 +- .../ansible/ansible/AnsibleRunner.java | 1422 +++++++++-------- ....java => AnsibleRunnerContextBuilder.java} | 586 +++---- .../ansible/plugin/AnsibleFileCopier.java | 23 +- .../plugin/AnsibleModuleWorkflowStep.java | 12 +- .../ansible/plugin/AnsibleNodeExecutor.java | 19 +- ...AnsiblePlaybookInlineWorkflowNodeStep.java | 13 +- .../AnsiblePlaybookInlineWorkflowStep.java | 20 +- .../AnsiblePlaybookWorflowNodeStep.java | 16 +- .../plugin/AnsiblePlaybookWorkflowStep.java | 17 +- .../plugin/AnsibleResourceModelSource.java | 71 +- .../AnsibleResourceModelSourceFactory.java | 2 +- .../plugins/ansible/util/AnsibleUtil.java | 18 +- .../plugins/ansible/util/ProcessExecutor.java | 65 + 15 files changed, 1132 insertions(+), 1176 deletions(-) rename src/main/groovy/com/rundeck/plugins/ansible/ansible/{AnsibleRunnerBuilder.java => AnsibleRunnerContextBuilder.java} (62%) create mode 100644 src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java diff --git a/build.gradle b/build.gradle index 12c4042d..ba6376aa 100644 --- a/build.gradle +++ b/build.gradle @@ -20,10 +20,10 @@ ext.pluginClassNames = [ apply plugin: 'java' apply plugin: 'groovy' -sourceCompatibility = 1.8 +sourceCompatibility = 1.11 scmVersion { - ignoreUncommittedChanges = true + ignoreUncommittedChanges = false tag { prefix = 'v' versionSeparator = '' @@ -47,8 +47,16 @@ configurations { dependencies { pluginLibs 'com.google.code.gson:gson:2.10.1' - implementation('org.rundeck:rundeck-core:4.17.2-rc1-20231025') + implementation('org.rundeck:rundeck-core:5.1.1-20240305') implementation 'org.codehaus.groovy:groovy-all:3.0.9' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.16.1' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' + + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' + + testCompileOnly 'org.projectlombok:lombok:1.18.30' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' testImplementation platform("org.spockframework:spock-bom:2.0-groovy-3.0") testImplementation "org.spockframework:spock-core" 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 a514ea56..53e7234a 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleDescribable.java @@ -156,7 +156,7 @@ public static String[] getValues() { public static final String PROJ_PROP_PREFIX = "project."; public static final String FWK_PROP_PREFIX = "framework."; - public static final String ENCRYPT_TEMP_FILES = "ansible-encrypt-temp-files"; + public static final String ANSIBLE_ENCRYPT_EXTRA_VARS = "ansible-encrypt-extra-vars"; public static Property PLAYBOOK_PATH_PROP = PropertyUtil.string( ANSIBLE_PLAYBOOK_PATH, @@ -522,10 +522,10 @@ public static String[] getValues() { .description("Set ansible binaries directory path.") .build(); - public static final Property CONFIG_ENCRYPT_TEMP_FILES = PropertyBuilder.builder() - .booleanType(ENCRYPT_TEMP_FILES) + public static final Property CONFIG_ENCRYPT_EXTRA_VARS = PropertyBuilder.builder() + .booleanType(ANSIBLE_ENCRYPT_EXTRA_VARS) .required(false) - .title("Encrypt temporary files.") - .description("Encrypt temporary files used for authentication and extra vars.") + .title("Encrypt Extra Vars.") + .description("Encrypt the value of the extra vars keys.") .build(); } 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 17e2755e..edc69547 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -1,10 +1,14 @@ package com.rundeck.plugins.ansible.ansible; -import com.rundeck.plugins.ansible.util.Logging; -import com.rundeck.plugins.ansible.util.ListenerFactory; -import com.rundeck.plugins.ansible.util.Listener; -import com.rundeck.plugins.ansible.util.ArgumentTokenizer; +import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.rundeck.plugins.ansible.util.*; import com.dtolabs.rundeck.core.utils.SSHAgentProcess; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import lombok.Builder; +import lombok.Data; + import java.io.*; import java.nio.file.FileVisitResult; @@ -16,785 +20,815 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.*; +@Builder +@Data public class AnsibleRunner { - enum AnsibleCommand { - AdHoc("ansible"), - PlaybookPath("ansible-playbook"), - PlaybookInline("ansible-playbook"); + enum AnsibleCommand { + AdHoc("ansible"), + PlaybookPath("ansible-playbook"), + PlaybookInline("ansible-playbook"); - final String command; - AnsibleCommand(String command) { - this.command = command; - } - } - - public static AnsibleRunner adHoc(String module, String arg) { - AnsibleRunner ar = new AnsibleRunner(AnsibleCommand.AdHoc); - ar.module = module; - ar.arg = arg; - return ar; - } - - public static AnsibleRunner playbookPath(String playbook) { - AnsibleRunner ar = new AnsibleRunner(AnsibleCommand.PlaybookPath); - ar.playbook = playbook; - return ar; - } - - public static AnsibleRunner playbookInline(String playbook) { - AnsibleRunner ar = new AnsibleRunner(AnsibleCommand.PlaybookInline); - ar.playbook = playbook; - return ar; - } - - /** - * Splits up a command and its arguments inf form of a string into a list of strings. - * @param commandline String with a possibly complex command and arguments - * @return a list of arguments - */ - public static List tokenizeCommand(String commandline) { - List tokens = ArgumentTokenizer.tokenize(commandline, true); - List args = new ArrayList<>(); - for (String token : tokens) { - args.add(token.replaceAll("\\\\", "\\\\").replaceAll("^\"|\"$", "")); - } - return args; - } - - private boolean done = false; - - private final AnsibleCommand type; - - private String playbook; - private String inventory; - private String module; - private String arg; - private String extraVars; - private String extraParams; - private String vaultPass; - private boolean ignoreErrors = false; - - // ansible ssh args - private boolean sshUsePassword = false; - private String sshPass; - private String sshUser; - private String sshPrivateKey; - private Integer sshTimeout; - private boolean sshUseAgent = false; - private String sshPassphrase; - private SSHAgentProcess sshAgent; - private Integer sshAgentTimeToLive = 0; - - // ansible become args - protected Boolean become = Boolean.FALSE; - protected String becomeMethod; - protected String becomeUser; - protected String becomePassword; - - private boolean debug = false; - - private Path baseDirectory; - private Path ansibleBinariesDirectory; - private boolean usingTempDirectory; - private boolean retainTempDirectory; - private final List limits = new ArrayList<>(); - private int result; - private Map options = new HashMap<>(); - private String executable = "sh"; - - protected String configFile; - - private Listener listener; - - private String generatedVaultPassword; - - private boolean encryptTempFiles = false; - - private AnsibleRunner(AnsibleCommand type) { - this.type = type; - } - - - public AnsibleRunner setInventory(String inv) { - if (inv != null && inv.length() > 0) { - inventory = inv; - } - return this; - } - - public AnsibleRunner limit(String host) { - limits.add(host); - return this; - } - - public AnsibleRunner limit(Collection hosts) { - limits.addAll(hosts); - return this; - } - - /** - * Additional arguments to pass to the process - * @param args extra commandline which gets appended to the base command and arguments - */ - public AnsibleRunner extraParams(String params) { - if (params != null && params.length() > 0) { - extraParams = params; - } - return this; - } - - public AnsibleRunner extraVars(String args) { - if (args != null && args.length() > 0) { - extraVars = args; - } - return this; - } - - /** - * Add options passed as Environment variables to ansible - */ - public AnsibleRunner options(Map options) { - this.options.putAll(options); - return this; - } - - /** - * Vault Password - * @param pass vault password to be used to decrypt group variables - */ - public AnsibleRunner vaultPass(String pass) { - if (pass != null && pass.length() > 0) { - vaultPass = pass; + final String command; + + AnsibleCommand(String command) { + this.command = command; + } } - return this; - } - public AnsibleRunner ignoreErrors(boolean ignoreErrors) { - this.ignoreErrors = ignoreErrors; - return this; - } + public static class AnsibleRunnerBuilder { + public AnsibleRunnerBuilder limits(String host) { + List hosts = new ArrayList<>(); + hosts.add(host); + this.limits = hosts; + return this; + } - public AnsibleRunner sshUser(String user) { - if (user != null && user.length() > 0) { - sshUser = user; - } - return this; - } + public AnsibleRunnerBuilder limits(List hosts) { + this.limits = hosts; + return this; + } - public AnsibleRunner sshPass(String pass) { - if (pass != null && pass.length() > 0) { - sshPass = pass; + /** + * Specify in which directory Ansible is run, noting it is a temporary directory. + */ + public AnsibleRunnerBuilder tempDirectory(Path dir) { + if (dir != null) { + this.baseDirectory = dir; + this.usingTempDirectory = true; + } + return this; + } } - return this; - } - public AnsibleRunner sshPrivateKey(String key) { - if (key != null && key.length() > 0) { - sshPrivateKey = key; - } - return this; - } - - public AnsibleRunner sshUsePassword(Boolean usePass) { - if (usePass != null) { - sshUsePassword = usePass; - } else { - sshUsePassword = false; + public static AnsibleRunner.AnsibleRunnerBuilder adHoc(String module, String arg) { + AnsibleRunner.AnsibleRunnerBuilder builder = AnsibleRunner.builder(); + builder.type(AnsibleCommand.AdHoc); + builder.module(module); + builder.arg(arg); + return builder; } - return this; - } - - public AnsibleRunner sshUseAgent(Boolean useAgent) { - if (useAgent != null) { - sshUseAgent = useAgent; - } else { - sshUseAgent = false; - } - return this; - } - public AnsibleRunner sshPassphrase(String passphrase) { - if (passphrase != null && passphrase.length() > 0) { - sshPassphrase = passphrase; + public static AnsibleRunner.AnsibleRunnerBuilder playbookPath(String playbook) { + AnsibleRunner.AnsibleRunnerBuilder builder = AnsibleRunner.builder(); + builder.type(AnsibleCommand.PlaybookPath); + builder.playbook(playbook); + return builder; } - return this; - } - public AnsibleRunner sshTimeout(Integer timeout) { - if (timeout != null && timeout > 0) { - sshTimeout = timeout; - } - return this; - } - - public AnsibleRunner become(Boolean useBecome) { - if (useBecome != null) { - become = useBecome; - } else { - become = false; + public static AnsibleRunner.AnsibleRunnerBuilder playbookInline(String playbook) { + AnsibleRunner.AnsibleRunnerBuilder builder = AnsibleRunner.builder(); + builder.type(AnsibleCommand.PlaybookInline); + builder.playbook(playbook); + return builder; } - return this; - } - public AnsibleRunner becomeMethod(String method) { - if (method != null && method.length() > 0) { - becomeMethod = method; - } - return this; - } + public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder contextBuilder) throws ConfigurationException { - public AnsibleRunner becomeUser(String user) { - if (user != null && user.length() > 0) { - becomeUser = user; - } - return this; - } + AnsibleRunner.AnsibleRunnerBuilder ansibleRunnerBuilder = null; - public AnsibleRunner becomePassword(String pass) { - if (pass != null && pass.length() > 0) { - becomePassword = pass; - } - return this; - } + String playbook; + String module; - public AnsibleRunner configFile(String path) { - if (path != null && path.length() > 0) { - configFile = path; - } - return this; - } - - /** - * Set the listener to notify, when run in stream mode, see {@link #stream()} - * @param listener the listener which will receive output lines - */ - public AnsibleRunner listener(Listener listener) { - this.listener = listener; - return this; - } - - /** - * Run Ansible with -vvvv and print the command and output to the console / log - */ - public AnsibleRunner debug() { - return debug(true); - } - - /** - * Run Ansible with -vvvv and print the command and output to the console / log - */ - public AnsibleRunner debug(boolean debug) { - this.debug = debug; - return this; - } - - /** - * Keep the temp dir around, dont' delete it. - */ - public AnsibleRunner retainTempDirectory() { - return retainTempDirectory(true); - } - - /** - * Keep the temp dir around, dont' delete it. - */ - public AnsibleRunner retainTempDirectory(boolean retainTempDirectory) { - this.retainTempDirectory = retainTempDirectory; - return this; - } - - /** - * Specify in which directory Ansible is run, noting it is a temporary directory. - */ - public AnsibleRunner tempDirectory(Path dir) { - if (dir != null) { - this.baseDirectory = dir; - this.usingTempDirectory = true; - } - return this; - } - - /** - * Specify in which directory Ansible is run. - * A temporary directory will be used if none is specified. - */ - public AnsibleRunner baseDirectory(String dir) { - if (dir != null) { - this.baseDirectory = Paths.get(dir); - } - return this; - } + if ((playbook = contextBuilder.getPlaybookPath()) != null) { + ansibleRunnerBuilder = AnsibleRunner.playbookPath(playbook); + } else if ((playbook = contextBuilder.getPlaybookInline()) != null) { + ansibleRunnerBuilder = AnsibleRunner.playbookInline(playbook); + } else if ((module = contextBuilder.getModule()) != null) { + ansibleRunnerBuilder = AnsibleRunner.adHoc(module, contextBuilder.getModuleArgs()); + } else { + throw new ConfigurationException("Missing module or playbook job arguments"); + } - public AnsibleRunner ansibleBinariesDirectory(String dir) { - if (dir != null) { - this.ansibleBinariesDirectory = Paths.get(dir); - } - return this; - } - - /** - * Specify the executable - */ - public AnsibleRunner executable(String executable) { - this.executable = executable; - return this; - } - - public AnsibleRunner encryptTemporaryFiles(boolean encryptVarsFiles){ - this.encryptTempFiles = encryptVarsFiles; - return this; - } - - public void deleteTempDirectory(Path tempDirectory) throws IOException { - Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } - - public int run() throws Exception { - if (done) { - throw new IllegalStateException("already done"); - } - done = true; + final AnsibleDescribable.AuthenticationType authType = contextBuilder.getSshAuthenticationType(); + if (AnsibleDescribable.AuthenticationType.privateKey == authType) { + final String privateKey = contextBuilder.getSshPrivateKey(); + if (privateKey != null) { + ansibleRunnerBuilder.sshPrivateKey(privateKey); + } + + if (contextBuilder.getUseSshAgent()) { + ansibleRunnerBuilder.sshUseAgent(true); + + String passphraseOption = contextBuilder.getPassphrase(); + ansibleRunnerBuilder.sshPassphrase(passphraseOption); + } + } else if (AnsibleDescribable.AuthenticationType.password == authType) { + final String password = contextBuilder.getSshPassword(); + if (password != null) { + ansibleRunnerBuilder.sshUsePassword(Boolean.TRUE).sshPass(password); + } + } - if (baseDirectory == null) { - // Use a temporary directory and mark it for possible removal later - this.usingTempDirectory = true; - baseDirectory = Files.createTempDirectory("ansible-rundeck"); - } + // set rundeck options as environment variables +// Map options = context.getDataContext().get("option"); +// if (options != null) { +// runner = runner.options(options); +// } - File tempPlaybook = null; - File tempFile = null; - File tempVaultFile = null; - File tempPkFile = null; - File tempVarsFile = null; + String inventory = contextBuilder.getInventory(); + if (inventory != null) { + ansibleRunnerBuilder.inventory(inventory); + } - List procArgs = new ArrayList<>(); + String limit = contextBuilder.getLimit(); + if (limit != null) { + ansibleRunnerBuilder.limits(limit); + } - if(encryptTempFiles){ - UUID uuid = UUID.randomUUID(); - generatedVaultPassword = uuid.toString(); + Boolean debug = contextBuilder.getDebug(); + if (debug != null) { + if (debug == Boolean.TRUE) { + ansibleRunnerBuilder.debug(Boolean.TRUE); + } else { + ansibleRunnerBuilder.debug(Boolean.FALSE); + } + } - tempVaultFile = File.createTempFile("ansible-runner", "internal-vault"); - Files.write(tempVaultFile.toPath(), generatedVaultPassword.getBytes()); - procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); - } + String extraParams = contextBuilder.getExtraParams(); + if (extraParams != null) { + ansibleRunnerBuilder.extraParams(extraParams); + } - String ansibleCommand = type.command; - if (ansibleBinariesDirectory != null) { - ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); - } - procArgs.add(ansibleCommand); - - // parse arguments - if (type == AnsibleCommand.AdHoc) { - procArgs.add("all"); - - procArgs.add("-m"); - procArgs.add(module); - - if (arg != null && arg.length() > 0) { - procArgs.add("-a"); - procArgs.add(arg); - } - procArgs.add("-t"); - procArgs.add(baseDirectory.toFile().getAbsolutePath()); - } else if (type == AnsibleCommand.PlaybookPath) { - procArgs.add(playbook); - } else if (type == AnsibleCommand.PlaybookInline) { - - tempPlaybook = File.createTempFile("ansible-runner", "playbook"); - Files.write(tempPlaybook.toPath(), playbook.toString().getBytes()); - procArgs.add(tempPlaybook.getAbsolutePath()); - } + String extraVars = contextBuilder.getExtraVars(); + if (extraVars != null) { + ansibleRunnerBuilder.extraVars(extraVars); + } - if (inventory != null && inventory.length() > 0) { - procArgs.add("--inventory-file" + "=" + inventory); - } + String user = contextBuilder.getSshUser(); + if (user != null) { + ansibleRunnerBuilder.sshUser(user); + } - if (limits != null && limits.size() == 1) { - procArgs.add("-l"); - procArgs.add(limits.get(0)); + String vault = contextBuilder.getVaultKey(); + if (vault != null) { + ansibleRunnerBuilder.vaultPass(vault); + } - } else if (limits != null && limits.size() > 1) { - tempFile = File.createTempFile("ansible-runner", "targets"); - StringBuilder sb = new StringBuilder(); - for (String limit : limits) { - sb.append(limit).append("\n"); - } - Files.write(tempFile.toPath(), sb.toString().getBytes()); + Integer timeout = contextBuilder.getSSHTimeout(); + if (timeout != null) { + ansibleRunnerBuilder.sshTimeout(timeout); + } - procArgs.add("-l"); - procArgs.add("@" + tempFile.getAbsolutePath()); - } + Boolean become = contextBuilder.getBecome(); + if (become != null) { + ansibleRunnerBuilder.become(become); + } - if (debug == Boolean.TRUE) { - procArgs.add("-vvv"); - } + String become_user = contextBuilder.getBecomeUser(); + if (become_user != null) { + ansibleRunnerBuilder.becomeUser(become_user); + } - if (extraVars != null && extraVars.length() > 0) { - tempVarsFile = this.createTemporaryFile("extra-vars",extraVars , encryptTempFiles); - //File.createTempFile("ansible-runner", "extra-vars"); - //Files.write(tempVarsFile.toPath(), extraVars.getBytes()); - procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); - } + AnsibleDescribable.BecomeMethodType become_method = contextBuilder.getBecomeMethod(); + if (become_method != null) { + ansibleRunnerBuilder.becomeMethod(become_method.name()); + } - if (vaultPass != null && vaultPass.length() > 0) { - tempVaultFile = File.createTempFile("ansible-runner", "vault"); - Files.write(tempVaultFile.toPath(), vaultPass.getBytes()); - procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); - } + String become_password = contextBuilder.getBecomePassword(); + if (become_password != null) { + ansibleRunnerBuilder.becomePassword(become_password); + } - if (sshPrivateKey != null && sshPrivateKey.length() > 0) { + String executable = contextBuilder.getExecutable(); + if (executable != null) { + ansibleRunnerBuilder.executable(executable); + } - String privateKeyData = sshPrivateKey.replaceAll("\r\n","\n"); - tempPkFile = this.createTemporaryFile("id_rsa",privateKeyData , false); + String configFile = contextBuilder.getConfigFile(); + if (configFile != null) { + ansibleRunnerBuilder.configFile(configFile); + } - // Only the owner can read and write - Set perms = new HashSet(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(tempPkFile.toPath(), perms); + String baseDir = contextBuilder.getBaseDir(); + if (baseDir != null) { + ansibleRunnerBuilder.baseDirectory(java.nio.file.Path.of(baseDir)); + } - if(sshUseAgent){ - registerKeySshAgent(tempPkFile.getAbsolutePath()); - } + String binariesFilePath = contextBuilder.getBinariesFilePath(); + if (binariesFilePath != null) { + ansibleRunnerBuilder.ansibleBinariesDirectory(java.nio.file.Path.of(binariesFilePath)); + } - if(encryptTempFiles){ - encryptFileAnsibleVault(tempPkFile); - } - procArgs.add("--private-key" + "=" + tempPkFile.toPath()); - } + boolean encryptExtraVars = contextBuilder.encryptExtraVars(); + if (encryptExtraVars) { + ansibleRunnerBuilder.encryptExtraVars(true); + } - if (sshUser != null && sshUser.length() > 0) { - procArgs.add("--user" + "=" + sshUser); + return ansibleRunnerBuilder.build(); + } + + + @Builder.Default + private boolean done = false; + + private final AnsibleCommand type; + + private String playbook; + private String inventory; + private String module; + private String arg; + private String extraVars; + private String extraParams; + private String vaultPass; + @Builder.Default + private boolean ignoreErrors = false; + + // ansible ssh args + @Builder.Default + private boolean sshUsePassword = false; + private String sshPass; + private String sshUser; + private String sshPrivateKey; + private Integer sshTimeout; + @Builder.Default + private boolean sshUseAgent = false; + private String sshPassphrase; + private SSHAgentProcess sshAgent; + @Builder.Default + private Integer sshAgentTimeToLive = 0; + + // ansible become args + @Builder.Default + protected Boolean become = Boolean.FALSE; + protected String becomeMethod; + protected String becomeUser; + protected String becomePassword; + + @Builder.Default + private boolean debug = false; + + private Path baseDirectory; + private Path ansibleBinariesDirectory; + private boolean usingTempDirectory; + private boolean retainTempDirectory; + private List limits = new ArrayList<>(); + private int result; + @Builder.Default + private Map options = new HashMap<>(); + @Builder.Default + private String executable = "sh"; + + protected String configFile; + + private Listener listener; + + private String generatedVaultPassword; + + @Builder.Default + private boolean encryptExtraVars = false; + + static ObjectMapper mapperYaml = new ObjectMapper(new YAMLFactory()); + static ObjectMapper mapperJson = new ObjectMapper(); + + + /** + * Add options passed as Environment variables to ansible + */ +// public AnsibleRunner options(Map options) { +// this.options.putAll(options); +// return this; +// } + + public void deleteTempDirectory(Path tempDirectory) throws IOException { + Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + + /** + * Splits up a command and its arguments inf form of a string into a list of strings. + * + * @param commandline String with a possibly complex command and arguments + * @return a list of arguments + */ + public static List tokenizeCommand(String commandline) { + List tokens = ArgumentTokenizer.tokenize(commandline, true); + List args = new ArrayList<>(); + for (String token : tokens) { + args.add(token.replaceAll("\\\\", "\\\\").replaceAll("^\"|\"$", "")); + } + return args; } - if (sshUsePassword) { - procArgs.add("--ask-pass"); - } + public int run() throws Exception { + if (done) { + throw new IllegalStateException("already done"); + } + done = true; - if (sshTimeout != null && sshTimeout > 0) { - procArgs.add("--timeout" + "=" + sshTimeout); - } + if (baseDirectory == null) { + // Use a temporary directory and mark it for possible removal later + this.usingTempDirectory = true; + baseDirectory = Files.createTempDirectory("ansible-rundeck"); + } - if (become == true) { - procArgs.add("--become"); - if (becomePassword != null && becomePassword.length() > 0) { - procArgs.add("--ask-become-pass"); - } - } + File tempPlaybook = null; + File tempFile = null; + File tempPkFile = null; + File tempVarsFile = null; - if (becomeMethod != null && becomeMethod.length() > 0) { - procArgs.add("--become-method" + "=" + becomeMethod); - } + List procArgs = new ArrayList<>(); - if (becomeUser != null && becomeUser.length() > 0) { - procArgs.add("--become-user" + "=" + becomeUser); - } + String ansibleCommand = type.command; + if (ansibleBinariesDirectory != null) { + ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); + } + procArgs.add(ansibleCommand); + + // parse arguments + if (type == AnsibleCommand.AdHoc) { + procArgs.add("all"); + + procArgs.add("-m"); + procArgs.add(module); + + if (arg != null && !arg.isEmpty()) { + procArgs.add("-a"); + procArgs.add(arg); + } + procArgs.add("-t"); + procArgs.add(baseDirectory.toFile().getAbsolutePath()); + } else if (type == AnsibleCommand.PlaybookPath) { + procArgs.add(playbook); + } else if (type == AnsibleCommand.PlaybookInline) { + tempPlaybook = AnsibleUtil.createTemporaryFile("playbook", playbook); + procArgs.add(tempPlaybook.getAbsolutePath()); + } - // default the listener to stdout logger - if (listener == null) { - listener = ListenerFactory.getListener(System.out); - } + if (encryptExtraVars) { + UUID uuid = UUID.randomUUID(); + generatedVaultPassword = uuid.toString(); + procArgs.add("--vault-id"); + procArgs.add("interval-encypt@prompt"); + } - if (extraParams != null && extraParams.length() > 0) { - procArgs.addAll(tokenizeCommand(extraParams)); - } + if (inventory != null && !inventory.isEmpty()) { + procArgs.add("--inventory-file" + "=" + inventory); + } - if (debug) { - System.out.println(" procArgs: " + procArgs); - } + if (limits != null && limits.size() == 1) { + procArgs.add("-l"); + procArgs.add(limits.get(0)); - // execute the ansible process - ProcessBuilder processBuilder = new ProcessBuilder() - .command(procArgs) - .directory(baseDirectory.toFile()); // set cwd - Process proc = null; + } else if (limits != null && limits.size() > 1) { + StringBuilder sb = new StringBuilder(); + for (String limit : limits) { + sb.append(limit).append("\n"); + } + tempFile = AnsibleUtil.createTemporaryFile("targets", sb.toString()); - Map processEnvironment = processBuilder.environment(); + procArgs.add("-l"); + procArgs.add("@" + tempFile.getAbsolutePath()); + } - if (configFile != null && configFile.length() > 0) { - if (debug) { - System.out.println(" ANSIBLE_CONFIG: "+configFile); - } + if (debug == Boolean.TRUE) { + procArgs.add("-vvv"); + } - processEnvironment.put("ANSIBLE_CONFIG", configFile); - } + if (extraVars != null && !extraVars.isEmpty()) { + String addeExtraVars = extraVars; - for (String optionName : this.options.keySet()) { - processEnvironment.put(optionName, this.options.get(optionName)); - } + if (encryptExtraVars) { + addeExtraVars = encryptExtraVarsKey(extraVars); + } - if(sshUseAgent && sshAgent!=null){ - processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); - } + System.out.println(addeExtraVars); - try { - proc = processBuilder.start(); - OutputStream stdin = proc.getOutputStream(); - OutputStreamWriter stdinw = new OutputStreamWriter(stdin); - - if (sshUsePassword) { - if (sshPass != null && sshPass.length() > 0) { - stdinw.write(sshPass+"\n"); - stdinw.flush(); - } else { - throw new AnsibleException("Missing ssh password.",AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } - } - - if (become) { - if (becomePassword != null && becomePassword.length() > 0) { - stdinw.write(becomePassword+"\n"); - stdinw.flush(); - } - } - - stdinw.close(); - Thread errthread = Logging.copyStreamThread(proc.getErrorStream(), listener); - Thread outthread = Logging.copyStreamThread(proc.getInputStream(), listener); - errthread.start(); - outthread.start(); - result = proc.waitFor(); - outthread.join(); - errthread.join(); - System.err.flush(); - System.out.flush(); - - - if(sshUseAgent){ - if(sshAgent!=null){ - sshAgent.stopAgent(); - } - } - - if (result != 0) { - if (ignoreErrors == false) { - throw new AnsibleException("ERROR: Ansible execution returned with non zero code.", - AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } - } - } catch (InterruptedException e) { - if(proc!=null) { - proc.destroy(); - } - Thread.currentThread().interrupt(); - throw new AnsibleException("ERROR: Ansible Execution Interrupted.", e, AnsibleException.AnsibleFailureReason.Interrupted); - } catch (IOException e) { - throw new AnsibleException("ERROR: Ansible IO failure: "+e.getMessage(), e, AnsibleException.AnsibleFailureReason.IOFailure); - } catch (AnsibleException e) { - throw e; - } catch (Exception e) { - if(proc!=null) { - proc.destroy(); - } - throw new AnsibleException("ERROR: Ansible execution returned with non zero code.", e, AnsibleException.AnsibleFailureReason.Unknown); - } finally { - // Make sure to always cleanup on failure and success - if(proc!=null) { - proc.getErrorStream().close(); - proc.getInputStream().close(); - proc.getOutputStream().close(); - proc.destroy(); - } - - if (tempFile != null && !tempFile.delete()) { - tempFile.deleteOnExit(); - } - if (tempPkFile != null && !tempPkFile.delete()) { - tempPkFile.deleteOnExit(); - } - if (tempVarsFile != null && !tempVarsFile.delete()) { - tempVarsFile.deleteOnExit(); - } - if (tempVaultFile != null && !tempVaultFile.delete()) { - tempVaultFile.deleteOnExit(); - } - if (tempPlaybook != null && !tempPlaybook.delete()) { - tempPlaybook.deleteOnExit(); - } - - if (usingTempDirectory && !retainTempDirectory) { - deleteTempDirectory(baseDirectory); + tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); + procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); } - } - return result; - } + if (vaultPass != null && !vaultPass.isEmpty()) { + //tempVaultFile = File.createTempFile("ansible-runner", "vault"); + //Files.write(tempVaultFile.toPath(), vaultPass.getBytes()); + //procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); + procArgs.add("--vault-id"); + procArgs.add("prompt"); + } - public int getResult() { - return result; - } + if (sshPrivateKey != null && !sshPrivateKey.isEmpty()) { + String privateKeyData = sshPrivateKey.replaceAll("\r\n", "\n"); + tempPkFile = AnsibleUtil.createTemporaryFile("id_rsa", privateKeyData); - public boolean registerKeySshAgent(String keyPath) throws AnsibleException, Exception { + // Only the owner can read and write + Set perms = new HashSet(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(tempPkFile.toPath(), perms); - if(sshAgent==null){ - sshAgent = new SSHAgentProcess(this.sshAgentTimeToLive); - } + if (sshUseAgent) { + registerKeySshAgent(tempPkFile.getAbsolutePath()); + } + procArgs.add("--private-key" + "=" + tempPkFile.toPath()); + } - List procArgs = new ArrayList<>(); - procArgs.add("/usr/bin/ssh-add"); - procArgs.add(keyPath); + if (sshUser != null && sshUser.length() > 0) { + procArgs.add("--user" + "=" + sshUser); + } - if (debug) { - System.out.println("ssh-agent socket " + sshAgent.getClass()); - System.out.println(" registerKeySshAgent: "+procArgs.toString()); - } + if (sshUsePassword) { + procArgs.add("--ask-pass"); + } - // execute the ssh-agent add process - ProcessBuilder processBuilder = new ProcessBuilder() - .command(procArgs) - .redirectErrorStream(true) - .directory(baseDirectory.toFile()); + if (sshTimeout != null && sshTimeout > 0) { + procArgs.add("--timeout" + "=" + sshTimeout); + } - Process proc = null; + if (become) { + procArgs.add("--become"); + if (becomePassword != null && !becomePassword.isEmpty()) { + procArgs.add("--ask-become-pass"); + } + } - Map env = processBuilder.environment(); - env.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); + if (becomeMethod != null && !becomeMethod.isEmpty()) { + procArgs.add("--become-method" + "=" + becomeMethod); + } - File tempPassVarsFile = null; - if (sshPassphrase != null && sshPassphrase.length() > 0) { - tempPassVarsFile = File.createTempFile("ansible-runner", "ssh-add-check"); - tempPassVarsFile.setExecutable(true); + if (becomeUser != null && !becomeUser.isEmpty()) { + procArgs.add("--become-user" + "=" + becomeUser); + } - List passScript = new ArrayList<>(); - passScript.add("read SECRET"); - passScript.add("echo $SECRET"); + // default the listener to stdout logger + if (listener == null) { + listener = ListenerFactory.getListener(System.out); + } - Files.write(tempPassVarsFile.toPath(),passScript); + if (extraParams != null && !extraParams.isEmpty()) { + procArgs.addAll(tokenizeCommand(extraParams)); + } - env.put("DISPLAY", "0"); - env.put("SSH_ASKPASS", tempPassVarsFile.getAbsolutePath()); - } + if (debug) { + System.out.println(" procArgs: " + procArgs); + } - try { - proc = processBuilder.start(); + ProcessExecutor.ProcessExecutorBuilder processExecutorBuilder = ProcessExecutor.builder() + .procArgs(procArgs); - OutputStream stdin = proc.getOutputStream(); - OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + if (baseDirectory != null) { + processExecutorBuilder.baseDirectory(baseDirectory.toFile()); + } + Process proc = null; - try{ - if (sshPassphrase != null && sshPassphrase.length() > 0) { - stdinw.write(sshPassphrase+"\n"); - stdinw.flush(); + //SET env variables + Map processEnvironment = new HashMap<>(); + + if (configFile != null && !configFile.isEmpty()) { + if (debug) { + System.out.println(" ANSIBLE_CONFIG: " + configFile); + } + + processEnvironment.put("ANSIBLE_CONFIG", configFile); } - } catch (Exception e) { - if (debug) { - System.out.println("not prompt enable"); - } - } - - stdinw.close(); - stdin.close(); - - Thread errthread = Logging.copyStreamThread(proc.getErrorStream(), ListenerFactory.getListener(System.err)); - Thread outthread = Logging.copyStreamThread(proc.getInputStream(), ListenerFactory.getListener(System.out)); - errthread.start(); - outthread.start(); - - int exitCode = proc.waitFor(); - - outthread.join(); - errthread.join(); - System.err.flush(); - System.out.flush(); - - if (exitCode != 0) { - throw new AnsibleException("ERROR: ssh-add returns with non zero code:" + procArgs.toString(), - AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } - - - } catch (IOException e) { - throw new AnsibleException("ERROR: error adding private key to ssh-agent." + procArgs.toString(), e, AnsibleException.AnsibleFailureReason.Unknown); - } catch (InterruptedException e) { - if(proc!=null) { - proc.destroy(); - } - Thread.currentThread().interrupt(); - throw new AnsibleException("ERROR: error adding private key to ssh-agent Interrupted.", e, AnsibleException.AnsibleFailureReason.Interrupted); - }finally { - // Make sure to always cleanup on failure and success - if(proc!=null) { - proc.destroy(); - } - - if(tempPassVarsFile!=null && !tempPassVarsFile.delete()){ - tempPassVarsFile.deleteOnExit(); - } - } - return true; - } +// for (String optionName : this.options.keySet()) { +// processEnvironment.put(optionName, this.options.get(optionName)); +// } + + if (sshUseAgent && sshAgent != null) { + processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); + } + + processExecutorBuilder.environmentVariables(processEnvironment); + //set STDIN variables + List stdinVariables = new ArrayList<>(); + if (sshUsePassword) { + if (sshPass != null && !sshPass.isEmpty()) { + stdinVariables.add(sshPass + "\n"); + } + } - public File createTemporaryFile(String suffix, String data, boolean encrypt) throws IOException { - File tempVarsFile = File.createTempFile("ansible-runner", suffix); - Files.write(tempVarsFile.toPath(), data.getBytes()); + if (become) { + if (becomePassword != null && !becomePassword.isEmpty()) { + stdinVariables.add(becomePassword + "\n"); + } + } - if(encrypt){ - encryptFileAnsibleVault(tempVarsFile); + if (encryptExtraVars) { + stdinVariables.add(generatedVaultPassword + "\n"); + } + + if (vaultPass != null && !vaultPass.isEmpty()) { + stdinVariables.add(vaultPass + "\n"); + } + + processExecutorBuilder.stdinVariables(stdinVariables); + + try { + proc = processExecutorBuilder.build().run(); + + Thread errthread = Logging.copyStreamThread(proc.getErrorStream(), listener); + Thread outthread = Logging.copyStreamThread(proc.getInputStream(), listener); + errthread.start(); + outthread.start(); + result = proc.waitFor(); + outthread.join(); + errthread.join(); + System.err.flush(); + System.out.flush(); + + if (sshUseAgent) { + if (sshAgent != null) { + sshAgent.stopAgent(); + } + } + + if (result != 0) { + if (!ignoreErrors) { + throw new AnsibleException("ERROR: Ansible execution returned with non zero code.", + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + } + } catch (InterruptedException e) { + if (proc != null) { + proc.destroy(); + } + Thread.currentThread().interrupt(); + throw new AnsibleException("ERROR: Ansible Execution Interrupted.", e, AnsibleException.AnsibleFailureReason.Interrupted); + } catch (IOException e) { + throw new AnsibleException("ERROR: Ansible IO failure: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.IOFailure); + } catch (AnsibleException e) { + throw e; + } catch (Exception e) { + if (proc != null) { + proc.destroy(); + } + throw new AnsibleException("ERROR: Ansible execution returned with non zero code.", e, AnsibleException.AnsibleFailureReason.Unknown); + } finally { + // Make sure to always cleanup on failure and success + if (proc != null) { + proc.getErrorStream().close(); + proc.getInputStream().close(); + proc.getOutputStream().close(); + proc.destroy(); + } + + if (tempFile != null && !tempFile.delete()) { + tempFile.deleteOnExit(); + } + if (tempPkFile != null && !tempPkFile.delete()) { + tempPkFile.deleteOnExit(); + } + if (tempVarsFile != null && !tempVarsFile.delete()) { + tempVarsFile.deleteOnExit(); + } + if (tempPlaybook != null && !tempPlaybook.delete()) { + tempPlaybook.deleteOnExit(); + } + + if (usingTempDirectory && !retainTempDirectory) { + deleteTempDirectory(baseDirectory); + } + } + + return result; } - return tempVarsFile; + public boolean registerKeySshAgent(String keyPath) throws Exception { - } + if (sshAgent == null) { + sshAgent = new SSHAgentProcess(this.sshAgentTimeToLive); + } - public void encryptFileAnsibleVault(File file){ + List procArgs = new ArrayList<>(); + procArgs.add("/usr/bin/ssh-add"); + procArgs.add(keyPath); - List procArgs = new ArrayList<>(); - procArgs.add("ansible-vault"); - procArgs.add("encrypt"); - procArgs.add(file.getAbsolutePath()); + if (debug) { + System.out.println("ssh-agent socket " + sshAgent.getClass()); + System.out.println(" registerKeySshAgent: " + procArgs); + } + + Map env = new HashMap<>(); + env.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); + + File tempPassVarsFile = null; + if (sshPassphrase != null && sshPassphrase.length() > 0) { + tempPassVarsFile = File.createTempFile("ansible-runner", "ssh-add-check"); + tempPassVarsFile.setExecutable(true); + + List passScript = new ArrayList<>(); + passScript.add("read SECRET"); + passScript.add("echo $SECRET"); + + Files.write(tempPassVarsFile.toPath(),passScript); - // execute the ssh-agent add process - ProcessBuilder processBuilder = new ProcessBuilder() - .command(procArgs) - .directory(baseDirectory.toFile()); - Process proc = null; + env.put("DISPLAY", "0"); + env.put("SSH_ASKPASS", tempPassVarsFile.getAbsolutePath()); + } - try { - proc = processBuilder.start(); + List stdinVariables = new ArrayList<>(); + if (sshPassphrase != null && !sshPassphrase.isEmpty()) { + stdinVariables.add(sshPassphrase + "\n"); + } - OutputStream stdin = proc.getOutputStream(); - OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + ProcessExecutor processExecutor = ProcessExecutor.builder() + .procArgs(procArgs) + .baseDirectory(baseDirectory.toFile()) + .environmentVariables(env) + .stdinVariables(stdinVariables) + .build(); + + Process proc = null; + + try { + proc = processExecutor.run(); + + Thread errthread = Logging.copyStreamThread(proc.getErrorStream(), ListenerFactory.getListener(System.err)); + Thread outthread = Logging.copyStreamThread(proc.getInputStream(), ListenerFactory.getListener(System.out)); + errthread.start(); + outthread.start(); + + int exitCode = proc.waitFor(); + + outthread.join(); + errthread.join(); + System.err.flush(); + System.out.flush(); + + if (exitCode != 0) { + throw new AnsibleException("ERROR: ssh-add returns with non zero code:" + procArgs, + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + + } catch (IOException e) { + throw new AnsibleException("ERROR: error adding private key to ssh-agent." + procArgs, e, AnsibleException.AnsibleFailureReason.Unknown); + } catch (InterruptedException e) { + if (proc != null) { + proc.destroy(); + } + Thread.currentThread().interrupt(); + throw new AnsibleException("ERROR: error adding private key to ssh-agen Interrupted.", e, AnsibleException.AnsibleFailureReason.Interrupted); + } finally { + // Make sure to always cleanup on failure and success + if (proc != null) { + proc.destroy(); + } + if(tempPassVarsFile!=null && !tempPassVarsFile.delete()){ + tempPassVarsFile.deleteOnExit(); + } + } - try { - stdinw.write(generatedVaultPassword + "\n"); - stdinw.write(generatedVaultPassword + "\n"); - stdinw.flush(); - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - } + return true; + } + + protected String encryptVariable(String content) throws IOException { + + List procArgs = new ArrayList<>(); + procArgs.add("ansible-vault"); + procArgs.add("encrypt_string"); + procArgs.add("--vault-id"); + procArgs.add("interval-encypt@prompt"); + + //send values to STDIN in order + List stdinVariables = new ArrayList<>(); + stdinVariables.add(generatedVaultPassword + "\n"); + stdinVariables.add(generatedVaultPassword + "\n"); + stdinVariables.add(content); + + Process proc = null; + + try { + proc = ProcessExecutor.builder().procArgs(procArgs) + .baseDirectory(baseDirectory.toFile()) + .stdinVariables(stdinVariables) + //.redirectErrorStream(true) + .build().run(); + + StringBuilder stringBuilder = new StringBuilder(); + + final InputStream stdoutInputStream = proc.getInputStream(); + final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + + String line1 = null; + boolean capture = false; + while ((line1 = stdoutReader.readLine()) != null) { + if (line1.toLowerCase().contains("!vault")) { + capture = true; + } + if (capture) { + stringBuilder.append(line1).append("\n"); + } + } + + int exitCode = proc.waitFor(); + + if (exitCode != 0) { + System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); + return null; + } + return stringBuilder.toString(); + + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + return null; + } finally { + // Make sure to always cleanup on failure and success + if (proc != null) { + proc.destroy(); + } + } + } - int exitCode = proc.waitFor(); - if (exitCode != 0) { - throw new AnsibleException("ERROR: encryptFileAnsibleVault:" + procArgs.toString(), - AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } + public String encryptExtraVarsKey(String extraVars) throws Exception { + Map extraVarsMap = null; + Map encryptedExtraVarsMap = new HashMap<>(); + try { + extraVarsMap = mapperYaml.readValue(extraVars, new TypeReference>() { + }); + } catch (Exception e) { + try { + extraVarsMap = mapperJson.readValue(extraVars, new TypeReference>() { + }); + } catch (Exception e2) { + System.err.println("error encryptExtraVars " + e2.getMessage()); + return null; + } + } + if (extraVarsMap == null || extraVarsMap.isEmpty()) { + System.err.println("error encryptExtraVars, the given vars cannot be mapped to a valid map."); + return null; + } - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - }finally { - // Make sure to always cleanup on failure and success - if(proc!=null) { - proc.destroy(); + extraVarsMap.forEach((key, value) -> { + try { + String encryptedKey = encryptVariable(value); + if (encryptedKey != null) { + encryptedExtraVarsMap.put(key, encryptedKey); + } + } catch (IOException e) { + System.out.println(e.getMessage()); + } + }); + + StringBuilder stringBuilder = new StringBuilder(); + encryptedExtraVarsMap.forEach((key, value) -> { + stringBuilder.append(key).append(":"); + stringBuilder.append(" ").append(value).append("\n"); + }); + + //return mapperYaml.writeValueAsString(encryptedExtraVarsMap); + return stringBuilder.toString(); + } + + public void encryptFileAnsibleVault(File file) { + + List procArgs = new ArrayList<>(); + procArgs.add("ansible-vault"); + procArgs.add("encrypt"); + procArgs.add(file.getAbsolutePath()); + + // execute the ssh-agent add process + ProcessBuilder processBuilder = new ProcessBuilder() + .command(procArgs) + .directory(baseDirectory.toFile()); + Process proc = null; + + try { + proc = processBuilder.start(); + + OutputStream stdin = proc.getOutputStream(); + OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + + try { + stdinw.write(generatedVaultPassword + "\n"); + stdinw.write(generatedVaultPassword + "\n"); + stdinw.flush(); + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + } + + int exitCode = proc.waitFor(); + + if (exitCode != 0) { + throw new AnsibleException("ERROR: encryptFileAnsibleVault:" + procArgs, + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + + + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + } finally { + // Make sure to always cleanup on failure and success + if (proc != null) { + proc.destroy(); + } } } - } -} +} \ No newline at end of file diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java similarity index 62% rename from src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java rename to src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java index 627d2e35..89db759f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java @@ -21,7 +21,9 @@ import java.nio.file.Files; import java.nio.file.Paths; +import lombok.Getter; import org.rundeck.storage.api.Path; + import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -30,17 +32,18 @@ import org.rundeck.storage.api.PathUtil; import org.rundeck.storage.api.StorageException; -public class AnsibleRunnerBuilder { +@Getter +public class AnsibleRunnerContextBuilder { - private ExecutionContext context; - private Framework framework; - private String frameworkProject; - private Map jobConf; - private Collection nodes; - private Collection tempFiles; + private final ExecutionContext context; + private final Framework framework; + private final String frameworkProject; + private final Map jobConf; + private final Collection nodes; + private final Collection tempFiles; - public AnsibleRunnerBuilder(final ExecutionContext context, final Framework framework, INodeSet nodes, final Map configuration) { + public AnsibleRunnerContextBuilder(final ExecutionContext context, final Framework framework, INodeSet nodes, final Map configuration) { this.context = context; this.framework = framework; this.frameworkProject = context.getFrameworkProject(); @@ -49,7 +52,7 @@ public AnsibleRunnerBuilder(final ExecutionContext context, final Framework fram this.tempFiles = new LinkedList<>(); } - public AnsibleRunnerBuilder(final INodeEntry node,final ExecutionContext context, final Framework framework, final Map configuration) { + public AnsibleRunnerContextBuilder(final INodeEntry node, final ExecutionContext context, final Framework framework, final Map configuration) { this.context = context; this.framework = framework; this.frameworkProject = context.getFrameworkProject(); @@ -75,8 +78,8 @@ public String getPrivateKeyfilePath() { getFrameworkProject(), getFramework(), getNode(), - getjobConf() - ); + getJobConf() + ); //expand properties in path if (path != null && path.contains("${")) { @@ -87,13 +90,13 @@ public String getPrivateKeyfilePath() { public String getPrivateKeyStoragePath() { String path = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH, + AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH, null, getFrameworkProject(), getFramework(), getNode(), - getjobConf() - ); + getJobConf() + ); //expand properties in path if (path != null && path.contains("${")) { path = DataContextUtils.replaceDataReferencesInString(path, context.getDataContext()); @@ -101,7 +104,7 @@ public String getPrivateKeyStoragePath() { return path; } - public byte[] getPrivateKeyStorageDataBytes() throws IOException { + public byte[] getPrivateKeyStorageDataBytes() throws IOException { String privateKeyResourcePath = getPrivateKeyStoragePath(); return this.loadStoragePathData(privateKeyResourcePath); } @@ -109,26 +112,26 @@ public byte[] getPrivateKeyStorageDataBytes() throws IOException { public String getPasswordStoragePath() { String path = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH, + AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH, null, getFrameworkProject(), getFramework(), getNode(), - getjobConf() - ); + getJobConf() + ); //expand properties in path if (path != null && path.contains("${")) { path = DataContextUtils.replaceDataReferencesInString(path, context.getDataContext()); } - return path; + return path; } - public String getSshPrivateKey() throws ConfigurationException{ + public String getSshPrivateKey() throws ConfigurationException { //look for storage option String storagePath = getPrivateKeyStoragePath(); - if(null!=storagePath){ + if (null != storagePath) { Path path = PathUtil.asPath(storagePath); try { ResourceMeta contents = context.getStorageTree().getResource(path) @@ -148,7 +151,7 @@ public String getSshPrivateKey() throws ConfigurationException{ return new String(Files.readAllBytes(Paths.get(path))); } catch (IOException e) { throw new ConfigurationException("Failed to read the ssh private key from path " + - path + ": " + e.getMessage()); + path + ": " + e.getMessage()); } } else { return null; @@ -156,28 +159,28 @@ public String getSshPrivateKey() throws ConfigurationException{ } } - public String getSshPassword() throws ConfigurationException{ + public String getSshPassword() throws ConfigurationException { //look for option values first //typically jobs use secure options to dynamically setup the ssh password final String passwordOption = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_PASSWORD_OPTION, - AnsibleDescribable.DEFAULT_ANSIBLE_SSH_PASSWORD_OPTION, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_SSH_PASSWORD_OPTION, + AnsibleDescribable.DEFAULT_ANSIBLE_SSH_PASSWORD_OPTION, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); String sshPassword = PropertyResolver.evaluateSecureOption(passwordOption, getContext()); - if(null!=sshPassword){ + if (null != sshPassword) { // is true if there is an ssh option defined in the private data context return sshPassword; } else { //look for storage option String storagePath = getPasswordStoragePath(); - if(null!=storagePath){ + if (null != storagePath) { //look up storage value Path path = PathUtil.asPath(storagePath); try { @@ -198,21 +201,21 @@ public String getSshPassword() throws ConfigurationException{ } public Integer getSSHTimeout() throws ConfigurationException { - Integer timeout = null; + Integer timeout = null; final String stimeout = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_TIMEOUT, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_SSH_TIMEOUT, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != stimeout) { try { - timeout = Integer.parseInt(stimeout); + timeout = Integer.parseInt(stimeout); } catch (NumberFormatException e) { throw new ConfigurationException("Can't parse timeout value" + - timeout + ": " + e.getMessage()); + timeout + ": " + e.getMessage()); } } return timeout; @@ -221,13 +224,13 @@ public Integer getSSHTimeout() throws ConfigurationException { public String getSshUser() { final String user; user = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_USER, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_SSH_USER, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != user && user.contains("${")) { return DataContextUtils.replaceDataReferencesInString(user, getContext().getDataContext()); @@ -238,16 +241,16 @@ public String getSshUser() { public AuthenticationType getSshAuthenticationType() { String authType = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_SSH_AUTH_TYPE, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_SSH_AUTH_TYPE, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != authType) { - return AuthenticationType.valueOf(authType); + return AuthenticationType.valueOf(authType); } return AuthenticationType.privateKey; } @@ -255,12 +258,12 @@ public AuthenticationType getSshAuthenticationType() { public String getBecomeUser() { final String user; user = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME_USER, - null, - getFrameworkProject(), - getFramework(),getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_BECOME_USER, + null, + getFrameworkProject(), + getFramework(), getNode(), + getJobConf() + ); if (null != user && user.contains("${")) { return DataContextUtils.replaceDataReferencesInString(user, getContext().getDataContext()); @@ -271,66 +274,66 @@ public String getBecomeUser() { public Boolean getBecome() { Boolean become = null; String sbecome = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_BECOME, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != sbecome) { - become = Boolean.parseBoolean(sbecome); + become = Boolean.parseBoolean(sbecome); } return become; } public String getExtraParams() { - final String extraParams; - extraParams = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_EXTRA_PARAM, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); - - if (null != extraParams && extraParams.contains("${")) { - return DataContextUtils.replaceDataReferencesInString(extraParams, getContext().getDataContext()); - } - return extraParams; + final String extraParams; + extraParams = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_EXTRA_PARAM, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); + + if (null != extraParams && extraParams.contains("${")) { + return DataContextUtils.replaceDataReferencesInString(extraParams, getContext().getDataContext()); + } + return extraParams; } public BecomeMethodType getBecomeMethod() { String becomeMethod = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME_METHOD, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_BECOME_METHOD, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != becomeMethod) { - return BecomeMethodType.valueOf(becomeMethod); + return BecomeMethodType.valueOf(becomeMethod); } return null; } - public byte[] getPasswordStorageData() throws IOException{ + public byte[] getPasswordStorageData() throws IOException { return loadStoragePathData(getPasswordStoragePath()); } public String getBecomePasswordStoragePath() { String path = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH, + AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH, null, getFrameworkProject(), getFramework(), getNode(), - getjobConf() - ); + getJobConf() + ); //expand properties in path if (path != null && path.contains("${")) { path = DataContextUtils.replaceDataReferencesInString(path, context.getDataContext()); @@ -338,39 +341,39 @@ public String getBecomePasswordStoragePath() { return path; } - public byte[] getBecomePasswordStorageData() throws IOException{ + public byte[] getBecomePasswordStorageData() throws IOException { return loadStoragePathData(getBecomePasswordStoragePath()); } - public String getBecomePassword() throws ConfigurationException{ + public String getBecomePassword() throws ConfigurationException { //look for option values first //typically jobs use secure options to dynamically setup the become password String passwordOption = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_OPTION, - AnsibleDescribable.DEFAULT_ANSIBLE_BECOME_PASSWORD_OPTION, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_OPTION, + AnsibleDescribable.DEFAULT_ANSIBLE_BECOME_PASSWORD_OPTION, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); String becomePassword = PropertyResolver.evaluateSecureOption(passwordOption, getContext()); - if(null!=becomePassword){ + if (null != becomePassword) { // is true if there is a become option defined in the private data context return becomePassword; } else { //look for storage option String storagePath = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); - if(null!=storagePath){ + if (null != storagePath) { //look up storage value if (storagePath.contains("${")) { storagePath = DataContextUtils.replaceDataReferencesInString( @@ -404,7 +407,7 @@ public String getVaultKeyStoragePath(){ getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); if(null!=storagePath) { @@ -443,13 +446,13 @@ public String getVaultKey() throws ConfigurationException{ } else { String path = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_VAULT_PATH, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_VAULT_PATH, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); //expand properties in path if (path != null && path.contains("${")) { @@ -457,22 +460,22 @@ public String getVaultKey() throws ConfigurationException{ } if (path != null) { - try { - return new String(Files.readAllBytes(Paths.get(path))); - } catch (IOException e) { - throw new ConfigurationException("Failed to read the ssh private key from path " + - path + ": " + e.getMessage()); - } + try { + return new String(Files.readAllBytes(Paths.get(path))); + } catch (IOException e) { + throw new ConfigurationException("Failed to read the ssh private key from path " + + path + ": " + e.getMessage()); + } } else { - return null; + return null; } } } public String getPlaybookPath() { String playbook = null; - if ( getjobConf().containsKey(AnsibleDescribable.ANSIBLE_PLAYBOOK_PATH) ) { - playbook = (String) jobConf.get(AnsibleDescribable.ANSIBLE_PLAYBOOK_PATH); + if (getJobConf().containsKey(AnsibleDescribable.ANSIBLE_PLAYBOOK_PATH)) { + playbook = (String) jobConf.get(AnsibleDescribable.ANSIBLE_PLAYBOOK_PATH); } if (null != playbook && playbook.contains("${")) { @@ -482,21 +485,21 @@ public String getPlaybookPath() { } public String getPlaybookInline() { - String playbook = null; - if ( getjobConf().containsKey(AnsibleDescribable.ANSIBLE_PLAYBOOK_INLINE) ) { - playbook = (String) jobConf.get(AnsibleDescribable.ANSIBLE_PLAYBOOK_INLINE); - } + String playbook = null; + if (getJobConf().containsKey(AnsibleDescribable.ANSIBLE_PLAYBOOK_INLINE)) { + playbook = (String) jobConf.get(AnsibleDescribable.ANSIBLE_PLAYBOOK_INLINE); + } - if (null != playbook && playbook.contains("${")) { - return DataContextUtils.replaceDataReferencesInString(playbook, getContext().getDataContext()); - } - return playbook; + if (null != playbook && playbook.contains("${")) { + return DataContextUtils.replaceDataReferencesInString(playbook, getContext().getDataContext()); + } + return playbook; } public String getModule() { String module = null; - if ( getjobConf().containsKey(AnsibleDescribable.ANSIBLE_MODULE) ) { - module = (String) jobConf.get(AnsibleDescribable.ANSIBLE_MODULE); + if (getJobConf().containsKey(AnsibleDescribable.ANSIBLE_MODULE)) { + module = (String) jobConf.get(AnsibleDescribable.ANSIBLE_MODULE); } if (null != module && module.contains("${")) { @@ -507,8 +510,8 @@ public String getModule() { public String getModuleArgs() { String args = null; - if ( getjobConf().containsKey(AnsibleDescribable.ANSIBLE_MODULE_ARGS) ) { - args = (String) jobConf.get(AnsibleDescribable.ANSIBLE_MODULE_ARGS); + if (getJobConf().containsKey(AnsibleDescribable.ANSIBLE_MODULE_ARGS)) { + args = (String) jobConf.get(AnsibleDescribable.ANSIBLE_MODULE_ARGS); } if (null != args && args.contains("${")) { @@ -520,13 +523,13 @@ public String getModuleArgs() { public String getExecutable() { final String executable; executable = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_EXECUTABLE, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_EXECUTABLE, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != executable && executable.contains("${")) { return DataContextUtils.replaceDataReferencesInString(executable, getContext().getDataContext()); @@ -537,13 +540,13 @@ public String getExecutable() { public Boolean getDebug() { Boolean debug = Boolean.FALSE; String sdebug = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_DEBUG, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_DEBUG, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != sdebug) { debug = Boolean.parseBoolean(sdebug); @@ -554,13 +557,13 @@ public Boolean getDebug() { public String getExtraVars() { final String extraVars; extraVars = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_EXTRA_VARS, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_EXTRA_VARS, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != extraVars && extraVars.contains("${")) { return DataContextUtils.replaceDataReferencesInString(extraVars, getContext().getDataContext()); @@ -571,16 +574,16 @@ public String getExtraVars() { public Boolean generateInventory() { Boolean generateInventory = null; String sgenerateInventory = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_GENERATE_INVENTORY, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_GENERATE_INVENTORY, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != sgenerateInventory) { - generateInventory = Boolean.parseBoolean(sgenerateInventory); + generateInventory = Boolean.parseBoolean(sgenerateInventory); } return generateInventory; } @@ -588,10 +591,10 @@ public Boolean generateInventory() { public String getInventory() throws ConfigurationException { String inventory; String inline_inventory; - Boolean isGenerated = generateInventory(); + Boolean isGenerated = generateInventory(); - if (isGenerated !=null && isGenerated) { + if (isGenerated != null && isGenerated) { File tempInventory = new AnsibleInventoryBuilder(this.nodes).buildInventory(); tempFiles.add(tempInventory); inventory = tempInventory.getAbsolutePath(); @@ -603,7 +606,7 @@ public String getInventory() throws ConfigurationException { getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); if (inline_inventory != null) { @@ -624,7 +627,7 @@ public String getInventory() throws ConfigurationException { getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); if (null != inventory && inventory.contains("${")) { @@ -638,26 +641,26 @@ public String getLimit() throws ConfigurationException { final String limit; // Return Null if Disabled - if(PropertyResolver.resolveBooleanProperty( - AnsibleDescribable.ANSIBLE_DISABLE_LIMIT, - Boolean.valueOf(AnsibleDescribable.DISABLE_LIMIT_PROP.getDefaultValue()), - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf())){ + if (PropertyResolver.resolveBooleanProperty( + AnsibleDescribable.ANSIBLE_DISABLE_LIMIT, + Boolean.valueOf(AnsibleDescribable.DISABLE_LIMIT_PROP.getDefaultValue()), + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf())) { - return null; + return null; } // Get Limit from Rundeck limit = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_LIMIT, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + AnsibleDescribable.ANSIBLE_LIMIT, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != limit && limit.contains("${")) { return DataContextUtils.replaceDataReferencesInString(limit, getContext().getDataContext()); @@ -674,7 +677,7 @@ public String getConfigFile() { getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); if (null != configFile && configFile.contains("${")) { @@ -685,8 +688,8 @@ public String getConfigFile() { public String getBaseDir() { String baseDir = null; - if ( getjobConf().containsKey(AnsibleDescribable.ANSIBLE_BASE_DIR_PATH) ) { - baseDir = (String) jobConf.get(AnsibleDescribable.ANSIBLE_BASE_DIR_PATH); + if (getJobConf().containsKey(AnsibleDescribable.ANSIBLE_BASE_DIR_PATH)) { + baseDir = (String) jobConf.get(AnsibleDescribable.ANSIBLE_BASE_DIR_PATH); } if (null != baseDir && baseDir.contains("${")) { @@ -697,14 +700,14 @@ public String getBaseDir() { public String getBinariesFilePath() { String binariesFilePathStr; - binariesFilePathStr = PropertyResolver.resolveProperty( - AnsibleDescribable.ANSIBLE_BINARIES_DIR_PATH, - null, - getFrameworkProject(), - getFramework(), - getNode(), - getjobConf() - ); + binariesFilePathStr = PropertyResolver.resolveProperty( + AnsibleDescribable.ANSIBLE_BINARIES_DIR_PATH, + null, + getFrameworkProject(), + getFramework(), + getNode(), + getJobConf() + ); if (null != binariesFilePathStr) { if (binariesFilePathStr.contains("${")) { return DataContextUtils.replaceDataReferencesInString(binariesFilePathStr, getContext().getDataContext()); @@ -713,163 +716,10 @@ public String getBinariesFilePath() { return binariesFilePathStr; } - public AnsibleRunner buildAnsibleRunner() throws ConfigurationException{ - - AnsibleRunner runner = null; - - String playbook; - String module; - - if ((playbook = getPlaybookPath()) != null) { - runner = AnsibleRunner.playbookPath(playbook); - } else if ((playbook = getPlaybookInline()) != null) { - runner = AnsibleRunner.playbookInline(playbook); - } else if ((module = getModule()) != null) { - runner = AnsibleRunner.adHoc(module, getModuleArgs()); - } else { - throw new ConfigurationException("Missing module or playbook job arguments"); - } - - final AuthenticationType authType = getSshAuthenticationType(); - if (AuthenticationType.privateKey == authType) { - final String privateKey = getSshPrivateKey(); - if (privateKey != null) { - runner = runner.sshPrivateKey(privateKey); - } - - if(getUseSshAgent()){ - runner.sshUseAgent(true); - - String passphraseOption = getPassphrase(); - runner.sshPassphrase(passphraseOption); - } - - - - } else if (AuthenticationType.password == authType) { - final String password = getSshPassword(); - if (password != null) { - runner = runner.sshUsePassword(Boolean.TRUE).sshPass(password); - } - } - - // set rundeck options as environment variables - Map options = context.getDataContext().get("option"); - if (options != null) { - runner = runner.options(options); - } - - String inventory = getInventory(); - if (inventory != null) { - runner = runner.setInventory(inventory); - } - - String limit = getLimit(); - if (limit != null) { - runner = runner.limit(limit); - } - - Boolean debug = getDebug(); - if (debug != null) { - if (debug == Boolean.TRUE) { - runner = runner.debug(Boolean.TRUE); - } else { - runner = runner.debug(Boolean.FALSE); - } - } - - String extraParams = getExtraParams(); - if (extraParams != null) { - runner = runner.extraParams(extraParams); - } - - String extraVars = getExtraVars(); - if (extraVars != null) { - runner = runner.extraVars(extraVars); - } - - String user = getSshUser(); - if (user != null) { - runner = runner.sshUser(user); - } - - String vault = getVaultKey(); - if (vault != null) { - runner = runner.vaultPass(vault); - } - - Integer timeout = getSSHTimeout(); - if (timeout != null) { - runner = runner.sshTimeout(timeout); - } - - Boolean become = getBecome(); - if (become != null) { - runner = runner.become(become); - } - - String become_user = getBecomeUser(); - if (become_user != null) { - runner = runner.becomeUser(become_user); - } - - BecomeMethodType become_method = getBecomeMethod(); - if (become_method != null) { - runner = runner.becomeMethod(become_method.name()); - } - - String become_password = getBecomePassword(); - if (become_password != null) { - runner = runner.becomePassword(become_password); - } - - String executable = getExecutable(); - if (executable != null) { - runner = runner.executable(executable); - } - - String configFile = getConfigFile(); - if (configFile != null) { - runner = runner.configFile(configFile); - } - - String baseDir = getBaseDir(); - if (baseDir != null) { - runner = runner.baseDirectory(baseDir); - } - - String binariesFilePath = getBinariesFilePath(); - if (binariesFilePath != null) { - runner = runner.ansibleBinariesDirectory(binariesFilePath); - } - - boolean encryptTempFiles = encryptTempFiles(); - if(encryptTempFiles){ - runner = runner.encryptTemporaryFiles(true); - } - - return runner; - } - - public ExecutionContext getContext() { - return context; - } - - public Framework getFramework() { - return framework; - } - public INodeEntry getNode() { return nodes.size() == 1 ? nodes.iterator().next() : null; } - public String getFrameworkProject() { - return frameworkProject; - } - - public Map getjobConf() { - return jobConf; - } public void cleanupTempFiles() { for (File temp : tempFiles) { @@ -888,7 +738,7 @@ public Boolean getUseSshAgent() { getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); if (null != sAgent) { @@ -906,19 +756,19 @@ String getPassphrase() throws ConfigurationException { getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); String sshPassword = PropertyResolver.evaluateSecureOption(passphraseOption, getContext()); - if(null!=sshPassword){ + if (null != sshPassword) { // is true if there is an ssh option defined in the private data context return sshPassword; - }else{ + } else { return getPassphraseStorageData(getPassphraseStoragePath()); } } - public String getPassphraseStoragePath(){ + public String getPassphraseStoragePath() { String storagePath = PropertyResolver.resolveProperty( AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE, @@ -926,10 +776,10 @@ public String getPassphraseStoragePath(){ getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); - if(null!=storagePath) { + if (null != storagePath) { //expand properties in path if (storagePath.contains("${")) { storagePath = DataContextUtils.replaceDataReferencesInString(storagePath, context.getDataContext()); @@ -941,9 +791,9 @@ public String getPassphraseStoragePath(){ return null; } - + public String getPassphraseStorageData(String storagePath) throws ConfigurationException { - if(storagePath == null){ + if (storagePath == null) { return null; } @@ -960,14 +810,14 @@ public String getPassphraseStorageData(String storagePath) throws ConfigurationE } } - public boolean encryptTempFiles() throws ConfigurationException { + public boolean encryptExtraVars() throws ConfigurationException { return PropertyResolver.resolveBooleanProperty( - AnsibleDescribable.ENCRYPT_TEMP_FILES, + AnsibleDescribable.ANSIBLE_ENCRYPT_EXTRA_VARS, false, getFrameworkProject(), getFramework(), getNode(), - getjobConf() + getJobConf() ); } } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java index b3455b39..df1470da 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java @@ -1,14 +1,15 @@ package com.rundeck.plugins.ansible.plugin; +import com.dtolabs.rundeck.core.execution.impl.common.DefaultFileCopierUtil; +import com.dtolabs.rundeck.core.execution.impl.common.FileCopierUtil; import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException.AnsibleFailureReason; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.common.INodeEntry; import com.dtolabs.rundeck.core.common.IRundeckProject; import com.dtolabs.rundeck.core.execution.ExecutionContext; -import com.dtolabs.rundeck.core.execution.impl.jsch.JschScpFileCopier; import com.dtolabs.rundeck.core.execution.service.FileCopier; import com.dtolabs.rundeck.core.execution.service.FileCopierException; import com.dtolabs.rundeck.core.plugins.Plugin; @@ -30,7 +31,7 @@ public class AnsibleFileCopier implements FileCopier, AnsibleDescribable, ProxyR public static final String SERVICE_PROVIDER_NAME = "com.batix.rundeck.plugins.AnsibleFileCopier"; public static Description DESC = null; - + private static FileCopierUtil util = new DefaultFileCopierUtil(); static { DescriptionBuilder builder = DescriptionBuilder.builder(); builder.name(SERVICE_PROVIDER_NAME); @@ -54,7 +55,6 @@ public class AnsibleFileCopier implements FileCopier, AnsibleDescribable, ProxyR builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); builder.mapping(ANSIBLE_CONFIG_FILE_PATH,PROJ_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); builder.frameworkMapping(ANSIBLE_CONFIG_FILE_PATH,FWK_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); @@ -68,8 +68,6 @@ public class AnsibleFileCopier implements FileCopier, AnsibleDescribable, ProxyR builder.frameworkMapping(ANSIBLE_SSH_PASSPHRASE_OPTION,FWK_PROP_PREFIX + ANSIBLE_SSH_PASSPHRASE_OPTION); builder.mapping(ANSIBLE_SSH_USE_AGENT,PROJ_PROP_PREFIX + ANSIBLE_SSH_USE_AGENT); builder.frameworkMapping(ANSIBLE_SSH_USE_AGENT,FWK_PROP_PREFIX + ANSIBLE_SSH_USE_AGENT); - builder.mapping(ENCRYPT_TEMP_FILES,PROJ_PROP_PREFIX + ENCRYPT_TEMP_FILES); - builder.frameworkMapping(ENCRYPT_TEMP_FILES,FWK_PROP_PREFIX + ENCRYPT_TEMP_FILES); DESC=builder.build(); } @@ -115,7 +113,7 @@ private String doFileCopy( String identity = (context.getDataContext() != null && context.getDataContext().get("job") != null) ? context.getDataContext().get("job").get("execid") : null; - destinationPath = JschScpFileCopier.generateRemoteFilepathForNode( + destinationPath = util.generateRemoteFilepathForNode( node, project, context.getFramework(), @@ -126,7 +124,7 @@ private String doFileCopy( } File localTempFile = scriptFile != null ? - scriptFile : JschScpFileCopier.writeTempFile(context, null, input, script); + scriptFile : util.writeTempFile(context, null, input, script); String cmdArgs = "src='" + localTempFile.getAbsolutePath() + "' dest='" + destinationPath + "'"; @@ -147,11 +145,10 @@ private String doFileCopy( jobConf.put(AnsibleDescribable.ANSIBLE_DEBUG,"False"); } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), jobConf); - + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), jobConf); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new FileCopierException("Error configuring Ansible.",AnsibleFailureReason.ParseArgumentsError, e); } @@ -162,7 +159,7 @@ private String doFileCopy( throw new FileCopierException("Error running Ansible.", AnsibleFailureReason.AnsibleError, e); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); return destinationPath; } @@ -176,7 +173,7 @@ public Description getDescription() { public List listSecretsPath(ExecutionContext context, INodeEntry node) { Map jobConf = new HashMap<>(); jobConf.put(AnsibleDescribable.ANSIBLE_LIMIT,node.getNodename()); - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), jobConf); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), jobConf); return AnsibleUtil.getSecretsPath(builder); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java index 25606dd9..4a286985 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java @@ -6,7 +6,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.execution.workflow.steps.StepException; import com.dtolabs.rundeck.core.plugins.Plugin; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; @@ -78,10 +78,10 @@ public void executeStep(PluginStepContext context, Map configura configuration.put(AnsibleDescribable.ANSIBLE_DEBUG, "False"); } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new StepException("Error configuring Ansible runner: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.ParseArgumentsError); } @@ -95,7 +95,7 @@ public void executeStep(PluginStepContext context, Map configura throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); } @Override @@ -105,13 +105,13 @@ public Description getDescription() { @Override public SecretBundle prepareSecretBundleWorkflowStep(ExecutionContext context, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context, context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(context, context.getFramework(), context.getNodes(), configuration); return AnsibleUtil.createBundle(builder); } @Override public List listSecretsPathWorkflowStep(ExecutionContext context, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context, context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(context, context.getFramework(), context.getNodes(), configuration); return AnsibleUtil.getSecretsPath(builder); } } 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 81516e9c..07c16473 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java @@ -2,11 +2,8 @@ import com.dtolabs.rundeck.core.execution.proxy.ProxyRunnerPlugin; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; -import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; -import com.rundeck.plugins.ansible.ansible.AnsibleException; -import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; -import com.rundeck.plugins.ansible.ansible.PropertyResolver; +import com.rundeck.plugins.ansible.ansible.*; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.common.INodeEntry; import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.service.NodeExecutor; @@ -54,7 +51,6 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); builder.mapping(ANSIBLE_BINARIES_DIR_PATH,PROJ_PROP_PREFIX + ANSIBLE_BINARIES_DIR_PATH); @@ -98,9 +94,6 @@ public class AnsibleNodeExecutor implements NodeExecutor, AnsibleDescribable, Pr builder.mapping(ANSIBLE_VAULTSTORE_PATH,PROJ_PROP_PREFIX + ANSIBLE_VAULTSTORE_PATH); builder.frameworkMapping(ANSIBLE_VAULTSTORE_PATH,FWK_PROP_PREFIX + ANSIBLE_VAULTSTORE_PATH); - builder.mapping(ENCRYPT_TEMP_FILES,PROJ_PROP_PREFIX + ENCRYPT_TEMP_FILES); - builder.frameworkMapping(ENCRYPT_TEMP_FILES,FWK_PROP_PREFIX + ENCRYPT_TEMP_FILES); - DESC=builder.build(); } @@ -173,10 +166,10 @@ public NodeExecutorResult executeCommand(ExecutionContext context, String[] comm } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), jobConf); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), jobConf); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { return NodeExecutorResultImpl.createFailure(AnsibleException.AnsibleFailureReason.ParseArgumentsError, e.getMessage(), node); } @@ -187,7 +180,7 @@ public NodeExecutorResult executeCommand(ExecutionContext context, String[] comm return NodeExecutorResultImpl.createFailure(AnsibleException.AnsibleFailureReason.AnsibleError, e.getMessage(), node); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); return NodeExecutorResultImpl.createSuccess(node); } @@ -201,7 +194,7 @@ public Description getDescription() { public List listSecretsPath(ExecutionContext context, INodeEntry node) { Map jobConf = new HashMap<>(); jobConf.put(AnsibleDescribable.ANSIBLE_LIMIT,node.getNodename()); - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), jobConf); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), jobConf); return AnsibleUtil.getSecretsPath(builder); } 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 d9675ff2..00eb659e 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java @@ -5,7 +5,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.common.INodeEntry; import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException; import com.dtolabs.rundeck.core.plugins.Plugin; @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Plugin(name = AnsiblePlaybookInlineWorkflowNodeStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowNodeStep) public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin { @@ -39,6 +38,7 @@ public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, An builder.property(BASE_DIR_PROP); builder.property(PLAYBOOK_INLINE_PROP); builder.property(EXTRA_VARS_PROP); + builder.property(CONFIG_ENCRYPT_EXTRA_VARS); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); builder.property(EXTRA_ATTRS_PROP); @@ -55,7 +55,6 @@ public class AnsiblePlaybookInlineWorkflowNodeStep implements NodeStepPlugin, An builder.property(BECOME_AUTH_TYPE_PROP); builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC=builder.build(); } @@ -82,10 +81,10 @@ public void executeNodeStep( configuration.put(AnsibleDescribable.ANSIBLE_DEBUG,"False"); } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new NodeStepException("Error configuring Ansible runner: "+e.getMessage(), AnsibleException.AnsibleFailureReason.ParseArgumentsError,e.getMessage()); } @@ -105,13 +104,13 @@ public void executeNodeStep( throw new NodeStepException(e.getMessage(),e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData, e.getMessage()); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); } @Override public List listSecretsPathWorkflowNodeStep(ExecutionContext context, INodeEntry node, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), configuration); return AnsibleUtil.getSecretsPath(builder); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java index b4138515..da1e5ba9 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java @@ -5,7 +5,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.execution.workflow.steps.StepException; import com.dtolabs.rundeck.core.plugins.Plugin; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; @@ -19,7 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Objects; @Plugin(name = AnsiblePlaybookInlineWorkflowStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowStep) public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin { @@ -38,6 +38,7 @@ public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDes builder.property(BASE_DIR_PROP); builder.property(PLAYBOOK_INLINE_PROP); builder.property(EXTRA_VARS_PROP); + builder.property(CONFIG_ENCRYPT_EXTRA_VARS); builder.property(INVENTORY_INLINE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); @@ -56,7 +57,6 @@ public class AnsiblePlaybookInlineWorkflowStep implements StepPlugin, AnsibleDes builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(DISABLE_LIMIT_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC = builder.build(); } @@ -73,7 +73,7 @@ public void executeStep(PluginStepContext context, Map configura nodes.append(","); } String limit = nodes.length() > 0 ? nodes.substring(0, nodes.length() - 1) : ""; - if (limit != "") { + if (!limit.isEmpty()) { configuration.put(AnsibleDescribable.ANSIBLE_LIMIT, limit); } // set log level @@ -83,10 +83,10 @@ public void executeStep(PluginStepContext context, Map configura configuration.put(AnsibleDescribable.ANSIBLE_DEBUG, "False"); } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new StepException("Error configuring Ansible runner: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.ParseArgumentsError); } @@ -97,19 +97,19 @@ public void executeStep(PluginStepContext context, Map configura } catch (AnsibleException e) { Map failureData = new HashMap<>(); failureData.put("message", e.getMessage()); - failureData.put("ansible-config", builder.getConfigFile()); + failureData.put("ansible-config", contextBuilder.getConfigFile()); throw new StepException(e.getMessage(), e, e.getFailureReason(), failureData); } catch (Exception e) { Map failureData = new HashMap<>(); failureData.put("message", e.getMessage()); - failureData.put("ansible-config", builder.getConfigFile()); + failureData.put("ansible-config", contextBuilder.getConfigFile()); throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); } @@ -122,7 +122,7 @@ public Description getDescription() { @Override public List listSecretsPathWorkflowStep(ExecutionContext context, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context, context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(context, context.getFramework(), context.getNodes(), configuration); return AnsibleUtil.getSecretsPath(builder); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java index 6a754a1f..42d83072 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java @@ -5,7 +5,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.common.INodeEntry; import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException; import com.dtolabs.rundeck.core.plugins.Plugin; @@ -19,8 +19,6 @@ import java.util.List; import java.util.Map; -import java.util.Properties; -import java.util.stream.Collectors; @Plugin(name = AnsiblePlaybookWorflowNodeStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowNodeStep) public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDescribable, ProxyRunnerPlugin { @@ -39,6 +37,7 @@ public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDe builder.property(BASE_DIR_PROP); builder.property(PLAYBOOK_PATH_PROP); builder.property(EXTRA_VARS_PROP); + builder.property(CONFIG_ENCRYPT_EXTRA_VARS); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); builder.property(EXTRA_ATTRS_PROP); @@ -55,7 +54,6 @@ public class AnsiblePlaybookWorflowNodeStep implements NodeStepPlugin, AnsibleDe builder.property(BECOME_AUTH_TYPE_PROP); builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC=builder.build(); } @@ -80,11 +78,11 @@ public void executeNodeStep( configuration.put(AnsibleDescribable.ANSIBLE_DEBUG,"False"); } - AnsibleRunnerBuilder - builder = new AnsibleRunnerBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder + contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new NodeStepException("Error configuring Ansible runner: "+e.getMessage(), AnsibleException.AnsibleFailureReason.ParseArgumentsError,e.getMessage()); } @@ -98,12 +96,12 @@ public void executeNodeStep( throw new NodeStepException(e.getMessage(),AnsibleException.AnsibleFailureReason.AnsibleError,e.getMessage()); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); } @Override public List listSecretsPathWorkflowNodeStep(ExecutionContext context, INodeEntry node, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(node, context, context.getFramework(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(node, context, context.getFramework(), configuration); return AnsibleUtil.getSecretsPath(builder); } 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 73dce781..44bf7b03 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java @@ -5,7 +5,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleException; import com.rundeck.plugins.ansible.ansible.AnsibleRunner; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.dtolabs.rundeck.core.execution.workflow.steps.StepException; import com.dtolabs.rundeck.core.plugins.Plugin; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Plugin(name = AnsiblePlaybookWorkflowStep.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowStep) public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribable, ProxyRunnerPlugin { @@ -38,6 +37,7 @@ public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribab builder.property(BASE_DIR_PROP); builder.property(PLAYBOOK_PATH_PROP); builder.property(EXTRA_VARS_PROP); + builder.property(CONFIG_ENCRYPT_EXTRA_VARS); builder.property(INVENTORY_INLINE_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_KEY_STORAGE_PROP); @@ -56,7 +56,6 @@ public class AnsiblePlaybookWorkflowStep implements StepPlugin, AnsibleDescribab builder.property(BECOME_USER_PROP); builder.property(BECOME_PASSWORD_STORAGE_PROP); builder.property(DISABLE_LIMIT_PROP); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); DESC = builder.build(); } @@ -83,10 +82,10 @@ public void executeStep(PluginStepContext context, Map configura configuration.put(AnsibleDescribable.ANSIBLE_DEBUG, "False"); } - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder contextBuilder = new AnsibleRunnerContextBuilder(context.getExecutionContext(), context.getFramework(), context.getNodes(), configuration); try { - runner = builder.buildAnsibleRunner(); + runner = AnsibleRunner.buildAnsibleRunner(contextBuilder); } catch (ConfigurationException e) { throw new StepException("Error configuring Ansible runner: " + e.getMessage(), e, AnsibleException.AnsibleFailureReason.ParseArgumentsError); } @@ -97,18 +96,18 @@ public void executeStep(PluginStepContext context, Map configura } catch (AnsibleException e) { Map failureData = new HashMap<>(); failureData.put("message", e.getMessage()); - failureData.put("ansible-config", builder.getConfigFile()); + failureData.put("ansible-config", contextBuilder.getConfigFile()); throw new StepException(e.getMessage(), e, e.getFailureReason(), failureData); } catch (Exception e) { Map failureData = new HashMap<>(); failureData.put("message", e.getMessage()); - failureData.put("ansible-config", builder.getConfigFile()); + failureData.put("ansible-config", contextBuilder.getConfigFile()); throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData); } - builder.cleanupTempFiles(); + contextBuilder.cleanupTempFiles(); } @Override @@ -118,7 +117,7 @@ public Description getDescription() { @Override public List listSecretsPathWorkflowStep(ExecutionContext context, Map configuration) { - AnsibleRunnerBuilder builder = new AnsibleRunnerBuilder(context, context.getFramework(), context.getNodes(), configuration); + AnsibleRunnerContextBuilder builder = new AnsibleRunnerContextBuilder(context, context.getFramework(), context.getNodes(), configuration); return AnsibleUtil.getSecretsPath(builder); } @Override 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 012492a1..c924f280 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -94,7 +94,7 @@ public class AnsibleResourceModelSource implements ResourceModelSource, ProxyRun protected String becamePasswordStoragePath; - protected boolean encryptTempFiles = false; + protected boolean encryptExtraVars = false; public AnsibleResourceModelSource(final Framework framework) { @@ -194,22 +194,22 @@ public void configure(Properties configuration) throws ConfigurationException { becamePasswordStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); - encryptTempFiles = "true".equals(resolveProperty(AnsibleDescribable.ENCRYPT_TEMP_FILES,"false",configuration,executionDataContext)); + encryptExtraVars = "true".equals(resolveProperty(AnsibleDescribable.ANSIBLE_ENCRYPT_EXTRA_VARS,"false",configuration,executionDataContext)); } - public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ + public AnsibleRunner.AnsibleRunnerBuilder buildAnsibleRunner() throws ResourceModelSourceException{ - AnsibleRunner runner = AnsibleRunner.playbookPath("gather-hosts.yml"); + AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = AnsibleRunner.playbookPath("gather-hosts.yml"); if ("true".equals(System.getProperty("ansible.debug"))) { - runner.debug(); + runnerBuilder.debug(true); } if (limit != null && limit.length() > 0) { List limitList = new ArrayList<>(); limitList.add(limit); - runner.limit(limitList); + runnerBuilder.limits(limitList); } StorageTree storageTree = services.getService(KeyStorageTree.class); @@ -223,25 +223,25 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } catch (IOException e) { throw new ResourceModelSourceException("Could not read privatekey file " + sshPrivateKeyFile,e); } - runner = runner.sshPrivateKey(sshPrivateKey); + runnerBuilder.sshPrivateKey(sshPrivateKey); } if(sshPrivateKeyPath !=null && !sshPrivateKeyPath.isEmpty()){ try { String sshPrivateKey = getStorageContentString(sshPrivateKeyPath, storageTree); - runner = runner.sshPrivateKey(sshPrivateKey); + runnerBuilder.sshPrivateKey(sshPrivateKey); } catch (ConfigurationException e) { throw new ResourceModelSourceException("Could not read private key from storage path " + sshPrivateKeyPath,e); } } if(sshAgent != null && sshAgent.equalsIgnoreCase("true")) { - runner = runner.sshUseAgent(Boolean.TRUE); + runnerBuilder.sshUseAgent(Boolean.TRUE); if(sshPassphraseStoragePath != null && !sshPassphraseStoragePath.isEmpty()) { try { String sshPassphrase = getStorageContentString(sshPassphraseStoragePath, storageTree); - runner = runner.sshPassphrase(sshPassphrase); + runnerBuilder.sshPassphrase(sshPassphrase); } catch (ConfigurationException e) { throw new ResourceModelSourceException("Could not read passphrase from storage path " + sshPassphraseStoragePath,e); } @@ -250,13 +250,13 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } else if ( sshAuthType.equalsIgnoreCase(AuthenticationType.password.name()) ) { if (sshPassword != null) { - runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); + runnerBuilder.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); } if(sshPasswordPath !=null && !sshPasswordPath.isEmpty()){ try { sshPassword = getStorageContentString(sshPasswordPath, storageTree); - runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); + runnerBuilder.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); } catch (ConfigurationException e) { throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); } @@ -264,51 +264,51 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } if (inventory != null) { - runner = runner.setInventory(inventory); + runnerBuilder.inventory(inventory); } if (ignoreErrors == true) { - runner = runner.ignoreErrors(ignoreErrors); + runnerBuilder.ignoreErrors(ignoreErrors); } if (sshUser != null) { - runner = runner.sshUser(sshUser); + runnerBuilder.sshUser(sshUser); } if (sshTimeout != null) { - runner = runner.sshTimeout(sshTimeout); + runnerBuilder.sshTimeout(sshTimeout); } if (become != null) { - runner = runner.become(become); + runnerBuilder.become(become); } if (becomeUser != null) { - runner = runner.becomeUser(becomeUser); + runnerBuilder.becomeUser(becomeUser); } if (becomeMethod != null) { - runner = runner.becomeMethod(becomeMethod); + runnerBuilder.becomeMethod(becomeMethod); } if (becomePassword != null) { - runner = runner.becomePassword(becomePassword); + runnerBuilder.becomePassword(becomePassword); } if(becamePasswordStoragePath != null && !becamePasswordStoragePath.isEmpty()){ try { becomePassword = getStorageContentString(becamePasswordStoragePath, storageTree); - runner = runner.becomePassword(becomePassword); + runnerBuilder.becomePassword(becomePassword); } catch (Exception e) { throw new ResourceModelSourceException("Could not read becomePassword from storage path " + becamePasswordStoragePath,e); } } if (configFile != null) { - runner = runner.configFile(configFile); + runnerBuilder.configFile(configFile); } if(vaultPassword!=null) { - runner.vaultPass(vaultPassword); + runnerBuilder.vaultPass(vaultPassword); } if(vaultPasswordPath!=null && !vaultPasswordPath.isEmpty()){ @@ -317,7 +317,7 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } catch (Exception e) { throw new ResourceModelSourceException("Could not read vaultPassword " + vaultPasswordPath,e); } - runner = runner.vaultPass(vaultPassword); + runnerBuilder.vaultPass(vaultPassword); } if (vaultFile != null) { @@ -327,23 +327,23 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } catch (IOException e) { throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); } - runner.vaultPass(vaultPassword); + runnerBuilder.vaultPass(vaultPassword); } if (baseDirectoryPath != null) { - runner.baseDirectory(baseDirectoryPath); + runnerBuilder.baseDirectory(java.nio.file.Path.of(baseDirectoryPath)); } if (ansibleBinariesDirectoryPath != null) { - runner.ansibleBinariesDirectory(ansibleBinariesDirectoryPath); + runnerBuilder.ansibleBinariesDirectory(java.nio.file.Path.of(ansibleBinariesDirectoryPath)); } if (extraParameters != null){ - runner.extraParams(extraParameters); + runnerBuilder.extraParams(extraParameters); } - runner.encryptTemporaryFiles(encryptTempFiles); + runnerBuilder.encryptExtraVars(encryptExtraVars); - return runner; + return runnerBuilder; } @@ -366,9 +366,10 @@ public INodeSet getNodes() throws ResourceModelSourceException { throw new ResourceModelSourceException("Error copying files."); } - AnsibleRunner runner = buildAnsibleRunner(); + AnsibleRunner.AnsibleRunnerBuilder runnerBuilder = buildAnsibleRunner(); - runner.tempDirectory(tempDirectory).retainTempDirectory(); + runnerBuilder.tempDirectory(tempDirectory); + runnerBuilder.retainTempDirectory(true); StringBuilder args = new StringBuilder(); args.append("facts: ") @@ -378,12 +379,12 @@ public INodeSet getNodes() throws ResourceModelSourceException { .append(tempDirectory.toFile().getAbsolutePath()) .append("'"); - runner.extraVars(args.toString()); + runnerBuilder.extraVars(args.toString()); + + AnsibleRunner runner = runnerBuilder.build(); try { runner.run(); - } catch (AnsibleException e) { - throw new ResourceModelSourceException(e.getMessage(), e); } catch (Exception e) { throw new ResourceModelSourceException(e.getMessage(),e); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java index 377c8515..adaa30ea 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java @@ -72,7 +72,7 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { builder.mapping(ANSIBLE_VAULT_PATH,PROJ_PROP_PREFIX + ANSIBLE_VAULT_PATH); builder.frameworkMapping(ANSIBLE_VAULT_PATH,FWK_PROP_PREFIX + ANSIBLE_VAULT_PATH); - builder.property(CONFIG_ENCRYPT_TEMP_FILES); + builder.property(CONFIG_ENCRYPT_EXTRA_VARS); DESC=builder.build(); } 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 bb367df5..42c02d08 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java @@ -5,9 +5,12 @@ import com.dtolabs.rundeck.core.execution.proxy.SecretBundle; import com.dtolabs.rundeck.core.plugins.configuration.Property; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; -import com.rundeck.plugins.ansible.ansible.AnsibleRunnerBuilder; +import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.rundeck.plugins.ansible.plugin.AnsibleNodeExecutor; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -16,7 +19,7 @@ public class AnsibleUtil { - public static SecretBundle createBundle(AnsibleRunnerBuilder builder){ + public static SecretBundle createBundle(AnsibleRunnerContextBuilder builder){ DefaultSecretBundle secretBundle = new DefaultSecretBundle(); @@ -49,7 +52,7 @@ public static SecretBundle createBundle(AnsibleRunnerBuilder builder){ } } - public static List getSecretsPath(AnsibleRunnerBuilder builder){ + public static List getSecretsPath(AnsibleRunnerContextBuilder builder){ List secretPaths = new ArrayList<>(); if(builder.getPasswordStoragePath()!=null){ secretPaths.add( @@ -103,4 +106,13 @@ public static Map getRuntimeProperties(ExecutionContext context, return filterProperties; } + + + public static File createTemporaryFile(String suffix, String data) throws IOException { + File tempVarsFile = File.createTempFile("ansible-runner", suffix); + Files.write(tempVarsFile.toPath(), data.getBytes()); + return tempVarsFile; + } + + } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java b/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java new file mode 100644 index 00000000..f3590f2a --- /dev/null +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/ProcessExecutor.java @@ -0,0 +1,65 @@ +package com.rundeck.plugins.ansible.util; + +import lombok.Builder; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.List; +import java.io.File; +import java.util.Map; + +@Builder +public class ProcessExecutor { + + private List procArgs; + + private File baseDirectory; + + private Map environmentVariables; + + private List stdinVariables; + + private boolean redirectErrorStream; + + + public Process run() throws IOException { + ProcessBuilder processBuilder = new ProcessBuilder() + .command(procArgs) + .redirectErrorStream(redirectErrorStream); + + if(baseDirectory!=null){ + processBuilder.directory(baseDirectory); + } + + if(environmentVariables!=null){ + Map processEnvironment = processBuilder.environment(); + + for (Map.Entry entry : environmentVariables.entrySet()) { + processEnvironment.put(entry.getKey(), entry.getValue()); + } + } + + Process proc = processBuilder.start(); + + OutputStream stdin = proc.getOutputStream(); + OutputStreamWriter stdinw = new OutputStreamWriter(stdin); + + if (stdinVariables != null) { + try { + + for (String stdinVariable : stdinVariables) { + stdinw.write(stdinVariable); + } + stdinw.flush(); + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + } + } + stdinw.close(); + stdin.close(); + + return proc; + } + +} From e2ebc1a5c0feb624738a287ab926186ba7178d23 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 7 Mar 2024 22:10:52 -0300 Subject: [PATCH 04/23] use a file to pass vault password --- .../ansible/ansible/AnsibleRunner.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) 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 edc69547..aa513433 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -262,7 +262,7 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte private Path ansibleBinariesDirectory; private boolean usingTempDirectory; private boolean retainTempDirectory; - private List limits = new ArrayList<>(); + private List limits; private int result; @Builder.Default private Map options = new HashMap<>(); @@ -338,6 +338,8 @@ public int run() throws Exception { File tempFile = null; File tempPkFile = null; File tempVarsFile = null; + File tempInternalVaultFile = null; + File tempVaultFile = null; List procArgs = new ArrayList<>(); @@ -370,8 +372,9 @@ public int run() throws Exception { if (encryptExtraVars) { UUID uuid = UUID.randomUUID(); generatedVaultPassword = uuid.toString(); + tempInternalVaultFile = AnsibleUtil.createTemporaryFile("interal-vault", generatedVaultPassword); procArgs.add("--vault-id"); - procArgs.add("interval-encypt@prompt"); + procArgs.add("interval-encypt@" + tempInternalVaultFile.getAbsolutePath()); } if (inventory != null && !inventory.isEmpty()) { @@ -401,21 +404,18 @@ public int run() throws Exception { String addeExtraVars = extraVars; if (encryptExtraVars) { - addeExtraVars = encryptExtraVarsKey(extraVars); + addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); } - System.out.println(addeExtraVars); - tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); } if (vaultPass != null && !vaultPass.isEmpty()) { - //tempVaultFile = File.createTempFile("ansible-runner", "vault"); - //Files.write(tempVaultFile.toPath(), vaultPass.getBytes()); //procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); + tempVaultFile = AnsibleUtil.createTemporaryFile("vault", vaultPass); procArgs.add("--vault-id"); - procArgs.add("prompt"); + procArgs.add(tempVaultFile.getAbsolutePath()); } if (sshPrivateKey != null && !sshPrivateKey.isEmpty()) { @@ -685,18 +685,16 @@ public boolean registerKeySshAgent(String keyPath) throws Exception { return true; } - protected String encryptVariable(String content) throws IOException { + protected String encryptVariable(String content, File vaultPassword) throws IOException { List procArgs = new ArrayList<>(); procArgs.add("ansible-vault"); procArgs.add("encrypt_string"); procArgs.add("--vault-id"); - procArgs.add("interval-encypt@prompt"); + procArgs.add("interval-encypt@" + vaultPassword.getAbsolutePath()); //send values to STDIN in order List stdinVariables = new ArrayList<>(); - stdinVariables.add(generatedVaultPassword + "\n"); - stdinVariables.add(generatedVaultPassword + "\n"); stdinVariables.add(content); Process proc = null; @@ -705,7 +703,7 @@ protected String encryptVariable(String content) throws IOException { proc = ProcessExecutor.builder().procArgs(procArgs) .baseDirectory(baseDirectory.toFile()) .stdinVariables(stdinVariables) - //.redirectErrorStream(true) + .redirectErrorStream(true) .build().run(); StringBuilder stringBuilder = new StringBuilder(); @@ -744,7 +742,7 @@ protected String encryptVariable(String content) throws IOException { } - public String encryptExtraVarsKey(String extraVars) throws Exception { + public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) throws Exception { Map extraVarsMap = null; Map encryptedExtraVarsMap = new HashMap<>(); try { @@ -767,7 +765,7 @@ public String encryptExtraVarsKey(String extraVars) throws Exception { extraVarsMap.forEach((key, value) -> { try { - String encryptedKey = encryptVariable(value); + String encryptedKey = encryptVariable(value, vaultPasswordFile); if (encryptedKey != null) { encryptedExtraVarsMap.put(key, encryptedKey); } From 0f258715caf9e64ff7dbe6906daef99b7ab90a52 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 8 Mar 2024 15:10:21 -0300 Subject: [PATCH 05/23] use a script to get the password for ansible-vault --- .../ansible/ansible/AnsibleRunner.java | 49 ++++++++++++++----- src/main/resources/vault-client.py | 22 +++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) create mode 100755 src/main/resources/vault-client.py 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 aa513433..567f03e6 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -1,6 +1,7 @@ package com.rundeck.plugins.ansible.ansible; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; +import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; import com.fasterxml.jackson.core.type.TypeReference; import com.rundeck.plugins.ansible.util.*; import com.dtolabs.rundeck.core.utils.SSHAgentProcess; @@ -11,13 +12,10 @@ import java.io.*; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; +import java.nio.file.*; import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; import java.util.*; @Builder @@ -340,6 +338,8 @@ public int run() throws Exception { File tempVarsFile = null; File tempInternalVaultFile = null; File tempVaultFile = null; + File tempSshVarsFile = null; + List procArgs = new ArrayList<>(); @@ -372,7 +372,18 @@ public int run() throws Exception { if (encryptExtraVars) { UUID uuid = UUID.randomUUID(); generatedVaultPassword = uuid.toString(); - tempInternalVaultFile = AnsibleUtil.createTemporaryFile("interal-vault", generatedVaultPassword); + tempInternalVaultFile = File.createTempFile("ansible-runner", "-client.py"); + + try { + Files.copy(this.getClass().getClassLoader().getResourceAsStream("vault-client.py"), tempInternalVaultFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + System.err.println("error copying vault-client.py " + e.getMessage()); + } + + Set perms = PosixFilePermissions.fromString("rwxr-xr-x"); + Files.setPosixFilePermissions(tempInternalVaultFile.toPath(), perms); + + procArgs.add("--vault-id"); procArgs.add("interval-encypt@" + tempInternalVaultFile.getAbsolutePath()); } @@ -439,7 +450,16 @@ public int run() throws Exception { } if (sshUsePassword) { - procArgs.add("--ask-pass"); + String extraVarsPassword = "ansible_ssh_password: " + sshPass; + String finalextraVarsPassword = extraVarsPassword; + if (encryptExtraVars) { + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + } + + tempSshVarsFile = AnsibleUtil.createTemporaryFile("ssh-extra-vars", finalextraVarsPassword); + procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); + + //procArgs.add("--ask-pass"); } if (sshTimeout != null && sshTimeout > 0) { @@ -501,6 +521,10 @@ public int run() throws Exception { processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); } + if (encryptExtraVars) { + processEnvironment.put("VAULT_ID_SECRET", generatedVaultPassword); + } + processExecutorBuilder.environmentVariables(processEnvironment); //set STDIN variables @@ -517,10 +541,6 @@ public int run() throws Exception { } } - if (encryptExtraVars) { - stdinVariables.add(generatedVaultPassword + "\n"); - } - if (vaultPass != null && !vaultPass.isEmpty()) { stdinVariables.add(vaultPass + "\n"); } @@ -588,6 +608,9 @@ public int run() throws Exception { if (tempPlaybook != null && !tempPlaybook.delete()) { tempPlaybook.deleteOnExit(); } + if (tempSshVarsFile != null && !tempSshVarsFile.delete()){ + tempSshVarsFile.deleteOnExit(); + } if (usingTempDirectory && !retainTempDirectory) { deleteTempDirectory(baseDirectory); @@ -697,6 +720,9 @@ protected String encryptVariable(String content, File vaultPassword) throws IOEx List stdinVariables = new ArrayList<>(); stdinVariables.add(content); + Map env = new HashMap<>(); + env.put("VAULT_ID_SECRET", generatedVaultPassword); + Process proc = null; try { @@ -704,6 +730,7 @@ protected String encryptVariable(String content, File vaultPassword) throws IOEx .baseDirectory(baseDirectory.toFile()) .stdinVariables(stdinVariables) .redirectErrorStream(true) + .environmentVariables(env) .build().run(); StringBuilder stringBuilder = new StringBuilder(); diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py new file mode 100755 index 00000000..8b632b81 --- /dev/null +++ b/src/main/resources/vault-client.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import os +import argparse + +parser = argparse.ArgumentParser(description='Get a vault password from user keyring') + +parser.add_argument('--vault-id', action='store', default='dev', + dest='vault_id', + help='name of the vault secret to get from keyring') + +args = parser.parse_args() +keyname = args.vault_id +secret=os.environ["VAULT_ID_SECRET"] + +if secret is None: + sys.stderr.write('ERROR: VAULT_ID_SECRET is not set\n') + sys.exit(1) + +sys.stdout.write('%s/%s\n' % (keyname,secret)) + From b4002375c37245fa792dc576cc1b789a337efa1a Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 8 Mar 2024 15:33:28 -0300 Subject: [PATCH 06/23] use java 11 for ci --- .github/workflows/gradle.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f2ab0d71..7c3694b5 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -14,10 +14,10 @@ jobs: - name: Get Fetch Tags run: git -c protocol.version=2 fetch --tags --progress --no-recurse-submodules origin if: "!contains(github.ref, 'refs/tags')" - - name: Set up JDK 1.8 + - name: Set up JDK 1.11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b85846d5..f19e3a8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,10 +15,10 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - - name: set up JDK 1.8 + - name: set up JDK 1.11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Build with Gradle run: ./gradlew build - name: Get Release Version From e0df1e4c1a2c0e0d7896ef74be531ed501c4097f Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 8 Mar 2024 19:14:35 -0300 Subject: [PATCH 07/23] ssh authentication using extra vars --- .../ansible/ansible/AnsibleRunner.java | 92 +++++++++---------- .../plugins/ansible/util/AnsibleUtil.java | 35 ++++++- src/main/resources/vault-client.py | 18 +++- 3 files changed, 86 insertions(+), 59 deletions(-) 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 567f03e6..a4355f0f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -340,7 +340,6 @@ public int run() throws Exception { File tempVaultFile = null; File tempSshVarsFile = null; - List procArgs = new ArrayList<>(); String ansibleCommand = type.command; @@ -370,22 +369,17 @@ public int run() throws Exception { } if (encryptExtraVars) { - UUID uuid = UUID.randomUUID(); - generatedVaultPassword = uuid.toString(); - tempInternalVaultFile = File.createTempFile("ansible-runner", "-client.py"); + generatedVaultPassword = AnsibleUtil.randomString(); - try { - Files.copy(this.getClass().getClassLoader().getResourceAsStream("vault-client.py"), tempInternalVaultFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - System.err.println("error copying vault-client.py " + e.getMessage()); + try{ + tempInternalVaultFile = AnsibleUtil.createVaultScriptAuth("internal"); + }catch (IOException e){ + throw new AnsibleException("ERROR: Ansible vault script file for authentication." + e.getMessage(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); } - Set perms = PosixFilePermissions.fromString("rwxr-xr-x"); - Files.setPosixFilePermissions(tempInternalVaultFile.toPath(), perms); - - procArgs.add("--vault-id"); - procArgs.add("interval-encypt@" + tempInternalVaultFile.getAbsolutePath()); + procArgs.add("internal-encrypt@" + tempInternalVaultFile.getAbsolutePath()); } if (inventory != null && !inventory.isEmpty()) { @@ -423,8 +417,13 @@ public int run() throws Exception { } if (vaultPass != null && !vaultPass.isEmpty()) { - //procArgs.add("--vault-password-file" + "=" + tempVaultFile.getAbsolutePath()); - tempVaultFile = AnsibleUtil.createTemporaryFile("vault", vaultPass); + try{ + tempVaultFile = AnsibleUtil.createVaultScriptAuth("user-vault"); + }catch (IOException e){ + throw new AnsibleException("ERROR: Ansible vault script file for user authentication." + e.getMessage(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + procArgs.add("--vault-id"); procArgs.add(tempVaultFile.getAbsolutePath()); } @@ -452,14 +451,13 @@ public int run() throws Exception { if (sshUsePassword) { String extraVarsPassword = "ansible_ssh_password: " + sshPass; String finalextraVarsPassword = extraVarsPassword; + if (encryptExtraVars) { finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); } tempSshVarsFile = AnsibleUtil.createTemporaryFile("ssh-extra-vars", finalextraVarsPassword); procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); - - //procArgs.add("--ask-pass"); } if (sshTimeout != null && sshTimeout > 0) { @@ -468,8 +466,17 @@ public int run() throws Exception { if (become) { procArgs.add("--become"); + if (becomePassword != null && !becomePassword.isEmpty()) { - procArgs.add("--ask-become-pass"); + String extraVarsPassword = "ansible_become_password: " + becomePassword; + String finalextraVarsPassword = extraVarsPassword; + + if (encryptExtraVars) { + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + } + + tempSshVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); + procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); } } @@ -521,24 +528,13 @@ public int run() throws Exception { processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); } - if (encryptExtraVars) { - processEnvironment.put("VAULT_ID_SECRET", generatedVaultPassword); - } - processExecutorBuilder.environmentVariables(processEnvironment); //set STDIN variables List stdinVariables = new ArrayList<>(); - if (sshUsePassword) { - if (sshPass != null && !sshPass.isEmpty()) { - stdinVariables.add(sshPass + "\n"); - } - } - if (become) { - if (becomePassword != null && !becomePassword.isEmpty()) { - stdinVariables.add(becomePassword + "\n"); - } + if (encryptExtraVars) { + stdinVariables.add(generatedVaultPassword + "\n"); } if (vaultPass != null && !vaultPass.isEmpty()) { @@ -708,13 +704,17 @@ public boolean registerKeySshAgent(String keyPath) throws Exception { return true; } - protected String encryptVariable(String content, File vaultPassword) throws IOException { + protected String encryptVariable(String key, String content, File vaultPassword) throws IOException { List procArgs = new ArrayList<>(); procArgs.add("ansible-vault"); procArgs.add("encrypt_string"); procArgs.add("--vault-id"); - procArgs.add("interval-encypt@" + vaultPassword.getAbsolutePath()); + procArgs.add("internal-encrypt@" + vaultPassword.getAbsolutePath()); + + if(debug){ + System.out.println("encryptVariable " + key + ": " + procArgs); + } //send values to STDIN in order List stdinVariables = new ArrayList<>(); @@ -792,7 +792,7 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro extraVarsMap.forEach((key, value) -> { try { - String encryptedKey = encryptVariable(value, vaultPasswordFile); + String encryptedKey = encryptVariable(key, value, vaultPasswordFile); if (encryptedKey != null) { encryptedExtraVarsMap.put(key, encryptedKey); } @@ -807,7 +807,6 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro stringBuilder.append(" ").append(value).append("\n"); }); - //return mapperYaml.writeValueAsString(encryptedExtraVarsMap); return stringBuilder.toString(); } @@ -818,25 +817,18 @@ public void encryptFileAnsibleVault(File file) { procArgs.add("encrypt"); procArgs.add(file.getAbsolutePath()); - // execute the ssh-agent add process - ProcessBuilder processBuilder = new ProcessBuilder() - .command(procArgs) - .directory(baseDirectory.toFile()); + List stdinVariables = new ArrayList<>(); + stdinVariables.add(generatedVaultPassword + "\n"); + Process proc = null; try { - proc = processBuilder.start(); - - OutputStream stdin = proc.getOutputStream(); - OutputStreamWriter stdinw = new OutputStreamWriter(stdin); - try { - stdinw.write(generatedVaultPassword + "\n"); - stdinw.write(generatedVaultPassword + "\n"); - stdinw.flush(); - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - } + proc = ProcessExecutor.builder().procArgs(procArgs) + .baseDirectory(baseDirectory.toFile()) + .stdinVariables(stdinVariables) + .redirectErrorStream(true) + .build().run(); int exitCode = proc.waitFor(); 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 42c02d08..bab111bc 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java @@ -11,10 +11,11 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.SecureRandom; +import java.util.*; import java.util.stream.Collectors; public class AnsibleUtil { @@ -107,7 +108,6 @@ public static Map getRuntimeProperties(ExecutionContext context, } - public static File createTemporaryFile(String suffix, String data) throws IOException { File tempVarsFile = File.createTempFile("ansible-runner", suffix); Files.write(tempVarsFile.toPath(), data.getBytes()); @@ -115,4 +115,29 @@ public static File createTemporaryFile(String suffix, String data) throws IOExce } + public static String randomString(){ + byte[] bytes = new byte[32]; + new SecureRandom().nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + + } + + public static File createVaultScriptAuth(String suffix) throws IOException { + File tempInternalVaultFile = File.createTempFile("ansible-runner", suffix + "-client.py"); + + try { + Files.copy(AnsibleUtil.class.getClassLoader().getResourceAsStream("vault-client.py"), + tempInternalVaultFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IOException("Failed to copy vault-client.py", e); + } + + Set perms = PosixFilePermissions.fromString("rwxr-xr-x"); + Files.setPosixFilePermissions(tempInternalVaultFile.toPath(), perms); + + return tempInternalVaultFile; + } + + } diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 8b632b81..489016c3 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 - import sys import os import argparse +import getpass parser = argparse.ArgumentParser(description='Get a vault password from user keyring') @@ -12,11 +12,21 @@ args = parser.parse_args() keyname = args.vault_id -secret=os.environ["VAULT_ID_SECRET"] -if secret is None: - sys.stderr.write('ERROR: VAULT_ID_SECRET is not set\n') +if "VAULT_ID_SECRET" in os.environ: + secret=os.environ["VAULT_ID_SECRET"] + sys.stdout.write('%s/%s\n' % (keyname,secret)) + sys.exit(0) + +if sys.stdin.isatty(): + secret = getpass.getpass() +else: + secret = sys.stdin.readline().rstrip() + +if secret is None or secret == '': + sys.stderr.write('ERROR: secret is not set\n') sys.exit(1) sys.stdout.write('%s/%s\n' % (keyname,secret)) +sys.exit(0) From 1f9f7bc8d9b52fb4187bf7b10d57043cba2cb13a Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 11 Mar 2024 16:37:17 -0300 Subject: [PATCH 08/23] remove the secure option from environment variables of the main process --- .../ansible/ansible/AnsibleRunner.java | 26 ++++++------------- .../ansible/AnsibleRunnerContextBuilder.java | 19 +++++++++++--- 2 files changed, 23 insertions(+), 22 deletions(-) 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 a4355f0f..aef25af2 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -1,7 +1,6 @@ package com.rundeck.plugins.ansible.ansible; import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; -import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; import com.fasterxml.jackson.core.type.TypeReference; import com.rundeck.plugins.ansible.util.*; import com.dtolabs.rundeck.core.utils.SSHAgentProcess; @@ -15,7 +14,6 @@ import java.nio.file.*; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; import java.util.*; @Builder @@ -119,10 +117,10 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte } // set rundeck options as environment variables -// Map options = context.getDataContext().get("option"); -// if (options != null) { -// runner = runner.options(options); -// } + Map options = contextBuilder.getListOptions(); + if (options != null) { + ansibleRunnerBuilder.options(options); + } String inventory = contextBuilder.getInventory(); if (inventory != null) { @@ -280,14 +278,6 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte static ObjectMapper mapperJson = new ObjectMapper(); - /** - * Add options passed as Environment variables to ansible - */ -// public AnsibleRunner options(Map options) { -// this.options.putAll(options); -// return this; -// } - public void deleteTempDirectory(Path tempDirectory) throws IOException { Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { @Override @@ -412,7 +402,7 @@ public int run() throws Exception { addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); } - tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); + tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); } @@ -520,9 +510,9 @@ public int run() throws Exception { processEnvironment.put("ANSIBLE_CONFIG", configFile); } -// for (String optionName : this.options.keySet()) { -// processEnvironment.put(optionName, this.options.get(optionName)); -// } + for (String optionName : this.options.keySet()) { + processEnvironment.put(optionName, this.options.get(optionName)); + } if (sshUseAgent && sshAgent != null) { processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); 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 89db759f..be3bd1c5 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java @@ -24,10 +24,7 @@ import lombok.Getter; import org.rundeck.storage.api.Path; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.Map; +import java.util.*; import org.rundeck.storage.api.PathUtil; import org.rundeck.storage.api.StorageException; @@ -820,4 +817,18 @@ public boolean encryptExtraVars() throws ConfigurationException { getJobConf() ); } + + public Map getListOptions(){ + Map options = new HashMap<>(); + Map optionsContext = context.getDataContext().get("option"); + Map secureOptionContext = context.getDataContext().get("secureOption"); + if (optionsContext != null) { + optionsContext.forEach((option, value) -> { + if(!secureOptionContext.containsKey(option)){ + options.put(option, value); + } + }); + } + return options; + } } From f170066fc995df1a97266b31d6260c6a05250248 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 11 Mar 2024 18:57:47 -0300 Subject: [PATCH 09/23] check if ansible-vault is installed use internal encryption for ssh pass auth use internal encryption for ssh became password auth --- .../ansible/ansible/AnsibleRunner.java | 50 +++++++++++++------ .../plugins/ansible/util/AnsibleUtil.java | 27 +++++++++- 2 files changed, 61 insertions(+), 16 deletions(-) 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 aef25af2..9ae8d2e5 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -274,6 +274,9 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte @Builder.Default private boolean encryptExtraVars = false; + @Builder.Default + private boolean useAnsibleVault = false; + static ObjectMapper mapperYaml = new ObjectMapper(new YAMLFactory()); static ObjectMapper mapperJson = new ObjectMapper(); @@ -358,18 +361,31 @@ public int run() throws Exception { procArgs.add(tempPlaybook.getAbsolutePath()); } - if (encryptExtraVars) { - generatedVaultPassword = AnsibleUtil.randomString(); + // use ansible-vault to encrypt extra-vars + // 1) if the encryptExtraVars is enabled (user input) + // 2) ssh-password is used for node authentication + // 3) become-password is used for node authentication + if (encryptExtraVars || + sshUsePassword || + (become && becomePassword != null && !becomePassword.isEmpty())) { - try{ - tempInternalVaultFile = AnsibleUtil.createVaultScriptAuth("internal"); - }catch (IOException e){ - throw new AnsibleException("ERROR: Ansible vault script file for authentication." + e.getMessage(), - AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } + useAnsibleVault = AnsibleUtil.checkAnsibleVault(); - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + tempInternalVaultFile.getAbsolutePath()); + if(useAnsibleVault) { + generatedVaultPassword = AnsibleUtil.randomString(); + + try { + tempInternalVaultFile = AnsibleUtil.createVaultScriptAuth("internal"); + } catch (IOException e) { + throw new AnsibleException("ERROR: Ansible vault script file for authentication." + e.getMessage(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); + } + + procArgs.add("--vault-id"); + procArgs.add("internal-encrypt@" + tempInternalVaultFile.getAbsolutePath()); + }else{ + System.err.println("WARN: ansible-vault is not installed, extra-vars will not be encrypted."); + } } if (inventory != null && !inventory.isEmpty()) { @@ -398,7 +414,7 @@ public int run() throws Exception { if (extraVars != null && !extraVars.isEmpty()) { String addeExtraVars = extraVars; - if (encryptExtraVars) { + if (encryptExtraVars && useAnsibleVault) { addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); } @@ -442,7 +458,7 @@ public int run() throws Exception { String extraVarsPassword = "ansible_ssh_password: " + sshPass; String finalextraVarsPassword = extraVarsPassword; - if (encryptExtraVars) { + if(useAnsibleVault){ finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); } @@ -461,7 +477,7 @@ public int run() throws Exception { String extraVarsPassword = "ansible_become_password: " + becomePassword; String finalextraVarsPassword = extraVarsPassword; - if (encryptExtraVars) { + if (useAnsibleVault) { finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); } @@ -523,7 +539,7 @@ public int run() throws Exception { //set STDIN variables List stdinVariables = new ArrayList<>(); - if (encryptExtraVars) { + if (useAnsibleVault) { stdinVariables.add(generatedVaultPassword + "\n"); } @@ -775,6 +791,12 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro } } + boolean checkAnsibleVault = AnsibleUtil.checkAnsibleVault(); + if(!checkAnsibleVault){ + System.err.println("error encryptExtraVars, ansible-vault is not installed."); + return null; + } + if (extraVarsMap == null || extraVarsMap.isEmpty()) { System.err.println("error encryptExtraVars, the given vars cannot be mapped to a valid map."); return null; 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 bab111bc..b0dec01d 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java @@ -8,8 +8,7 @@ import com.rundeck.plugins.ansible.ansible.AnsibleRunnerContextBuilder; import com.rundeck.plugins.ansible.plugin.AnsibleNodeExecutor; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.PosixFilePermission; @@ -140,4 +139,28 @@ public static File createVaultScriptAuth(String suffix) throws IOException { } + public static boolean checkAnsibleVault() { + List procArgs = new ArrayList<>(); + procArgs.add("ansible-vault"); + procArgs.add("--version"); + + Process proc = null; + + try { + proc = ProcessExecutor.builder().procArgs(procArgs) + .redirectErrorStream(true) + .build().run(); + + int exitCode = proc.waitFor(); + return exitCode == 0; + } catch (IOException | InterruptedException e) { + System.err.println("Failed to check ansible-vault: " + e.getMessage()); + return false; + }finally { + if (proc != null) { + proc.destroy(); + } + } + } + } From 7fdbbd3a49d32ff7d4fe800b397514247eb47662 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 13 Mar 2024 12:13:01 -0300 Subject: [PATCH 10/23] add unit test for ansible runner --- .../ansible/ansible/AnsibleRunner.java | 484 +++++++----------- .../plugins/ansible/ansible/AnsibleVault.java | 134 +++++ .../plugins/ansible/util/AnsibleUtil.java | 44 -- .../ansible/ansible/AnsibleRunnerSpec.groovy | 259 ++++++++++ 4 files changed, 582 insertions(+), 339 deletions(-) create mode 100644 src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java create mode 100644 src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy 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 9ae8d2e5..7ecf87cc 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -269,8 +269,6 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte private Listener listener; - private String generatedVaultPassword; - @Builder.Default private boolean encryptExtraVars = false; @@ -280,6 +278,18 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte static ObjectMapper mapperYaml = new ObjectMapper(new YAMLFactory()); static ObjectMapper mapperJson = new ObjectMapper(); + private ProcessExecutor.ProcessExecutorBuilder processExecutorBuilder; + private AnsibleVault ansibleVault; + + //temporary files + File tempPlaybook; + File tempFile; + File tempPkFile; + File tempVarsFile; + File tempInternalVaultFile; + File tempVaultFile ; + File tempSshVarsFile ; + public void deleteTempDirectory(Path tempDirectory) throws IOException { Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { @@ -325,231 +335,225 @@ public int run() throws Exception { baseDirectory = Files.createTempDirectory("ansible-rundeck"); } - File tempPlaybook = null; - File tempFile = null; - File tempPkFile = null; - File tempVarsFile = null; - File tempInternalVaultFile = null; - File tempVaultFile = null; - File tempSshVarsFile = null; - - List procArgs = new ArrayList<>(); + if(ansibleVault==null){ + tempInternalVaultFile = AnsibleVault.createVaultScriptAuth("ansible-script-vault"); - String ansibleCommand = type.command; - if (ansibleBinariesDirectory != null) { - ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); + ansibleVault = AnsibleVault.builder() + .baseDirectory(baseDirectory) + .masterPassword(AnsibleUtil.randomString()) + .vaultPasswordScriptFile(tempInternalVaultFile) + .debug(debug).build(); } - procArgs.add(ansibleCommand); - // parse arguments - if (type == AnsibleCommand.AdHoc) { - procArgs.add("all"); + List procArgs = new ArrayList<>(); + Process proc = null; - procArgs.add("-m"); - procArgs.add(module); + try { - if (arg != null && !arg.isEmpty()) { - procArgs.add("-a"); - procArgs.add(arg); + String ansibleCommand = type.command; + if (ansibleBinariesDirectory != null) { + ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); } - procArgs.add("-t"); - procArgs.add(baseDirectory.toFile().getAbsolutePath()); - } else if (type == AnsibleCommand.PlaybookPath) { - procArgs.add(playbook); - } else if (type == AnsibleCommand.PlaybookInline) { - tempPlaybook = AnsibleUtil.createTemporaryFile("playbook", playbook); - procArgs.add(tempPlaybook.getAbsolutePath()); - } - - // use ansible-vault to encrypt extra-vars - // 1) if the encryptExtraVars is enabled (user input) - // 2) ssh-password is used for node authentication - // 3) become-password is used for node authentication - if (encryptExtraVars || - sshUsePassword || - (become && becomePassword != null && !becomePassword.isEmpty())) { + procArgs.add(ansibleCommand); - useAnsibleVault = AnsibleUtil.checkAnsibleVault(); + // parse arguments + if (type == AnsibleCommand.AdHoc) { + procArgs.add("all"); - if(useAnsibleVault) { - generatedVaultPassword = AnsibleUtil.randomString(); + procArgs.add("-m"); + procArgs.add(module); - try { - tempInternalVaultFile = AnsibleUtil.createVaultScriptAuth("internal"); - } catch (IOException e) { - throw new AnsibleException("ERROR: Ansible vault script file for authentication." + e.getMessage(), - AnsibleException.AnsibleFailureReason.AnsibleNonZero); + if (arg != null && !arg.isEmpty()) { + procArgs.add("-a"); + procArgs.add(arg); + } + procArgs.add("-t"); + procArgs.add(baseDirectory.toFile().getAbsolutePath()); + } else if (type == AnsibleCommand.PlaybookPath) { + procArgs.add(playbook); + } else if (type == AnsibleCommand.PlaybookInline) { + tempPlaybook = AnsibleUtil.createTemporaryFile("playbook", playbook); + procArgs.add(tempPlaybook.getAbsolutePath()); + } + + // use ansible-vault to encrypt extra-vars + // 1) if the encryptExtraVars is enabled (user input) + // 2) ssh-password is used for node authentication + // 3) become-password is used for node authentication + if (encryptExtraVars || + sshUsePassword || + (become && becomePassword != null && !becomePassword.isEmpty())) { + + useAnsibleVault = ansibleVault.checkAnsibleVault(); + + if(useAnsibleVault) { + tempInternalVaultFile = ansibleVault.getVaultPasswordScriptFile(); + + procArgs.add("--vault-id"); + procArgs.add("internal-encrypt@" + tempInternalVaultFile.getAbsolutePath()); + }else{ + System.err.println("WARN: ansible-vault is not installed, extra-vars will not be encrypted."); } + } - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + tempInternalVaultFile.getAbsolutePath()); - }else{ - System.err.println("WARN: ansible-vault is not installed, extra-vars will not be encrypted."); + if (inventory != null && !inventory.isEmpty()) { + procArgs.add("--inventory-file" + "=" + inventory); } - } - if (inventory != null && !inventory.isEmpty()) { - procArgs.add("--inventory-file" + "=" + inventory); - } + if (limits != null && limits.size() == 1) { + procArgs.add("-l"); + procArgs.add(limits.get(0)); - 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"); + } + tempFile = AnsibleUtil.createTemporaryFile("targets", sb.toString()); - } else if (limits != null && limits.size() > 1) { - StringBuilder sb = new StringBuilder(); - for (String limit : limits) { - sb.append(limit).append("\n"); + procArgs.add("-l"); + procArgs.add("@" + tempFile.getAbsolutePath()); } - tempFile = AnsibleUtil.createTemporaryFile("targets", sb.toString()); - procArgs.add("-l"); - procArgs.add("@" + tempFile.getAbsolutePath()); - } + if (debug == Boolean.TRUE) { + procArgs.add("-vvv"); + } - if (debug == Boolean.TRUE) { - procArgs.add("-vvv"); - } + if (extraVars != null && !extraVars.isEmpty()) { + String addeExtraVars = extraVars; - if (extraVars != null && !extraVars.isEmpty()) { - String addeExtraVars = extraVars; + if (encryptExtraVars && useAnsibleVault) { + addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); + } - if (encryptExtraVars && useAnsibleVault) { - addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); + tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); + procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); } - tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); - procArgs.add("--extra-vars" + "=" + "@" + tempVarsFile.getAbsolutePath()); - } - - if (vaultPass != null && !vaultPass.isEmpty()) { - try{ - tempVaultFile = AnsibleUtil.createVaultScriptAuth("user-vault"); - }catch (IOException e){ - throw new AnsibleException("ERROR: Ansible vault script file for user authentication." + e.getMessage(), - AnsibleException.AnsibleFailureReason.AnsibleNonZero); + if (vaultPass != null && !vaultPass.isEmpty()) { + tempVaultFile = ansibleVault.getVaultPasswordScriptFile(); + procArgs.add("--vault-id"); + procArgs.add(tempVaultFile.getAbsolutePath()); } - procArgs.add("--vault-id"); - procArgs.add(tempVaultFile.getAbsolutePath()); - } + if (sshPrivateKey != null && !sshPrivateKey.isEmpty()) { + String privateKeyData = sshPrivateKey.replaceAll("\r\n", "\n"); + tempPkFile = AnsibleUtil.createTemporaryFile("id_rsa", privateKeyData); - if (sshPrivateKey != null && !sshPrivateKey.isEmpty()) { - String privateKeyData = sshPrivateKey.replaceAll("\r\n", "\n"); - tempPkFile = AnsibleUtil.createTemporaryFile("id_rsa", privateKeyData); + // Only the owner can read and write + Set perms = new HashSet(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(tempPkFile.toPath(), perms); - // Only the owner can read and write - Set perms = new HashSet(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(tempPkFile.toPath(), perms); + if (sshUseAgent) { + registerKeySshAgent(tempPkFile.getAbsolutePath()); + } + procArgs.add("--private-key" + "=" + tempPkFile.toPath()); + } - if (sshUseAgent) { - registerKeySshAgent(tempPkFile.getAbsolutePath()); + if (sshUser != null && sshUser.length() > 0) { + procArgs.add("--user" + "=" + sshUser); } - procArgs.add("--private-key" + "=" + tempPkFile.toPath()); - } - if (sshUser != null && sshUser.length() > 0) { - procArgs.add("--user" + "=" + sshUser); - } + if (sshUsePassword) { + String extraVarsPassword = "ansible_ssh_password: " + sshPass; + String finalextraVarsPassword = extraVarsPassword; - if (sshUsePassword) { - String extraVarsPassword = "ansible_ssh_password: " + sshPass; - String finalextraVarsPassword = extraVarsPassword; + if(useAnsibleVault){ + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + } - if(useAnsibleVault){ - finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + tempSshVarsFile = AnsibleUtil.createTemporaryFile("ssh-extra-vars", finalextraVarsPassword); + procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); } - tempSshVarsFile = AnsibleUtil.createTemporaryFile("ssh-extra-vars", finalextraVarsPassword); - procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); - } + if (sshTimeout != null && sshTimeout > 0) { + procArgs.add("--timeout" + "=" + sshTimeout); + } - if (sshTimeout != null && sshTimeout > 0) { - procArgs.add("--timeout" + "=" + sshTimeout); - } + if (become) { + procArgs.add("--become"); - if (become) { - procArgs.add("--become"); + if (becomePassword != null && !becomePassword.isEmpty()) { + String extraVarsPassword = "ansible_become_password: " + becomePassword; + String finalextraVarsPassword = extraVarsPassword; - if (becomePassword != null && !becomePassword.isEmpty()) { - String extraVarsPassword = "ansible_become_password: " + becomePassword; - String finalextraVarsPassword = extraVarsPassword; + if (useAnsibleVault) { + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + } - if (useAnsibleVault) { - finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + tempSshVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); + procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); } + } - tempSshVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); - procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); + if (becomeMethod != null && !becomeMethod.isEmpty()) { + procArgs.add("--become-method" + "=" + becomeMethod); } - } - if (becomeMethod != null && !becomeMethod.isEmpty()) { - procArgs.add("--become-method" + "=" + becomeMethod); - } + if (becomeUser != null && !becomeUser.isEmpty()) { + procArgs.add("--become-user" + "=" + becomeUser); + } - if (becomeUser != null && !becomeUser.isEmpty()) { - procArgs.add("--become-user" + "=" + becomeUser); - } + // default the listener to stdout logger + if (listener == null) { + listener = ListenerFactory.getListener(System.out); + } - // default the listener to stdout logger - if (listener == null) { - listener = ListenerFactory.getListener(System.out); - } + if (extraParams != null && !extraParams.isEmpty()) { + procArgs.addAll(tokenizeCommand(extraParams)); + } - if (extraParams != null && !extraParams.isEmpty()) { - procArgs.addAll(tokenizeCommand(extraParams)); - } + if (debug) { + System.out.println(" procArgs: " + procArgs); + } - if (debug) { - System.out.println(" procArgs: " + procArgs); - } + if(processExecutorBuilder==null){ + processExecutorBuilder = ProcessExecutor.builder(); + } - ProcessExecutor.ProcessExecutorBuilder processExecutorBuilder = ProcessExecutor.builder() - .procArgs(procArgs); + //set main process command + processExecutorBuilder.procArgs(procArgs); - if (baseDirectory != null) { - processExecutorBuilder.baseDirectory(baseDirectory.toFile()); - } - Process proc = null; + if (baseDirectory != null) { + processExecutorBuilder.baseDirectory(baseDirectory.toFile()); + } - //SET env variables - Map processEnvironment = new HashMap<>(); + //SET env variables + Map processEnvironment = new HashMap<>(); - if (configFile != null && !configFile.isEmpty()) { - if (debug) { - System.out.println(" ANSIBLE_CONFIG: " + configFile); - } + if (configFile != null && !configFile.isEmpty()) { + if (debug) { + System.out.println(" ANSIBLE_CONFIG: " + configFile); + } - processEnvironment.put("ANSIBLE_CONFIG", configFile); - } + processEnvironment.put("ANSIBLE_CONFIG", configFile); + } - for (String optionName : this.options.keySet()) { - processEnvironment.put(optionName, this.options.get(optionName)); - } + for (String optionName : this.options.keySet()) { + processEnvironment.put(optionName, this.options.get(optionName)); + } - if (sshUseAgent && sshAgent != null) { - processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); - } + if (sshUseAgent && sshAgent != null) { + processEnvironment.put("SSH_AUTH_SOCK", this.sshAgent.getSocketPath()); + } - processExecutorBuilder.environmentVariables(processEnvironment); + processExecutorBuilder.environmentVariables(processEnvironment); - //set STDIN variables - List stdinVariables = new ArrayList<>(); + //set STDIN variables + List stdinVariables = new ArrayList<>(); - if (useAnsibleVault) { - stdinVariables.add(generatedVaultPassword + "\n"); - } + if (useAnsibleVault) { + stdinVariables.add(ansibleVault.getMasterPassword() + "\n"); + } - if (vaultPass != null && !vaultPass.isEmpty()) { - stdinVariables.add(vaultPass + "\n"); - } + if (vaultPass != null && !vaultPass.isEmpty()) { + stdinVariables.add(vaultPass + "\n"); + } - processExecutorBuilder.stdinVariables(stdinVariables); + processExecutorBuilder.stdinVariables(stdinVariables); - try { proc = processExecutorBuilder.build().run(); Thread errthread = Logging.copyStreamThread(proc.getErrorStream(), listener); @@ -614,6 +618,10 @@ public int run() throws Exception { tempSshVarsFile.deleteOnExit(); } + if (tempInternalVaultFile != null && !tempInternalVaultFile.delete()){ + tempInternalVaultFile.deleteOnExit(); + } + if (usingTempDirectory && !retainTempDirectory) { deleteTempDirectory(baseDirectory); } @@ -710,70 +718,6 @@ public boolean registerKeySshAgent(String keyPath) throws Exception { return true; } - protected String encryptVariable(String key, String content, File vaultPassword) throws IOException { - - List procArgs = new ArrayList<>(); - procArgs.add("ansible-vault"); - procArgs.add("encrypt_string"); - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + vaultPassword.getAbsolutePath()); - - if(debug){ - System.out.println("encryptVariable " + key + ": " + procArgs); - } - - //send values to STDIN in order - List stdinVariables = new ArrayList<>(); - stdinVariables.add(content); - - Map env = new HashMap<>(); - env.put("VAULT_ID_SECRET", generatedVaultPassword); - - Process proc = null; - - try { - proc = ProcessExecutor.builder().procArgs(procArgs) - .baseDirectory(baseDirectory.toFile()) - .stdinVariables(stdinVariables) - .redirectErrorStream(true) - .environmentVariables(env) - .build().run(); - - StringBuilder stringBuilder = new StringBuilder(); - - final InputStream stdoutInputStream = proc.getInputStream(); - final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); - - String line1 = null; - boolean capture = false; - while ((line1 = stdoutReader.readLine()) != null) { - if (line1.toLowerCase().contains("!vault")) { - capture = true; - } - if (capture) { - stringBuilder.append(line1).append("\n"); - } - } - - int exitCode = proc.waitFor(); - - if (exitCode != 0) { - System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); - return null; - } - return stringBuilder.toString(); - - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - return null; - } finally { - // Make sure to always cleanup on failure and success - if (proc != null) { - proc.destroy(); - } - } - } - public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) throws Exception { Map extraVarsMap = null; @@ -781,38 +725,26 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro try { extraVarsMap = mapperYaml.readValue(extraVars, new TypeReference>() { }); + + for (Map.Entry entry : extraVarsMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + String encryptedKey = ansibleVault.encryptVariable(key, value); + if (encryptedKey != null) { + encryptedExtraVarsMap.put(key, encryptedKey); + } + } + } catch (Exception e) { try { extraVarsMap = mapperJson.readValue(extraVars, new TypeReference>() { }); } catch (Exception e2) { - System.err.println("error encryptExtraVars " + e2.getMessage()); - return null; + throw new AnsibleException("ERROR: cannot parse extra var values: " + e2.getMessage(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); } } - boolean checkAnsibleVault = AnsibleUtil.checkAnsibleVault(); - if(!checkAnsibleVault){ - System.err.println("error encryptExtraVars, ansible-vault is not installed."); - return null; - } - - if (extraVarsMap == null || extraVarsMap.isEmpty()) { - System.err.println("error encryptExtraVars, the given vars cannot be mapped to a valid map."); - return null; - } - - extraVarsMap.forEach((key, value) -> { - try { - String encryptedKey = encryptVariable(key, value, vaultPasswordFile); - if (encryptedKey != null) { - encryptedExtraVarsMap.put(key, encryptedKey); - } - } catch (IOException e) { - System.out.println(e.getMessage()); - } - }); - StringBuilder stringBuilder = new StringBuilder(); encryptedExtraVarsMap.forEach((key, value) -> { stringBuilder.append(key).append(":"); @@ -822,42 +754,4 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro return stringBuilder.toString(); } - public void encryptFileAnsibleVault(File file) { - - List procArgs = new ArrayList<>(); - procArgs.add("ansible-vault"); - procArgs.add("encrypt"); - procArgs.add(file.getAbsolutePath()); - - List stdinVariables = new ArrayList<>(); - stdinVariables.add(generatedVaultPassword + "\n"); - - Process proc = null; - - try { - - proc = ProcessExecutor.builder().procArgs(procArgs) - .baseDirectory(baseDirectory.toFile()) - .stdinVariables(stdinVariables) - .redirectErrorStream(true) - .build().run(); - - int exitCode = proc.waitFor(); - - if (exitCode != 0) { - throw new AnsibleException("ERROR: encryptFileAnsibleVault:" + procArgs, - AnsibleException.AnsibleFailureReason.AnsibleNonZero); - } - - - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - } finally { - // Make sure to always cleanup on failure and success - if (proc != null) { - proc.destroy(); - } - } - } - } \ No newline at end of file diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java new file mode 100644 index 00000000..daeb1a6b --- /dev/null +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -0,0 +1,134 @@ +package com.rundeck.plugins.ansible.ansible; + +import com.rundeck.plugins.ansible.util.AnsibleUtil; +import com.rundeck.plugins.ansible.util.ProcessExecutor; +import lombok.Builder; +import lombok.Data; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.*; + +@Data +@Builder +public class AnsibleVault { + + private File vaultPasswordScriptFile; + private String masterPassword; + private boolean debug; + private Path baseDirectory; + + public final String ANSIBLE_VAULT_COMMAND = "ansible-vault"; + + public boolean checkAnsibleVault() { + List procArgs = new ArrayList<>(); + procArgs.add(ANSIBLE_VAULT_COMMAND); + procArgs.add("--version"); + + Process proc = null; + + try { + proc = ProcessExecutor.builder().procArgs(procArgs) + .redirectErrorStream(true) + .build().run(); + + int exitCode = proc.waitFor(); + return exitCode == 0; + } catch (IOException | InterruptedException e) { + System.err.println("Failed to check ansible-vault: " + e.getMessage()); + return false; + }finally { + if (proc != null) { + proc.destroy(); + } + } + } + + public String encryptVariable(String key, + String content ) throws IOException { + + List procArgs = new ArrayList<>(); + procArgs.add("ansible-vault"); + procArgs.add("encrypt_string"); + procArgs.add("--vault-id"); + procArgs.add("internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); + + if(debug){ + System.out.println("encryptVariable " + key + ": " + procArgs); + } + + //send values to STDIN in order + List stdinVariables = new ArrayList<>(); + stdinVariables.add(content); + + Map env = new HashMap<>(); + env.put("VAULT_ID_SECRET", masterPassword); + + Process proc = null; + + try { + proc = ProcessExecutor.builder().procArgs(procArgs) + .baseDirectory(baseDirectory.toFile()) + .stdinVariables(stdinVariables) + .redirectErrorStream(true) + .environmentVariables(env) + .build().run(); + + StringBuilder stringBuilder = new StringBuilder(); + + final InputStream stdoutInputStream = proc.getInputStream(); + final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + + String line1 = null; + boolean capture = false; + while ((line1 = stdoutReader.readLine()) != null) { + if (line1.toLowerCase().contains("!vault")) { + capture = true; + } + if (capture) { + stringBuilder.append(line1).append("\n"); + } + } + + int exitCode = proc.waitFor(); + + if (exitCode != 0) { + System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); + return null; + } + return stringBuilder.toString(); + + } catch (Exception e) { + System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); + return null; + } finally { + // Make sure to always cleanup on failure and success + if (proc != null) { + proc.destroy(); + } + } + } + + + public static File createVaultScriptAuth(String suffix) throws IOException { + File tempInternalVaultFile = File.createTempFile("ansible-runner", suffix + "-client.py"); + + try { + Files.copy(AnsibleUtil.class.getClassLoader().getResourceAsStream("vault-client.py"), + tempInternalVaultFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IOException("Failed to copy vault-client.py", e); + } + + Set perms = PosixFilePermissions.fromString("rwxr-xr-x"); + Files.setPosixFilePermissions(tempInternalVaultFile.toPath(), perms); + + return tempInternalVaultFile; + } + +} 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 b0dec01d..f2e83ea7 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/util/AnsibleUtil.java @@ -10,9 +10,6 @@ import java.io.*; import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; import java.security.SecureRandom; import java.util.*; import java.util.stream.Collectors; @@ -121,46 +118,5 @@ public static String randomString(){ } - public static File createVaultScriptAuth(String suffix) throws IOException { - File tempInternalVaultFile = File.createTempFile("ansible-runner", suffix + "-client.py"); - - try { - Files.copy(AnsibleUtil.class.getClassLoader().getResourceAsStream("vault-client.py"), - tempInternalVaultFile.toPath(), - StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new IOException("Failed to copy vault-client.py", e); - } - - Set perms = PosixFilePermissions.fromString("rwxr-xr-x"); - Files.setPosixFilePermissions(tempInternalVaultFile.toPath(), perms); - - return tempInternalVaultFile; - } - - - public static boolean checkAnsibleVault() { - List procArgs = new ArrayList<>(); - procArgs.add("ansible-vault"); - procArgs.add("--version"); - - Process proc = null; - - try { - proc = ProcessExecutor.builder().procArgs(procArgs) - .redirectErrorStream(true) - .build().run(); - - int exitCode = proc.waitFor(); - return exitCode == 0; - } catch (IOException | InterruptedException e) { - System.err.println("Failed to check ansible-vault: " + e.getMessage()); - return false; - }finally { - if (proc != null) { - proc.destroy(); - } - } - } } diff --git a/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy new file mode 100644 index 00000000..3f26cc1a --- /dev/null +++ b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy @@ -0,0 +1,259 @@ +package com.rundeck.plugins.ansible.ansible + +import com.rundeck.plugins.ansible.util.ProcessExecutor +import spock.lang.Specification + +import java.nio.file.Path + +class AnsibleRunnerSpec extends Specification{ + + def setup(){ + + } + + def "wrong extra vars format"() { + given: + String playbook = "test" + String privateKey = "privateKey" + String extraVars = "123dxxx" + + def runner = AnsibleRunner.playbookInline(playbook) + runner.encryptExtraVars(true) + runner.sshPrivateKey(privateKey) + runner.extraVars(extraVars) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + } + + + runner.processExecutorBuilder(processBuilder) + runner.ansibleVault(ansibleVault) + + when: + runner.build().run() + + then: + def e = thrown(Exception) + e.message.contains("cannot parse extra var values") + + } + + + def "test encrypt extra vars"() { + + given: + + String playbook = "test" + String privateKey = "privateKey" + String extraVars = "test: 123\ntest2: 456" + + + def runnerBuilder = AnsibleRunner.builder() + runnerBuilder.type(AnsibleRunner.AnsibleCommand.PlaybookPath) + runnerBuilder.playbook(playbook) + runnerBuilder.encryptExtraVars(true) + runnerBuilder.sshPrivateKey(privateKey) + runnerBuilder.extraVars(extraVars) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + } + + runnerBuilder.processExecutorBuilder(processBuilder) + runnerBuilder.ansibleVault(ansibleVault) + + when: + def result = runnerBuilder.build().run() + + then: + + 2 * ansibleVault.encryptVariable(_,_) >> "!vault | value" + 1* processBuilder.procArgs(_) >> { args -> + def cmd = args[0] + assert cmd.contains("--vault-id") + assert cmd.contains("internal-encrypt@" + ansibleVault.getVaultPasswordScriptFile().absolutePath) + } + result == 0 + } + + + def "test clean temporary directory"(){ + given: + def tmpDirectory = File.createTempDir("ansible-runner-test-", "tmp") + String playbook = "test" + String privateKey = "privateKey" + String extraVars = "test: 123\ntest2: 456" + + def runner = AnsibleRunner.playbookInline(playbook) + runner.encryptExtraVars(true) + runner.tempDirectory(Path.of(tmpDirectory.absolutePath)) + runner.sshPrivateKey(privateKey) + runner.extraVars(extraVars) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + encryptVariable(_,_) >> { throw new Exception("Error encrypting value") } + } + + runner.processExecutorBuilder(processBuilder) + runner.ansibleVault(ansibleVault) + + when: + runner.build().run() + + then: + def e = thrown(Exception) + e.message.contains("cannot parse extra var values") + + !tmpDirectory.exists() + } + + def "test clean temporary files when a exception is trigger"(){ + given: + + String playbook = "test" + String privateKey = "privateKey" + String extraVars = "test: 123\ntest2: 456" + + def runnerBuilder = AnsibleRunner.playbookInline(playbook) + runnerBuilder.encryptExtraVars(true) + runnerBuilder.sshPrivateKey(privateKey) + runnerBuilder.extraVars(extraVars) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + encryptVariable(_,_) >> { throw new Exception("Error encrypting value") } + } + + runnerBuilder.processExecutorBuilder(processBuilder) + runnerBuilder.ansibleVault(ansibleVault) + + when: + AnsibleRunner runner = runnerBuilder.build() + runner.run() + + then: + def e = thrown(Exception) + e.message.contains("cannot parse extra var values") + + !runner.getTempPlaybook().exists() + + + } + + def "test clean temporary files when process finished"(){ + given: + + String playbook = "test" + String privateKey = "privateKey" + String extraVars = "test: 123\ntest2: 456" + + def runnerBuilder = AnsibleRunner.playbookInline(playbook) + runnerBuilder.encryptExtraVars(true) + runnerBuilder.sshPrivateKey(privateKey) + runnerBuilder.extraVars(extraVars) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + } + + runnerBuilder.processExecutorBuilder(processBuilder) + runnerBuilder.ansibleVault(ansibleVault) + + when: + AnsibleRunner runner = runnerBuilder.build() + def result = runner.run() + + then: + 2 * ansibleVault.encryptVariable(_,_) >> "!vault | value" + result == 0 + !runner.getTempPlaybook().exists() + !runner.getTempPkFile().exists() + !runner.getTempVarsFile().exists() + + + } +} From 899ddee8cbbbcdaaa3c8a328741d6fc07bf63969 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 13 Mar 2024 13:00:33 -0300 Subject: [PATCH 11/23] add more tests --- .../ansible/ansible/AnsibleRunner.java | 10 +- .../ansible/ansible/AnsibleRunnerSpec.groovy | 95 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) 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 7ecf87cc..47b0c93a 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -289,7 +289,7 @@ public static AnsibleRunner buildAnsibleRunner(AnsibleRunnerContextBuilder conte File tempInternalVaultFile; File tempVaultFile ; File tempSshVarsFile ; - + File tempBecameVarsFile ; public void deleteTempDirectory(Path tempDirectory) throws IOException { Files.walkFileTree(tempDirectory, new SimpleFileVisitor() { @@ -483,8 +483,8 @@ public int run() throws Exception { finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); } - tempSshVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); - procArgs.add("--extra-vars" + "=" + "@" + tempSshVarsFile.getAbsolutePath()); + tempBecameVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); + procArgs.add("--extra-vars" + "=" + "@" + tempBecameVarsFile.getAbsolutePath()); } } @@ -618,6 +618,10 @@ public int run() throws Exception { tempSshVarsFile.deleteOnExit(); } + if (tempBecameVarsFile != null && !tempBecameVarsFile.delete()){ + tempBecameVarsFile.deleteOnExit(); + } + if (tempInternalVaultFile != null && !tempInternalVaultFile.delete()){ tempInternalVaultFile.deleteOnExit(); } diff --git a/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy index 3f26cc1a..57575a65 100644 --- a/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy +++ b/src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy @@ -256,4 +256,99 @@ class AnsibleRunnerSpec extends Specification{ } + + def "test password authentication with encrypted extra vars "(){ + given: + + String playbook = "test" + + def runnerBuilder = AnsibleRunner.playbookInline(playbook) + runnerBuilder.sshPass("123456") + runnerBuilder.sshUser("user") + runnerBuilder.sshUsePassword(true) + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + } + + runnerBuilder.processExecutorBuilder(processBuilder) + runnerBuilder.ansibleVault(ansibleVault) + + when: + AnsibleRunner runner = runnerBuilder.build() + def result = runner.run() + + then: + 1 * ansibleVault.encryptVariable(_,_) >> "!vault | value" + result == 0 + !runner.getTempPlaybook().exists() + !runner.getTempSshVarsFile().exists() + } + + def "test password authentication and became user with encrypted extra vars "(){ + given: + + String playbook = "test" + + def runnerBuilder = AnsibleRunner.playbookInline(playbook) + runnerBuilder.sshPass("123456") + runnerBuilder.sshUser("user") + runnerBuilder.sshUsePassword(true) + runnerBuilder.become(true) + runnerBuilder.becomeUser("root") + runnerBuilder.becomePassword("123") + runnerBuilder.becomeMethod("sudo") + + def process = Mock(Process){ + waitFor() >> 0 + getInputStream()>> new ByteArrayInputStream("".getBytes()) + getOutputStream() >> new ByteArrayOutputStream() + getErrorStream() >> new ByteArrayInputStream("".getBytes()) + } + + def processExecutor = Mock(ProcessExecutor){ + run()>>process + } + + def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder){ + build() >> processExecutor + } + + + def ansibleVault = Mock(AnsibleVault){ + checkAnsibleVault() >> true + getVaultPasswordScriptFile() >> new File("vault-script-client.py") + } + + runnerBuilder.processExecutorBuilder(processBuilder) + runnerBuilder.ansibleVault(ansibleVault) + + when: + AnsibleRunner runner = runnerBuilder.build() + def result = runner.run() + + then: + 2 * ansibleVault.encryptVariable(_,_) >> "!vault | value" + result == 0 + !runner.getTempPlaybook().exists() + !runner.getTempSshVarsFile().exists() + !runner.getTempBecameVarsFile().exists() + } } From 84a905c05053cdc81b1a5e1a594168dc5db78119 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Thu, 14 Mar 2024 22:19:19 -0300 Subject: [PATCH 12/23] add functional test --- .dockerignore | 2 +- .env | 1 - Dockerfile | 48 ------ build.gradle | 6 +- functional-test/build.gradle | 43 +++++ .../functional/BasicIntegrationSpec.groovy | 158 ++++++++++++++++++ .../groovy/functional/util/TestUtil.groovy | 47 ++++++ .../src/test/resources/docker/.env | 1 + .../test/resources/docker/ansible/ansible.cfg | 13 ++ .../resources/docker/ansible}/inventory.ini | 0 .../test/resources/docker/docker-compose.yml | 6 +- .../test/resources/docker}/node/Dockerfile | 0 .../src/test/resources/docker}/node/init.sh | 0 .../test/resources/docker/rundeck/Dockerfile | 42 +++++ .../docker/rundeck}/project.properties | 0 .../files/etc/project.properties | 32 ++++ ...b-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml | 40 +++++ settings.gradle | 1 + 18 files changed, 384 insertions(+), 56 deletions(-) delete mode 100644 .env delete mode 100644 Dockerfile create mode 100644 functional-test/build.gradle create mode 100644 functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy create mode 100644 functional-test/src/test/groovy/functional/util/TestUtil.groovy create mode 100644 functional-test/src/test/resources/docker/.env create mode 100644 functional-test/src/test/resources/docker/ansible/ansible.cfg rename {docker => functional-test/src/test/resources/docker/ansible}/inventory.ini (100%) rename docker-compose.yml => functional-test/src/test/resources/docker/docker-compose.yml (84%) rename {docker => functional-test/src/test/resources/docker}/node/Dockerfile (100%) rename {docker => functional-test/src/test/resources/docker}/node/init.sh (100%) create mode 100644 functional-test/src/test/resources/docker/rundeck/Dockerfile rename {docker => functional-test/src/test/resources/docker/rundeck}/project.properties (100%) create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml diff --git a/.dockerignore b/.dockerignore index 7e938add..3efd64ab 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ * -!docker +!src/test/resources/docker !build/libs diff --git a/.env b/.env deleted file mode 100644 index df71ba26..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -RUNDECK_IMAGE=rundeck/rundeck:4.1.0 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 866d6fd1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -# Ubuntu 16.04 based, runs as rundeck user -# https://hub.docker.com/r/rundeck/rundeck/tags -ARG RUNDECK_IMAGE -FROM ${RUNDECK_IMAGE} -MAINTAINER Rundeck Team - -ENV ANSIBLE_HOST_KEY_CHECKING=false -ENV RDECK_BASE=/home/rundeck -ENV MANPATH=${MANPATH}:${RDECK_BASE}/docs/man -ENV PATH=${PATH}:${RDECK_BASE}/tools/bin -ENV PROJECT_BASE=${RDECK_BASE}/projects/Test-Project - -# mkdir -p /etc/ansible \ -# ${PROJECT_BASE}/acls \ - -# install ansible -# base image already installed: curl, openjdk-8-jdk-headless, ssh-client, sudo, uuid-runtime, wget -# (see https://github.com/rundeck/rundeck/blob/master/docker/ubuntu-base/Dockerfile) -RUN sudo apt-get -y update \ - && sudo apt-get -y --no-install-recommends install ca-certificates python3-pip python3-setuptools \ - python3-venv sshpass zip unzip \ - # https://pypi.org/project/ansible/#history - && sudo -H pip3 install --upgrade pip==20.3.4 \ - && sudo -H pip3 --no-cache-dir install ansible==2.9.22 \ - && sudo rm -rf /var/lib/apt/lists/* \ - && mkdir -p ${PROJECT_BASE}/etc/ \ - && sudo mkdir /etc/ansible - -# install ansible 2.10 in a virtualenv -RUN mkdir -p $HOME/.venv \ - && python3 -m venv $HOME/.venv/ansible-2.10 \ - && source $HOME/.venv/ansible-2.10/bin/activate \ - && pip install --upgrade pip==20.3.4 \ - && pip install ansible==2.10.1 - -# add default project -COPY --chown=rundeck:rundeck docker/project.properties ${PROJECT_BASE}/etc/ -COPY --chown=rundeck:rundeck docker/project.properties ${PROJECT_BASE}/etc/ - -# remove embedded rundeck-ansible-plugin -RUN zip -d rundeck.war WEB-INF/rundeck/plugins/ansible-plugin-* \ - && unzip -C rundeck.war WEB-INF/rundeck/plugins/manifest.properties \ - && sed -i "s/\(.*\)\(ansible-plugin-.*\.jar,\)\(.*\)/\1\3/" WEB-INF/rundeck/plugins/manifest.properties \ - && zip -u rundeck.war WEB-INF/rundeck/plugins/manifest.properties \ - && rm WEB-INF/rundeck/plugins/manifest.properties - -# add locally built ansible plugin -COPY --chown=rundeck:rundeck build/libs/ansible-plugin-*.jar ${RDECK_BASE}/libext/ diff --git a/build.gradle b/build.gradle index ba6376aa..2686b6fe 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ configurations { dependencies { pluginLibs 'com.google.code.gson:gson:2.10.1' implementation('org.rundeck:rundeck-core:5.1.1-20240305') - implementation 'org.codehaus.groovy:groovy-all:3.0.9' + implementation 'org.codehaus.groovy:groovy-all:3.0.15' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.16.1' implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' @@ -58,8 +58,8 @@ dependencies { testCompileOnly 'org.projectlombok:lombok:1.18.30' testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' - testImplementation platform("org.spockframework:spock-bom:2.0-groovy-3.0") - testImplementation "org.spockframework:spock-core" + testImplementation "org.spockframework:spock-core:2.1-groovy-3.0" + } tasks.withType(Test).configureEach { diff --git a/functional-test/build.gradle b/functional-test/build.gradle new file mode 100644 index 00000000..94441c3a --- /dev/null +++ b/functional-test/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'groovy' +} + +repositories { + mavenLocal() + mavenCentral() +} + + +group = 'com.github.rundeck-plugins' + +configurations { + configurations { + instrumentedClasspath { + canBeConsumed = false + } + } +} +dependencies { + testImplementation "org.spockframework:spock-core:2.1-groovy-3.0" + + testImplementation "org.testcontainers:testcontainers:1.17.2" + testImplementation "org.testcontainers:spock:1.17.2" + testImplementation group: 'org.rundeck.api', name: 'rd-api-client', version: '2.0.8' + + +} + +tasks.register('functionalTest', Test) { + useJUnitPlatform() + systemProperty('TEST_IMAGE', "rundeck/rundeck:SNAPSHOT") + systemProperty("COMPOSE_PATH", "docker/compose/oss/docker-compose.yml") + //systemProperty('spock.configuration', 'spock-configs/IncludeAPITestsConfig.groovy') + description = "Run API tests" +} + +tasks.register('copyJar', Copy) { + from("$rootDir/build/libs/"){ + include '**/*.jar' + } + into "$projectDir/src/test/resources/docker/rundeck/plugins" +} \ No newline at end of file diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy new file mode 100644 index 00000000..b7270658 --- /dev/null +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -0,0 +1,158 @@ +package functional + +import functional.util.TestUtil +import okhttp3.RequestBody +import org.rundeck.client.RundeckClient +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.ExecLog +import org.rundeck.client.api.model.ExecOutput +import org.rundeck.client.api.model.ExecutionStateResponse +import org.rundeck.client.api.model.JobRun +import org.rundeck.client.api.model.ProjectItem +import org.rundeck.client.util.Client +import org.testcontainers.containers.DockerComposeContainer +import spock.lang.Shared +import spock.lang.Specification +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.spock.Testcontainers + +import java.time.Duration + +@Testcontainers +class BasicIntegrationSpec extends Specification { + + @Shared + public static DockerComposeContainer rundeckEnvironment = + new DockerComposeContainer(new File("src/test/resources/docker/docker-compose.yml")) + .withExposedService("rundeck", 4440, + Wait.forHttp("/api/41/system/info").forStatusCode(403).withStartupTimeout(Duration.ofMinutes(5)) + ) + + + @Shared + Client client + + static String PROJ_NAME = 'ansible-test' + + static class TestLogger implements Client.Logger { + @Override + void output(String out) { + println(out) + } + + @Override + void warning(String warn) { + System.err.println(warn) + } + + @Override + void error(String err) { + System.err.println(err) + } + } + + def setup() { + rundeckEnvironment.start() + String address = rundeckEnvironment.getServiceHost("rundeck",4440) + Integer port = 4440 + def rdUrl = "http://${address}:${port}/api/41" + System.err.println("rdUrl: $rdUrl") + client = RundeckClient.builder().with { + baseUrl rdUrl + passwordAuth('admin', 'admin') + //tokenAuth 'letmeinplease'// token.token + logger(new TestLogger()) + build() + } + + def projList = client.apiCall(api -> api.listProjects()) + + if (!projList*.name.contains(PROJ_NAME)) { + def project = client.apiCall(api -> api.createProject(new ProjectItem(name: PROJ_NAME))) + } + + //import test project + File projectFile = TestUtil.createArchiveJarFile(PROJ_NAME, new File("src/test/resources/project-import/ansible-test")) + RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) + client.apiCall(api -> + api.importProjectArchive(PROJ_NAME, "preserve", true, true, true, true, true, true, true, [:], body) + ) + + def result = client.apiCall {api-> api.listNodes(PROJ_NAME,".*")} + + while(result.get("ssh-node")==null){ + sleep(2000) + result = client.apiCall {api-> api.listNodes(PROJ_NAME,".*")} + } + } + + def "test simple inline playbook"(){ + when: + + def jobId = "4ecd6b86-b437-4792-a37e-af1fa5a2ca0c" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + } + + ExecutionStateResponse waitForJob(String executionId){ + def finalStatus = [ + 'aborted', + 'failed', + 'succeeded', + 'timedout', + 'other' + ] + + while(true) { + ExecutionStateResponse result=client.apiCall { api-> api.getExecutionState(executionId)} + if (finalStatus.contains(result?.getExecutionState()?.toLowerCase())) { + return result + } else { + sleep (10000) + } + } + + } + + + List getLogs(String executionId){ + def offset = 0 + def maxLines = 1000 + def lastmod = 0 + boolean isCompleted = false + + List logs = [] + + while (!isCompleted){ + ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} + println(result) + isCompleted = result.completed + + offset = result.offset + lastmod = result.lastModified + + logs.addAll(result.entries) + + if(result.unmodified){ + sleep(5000) + }else{ + sleep(2000) + } + } + + + return logs + } +} diff --git a/functional-test/src/test/groovy/functional/util/TestUtil.groovy b/functional-test/src/test/groovy/functional/util/TestUtil.groovy new file mode 100644 index 00000000..99d12bb2 --- /dev/null +++ b/functional-test/src/test/groovy/functional/util/TestUtil.groovy @@ -0,0 +1,47 @@ +package functional.util + +import java.text.SimpleDateFormat +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.jar.Manifest + +class TestUtil { + + static File createArchiveJarFile(String name, File projectArchiveDirectory) { + if(!projectArchiveDirectory.isDirectory()){ + throw new IllegalArgumentException("Must be a directory") + } + //create a project archive from the contents of the directory + def tempFile = File.createTempFile("import-temp-${name}", ".zip") + tempFile.deleteOnExit() + //create Manifest + def manifest = new Manifest() + manifest.mainAttributes.putValue("Manifest-Version", "1.0") + manifest.mainAttributes.putValue("Rundeck-Archive-Project-Name", name) + manifest.mainAttributes.putValue("Rundeck-Archive-Format-Version", "1.0") + manifest.mainAttributes.putValue("Rundeck-Application-Version", "5.0.0") + manifest.mainAttributes.putValue( + "Rundeck-Archive-Export-Date", + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX").format(new Date()) + ) + + tempFile.withOutputStream { os -> + def jos = new JarOutputStream(os, manifest) + + jos.withCloseable { jarOutputStream -> + + projectArchiveDirectory.eachFileRecurse { file -> + def entry = new JarEntry(projectArchiveDirectory.toPath().relativize(file.toPath()).toString()) + jarOutputStream.putNextEntry(entry) + if (file.isFile()) { + file.withInputStream { is -> + jarOutputStream << is + } + } + } + } + } + tempFile + } + +} diff --git a/functional-test/src/test/resources/docker/.env b/functional-test/src/test/resources/docker/.env new file mode 100644 index 00000000..f56f5e67 --- /dev/null +++ b/functional-test/src/test/resources/docker/.env @@ -0,0 +1 @@ +RUNDECK_IMAGE=rundeck/rundeck:5.1.1 diff --git a/functional-test/src/test/resources/docker/ansible/ansible.cfg b/functional-test/src/test/resources/docker/ansible/ansible.cfg new file mode 100644 index 00000000..50d0b535 --- /dev/null +++ b/functional-test/src/test/resources/docker/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = /home/rundeck/ansible/inventory.ini +host_key_checking = False +interpreter_python=/usr/bin/python3 +show_custom_stats = False + +[persistent_connection] +ssh_type = paramiko + +#[ssh_connection] +#ssh_args = -o ControlMaster=no -o ControlPersist=no +#control_path = /dev/null + diff --git a/docker/inventory.ini b/functional-test/src/test/resources/docker/ansible/inventory.ini similarity index 100% rename from docker/inventory.ini rename to functional-test/src/test/resources/docker/ansible/inventory.ini diff --git a/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml similarity index 84% rename from docker-compose.yml rename to functional-test/src/test/resources/docker/docker-compose.yml index 82b762e7..54c4daee 100644 --- a/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -3,7 +3,7 @@ services: ssh-node: build: - context: docker/node + context: node image: rundeck-ssh-node:latest networks: - rundeck @@ -14,7 +14,7 @@ services: rundeck: build: - context: . + context: rundeck args: RUNDECK_IMAGE: ${RUNDECK_IMAGE:-rundeck/rundeck:SNAPSHOT} image: rundeck-ansible-plugin:latest @@ -26,7 +26,7 @@ services: ports: - "4440:4440" volumes: - - ${PWD}/docker/inventory.ini:/home/rundeck/data/inventory.ini:rw + - ${PWD}/ansible:/home/rundeck/ansible:rw - ssh-data:/home/rundeck/.ssh:rw volumes: diff --git a/docker/node/Dockerfile b/functional-test/src/test/resources/docker/node/Dockerfile similarity index 100% rename from docker/node/Dockerfile rename to functional-test/src/test/resources/docker/node/Dockerfile diff --git a/docker/node/init.sh b/functional-test/src/test/resources/docker/node/init.sh similarity index 100% rename from docker/node/init.sh rename to functional-test/src/test/resources/docker/node/init.sh diff --git a/functional-test/src/test/resources/docker/rundeck/Dockerfile b/functional-test/src/test/resources/docker/rundeck/Dockerfile new file mode 100644 index 00000000..8c65840a --- /dev/null +++ b/functional-test/src/test/resources/docker/rundeck/Dockerfile @@ -0,0 +1,42 @@ +# Ubuntu 16.04 based, runs as rundeck user +# https://hub.docker.com/r/rundeck/rundeck/tags +ARG RUNDECK_IMAGE +FROM ${RUNDECK_IMAGE} +MAINTAINER Rundeck Team + +ENV ANSIBLE_HOST_KEY_CHECKING=false +ENV RDECK_BASE=/home/rundeck +ENV MANPATH=${MANPATH}:${RDECK_BASE}/docs/man +ENV PATH=${PATH}:${RDECK_BASE}/tools/bin +ENV PROJECT_BASE=${RDECK_BASE}/projects/Test-Project + +USER root + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl vim unzip + +# install ansible +RUN apt-get -y install sshpass && \ + apt-get -y install python3-pip && \ + apt-get -y install sudo && \ + pip3 install --upgrade pip + +RUN pip3 install ansible + +RUN ln -s /usr/bin/python3 /usr/bin/python + +user rundeck + +# add default project +COPY --chown=rundeck:rundeck project.properties ${PROJECT_BASE}/etc/ + +# remove embedded rundeck-ansible-plugin +#RUN zip -d rundeck.war WEB-INF/rundeck/plugins/ansible-plugin-* \ +# && unzip -C rundeck.war WEB-INF/rundeck/plugins/manifest.properties \ +# && sed -i "s/\(.*\)\(ansible-plugin-.*\.jar,\)\(.*\)/\1\3/" WEB-INF/rundeck/plugins/manifest.properties \ +# && zip -u rundeck.war WEB-INF/rundeck/plugins/manifest.properties \ +# && rm WEB-INF/rundeck/plugins/manifest.properties + +# add locally built ansible plugin +COPY --chown=rundeck:rundeck plugins ${RDECK_BASE}/libext/ diff --git a/docker/project.properties b/functional-test/src/test/resources/docker/rundeck/project.properties similarity index 100% rename from docker/project.properties rename to functional-test/src/test/resources/docker/rundeck/project.properties diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties new file mode 100644 index 00000000..8f39fa19 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties @@ -0,0 +1,32 @@ +#Exported configuration +#Thu Mar 14 17:01:33 GMT 2024 +project.disable.executions=false +project.disable.schedule=false +project.execution.history.cleanup.batch=500 +project.execution.history.cleanup.enabled=false +project.execution.history.cleanup.retention.days=60 +project.execution.history.cleanup.retention.minimum=50 +project.execution.history.cleanup.schedule=0 0 0 1/1 * ? * +project.jobs.gui.groupExpandLevel=1 +project.later.executions.disable.value=0 +project.later.executions.disable=false +project.later.executions.enable.value= +project.later.executions.enable=false +project.later.schedule.disable.value= +project.later.schedule.disable=false +project.later.schedule.enable.value= +project.later.schedule.enable=false +project.name=ansible-test +project.nodeCache.enabled=true +project.nodeCache.firstLoadSynch=true +project.output.allowUnsanitized=false +project.retry-counter=3 +project.ssh-authentication=privateKey +resources.source.1.type=local +resources.source.2.config.ansible-config-file-path=/home/rundeck/ansible/ansible.cfg +resources.source.2.config.ansible-gather-facts=true +resources.source.2.config.ansible-ignore-errors=true +resources.source.2.config.ansible-import-inventory-vars=true +resources.source.2.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory +service.FileCopier.default.provider=sshj-scp +service.NodeExecutor.default.provider=sshj-ssh \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml new file mode 100644 index 00000000..1c8e0672 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml @@ -0,0 +1,40 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + 4ecd6b86-b437-4792-a37e-af1fa5a2ca0c + INFO + simple-inline-playbook + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + 4ecd6b86-b437-4792-a37e-af1fa5a2ca0c + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index cdb60193..b6e60ad4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ rootProject.name = 'ansible-plugin' +include 'functional-test' \ No newline at end of file From 56faa97a26b20c2b98b307e7f655de8f58a1540a Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 15 Mar 2024 14:11:53 -0300 Subject: [PATCH 13/23] add different authentications to the ssh node add new test jobs with other auth methods --- functional-test/.gitignore | 5 + functional-test/build.gradle | 7 +- .../functional/BasicIntegrationSpec.groovy | 115 ++++++++------- .../groovy/functional/RundeckCompose.groovy | 139 ++++++++++++++++++ .../groovy/functional/util/TestUtil.groovy | 21 +++ .../test/resources/docker/ansible/ansible.cfg | 4 +- .../resources/docker/ansible/inventory.ini | 2 +- .../test/resources/docker/docker-compose.yml | 7 +- .../src/test/resources/docker/keys/README.md | 1 + .../src/test/resources/docker/node/init.sh | 27 ++-- .../files/acls/node-acl.aclpolicy | 8 + .../files/etc/project.properties | 3 + ...b-243ba9cb-b95c-4d4b-a569-09935a62397d.xml | 45 ++++++ ...b-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml | 4 + ...b-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml | 44 ++++++ 15 files changed, 356 insertions(+), 76 deletions(-) create mode 100644 functional-test/.gitignore create mode 100644 functional-test/src/test/groovy/functional/RundeckCompose.groovy create mode 100644 functional-test/src/test/resources/docker/keys/README.md create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/acls/node-acl.aclpolicy create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml diff --git a/functional-test/.gitignore b/functional-test/.gitignore new file mode 100644 index 00000000..377c61ca --- /dev/null +++ b/functional-test/.gitignore @@ -0,0 +1,5 @@ +src/test/resources/docker/keys/id_rsa +src/test/resources/docker/keys/id_rsa.pub +src/test/resources/docker/keys/id_rsa_passphrase +src/test/resources/docker/keys/id_rsa_passphrase.pub +src/test/resources/docker/rundeck/plugins \ No newline at end of file diff --git a/functional-test/build.gradle b/functional-test/build.gradle index 94441c3a..43067eaa 100644 --- a/functional-test/build.gradle +++ b/functional-test/build.gradle @@ -24,15 +24,14 @@ dependencies { testImplementation "org.testcontainers:spock:1.17.2" testImplementation group: 'org.rundeck.api', name: 'rd-api-client', version: '2.0.8' + testImplementation group: 'com.jcraft', name: 'jsch', version: '0.1.55' } tasks.register('functionalTest', Test) { useJUnitPlatform() - systemProperty('TEST_IMAGE', "rundeck/rundeck:SNAPSHOT") - systemProperty("COMPOSE_PATH", "docker/compose/oss/docker-compose.yml") - //systemProperty('spock.configuration', 'spock-configs/IncludeAPITestsConfig.groovy') - description = "Run API tests" + systemProperty('RUNDECK_TEST_IMAGE', "rundeck/rundeck:5.1.1") + description = "Run Ansible integration tests" } tasks.register('copyJar', Copy) { diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index b7270658..db444b62 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -1,95 +1,89 @@ package functional import functional.util.TestUtil -import okhttp3.RequestBody -import org.rundeck.client.RundeckClient import org.rundeck.client.api.RundeckApi import org.rundeck.client.api.model.ExecLog import org.rundeck.client.api.model.ExecOutput import org.rundeck.client.api.model.ExecutionStateResponse import org.rundeck.client.api.model.JobRun -import org.rundeck.client.api.model.ProjectItem import org.rundeck.client.util.Client -import org.testcontainers.containers.DockerComposeContainer import spock.lang.Shared import spock.lang.Specification -import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.spock.Testcontainers -import java.time.Duration @Testcontainers class BasicIntegrationSpec extends Specification { @Shared - public static DockerComposeContainer rundeckEnvironment = - new DockerComposeContainer(new File("src/test/resources/docker/docker-compose.yml")) - .withExposedService("rundeck", 4440, - Wait.forHttp("/api/41/system/info").forStatusCode(403).withStartupTimeout(Duration.ofMinutes(5)) - ) - + public static RundeckCompose rundeckEnvironment = new RundeckCompose(new File("src/test/resources/docker/docker-compose.yml").toURI()) @Shared Client client static String PROJ_NAME = 'ansible-test' - static class TestLogger implements Client.Logger { - @Override - void output(String out) { - println(out) - } + def setupSpec() { + rundeckEnvironment.startCompose() + client = rundeckEnvironment.configureRundeck(PROJ_NAME) + } - @Override - void warning(String warn) { - System.err.println(warn) - } + def "test simple inline playbook"(){ + when: - @Override - void error(String err) { - System.err.println(err) - } + def jobId = "4ecd6b86-b437-4792-a37e-af1fa5a2ca0c" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 } - def setup() { - rundeckEnvironment.start() - String address = rundeckEnvironment.getServiceHost("rundeck",4440) - Integer port = 4440 - def rdUrl = "http://${address}:${port}/api/41" - System.err.println("rdUrl: $rdUrl") - client = RundeckClient.builder().with { - baseUrl rdUrl - passwordAuth('admin', 'admin') - //tokenAuth 'letmeinplease'// token.token - logger(new TestLogger()) - build() - } + def "test simple inline playbook password authentication"(){ + when: - def projList = client.apiCall(api -> api.listProjects()) + def jobId = "d8e88ac2-a310-4461-be54-fd38cdac5e11" - if (!projList*.name.contains(PROJ_NAME)) { - def project = client.apiCall(api -> api.createProject(new ProjectItem(name: PROJ_NAME))) - } + JobRun request = new JobRun() + request.loglevel = 'INFO' - //import test project - File projectFile = TestUtil.createArchiveJarFile(PROJ_NAME, new File("src/test/resources/project-import/ansible-test")) - RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) - client.apiCall(api -> - api.importProjectArchive(PROJ_NAME, "preserve", true, true, true, true, true, true, true, [:], body) - ) + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id - def result = client.apiCall {api-> api.listNodes(PROJ_NAME,".*")} + def executionState = waitForJob(executionId) - while(result.get("ssh-node")==null){ - sleep(2000) - result = client.apiCall {api-> api.listNodes(PROJ_NAME,".*")} - } + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 } - def "test simple inline playbook"(){ + def "test simple inline playbook private-key with passphrase authentication"(){ when: - def jobId = "4ecd6b86-b437-4792-a37e-af1fa5a2ca0c" + def jobId = "243ba9cb-b95c-4d4b-a569-09935a62397d" JobRun request = new JobRun() request.loglevel = 'INFO' @@ -100,10 +94,16 @@ class BasicIntegrationSpec extends Specification { def executionState = waitForJob(executionId) def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) then: executionState!=null executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 } ExecutionStateResponse waitForJob(String executionId){ @@ -155,4 +155,9 @@ class BasicIntegrationSpec extends Specification { return logs } + + + + + } diff --git a/functional-test/src/test/groovy/functional/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/RundeckCompose.groovy new file mode 100644 index 00000000..2865a2ef --- /dev/null +++ b/functional-test/src/test/groovy/functional/RundeckCompose.groovy @@ -0,0 +1,139 @@ +package functional + +import com.jcraft.jsch.JSch +import com.jcraft.jsch.KeyPair +import functional.util.TestUtil +import okhttp3.RequestBody +import org.rundeck.client.RundeckClient +import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.ProjectItem +import org.rundeck.client.util.Client +import org.testcontainers.containers.DockerComposeContainer +import org.testcontainers.containers.wait.strategy.Wait + +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission +import java.time.Duration + +class RundeckCompose extends DockerComposeContainer { + + public static final String RUNDECK_IMAGE = System.getenv("RUNDECK_TEST_IMAGE") ?: System.getProperty("RUNDECK_TEST_IMAGE") + public static final String NODE_USER_PASSWORD = "testpassword123" + public static final String NODE_KEY_PASSPHRASE = "testpassphrase123" + + RundeckCompose(URI composeFilePath) { + super(new File(composeFilePath)) + + withExposedService("rundeck", 4440, + Wait.forHttp("/api/41/system/info").forStatusCode(403).withStartupTimeout(Duration.ofMinutes(5)) + ) + withEnv("RUNDECK_IMAGE", RUNDECK_IMAGE) + withEnv("NODE_USER_PASSWORD", NODE_USER_PASSWORD) + } + + + def startCompose() { + //generate SSH private key for node authentication + File keyPath = new File("src/test/resources/docker/keys") + generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa") + generatePrivateKey(keyPath.getAbsolutePath(),"id_rsa_passphrase", NODE_KEY_PASSPHRASE) + + start() + } + + + Client configureRundeck(String projectName){ + + //configure rundeck api + String address = getServiceHost("rundeck",4440) + Integer port = 4440 + def rdUrl = "http://${address}:${port}/api/41" + System.err.println("rdUrl: $rdUrl") + Client client = RundeckClient.builder().with { + baseUrl rdUrl + passwordAuth('admin', 'admin') + logger(new TestLogger()) + build() + } + //add private key + RequestBody requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa"), Client.MEDIA_TYPE_OCTET_STREAM) + def keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.key", requestBody)} + + //add private key with passphrase + requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa_passphrase"), Client.MEDIA_TYPE_OCTET_STREAM) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.key", requestBody)} + + //add passphrase + requestBody = RequestBody.create(NODE_KEY_PASSPHRASE.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node-passphrase.pass", requestBody)} + + //add node user ssh-password + requestBody = RequestBody.create(NODE_USER_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.pass", requestBody)} + + //create project + def projList = client.apiCall(api -> api.listProjects()) + + if (!projList*.name.contains(projectName)) { + def project = client.apiCall(api -> api.createProject(new ProjectItem(name: projectName))) + } + + //import project + File projectFile = TestUtil.createArchiveJarFile(projectName, new File("src/test/resources/project-import/ansible-test")) + RequestBody body = RequestBody.create(projectFile, Client.MEDIA_TYPE_ZIP) + client.apiCall(api -> + api.importProjectArchive(projectName, "preserve", true, true, true, true, true, true, true, [:], body) + ) + + //wait for node to be available + def result = client.apiCall {api-> api.listNodes(projectName,".*")} + def count =0 + + while(result.get("ssh-node")==null && count<5){ + sleep(2000) + result = client.apiCall {api-> api.listNodes(projectName,".*")} + count++ + } + + return client + } + + static def generatePrivateKey(String filePath, String keyName, String passphrase = null){ + JSch jsch=new JSch() + KeyPair keyPair=KeyPair.genKeyPair(jsch, KeyPair.RSA) + if(passphrase){ + keyPair.writePrivateKey(filePath + File.separator + keyName, passphrase.getBytes()) + }else{ + keyPair.writePrivateKey(filePath + File.separator + keyName) + } + + keyPair.writePublicKey(filePath + File.separator + keyName + ".pub", "test private key") + + keyPair.dispose() + + File privateKey = new File(filePath + File.separator + keyName) + Set perms = new HashSet() + perms.add(PosixFilePermission.OWNER_READ) + perms.add(PosixFilePermission.OWNER_WRITE) + Files.setPosixFilePermissions(privateKey.toPath(), perms) + } + + + static class TestLogger implements Client.Logger { + @Override + void output(String out) { + println(out) + } + + @Override + void warning(String warn) { + System.err.println(warn) + } + + @Override + void error(String err) { + System.err.println(err) + } + } + +} diff --git a/functional-test/src/test/groovy/functional/util/TestUtil.groovy b/functional-test/src/test/groovy/functional/util/TestUtil.groovy index 99d12bb2..63904f0e 100644 --- a/functional-test/src/test/groovy/functional/util/TestUtil.groovy +++ b/functional-test/src/test/groovy/functional/util/TestUtil.groovy @@ -1,9 +1,12 @@ package functional.util +import org.rundeck.client.api.model.ExecLog + import java.text.SimpleDateFormat import java.util.jar.JarEntry import java.util.jar.JarOutputStream import java.util.jar.Manifest +import java.util.stream.Collectors class TestUtil { @@ -44,4 +47,22 @@ class TestUtil { tempFile } + static Map getAnsibleNodeResult(List logs){ + String nodeResumeLog = null + + for(ExecLog execLog: logs){ + if(execLog.log.startsWith("ssh-node") && execLog.log.contains("ok=")){ + nodeResumeLog = execLog.log + } + } + + if(nodeResumeLog==null){ + return null + } + + List listNodeResultStatus = nodeResumeLog.split("\\s",-1).findAll{!it.isEmpty() && it.contains("=")} + + return listNodeResultStatus.stream().collect(Collectors.toMap(s -> s.toString().split("=")[0], s -> Integer.valueOf(s.toString().split("=")[1]))) + } + } diff --git a/functional-test/src/test/resources/docker/ansible/ansible.cfg b/functional-test/src/test/resources/docker/ansible/ansible.cfg index 50d0b535..ed6e0d96 100644 --- a/functional-test/src/test/resources/docker/ansible/ansible.cfg +++ b/functional-test/src/test/resources/docker/ansible/ansible.cfg @@ -7,7 +7,7 @@ show_custom_stats = False [persistent_connection] ssh_type = paramiko -#[ssh_connection] -#ssh_args = -o ControlMaster=no -o ControlPersist=no +[ssh_connection] +ssh_args = -o ControlMaster=no -o ControlPersist=no #control_path = /dev/null diff --git a/functional-test/src/test/resources/docker/ansible/inventory.ini b/functional-test/src/test/resources/docker/ansible/inventory.ini index 9806e535..10bf2572 100644 --- a/functional-test/src/test/resources/docker/ansible/inventory.ini +++ b/functional-test/src/test/resources/docker/ansible/inventory.ini @@ -1,2 +1,2 @@ [servers] -ssh-node ansible_host=ssh-node ansible_user=agent +ssh-node ansible_host=ssh-node diff --git a/functional-test/src/test/resources/docker/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml index 54c4daee..8384b4fc 100644 --- a/functional-test/src/test/resources/docker/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -4,13 +4,14 @@ services: ssh-node: build: context: node - image: rundeck-ssh-node:latest + environment: + NODE_USER_PASSWORD: ${NODE_USER_PASSWORD:-rundeck} networks: - rundeck ports: - "2222:22" volumes: - - ssh-data:/configuration:rw + - ${PWD}/keys:/configuration:rw rundeck: build: @@ -27,7 +28,7 @@ services: - "4440:4440" volumes: - ${PWD}/ansible:/home/rundeck/ansible:rw - - ssh-data:/home/rundeck/.ssh:rw + #- ssh-data:/home/rundeck/.ssh:rw volumes: rundeck-data: diff --git a/functional-test/src/test/resources/docker/keys/README.md b/functional-test/src/test/resources/docker/keys/README.md new file mode 100644 index 00000000..8ad04db4 --- /dev/null +++ b/functional-test/src/test/resources/docker/keys/README.md @@ -0,0 +1 @@ +## ssh key directory \ No newline at end of file diff --git a/functional-test/src/test/resources/docker/node/init.sh b/functional-test/src/test/resources/docker/node/init.sh index a6f894b6..76bdc78a 100644 --- a/functional-test/src/test/resources/docker/node/init.sh +++ b/functional-test/src/test/resources/docker/node/init.sh @@ -1,16 +1,21 @@ #!/bin/bash -rm /configuration/id_rsa -rm /configuration/id_rsa.pub -rm /configuration/authorized_keys -rm /configuration/known_hosts +USERNAME=rundeck +HOME=/home/rundeck -ssh-keygen -q -t rsa -N '' -f /configuration/id_rsa +adduser --home $HOME --disabled-password --gecos "rundeck" $USERNAME -touch /configuration/known_hosts -touch /configuration/authorized_keys +mkdir $HOME/.ssh +chmod 755 $HOME/.ssh +chown -R $USERNAME $HOME/.ssh +echo "rundeck:$NODE_USER_PASSWORD"|chpasswd -chown agent:root /configuration/id_rsa -chown agent:root /configuration/id_rsa.pub -chown agent:root /configuration/known_hosts -chown agent:root /configuration/authorized_keys \ No newline at end of file +# authorize SSH connection with root account +sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication yes/g" /etc/ssh/sshd_config + +# authorize SSH connection with rundeck account +mkdir -p /home/rundeck/.ssh +chown -R "$USERNAME" $HOME/.ssh + +cat /configuration/id_rsa.pub > /home/rundeck/.ssh/authorized_keys +cat /configuration/id_rsa_passphrase.pub >> /home/rundeck/.ssh/authorized_keys diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/acls/node-acl.aclpolicy b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/acls/node-acl.aclpolicy new file mode 100644 index 00000000..1dbf268e --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/acls/node-acl.aclpolicy @@ -0,0 +1,8 @@ +by: + urn: project:ansible-test +for: + storage: + - match: + path: 'keys/project/ansible-test/.*' + allow: [read] +description: Allow access to key storage \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties index 8f39fa19..eff401bb 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties @@ -27,6 +27,9 @@ resources.source.2.config.ansible-config-file-path=/home/rundeck/ansible/ansible resources.source.2.config.ansible-gather-facts=true resources.source.2.config.ansible-ignore-errors=true resources.source.2.config.ansible-import-inventory-vars=true +resources.source.2.config.ansible-ssh-auth-type=privateKey +resources.source.2.config.ansible-ssh-key-storage-path=keys/project/ansible-test/ssh-node.key +resources.source.2.config.ansible-ssh-user=rundeck resources.source.2.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory service.FileCopier.default.provider=sshj-scp service.NodeExecutor.default.provider=sshj-ssh \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml new file mode 100644 index 00000000..f2e02aaa --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml @@ -0,0 +1,45 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + 243ba9cb-b95c-4d4b-a569-09935a62397d + INFO + simple-inline-playbook-key-passphrase + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + + 243ba9cb-b95c-4d4b-a569-09935a62397d + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml index 1c8e0672..ce67f923 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml @@ -27,10 +27,14 @@ + + + + diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml new file mode 100644 index 00000000..8563cb27 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml @@ -0,0 +1,44 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + d8e88ac2-a310-4461-be54-fd38cdac5e11 + INFO + simple-inline-playbook-password + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + d8e88ac2-a310-4461-be54-fd38cdac5e11 + + \ No newline at end of file From e7132664d6519aa16450ff72fef9aa78bb9ce2c3 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 15 Mar 2024 14:23:58 -0300 Subject: [PATCH 14/23] add functional test circleci --- .github/workflows/gradle.yml | 4 ++++ functional-test/.gitignore | 2 +- functional-test/build.gradle | 2 +- functional-test/src/test/resources/docker/.env | 1 - .../src/test/resources/docker/rundeck/plugins/README.md | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) delete mode 100644 functional-test/src/test/resources/docker/.env create mode 100644 functional-test/src/test/resources/docker/rundeck/plugins/README.md diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7c3694b5..78c529c2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,6 +22,10 @@ jobs: run: chmod +x gradlew - name: Build with Gradle run: ./gradlew build + - name: Copy Artifact for integration test + run: ./gradlew :functional-test:copyPluginArtifact + - name: Run integration Test + run: ./gradlew :functional-test:functionalTest - name: Get Release Version id: get_version run: VERSION=$(./gradlew currentVersion -q -Prelease.quiet) && echo ::set-output name=VERSION::$VERSION diff --git a/functional-test/.gitignore b/functional-test/.gitignore index 377c61ca..9bb9eee8 100644 --- a/functional-test/.gitignore +++ b/functional-test/.gitignore @@ -2,4 +2,4 @@ src/test/resources/docker/keys/id_rsa src/test/resources/docker/keys/id_rsa.pub src/test/resources/docker/keys/id_rsa_passphrase src/test/resources/docker/keys/id_rsa_passphrase.pub -src/test/resources/docker/rundeck/plugins \ No newline at end of file +src/test/resources/docker/rundeck/plugins/*.jar \ No newline at end of file diff --git a/functional-test/build.gradle b/functional-test/build.gradle index 43067eaa..054f0ec2 100644 --- a/functional-test/build.gradle +++ b/functional-test/build.gradle @@ -34,7 +34,7 @@ tasks.register('functionalTest', Test) { description = "Run Ansible integration tests" } -tasks.register('copyJar', Copy) { +tasks.register('copyPluginArtifact', Copy) { from("$rootDir/build/libs/"){ include '**/*.jar' } diff --git a/functional-test/src/test/resources/docker/.env b/functional-test/src/test/resources/docker/.env deleted file mode 100644 index f56f5e67..00000000 --- a/functional-test/src/test/resources/docker/.env +++ /dev/null @@ -1 +0,0 @@ -RUNDECK_IMAGE=rundeck/rundeck:5.1.1 diff --git a/functional-test/src/test/resources/docker/rundeck/plugins/README.md b/functional-test/src/test/resources/docker/rundeck/plugins/README.md new file mode 100644 index 00000000..af21a484 --- /dev/null +++ b/functional-test/src/test/resources/docker/rundeck/plugins/README.md @@ -0,0 +1 @@ +## plugin folder \ No newline at end of file From 64d39f2fc97eae6313f8c3c7ee842eac3b742ce2 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Fri, 15 Mar 2024 20:51:58 -0300 Subject: [PATCH 15/23] more functional test --- .../functional/BasicIntegrationSpec.groovy | 141 +++++++++++++++++- .../groovy/functional/RundeckCompose.groovy | 5 + .../test/resources/docker/ansible/ansible.cfg | 2 + .../resources/docker/ansible/playbook.yaml | 15 ++ .../test/resources/docker/docker-compose.yml | 1 - .../src/test/resources/docker/node/init.sh | 1 + .../test/resources/docker/rundeck/Dockerfile | 3 - .../docker/rundeck/project.properties | 16 -- .../files/etc/project.properties | 23 ++- ...b-03f2ed76-f986-4ad5-a9ea-640d326d4b73.xml | 50 +++++++ ...b-243ba9cb-b95c-4d4b-a569-09935a62397d.xml | 1 - ...b-284e1a6e-bae0-4778-a838-50647fb340e3.xml | 50 +++++++ ...b-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml | 1 - ...b-6a49e380-bfdf-4bfd-b075-a4321ff78836.xml | 46 ++++++ ...b-6b309548-bcc9-40d8-8c79-bfc0d1f1e49c.xml | 39 +++++ ...b-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml | 45 ++++++ ...b-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml | 1 - ...b-f302db98-8737-4b87-8832-f830622ccf85.xml | 51 +++++++ 18 files changed, 456 insertions(+), 35 deletions(-) create mode 100644 functional-test/src/test/resources/docker/ansible/playbook.yaml delete mode 100644 functional-test/src/test/resources/docker/rundeck/project.properties create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-03f2ed76-f986-4ad5-a9ea-640d326d4b73.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-284e1a6e-bae0-4778-a838-50647fb340e3.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6a49e380-bfdf-4bfd-b075-a4321ff78836.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6b309548-bcc9-40d8-8c79-bfc0d1f1e49c.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml create mode 100644 functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-f302db98-8737-4b87-8832-f830622ccf85.xml diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index db444b62..1e60feb1 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -60,7 +60,7 @@ class BasicIntegrationSpec extends Specification { def jobId = "d8e88ac2-a310-4461-be54-fd38cdac5e11" JobRun request = new JobRun() - request.loglevel = 'INFO' + request.loglevel = 'DEBUG' def result = client.apiCall {api-> api.runJob(jobId, request)} def executionId = result.id @@ -78,6 +78,7 @@ class BasicIntegrationSpec extends Specification { ansibleNodeExecutionStatus.get("failed")==0 ansibleNodeExecutionStatus.get("skipped")==0 ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("encryptVariable ansible_ssh_password:")}.size() == 1 } def "test simple inline playbook private-key with passphrase authentication"(){ @@ -106,6 +107,141 @@ class BasicIntegrationSpec extends Specification { ansibleNodeExecutionStatus.get("ignored")==0 } + def "test inline playbook secure option are not added to the env vars"(){ + when: + + def jobId = "f302db98-8737-4b87-8832-f830622ccf85" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("username='value123'")}.size() == 1 + logs.findAll {it.log.contains("password=''")}.size() == 1 + + + } + + def "test inline playbook encrypt env vars"(){ + when: + + def jobId = "284e1a6e-bae0-4778-a838-50647fb340e3" + + JobRun request = new JobRun() + request.loglevel = 'DEBUG' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("encryptVariable password")}.size() == 1 + logs.findAll {it.log.contains("encryptVariable username")}.size() == 1 + logs.findAll {it.log.contains("\"msg\": \"rundeck\"")}.size() == 1 + logs.findAll {it.log.contains("\"msg\": \"demo\"")}.size() == 1 + + } + + def "test simple file playbook"(){ + when: + + def jobId = "03f2ed76-f986-4ad5-a9ea-640d326d4b73" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + } + + def "test inline playbook became sudo authentication"(){ + when: + + def jobId = "6a49e380-bfdf-4bfd-b075-a4321ff78836" + + JobRun request = new JobRun() + request.loglevel = 'DEBUG' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("encryptVariable ansible_become_password")}.size() == 1 + logs.findAll {it.log.contains("\"msg\": \"root\"")}.size() == 1 + } + + def "test simple script ansible node-executor file-copier"(){ + when: + + def jobId = "6b309548-bcc9-40d8-8c79-bfc0d1f1e49c" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + } + ExecutionStateResponse waitForJob(String executionId){ def finalStatus = [ 'aborted', @@ -137,9 +273,7 @@ class BasicIntegrationSpec extends Specification { while (!isCompleted){ ExecOutput result = client.apiCall { api -> api.getOutput(executionId, offset,lastmod, maxLines)} - println(result) isCompleted = result.completed - offset = result.offset lastmod = result.lastModified @@ -152,7 +286,6 @@ class BasicIntegrationSpec extends Specification { } } - return logs } diff --git a/functional-test/src/test/groovy/functional/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/RundeckCompose.groovy index 2865a2ef..337f299f 100644 --- a/functional-test/src/test/groovy/functional/RundeckCompose.groovy +++ b/functional-test/src/test/groovy/functional/RundeckCompose.groovy @@ -20,6 +20,7 @@ class RundeckCompose extends DockerComposeContainer { public static final String RUNDECK_IMAGE = System.getenv("RUNDECK_TEST_IMAGE") ?: System.getProperty("RUNDECK_TEST_IMAGE") public static final String NODE_USER_PASSWORD = "testpassword123" public static final String NODE_KEY_PASSPHRASE = "testpassphrase123" + public static final String USER_VAULT_PASSWORD = "vault123" RundeckCompose(URI composeFilePath) { super(new File(composeFilePath)) @@ -71,6 +72,10 @@ class RundeckCompose extends DockerComposeContainer { requestBody = RequestBody.create(NODE_USER_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/ssh-node.pass", requestBody)} + //user vault password + requestBody = RequestBody.create(USER_VAULT_PASSWORD.getBytes(), Client.MEDIA_TYPE_X_RUNDECK_PASSWORD) + keyResult = client.apiCall {api-> api.createKeyStorage("project/$projectName/vault-user.pass", requestBody)} + //create project def projList = client.apiCall(api -> api.listProjects()) diff --git a/functional-test/src/test/resources/docker/ansible/ansible.cfg b/functional-test/src/test/resources/docker/ansible/ansible.cfg index ed6e0d96..c8c55b54 100644 --- a/functional-test/src/test/resources/docker/ansible/ansible.cfg +++ b/functional-test/src/test/resources/docker/ansible/ansible.cfg @@ -3,6 +3,8 @@ inventory = /home/rundeck/ansible/inventory.ini host_key_checking = False interpreter_python=/usr/bin/python3 show_custom_stats = False +display_ok_hosts = True +callback_format_pretty = True [persistent_connection] ssh_type = paramiko diff --git a/functional-test/src/test/resources/docker/ansible/playbook.yaml b/functional-test/src/test/resources/docker/ansible/playbook.yaml new file mode 100644 index 00000000..0a6a64f4 --- /dev/null +++ b/functional-test/src/test/resources/docker/ansible/playbook.yaml @@ -0,0 +1,15 @@ +--- +- hosts: all + tasks: + - name: Get Disk Space + shell: "df -h && date && env" + register: pid + + - debug: var=pid.stdout_lines + - debug: msg="{{ username }}" + - debug: msg="{{ test }}" + + + + + diff --git a/functional-test/src/test/resources/docker/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml index 8384b4fc..044da9c3 100644 --- a/functional-test/src/test/resources/docker/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -28,7 +28,6 @@ services: - "4440:4440" volumes: - ${PWD}/ansible:/home/rundeck/ansible:rw - #- ssh-data:/home/rundeck/.ssh:rw volumes: rundeck-data: diff --git a/functional-test/src/test/resources/docker/node/init.sh b/functional-test/src/test/resources/docker/node/init.sh index 76bdc78a..70e6fc38 100644 --- a/functional-test/src/test/resources/docker/node/init.sh +++ b/functional-test/src/test/resources/docker/node/init.sh @@ -9,6 +9,7 @@ mkdir $HOME/.ssh chmod 755 $HOME/.ssh chown -R $USERNAME $HOME/.ssh echo "rundeck:$NODE_USER_PASSWORD"|chpasswd +echo 'rundeck ALL=(ALL:ALL) ALL' >> /etc/sudoers # authorize SSH connection with root account sed -i "s/.*PasswordAuthentication.*/PasswordAuthentication yes/g" /etc/ssh/sshd_config diff --git a/functional-test/src/test/resources/docker/rundeck/Dockerfile b/functional-test/src/test/resources/docker/rundeck/Dockerfile index 8c65840a..0df138a1 100644 --- a/functional-test/src/test/resources/docker/rundeck/Dockerfile +++ b/functional-test/src/test/resources/docker/rundeck/Dockerfile @@ -28,9 +28,6 @@ RUN ln -s /usr/bin/python3 /usr/bin/python user rundeck -# add default project -COPY --chown=rundeck:rundeck project.properties ${PROJECT_BASE}/etc/ - # remove embedded rundeck-ansible-plugin #RUN zip -d rundeck.war WEB-INF/rundeck/plugins/ansible-plugin-* \ # && unzip -C rundeck.war WEB-INF/rundeck/plugins/manifest.properties \ diff --git a/functional-test/src/test/resources/docker/rundeck/project.properties b/functional-test/src/test/resources/docker/rundeck/project.properties deleted file mode 100644 index c7666de8..00000000 --- a/functional-test/src/test/resources/docker/rundeck/project.properties +++ /dev/null @@ -1,16 +0,0 @@ -#Project Test-Project configuration, generated -#Sat May 13 17:20:30 GMT 2017 -project.ansible-executable=/bin/bash -project.name=Test-Project -project.ssh-authentication=privateKey -project.ssh-keypath=/home/rundeck/.ssh/id_rsa -resources.source.1.config.ansible-become=false -resources.source.1.config.ansible-gather-facts=true -resources.source.1.config.ansible-ignore-errors=true -resources.source.1.config.ansible-inventory=/home/rundeck/data/inventory.ini -resources.source.1.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory -service.FileCopier.default.provider=com.batix.rundeck.plugins.AnsibleFileCopier -service.NodeExecutor.default.provider=com.batix.rundeck.plugins.AnsibleNodeExecutor - -# TODO remove once fixed -project.ansible-inventory=/home/rundeck/data/inventory.ini diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties index eff401bb..6268c65c 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/files/etc/project.properties @@ -1,5 +1,15 @@ -#Exported configuration -#Thu Mar 14 17:01:33 GMT 2024 +#Fri Mar 15 23:29:31 GMT 2024 +#edit below +ansible-ssh-auth-type=privateKey +ansible-ssh-key-storage-path=keys/project/ansible-test/ssh-node.key +ansible-ssh-user=rundeck +project.ansible-config-file-path=/home/rundeck/ansible/ansible.cfg +project.ansible-executable=/bin/bash +project.ansible-ssh-auth-type=privateKey +project.ansible-ssh-key-storage-path=keys/project/ansible-test/ssh-node.key +project.ansible-ssh-passphrase-option=option.password +project.ansible-ssh-user=rundeck +project.description= project.disable.executions=false project.disable.schedule=false project.execution.history.cleanup.batch=500 @@ -8,13 +18,10 @@ project.execution.history.cleanup.retention.days=60 project.execution.history.cleanup.retention.minimum=50 project.execution.history.cleanup.schedule=0 0 0 1/1 * ? * project.jobs.gui.groupExpandLevel=1 -project.later.executions.disable.value=0 +project.label= project.later.executions.disable=false -project.later.executions.enable.value= project.later.executions.enable=false -project.later.schedule.disable.value= project.later.schedule.disable=false -project.later.schedule.enable.value= project.later.schedule.enable=false project.name=ansible-test project.nodeCache.enabled=true @@ -31,5 +38,5 @@ resources.source.2.config.ansible-ssh-auth-type=privateKey resources.source.2.config.ansible-ssh-key-storage-path=keys/project/ansible-test/ssh-node.key resources.source.2.config.ansible-ssh-user=rundeck resources.source.2.type=com.batix.rundeck.plugins.AnsibleResourceModelSourceFactory -service.FileCopier.default.provider=sshj-scp -service.NodeExecutor.default.provider=sshj-ssh \ No newline at end of file +service.FileCopier.default.provider=com.batix.rundeck.plugins.AnsibleFileCopier +service.NodeExecutor.default.provider=com.batix.rundeck.plugins.AnsibleNodeExecutor \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-03f2ed76-f986-4ad5-a9ea-640d326d4b73.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-03f2ed76-f986-4ad5-a9ea-640d326d4b73.xml new file mode 100644 index 00000000..60ed8fb7 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-03f2ed76-f986-4ad5-a9ea-640d326d4b73.xml @@ -0,0 +1,50 @@ + + + + + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + 03f2ed76-f986-4ad5-a9ea-640d326d4b73 + INFO + simple-file-playbook + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + 03f2ed76-f986-4ad5-a9ea-640d326d4b73 + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml index f2e02aaa..274959ca 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-243ba9cb-b95c-4d4b-a569-09935a62397d.xml @@ -27,7 +27,6 @@ - diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-284e1a6e-bae0-4778-a838-50647fb340e3.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-284e1a6e-bae0-4778-a838-50647fb340e3.xml new file mode 100644 index 00000000..e288facf --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-284e1a6e-bae0-4778-a838-50647fb340e3.xml @@ -0,0 +1,50 @@ + + + + + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + 284e1a6e-bae0-4778-a838-50647fb340e3 + INFO + simple-inline-playbook-encrypt-vars + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + 284e1a6e-bae0-4778-a838-50647fb340e3 + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml index ce67f923..10fba818 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-4ecd6b86-b437-4792-a37e-af1fa5a2ca0c.xml @@ -27,7 +27,6 @@ - diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6a49e380-bfdf-4bfd-b075-a4321ff78836.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6a49e380-bfdf-4bfd-b075-a4321ff78836.xml new file mode 100644 index 00000000..03e17341 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6a49e380-bfdf-4bfd-b075-a4321ff78836.xml @@ -0,0 +1,46 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + 6a49e380-bfdf-4bfd-b075-a4321ff78836 + INFO + simple-inline-playbook-became-sudo + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + + + 6a49e380-bfdf-4bfd-b075-a4321ff78836 + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6b309548-bcc9-40d8-8c79-bfc0d1f1e49c.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6b309548-bcc9-40d8-8c79-bfc0d1f1e49c.xml new file mode 100644 index 00000000..683a03df --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-6b309548-bcc9-40d8-8c79-bfc0d1f1e49c.xml @@ -0,0 +1,39 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + 6b309548-bcc9-40d8-8c79-bfc0d1f1e49c + INFO + run-simple-script + false + + name: ssh-node + + true + + true + + + + + + + 6b309548-bcc9-40d8-8c79-bfc0d1f1e49c + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml new file mode 100644 index 00000000..466a59c6 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml @@ -0,0 +1,45 @@ + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + c4d8ddec-ded6-4840-9fab-7eaf2022e12d + INFO + simple-inline-playbook-user-encryption + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + + c4d8ddec-ded6-4840-9fab-7eaf2022e12d + + \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml index 8563cb27..fc813a1c 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-d8e88ac2-a310-4461-be54-fd38cdac5e11.xml @@ -27,7 +27,6 @@ - diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-f302db98-8737-4b87-8832-f830622ccf85.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-f302db98-8737-4b87-8832-f830622ccf85.xml new file mode 100644 index 00000000..fcebd9c2 --- /dev/null +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-f302db98-8737-4b87-8832-f830622ccf85.xml @@ -0,0 +1,51 @@ + + + + + + + nodes + + + true + false + ascending + false + 1 + + true + Ansible + f302db98-8737-4b87-8832-f830622ccf85 + INFO + simple-inline-playbook-options + false + + name: ssh-node + + true + + true + + + + + + + + + + + + + + + + + + + f302db98-8737-4b87-8832-f830622ccf85 + + \ No newline at end of file From 21ff1296094c2785184bb56fe2c8aa764384951a Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 18 Mar 2024 12:53:26 -0300 Subject: [PATCH 16/23] optional label for ansible vault script --- src/main/resources/vault-client.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 489016c3..5e45e163 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -6,16 +6,20 @@ parser = argparse.ArgumentParser(description='Get a vault password from user keyring') -parser.add_argument('--vault-id', action='store', default='dev', - dest='vault_id', - help='name of the vault secret to get from keyring') +parser.add_argument('--vault-id', action='store', default='', + dest='vault_id', + help='name of the vault secret to get from keyring') args = parser.parse_args() keyname = args.vault_id -if "VAULT_ID_SECRET" in os.environ: - secret=os.environ["VAULT_ID_SECRET"] - sys.stdout.write('%s/%s\n' % (keyname,secret)) +secret=os.environ["VAULT_ID_SECRET"] + +if secret: + if keyname: + sys.stdout.write('%s/%s\n' % (keyname,secret)) + else: + sys.stdout.write('%s\n' % (secret)) sys.exit(0) if sys.stdin.isatty(): @@ -23,10 +27,12 @@ else: secret = sys.stdin.readline().rstrip() -if secret is None or secret == '': - sys.stderr.write('ERROR: secret is not set\n') +if secret is None: + sys.stderr.write('ERROR: VAULT_ID_SECRET is not set\n') sys.exit(1) -sys.stdout.write('%s/%s\n' % (keyname,secret)) +if keyname: + sys.stdout.write('%s/%s\n' % (keyname,secret)) +else: + sys.stdout.write('%s\n' % (secret)) sys.exit(0) - From 698a4fbbcfdc85c474e724db0ba6f5e682c68eb0 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 18 Mar 2024 14:35:53 -0300 Subject: [PATCH 17/23] last functional tests --- .../functional/BasicIntegrationSpec.groovy | 28 +++++++++++++++++++ .../ansible/user-encrypted-env-vars.yaml | 10 +++++++ ...b-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml | 2 +- src/main/resources/vault-client.py | 5 ++-- 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 functional-test/src/test/resources/docker/ansible/user-encrypted-env-vars.yaml diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index 1e60feb1..a2a4b7dd 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -242,6 +242,34 @@ class BasicIntegrationSpec extends Specification { executionState.getExecutionState()=="SUCCEEDED" } + def "test use encrypted user file"(){ + when: + + def jobId = "c4d8ddec-ded6-4840-9fab-7eaf2022e12d" + + JobRun request = new JobRun() + request.loglevel = 'INFO' + + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionId = result.id + + def executionState = waitForJob(executionId) + + def logs = getLogs(executionId) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" + ansibleNodeExecutionStatus.get("ok")!=0 + ansibleNodeExecutionStatus.get("unreachable")==0 + ansibleNodeExecutionStatus.get("failed")==0 + ansibleNodeExecutionStatus.get("skipped")==0 + ansibleNodeExecutionStatus.get("ignored")==0 + logs.findAll {it.log.contains("\"environmentTest\": \"test\"")}.size() == 1 + logs.findAll {it.log.contains("\"token\": 13231232312321321321321")}.size() == 1 + } + ExecutionStateResponse waitForJob(String executionId){ def finalStatus = [ 'aborted', diff --git a/functional-test/src/test/resources/docker/ansible/user-encrypted-env-vars.yaml b/functional-test/src/test/resources/docker/ansible/user-encrypted-env-vars.yaml new file mode 100644 index 00000000..15118660 --- /dev/null +++ b/functional-test/src/test/resources/docker/ansible/user-encrypted-env-vars.yaml @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +63383634353464633139346435303965613930323463316333346631613334353562353233663431 +6666323562613939626261333839323432323437623432620a336532313266373131363637643364 +35613331343332663832643637323432663733383036313332356363636439396166396263393832 +6335663462363435370a663137343164306137333938316135373130326432366236306462353935 +34653361346139303462383264316463656363306139383035386665396364393830366234613735 +36353531363237353863313866383932343862363636666333303731316562336164623265386461 +33393063393137636663663532366137313835663565663439303531613862306461386138343734 +63353964323231326336653639383234396531333232666431333134626433376664343466316165 +3137 diff --git a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml index 466a59c6..fb2914e5 100644 --- a/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml +++ b/functional-test/src/test/resources/project-import/ansible-test/rundeck-ansible-test/jobs/job-c4d8ddec-ded6-4840-9fab-7eaf2022e12d.xml @@ -29,7 +29,7 @@ - + diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 5e45e163..559a9d81 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -13,7 +13,7 @@ args = parser.parse_args() keyname = args.vault_id -secret=os.environ["VAULT_ID_SECRET"] +secret=os.getenv('VAULT_ID_SECRET', None) if secret: if keyname: @@ -28,11 +28,12 @@ secret = sys.stdin.readline().rstrip() if secret is None: - sys.stderr.write('ERROR: VAULT_ID_SECRET is not set\n') + sys.stderr.write('ERROR: secret is not set\n') sys.exit(1) if keyname: sys.stdout.write('%s/%s\n' % (keyname,secret)) else: sys.stdout.write('%s\n' % (secret)) + sys.exit(0) From aca69e36880a6822b6d411a6cd0f7cd866724190 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Mon, 18 Mar 2024 21:30:29 -0300 Subject: [PATCH 18/23] remove vault-id for the vault-client.py remove exposing port from test docker-compose env --- .../groovy/functional/RundeckCompose.groovy | 2 +- .../test/resources/docker/docker-compose.yml | 4 +++- src/main/resources/vault-client.py | 21 ++----------------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/functional-test/src/test/groovy/functional/RundeckCompose.groovy b/functional-test/src/test/groovy/functional/RundeckCompose.groovy index 337f299f..58720636 100644 --- a/functional-test/src/test/groovy/functional/RundeckCompose.groovy +++ b/functional-test/src/test/groovy/functional/RundeckCompose.groovy @@ -47,7 +47,7 @@ class RundeckCompose extends DockerComposeContainer { //configure rundeck api String address = getServiceHost("rundeck",4440) - Integer port = 4440 + Integer port = getServicePort("rundeck",4440) def rdUrl = "http://${address}:${port}/api/41" System.err.println("rdUrl: $rdUrl") Client client = RundeckClient.builder().with { diff --git a/functional-test/src/test/resources/docker/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml index 044da9c3..80b85e4e 100644 --- a/functional-test/src/test/resources/docker/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -22,10 +22,12 @@ services: command: "-Dansible.debug=false" environment: RUNDECK_GRAILS_URL: http://localhost:4440 + RUNDECK_MULTIURL_ENABLED: "true" + RUNDECK_SERVER_FORWARDED: "true" networks: - rundeck ports: - - "4440:4440" + - "4440" volumes: - ${PWD}/ansible:/home/rundeck/ansible:rw diff --git a/src/main/resources/vault-client.py b/src/main/resources/vault-client.py index 559a9d81..4c2c253b 100755 --- a/src/main/resources/vault-client.py +++ b/src/main/resources/vault-client.py @@ -1,25 +1,12 @@ #!/usr/bin/env python3 import sys import os -import argparse import getpass -parser = argparse.ArgumentParser(description='Get a vault password from user keyring') - -parser.add_argument('--vault-id', action='store', default='', - dest='vault_id', - help='name of the vault secret to get from keyring') - -args = parser.parse_args() -keyname = args.vault_id - secret=os.getenv('VAULT_ID_SECRET', None) if secret: - if keyname: - sys.stdout.write('%s/%s\n' % (keyname,secret)) - else: - sys.stdout.write('%s\n' % (secret)) + sys.stdout.write('%s\n' % (secret)) sys.exit(0) if sys.stdin.isatty(): @@ -31,9 +18,5 @@ sys.stderr.write('ERROR: secret is not set\n') sys.exit(1) -if keyname: - sys.stdout.write('%s/%s\n' % (keyname,secret)) -else: - sys.stdout.write('%s\n' % (secret)) - +sys.stdout.write('%s\n' % (secret)) sys.exit(0) From eb88775719683214f0765c1b4d328e80b37a3337 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 19 Mar 2024 16:54:40 -0300 Subject: [PATCH 19/23] fix issue passing list options using runner add the binary path to the ansible-vault exporting jackson dependency --- build.gradle | 4 ++-- .../plugins/ansible/ansible/AnsibleRunner.java | 1 + .../ansible/AnsibleRunnerContextBuilder.java | 4 +++- .../plugins/ansible/ansible/AnsibleVault.java | 14 ++++++++++++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 2686b6fe..e7989e15 100644 --- a/build.gradle +++ b/build.gradle @@ -49,8 +49,8 @@ dependencies { pluginLibs 'com.google.code.gson:gson:2.10.1' implementation('org.rundeck:rundeck-core:5.1.1-20240305') implementation 'org.codehaus.groovy:groovy-all:3.0.15' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.16.1' - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' + 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' 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/AnsibleRunner.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java index 47b0c93a..6005d522 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -342,6 +342,7 @@ public int run() throws Exception { .baseDirectory(baseDirectory) .masterPassword(AnsibleUtil.randomString()) .vaultPasswordScriptFile(tempInternalVaultFile) + .ansibleBinariesDirectory(ansibleBinariesDirectory) .debug(debug).build(); } 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 be3bd1c5..77f3294f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java @@ -822,12 +822,14 @@ public Map getListOptions(){ Map options = new HashMap<>(); Map optionsContext = context.getDataContext().get("option"); Map secureOptionContext = context.getDataContext().get("secureOption"); - if (optionsContext != null) { + if (optionsContext != null && secureOptionContext!=null) { optionsContext.forEach((option, value) -> { if(!secureOptionContext.containsKey(option)){ options.put(option, value); } }); + }else if (optionsContext != null) { + options.putAll(optionsContext); } return options; } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java index daeb1a6b..ce7519f1 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -8,6 +8,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; @@ -21,12 +22,17 @@ public class AnsibleVault { private String masterPassword; private boolean debug; private Path baseDirectory; + private Path ansibleBinariesDirectory; public final String ANSIBLE_VAULT_COMMAND = "ansible-vault"; public boolean checkAnsibleVault() { List procArgs = new ArrayList<>(); - procArgs.add(ANSIBLE_VAULT_COMMAND); + String ansibleCommand = ANSIBLE_VAULT_COMMAND; + if (ansibleBinariesDirectory != null) { + ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); + } + procArgs.add(ansibleCommand); procArgs.add("--version"); Process proc = null; @@ -52,7 +58,11 @@ public String encryptVariable(String key, String content ) throws IOException { List procArgs = new ArrayList<>(); - procArgs.add("ansible-vault"); + String ansibleCommand = ANSIBLE_VAULT_COMMAND; + if (ansibleBinariesDirectory != null) { + ansibleCommand = Paths.get(ansibleBinariesDirectory.toFile().getAbsolutePath(), ansibleCommand).toFile().getAbsolutePath(); + } + procArgs.add(ansibleCommand); procArgs.add("encrypt_string"); procArgs.add("--vault-id"); procArgs.add("internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); From 80fab9e74c8ee3cc5841e7eb504ce66b5da7cf39 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 19 Mar 2024 18:25:42 -0300 Subject: [PATCH 20/23] fix encryptExtraVarsKey move file copier clean to a finally block --- .../ansible/ansible/AnsibleRunner.java | 28 +++++++++---------- .../ansible/plugin/AnsibleFileCopier.java | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) 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 6005d522..7be7e2a3 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -424,7 +424,7 @@ public int run() throws Exception { String addeExtraVars = extraVars; if (encryptExtraVars && useAnsibleVault) { - addeExtraVars = encryptExtraVarsKey(extraVars, tempInternalVaultFile); + addeExtraVars = encryptExtraVarsKey(extraVars); } tempVarsFile = AnsibleUtil.createTemporaryFile("extra-vars", addeExtraVars); @@ -462,7 +462,7 @@ public int run() throws Exception { String finalextraVarsPassword = extraVarsPassword; if(useAnsibleVault){ - finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword); } tempSshVarsFile = AnsibleUtil.createTemporaryFile("ssh-extra-vars", finalextraVarsPassword); @@ -481,7 +481,7 @@ public int run() throws Exception { String finalextraVarsPassword = extraVarsPassword; if (useAnsibleVault) { - finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword, tempInternalVaultFile); + finalextraVarsPassword = encryptExtraVarsKey(extraVarsPassword); } tempBecameVarsFile = AnsibleUtil.createTemporaryFile("become-extra-vars", finalextraVarsPassword); @@ -724,22 +724,13 @@ public boolean registerKeySshAgent(String keyPath) throws Exception { } - public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) throws Exception { - Map extraVarsMap = null; + public String encryptExtraVarsKey(String extraVars) throws Exception { + Map extraVarsMap = new HashMap<>(); Map encryptedExtraVarsMap = new HashMap<>(); try { extraVarsMap = mapperYaml.readValue(extraVars, new TypeReference>() { }); - for (Map.Entry entry : extraVarsMap.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - String encryptedKey = ansibleVault.encryptVariable(key, value); - if (encryptedKey != null) { - encryptedExtraVarsMap.put(key, encryptedKey); - } - } - } catch (Exception e) { try { extraVarsMap = mapperJson.readValue(extraVars, new TypeReference>() { @@ -750,6 +741,15 @@ public String encryptExtraVarsKey(String extraVars, File vaultPasswordFile) thro } } + for (Map.Entry entry : extraVarsMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + String encryptedKey = ansibleVault.encryptVariable(key, value); + if (encryptedKey != null) { + encryptedExtraVarsMap.put(key, encryptedKey); + } + } + StringBuilder stringBuilder = new StringBuilder(); encryptedExtraVarsMap.forEach((key, value) -> { stringBuilder.append(key).append(":"); diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java index df1470da..7c861a3a 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleFileCopier.java @@ -157,10 +157,10 @@ private String doFileCopy( runner.run(); } catch (Exception e) { throw new FileCopierException("Error running Ansible.", AnsibleFailureReason.AnsibleError, e); + }finally { + contextBuilder.cleanupTempFiles(); } - contextBuilder.cleanupTempFiles(); - return destinationPath; } From 6a0f38e639daa269414a4e1118d75f1205ece9de Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 19 Mar 2024 18:33:27 -0300 Subject: [PATCH 21/23] fix test --- .../plugins/ansible/ansible/AnsibleRunner.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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 7be7e2a3..3b8e7980 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java @@ -741,13 +741,18 @@ public String encryptExtraVarsKey(String extraVars) throws Exception { } } - for (Map.Entry entry : extraVarsMap.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - String encryptedKey = ansibleVault.encryptVariable(key, value); - if (encryptedKey != null) { - encryptedExtraVarsMap.put(key, encryptedKey); + try { + for (Map.Entry entry : extraVarsMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + String encryptedKey = ansibleVault.encryptVariable(key, value); + if (encryptedKey != null) { + encryptedExtraVarsMap.put(key, encryptedKey); + } } + } catch (Exception e) { + throw new AnsibleException("ERROR: cannot parse extra var values: " + e.getMessage(), + AnsibleException.AnsibleFailureReason.AnsibleNonZero); } StringBuilder stringBuilder = new StringBuilder(); From 7edb8ae2e52380062d0b7042d4b5acaae4237081 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 19 Mar 2024 19:15:45 -0300 Subject: [PATCH 22/23] put cleanupTempFiles in a finally block for all plugins --- .../plugins/ansible/plugin/AnsibleModuleWorkflowStep.java | 3 ++- .../rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java | 4 ++-- .../ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java | 3 ++- .../ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java | 4 ++-- .../ansible/plugin/AnsiblePlaybookWorflowNodeStep.java | 4 ++-- .../plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java index 4a286985..c8f35c41 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleModuleWorkflowStep.java @@ -93,9 +93,10 @@ public void executeStep(PluginStepContext context, Map configura throw new StepException(e.getMessage(), e, e.getFailureReason()); } catch (Exception e) { throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError); + }finally { + contextBuilder.cleanupTempFiles(); } - contextBuilder.cleanupTempFiles(); } @Override 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 07c16473..11ed9305 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleNodeExecutor.java @@ -178,10 +178,10 @@ public NodeExecutorResult executeCommand(ExecutionContext context, String[] comm runner.run(); } catch (Exception e) { return NodeExecutorResultImpl.createFailure(AnsibleException.AnsibleFailureReason.AnsibleError, e.getMessage(), node); + }finally { + contextBuilder.cleanupTempFiles(); } - contextBuilder.cleanupTempFiles(); - return NodeExecutorResultImpl.createSuccess(node); } 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 00eb659e..20ef6a10 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowNodeStep.java @@ -102,9 +102,10 @@ public void executeNodeStep( failureData.put("message",e.getMessage()); failureData.put("ansible-config", configuration); throw new NodeStepException(e.getMessage(),e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData, e.getMessage()); + }finally { + contextBuilder.cleanupTempFiles(); } - contextBuilder.cleanupTempFiles(); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java index da1e5ba9..afef685c 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookInlineWorkflowStep.java @@ -107,9 +107,9 @@ public void executeStep(PluginStepContext context, Map configura throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData); + }finally { + contextBuilder.cleanupTempFiles(); } - - contextBuilder.cleanupTempFiles(); } diff --git a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java index 42d83072..6299919f 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorflowNodeStep.java @@ -94,9 +94,9 @@ public void executeNodeStep( throw new NodeStepException(e.getMessage(), e.getFailureReason(),e.getMessage()); } catch (Exception e) { throw new NodeStepException(e.getMessage(),AnsibleException.AnsibleFailureReason.AnsibleError,e.getMessage()); + }finally { + contextBuilder.cleanupTempFiles(); } - - contextBuilder.cleanupTempFiles(); } @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 44bf7b03..8b4ca4bb 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsiblePlaybookWorkflowStep.java @@ -105,9 +105,9 @@ public void executeStep(PluginStepContext context, Map configura failureData.put("ansible-config", contextBuilder.getConfigFile()); throw new StepException(e.getMessage(), e, AnsibleException.AnsibleFailureReason.AnsibleError, failureData); + }finally { + contextBuilder.cleanupTempFiles(); } - - contextBuilder.cleanupTempFiles(); } @Override From aa9822679e1937d0ffea51e4da561187e6e7a12e Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 20 Mar 2024 16:37:26 -0300 Subject: [PATCH 23/23] set java version 11 to jitpack --- jitpack.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 jitpack.yml diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..46c85291 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file