jenkins2 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []