diff --git a/build.gradle b/build.gradle index ed7b06e4..38970e34 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ configurations { dependencies { pluginLibs 'com.google.code.gson:gson:2.10.1' - implementation('org.rundeck:rundeck-core:4.14.0-rc1-20230606') + implementation('org.rundeck:rundeck-core:4.16.0-rc1-20230815') implementation 'org.codehaus.groovy:groovy-all:3.0.9' } 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 e622237b..8a5544dd 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSource.java @@ -1,5 +1,11 @@ package com.rundeck.plugins.ansible.plugin; +import com.dtolabs.rundeck.core.execution.proxy.DefaultSecretBundle; +import com.dtolabs.rundeck.core.execution.proxy.ProxySecretBundleCreator; +import com.dtolabs.rundeck.core.execution.proxy.SecretBundle; +import com.dtolabs.rundeck.core.storage.ResourceMeta; +import com.dtolabs.rundeck.core.storage.StorageTree; +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable; import com.rundeck.plugins.ansible.ansible.AnsibleDescribable.AuthenticationType; import com.rundeck.plugins.ansible.ansible.AnsibleException; @@ -18,8 +24,12 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import org.rundeck.app.spi.Services; +import org.rundeck.storage.api.PathUtil; +import org.rundeck.storage.api.StorageException; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @@ -28,10 +38,12 @@ import java.util.*; import java.util.Map.Entry; -public class AnsibleResourceModelSource implements ResourceModelSource { +public class AnsibleResourceModelSource implements ResourceModelSource, ProxySecretBundleCreator { private Framework framework; + private Services services; + private String project; private String sshAuthType; @@ -55,6 +67,9 @@ public class AnsibleResourceModelSource implements ResourceModelSource { protected Boolean sshUsePassword; protected String sshPassword; protected String sshPrivateKeyFile; + + protected String sshPasswordPath; + protected String sshPrivateKeyPath; protected String sshPass; protected Integer sshTimeout; @@ -68,12 +83,20 @@ public class AnsibleResourceModelSource implements ResourceModelSource { protected String vaultFile; protected String vaultPassword; + protected String vaultPasswordPath; + protected String baseDirectoryPath; protected String ansibleBinariesDirectoryPath; protected String extraParameters; + protected String sshAgent; + protected String sshPassphraseStoragePath; + + protected String becamePasswordStoragePath; + + public AnsibleResourceModelSource(final Framework framework) { this.framework = framework; } @@ -99,6 +122,10 @@ private static Boolean skipVar(final String hostVar, final List varList) return false; } + public void setServices(Services services) { + this.services = services; + } + public void configure(Properties configuration) throws ConfigurationException { project = configuration.getProperty("project"); @@ -156,6 +183,16 @@ public void configure(Properties configuration) throws ConfigurationException { extraParameters = (String) resolveProperty(AnsibleDescribable.ANSIBLE_EXTRA_PARAM,null,configuration,executionDataContext); + sshPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); + sshPrivateKeyPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH,null,configuration,executionDataContext); + + vaultPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_VAULTSTORE_PATH,null,configuration,executionDataContext); + + sshAgent = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_USE_AGENT,null,configuration,executionDataContext); + sshPassphraseStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE,null,configuration,executionDataContext); + vaultPasswordPath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); + + becamePasswordStoragePath = (String) resolveProperty(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH,null,configuration,executionDataContext); } public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ @@ -172,6 +209,9 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner.limit(limitList); } + StorageTree storageTree = services.getService(KeyStorageTree.class); + + if ( sshAuthType.equalsIgnoreCase(AuthenticationType.privateKey.name()) ) { if (sshPrivateKeyFile != null) { String sshPrivateKey; @@ -182,12 +222,43 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ } runner = runner.sshPrivateKey(sshPrivateKey); } + + if(sshPrivateKeyPath !=null && !sshPrivateKeyPath.isEmpty()){ + try { + String sshPrivateKey = getStorageContentString(sshPrivateKeyPath, storageTree); + runner = runner.sshPrivateKey(sshPrivateKey); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); + } + } + + if(sshAgent != null && sshAgent.equalsIgnoreCase("true")) { + runner = runner.sshUseAgent(Boolean.TRUE); + + if(sshPassphraseStoragePath != null && !sshPassphraseStoragePath.isEmpty()) { + try { + String sshPassphrase = getStorageContentString(sshPassphraseStoragePath, storageTree); + runner = runner.sshPassphrase(sshPassphrase); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read passphrase from storage path " + sshPassphraseStoragePath,e); + } + } + } + } else if ( sshAuthType.equalsIgnoreCase(AuthenticationType.password.name()) ) { if (sshPassword != null) { runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); } - } + if(sshPasswordPath !=null && !sshPasswordPath.isEmpty()){ + try { + sshPassword = getStorageContentString(sshPasswordPath, storageTree); + runner = runner.sshUsePassword(Boolean.TRUE).sshPass(sshPassword); + } catch (ConfigurationException e) { + throw new ResourceModelSourceException("Could not read password from storage path " + sshPasswordPath,e); + } + } + } if (inventory != null) { runner = runner.setInventory(inventory); @@ -220,34 +291,55 @@ public AnsibleRunner buildAnsibleRunner() throws ResourceModelSourceException{ runner = runner.becomePassword(becomePassword); } - if (configFile != null) { - runner = runner.configFile(configFile); + if(becamePasswordStoragePath != null && !becamePasswordStoragePath.isEmpty()){ + try { + becomePassword = getStorageContentString(becamePasswordStoragePath, storageTree); + runner = runner.becomePassword(becomePassword); + } catch (Exception e) { + throw new ResourceModelSourceException("Could not read becomePassword from storage path " + becamePasswordStoragePath,e); } + } + + if (configFile != null) { + runner = runner.configFile(configFile); + } - if(vaultPassword!=null) { + if(vaultPassword!=null) { runner.vaultPass(vaultPassword); - } + } - if (vaultFile != null) { - String vaultPassword; - try { - vaultPassword = new String(Files.readAllBytes(Paths.get(vaultFile))); - } catch (IOException e) { - throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); - } - runner.vaultPass(vaultPassword); - } - if (baseDirectoryPath != null) { - runner.baseDirectory(baseDirectoryPath); + if(vaultPasswordPath!=null && !vaultPasswordPath.isEmpty()){ + try { + vaultPassword = getStorageContentString(vaultPasswordPath, storageTree); + } catch (Exception e) { + throw new ResourceModelSourceException("Could not read vaultPassword " + vaultPasswordPath,e); } + runner = runner.vaultPass(vaultPassword); + } - if (ansibleBinariesDirectoryPath != null) { - runner.ansibleBinariesDirectory(ansibleBinariesDirectoryPath); + if (vaultFile != null) { + String vaultPassword; + try { + vaultPassword = new String(Files.readAllBytes(Paths.get(vaultFile))); + } catch (IOException e) { + throw new ResourceModelSourceException("Could not read vault file " + vaultFile,e); } + runner.vaultPass(vaultPassword); + } + if (baseDirectoryPath != null) { + runner.baseDirectory(baseDirectoryPath); + } + + if (ansibleBinariesDirectoryPath != null) { + runner.ansibleBinariesDirectory(ansibleBinariesDirectoryPath); + } + + if (extraParameters != null){ + runner.extraParams(extraParameters); + } + + - if (extraParameters != null){ - runner.extraParams(extraParameters); - } return runner; } @@ -534,4 +626,126 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx return nodes; } + + + + private String getStorageContentString(String storagePath, StorageTree storageTree) throws ConfigurationException { + return new String(this.getStorageContent(storagePath, storageTree)); + } + + private byte[] getStorageContent(String storagePath, StorageTree storageTree) throws ConfigurationException { + org.rundeck.storage.api.Path path = PathUtil.asPath(storagePath); + try { + ResourceMeta contents = storageTree.getResource(path).getContents(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + contents.writeContent(byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (StorageException e) { + throw new ConfigurationException("Failed to read the ssh private key for " + + "storage path: " + storagePath + ": " + e.getMessage()); + } catch (IOException e) { + throw new ConfigurationException("Failed to read the ssh private key for " + + "storage path: " + storagePath + ": " + e.getMessage()); + } + } + + + @Override + public List listSecretsPathResourceModel(Map configuration){ + List keys = new ArrayList<>(); + + String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); + String privateKeyStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH); + String passphraseStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE); + String vaultPasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_VAULTSTORE_PATH); + String becamePasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH); + + if(passwordStoragePath!=null && !passwordStoragePath.isEmpty()){ + keys.add(passwordStoragePath); + } + + if(privateKeyStoragePath!=null && !privateKeyStoragePath.isEmpty()){ + if(!keys.contains(privateKeyStoragePath)){ + keys.add(privateKeyStoragePath); + } + } + + if(passphraseStoragePath!=null && !passphraseStoragePath.isEmpty()){ + if(!keys.contains(passphraseStoragePath)){ + keys.add(passphraseStoragePath); + } + } + + if(vaultPasswordStoragePath!=null && !vaultPasswordStoragePath.isEmpty()){ + if(!keys.contains(vaultPasswordStoragePath)){ + keys.add(vaultPasswordStoragePath); + } + } + + if(becamePasswordStoragePath!=null && !becamePasswordStoragePath.isEmpty()){ + if(!keys.contains(becamePasswordStoragePath)){ + keys.add(becamePasswordStoragePath); + } + } + + return keys; + + } + + @Override + public SecretBundle prepareSecretBundleResourceModel(Services services, Map configuration){ + DefaultSecretBundle secretBundle = new DefaultSecretBundle(); + + try { + StorageTree storageTree = services.getService(KeyStorageTree.class); + + String passwordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSWORD_STORAGE_PATH); + String privateKeyStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_KEYPATH_STORAGE_PATH); + String passphraseStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_SSH_PASSPHRASE); + String vaultPasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_VAULTSTORE_PATH); + String becamePasswordStoragePath = (String) configuration.get(AnsibleDescribable.ANSIBLE_BECOME_PASSWORD_STORAGE_PATH); + + if(passwordStoragePath!=null && !passwordStoragePath.isEmpty()){ + secretBundle.addSecret( + passwordStoragePath, + getStorageContent(passwordStoragePath,storageTree ) + ); + } + + if(privateKeyStoragePath!=null && !privateKeyStoragePath.isEmpty()){ + secretBundle.addSecret( + privateKeyStoragePath, + getStorageContent(privateKeyStoragePath,storageTree ) + ); + } + + if(passphraseStoragePath!=null && !passphraseStoragePath.isEmpty()){ + secretBundle.addSecret( + passphraseStoragePath, + getStorageContent(passphraseStoragePath,storageTree ) + ); + } + + if(vaultPasswordStoragePath!=null && !vaultPasswordStoragePath.isEmpty()){ + secretBundle.addSecret( + vaultPasswordStoragePath, + getStorageContent(vaultPasswordStoragePath,storageTree ) + ); + } + + if(becamePasswordStoragePath!=null && !becamePasswordStoragePath.isEmpty()){ + secretBundle.addSecret( + becamePasswordStoragePath, + getStorageContent(becamePasswordStoragePath,storageTree ) + ); + } + + return secretBundle; + + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + } 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 a4159ac2..a49366df 100644 --- a/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java +++ b/src/main/groovy/com/rundeck/plugins/ansible/plugin/AnsibleResourceModelSourceFactory.java @@ -9,6 +9,7 @@ import com.dtolabs.rundeck.core.resources.ResourceModelSourceFactory; import com.dtolabs.rundeck.plugins.ServiceNameConstants; import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; +import org.rundeck.app.spi.Services; import java.util.Properties; @@ -53,6 +54,15 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { builder.property(BECOME_PASSWORD_PROP); builder.property(VAULT_KEY_FILE_PROP); builder.property(VAULT_PASSWORD_PROP); + builder.property(VAULT_KEY_STORAGE_PROP); + + builder.property(SSH_PASSWORD_STORAGE_PROP); + builder.property(SSH_KEY_STORAGE_PROP); + builder.property(SSH_PASSPHRASE); + + builder.property(SSH_USE_AGENT); + builder.property(BECOME_PASSWORD_STORAGE_PROP); + builder.mapping(ANSIBLE_INVENTORY,PROJ_PROP_PREFIX + ANSIBLE_INVENTORY); builder.frameworkMapping(ANSIBLE_INVENTORY,FWK_PROP_PREFIX + ANSIBLE_INVENTORY); builder.mapping(ANSIBLE_CONFIG_FILE_PATH,PROJ_PROP_PREFIX + ANSIBLE_CONFIG_FILE_PATH); @@ -68,12 +78,18 @@ public AnsibleResourceModelSourceFactory(final Framework framework) { @Override public ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { - AnsibleResourceModelSource ansibleResourceModelSource = new AnsibleResourceModelSource(framework); - ansibleResourceModelSource.configure(configuration); - return ansibleResourceModelSource; + return null; } - @Override + @Override + public ResourceModelSource createResourceModelSource(Services services, Properties configuration) throws ConfigurationException { + AnsibleResourceModelSource ansibleResourceModelSource = new AnsibleResourceModelSource(framework); + ansibleResourceModelSource.configure(configuration); + ansibleResourceModelSource.setServices(services); + return ansibleResourceModelSource; + } + + @Override public Description getDescription() { return DESC; }