diff --git a/README.md b/README.md index 9f135b7..5a55631 100644 --- a/README.md +++ b/README.md @@ -44,35 +44,42 @@ allows for a convenient way of doing quick tasks with Ansible. #### Scripted -Due -to [JENKINS-43782](https://issues.jenkins.io/browse/JENKINS-43782) -and [JENKINS-49056](https://issues.jenkins.io/browse/JENKINS-49056), -adhoc commands cannot be run with a pipeline job. +**Jenkinsfile** + +``` groovy +ansibleAdhoc credentialsId: 'private_key', inventory: 'inventories/a/hosts', hosts: 'hosts_pattern', moduleArguments: 'module_arguments' +``` #### Declarative +**Jenkinsfile** + +``` groovy +ansibleAdhoc(credentialsId: 'private_key', inventory: 'inventories/a/hosts', hosts: 'hosts_pattern', moduleArguments: 'module_arguments') +``` + ### Arguments See also [jenkins.io](https://jenkins.io/doc/pipeline/steps/ansible/) documentation. -| Freestyle Name | Description | -| -------------------------------------- | ------------------------------------------------------------- | -| Ansible installation | Ansible installation to use for the playbook invocation | -| Host pattern | The host pattern to manage. See Ansible Patterns for details. | -| Module | CLI arg: `-m` | -| Module arguments or command to execute | CLI arg: `-a` | -| Inventory file or host list | CLI arg: `-i`: See the Inventory section for additional details. | -| Inventory inline content | CLI arg: `-i`: See the Inventory section for additional details. | -| Credentials | The Jenkins credential to use for the SSH connection. See the Authentication section for additional details. | -| Vault Credentials | CLI arg: `--vault-password-file`: The Jenkins credential to use as the vault credential. See the Vault Credentials section for additional details. | -| sudo | CLI arg: `-s` | -| sudo user | CLI arg: `-U` | -| Number of parallel processes | CLI arg: `-f` | -| Check host SSH key | Toggle checking of the host key. Sets the environment variable `ANSIBLE_HOST_KEY_CHECKING`, similar to the recommendations for running with Vagrant. | -| Unbuffered stdout | Toggle buffering of standard out. Sets the environment variable `PYTHONUNBUFFERED`, similar to the recommendations for running with Vagrant. | -| Colorized stdout | Toggle color codes in console text. See Colorized Output section for example usage. Sets the environment variable `ANSIBLE_FORCE_COLOR`, similar to the recommendations for running with Vagrant. | -| Extra Variables | CLI arg: `-e` | -| Additional parameters | String passed to the Ansible Command Line invocation as-is. | +| Freestyle Name | Pipeline Name | Description | +| -------------------------------------- | ------------------ | ------------------------------------------------------------- | +| Ansible installation | installation | Ansible installation to use for the playbook invocation | +| Host pattern | hosts | The host pattern to manage. See Ansible Patterns for details. | +| Module | module | CLI arg: `-m` | +| Module arguments or command to execute | moduleArguments | CLI arg: `-a` | +| Inventory file or host list | inventory | CLI arg: `-i`: See the Inventory section for additional details. | +| Inventory inline content | inventoryContent | CLI arg: `-i`: See the Inventory section for additional details. | +| Credentials | credentialsId | The Jenkins credential to use for the SSH connection. See the Authentication section for additional details. | +| Vault Credentials | vaultCredentialsId | CLI arg: `--vault-password-file`: The Jenkins credential to use as the vault credential. See the Vault Credentials section for additional details. | +| sudo | become | CLI arg: `-s` | +| sudo user | becomeUser | CLI arg: `-U` | +| Number of parallel processes | forks | CLI arg: `-f` | +| Check host SSH key | hostKeyChecking | Toggle checking of the host key. Sets the environment variable `ANSIBLE_HOST_KEY_CHECKING`, similar to the recommendations for running with Vagrant. | +| Unbuffered stdout | | Toggle buffering of standard out. Sets the environment variable `PYTHONUNBUFFERED`, similar to the recommendations for running with Vagrant. | +| Colorized stdout | colorized | Toggle color codes in console text. See Colorized Output section for example usage. Sets the environment variable `ANSIBLE_FORCE_COLOR`, similar to the recommendations for running with Vagrant. | +| Extra Variables | extraVars | CLI arg: `-e` | +| Additional parameters | extras | String passed to the Ansible Command Line invocation as-is. | ## Playbook diff --git a/src/main/java/org/jenkinsci/plugins/ansible/workflow/AnsibleAdhocStep.java b/src/main/java/org/jenkinsci/plugins/ansible/workflow/AnsibleAdhocStep.java new file mode 100644 index 0000000..6eefcf8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ansible/workflow/AnsibleAdhocStep.java @@ -0,0 +1,318 @@ +/* + * Copyright 2015-2016 Jean-Christophe Sirot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.ansible.workflow; + +import static com.cloudbees.plugins.credentials.CredentialsMatchers.anyOf; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; + +import java.util.List; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.google.inject.Inject; +import hudson.*; +import hudson.model.Computer; +import hudson.model.Project; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.ListBoxModel; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.ansible.AnsibleAdHocCommandBuilder; +import org.jenkinsci.plugins.ansible.AnsibleInstallation; +import org.jenkinsci.plugins.ansible.ExtraVar; +import org.jenkinsci.plugins.ansible.Inventory; +import org.jenkinsci.plugins.ansible.InventoryPath; +import org.jenkinsci.plugins.ansible.InventoryContent; +import org.jenkinsci.plugins.ansible.InventoryDoNotSpecify; +import org.jenkinsci.plugins.plaincredentials.FileCredentials; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +/** + * The Ansible adhoc invocation step for the Jenkins workflow plugin. + */ +public class AnsibleAdhocStep extends AbstractStepImpl { + + private String hosts; + private String module; + private String moduleArguments; + private String inventory; + private String inventoryContent; + private boolean dynamicInventory = false; + private String installation; + private String credentialsId; + private String vaultCredentialsId; + private boolean become = false; + private String becomeUser = "root"; + private List extraVars = null; + private String extras = null; + private boolean colorized = false; + private int forks = 0; + private boolean hostKeyChecking = false; + + @DataBoundConstructor + public AnsibleAdhocStep(String hosts) { + this.hosts = hosts; + } + + @DataBoundSetter + public void setModule(String module) { + this.module = Util.fixEmptyAndTrim(module); + } + + @DataBoundSetter + public void setModuleArguments(String moduleArguments) { + this.moduleArguments = Util.fixEmptyAndTrim(moduleArguments); + } + + @DataBoundSetter + public void setInventory(String inventory) { + this.inventory = Util.fixEmptyAndTrim(inventory); + } + + @DataBoundSetter + public void setInventoryContent(String inventoryContent) { + this.inventoryContent = Util.fixEmptyAndTrim(inventoryContent); + } + + @DataBoundSetter + public void setDynamicInventory(boolean dynamicInventory) { + this.dynamicInventory = dynamicInventory; + } + + @DataBoundSetter + public void setCredentialsId(String credentialsId) { + this.credentialsId = Util.fixEmptyAndTrim(credentialsId); + } + + @DataBoundSetter + public void setVaultCredentialsId(String vaultCredentialsId) { + this.vaultCredentialsId = Util.fixEmptyAndTrim(vaultCredentialsId); + } + + @DataBoundSetter + public void setBecome(boolean become) { + this.become = become; + } + + @DataBoundSetter + public void setBecomeUser(String becomeUser) { + this.becomeUser = Util.fixEmptyAndTrim(becomeUser); + } + + @DataBoundSetter + public void setInstallation(String installation) { + this.installation = Util.fixEmptyAndTrim(installation); + } + + @DataBoundSetter + public void setExtraVars(List extraVars) { + this.extraVars = extraVars; + } + + @DataBoundSetter + public void setExtras(String extras) { + this.extras = Util.fixEmptyAndTrim(extras); + } + + @DataBoundSetter + public void setColorized(boolean colorized) { + this.colorized = colorized; + } + + @DataBoundSetter + public void setForks(int forks) { + this.forks = forks; + } + + @DataBoundSetter + public void setHostKeyChecking(boolean hostKeyChecking) { + this.hostKeyChecking = hostKeyChecking; + } + + public String getInstallation() { + return installation; + } + + public String getHosts() { + return hosts; + } + + public String getModule() { + return module; + } + + public String getModuleArguments() { + return moduleArguments; + } + + public String getInventory() { + return inventory; + } + + public String getInventoryContent() { + return inventoryContent; + } + + public boolean isDynamicInventory() { + return dynamicInventory; + } + + public String getCredentialsId() { + return credentialsId; + } + + public String getVaultCredentialsId() { + return vaultCredentialsId; + } + + public boolean isBecome() { + return become; + } + + public String getBecomeUser() { + return becomeUser; + } + + public List getExtraVars() { + return extraVars; + } + + public String getExtras() { + return extras; + } + + public boolean isHostKeyChecking() { + return hostKeyChecking; + } + + public int getForks() { + return forks; + } + + public boolean isColorized() { + return colorized; + } + + @Extension + public static final class DescriptorImpl extends AbstractStepDescriptorImpl { + + public DescriptorImpl() { + super(AnsibleAdhocExecution.class); + } + + @Override + public String getFunctionName() { + return "ansibleAdhoc"; + } + + @Override + public String getDisplayName() { + return "Invoke an ansible adhoc command"; + } + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Project project) { + return new StandardListBoxModel() + .withEmptySelection() + .withMatching(anyOf( + instanceOf(SSHUserPrivateKey.class), + instanceOf(UsernamePasswordCredentials.class)), + CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, project)); + } + + public ListBoxModel doFillVaultCredentialsIdItems(@AncestorInPath Project project) { + return new StandardListBoxModel() + .withEmptySelection() + .withMatching(anyOf( + instanceOf(FileCredentials.class), + instanceOf(StringCredentials.class)), + CredentialsProvider.lookupCredentials(StandardCredentials.class, project)); + } + + public ListBoxModel doFillInstallationItems() { + ListBoxModel model = new ListBoxModel(); + for (AnsibleInstallation tool : AnsibleInstallation.allInstallations()) { + model.add(tool.getName()); + } + return model; + } + } + + public static final class AnsibleAdhocExecution extends AbstractSynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 1; + + @Inject + private transient AnsibleAdhocStep step; + + @StepContextParameter + private transient TaskListener listener; + + @StepContextParameter + private transient Launcher launcher; + + @StepContextParameter + private transient Run run; + + @StepContextParameter + private transient FilePath ws; + + @StepContextParameter + private transient EnvVars envVars; + + @StepContextParameter + private transient Computer computer; + + @Override + protected Void run() throws Exception { + Inventory inventory = null; + if (StringUtils.isNotBlank(step.getInventory())) { + inventory = new InventoryPath(step.getInventory()); + } else if (StringUtils.isNotBlank(step.getInventoryContent())) { + inventory = new InventoryContent( + step.getInventoryContent(), + step.isDynamicInventory() + ); + } else { + inventory = new InventoryDoNotSpecify(); + } + AnsibleAdHocCommandBuilder builder = new AnsibleAdHocCommandBuilder(step.getHosts(), inventory, step.getModule(), step.getModuleArguments()); + builder.setAnsibleName(step.getInstallation()); + builder.setBecome(step.isBecome()); + builder.setBecomeUser(step.getBecomeUser()); + builder.setCredentialsId(step.getCredentialsId()); + builder.setVaultCredentialsId(step.getVaultCredentialsId()); + builder.setForks(step.getForks()); + builder.setExtraVars(step.getExtraVars()); + builder.setAdditionalParameters(step.getExtras()); + builder.setHostKeyChecking(step.isHostKeyChecking()); + builder.setColorizedOutput(step.isColorized()); + builder.perform(run, ws, launcher, listener); + return null; + } + } + +} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/config.jelly new file mode 100644 index 0000000..3676abd --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/config.jelly @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + + +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/help.html b/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/help.html new file mode 100644 index 0000000..73fc0f8 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ansible/workflow/AnsibleAdHocStep/help.html @@ -0,0 +1,3 @@ +
+ Execute an Ansible Adhoc command. Only hosts and moduleArguments parameters are mandatories. +
diff --git a/src/test/java/org/jenkinsci/plugins/ansible/PipelineTest.java b/src/test/java/org/jenkinsci/plugins/ansible/PipelineTest.java index 6a66dfa..9b44b40 100644 --- a/src/test/java/org/jenkinsci/plugins/ansible/PipelineTest.java +++ b/src/test/java/org/jenkinsci/plugins/ansible/PipelineTest.java @@ -168,4 +168,15 @@ public void testVaultCredentialsFileViaExtras() throws Exception { )); } + @Test + public void testAdhocCommand() throws Exception { + String pipeline = IOUtils.toString(PipelineTest.class.getResourceAsStream("/pipelines/adhocCommand.groovy"), StandardCharsets.UTF_8); + WorkflowJob workflowJob = jenkins.createProject(WorkflowJob.class); + workflowJob.setDefinition(new CpsFlowDefinition(pipeline, true)); + WorkflowRun run1 = workflowJob.scheduleBuild2(0).waitForStart(); + jenkins.waitForCompletion(run1); + assertThat(run1.getLog(), allOf( + containsString("ansible 127.0.0.1 -i inventory -a " + "\"" + "echo something" + "\"") + )); + } } diff --git a/src/test/resources/pipelines/adhocCommand.groovy b/src/test/resources/pipelines/adhocCommand.groovy new file mode 100644 index 0000000..2ce19cb --- /dev/null +++ b/src/test/resources/pipelines/adhocCommand.groovy @@ -0,0 +1,23 @@ +pipeline { + agent { + label('test-agent') + } + stages { + stage('Create inventory') { + steps { + writeFile(encoding: 'UTF-8', file: 'inventory', text: '''127.0.0.1 ansible_connection=local''') + } + } + stage('Ansible adhoc command') { + steps { + warnError(message: 'ansible command not found?') { + ansibleAdhoc( + inventory: 'inventory', + hosts: '127.0.0.1', + moduleArguments: 'echo something', + ) + } + } + } + } +}