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.
@@ -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
@@ -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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/jenkins2'
4
+ Jenkins2::CommandLine.new( ARGV ).run
@@ -0,0 +1,5 @@
1
+ require_relative 'jenkins2/command_line'
2
+ require_relative 'jenkins2/version'
3
+ require_relative 'jenkins2/log'
4
+ require_relative 'jenkins2/try'
5
+ require_relative 'jenkins2/wait'
@@ -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
+
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ module Jenkins2
2
+ module URI
3
+ PATH = "a-zA-Z0-9\\-\\.\\_\\~\\!\\?\\$\\&\\'\\(\\)\\*\\+\\,\\;\\=\\:\\@\\/"
4
+ def self.escape( s )
5
+ s.gsub( /[^#{PATH}]/ ) do |char|
6
+ char.unpack( 'C*' ).map{|c| ("%%%02x" % c).upcase }.join
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Jenkins2
2
+ VERSION = '0.0.2'
3
+ end
@@ -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: []