├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── cd.yml │ └── stale.yml ├── .gitignore ├── .mvn ├── extensions.xml ├── jvm.config └── maven.config ├── CHANGELOG.adoc ├── CONTRIBUTING.adoc ├── Jenkinsfile ├── LICENSE ├── NOTICE ├── PULL_REQUEST_TEMPLATE.md ├── README.adoc ├── docs └── static │ └── images │ ├── ExampleWithCredentials.png │ ├── ExampleWithCredentialsBlueOcean.png │ └── JenkinsPlusSSH.png ├── pom.xml └── src ├── main ├── groovy │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── sshsteps │ │ ├── SSHService.groovy │ │ └── util │ │ └── Common.groovy ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── sshsteps │ │ ├── steps │ │ ├── BasicSSHStep.java │ │ ├── CommandStep.java │ │ ├── GetStep.java │ │ ├── PutStep.java │ │ ├── RemoveStep.java │ │ └── ScriptStep.java │ │ └── util │ │ ├── CustomLogHandler.java │ │ ├── SSHMasterToSlaveCallable.java │ │ ├── SSHStepDescriptorImpl.java │ │ └── SSHStepExecution.java └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── sshsteps │ └── steps │ ├── CommandStep │ ├── config.jelly │ └── help.html │ ├── GetStep │ ├── config.jelly │ └── help.html │ ├── PutStep │ ├── config.jelly │ └── help.html │ ├── RemoveStep │ ├── config.jelly │ └── help.html │ └── ScriptStep │ ├── config.jelly │ └── help.html └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── sshsteps │ ├── SSHServiceTest.java │ ├── steps │ ├── BaseTest.java │ ├── CommandStepTest.java │ ├── GetStepTest.java │ ├── PutStepTest.java │ ├── RemoveStepTest.java │ └── ScriptStepTest.java │ └── util │ └── TestVirtualChannel.java └── resources └── log4j.properties /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @jenkinsci/ssh-steps-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: "maven" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale issue automation" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 9 * * *" 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | operations-per-run: 200 19 | days-before-stale: 60 20 | days-before-close: 0 21 | close-issue-message: 'Closing this issue due to 60 days of inactivity.' 22 | close-pr-message: 'Closing this PR due to 60 days of inactivity.' 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | /bin 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | *.iml 10 | .idea 11 | **/target 12 | **/checkouts 13 | *.*~ 14 | *.project 15 | *.settings 16 | *.classpath 17 | .DS_Store 18 | _site/ 19 | .sass-cache/ 20 | .jekyll-metadata 21 | _pdf 22 | /.gradle/ 23 | *.DS_Store 24 | .project 25 | *.settings 26 | *.bak 27 | public/ 28 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | git-changelist-maven-extension 6 | io.jenkins.tools.incrementals 7 | 1.8 8 | 9 | 10 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | -Djava.awt.headless=true 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Release Notes. 2 | 3 | == 2.0.1 (Unreleased) 4 | 5 | 6 | == 2.0.0 7 | 8 | * https://issues.jenkins-ci.org/browse/JENKINS-56989[JENKINS-56989] - Default `pty` to `false` for sshCommand. 9 | 10 | CAUTION: `sshCommand` with `sudo:true` param also requires `pty:true` on remote config with this upgrade. (This is only applicable for few platforms like Linux so apply this accordingly) 11 | 12 | * Upgrade pom dependencies 13 | ** Minimum Jenkins Requirement: 2.121.1 14 | ** Groovy SSH - 2.10.1 15 | ** Parent POM - 3.34. 16 | ** Upgraded few other test dependencies. 17 | 18 | == 1.2.1 19 | 20 | * https://issues.jenkins-ci.org/browse/JENKINS-55194[JENKINS-55194] - Add logLevel to support debugging and also reduce default excessive logging. 21 | ** Make `filterBy` and `filterRegex` optional. 22 | 23 | == 1.2.0 24 | 25 | * https://issues.jenkins-ci.org/browse/JENKINS-55194[JENKINS-55194] - Add logLevel to support debugging and also reduce default excessive logging. 26 | * https://issues.jenkins-ci.org/browse/JENKINS-53361[JENKINS-53361] - Copy files using a regular expression. 27 | * Code cleanup, update documentation. 28 | 29 | == 1.1.1 30 | 31 | * https://issues.jenkins-ci.org/browse/JENKINS-53556[JENKINS-53556] - Fix invalid proxy settings and validation. 32 | 33 | == 1.1.0 34 | 35 | * https://issues.jenkins-ci.org/browse/JENKINS-52225[JENKINS-52225] - Upgrade to latest JSch version. 36 | * https://issues.jenkins-ci.org/browse/JENKINS-52390[JENKINS-52390] - Throw appropriate error when the code is not in node block. 37 | * https://issues.jenkins-ci.org/browse/JENKINS-52532[JENKINS-52532] - Throws exception when used remote with gateway option. 38 | 39 | == 1.0.0 40 | 41 | * Initial release. 42 | * Supported Steps. 43 | ** sshCommand 44 | ** sshScript 45 | ** sshGet 46 | ** sshPut 47 | ** sshRemove 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Contributing 2 | :1: https://issues.jenkins-ci.org/issues/?filter=18640 3 | :2: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 4 | :3: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 5 | :4: ./CHANGELOG.adoc 6 | :5: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 7 | :6: https://help.github.com/articles/using-pull-requests 8 | 9 | Help us to make this project better by contributing. Whether it's new features, bug fixes, or simply improving documentation, your contributions are welcome. Please start with logging a {1}[JIRA] or submit a pull request. 10 | 11 | Before you contribute, please review these guidelines to help ensure a smooth process for everyone. 12 | 13 | == Issue reporting 14 | 15 | * Please browse the existing {1}[JIRAs] before logging new JIRA. 16 | * Check that the issue has not already been fixed in the `master` branch. 17 | * Create JIRA with a descriptive title and a summary. 18 | * Please be as clear and explicit as you can in your description of the problem. 19 | * Please state the version of the plugin you are using in the description. 20 | * Include any relevant code in the issue summary. 21 | 22 | == Pull requests 23 | 24 | * Read {2}[how to properly contribute to open source projects on Github]. 25 | * Fork the project. 26 | * Use a feature branch. 27 | * Write {3}[good commit messages]. 28 | * Use the same coding conventions as the rest of the project. This project is using Google https://google.github.io/styleguide/javaguide.html[StyleGuide] 29 | ** Download StyleGuides from https://github.com/google/styleguide[Github]. 30 | * Commit locally and push to your fork until you are happy with your contribution. 31 | * Make sure to add tests and verify all the tests are passing when merging upstream. 32 | * Add an entry to the link:{4}[Changelog] accordingly. 33 | * {5}[Squash related commits together]. 34 | * Open a {6}[pull request]. 35 | * The pull request will be reviewed by the community and merged by the project committers. 36 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | /* `buildPlugin` step provided by: https://github.com/jenkins-infra/pipeline-library */ 4 | 5 | buildPlugin( 6 | useContainerAgent: true, 7 | configurations: [ 8 | [platform: 'linux', jdk: 21], 9 | [platform: 'windows', jdk: 17], 10 | ]) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2019 Cerner Corporation. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | See [JENKINS-12345](https://issues.jenkins-ci.org/browse/JENKINS-12345). 4 | 5 | # Submitter checklist 6 | - [ ] Link to JIRA ticket in description, if appropriate. 7 | - [ ] Change is code complete and matches issue description. 8 | - [ ] Appropriate unit or acceptance tests or explanation to why this change has no tests. 9 | - [ ] Reviewer's manual test instructions provided in PR description. See Reviewer's first task below. 10 | 11 | # Reviewer checklist 12 | - [ ] Run the changes and verified the change matches the issue description. 13 | - [ ] Reviewed the code. 14 | - [ ] Verified that the appropriate tests have been written or valid explanation given. 15 | - [ ] If applicable, test installing this plugin on the Jenkins instance. 16 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = SSH Pipeline Steps 2 | :toc: macro 3 | :note-caption: :information_source: 4 | :tip-caption: :bulb: 5 | 6 | image::docs/static/images/JenkinsPlusSSH.png[Jenkins,300] 7 | 8 | link:https://ci.jenkins.io/job/Plugins/job/ssh-steps-plugin/job/master/[image:https://ci.jenkins.io/job/Plugins/job/ssh-steps-plugin/job/master/badge/icon[Build]] image:https://img.shields.io/badge/License-Apache%202.0-blue.svg[License] link:https://plugins.jenkins.io/ssh-steps[image:https://img.shields.io/badge/SSH%20Steps-WIKI-blue.svg[Wiki]] image:https://badges.gitter.im/jenkinsci/ssh-steps-plugin.svg[link="https://gitter.im/jenkinsci/ssh-steps-plugin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] 9 | 10 | Jenkins pipeline steps which provides SSH facilities such as command execution or file transfer for continuous delivery. It internally uses the library of https://github.com/int128/groovy-ssh[Groovy SSH]. 11 | 12 | Read more about the YAML extension of this plugin at https://engineering.cerner.com/blog/ssh-steps-for-jenkins-pipeline/[this blog post] 13 | 14 | ''' 15 | toc::[] 16 | 17 | == Configuration 18 | === Remote 19 | 20 | Most of the steps in this plugin require a common step variable called `remote`, which is Map of remote node settings such as user name, password and so on. Here is list of all possible keys that can be set. 21 | 22 | [%header,cols=3*] 23 | |=== 24 | |Key 25 | |Type 26 | |Description 27 | 28 | |name 29 | |String, *Mandatory* 30 | |Remote name, usually this is same as host name. 31 | 32 | |host 33 | |String, *Mandatory* 34 | |Hostname or IP address of the remote host. 35 | 36 | |port 37 | |int 38 | |Port. Defaults to port `22`. 39 | 40 | |user 41 | |String, *Mandatory* 42 | |User name. 43 | 44 | |allowAnyHosts 45 | |boolean 46 | |If this is `true`, knownHosts is optional. Defaults to `false`. 47 | 48 | |knownHosts 49 | |String, *Mandatory* 50 | |Known hosts file for host key checking. 51 | 52 | |password 53 | |String, *one of password, identity or identityFile is required* 54 | |Password for password authentication. 55 | 56 | |identity 57 | |String, *one of password, identity or identityFile is required* 58 | |Private key for public-key authentication. 59 | 60 | |identityFile 61 | |String, *one of password, identity or identityFile is required* 62 | |Private key file name for public-key authentication. 63 | 64 | |passphrase 65 | |String 66 | |Pass-phrase for the private key. 67 | 68 | |agent 69 | |boolean 70 | |If this is `true`, Putty Agent or ssh-agent is used on authentication. Defaults to `false`. 71 | 72 | |timeoutSec 73 | |int 74 | |Connection timeout and socket read timeout. Defaults to 0 (OS default). 75 | 76 | |retryCount 77 | |int 78 | |Retry count to establish connection. Defaults to 0 (no retry). 79 | 80 | |retryWaitSec 81 | |int 82 | |Interval time between each retries. Defaults to 0 (immediately). 83 | 84 | |keepAliveSec 85 | |int 86 | |Interval time of keep alive messages sent to the remote host. Defaults to 60 seconds. 87 | 88 | |agentForwarding 89 | |boolean 90 | |If this is `true`, the agent forwarding is requested on the command execution. Defaults to false. 91 | 92 | |fileTransfer 93 | |String 94 | |File transfer method, that is `sftp` or `scp`. Defaults to `sftp`. 95 | 96 | |encoding 97 | |String 98 | |Encoding of input and output on the command or shell execution. Defaults to `UTF-8`. 99 | 100 | |proxy 101 | |Proxy, refer below. 102 | |If this is set, the proxy server is used to reach the remote host. Defaults to no proxy. 103 | 104 | |gateway 105 | |Remote 106 | |Gateway remote host. If this is set, the port-forwarding tunnel is used to reach the remote host. Defaults to no gateway. 107 | 108 | |appendName 109 | |boolean 110 | |If this is `true`, `name` is prefixed to each line in the log output. New format: `name\|log`. 111 | 112 | |logLevel 113 | |String 114 | a|Defaults to *SEVERE* 115 | 116 | Possible values, refer to java logging https://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html[levels] 117 | 118 | * SEVERE (highest value) 119 | * WARNING 120 | * INFO 121 | * CONFIG 122 | * FINE 123 | * FINER 124 | * FINEST (lowest value) 125 | 126 | |pty 127 | |boolean 128 | |If this is true, a PTY (pseudo-terminal) is allocated on the command execution. Defaults to `false`. 129 | |=== 130 | 131 | === Proxy 132 | 133 | [%header,cols=3*] 134 | |=== 135 | |Key 136 | |Type 137 | |Description 138 | 139 | |name 140 | |String, *Mandatory* 141 | |Proxy name 142 | 143 | |host 144 | |String, *Mandatory* 145 | |Hostname or IP address of the proxy server. 146 | 147 | |port 148 | |int, *Mandatory* 149 | |Port of the proxy server. 150 | 151 | |type 152 | |String, *Mandatory* 153 | |Type of the proxy server: `SOCKS` or `HTTP`. 154 | 155 | |user 156 | |String 157 | |User name of the proxy server. 158 | 159 | |password 160 | |String 161 | |Password of the proxy server. 162 | 163 | |socksVersion 164 | |int 165 | |Protocol version when using `SOCKS`: 4 or 5. Defaults to 5. 166 | |=== 167 | 168 | == Pipeline Steps 169 | 170 | The following pipeline steps are available with the initial version of this plugin. 171 | 172 | === sshCommand 173 | 174 | This step executes given command on remote node and responds with output. 175 | 176 | ==== Input 177 | 178 | [%header,cols=3*] 179 | |=== 180 | |Key 181 | |Type 182 | |Description 183 | 184 | |remote 185 | |Remote, *Mandatory*, Refer to the Remote config for more details. 186 | |Host config to run the command on. 187 | 188 | |command 189 | |String, *Mandatory* 190 | |Shell command to run. Appending sudo is optional when `sudo` is `true`. 191 | 192 | |sudo 193 | |boolean, default: `false`. 194 | |Interactively supplies the password, not required for password less sudo commands. + 195 | + 196 | sshCommand with sudo:true param also requires pty:true on remote config with this upgrade. (This is only applicable for few platforms like Linux so apply this accordingly.) 197 | 198 | |failOnError 199 | |boolean, default: `true`. 200 | |If this is `false`, no job failure would occur though there is an error while running the command. 201 | 202 | |dryRun 203 | |boolean, default: `false` 204 | |If this is true, no actual connection or operation is performed. 205 | |=== 206 | 207 | ==== Example 208 | 209 | ```groovy 210 | node { 211 | def remote = [:] 212 | remote.name = 'test' 213 | remote.host = 'test.domain.com' 214 | remote.user = 'root' 215 | remote.password = 'password' 216 | remote.allowAnyHosts = true 217 | stage('Remote SSH') { 218 | sshCommand remote: remote, command: "ls -lrt" 219 | sshCommand remote: remote, command: "for i in {1..5}; do echo -n \"Loop \$i \"; date ; sleep 1; done" 220 | } 221 | } 222 | ``` 223 | 224 | === sshScript 225 | 226 | This step executes given script(file) on remote node and responds with output. 227 | 228 | ==== Input 229 | 230 | [%header,cols=3*] 231 | |=== 232 | |Key 233 | |Type 234 | |Description 235 | 236 | |remote 237 | |Remote, *Mandatory*, Refer to the Remote config for more details. 238 | |Host config to run the command on. 239 | 240 | |script 241 | |String, *Mandatory* 242 | |Script file name from the workspace, current this doesn't support script with arguments. For that option you would need to copy over the file to remote node and run it as a command. 243 | 244 | |failOnError 245 | |boolean, default: `true`. 246 | |If this is `false`, no job failure would occur though there is an error while running the command. 247 | 248 | |dryRun 249 | |boolean, default: `false` 250 | |If this is true, no actual connection or operation is performed. 251 | |=== 252 | 253 | ==== Example 254 | 255 | ```groovy 256 | node { 257 | def remote = [:] 258 | remote.name = 'test' 259 | remote.host = 'test.domain.com' 260 | remote.user = 'root' 261 | remote.password = 'password' 262 | remote.allowAnyHosts = true 263 | stage('Remote SSH') { 264 | writeFile file: 'abc.sh', text: 'ls -lrt' 265 | sshScript remote: remote, script: "abc.sh" 266 | } 267 | } 268 | ``` 269 | 270 | === sshPut 271 | 272 | Put a file or directory into the remote host. 273 | 274 | ==== Input 275 | 276 | [%header,cols=3*] 277 | |=== 278 | |Key 279 | |Type 280 | |Description 281 | 282 | |remote 283 | |Remote, *Mandatory*, Refer to the Remote config for more details. 284 | |Host config to run the command on. 285 | 286 | |from 287 | |String, *Mandatory* 288 | |file or directory path from the workspace. 289 | 290 | |into 291 | |String, *Mandatory* 292 | |file or directory path on the remote node. 293 | 294 | |filterBy 295 | |String, *Optional*, Defaults to `name`. 296 | |Put files by a file filter. Possible values are params on the java File object. 297 | 298 | |filterRegex 299 | |String, *Optional*. 300 | |Put files by a file regex (Groovy syntax). Example: /\.xml$/ - Puts all xml files. 301 | 302 | |failOnError 303 | |boolean, default: `true`. 304 | |If this is `false`, no job failure would occur though there is an error while running the command. 305 | 306 | |dryRun 307 | |boolean, default: `false` 308 | |If this is true, no actual connection or operation is performed. 309 | |=== 310 | 311 | ==== Example 312 | 313 | ```groovy 314 | node { 315 | def remote = [:] 316 | remote.name = 'test' 317 | remote.host = 'test.domain.com' 318 | remote.user = 'root' 319 | remote.password = 'password' 320 | remote.allowAnyHosts = true 321 | stage('Remote SSH') { 322 | writeFile file: 'abc.sh', text: 'ls -lrt' 323 | sshPut remote: remote, from: 'abc.sh', into: '.' 324 | } 325 | } 326 | ``` 327 | 328 | === sshGet 329 | 330 | Get a file or directory from the remote host. 331 | 332 | ==== Input 333 | 334 | [%header,cols=3*] 335 | |=== 336 | |Key 337 | |Type 338 | |Description 339 | 340 | |remote 341 | |Remote, *Mandatory*, Refer to the Remote config for more details. 342 | |Host config to run the command on. 343 | 344 | |from 345 | |String, *Mandatory* 346 | |file or directory path from the remote node. 347 | 348 | |into 349 | |String, *Mandatory* 350 | |file or directory path on current workspace. 351 | 352 | |filterBy 353 | |String, *Optional*, Defaults to `name`. 354 | |Get files by a file filter. Possible values are params on the java File object. 355 | 356 | |filterRegex 357 | |String, *Optional*. 358 | |Get files by a file regex (Groovy syntax). Example: /\.xml$/ - Gets all xml files. 359 | 360 | |failOnError 361 | |boolean, default: `true`. 362 | |If this is `false`, no job failure would occur though there is an error while running the command. 363 | 364 | |dryRun 365 | |boolean, default: `false` 366 | |If this is true, no actual connection or operation is performed. 367 | |=== 368 | 369 | ==== Example 370 | 371 | ```groovy 372 | node { 373 | def remote = [:] 374 | remote.name = 'test' 375 | remote.host = 'test.domain.com' 376 | remote.fileTransfer = 'scp' 377 | remote.user = 'root' 378 | remote.password = 'password' 379 | remote.allowAnyHosts = true 380 | stage('Remote SSH') { 381 | sshGet remote: remote, from: 'abc.sh', into: 'abc_get.sh', override: true 382 | } 383 | stage('Retrieve files with regex') { 384 | def regexPattern = ".+\\.(log|csv)\$" 385 | sshGet remote: remote, from: '/home/jenkins/', filterRegex: regexPattern, into: 'tests/', override: true 386 | } 387 | } 388 | ``` 389 | 390 | === sshRemove 391 | 392 | Remove a file or directory on the remote host. 393 | 394 | ==== Input 395 | 396 | [%header,cols=3*] 397 | |=== 398 | |Key 399 | |Type 400 | |Description 401 | 402 | |remote 403 | |Remote, *Mandatory*, Refer to the Remote config for more details. 404 | |Host config to run the command on. 405 | 406 | |path 407 | |String, *Mandatory* 408 | |file or directory path on the remote node 409 | 410 | |failOnError 411 | |boolean, default: `true`. 412 | |If this is `false`, no job failure would occur though there is an error while running the command. 413 | 414 | |dryRun 415 | |boolean, default: `false` 416 | |If this is true, no actual connection or operation is performed. 417 | |=== 418 | 419 | ==== Example 420 | 421 | ```groovy 422 | node { 423 | def remote = [:] 424 | remote.name = 'test' 425 | remote.host = 'test.domain.com' 426 | remote.user = 'root' 427 | remote.password = 'password' 428 | remote.allowAnyHosts = true 429 | stage('Remote SSH') { 430 | sshRemove remote: remote, path: "abc.sh" 431 | } 432 | } 433 | ``` 434 | == Examples 435 | 436 | === withCredentials 437 | 438 | An example how these steps can leverage `withCredentials` to read private key from Jenkins credentials store. 439 | ```groovy 440 | def remote = [:] 441 | remote.name = "node-1" 442 | remote.host = "10.000.000.153" 443 | remote.allowAnyHosts = true 444 | 445 | node { 446 | withCredentials([sshUserPrivateKey(credentialsId: 'sshUser', keyFileVariable: 'identity', passphraseVariable: '', usernameVariable: 'userName')]) { 447 | remote.user = userName 448 | remote.identityFile = identity 449 | stage("SSH Steps Rocks!") { 450 | writeFile file: 'abc.sh', text: 'ls' 451 | sshCommand remote: remote, command: 'for i in {1..5}; do echo -n \"Loop \$i \"; date ; sleep 1; done' 452 | sshPut remote: remote, from: 'abc.sh', into: '.' 453 | sshGet remote: remote, from: 'abc.sh', into: 'bac.sh', override: true 454 | sshScript remote: remote, script: 'abc.sh' 455 | sshRemove remote: remote, path: 'abc.sh' 456 | } 457 | } 458 | } 459 | ``` 460 | 461 | ==== Classic View: 462 | image::docs/static/images/ExampleWithCredentials.png[ExampleWithCredentials,900] 463 | 464 | ==== Blue Ocean View: 465 | image::docs/static/images/ExampleWithCredentialsBlueOcean.png[ExampleWithCredentialsBlueOcean,900] 466 | 467 | == link:CONTRIBUTING.adoc[Contributing Guide] 468 | 469 | == link:CHANGELOG.adoc[Changelog] 470 | 471 | == Maintainers 472 | 473 | * https://github.com/nrayapati[Naresh Rayapati] 474 | * https://github.com/ghenkes[Gabe Henkes] 475 | * https://github.com/wwftw[Wuchen Wang] 476 | 477 | == Disclaimer 478 | 479 | Please don't hesitate to log a http://issues.jenkins-ci.org/[JIRA] or github pull request if you need any help or if you can be of help with this plugin :). 480 | Refer to the link:./CONTRIBUTING.adoc[contribution guide] for more information. 481 | 482 | == License 483 | ------- 484 | Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the 485 | License. You may obtain a copy of the License at 486 | 487 | http://www.apache.org/licenses/LICENSE-2.0 488 | 489 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 490 | “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 491 | language governing permissions and limitations under the License. 492 | ------- 493 | -------------------------------------------------------------------------------- /docs/static/images/ExampleWithCredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/ssh-steps-plugin/ed2f81642035d437e1dfbdb906d4ed8e6c206c99/docs/static/images/ExampleWithCredentials.png -------------------------------------------------------------------------------- /docs/static/images/ExampleWithCredentialsBlueOcean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/ssh-steps-plugin/ed2f81642035d437e1dfbdb906d4ed8e6c206c99/docs/static/images/ExampleWithCredentialsBlueOcean.png -------------------------------------------------------------------------------- /docs/static/images/JenkinsPlusSSH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/ssh-steps-plugin/ed2f81642035d437e1dfbdb906d4ed8e6c206c99/docs/static/images/JenkinsPlusSSH.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 5.9 9 | 10 | ssh-steps 11 | ${revision}.${changelist} 12 | hpi 13 | SSH Pipeline Steps 14 | 2018 15 | https://github.com/jenkinsci/${project.artifactId}-plugin 16 | 17 | 2.0 18 | 999999-SNAPSHOT 19 | 20 | 2.479 21 | ${jenkins.baseline}.1 22 | jenkinsci/${project.artifactId}-plugin 23 | 2.10.1 24 | 1.18.36 25 | 3.27.3 26 | 27 | 28 | 29 | Apache License, Version 2.0 30 | https://www.apache.org/licenses/LICENSE-2.0.txt 31 | repo 32 | 33 | 34 | 35 | 36 | nrayapati 37 | Naresh Rayapati 38 | 39 | 40 | 41 | 42 | scm:git:https://github.com/${gitHubRepo} 43 | scm:git:https://github.com/${gitHubRepo} 44 | ${scmTag} 45 | https://github.com/${gitHubRepo} 46 | 47 | 48 | 49 | 50 | repo.jenkins-ci.org 51 | https://repo.jenkins-ci.org/public/ 52 | 53 | 54 | 55 | 56 | repo.jenkins-ci.org 57 | https://repo.jenkins-ci.org/public/ 58 | 59 | 60 | 61 | 62 | 63 | 64 | bom-${jenkins.baseline}.x 65 | io.jenkins.tools.bom 66 | import 67 | pom 68 | 4228.v0a_71308d905b_ 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.jenkins-ci.plugins.workflow 76 | workflow-step-api 77 | 78 | 79 | org.jenkins-ci.plugins 80 | jsch 81 | 82 | 83 | org.projectlombok 84 | lombok 85 | ${lombok.version} 86 | provided 87 | 88 | 89 | org.hidetake 90 | groovy-ssh 91 | ${groovy.ssh.version} 92 | 93 | 94 | * 95 | com.jcraft 96 | 97 | 98 | groovy-all 99 | org.codehaus.groovy 100 | 101 | 102 | 103 | 104 | 105 | org.mockito 106 | mockito-core 107 | test 108 | 109 | 110 | net.java.dev.jna 111 | jna-platform 112 | 5.16.0 113 | test 114 | 115 | 116 | org.assertj 117 | assertj-core 118 | ${assertj-core.version} 119 | test 120 | 121 | 122 | 123 | 124 | 125 | org.codehaus.gmavenplus 126 | gmavenplus-plugin 127 | 4.1.1 128 | 129 | 130 | 131 | addSources 132 | addTestSources 133 | generateStubs 134 | compile 135 | generateTestStubs 136 | compileTests 137 | removeStubs 138 | removeTestStubs 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | doclint-java8-disable 148 | 149 | [1.8,) 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-javadoc-plugin 156 | 157 | ${project.basedir}/src/main/java;${project.basedir}/target/generated-sources/annotations;${project.basedir}/target/generated-sources/groovy-stubs 158 | -Xdoclint:none 159 | 160 | 161 | 162 | org.jenkins-ci.tools 163 | maven-hpi-plugin 164 | 165 | 2.0.0 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/main/groovy/org/jenkinsci/plugins/sshsteps/SSHService.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings 4 | import groovy.util.logging.Slf4j 5 | import org.hidetake.groovy.ssh.Ssh 6 | import org.hidetake.groovy.ssh.connection.AllowAnyHosts 7 | import org.hidetake.groovy.ssh.core.Service 8 | import org.hidetake.groovy.ssh.core.settings.LoggingMethod 9 | import org.jenkinsci.plugins.sshsteps.util.Common 10 | import org.jenkinsci.plugins.sshsteps.util.CustomLogHandler 11 | import org.slf4j.MDC 12 | 13 | import java.util.logging.Level 14 | import java.util.logging.Logger 15 | 16 | /** 17 | * SSH Service, wrapper on top of hidetake's ssh service. 18 | * 19 | * @author Naresh Rayapati 20 | */ 21 | @Slf4j 22 | @SuppressFBWarnings 23 | class SSHService implements Serializable { 24 | 25 | private final Map remote 26 | private final boolean failOnError 27 | private final boolean dryRunFlag 28 | private final transient PrintStream logger 29 | private final transient Service ssh 30 | 31 | /** 32 | * Constructor. 33 | * 34 | * @param remote 35 | * @param failOnError 36 | * @param dryRun 37 | * @param logger 38 | */ 39 | private SSHService(Map remote, boolean failOnError, boolean dryRun, PrintStream logger) { 40 | this.remote = remote 41 | this.logger = logger 42 | this.failOnError = failOnError 43 | this.dryRunFlag = dryRun 44 | validateRemote() 45 | ssh = Ssh.newService() 46 | } 47 | 48 | static SSHService create(Map remote, boolean failOnError, boolean dryRun, PrintStream logger) { 49 | new SSHService(remote, failOnError, dryRun, logger) 50 | } 51 | 52 | /** 53 | * Register Log handler for all hidetake's classes. 54 | */ 55 | private void registerLogHandler(message) { 56 | Logger rootLogger = Logger.getLogger("org.hidetake") 57 | rootLogger.addHandler(new CustomLogHandler(logger, MDC.get("execution.id"))) 58 | if (remote.logLevel) { 59 | rootLogger.setLevel(Level.parse(remote.logLevel)) 60 | } else { 61 | logger.println(message) 62 | rootLogger.setLevel(Level.SEVERE) 63 | } 64 | } 65 | 66 | private void validateRemote() { 67 | new Common(logger).validateRemote(this.remote) 68 | } 69 | 70 | private void defineRemote(remote) { 71 | ssh.remotes { 72 | "$remote.name" { 73 | host = remote.host 74 | if (remote.port) 75 | port = remote.port 76 | user = remote.user 77 | if (remote.password) 78 | password = remote.password 79 | 80 | // Gateway. 81 | if (remote.gateway) { 82 | defineRemote(remote.gateway) 83 | gateway = ssh.remotes."$remote.gateway.name" 84 | } 85 | 86 | // Connection Settings applicable for Command, Script, FTP/SCP Operations. 87 | timeoutSec = remote.timeoutSec 88 | retryCount = remote.retryCount 89 | agent = remote.agent 90 | dryRun = dryRunFlag 91 | retryWaitSec = remote.retryWaitSec 92 | if (remote.keepAliveSec) 93 | keepAliveSec = remote.keepAliveSec 94 | jschLog = true 95 | 96 | // Agent forwarding for command, need to find the difference between this and agent. 97 | if (remote.agentForwarding) 98 | agentForwarding = remote.agentForwarding 99 | 100 | // Ignore error don't fail the pipeline build 101 | ignoreError = !failOnError 102 | 103 | if (remote.fileTransfer) 104 | fileTransfer = remote.fileTransfer 105 | 106 | // Avoid excessive logging in Jenkins master. 107 | logging = LoggingMethod.none 108 | 109 | def logPrefix = remote.appendName ? "$remote.name|" : '' 110 | 111 | // Pipe logs to TaskListener's print stream. 112 | interaction = { 113 | when(line: _, from: standardOutput) { 114 | logger.println("$logPrefix$it") 115 | } 116 | when(line: _, from: standardError) { 117 | logger.println("$logPrefix$it") 118 | } 119 | } 120 | 121 | if (remote.pty) { 122 | pty = remote.pty 123 | } 124 | 125 | // Encoding 126 | if (remote.encoding) 127 | encoding = remote.encoding 128 | 129 | // Host authentication 130 | if (remote.allowAnyHosts) 131 | knownHosts = AllowAnyHosts.instance 132 | else if (remote.knownHosts) 133 | knownHosts = remote.knownHosts 134 | 135 | // Public and private key authentication 136 | if (remote.identity) 137 | identity = remote.identity 138 | passphrase = remote.passphrase 139 | 140 | // Proxy. 141 | if (remote.proxy) { 142 | defineProxy(remote.proxy) 143 | proxy = ssh.proxies."$remote.proxy.name" 144 | } 145 | 146 | } 147 | } 148 | } 149 | 150 | private void defineProxy(proxy) { 151 | ssh.proxies { 152 | "$proxy.name" { 153 | host = proxy.host 154 | type = proxy.type 155 | if (proxy.port) 156 | port = proxy.port 157 | if (proxy.socksVersion) 158 | socksVersion = proxy.socksVersion 159 | if (proxy.user) 160 | user = proxy.user 161 | if (proxy.password) 162 | password = proxy.password 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Executes given command with sudo (optional). 169 | * 170 | * @param command shell command. 171 | * @param sudo execute it as sudo when true. 172 | * @return response from ssh run. 173 | */ 174 | def executeCommand(String command, boolean sudo) { 175 | registerLogHandler("Executing command on $remote.name[$remote.host]: $command sudo: $sudo") 176 | defineRemote(remote) 177 | ssh.run { 178 | session(ssh.remotes."$remote.name") { 179 | if (sudo) 180 | executeSudo command 181 | else 182 | execute command 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Executes a given script. 189 | * 190 | * @param pathname file name from workspace. 191 | * @return response from ssh run. 192 | */ 193 | def executeScriptFromFile(String pathname) { 194 | registerLogHandler("Executing script on $remote.name[$remote.host]: $pathname") 195 | defineRemote(remote) 196 | ssh.run { 197 | session(ssh.remotes."$remote.name") { 198 | executeScript new File(pathname) 199 | } 200 | } 201 | } 202 | 203 | /** 204 | * Puts a file to remote node. 205 | * 206 | * @param from location to put file to. 207 | * @param into location to put file from. 208 | * @param filterBy put files by a file filter. 209 | * @param filterRegex filter regex. 210 | * @return response from ssh run. 211 | */ 212 | def put(String from, String into, String filterBy, String filterRegex) { 213 | registerLogHandler("Sending a file/directory to $remote.name[$remote.host]: from: $from into: $into") 214 | defineRemote(remote) 215 | ssh.run { 216 | session(ssh.remotes."$remote.name") { 217 | if (filterBy && filterRegex) 218 | put from: from, into: into, filter: { it."$filterBy" =~ filterRegex } 219 | else 220 | put from: from, into: into 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * Gets a file from remote node. 227 | * 228 | * @param from location to get file from. 229 | * @param into location to get file into. 230 | * @param filterBy get files by a file filter. 231 | * @param filterRegex filter regex. 232 | * @return response from ssh run. 233 | */ 234 | def get(String from, String into, String filterBy, String filterRegex) { 235 | registerLogHandler("Receiving a file/directory from $remote.name[$remote.host]: from: $from into: $into") 236 | defineRemote(remote) 237 | ssh.run { 238 | session(ssh.remotes."$remote.name") { 239 | if (filterBy && filterRegex) 240 | get from: from, into: into, filter: { it."$filterBy" =~ filterRegex } 241 | else 242 | get from: from, into: into 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * Removes a file from remote node. 249 | * 250 | * @param name name of the file/dir. 251 | * @return output from ssh's remove operation. 252 | */ 253 | def remove(String path) { 254 | registerLogHandler("Removing a file/directory on $remote.name[$remote.host]: $path") 255 | defineRemote(remote) 256 | ssh.run { 257 | session(ssh.remotes."$remote.name") { 258 | remove path 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main/groovy/org/jenkinsci/plugins/sshsteps/util/Common.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings 4 | import org.hidetake.groovy.ssh.core.ProxyType 5 | 6 | import java.util.logging.Level 7 | 8 | /** 9 | * Basic validation for remote. 10 | * 11 | * @author Naresh Rayapati 12 | * 13 | */ 14 | @SuppressFBWarnings 15 | class Common { 16 | 17 | private final PrintStream logger 18 | 19 | Common(PrintStream logger) { 20 | this.logger = logger 21 | } 22 | 23 | static String getPrefix() { 24 | "SSH Steps: " as String 25 | } 26 | 27 | /** 28 | * Validate remote. 29 | * 30 | * @param remote 31 | */ 32 | void validateRemote(remote) { 33 | assert remote, getPrefix() + "remote is null or empty" 34 | assert remote.name, getPrefix() + "a remote (or a gateway) is missing the required field 'name'" 35 | if (remote.retryCount) 36 | assert remote.retryCount >= 0, getPrefix() + "retryCount must be zero or positive ($remote.name)" 37 | if (remote.retryWaitSec) 38 | assert remote.retryWaitSec >= 0, getPrefix() + "retryWaitSec must be zero or positive ($remote.name)" 39 | if (remote.keepAliveSec) 40 | assert remote.keepAliveSec >= 0, getPrefix() + "keepAliveSec must be zero or positive ($remote.name)" 41 | validateUserAuthentication(remote) 42 | validateHostAuthentication(remote) 43 | validateProxyConnection(remote) 44 | if (remote.logLevel) { 45 | validateLogLevel(remote) 46 | } 47 | if (remote.gateway) { 48 | validateRemote(remote.gateway) 49 | } 50 | } 51 | 52 | /** 53 | * Validates user authentication. 54 | * 55 | * @param remote map of settings. 56 | */ 57 | private void validateUserAuthentication(remote) { 58 | assert remote.user, getPrefix() + "user must be given ($remote.name)" 59 | } 60 | 61 | /** 62 | * Validate host params from the given remote. 63 | * 64 | * @param remote map of settings. 65 | */ 66 | private void validateHostAuthentication(remote) { 67 | if (remote.knownHosts) { 68 | remote.knownHosts = new File("$remote.knownHosts") 69 | } 70 | if (!remote.allowAnyHosts && !remote.knownHosts) { 71 | throw new IllegalArgumentException(getPrefix() + "knownHosts must be provided when allowAnyHosts is false: $remote.name") 72 | } 73 | 74 | if (remote.identity) { 75 | remote.identity = remote.identity 76 | } else if (remote.identityFile) { 77 | remote.identity = new File("$remote.identityFile") 78 | remote.remove('identityFile') 79 | } 80 | } 81 | 82 | /** 83 | * Validate proxy arguments from remote. 84 | * 85 | * @param remote map of values. 86 | */ 87 | private void validateProxyConnection(remote) { 88 | def proxy = remote.proxy 89 | if (proxy) { 90 | assert proxy.name, getPrefix() + " proxy name must be given ($remote.name)" 91 | if (!ProxyType.values().contains(ProxyType.valueOf(proxy.type))) { 92 | throw new IllegalArgumentException(getPrefix() + "Unsupported ProxyType ${proxy.type}. Supported types: ${ProxyType.collect { "$it" }.join(', ')}.") 93 | } 94 | if (!proxy.user && proxy.password) { 95 | logger.println(getPrefix() + "proxy.password is set but proxy.user is null. Credentials are ignored for proxy '${proxy.name}'") 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Validate log level. 102 | * 103 | * @param remote map of values. 104 | */ 105 | private void validateLogLevel(remote) { 106 | try { 107 | Level.parse(remote.logLevel) 108 | } catch (IllegalArgumentException e) { 109 | throw new IllegalArgumentException(getPrefix() + "Bad log level $remote.logLevel for $remote.name") 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/BasicSSHStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.jenkinsci.plugins.workflow.steps.Step; 8 | import org.kohsuke.stapler.DataBoundSetter; 9 | 10 | /** 11 | * Base class for all SSH steps 12 | * 13 | * @author Naresh Rayapati 14 | */ 15 | public abstract class BasicSSHStep extends Step implements Serializable { 16 | 17 | @Getter 18 | @Setter 19 | @DataBoundSetter 20 | private Map remote; 21 | 22 | @Getter 23 | @DataBoundSetter 24 | private boolean failOnError = true; 25 | 26 | @Getter 27 | @DataBoundSetter 28 | private boolean dryRun = false; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/CommandStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.Util; 6 | import hudson.model.TaskListener; 7 | import java.io.IOException; 8 | import java.io.Serial; 9 | 10 | import lombok.Getter; 11 | import org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable; 12 | import org.jenkinsci.plugins.sshsteps.util.SSHStepDescriptorImpl; 13 | import org.jenkinsci.plugins.sshsteps.util.SSHStepExecution; 14 | import org.jenkinsci.plugins.workflow.steps.StepContext; 15 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 16 | import org.kohsuke.stapler.DataBoundConstructor; 17 | import org.kohsuke.stapler.DataBoundSetter; 18 | 19 | /** 20 | * Step to execute a command on remote node. 21 | * 22 | * @author Naresh Rayapati 23 | */ 24 | @Getter 25 | public class CommandStep extends BasicSSHStep { 26 | 27 | @Serial 28 | private static final long serialVersionUID = 7492916747486604582L; 29 | 30 | private final String command; 31 | 32 | @DataBoundSetter 33 | private boolean sudo = false; 34 | 35 | @DataBoundConstructor 36 | public CommandStep(String command) { 37 | this.command = command; 38 | } 39 | 40 | @Override 41 | public StepExecution start(StepContext context) throws Exception { 42 | return new Execution(this, context); 43 | } 44 | 45 | @Extension 46 | public static class DescriptorImpl extends SSHStepDescriptorImpl { 47 | 48 | @Override 49 | public String getFunctionName() { 50 | return "sshCommand"; 51 | } 52 | 53 | @NonNull 54 | @Override 55 | public String getDisplayName() { 56 | return getPrefix() + getFunctionName() + " - Execute command on remote node."; 57 | } 58 | } 59 | 60 | public static class Execution extends SSHStepExecution { 61 | 62 | @Serial 63 | private static final long serialVersionUID = -5293952534324828128L; 64 | 65 | protected Execution(CommandStep step, StepContext context) 66 | throws IOException, InterruptedException { 67 | super(step, context); 68 | } 69 | 70 | @Override 71 | protected Object run() throws Exception { 72 | CommandStep step = (CommandStep) getStep(); 73 | if (Util.fixEmpty(step.getCommand()) == null) { 74 | throw new IllegalArgumentException("command is null or empty"); 75 | } 76 | 77 | return getChannel().call(new CommandCallable(step, getListener())); 78 | } 79 | 80 | private static class CommandCallable extends SSHMasterToSlaveCallable { 81 | 82 | public CommandCallable(CommandStep step, TaskListener listener) { 83 | super(step, listener); 84 | } 85 | 86 | @Override 87 | public Object execute() { 88 | CommandStep step = (CommandStep) getStep(); 89 | return getService().executeCommand(step.getCommand(), step.isSudo()); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/GetStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.FilePath; 6 | import hudson.Util; 7 | import hudson.model.TaskListener; 8 | import java.io.IOException; 9 | import java.io.Serial; 10 | 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable; 14 | import org.jenkinsci.plugins.sshsteps.util.SSHStepDescriptorImpl; 15 | import org.jenkinsci.plugins.sshsteps.util.SSHStepExecution; 16 | import org.jenkinsci.plugins.workflow.steps.StepContext; 17 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 18 | import org.kohsuke.stapler.DataBoundConstructor; 19 | import org.kohsuke.stapler.DataBoundSetter; 20 | 21 | /** 22 | * Step to get a file from remote node to local workspace. 23 | * 24 | * @author Naresh Rayapati 25 | */ 26 | @Getter 27 | public class GetStep extends BasicSSHStep { 28 | 29 | @Serial 30 | private static final long serialVersionUID = -8831609599645560972L; 31 | 32 | private final String from; 33 | 34 | private final String into; 35 | 36 | @Setter 37 | @DataBoundSetter 38 | private String filterBy = "name"; 39 | 40 | @Setter 41 | @DataBoundSetter 42 | private String filterRegex; 43 | 44 | @Setter 45 | @DataBoundSetter 46 | private boolean override = false; 47 | 48 | @DataBoundConstructor 49 | public GetStep(String from, String into) { 50 | this.from = from; 51 | this.into = into; 52 | } 53 | 54 | @Override 55 | public StepExecution start(StepContext context) throws Exception { 56 | return new Execution(this, context); 57 | } 58 | 59 | @Extension 60 | public static class DescriptorImpl extends SSHStepDescriptorImpl { 61 | 62 | @Override 63 | public String getFunctionName() { 64 | return "sshGet"; 65 | } 66 | 67 | @NonNull 68 | @Override 69 | public String getDisplayName() { 70 | return getPrefix() + getFunctionName() + " - Get a file or directory from remote node."; 71 | } 72 | } 73 | 74 | public static class Execution extends SSHStepExecution { 75 | 76 | @Serial 77 | private static final long serialVersionUID = 8544114488028417422L; 78 | 79 | protected Execution(GetStep step, StepContext context) 80 | throws IOException, InterruptedException { 81 | super(step, context); 82 | } 83 | 84 | @Override 85 | protected Object run() throws Exception { 86 | GetStep step = (GetStep) getStep(); 87 | FilePath ws = getContext().get(FilePath.class); 88 | assert ws != null; 89 | FilePath intoPath; 90 | 91 | if (Util.fixEmpty(step.getFrom()) == null) { 92 | throw new IllegalArgumentException("from is null or empty"); 93 | } 94 | 95 | if (Util.fixEmpty(step.getInto()) == null) { 96 | throw new IllegalArgumentException("into is null or empty"); 97 | } 98 | 99 | intoPath = ws.child(step.getInto()); 100 | 101 | if (intoPath.exists() && !step.isOverride()) { 102 | throw new IllegalArgumentException( 103 | intoPath.getRemote() + " already exist. Please set override to true just in case."); 104 | } 105 | 106 | return getChannel().call(new GetCallable(step, getListener(), intoPath.getRemote())); 107 | } 108 | 109 | private static class GetCallable extends SSHMasterToSlaveCallable { 110 | 111 | private final String into; 112 | 113 | public GetCallable(GetStep step, TaskListener listener, String into) { 114 | super(step, listener); 115 | this.into = into; 116 | } 117 | 118 | @Override 119 | public Object execute() { 120 | final GetStep step = (GetStep) getStep(); 121 | return getService().get(step.getFrom(), into, step.getFilterBy(), step.getFilterRegex()); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/PutStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.FilePath; 6 | import hudson.Util; 7 | import hudson.model.TaskListener; 8 | import java.io.IOException; 9 | import java.io.Serial; 10 | 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable; 14 | import org.jenkinsci.plugins.sshsteps.util.SSHStepDescriptorImpl; 15 | import org.jenkinsci.plugins.sshsteps.util.SSHStepExecution; 16 | import org.jenkinsci.plugins.workflow.steps.StepContext; 17 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 18 | import org.kohsuke.stapler.DataBoundConstructor; 19 | import org.kohsuke.stapler.DataBoundSetter; 20 | 21 | /** 22 | * Step to place a file/directory onto a remote node. 23 | * 24 | * @author Naresh Rayapati 25 | */ 26 | @Getter 27 | public class PutStep extends BasicSSHStep { 28 | 29 | @Serial 30 | private static final long serialVersionUID = 9183111587222550149L; 31 | 32 | private final String from; 33 | 34 | private final String into; 35 | 36 | @Setter 37 | @DataBoundSetter 38 | private String filterBy = "name"; 39 | 40 | @Setter 41 | @DataBoundSetter 42 | private String filterRegex; 43 | 44 | @DataBoundConstructor 45 | public PutStep(String from, String into) { 46 | this.from = from; 47 | this.into = into; 48 | } 49 | 50 | @Override 51 | public StepExecution start(StepContext context) throws Exception { 52 | return new Execution(this, context); 53 | } 54 | 55 | @Extension 56 | public static class DescriptorImpl extends SSHStepDescriptorImpl { 57 | 58 | @Override 59 | public String getFunctionName() { 60 | return "sshPut"; 61 | } 62 | 63 | @NonNull 64 | @Override 65 | public String getDisplayName() { 66 | return getPrefix() + getFunctionName() + " - Put a file or directory on remote node."; 67 | } 68 | } 69 | 70 | public static class Execution extends SSHStepExecution { 71 | 72 | @Serial 73 | private static final long serialVersionUID = -4497192469254138827L; 74 | 75 | protected Execution(PutStep step, StepContext context) 76 | throws IOException, InterruptedException { 77 | super(step, context); 78 | } 79 | 80 | @Override 81 | protected Object run() throws Exception { 82 | PutStep step = (PutStep) getStep(); 83 | FilePath ws = getContext().get(FilePath.class); 84 | assert ws != null; 85 | FilePath fromPath; 86 | 87 | if (Util.fixEmpty(step.getFrom()) == null) { 88 | throw new IllegalArgumentException("from is null or empty"); 89 | } 90 | 91 | fromPath = ws.child(step.getFrom()); 92 | 93 | if (!fromPath.exists()) { 94 | throw new IllegalArgumentException(fromPath.getRemote() + " does not exist."); 95 | } 96 | 97 | if (Util.fixEmpty(step.getInto()) == null) { 98 | throw new IllegalArgumentException("into is null or empty"); 99 | } 100 | 101 | return getChannel().call(new PutCallable(step, getListener(), fromPath.getRemote())); 102 | } 103 | 104 | private static class PutCallable extends SSHMasterToSlaveCallable { 105 | 106 | private final String from; 107 | 108 | public PutCallable(PutStep step, TaskListener listener, String from) { 109 | super(step, listener); 110 | this.from = from; 111 | } 112 | 113 | @Override 114 | public Object execute() { 115 | final PutStep step = (PutStep) getStep(); 116 | return getService().put(from, step.getInto(), step.getFilterBy(), step.getFilterRegex()); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/RemoveStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.Util; 6 | import hudson.model.TaskListener; 7 | import java.io.IOException; 8 | import java.io.Serial; 9 | 10 | import lombok.Getter; 11 | import org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable; 12 | import org.jenkinsci.plugins.sshsteps.util.SSHStepDescriptorImpl; 13 | import org.jenkinsci.plugins.sshsteps.util.SSHStepExecution; 14 | import org.jenkinsci.plugins.workflow.steps.StepContext; 15 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 16 | import org.kohsuke.stapler.DataBoundConstructor; 17 | 18 | /** 19 | * Step to remove a file/directory on remote node. 20 | * 21 | * @author Naresh Rayapati 22 | */ 23 | @Getter 24 | public class RemoveStep extends BasicSSHStep { 25 | 26 | @Serial 27 | private static final long serialVersionUID = -177489327125117255L; 28 | 29 | private final String path; 30 | 31 | @DataBoundConstructor 32 | public RemoveStep(String path) { 33 | this.path = path; 34 | } 35 | 36 | @Override 37 | public StepExecution start(StepContext context) throws Exception { 38 | return new Execution(this, context); 39 | } 40 | 41 | @Extension 42 | public static class DescriptorImpl extends SSHStepDescriptorImpl { 43 | 44 | @Override 45 | public String getFunctionName() { 46 | return "sshRemove"; 47 | } 48 | 49 | @NonNull 50 | @Override 51 | public String getDisplayName() { 52 | return getPrefix() + getFunctionName() + " - Remove a file or directory from remote node."; 53 | } 54 | } 55 | 56 | public static class Execution extends SSHStepExecution { 57 | 58 | @Serial 59 | private static final long serialVersionUID = 862708152481251266L; 60 | 61 | protected Execution(RemoveStep step, StepContext context) 62 | throws IOException, InterruptedException { 63 | super(step, context); 64 | } 65 | 66 | @Override 67 | protected Object run() throws Exception { 68 | RemoveStep step = (RemoveStep) getStep(); 69 | if (Util.fixEmpty(step.getPath()) == null) { 70 | throw new IllegalArgumentException("path is null or empty"); 71 | } 72 | 73 | return getChannel().call(new RemoveCallable(step, getListener())); 74 | } 75 | 76 | private static class RemoveCallable extends SSHMasterToSlaveCallable { 77 | 78 | public RemoveCallable(RemoveStep step, TaskListener listener) { 79 | super(step, listener); 80 | } 81 | 82 | @Override 83 | public Object execute() { 84 | return getService().remove(((RemoveStep) getStep()).getPath()); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/steps/ScriptStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.FilePath; 6 | import hudson.Util; 7 | import hudson.model.TaskListener; 8 | import java.io.IOException; 9 | import java.io.Serial; 10 | 11 | import lombok.Getter; 12 | import org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable; 13 | import org.jenkinsci.plugins.sshsteps.util.SSHStepDescriptorImpl; 14 | import org.jenkinsci.plugins.sshsteps.util.SSHStepExecution; 15 | import org.jenkinsci.plugins.workflow.steps.StepContext; 16 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 17 | import org.kohsuke.stapler.DataBoundConstructor; 18 | 19 | /** 20 | * Step to execute a script (a file) on remote node. 21 | * 22 | * @author Naresh Rayapati 23 | */ 24 | @Getter 25 | public class ScriptStep extends BasicSSHStep { 26 | 27 | @Serial 28 | private static final long serialVersionUID = 7358533459289529723L; 29 | 30 | private final String script; 31 | 32 | @DataBoundConstructor 33 | public ScriptStep(String script) { 34 | this.script = script; 35 | } 36 | 37 | @Override 38 | public StepExecution start(StepContext context) throws Exception { 39 | return new Execution(this, context); 40 | } 41 | 42 | @Extension 43 | public static class DescriptorImpl extends SSHStepDescriptorImpl { 44 | 45 | @Override 46 | public String getFunctionName() { 47 | return "sshScript"; 48 | } 49 | 50 | @NonNull 51 | @Override 52 | public String getDisplayName() { 53 | return getPrefix() + getFunctionName() + " - Execute script(file) on remote node."; 54 | } 55 | } 56 | 57 | public static class Execution extends SSHStepExecution { 58 | 59 | @Serial 60 | private static final long serialVersionUID = 6008070200393301960L; 61 | 62 | protected Execution(ScriptStep step, StepContext context) 63 | throws IOException, InterruptedException { 64 | super(step, context); 65 | } 66 | 67 | @Override 68 | protected Object run() throws Exception { 69 | ScriptStep step = (ScriptStep) getStep(); 70 | FilePath ws = getContext().get(FilePath.class); 71 | assert ws != null; 72 | FilePath path; 73 | if (Util.fixEmpty(step.getScript()) == null) { 74 | throw new IllegalArgumentException("script is null or empty"); 75 | } 76 | 77 | path = ws.child(step.getScript()); 78 | 79 | if (!path.exists()) { 80 | throw new IllegalArgumentException(path.getRemote() + " does not exist."); 81 | } 82 | 83 | if (path.isDirectory()) { 84 | throw new IllegalArgumentException(path.getRemote() + " is a directory."); 85 | } 86 | 87 | return getChannel().call(new ScriptCallable(step, getListener(), path.getRemote())); 88 | } 89 | 90 | private static class ScriptCallable extends SSHMasterToSlaveCallable { 91 | 92 | private final String script; 93 | 94 | public ScriptCallable(ScriptStep step, TaskListener listener, String script) { 95 | super(step, listener); 96 | this.script = script; 97 | } 98 | 99 | @Override 100 | public Object execute() { 101 | return getService().executeScriptFromFile(script); 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/util/CustomLogHandler.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util; 2 | 3 | import java.io.PrintStream; 4 | import java.util.logging.Handler; 5 | import java.util.logging.LogRecord; 6 | import org.slf4j.MDC; 7 | 8 | /** 9 | * Custom log handler for hidetake's library. 10 | * 11 | * @author Naresh Rayapati 12 | */ 13 | public class CustomLogHandler extends Handler { 14 | 15 | private final PrintStream logger; 16 | private String uuid; 17 | 18 | /** 19 | * Constructor. 20 | * 21 | * @param logger PrintStream to print messages to. 22 | */ 23 | public CustomLogHandler(PrintStream logger, String uuid) { 24 | this.logger = logger; 25 | this.uuid = uuid; 26 | } 27 | 28 | @Override 29 | public void publish(LogRecord record) { 30 | // First time running publish method on this object - assign current execution id. 31 | if (this.uuid == null) { 32 | this.uuid = MDC.get("execution.id"); 33 | } 34 | if (this.uuid.equals(MDC.get("execution.id"))) { 35 | logger.println(record.getMessage()); 36 | } 37 | } 38 | 39 | @Override 40 | public void flush() { 41 | this.logger.flush(); 42 | } 43 | 44 | @Override 45 | public void close() throws SecurityException { 46 | // logger (PrintStream) is off of pipeline step, and being used after, so not closing it here. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/util/SSHMasterToSlaveCallable.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import hudson.model.TaskListener; 5 | import java.io.IOException; 6 | import java.util.UUID; 7 | import jenkins.security.MasterToSlaveCallable; 8 | import lombok.Getter; 9 | import org.apache.log4j.MDC; 10 | import org.jenkinsci.plugins.sshsteps.SSHService; 11 | import org.jenkinsci.plugins.sshsteps.steps.BasicSSHStep; 12 | 13 | /** 14 | * Base Callable for all SSH Steps. 15 | * 16 | * @author Naresh Rayapati. 17 | */ 18 | public abstract class SSHMasterToSlaveCallable extends MasterToSlaveCallable { 19 | 20 | @Getter 21 | private final BasicSSHStep step; 22 | private final TaskListener listener; 23 | @Getter 24 | private SSHService service; 25 | 26 | public SSHMasterToSlaveCallable(BasicSSHStep step, TaskListener listener) { 27 | this.step = step; 28 | this.listener = listener; 29 | } 30 | 31 | @Override 32 | public Object call() { 33 | MDC.put("execution.id", UUID.randomUUID().toString()); 34 | this.service = createService(); 35 | return execute(); 36 | } 37 | 38 | @VisibleForTesting 39 | public SSHService createService() { 40 | return SSHService 41 | .create(step.getRemote(), step.isFailOnError(), step.isDryRun(), listener.getLogger()); 42 | } 43 | 44 | protected abstract Object execute(); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/util/SSHStepDescriptorImpl.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import hudson.EnvVars; 5 | import hudson.FilePath; 6 | import hudson.Launcher; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import java.util.Set; 10 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 11 | 12 | /** 13 | * Default StepDescriptorImpl for all SSH steps. 14 | * 15 | * @author Naresh Rayapati 16 | */ 17 | public abstract class SSHStepDescriptorImpl extends StepDescriptor { 18 | 19 | protected String getPrefix() { 20 | return Common.getPrefix(); 21 | } 22 | 23 | @Override 24 | public Set> getRequiredContext() { 25 | return ImmutableSet 26 | .of(Launcher.class, FilePath.class, Run.class, TaskListener.class, EnvVars.class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/sshsteps/util/SSHStepExecution.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Launcher; 5 | import hudson.model.TaskListener; 6 | import hudson.remoting.VirtualChannel; 7 | import hudson.security.ACL; 8 | import hudson.security.ACLContext; 9 | import hudson.util.ClassLoaderSanityThreadFactory; 10 | import hudson.util.DaemonThreadFactory; 11 | import hudson.util.NamingThreadFactory; 12 | import java.io.IOException; 13 | import java.util.UUID; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.Future; 17 | import jenkins.model.Jenkins; 18 | import lombok.Getter; 19 | import org.apache.log4j.MDC; 20 | import org.jenkinsci.plugins.sshsteps.steps.BasicSSHStep; 21 | import org.jenkinsci.plugins.workflow.steps.StepContext; 22 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 23 | import org.springframework.security.core.Authentication; 24 | 25 | /** 26 | * Non-blocking step execution for ssh steps. 27 | * 28 | * @param the type of the return value (may be {@link Void}) 29 | * @author Naresh Rayapati 30 | * @see StepExecution 31 | */ 32 | public abstract class SSHStepExecution extends StepExecution { 33 | 34 | @Getter 35 | private final transient TaskListener listener; 36 | @Getter 37 | private final transient Launcher launcher; 38 | private static ExecutorService executorService; 39 | @Getter 40 | private final BasicSSHStep step; 41 | 42 | private transient volatile Future task; 43 | private transient String threadName; 44 | private transient Throwable stopCause; 45 | 46 | protected SSHStepExecution(BasicSSHStep step, @NonNull StepContext context) 47 | throws IOException, InterruptedException { 48 | super(context); 49 | listener = context.get(TaskListener.class); 50 | launcher = context.get(Launcher.class); 51 | this.step = step; 52 | } 53 | 54 | static synchronized ExecutorService getExecutorService() { 55 | if (executorService == null) { 56 | executorService = Executors.newCachedThreadPool( 57 | new NamingThreadFactory(new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()), 58 | "org.jenkinsci.plugins.ssh.util.SSHStepExecution")); 59 | } 60 | return executorService; 61 | } 62 | 63 | /** 64 | * Meat of the execution. 65 | * 66 | * When this method returns, a step execution is over. 67 | */ 68 | protected abstract T run() throws Exception; 69 | 70 | protected VirtualChannel getChannel() { 71 | final VirtualChannel channel = getLauncher().getChannel(); 72 | if (channel == null) { 73 | throw new IllegalArgumentException( 74 | "Unable to get the channel, Perhaps you forgot to surround the code with a step that provides this, such as: node, dockerNode"); 75 | } 76 | return channel; 77 | } 78 | 79 | @Override 80 | public final boolean start() { 81 | Authentication auth = Jenkins.getAuthentication2(); 82 | task = getExecutorService().submit(() -> { 83 | threadName = Thread.currentThread().getName(); 84 | try { 85 | MDC.put("execution.id", UUID.randomUUID().toString()); 86 | T ret; 87 | try (ACLContext acl = ACL.as2(auth)) { 88 | ret = run(); 89 | } 90 | getContext().onSuccess(ret); 91 | } catch (Throwable x) { 92 | if (stopCause == null) { 93 | getContext().onFailure(x); 94 | } else { 95 | stopCause.addSuppressed(x); 96 | } 97 | } finally { 98 | MDC.clear(); 99 | } 100 | }); 101 | return false; 102 | } 103 | 104 | /** 105 | * If the computation is going synchronously, try to cancel that. 106 | */ 107 | @Override 108 | public void stop(@NonNull Throwable cause) throws Exception { 109 | if (task != null) { 110 | stopCause = cause; 111 | task.cancel(true); 112 | } 113 | super.stop(cause); 114 | } 115 | 116 | @Override 117 | public void onResume() { 118 | getContext().onFailure( 119 | new Exception("Resume after a restart not supported for non-blocking synchronous steps")); 120 | } 121 | 122 | @Override 123 | public @NonNull 124 | String getStatus() { 125 | if (threadName != null) { 126 | return "running in thread: " + threadName; 127 | } else { 128 | return "not yet scheduled"; 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Jenkins pipeline steps which provides SSH facilities such as command execution or file transfer for continuous delivery. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/CommandStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This is a special step. No snippet generation available. See inline help or docs on the README 8 | for more information. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/CommandStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Takes a remote (map) of settings and command to execute it on the remote node and 4 | returns output. 5 | See docs on the README for more information.

