jenkins2 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/bin/jenkins2 +4 -0
- data/lib/jenkins2.rb +5 -0
- data/lib/jenkins2/client.rb +105 -0
- data/lib/jenkins2/client/credential_commands.rb +134 -0
- data/lib/jenkins2/client/node_commands.rb +105 -0
- data/lib/jenkins2/client/plugin_commands.rb +53 -0
- data/lib/jenkins2/cmdparse.rb +51 -0
- data/lib/jenkins2/command_line.rb +221 -0
- data/lib/jenkins2/log.rb +29 -0
- data/lib/jenkins2/try.rb +36 -0
- data/lib/jenkins2/uri.rb +10 -0
- data/lib/jenkins2/version.rb +3 -0
- data/lib/jenkins2/wait.rb +29 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 755ca5e600a8dcf27d558a859feb3dcea22cf14c
|
4
|
+
data.tar.gz: 6bd09c8366e2e0069a6139d25ba51bb6c87ceb4d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dfce468bca6cf11efdc4cbaee51a7ae142bd0a7cb0d8e3e24827f3facbdf71085f816e162df80c035749c8ccad0b16cec736c6f789841cb55c0d5fc012af47d7
|
7
|
+
data.tar.gz: e9af7dca064ea182b7e8d3bd6ca66de161438d1a7ceb89ab231588ff72a7d630515ffee33f9bedba8fe723ac1b4aa12f2bbc490d466847aa8b705ce60370e64d
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
## [v0.0.2](https://bitbucket.org/DracoAter/jenkins2/commits/tag/v0.0.2) (2016-10-25)
|
4
|
+
### Enhancements
|
5
|
+
- Hopefully this gem is now usable
|
6
|
+
- Get Jenkins version
|
7
|
+
- Prepare for / cancel Jenkins shutdown
|
8
|
+
- Wait for all nodes to be idle
|
9
|
+
- Set node temporarily offline / online
|
10
|
+
- Connect / Disconnect node
|
11
|
+
- Wait for node to become idle, check if node is idle
|
12
|
+
- Get node definition as XML
|
13
|
+
- Update node definition from XML
|
14
|
+
- Run [parameterized] build
|
15
|
+
- List installed plugins
|
16
|
+
- Install / uninstall a plugin by short name (i.e. ssh-credentials)
|
17
|
+
- Create username with password credential ( Requires credentials plugin on Jenkins )
|
18
|
+
- Create ssh username with private key credential ( Requires ssh-credentials plugin on Jenkins )
|
19
|
+
- Create secret string credential ( Requires plain-credentials plugin on Jenkins )
|
20
|
+
- Create secret file credential ( Requires plain-credentials plugin on Jenkins )
|
21
|
+
- Get credential by id
|
22
|
+
- List credentials
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (C) 2016 by Juri Timošin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# Jenkins2
|
2
|
+
|
3
|
+
Jenkins2 gem is a command line interface and API client for Jenkins 2 CI Server. This gem has been
|
4
|
+
tested with Jenkins 2.19.1 LTS.
|
5
|
+
|
6
|
+
# Features available
|
7
|
+
## Global
|
8
|
+
- Get Jenkins version
|
9
|
+
- Prepare for shutdown
|
10
|
+
- Cancel shutdown
|
11
|
+
- Wait for all nodes to be idle
|
12
|
+
|
13
|
+
## Node
|
14
|
+
- Set node temporarily offline / online
|
15
|
+
- Connect / Disconnect node
|
16
|
+
- Wait for node to become idle
|
17
|
+
- Get node definition as XML
|
18
|
+
- Update node definition from XML
|
19
|
+
|
20
|
+
## Job
|
21
|
+
- Run [parameterized] build
|
22
|
+
|
23
|
+
## Plugin
|
24
|
+
- List installed plugins
|
25
|
+
- Install / uninstall a plugin by short name (i.e. ssh-credentials)
|
26
|
+
|
27
|
+
|
28
|
+
## Credentials
|
29
|
+
- Create username with password credential ( Requires credentials plugin on Jenkins )
|
30
|
+
- Create ssh username with private key credential ( Requires ssh-credentials plugin on Jenkins )
|
31
|
+
- Create secret string credential ( Requires plain-credentials plugin on Jenkins )
|
32
|
+
- Create secret file credential ( Requires plain-credentials plugin on Jenkins )
|
33
|
+
- Get credential by id
|
34
|
+
- List credentials
|
35
|
+
|
36
|
+
# Installation
|
37
|
+
|
38
|
+
gem install jenkins2
|
39
|
+
|
40
|
+
# Configuration
|
41
|
+
|
42
|
+
The gem does not require any configuration. However, if your Jenkins is secured you will have to
|
43
|
+
provide credentials with every CLI call.
|
44
|
+
|
45
|
+
jenkins2 -s http://jenkins.example.com -u admin -k mysecretkey offline-node -n mynode
|
46
|
+
|
47
|
+
This can be avoided by creating a json configuration file like this
|
48
|
+
|
49
|
+
{
|
50
|
+
"server": "http://jenkins.example.com",
|
51
|
+
"user": "admin",
|
52
|
+
"key": "mysecretkey"
|
53
|
+
}
|
54
|
+
|
55
|
+
By default Jenkins2 expects this file to be at ~/.jenkins2.json, but you can provide your own path
|
56
|
+
with --config-file switch. This way the above mentioned command will be much shorter.
|
57
|
+
|
58
|
+
jenkins2 -c offline-node -n mynode # => -c switch tells Jenkins2 to read configuration file
|
59
|
+
|
60
|
+
# Usage
|
61
|
+
|
62
|
+
Either run it from command line:
|
63
|
+
|
64
|
+
jenkins2 -s http://jenkins.example.com offline-node -n mynode
|
65
|
+
jenkins2 --help # => for help and list of available commands
|
66
|
+
jenkins2 --help <command> # => for help on particular command
|
67
|
+
|
68
|
+
Or use it in your ruby code:
|
69
|
+
|
70
|
+
require 'jenkins2'
|
71
|
+
jc = Jenkins2::Client.new( server: 'http://jenkins.example.com' )
|
72
|
+
jc.version
|
73
|
+
jc.offline_node( node: 'mynode' )
|
74
|
+
|
75
|
+
# License
|
76
|
+
|
77
|
+
MIT - see the accompanying [LICENSE](LICENSE) file for details.
|
78
|
+
|
79
|
+
# Changelog
|
80
|
+
|
81
|
+
To see what has changed in recent versions see the [CHANGELOG](CHANGELOG.md).
|
82
|
+
Jenkins2 gem follows the [Semantic Versioning Policy](http://guides.rubygems.org/patterns).
|
83
|
+
|
84
|
+
# Contributing
|
85
|
+
|
86
|
+
Additional commands and bugfixes are welcome! Please fork and submit a pull request on an
|
87
|
+
individual branch per change. The project follows GitHub Script
|
88
|
+
["Scripts To Rule Them All"] (https://github.com/github/scripts-to-rule-them-all) pattern.
|
89
|
+
|
90
|
+
## Bootstrap
|
91
|
+
|
92
|
+
After cloning the project, run:
|
93
|
+
|
94
|
+
script/bootstrap
|
95
|
+
|
96
|
+
to download gem and other dependencies (currently tested only on ubuntu xenial).
|
97
|
+
|
98
|
+
## Tests
|
99
|
+
|
100
|
+
The project is expected to be heavily tested :) with unit and integratin tests. To run unit tests,
|
101
|
+
you will need to have some gems installed (see jenkins2.gemspec -> development\_dependencies or
|
102
|
+
run bootstrap script). To run unit tests run
|
103
|
+
|
104
|
+
script/unit_test
|
105
|
+
|
106
|
+
Integration tests are run against a Jenkins server. Currently they require an lxd to setup it.
|
107
|
+
To run integration tests type
|
108
|
+
|
109
|
+
script/integration_test
|
110
|
+
|
111
|
+
This will start Jenkins in lxd container, run the tests and then kill the container.
|
112
|
+
|
113
|
+
## Continuous Integration
|
114
|
+
|
115
|
+
If you would like to automate test runs the progect already has [Jenkinsfile](Jenkinsfile) for
|
116
|
+
quick and easy integration with Jenkins Pipelines. If you are using another CI server, just make
|
117
|
+
sure it runs
|
118
|
+
|
119
|
+
script/cibuild
|
120
|
+
|
121
|
+
and then collects the data from the generated reports.
|
data/bin/jenkins2
ADDED
data/lib/jenkins2.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require_relative 'log'
|
5
|
+
require_relative 'uri'
|
6
|
+
require_relative 'wait'
|
7
|
+
require_relative 'client/credential_commands'
|
8
|
+
require_relative 'client/node_commands'
|
9
|
+
require_relative 'client/plugin_commands'
|
10
|
+
|
11
|
+
module Jenkins2
|
12
|
+
# The entrance point for your Jenkins remote management.
|
13
|
+
class Client
|
14
|
+
include CredentialCommands
|
15
|
+
include PluginCommands
|
16
|
+
include NodeCommands
|
17
|
+
# Creates a "connection" to Jenkins.
|
18
|
+
# Keyword parameters:
|
19
|
+
# +server+:: Jenkins Server URL.
|
20
|
+
# +user+:: Jenkins API user. Can be omitted, if no authentication required.
|
21
|
+
# +key+:: Jenkins API key. Can be omitted, if no authentication required.
|
22
|
+
def initialize( **args )
|
23
|
+
@server = args[:server]
|
24
|
+
@user = args[:user]
|
25
|
+
@key = args[:key]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns Jenkins version
|
29
|
+
def version
|
30
|
+
api_request( :get, '/', :raw )['X-Jenkins']
|
31
|
+
end
|
32
|
+
|
33
|
+
# Stops executing new builds, so that the system can be eventually shut down safely.
|
34
|
+
# Parameters are ignored
|
35
|
+
def prepare_for_shutdown( **args )
|
36
|
+
api_request( :post, '/quietDown' )
|
37
|
+
end
|
38
|
+
|
39
|
+
# Cancels the effect of +prepare-for-shutdown+ command.
|
40
|
+
# Parameters are ignored
|
41
|
+
def cancel_shutdown( **args )
|
42
|
+
api_request( :post, '/cancelQuietDown' )
|
43
|
+
end
|
44
|
+
|
45
|
+
# Waits for all the nodes to become idle or until +max_wait_minutes+ pass. Is expected to be
|
46
|
+
# called after +prepare_for_shutdown+, otherwise new builds will still be run.
|
47
|
+
# +max_wait_minutes+:: Maximum wait time in minutes. Default 60.
|
48
|
+
def wait_nodes_idle( max_wait_minutes: 60 )
|
49
|
+
Wait.wait( max_wait_minutes: max_wait_minutes ) do
|
50
|
+
api_request( :get, '/computer/api/json' )['busyExecutors'].zero?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Job Commands
|
55
|
+
|
56
|
+
# Starts a build
|
57
|
+
# +job_name+:: Name of the job to build
|
58
|
+
# +build_params+:: Build parameters as hash, where keys are names of variables.
|
59
|
+
def build( **args )
|
60
|
+
job, params = args[:job], args[:params]
|
61
|
+
if params.nil? or params.empty?
|
62
|
+
api_request( :post, "/job/#{job}/build" )
|
63
|
+
else
|
64
|
+
api_request( :post, "/job/#{job}/buildWithParameters" ) do |req|
|
65
|
+
req.form_data = params
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def api_request( method, path, reply_with=:json )
|
72
|
+
req = case method
|
73
|
+
when :get then Net::HTTP::Get
|
74
|
+
when :post then Net::HTTP::Post
|
75
|
+
end.new( URI File.join( @server, URI.escape( path ) ) )
|
76
|
+
Log.debug { "Request: #{method} #{req.uri}" }
|
77
|
+
req.basic_auth @user, @key
|
78
|
+
yield req if block_given?
|
79
|
+
req.content_type ||= 'application/x-www-form-urlencoded'
|
80
|
+
Log.debug { "Request content_type: #{req.content_type}, body: #{req.body}" }
|
81
|
+
response = Net::HTTP.start( req.uri.hostname, req.uri.port ){ |http| http.request req }
|
82
|
+
handle_response( response, reply_with )
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_response( response, reply_with )
|
86
|
+
Log.debug { "Response: #{response.code}, #{response.body}" }
|
87
|
+
case response
|
88
|
+
when Net::HTTPSuccess
|
89
|
+
case reply_with
|
90
|
+
when :json then JSON.parse response.body
|
91
|
+
when :body then response.body
|
92
|
+
when :raw then response
|
93
|
+
end
|
94
|
+
when Net::HTTPRedirection
|
95
|
+
response['location']
|
96
|
+
when Net::HTTPClientError, Net::HTTPServerError
|
97
|
+
Log.error { "Response: #{response.code}, #{response.body}" }
|
98
|
+
response.value
|
99
|
+
else
|
100
|
+
Log.error { "Response: #{response.code}, #{response.body}" }
|
101
|
+
response.value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Jenkins2
|
2
|
+
class Client
|
3
|
+
module CredentialCommands
|
4
|
+
BOUNDARY = '----Jenkins2RubyMultipartClient' + rand(1000000).to_s
|
5
|
+
|
6
|
+
# Creates credentials based on provided arguments. Calls either:
|
7
|
+
# - +create_credential_username_password+
|
8
|
+
# - +create_credential_ssh+
|
9
|
+
# - +create_credential_secret_text+
|
10
|
+
# - +create_credential_secret_file+
|
11
|
+
# See those functions' documentation for the exact parameters required
|
12
|
+
def create_credential( **args )
|
13
|
+
return create_credential_username_password( args ) if args[:password]
|
14
|
+
return create_credential_ssh( args ) if args[:private_key]
|
15
|
+
return create_credential_secret_text( args ) if args[:secret]
|
16
|
+
return create_credential_secret_file( args ) if args[:content]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates username and password credential. Accepts hash with the following parameters.
|
20
|
+
# +scope+:: Scope of the credential. GLOBAL or SYSTEM
|
21
|
+
# +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
|
22
|
+
# +description+:: Human readable text, what this credential is used for.
|
23
|
+
# +username+:: Username.
|
24
|
+
# +password+:: Password in plain text.
|
25
|
+
def create_credential_username_password( **args )
|
26
|
+
json_body = { "" => "0",
|
27
|
+
credentials: args.merge(
|
28
|
+
'$class' => 'com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl'
|
29
|
+
)
|
30
|
+
}.to_json
|
31
|
+
api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
|
32
|
+
req.body = "json=#{CGI::escape json_body}"
|
33
|
+
req.content_type = 'application/x-www-form-urlencoded'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates ssh username with private key credential. Jenkins must have ssh-credentials plugin
|
38
|
+
# installed, to use this functionality. Accepts hash with the following parameters.
|
39
|
+
# +scope+:: Scope of the credential. GLOBAL or SYSTEM
|
40
|
+
# +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
|
41
|
+
# +description+:: Human readable text, what this credential is used for.
|
42
|
+
# +username+:: Ssh username.
|
43
|
+
# +private_key+:: Ssh private key, with new lines replaced by <tt>\n</tt> sequence.
|
44
|
+
# +passphrase+:: Passphrase for the private key. Empty string, if not provided.
|
45
|
+
def create_credential_ssh( **args )
|
46
|
+
json_body = { "" => "1",
|
47
|
+
credentials: {
|
48
|
+
scope: args[:scope],
|
49
|
+
username: args[:username],
|
50
|
+
privateKeySource: {
|
51
|
+
value: "0",
|
52
|
+
privateKey: args[:private_key],
|
53
|
+
'stapler-class' => 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource'
|
54
|
+
},
|
55
|
+
passphrase: args[:passphrase],
|
56
|
+
id: args[:id],
|
57
|
+
description: args[:description],
|
58
|
+
'$class' => 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey'
|
59
|
+
}
|
60
|
+
}.to_json
|
61
|
+
api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
|
62
|
+
req.body = "json=#{CGI::escape json_body}"
|
63
|
+
req.content_type = 'application/x-www-form-urlencoded'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
# Creates a secret text credential. Jenkins must have plain-credentials plugin
|
67
|
+
# installed, to use this functionality. Accepts hash with the following parameters.
|
68
|
+
# +scope+:: Scope of the credential. GLOBAL or SYSTEM
|
69
|
+
# +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
|
70
|
+
# +description+:: Human readable text, what this credential is used for.
|
71
|
+
# +secret+:: Some secret text.
|
72
|
+
def create_credential_secret_text( **args )
|
73
|
+
json_body = { "" => "3",
|
74
|
+
credentials: args.merge(
|
75
|
+
'$class' => 'org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl'
|
76
|
+
)
|
77
|
+
}.to_json
|
78
|
+
api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
|
79
|
+
req.body = "json=#{CGI::escape json_body}"
|
80
|
+
req.content_type = 'application/x-www-form-urlencoded'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a secret file credential. Jenkins must have plain-credentials plugin
|
85
|
+
# installed, to use this functionality. Accepts hash with the following parameters.
|
86
|
+
# +scope+:: Scope of the credential. GLOBAL or SYSTEM
|
87
|
+
# +id+:: Id of the credential. Will be Generated by Jenkins, if not provided.
|
88
|
+
# +description+:: Human readable text, what this credential is used for.
|
89
|
+
# +filename+:: Name of the file.
|
90
|
+
# +content+:: File content.
|
91
|
+
def create_credential_secret_file( **args )
|
92
|
+
filename = args.delete :filename
|
93
|
+
content = args.delete :content
|
94
|
+
body = "--#{BOUNDARY}\r\n"
|
95
|
+
body << "Content-Disposition: form-data; name=\"file0\"; filename=\"#{filename}\"\r\n"
|
96
|
+
body << "Content-Type: application/octet-stream\r\n\r\n"
|
97
|
+
body << content
|
98
|
+
body << "\r\n"
|
99
|
+
body << "--#{BOUNDARY}\r\n"
|
100
|
+
body << "Content-Disposition: form-data; name=\"json\"\r\n\r\n"
|
101
|
+
body << { "" => "2",
|
102
|
+
credentials: args.merge(
|
103
|
+
'$class' => 'org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl',
|
104
|
+
'file' => 'file0'
|
105
|
+
)
|
106
|
+
}.to_json
|
107
|
+
body << "\r\n\r\n--#{BOUNDARY}--\r\n"
|
108
|
+
api_request( :post, '/credentials/store/system/domain/_/createCredentials' ) do |req|
|
109
|
+
req.add_field 'Content-Type', "multipart/form-data, boundary=#{BOUNDARY}"
|
110
|
+
req.body = body
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Deletes credential
|
115
|
+
# +id+:: Credential's id
|
116
|
+
def delete_credential( id )
|
117
|
+
api_request( :post, "/credentials/store/system/domain/_/credential/#{id}/doDelete" ) do |req|
|
118
|
+
req.content_type = 'application/x-www-form-urlencoded'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns credential as json. Raises Net::HTTPNotFound, if no such credential
|
123
|
+
# +id+:: Credential's id
|
124
|
+
def get_credential( id )
|
125
|
+
api_request( :get, "/credentials/store/system/domain/_/credential/#{id}/api/json" )
|
126
|
+
end
|
127
|
+
|
128
|
+
# Lists all credentials
|
129
|
+
def list_credentials( store: 'system', domain: '_' )
|
130
|
+
api_request( :get, "/credentials/store/#{store}/domain/#{domain}/api/json?depth=1" )['credentials']
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Jenkins2
|
4
|
+
class Client
|
5
|
+
module NodeCommands
|
6
|
+
# Connects a node.
|
7
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
8
|
+
def connect_node( node: '(master)' )
|
9
|
+
api_request( :post, "/computer/#{node}/launchSlaveAgent" )
|
10
|
+
end
|
11
|
+
|
12
|
+
# Creates a new node, by providing node definition XML.
|
13
|
+
# Keyword parameters:
|
14
|
+
# +node+:: Node name.
|
15
|
+
# +xml_config+:: New configuration in xml format.
|
16
|
+
def create_node( node: nil, xml_config: nil )
|
17
|
+
xml_config = STDIN.read if xml_config.nil?
|
18
|
+
api_request( :post, "/computer/doCreateItem", :raw ) do |req|
|
19
|
+
req.form_data = { 'name' => node, type: "hudson.slaves.DumbSlave$DescriptorImpl",
|
20
|
+
json: {}.to_json }
|
21
|
+
end
|
22
|
+
update_node( node: node, xml_config: xml_config )
|
23
|
+
end
|
24
|
+
|
25
|
+
# Deletes a node
|
26
|
+
# +node+:: Node name. Master cannot be deleted.
|
27
|
+
def delete_node( node: nil )
|
28
|
+
raise ArgumentError, 'node must be provided' if node.nil?
|
29
|
+
api_request( :post, "/computer/#{node}/doDelete" )
|
30
|
+
end
|
31
|
+
|
32
|
+
# Disconnects a node.
|
33
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
34
|
+
# +message+:: Reason why the node is being disconnected.
|
35
|
+
def disconnect_node( node: '(master)', message: nil )
|
36
|
+
api_request( :post, "/computer/#{node}/doDisconnect" ) do |req|
|
37
|
+
req.body = "offlineMessage=#{CGI::escape message}" unless message.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the node definition XML.
|
42
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
43
|
+
def get_node_xml( node: '(master)' )
|
44
|
+
api_request( :get, "/computer/#{node}/config.xml", :body )
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the node state
|
48
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
49
|
+
def get_node( node: '(master)' )
|
50
|
+
api_request( :get, "/computer/#{node}/api/json" )
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets node temporarily offline. Does nothing, if node is already offline.
|
54
|
+
# +node+:: Node name, or <tt>(master)</tt> for master.
|
55
|
+
# +message+:: Record the note about this node is being disconnected.
|
56
|
+
def offline_node( node: '(master)', message: nil )
|
57
|
+
if node_online?( node: node )
|
58
|
+
api_request( :post, "/computer/#{node}/toggleOffline" ) do |req|
|
59
|
+
req.body = "offlineMessage=#{CGI::escape message}" unless message.nil?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets node back online, if node is temporarily offline.
|
65
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
66
|
+
def online_node( node: '(master)' )
|
67
|
+
api_request( :post, "/computer/#{node}/toggleOffline" ) unless node_online?( node: node )
|
68
|
+
end
|
69
|
+
|
70
|
+
# Updates the node definition XML
|
71
|
+
# Keyword parameters:
|
72
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
73
|
+
# +xml_config+:: New configuration in xml format.
|
74
|
+
def update_node( node: '(master)', xml_config: nil )
|
75
|
+
xml_config = STDIN.read if xml_config.nil?
|
76
|
+
api_request( :post, "/computer/#{node}/config.xml", :body ) do |req|
|
77
|
+
req.body = xml_config
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Waits for node to become idle or until +max_wait_minutes+ pass.
|
82
|
+
# +node+:: Node name, <tt>(master)</tt> for master.
|
83
|
+
# +max_wait_minutes+:: Maximum wait time in minutes. Default 60.
|
84
|
+
def wait_node_idle( node: '(master)', max_wait_minutes: 60 )
|
85
|
+
Jenkins2::Wait.wait( max_wait_minutes: max_wait_minutes ){ node_idle? node: node }
|
86
|
+
end
|
87
|
+
|
88
|
+
def node_idle?( node: '(master)' )
|
89
|
+
get_node( node: node )['idle']
|
90
|
+
end
|
91
|
+
|
92
|
+
# Checks if node is online (= not temporarily offline )
|
93
|
+
# +node+:: Node name. Use <tt>(master)</tt> for master.
|
94
|
+
def node_online?( node: '(master)' )
|
95
|
+
!get_node( node: node )['temporarilyOffline']
|
96
|
+
end
|
97
|
+
|
98
|
+
# Checks if node is connected, i.e. Master connected and launched client on it.
|
99
|
+
# +node+:: Node name. Use <tt>(master)</tt> for master.
|
100
|
+
def node_connected?( node: '(master)' )
|
101
|
+
!get_node( node: node )['offline']
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Jenkins2
|
2
|
+
class Client
|
3
|
+
module PluginCommands
|
4
|
+
# Installs plugins by short name (like +thinBackup+).
|
5
|
+
# +names+:: List of short names.
|
6
|
+
def install_plugins( *names )
|
7
|
+
api_request( :post, '/pluginManager/install' ) do |req|
|
8
|
+
req.form_data = names.flatten.inject({}) do |memo,obj|
|
9
|
+
memo.merge "plugin.#{obj}.default" => 'on'
|
10
|
+
end.merge( 'dynamicLoad' => 'Install without restart' )
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Installs a plugin by uploading *.hpi or *.jpi file.
|
15
|
+
# +plugin_file+:: A *.hpi or *.jpi file itself ( not some path )
|
16
|
+
def upload_plugin( plugin_file )
|
17
|
+
api_request( :post, '/pluginManager/uploadPlugin' ) do |req|
|
18
|
+
req.body = plugin_file
|
19
|
+
req.content_type = 'multipart/form-data'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Lists installed plugins
|
24
|
+
def list_plugins
|
25
|
+
api_request( :get, '/pluginManager/api/json?depth=1' )['plugins']
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks, if all of the plugins from the passed list are installed
|
29
|
+
# +names+:: List of short names of plugins (like +thinBackup+).
|
30
|
+
def plugins_installed?( *names )
|
31
|
+
plugins = list_plugins
|
32
|
+
return false if plugins.nil?
|
33
|
+
names.flatten.all? do |name|
|
34
|
+
plugins.detect{|p| p['shortName'] == name and !p['deleted'] }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Uninstalls a plugin
|
39
|
+
# +name+:: Plugin short name
|
40
|
+
def uninstall_plugin( name )
|
41
|
+
api_request( :post, "/pluginManager/plugin/#{name}/doUninstall" ) do |req|
|
42
|
+
req.form_data = { 'Submit' => 'Yes', 'json' => '{}' }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait_plugins_installed( *names, max_wait_minutes: 2 )
|
47
|
+
Wait.wait( max_wait_minutes: max_wait_minutes ) do
|
48
|
+
plugins_installed? names
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Jenkins2
|
4
|
+
class CommandParser < OptionParser
|
5
|
+
attr_reader :command_name
|
6
|
+
|
7
|
+
def command( key, desc, &block )
|
8
|
+
sw = OptionParser::Switch::NoArgument.new( key, nil, [key], nil, nil, [desc],
|
9
|
+
Proc.new{ OptionParser.new( &block ) } ), [], [key]
|
10
|
+
commands[key.to_s] = sw[0]
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse!( argv=default_argv )
|
14
|
+
@command_name = argv.detect{|c| commands.has_key? c }
|
15
|
+
if command_name
|
16
|
+
#create temporary parser with option definitions from both: globalparse and subparse
|
17
|
+
OptionParser.new do |parser|
|
18
|
+
parser.instance_variable_set(:@stack,
|
19
|
+
commands[command_name.to_s].block.call.instance_variable_get(:@stack) + @stack)
|
20
|
+
end.parse! argv
|
21
|
+
else
|
22
|
+
super( argv )
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def commands
|
27
|
+
@commands ||= {}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
|
32
|
+
super(to, width, max, indent, &blk)
|
33
|
+
if command_name and commands.has_key?( command_name )
|
34
|
+
to << "Command:\n"
|
35
|
+
commands[command_name].summarize( {}, {}, width, max, indent ) do |l|
|
36
|
+
to << (l.index($/, -1) ? l : l + $/)
|
37
|
+
end
|
38
|
+
to << "Command options:\n"
|
39
|
+
commands[command_name].block.call.summarize( to, width, max, indent, &blk )
|
40
|
+
else
|
41
|
+
to << "Commands:\n"
|
42
|
+
commands.each do |name, command|
|
43
|
+
command.summarize( {}, {}, width, max, indent ) do |l|
|
44
|
+
to << (l.index($/, -1) ? l : l + $/)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
to
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'optparse/uri'
|
2
|
+
require_relative 'cmdparse'
|
3
|
+
require_relative 'client'
|
4
|
+
require_relative 'log'
|
5
|
+
|
6
|
+
module Jenkins2
|
7
|
+
class CommandLine
|
8
|
+
attr_accessor :global_options
|
9
|
+
attr_accessor :command_options
|
10
|
+
|
11
|
+
def initialize( args )
|
12
|
+
@global_options = OptionParser::OptionMap.new
|
13
|
+
@log_options = { verbose: 0, log: STDOUT }
|
14
|
+
@command_options = OptionParser::OptionMap.new
|
15
|
+
global = CommandParser.new 'Usage: jenkins [global-options] <command> [options]' do |opts|
|
16
|
+
opts.separator ''
|
17
|
+
opts.separator "Global options (accepted by all commands):"
|
18
|
+
opts.on '-s', '--server URL', ::URI, 'Jenkins Server Url' do |opt|
|
19
|
+
@global_options[:server] = opt
|
20
|
+
end
|
21
|
+
opts.on '-u', '--user USER', 'Jenkins API user' do |opt|
|
22
|
+
@global_options[:user] = opt
|
23
|
+
end
|
24
|
+
opts.on '-k', '--key KEY', 'Jenkins API key' do |opt|
|
25
|
+
@global_options[:key] = opt
|
26
|
+
end
|
27
|
+
opts.on '-c', '--config [PATH]', 'Use configuration file. Instead of providing '\
|
28
|
+
'server, user and key through command line, you can do that with configuration file. '\
|
29
|
+
'File format is json: { "server": "http://jenkins.example.com", "user": "admin", '\
|
30
|
+
'"key": "123456" }. Options provided in command line will overwrite ones from '\
|
31
|
+
'configuration file. Program looks for ~/.jenkins2.json if no PATH is provided.' do |opt|
|
32
|
+
@global_options[:config] = opt || ::File.join( ENV['HOME'], '.jenkins2.json' )
|
33
|
+
end
|
34
|
+
opts.on '-l', '--log FILE', 'Log file. Prints to standard out, if not provided' do |opt|
|
35
|
+
@log_options[:log] = opt
|
36
|
+
end
|
37
|
+
opts.on '-v', '--verbose', 'Print more info. Up to -vvv. Prints only errors by default.' do
|
38
|
+
@log_options[:verbose] += 1
|
39
|
+
end
|
40
|
+
opts.on '-h', '--help', 'Show help' do
|
41
|
+
@global_options[:help] = true
|
42
|
+
end
|
43
|
+
opts.on '-V', '--version', 'Show version' do
|
44
|
+
puts VERSION
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.separator ''
|
49
|
+
opts.separator 'For command specific options run: jenkins2 --help <command>'
|
50
|
+
opts.separator ''
|
51
|
+
opts.command 'version', 'Outputs the current version of Jenkins'
|
52
|
+
opts.command 'prepare-for-shutdown', 'Stop executing new builds, so that the system can '\
|
53
|
+
'be eventually shut down safely.'
|
54
|
+
opts.command 'cancel-shutdown', 'Cancel the effect of "prepare-for-shutshow" command.'
|
55
|
+
opts.command 'wait-nodes-idle', 'Wait for all nodes to become idle. Is expected to be '\
|
56
|
+
'called after "prepare_for_shutdown", otherwise new builds will still be run.' do |cmd|
|
57
|
+
cmd.on '-m', '--max-wait-minutes INT', Integer, 'Wait for INT minutes at most. '\
|
58
|
+
'Default 60' do |opt|
|
59
|
+
@command_options[:max_wait_minutes] = opt
|
60
|
+
end
|
61
|
+
end
|
62
|
+
opts.command 'offline-node', 'Stop using a node for performing builds temporarily, until '\
|
63
|
+
'the next "online-node" command.' do |cmd|
|
64
|
+
cmd.on '-n', '--node NAME', 'Name of the node or empty for master' do |opt|
|
65
|
+
@command_options[:node] = opt
|
66
|
+
end
|
67
|
+
cmd.on '-m', '--message MESSAGE', 'Record the note about why you are '\
|
68
|
+
'disconnecting this node' do |opt|
|
69
|
+
@command_options[:message] = opt
|
70
|
+
end
|
71
|
+
end
|
72
|
+
opts.command 'online-node', 'Resume using a node for performing builds, to cancel out '\
|
73
|
+
'the earlier "offline-node" command.' do |cmd|
|
74
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
75
|
+
@command_options[:node] = opt
|
76
|
+
end
|
77
|
+
end
|
78
|
+
opts.command 'connect-node', 'Connects a node, i.e. starts Jenkins slave on a node.' do |cmd|
|
79
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
80
|
+
@command_options[:node] = opt
|
81
|
+
end
|
82
|
+
end
|
83
|
+
opts.command 'create-node', 'Creates a new node from XML' do |cmd|
|
84
|
+
cmd.on '-n', '--node NAME', 'Name of the new node' do |opt|
|
85
|
+
@command_options[:node] = opt
|
86
|
+
end
|
87
|
+
cmd.on '-x', '--xml FILE', 'Path to XML configuration file' do |opt|
|
88
|
+
@command_options[:xml] = IO.read opt
|
89
|
+
end
|
90
|
+
end
|
91
|
+
opts.command 'delete-node', 'Deletes a node' do |cmd|
|
92
|
+
cmd.on '-n', '--node NAME', 'Node name' do |opt|
|
93
|
+
@command_options[:node] = opt
|
94
|
+
end
|
95
|
+
end
|
96
|
+
opts.command 'create-credential', 'Creates credential.' do |cmd|
|
97
|
+
cmd.on '-S', '--scope SCOPE', 'GLOBAL or SYSTEM scope' do |opt|
|
98
|
+
@command_options[:scope] = opt
|
99
|
+
end
|
100
|
+
cmd.on '-i', '--id ID', 'Unique Id of credential. Will be generated automactically, if '\
|
101
|
+
'not provided' do |opt|
|
102
|
+
@command_options[:id] = opt
|
103
|
+
end
|
104
|
+
cmd.on '-d', '--description DESC', 'Human readable text, what this credential is used for.' do |opt|
|
105
|
+
@command_options[:description] = opt
|
106
|
+
end
|
107
|
+
cmd.on '-n', '--username NAME', 'Username for Username-Password or SSH credential' do |opt|
|
108
|
+
@command_options[:username] = opt
|
109
|
+
end
|
110
|
+
cmd.on '-p', '--password PASS', 'Password in plain text for Username-Password credential' do |opt|
|
111
|
+
@command_options[:password] = opt
|
112
|
+
end
|
113
|
+
cmd.on '-f', '--private-key FILE', 'Path to private key file for SSH credential' do |opt|
|
114
|
+
@command_options[:private_key] = IO.read( opt ).gsub( "\n", "\\n" )
|
115
|
+
end
|
116
|
+
cmd.on '-P', '--passphrase PHRASE', 'Passphrase for the private key for SSH credential' do |opt|
|
117
|
+
@command_options[:passphrase] = opt
|
118
|
+
end
|
119
|
+
cmd.on '-e', '--secret SECRET', 'Some secret text for Secret credential' do |opt|
|
120
|
+
@command_options[:secret] = opt
|
121
|
+
end
|
122
|
+
cmd.on '-F', '--secret-file FILE', 'Path to secret file for Secret File credential' do |opt|
|
123
|
+
@command_options[:filename] = File.basename opt
|
124
|
+
@command_options[:content] = IO.read opt
|
125
|
+
end
|
126
|
+
end
|
127
|
+
opts.command 'delete-credential', 'Deletes credential.' do |cmd|
|
128
|
+
cmd.on '-i', '--id ID', 'Credential id' do |opt|
|
129
|
+
@command_options[:id] = opt
|
130
|
+
end
|
131
|
+
end
|
132
|
+
opts.command 'get-credential', 'Returns credential as json.' do |cmd|
|
133
|
+
cmd.on '-i', '--id ID', 'Credential id' do |opt|
|
134
|
+
@command_options[:id] = opt
|
135
|
+
end
|
136
|
+
end
|
137
|
+
opts.command 'list-credentials', 'Lists all credentials.'
|
138
|
+
opts.command 'disconnect-node', 'Disconnects a node.' do |cmd|
|
139
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
140
|
+
@command_options[:node] = opt
|
141
|
+
end
|
142
|
+
cmd.on '-m', '--message MESSAGE', 'Reason, why the node is being disconnected.' do |opt|
|
143
|
+
@command_options[:message] = opt
|
144
|
+
end
|
145
|
+
end
|
146
|
+
opts.command 'wait-node-idle', 'Wait for the node to become idle. Make sure you run '\
|
147
|
+
'"offline-node" first.' do |cmd|
|
148
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
149
|
+
@command_options[:node] = opt
|
150
|
+
end
|
151
|
+
cmd.on '-m', '--max-wait-minutes INT', Integer, 'Wait for INT minutes at most. '\
|
152
|
+
'Default 60' do |opt|
|
153
|
+
@command_options[:max_wait_minutes] = opt
|
154
|
+
end
|
155
|
+
end
|
156
|
+
opts.command 'get-node-xml', 'Returns the node definition XML.' do |cmd|
|
157
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
158
|
+
@command_options[:node] = opt
|
159
|
+
end
|
160
|
+
end
|
161
|
+
opts.command 'get-node', 'Returns the node state.' do |cmd|
|
162
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
163
|
+
@command_options[:node] = opt
|
164
|
+
end
|
165
|
+
end
|
166
|
+
opts.command 'update-node', 'Updates the node definition XML from stdin or file.' do |cmd|
|
167
|
+
cmd.on '-n', '--node [NAME]', 'Name of the node or empty for master' do |opt|
|
168
|
+
@command_options[:node] = opt
|
169
|
+
end
|
170
|
+
cmd.on '-x', '--xml-config FILE', 'File to read definition from. Omit this to read from stdin' do |opt|
|
171
|
+
@command_options[:xml_config] = IO.read( opt )
|
172
|
+
end
|
173
|
+
end
|
174
|
+
opts.command 'build', 'Starts a build.' do |cmd|
|
175
|
+
cmd.on '-j', '--job NAME', 'Name of the job' do |opt|
|
176
|
+
@command_options[:job] = opt
|
177
|
+
end
|
178
|
+
cmd.on '-p', '--params KEY=VALUE[,KEY=VALUE...]', Array, 'Build parameters, where keys are'\
|
179
|
+
' names of variables' do |opt|
|
180
|
+
@command_options[:params] = opt.collect{|i| i.split( '=', 2 ) }.to_h
|
181
|
+
end
|
182
|
+
end
|
183
|
+
opts.command 'install-plugin', 'Installs a plugin from url or by short name. '\
|
184
|
+
'Provide either --url or --name.' do |cmd|
|
185
|
+
cmd.on '-u', '--uri URI', ::URI, 'Uri to install plugin from.' do |opt|
|
186
|
+
@command_options[:uri] = opt
|
187
|
+
end
|
188
|
+
cmd.on '-n', '--name SHORTNAME', 'Plugin short name (like thinBackup).' do |opt|
|
189
|
+
@command_options[:name] = opt
|
190
|
+
end
|
191
|
+
end
|
192
|
+
opts.command 'list-plugins', 'Lists installed plugins'
|
193
|
+
end
|
194
|
+
begin
|
195
|
+
global.parse!( args )
|
196
|
+
@global_options[:command] = global.command_name
|
197
|
+
if @global_options[:config]
|
198
|
+
from_file = JSON.parse( IO.read( @global_options[:config] ), symbolize_names: true )
|
199
|
+
@global_options = from_file.merge( @global_options )
|
200
|
+
end
|
201
|
+
Log.init @log_options
|
202
|
+
if @global_options[:help]
|
203
|
+
Log.unknown { global.help }
|
204
|
+
exit
|
205
|
+
end
|
206
|
+
raise OptionParser::MissingArgument, :command unless @global_options[:command]
|
207
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
208
|
+
Log.fatal { e.message }
|
209
|
+
Log.fatal { global.help }
|
210
|
+
exit 1
|
211
|
+
end
|
212
|
+
Log.debug { "Options: #{@global_options}\nUnparsed args: #{ARGV}" }
|
213
|
+
end
|
214
|
+
|
215
|
+
def run
|
216
|
+
jc = Client.new( @global_options )
|
217
|
+
jc.send( @global_options[:command].gsub( '-', '_' ), @command_options )
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
data/lib/jenkins2/log.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Jenkins2
|
5
|
+
class Log
|
6
|
+
extend SingleForwardable
|
7
|
+
|
8
|
+
def self.init( log: $stdout, verbose: 0 )
|
9
|
+
@logger = Logger.new log
|
10
|
+
@logger.level = Logger::ERROR - verbose
|
11
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
12
|
+
if log == $stdout
|
13
|
+
"#{msg}\n"
|
14
|
+
else
|
15
|
+
"[#{datetime.strftime '%FT%T%:z'}] #{severity} #{msg}\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
@logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown, :level
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.logger
|
26
|
+
@logger ||= self.init
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/jenkins2/try.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
require_relative 'log'
|
4
|
+
|
5
|
+
module Jenkins2
|
6
|
+
module Try
|
7
|
+
# Tries a block several times, if raised exception is <tt>Net::HTTPFatalError</tt>,
|
8
|
+
# <tt>Errno::ECONNREFUSED</tt> or <tt>Net::ReadTimeout</tt>.
|
9
|
+
# +retries+:: Number of retries.
|
10
|
+
# +retry_delay+:: Seconds to sleep, before attempting next retry.
|
11
|
+
# +&block+:: Code to run inside <tt>retry</tt> loop.
|
12
|
+
#
|
13
|
+
# Returns the result of a block, if it eventually succeeded or throws the exception, thown by
|
14
|
+
# the block on last try.
|
15
|
+
#
|
16
|
+
# Note that this is both a method of module Try, so you can <tt>include Jenkins::Try</tt>
|
17
|
+
# into your classes so they have a #try method, as well as a module method, so you can call it
|
18
|
+
# directly as ::try().
|
19
|
+
def try( retries: 3, retry_delay: 5, &block )
|
20
|
+
yield
|
21
|
+
rescue Errno::ECONNREFUSED, Net::HTTPFatalError, Net::ReadTimeout => e
|
22
|
+
i ||= 0
|
23
|
+
unless ( i += 1 ) == retries
|
24
|
+
Log.warn { "Received error: #{e}." }
|
25
|
+
Log.warn { "Retry request in #{retry_delay} seconds." }
|
26
|
+
sleep retry_delay
|
27
|
+
retry
|
28
|
+
end
|
29
|
+
Log.error { "Received error: #{e}." }
|
30
|
+
Log.error { "Reached maximum number of retries (#{retries}). Give up." }
|
31
|
+
raise e
|
32
|
+
end
|
33
|
+
|
34
|
+
module_function :try
|
35
|
+
end
|
36
|
+
end
|
data/lib/jenkins2/uri.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
require_relative 'log'
|
4
|
+
|
5
|
+
module Jenkins2
|
6
|
+
module Wait
|
7
|
+
# Waits for a block to return +truthful+ value. Useful, for example, when you set a node tenporarily
|
8
|
+
# offline, and then wait for it to become idle.
|
9
|
+
# +max_wait_minutes+:: Maximum wait time in minutes.
|
10
|
+
# +&block+:: Run this block until it returs true, max_wait_minutes pass or block throws some
|
11
|
+
# kind of exception.
|
12
|
+
#
|
13
|
+
# Returns the result of a block, if it eventually succeeded or nil in case of timeout.
|
14
|
+
#
|
15
|
+
# Note that this is both a method of module Wait, so you can <tt>include Jenkins::Wait</tt>
|
16
|
+
# into your classes so they have a #wait method, as well as a module method, so you can call it
|
17
|
+
# directly as ::wait().
|
18
|
+
def wait( max_wait_minutes: 60, &block )
|
19
|
+
[3, 5, 7, 15, 30, [60] * (max_wait_minutes - 1)].flatten.each do |sec|
|
20
|
+
result = yield
|
21
|
+
return result if result
|
22
|
+
sleep sec
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
module_function :wait
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jenkins2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Juri Timošin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '11.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '11.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ci_reporter_minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.10'
|
83
|
+
description: Command line interface and API client for Jenkins 2. Allows manipulating
|
84
|
+
nodes, jobs, plugins, credentials. See README.md for details.
|
85
|
+
email: draco.ater@gmail.com
|
86
|
+
executables:
|
87
|
+
- jenkins2
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- CHANGELOG.md
|
92
|
+
- LICENSE
|
93
|
+
- README.md
|
94
|
+
- bin/jenkins2
|
95
|
+
- lib/jenkins2.rb
|
96
|
+
- lib/jenkins2/client.rb
|
97
|
+
- lib/jenkins2/client/credential_commands.rb
|
98
|
+
- lib/jenkins2/client/node_commands.rb
|
99
|
+
- lib/jenkins2/client/plugin_commands.rb
|
100
|
+
- lib/jenkins2/cmdparse.rb
|
101
|
+
- lib/jenkins2/command_line.rb
|
102
|
+
- lib/jenkins2/log.rb
|
103
|
+
- lib/jenkins2/try.rb
|
104
|
+
- lib/jenkins2/uri.rb
|
105
|
+
- lib/jenkins2/version.rb
|
106
|
+
- lib/jenkins2/wait.rb
|
107
|
+
homepage: https://bitbucket.org/DracoAter/jenkins2
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '2.0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.5.1
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Command line interface and API client for Jenkins 2.
|
131
|
+
test_files: []
|