diff --git a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy index b00ecfc..7508cfe 100644 --- a/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/BasicIntegrationSpec.groovy @@ -10,10 +10,29 @@ import org.testcontainers.spock.Testcontainers class BasicIntegrationSpec extends BaseTestConfiguration { static String PROJ_NAME = 'ansible-test' + static String DEFAULT_NODE_NAME = "ssh-node" def setupSpec() { startCompose() - configureRundeck(PROJ_NAME) + configureRundeck(PROJ_NAME, DEFAULT_NODE_NAME) + } + + def "ansible node executor with ssh password"(){ + setup: + String ansibleNodeExecutorProjectName = "sshPasswordProject" + String nodeName = "ssh-node-password" + configureRundeck(ansibleNodeExecutorProjectName, nodeName) + when: + def jobId = "f04f17a9-77cf-4feb-aec1-889a3de0f5ae" + JobRun request = new JobRun() + request.loglevel = 'INFO' + def result = client.apiCall {api-> api.runJob(jobId, request)} + def executionState = waitForJob(result.id) + def logs = getLogs(result.id) + Map ansibleNodeExecutionStatus = TestUtil.getAnsibleNodeResult(logs) + then: + executionState!=null + executionState.getExecutionState()=="SUCCEEDED" } def "test simple inline playbook"(){ diff --git a/functional-test/src/test/groovy/functional/EncryptedInventorySpec.groovy b/functional-test/src/test/groovy/functional/EncryptedInventorySpec.groovy index e7cad1e..1b4046b 100644 --- a/functional-test/src/test/groovy/functional/EncryptedInventorySpec.groovy +++ b/functional-test/src/test/groovy/functional/EncryptedInventorySpec.groovy @@ -7,10 +7,11 @@ import org.testcontainers.spock.Testcontainers class EncryptedInventorySpec extends BaseTestConfiguration { static String PROJ_NAME = 'ansible-encrypted-inventory' + static String DEFAULT_NODE_NAME = "ssh-node" def setupSpec() { startCompose() - configureRundeck(PROJ_NAME) + configureRundeck(PROJ_NAME, DEFAULT_NODE_NAME) } def "test encrypted inventory"(){ diff --git a/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy index 09ca61c..f3ef63a 100644 --- a/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy +++ b/functional-test/src/test/groovy/functional/PluginGroupIntegrationSpec.groovy @@ -10,10 +10,11 @@ import org.testcontainers.spock.Testcontainers class PluginGroupIntegrationSpec extends BaseTestConfiguration { static String PROJ_NAME = 'ansible-plugin-group-test' + static String DEFAULT_NODE_NAME = "ssh-node" def setupSpec() { startCompose() - configureRundeck(PROJ_NAME) + configureRundeck(PROJ_NAME, DEFAULT_NODE_NAME) } def "test simple inline playbook"(){ diff --git a/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy b/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy index 47fd531..4ad0ba5 100644 --- a/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy +++ b/functional-test/src/test/groovy/functional/base/BaseTestConfiguration.groovy @@ -85,7 +85,7 @@ class BaseTestConfiguration extends Specification{ return logs } - def configureRundeck(String projectName){ + def configureRundeck(String projectName, String nodeName){ //add private key RequestBody requestBody = RequestBody.create(new File("src/test/resources/docker/keys/id_rsa"), Client.MEDIA_TYPE_OCTET_STREAM) @@ -125,15 +125,18 @@ class BaseTestConfiguration extends Specification{ api.importProjectArchive(projectName, "preserve", true, true, true, true, true, true, true, [:], body) ) - //wait for node to be available + waitForNodeAvailability(projectName, nodeName) + + } + + def waitForNodeAvailability(String projectName, String nodeName){ def result = client.apiCall {api-> api.listNodes(projectName,".*")} def count =0 - while(result.get("ssh-node")==null && count<5){ + while(result.get(nodeName)==null && count<5){ sleep(2000) result = client.apiCall {api-> api.listNodes(projectName,".*")} count++ } - } } diff --git a/functional-test/src/test/resources/docker/ansible/ansible.cfg b/functional-test/src/test/resources/docker/ansible/ansible.cfg index c8c55b5..1abf9e2 100644 --- a/functional-test/src/test/resources/docker/ansible/ansible.cfg +++ b/functional-test/src/test/resources/docker/ansible/ansible.cfg @@ -1,5 +1,6 @@ [defaults] inventory = /home/rundeck/ansible/inventory.ini +vault_password_file=/home/rundeck/ansible/mp.pass host_key_checking = False interpreter_python=/usr/bin/python3 show_custom_stats = False diff --git a/functional-test/src/test/resources/docker/ansible/inventory.ini b/functional-test/src/test/resources/docker/ansible/inventory.ini index 10bf257..f019bb8 100644 --- a/functional-test/src/test/resources/docker/ansible/inventory.ini +++ b/functional-test/src/test/resources/docker/ansible/inventory.ini @@ -1,2 +1,3 @@ [servers] ssh-node ansible_host=ssh-node +ssh-node-password ansible_host=ssh-node ansible_connection=ssh ansible_user=rundeck ansible_ssh_pass=testpassword123 \ No newline at end of file diff --git a/functional-test/src/test/resources/docker/ansible/mp.pass b/functional-test/src/test/resources/docker/ansible/mp.pass new file mode 100644 index 0000000..4632e06 --- /dev/null +++ b/functional-test/src/test/resources/docker/ansible/mp.pass @@ -0,0 +1 @@ +123456 \ No newline at end of file diff --git a/functional-test/src/test/resources/docker/ansible/rundeckNodes.yaml b/functional-test/src/test/resources/docker/ansible/rundeckNodes.yaml new file mode 100644 index 0000000..bd46aec --- /dev/null +++ b/functional-test/src/test/resources/docker/ansible/rundeckNodes.yaml @@ -0,0 +1,6 @@ +ssh-node-password: + nodename: ssh-node-password + hostname: ssh-node + osFamily: Linux + username: rundeck + tags: '' \ No newline at end of file diff --git a/functional-test/src/test/resources/docker/docker-compose.yml b/functional-test/src/test/resources/docker/docker-compose.yml index f2942cd..e5ad23a 100644 --- a/functional-test/src/test/resources/docker/docker-compose.yml +++ b/functional-test/src/test/resources/docker/docker-compose.yml @@ -5,19 +5,19 @@ services: build: context: node environment: - NODE_USER_PASSWORD: ${NODE_USER_PASSWORD:-rundeck} + NODE_USER_PASSWORD: testpassword123 networks: - rundeck ports: - "2222:22" volumes: - - ${PWD}/keys:/configuration:rw + - ./keys:/configuration:rw rundeck: build: context: rundeck args: - RUNDECK_IMAGE: ${RUNDECK_IMAGE:-rundeck/rundeck:SNAPSHOT} + RUNDECK_IMAGE: rundeck/rundeck:SNAPSHOT image: rundeck-ansible-plugin:latest command: "-Dansible.debug=false" environment: @@ -32,7 +32,8 @@ services: ports: - "4440" volumes: - - ${PWD}/ansible:/home/rundeck/ansible:rw + - ./ansible:/home/rundeck/ansible:rw + - ./ansible/ansible.cfg:/etc/ansible/ansible.cfg:rw volumes: rundeck-data: diff --git a/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/acls/node-acl.aclpolicy b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/acls/node-acl.aclpolicy new file mode 100644 index 0000000..b052b49 --- /dev/null +++ b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/acls/node-acl.aclpolicy @@ -0,0 +1,8 @@ +by: + urn: project:sshPasswordProject +for: + storage: + - match: + path: 'keys/project/sshPasswordProject/.*' + allow: [read] +description: Allow access to key storage \ No newline at end of file diff --git a/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/etc/project.properties b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/etc/project.properties new file mode 100644 index 0000000..ba5c5de --- /dev/null +++ b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/files/etc/project.properties @@ -0,0 +1,38 @@ +#Mon Jul 08 21:50:51 GMT 2024 +#edit below +project.ansible-binaries-dir-path=/usr/local/bin/ +project.ansible-config-file-path=/home/rundeck/ansible/ansible.cfg +project.ansible-executable=/bin/bash +project.ansible-generate-inventory=true +project.ansible-ssh-auth-type=password +project.ansible-ssh-passphrase-option=option.password +project.ansible-ssh-password-storage-path=keys/project/sshPasswordProject/ssh-node.pass +project.ansible-ssh-user=rundeck +project.description= +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.label= +project.later.executions.disable=false +project.later.executions.enable=false +project.later.schedule.disable=false +project.later.schedule.enable=false +project.name=sshPasswordProject +project.nodeCache.enabled=false +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.file=/home/rundeck/ansible/rundeckNodes.yaml +resources.source.2.config.format=resourceyaml +resources.source.2.config.generateFileAutomatically=true +resources.source.2.config.writeable=true +resources.source.2.type=file +service.FileCopier.default.provider=sshj-scp +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/sshPasswordProject/rundeck-sshPasswordProject/jobs/job-f04f17a9-77cf-4feb-aec1-889a3de0f5ae.xml b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/jobs/job-f04f17a9-77cf-4feb-aec1-889a3de0f5ae.xml new file mode 100644 index 0000000..85f104e --- /dev/null +++ b/functional-test/src/test/resources/project-import/sshPasswordProject/rundeck-sshPasswordProject/jobs/job-f04f17a9-77cf-4feb-aec1-889a3de0f5ae.xml @@ -0,0 +1,30 @@ + + + nodes + it targets a single ansible node + + true + false + ascending + false + 1 + + true + f04f17a9-77cf-4feb-aec1-889a3de0f5ae + INFO + simpleCommand + false + + name: ssh-node-password + + true + + true + + + whoami + + + f04f17a9-77cf-4feb-aec1-889a3de0f5ae + + \ 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 index b185ad0..05dd9dd 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleVault.java @@ -14,6 +14,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.*; +import com.rundeck.plugins.ansible.ansible.AnsibleException; @Data @Builder @@ -56,7 +57,7 @@ public boolean checkAnsibleVault() { } public String encryptVariable(String key, - String content ) throws IOException { + String content ) throws IOException, AnsibleException, InterruptedException { List procArgs = new ArrayList<>(); String ansibleCommand = ANSIBLE_VAULT_COMMAND; @@ -65,8 +66,8 @@ public String encryptVariable(String key, } procArgs.add(ansibleCommand); procArgs.add("encrypt_string"); - procArgs.add("--vault-id"); - procArgs.add("internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); + procArgs.add("--encrypt-vault-id"); + procArgs.add("internal-encrypt"); if(debug){ System.out.println("encryptVariable " + key + ": " + procArgs); @@ -81,6 +82,7 @@ public String encryptVariable(String key, Map env = new HashMap<>(); env.put("VAULT_ID_SECRET", masterPassword); + env.put("ANSIBLE_VAULT_IDENTITY_LIST", "internal-encrypt@" + vaultPasswordScriptFile.getAbsolutePath()); Process proc = null; @@ -97,10 +99,12 @@ public String encryptVariable(String key, final InputStream stdoutInputStream = proc.getInputStream(); final BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdoutInputStream)); + int exitCode = proc.waitFor(); + String line1 = null; boolean capture = false; while ((line1 = stdoutReader.readLine()) != null) { - if (line1.toLowerCase().contains("!vault")) { + if (line1.toLowerCase().contains("!vault") || exitCode != 0) { capture = true; } if (capture) { @@ -108,18 +112,14 @@ public String encryptVariable(String key, } } - int exitCode = proc.waitFor(); - if (exitCode != 0) { - System.err.println("ERROR: encryptFileAnsibleVault:" + procArgs); - return null; + throw new AnsibleException(stringBuilder.toString(), AnsibleException.AnsibleFailureReason.IOFailure); } return stringBuilder.toString(); - } catch (Exception e) { - System.err.println("error encryptFileAnsibleVault file " + e.getMessage()); - return null; - } finally { + } catch(Exception e) { + throw e; + }finally { // Make sure to always cleanup on failure and success if (proc != null) { proc.destroy();