7 |

8 |
 9 |     def remote = [:]
10 |     remote.name = 'test'
11 |     remote.host = 'test.domain.com'
12 |     remote.user = 'root'
13 |     remote.password = 'password'
14 |     remote.allowAnyHosts = true
15 |     stage('Remote SSH') {
16 |       sshCommand remote: remote, command: "ls -lrt"
17 |       sshCommand remote: remote, command: "for i in {1..5}; do echo -n \"Loop \$i \"; date ; sleep 1; done"
18 |     }
19 |   
20 |
21 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/GetStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This is a special step. No snippet generation available. See inline help or docs on the README 8 | for more information. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/GetStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Takes a remote (map) of settings, local file/directory into to get the given 4 | file/directory from remote node. 5 | See docs on the README for more information.

7 |

8 |
 9 |     def remote = [:]
10 |     remote.name = 'test'
11 |     remote.host = 'test.domain.com'
12 |     remote.user = 'root'
13 |     remote.password = 'password'
14 |     remote.allowAnyHosts = true
15 |     stage('Remote SSH') {
16 |       sshGet remote: remote, from: 'abc.sh', into: 'abc_get.sh', override: true
17 |     }
18 |   
19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/PutStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This is a special step. No snippet generation available. See inline help or docs on the README 8 | for more information. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/PutStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Takes a remote (map) of settings, local file/directory from workspace and path to 4 | put this into remote node. 5 | See docs on the README for more information.

7 |

8 |
 9 |     def remote = [:]
10 |     remote.name = 'test'
11 |     remote.host = 'test.domain.com'
12 |     remote.user = 'root'
13 |     remote.password = 'password'
14 |     remote.allowAnyHosts = true
15 |     stage('Remote SSH') {
16 |       writeFile file: 'abc.sh', text: 'ls -lrt'
17 |       sshPut remote: remote, from: 'abc.sh', into: '.'
18 |     }
19 |   
20 |
21 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/RemoveStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This is a special step. No snippet generation available. See inline help or docs on the README 8 | for more information. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/RemoveStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Takes a remote (map) of settings and path (file/directory) to remove from remote 4 | node. 5 | See docs on the README for more information.

7 |

8 |
 9 |     def remote = [:]
10 |     remote.name = 'test'
11 |     remote.host = 'test.domain.com'
12 |     remote.user = 'root'
13 |     remote.password = 'password'
14 |     remote.allowAnyHosts = true
15 |     stage('Remote SSH') {
16 |       sshRemove remote: remote, path: "abc.sh"
17 |     }
18 |   
19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/ScriptStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

This is a special step. No snippet generation available. See inline help or docs on the README 8 | for more information. 9 |

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/sshsteps/steps/ScriptStep/help.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Takes a remote (map) of settings and script a shell script file to execute it on the remote node 4 | and returns output. 5 | See docs on the README for more information.

7 |

8 |
 9 |     def remote = [:]
10 |     remote.name = 'test'
11 |     remote.host = 'test.domain.com'
12 |     remote.user = 'root'
13 |     remote.password = 'password'
14 |     remote.allowAnyHosts = true
15 |     stage('Remote SSH') {
16 |       writeFile file: 'abc.sh', text: 'ls -lrt'
17 |       sshScript remote: remote, script: "abc.sh"
18 |     }
19 |   
20 |
21 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/SSHServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import org.junit.jupiter.api.Test; 8 | 9 | /** 10 | * Test cases for SSHService. 11 | * 12 | * @author Naresh Rayapati. 13 | */ 14 | class SSHServiceTest { 15 | 16 | @Test 17 | void testWithEmptyRemoteThrowsAssertionError() { 18 | Map remote = new HashMap<>(); 19 | 20 | assertThatExceptionOfType(AssertionError.class) 21 | .isThrownBy(() -> SSHService.create(remote, false, false, null)) 22 | .withMessage("SSH Steps: remote is null or empty. Expression: remote. Values: remote = [:]") 23 | .withStackTraceContaining("AssertionError") 24 | .withNoCause(); 25 | } 26 | 27 | @Test 28 | void testRemoteWithEmptyNameThrowsAssertionError() { 29 | Map remote = new HashMap<>(); 30 | remote.put("name", ""); 31 | 32 | assertThatExceptionOfType(AssertionError.class) 33 | .isThrownBy(() -> SSHService.create(remote, false, false, null)) 34 | .withMessage( 35 | "SSH Steps: a remote (or a gateway) is missing the required field 'name'. Expression: remote.name") 36 | .withStackTraceContaining("AssertionError") 37 | .withNoCause(); 38 | } 39 | 40 | @Test 41 | void testRemoteWithEmptyUserThrowsAssertionError() { 42 | Map remote = new HashMap<>(); 43 | remote.put("name", "dummy"); 44 | 45 | assertThatExceptionOfType(AssertionError.class) 46 | .isThrownBy(() -> SSHService.create(remote, false, false, null)) 47 | .withMessage("SSH Steps: user must be given (dummy). Expression: remote.user") 48 | .withStackTraceContaining("AssertionError") 49 | .withNoCause(); 50 | } 51 | 52 | @Test 53 | void testRemoteWithOutKnownHostsAndAllowAnyHostsThrowsIllegalArgumentException() { 54 | Map remote = new HashMap<>(); 55 | remote.put("name", "dummy"); 56 | remote.put("user", "dummy"); 57 | 58 | assertThatExceptionOfType(IllegalArgumentException.class) 59 | .isThrownBy(() -> SSHService.create(remote, false, false, null)) 60 | .withMessage("SSH Steps: knownHosts must be provided when allowAnyHosts is false: dummy") 61 | .withStackTraceContaining("IllegalArgumentException") 62 | .withNoCause(); 63 | } 64 | 65 | @Test 66 | void testRemoteWithMinimumRequiredParams() { 67 | Map remote = new HashMap<>(); 68 | remote.put("name", "dummy"); 69 | remote.put("user", "dummy"); 70 | remote.put("allowAnyHosts", true); 71 | 72 | SSHService.create(remote, false, false, null); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/BaseTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import hudson.EnvVars; 4 | import hudson.Launcher; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 7 | import org.jenkinsci.plugins.sshsteps.SSHService; 8 | import org.jenkinsci.plugins.sshsteps.util.TestVirtualChannel; 9 | import org.jenkinsci.plugins.workflow.steps.StepContext; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.mockito.Mock; 13 | import org.mockito.MockedStatic; 14 | import org.mockito.Mockito; 15 | import org.mockito.MockitoAnnotations; 16 | 17 | import java.io.IOException; 18 | import java.io.PrintStream; 19 | 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.ArgumentMatchers.anyBoolean; 22 | import static org.mockito.Mockito.doNothing; 23 | import static org.mockito.Mockito.when; 24 | 25 | /** 26 | * Base Test Class. 27 | * 28 | * @author Naresh Rayapati 29 | */ 30 | class BaseTest { 31 | 32 | @Mock 33 | TaskListener taskListenerMock; 34 | @Mock 35 | Run runMock; 36 | @Mock 37 | EnvVars envVarsMock; 38 | @Mock 39 | PrintStream printStreamMock; 40 | @Mock 41 | SSHService sshServiceMock; 42 | @Mock 43 | StepContext contextMock; 44 | @Mock 45 | Launcher launcherMock; 46 | 47 | private AutoCloseable closeable; 48 | private MockedStatic sshService; 49 | 50 | @BeforeEach 51 | void setUpBase() throws IOException, InterruptedException { 52 | 53 | closeable = MockitoAnnotations.openMocks(this); 54 | 55 | when(runMock.getCauses()).thenReturn(null); 56 | when(taskListenerMock.getLogger()).thenReturn(printStreamMock); 57 | doNothing().when(printStreamMock).println(); 58 | when(launcherMock.getChannel()).thenReturn(new TestVirtualChannel()); 59 | 60 | sshService = Mockito.mockStatic(SSHService.class); 61 | sshService.when(() -> SSHService.create(any(), anyBoolean(), anyBoolean(), any())).thenReturn(sshServiceMock); 62 | 63 | when(contextMock.get(Run.class)).thenReturn(runMock); 64 | when(contextMock.get(TaskListener.class)).thenReturn(taskListenerMock); 65 | when(contextMock.get(EnvVars.class)).thenReturn(envVarsMock); 66 | when(contextMock.get(Launcher.class)).thenReturn(launcherMock); 67 | } 68 | 69 | @AfterEach 70 | void tearUpBase() throws Exception { 71 | sshService.close(); 72 | closeable.close(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/CommandStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | 9 | /** 10 | * Unit test cases for CommandStep class. 11 | * 12 | * @author Naresh Rayapati 13 | */ 14 | class CommandStepTest extends BaseTest { 15 | 16 | CommandStep.Execution stepExecution; 17 | 18 | @Test 19 | void testWithEmptyCommandThrowsIllegalArgumentException() throws Exception { 20 | final CommandStep step = new CommandStep(""); 21 | stepExecution = new CommandStep.Execution(step, contextMock); 22 | 23 | // Execute and assert Test. 24 | assertThatExceptionOfType(IllegalArgumentException.class) 25 | .isThrownBy(() -> stepExecution.run()) 26 | .withMessage("command is null or empty") 27 | .withStackTraceContaining("IllegalArgumentException") 28 | .withNoCause(); 29 | } 30 | 31 | @Test 32 | void testSuccessfulExecuteCommand() throws Exception { 33 | final CommandStep step = new CommandStep("ls -lrt"); 34 | 35 | // Since SSHService is a mock, it is not validating remote. 36 | stepExecution = new CommandStep.Execution(step, contextMock); 37 | 38 | // Execute Test. 39 | stepExecution.run(); 40 | 41 | // Assert Test 42 | verify(sshServiceMock, times(1)).executeCommand("ls -lrt", false); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/GetStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import hudson.FilePath; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.when; 15 | 16 | /** 17 | * Unit test cases for GetStep class. 18 | * 19 | * @author Naresh Rayapati 20 | */ 21 | class GetStepTest extends BaseTest { 22 | 23 | final String path = "test.sh"; 24 | final String filterBy = "name"; 25 | final String filterRegex = null; 26 | 27 | @Mock 28 | FilePath filePathMock; 29 | 30 | GetStep.Execution stepExecution; 31 | 32 | @BeforeEach 33 | void setup() throws IOException, InterruptedException { 34 | 35 | when(filePathMock.child(any())).thenReturn(filePathMock); 36 | when(filePathMock.exists()).thenReturn(true); 37 | when(filePathMock.isDirectory()).thenReturn(false); 38 | when(filePathMock.getRemote()).thenReturn(path); 39 | 40 | when(contextMock.get(FilePath.class)).thenReturn(filePathMock); 41 | 42 | } 43 | 44 | @Test 45 | void testWithEmptyFromThrowsIllegalArgumentException() throws Exception { 46 | final GetStep step = new GetStep("", path); 47 | stepExecution = new GetStep.Execution(step, contextMock); 48 | 49 | // Execute and assert Test. 50 | assertThatExceptionOfType(IllegalArgumentException.class) 51 | .isThrownBy(() -> stepExecution.run()) 52 | .withMessage("from is null or empty") 53 | .withStackTraceContaining("IllegalArgumentException") 54 | .withNoCause(); 55 | } 56 | 57 | @Test 58 | void testWithEmptyIntoThrowsIllegalArgumentException() throws Exception { 59 | final GetStep step = new GetStep(path, ""); 60 | step.setOverride(true); 61 | stepExecution = new GetStep.Execution(step, contextMock); 62 | 63 | // Execute and assert Test. 64 | assertThatExceptionOfType(IllegalArgumentException.class) 65 | .isThrownBy(() -> stepExecution.run()) 66 | .withMessage("into is null or empty") 67 | .withStackTraceContaining("IllegalArgumentException") 68 | .withNoCause(); 69 | } 70 | 71 | @Test 72 | void testSuccessfulExecuteScript() throws Exception { 73 | final GetStep step = new GetStep(path, path); 74 | step.setOverride(true); 75 | 76 | // Since SSHService is a mock, it is not validating remote. 77 | stepExecution = new GetStep.Execution(step, contextMock); 78 | 79 | // Execute Test. 80 | stepExecution.run(); 81 | 82 | // Assert Test 83 | verify(sshServiceMock, times(1)).get(path, path, filterBy, filterRegex); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/PutStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import hudson.FilePath; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.when; 15 | 16 | /** 17 | * Unit test cases for PutStep class. 18 | * 19 | * @author Naresh Rayapati 20 | */ 21 | class PutStepTest extends BaseTest { 22 | 23 | final String path = "test.sh"; 24 | final String filterBy = "name"; 25 | final String filterRegex = null; 26 | 27 | @Mock 28 | FilePath filePathMock; 29 | 30 | PutStep.Execution stepExecution; 31 | 32 | @BeforeEach 33 | void setup() throws IOException, InterruptedException { 34 | 35 | when(filePathMock.child(any())).thenReturn(filePathMock); 36 | when(filePathMock.exists()).thenReturn(true); 37 | when(filePathMock.isDirectory()).thenReturn(false); 38 | when(filePathMock.getRemote()).thenReturn(path); 39 | 40 | when(contextMock.get(FilePath.class)).thenReturn(filePathMock); 41 | 42 | } 43 | 44 | @Test 45 | void testWithEmptyFromThrowsIllegalArgumentException() throws Exception { 46 | final PutStep step = new PutStep("", path); 47 | stepExecution = new PutStep.Execution(step, contextMock); 48 | 49 | // Execute and assert Test. 50 | assertThatExceptionOfType(IllegalArgumentException.class) 51 | .isThrownBy(() -> stepExecution.run()) 52 | .withMessage("from is null or empty") 53 | .withStackTraceContaining("IllegalArgumentException") 54 | .withNoCause(); 55 | } 56 | 57 | @Test 58 | void testWithEmptyIntoThrowsIllegalArgumentException() throws Exception { 59 | final PutStep step = new PutStep(path, ""); 60 | stepExecution = new PutStep.Execution(step, contextMock); 61 | 62 | // Execute and assert Test. 63 | assertThatExceptionOfType(IllegalArgumentException.class) 64 | .isThrownBy(() -> stepExecution.run()) 65 | .withMessage("into is null or empty") 66 | .withStackTraceContaining("IllegalArgumentException") 67 | .withNoCause(); 68 | } 69 | 70 | @Test 71 | void testSuccessfulPut() throws Exception { 72 | final PutStep step = new PutStep(path, path); 73 | 74 | // Since SSHService is a mock, it is not validating remote. 75 | stepExecution = new PutStep.Execution(step, contextMock); 76 | 77 | // Execute Test. 78 | stepExecution.run(); 79 | 80 | // Assert Test 81 | verify(sshServiceMock, times(1)).put(path, path, filterBy, filterRegex); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/RemoveStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import hudson.FilePath; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.when; 15 | 16 | /** 17 | * Unit test cases for RemoveStep class. 18 | * 19 | * @author Naresh Rayapati 20 | */ 21 | class RemoveStepTest extends BaseTest { 22 | 23 | final String path = "test.sh"; 24 | 25 | @Mock 26 | FilePath filePathMock; 27 | 28 | RemoveStep.Execution stepExecution; 29 | 30 | @BeforeEach 31 | void setup() throws IOException, InterruptedException { 32 | 33 | when(filePathMock.child(any())).thenReturn(filePathMock); 34 | when(filePathMock.exists()).thenReturn(true); 35 | when(filePathMock.isDirectory()).thenReturn(false); 36 | when(filePathMock.getRemote()).thenReturn(path); 37 | 38 | when(contextMock.get(FilePath.class)).thenReturn(filePathMock); 39 | 40 | } 41 | 42 | @Test 43 | void testWithEmptyPathThrowsIllegalArgumentException() throws Exception { 44 | final RemoveStep step = new RemoveStep(""); 45 | stepExecution = new RemoveStep.Execution(step, contextMock); 46 | 47 | // Execute and assert Test. 48 | assertThatExceptionOfType(IllegalArgumentException.class) 49 | .isThrownBy(() -> stepExecution.run()) 50 | .withMessage("path is null or empty") 51 | .withStackTraceContaining("IllegalArgumentException") 52 | .withNoCause(); 53 | } 54 | 55 | @Test 56 | void testSuccessfulRemove() throws Exception { 57 | final RemoveStep step = new RemoveStep(path); 58 | 59 | // Since SSHService is a mock, it is not validating remote. 60 | stepExecution = new RemoveStep.Execution(step, contextMock); 61 | 62 | // Execute Test. 63 | stepExecution.run(); 64 | 65 | // Assert Test 66 | verify(sshServiceMock, times(1)).remove(path); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/steps/ScriptStepTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.steps; 2 | 3 | import hudson.FilePath; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.when; 15 | 16 | /** 17 | * Unit test cases for ScriptStep class. 18 | * 19 | * @author Naresh Rayapati 20 | */ 21 | class ScriptStepTest extends BaseTest { 22 | 23 | final String scriptName = "test.sh"; 24 | 25 | @Mock 26 | FilePath filePathMock; 27 | 28 | ScriptStep.Execution stepExecution; 29 | 30 | @BeforeEach 31 | void setup() throws IOException, InterruptedException { 32 | 33 | when(filePathMock.child(any())).thenReturn(filePathMock); 34 | when(filePathMock.exists()).thenReturn(true); 35 | when(filePathMock.isDirectory()).thenReturn(false); 36 | when(filePathMock.getRemote()).thenReturn(scriptName); 37 | 38 | when(contextMock.get(FilePath.class)).thenReturn(filePathMock); 39 | 40 | } 41 | 42 | @Test 43 | void testWithEmptyCommandThrowsIllegalArgumentException() throws Exception { 44 | final ScriptStep step = new ScriptStep(""); 45 | stepExecution = new ScriptStep.Execution(step, contextMock); 46 | 47 | // Execute and assert Test. 48 | assertThatExceptionOfType(IllegalArgumentException.class) 49 | .isThrownBy(() -> stepExecution.run()) 50 | .withMessage("script is null or empty") 51 | .withStackTraceContaining("IllegalArgumentException") 52 | .withNoCause(); 53 | } 54 | 55 | @Test 56 | void testSuccessfulExecuteScript() throws Exception { 57 | final ScriptStep step = new ScriptStep(scriptName); 58 | 59 | // Since SSHService is a mock, it is not validating remote. 60 | stepExecution = new ScriptStep.Execution(step, contextMock); 61 | 62 | // Execute Test. 63 | stepExecution.run(); 64 | 65 | // Assert Test 66 | verify(sshServiceMock, times(1)).executeScriptFromFile(scriptName); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/sshsteps/util/TestVirtualChannel.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.sshsteps.util; 2 | 3 | import hudson.remoting.Callable; 4 | import hudson.remoting.Future; 5 | import hudson.remoting.VirtualChannel; 6 | 7 | /** 8 | * VirtualChannel for testing. 9 | * 10 | * @author Naresh Rayapati 11 | */ 12 | public class TestVirtualChannel implements VirtualChannel { 13 | 14 | @Override 15 | public V call(Callable callable) throws T { 16 | return callable.call(); 17 | } 18 | 19 | @Override 20 | public Future callAsync(Callable callable) { 21 | return null; 22 | } 23 | 24 | @Override 25 | public void close() { 26 | 27 | } 28 | 29 | @Override 30 | public void join() { 31 | 32 | } 33 | 34 | @Override 35 | public void join(long timeout) { 36 | 37 | } 38 | 39 | @Override 40 | public T export(Class type, T instance) { 41 | return null; 42 | } 43 | 44 | @Override 45 | public void syncLocalIO() { 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | # Direct log messages to stdout 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target=System.out 6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n 8 | --------------------------------------------------------------------------------